npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hapi-aegis

v1.5.0

Published

A Hapi.js plugin that sets security-related HTTP response headers.

Readme

hapi-aegis

npm version CI coverage license

A Hapi.js plugin that sets security-related HTTP response headers — one plugin, sensible defaults, per-middleware and per-route configuration.

Why hapi-aegis?

The Node security-header landscape is dominated by Helmet, which is Express-only. Hapi developers who want the same coverage today either wire up several single-purpose plugins or hand-roll an onPreResponse extension that sets a dozen headers and juggles Boom error responses. hapi-aegis fills that gap: one plugin, every common header, with first-class support for Hapi conventions — plugins.aegis route overrides, Boom-aware response handling, and zero runtime dependencies beyond the @hapi/hapi peer. It is inspired by Helmet, but built natively for Hapi rather than ported.

Quick Start

npm install hapi-aegis
const Hapi = require('@hapi/hapi');
const Aegis = require('hapi-aegis');

const server = Hapi.server({ host: 'localhost', port: 3000 });
await server.register(Aegis);
await server.start();

That's it — every response now carries a secure baseline set of headers. See examples/basic.js and examples/custom.js for runnable servers.

API Reference

Register with options to customise any middleware. Pass false to any middleware key to disable it entirely, or pass an options object to override its defaults.

await server.register({
    plugin: require('hapi-aegis'),
    options: {
        hsts: { maxAge: 63072000, preload: true },
        frameguard: { action: 'deny' },
        xssFilter: false
    }
});

Middleware summary

| Middleware | Header | Default | Options | |---|---|---|---| | clearSiteData | Clear-Site-Data | not set (opt-in) | directives | | contentSecurityPolicy | Content-Security-Policy | built-in directive set (see Content Security Policy) | directives, useDefaults, reportOnly | | crossOriginEmbedderPolicy | Cross-Origin-Embedder-Policy | require-corp | policy | | crossOriginOpenerPolicy | Cross-Origin-Opener-Policy | same-origin | policy | | crossOriginResourcePolicy | Cross-Origin-Resource-Policy | same-origin | policy | | dnsPrefetchControl | X-DNS-Prefetch-Control | off | allow | | expectCt | Expect-CT | max-age=0 | maxAge, enforce, reportUri (deprecated — see FAQ) | | frameguard | X-Frame-Options | SAMEORIGIN | action | | hidePoweredBy | removes X-Powered-By and Server | — | none (boolean) | | hsts | Strict-Transport-Security | max-age=15552000; includeSubDomains | maxAge, includeSubDomains, preload | | ieNoOpen | X-Download-Options | noopen | none (boolean) | | noSniff | X-Content-Type-Options | nosniff | none (boolean) | | originAgentCluster | Origin-Agent-Cluster | ?1 | none (boolean) | | permissionsPolicy | Permissions-Policy | 8 sensor/device features denied | features | | permittedCrossDomainPolicies | X-Permitted-Cross-Domain-Policies | none | permittedPolicies | | referrerPolicy | Referrer-Policy | no-referrer | policy | | reportingEndpoints | Reporting-Endpoints | not set (opt-in) | map of endpoint name → URL | | reportTo | Report-To | not set (opt-in) | group, maxAge, endpoints, includeSubdomains | | xssFilter | X-XSS-Protection | 0 | none (boolean) — see FAQ |

clearSiteData

Sets Clear-Site-Data, which instructs the browser to clear data (cookies, cache, storage, execution contexts) associated with the origin. Opt-in: when no clearSiteData option is provided, the header is not emitted.

This header is destructive and intended for specific endpoints such as logout, so configure it per route via plugins.aegis.clearSiteData rather than enabling it server-wide. Browsers only honour it on secure (HTTPS) responses.

  • directives (array, required) — non-empty list of tokens, each one of 'cache', 'cookies', 'storage', 'executionContexts', or '*' (wildcard for all types). Rendered as comma-separated, double-quoted values, e.g. "cache", "cookies".

Apply it to a logout route while leaving the rest of the site untouched:

server.route({
    method: 'POST',
    path: '/logout',
    options: {
        plugins: {
            aegis: {
                clearSiteData: { directives: ['cache', 'cookies', 'storage'] }
            }
        }
    },
    handler: (request, h) => {

        request.cookieAuth.clear();
        return h.redirect('/');
    }
});

contentSecurityPolicy

Sets Content-Security-Policy (or Content-Security-Policy-Report-Only when reportOnly is true). See the Content Security Policy section for the default directives, merging rules, and warnings.

  • directives (object) — map of camelCase directive names to a string or array of sources.
  • useDefaults (boolean, default true) — merge your directives with the built-in defaults; when false, only your directives are used.
  • reportOnly (boolean, default false) — switch the header name to Content-Security-Policy-Report-Only.

crossOriginEmbedderPolicy

Sets Cross-Origin-Embedder-Policy.

  • policy (string, default 'require-corp') — one of require-corp, credentialless, unsafe-none.

crossOriginOpenerPolicy

Sets Cross-Origin-Opener-Policy.

  • policy (string, default 'same-origin') — one of same-origin, same-origin-allow-popups, unsafe-none.

crossOriginResourcePolicy

Sets Cross-Origin-Resource-Policy.

  • policy (string, default 'same-origin') — one of same-origin, same-site, cross-origin.

dnsPrefetchControl

Sets X-DNS-Prefetch-Control.

  • allow (boolean, default false) — when true emits on, otherwise off.

expectCt

Sets Expect-CT. The underlying header is deprecated by browsers; this middleware exists for legacy compatibility and is easy to disable with expectCt: false.

  • maxAge (integer, default 0) — non-negative seconds.
  • enforce (boolean, default false) — adds the enforce directive.
  • reportUri (string, optional) — quoted in the header value.

frameguard

Sets X-Frame-Options.

  • action (string, default 'sameorigin')deny or sameorigin (case-insensitive; emitted in upper case).

hidePoweredBy

Removes X-Powered-By and Server from responses. No options — enable or disable with true / false.

hsts

Sets Strict-Transport-Security.

  • maxAge (integer, default 15552000 — 180 days) — non-negative seconds.
  • includeSubDomains (boolean, default true) — adds includeSubDomains.
  • preload (boolean, default false) — adds preload; only use if you intend to submit to the HSTS preload list.

ieNoOpen

Sets X-Download-Options: noopen. No options.

noSniff

Sets X-Content-Type-Options: nosniff. No options.

originAgentCluster

Sets Origin-Agent-Cluster: ?1. No options.

permissionsPolicy

Sets Permissions-Policy (formerly Feature-Policy) to control which browser features can be used on the page and in embedded iframes.

  • features (object, optional) — map of feature names (camelCase, converted to kebab-case in the header) to an allowlist array:
    • [] — feature fully denied.
    • ['*'] — feature allowed on all origins.
    • ['self'] — feature allowed only on same origin.
    • ['self', 'https://maps.example.com'] — feature allowed on same origin plus the listed origins.

When features is not supplied, a conservative set of high-risk sensor/device features is denied:

accelerometer=(), camera=(), geolocation=(), gyroscope=(),
magnetometer=(), microphone=(), payment=(), usb=()

Providing features fully replaces the default set — list only the features you want in the header. Unknown feature names emit a console.warn (the spec evolves) but are still included; allowlist values that are neither a keyword (self, src, *) nor an origin-like URL also warn.

permittedCrossDomainPolicies

Sets X-Permitted-Cross-Domain-Policies.

  • permittedPolicies (string, default 'none') — one of none, master-only, by-content-type, all.

referrerPolicy

Sets Referrer-Policy.

  • policy (string or string[], default 'no-referrer') — one or more of no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url. When an array is given, values are joined with , for fallback handling.

reportingEndpoints

Sets Reporting-Endpoints, the Reporting API Level 2 header that maps named reporting endpoints to URLs. It is the modern successor to reportTo and is the form newer browsers prefer. Opt-in: when no reportingEndpoints option is provided, the header is not emitted.

Pass an object mapping each endpoint name to a URL:

  • endpoint name (key) — a lowercase structured-field key: a lowercase letter or *, then letters, digits, and _ - . *. This is the name CSP's report-to directive references.
  • URL (value, required) — an absolute http: or https: URL. Values are parsed with the WHATWG URL parser and emitted in normalized form (e.g. a bare origin gains a trailing /); relative references and non-HTTP(S) schemes are rejected.

The object must contain at least one endpoint.

await server.register({
    plugin: Aegis,
    options: {
        reportingEndpoints: {
            default: 'https://example.com/reports',
            csp: 'https://example.com/csp-reports'
        }
    }
});
// Reporting-Endpoints: default="https://example.com/reports", csp="https://example.com/csp-reports"

Wiring CSP reports end-to-end

Route CSP violation reports to a named endpoint with the modern Reporting API: name the endpoint in reportingEndpoints, then point CSP's report-to directive at that name.

await server.register({
    plugin: Aegis,
    options: {
        reportingEndpoints: { default: 'https://example.com/csp-reports' },
        contentSecurityPolicy: {
            directives: { reportTo: ['default'] }
        }
    }
});
// Content-Security-Policy: …; report-to default
// Reporting-Endpoints: default="https://example.com/csp-reports"

For older browsers that still rely on the legacy Report-To header, additionally configure the reportTo middleware with a matching group name (reportTo: { group: 'default', … }) so both headers point at the same endpoint.

reportTo

Sets Report-To, the Reporting API header that registers named endpoint groups browsers can deliver reports to. Opt-in: when no reportTo option is provided, the header is not emitted.

Pass a single group object or an array of groups. Each group accepts:

  • group (string, default 'default') — endpoint group name. Referenced from CSP via directives: { reportTo: '<group>' }.
  • maxAge (integer, required) — non-negative seconds the browser should remember the group.
  • endpoints (array, required) — non-empty list of { url: string } objects.
  • includeSubdomains (boolean, default false) — when true, the group also applies to subdomains.

Pair it with CSP's report-to directive to route CSP violation reports to the same endpoint:

await server.register({
    plugin: Aegis,
    options: {
        reportTo: {
            group: 'csp-endpoint',
            maxAge: 10886400,
            endpoints: [{ url: 'https://example.com/csp-reports' }],
            includeSubdomains: true
        },
        contentSecurityPolicy: {
            directives: { reportTo: 'csp-endpoint' }
        }
    }
});

Multiple groups (e.g. one for CSP and a fallback default):

options: {
    reportTo: [
        { group: 'csp-endpoint', maxAge: 10886400, endpoints: [{ url: 'https://example.com/csp' }] },
        { group: 'default',      maxAge: 86400,    endpoints: [{ url: 'https://example.com/default' }] }
    ]
}

The Report-To header is being superseded by Reporting-Endpoints (Reporting API Level 2). Both still work in major browsers today; configure the modern header via the reportingEndpoints middleware.

xssFilter

Sets X-XSS-Protection: 0. No options. See the FAQ for the rationale.

Content Security Policy

CSP is the most involved header, so it gets its own section.

Defaults

With useDefaults: true (the default) the built-in directives are:

{
    defaultSrc: ["'self'"],
    baseUri: ["'self'"],
    fontSrc: ["'self'", 'https:', 'data:'],
    formAction: ["'self'"],
    frameAncestors: ["'self'"],
    imgSrc: ["'self'", 'data:'],
    objectSrc: ["'none'"],
    scriptSrc: ["'self'"],
    scriptSrcAttr: ["'none'"],
    styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
    upgradeInsecureRequests: []
}

Which produces:

Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests

Custom directives (merging)

When useDefaults: true, user directives are merged with defaults on a per-key replace basis. Providing scriptSrc fully replaces the default scriptSrc; it is not concatenated.

options: {
    contentSecurityPolicy: {
        directives: {
            scriptSrc: ["'self'", 'https://cdn.example.com'],
            imgSrc: ["'self'", 'data:', 'https://images.example.com']
        }
    }
}

All other default directives (defaultSrc, styleSrc, etc.) are kept as-is.

Opting out of defaults

Set useDefaults: false to emit only your directives:

contentSecurityPolicy: {
    useDefaults: false,
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'"]
    }
}

Report-only mode

contentSecurityPolicy: {
    reportOnly: true,
    directives: { reportUri: ['/csp-report'] }
}

This switches the emitted header name to Content-Security-Policy-Report-Only.

Dynamic per-request directives

Any directive value may be a function (request) => string | string[], either as the whole value or as an item inside a mixed array. The function is invoked during onPreResponse with the current Hapi request, and its return is merged into the emitted policy. This enables per-request nonces, hashes, or any CSP variation without disabling the middleware.

await server.register({
    plugin: Aegis,
    options: {
        contentSecurityPolicy: {
            directives: {
                scriptSrc: ["'self'", (request) => `'nonce-${request.app.nonce}'`]
            }
        }
    }
});

Pair it with a small onRequest extension that stashes a fresh nonce on request.app and your templates will have a matching value to attach to inline scripts:

const Crypto = require('crypto');

server.ext('onRequest', (request, h) => {

    request.app.nonce = Crypto.randomBytes(16).toString('base64');
    return h.continue;
});

Rules for the resolver:

  • A function may return string or string[]. Array returns are flattened into the surrounding directive list.
  • A function that returns null or undefined is skipped (useful for conditional values).
  • All other return types throw hapi-aegis: contentSecurityPolicy directive "<name>" function returned invalid value; expected string or string[].
  • Resolved values flow through the normal validation path, so quote-keyword and unknown-directive warnings still fire on function outputs.
  • Resolution runs for every response, including Boom errors, so the header is consistent on 500s.

Automatic nonce generation

Set generateNonces: true to have hapi-aegis mint a fresh base64 nonce per request and append 'nonce-<value>' to both script-src and style-src automatically. The nonce is exposed on request.plugins.aegis.nonce so handlers and templates can attach the matching nonce="..." attribute to inline <script> / <style> tags.

await server.register({
    plugin: Aegis,
    options: {
        contentSecurityPolicy: { generateNonces: true }
    }
});

server.route({
    method: 'GET',
    path: '/',
    handler: (request) => `
        <!doctype html>
        <script nonce="${request.plugins.aegis.nonce}">/* inline init */</script>
    `
});

Notes:

  • The nonce is generated in an onPreAuth extension, so it is available to auth strategies, validators, and route handlers.
  • It is injected after any user-supplied directive values resolve, so user entries (including function-valued ones) are preserved.
  • With useDefaults: false and no user-supplied scriptSrc / styleSrc, the middleware emits those directives with only the nonce.
  • The switch can be toggled per route via options.plugins.aegis.contentSecurityPolicy.generateNonces — handy for static-asset or API-only routes that don't need a nonce.
  • Prefer this to the manual onRequest + function-valued pattern above unless you need a custom nonce shape or a different injection point.

Naming and edge cases

  • camelCase → kebab-case. Directive names are given in camelCase and converted automatically: scriptSrcAttrscript-src-attr, upgradeInsecureRequestsupgrade-insecure-requests.
  • Empty-array directives. A directive whose value is [] renders without a value — useful for flag-style directives like upgradeInsecureRequests: [].
  • Unknown directives. hapi-aegis warns via console.warn for directives it doesn't recognise, but still emits them. The CSP spec evolves; this is a nudge, not an error.
  • Unquoted keywords. Values like self, none, and unsafe-inline must be single-quoted ("'self'"). Bare usage triggers a console.warn so a missing quote doesn't silently weaken the policy.

Route-Level Configuration

Every middleware can be overridden per route via options.plugins.aegis. Route-level settings take precedence for the middlewares they mention; everything else falls back to the server-level configuration.

server.route({
    method: 'GET',
    path: '/api',
    options: {
        plugins: {
            aegis: {
                contentSecurityPolicy: false,        // disable CSP for this route
                frameguard: { action: 'deny' }       // tighten X-Frame-Options here
            }
        }
    },
    handler: () => ({ ok: true })
});

Common patterns:

  • Disable a header for a specific endpoint. JSON APIs often don't need CSP — set contentSecurityPolicy: false on the route.
  • Relax CSP for a page that needs third-party scripts. Provide a different directives object on that route only.
  • Tighten one route past the server default. For instance, frameguard: { action: 'deny' } on an admin page while the rest of the site runs SAMEORIGIN.

Comparison with Helmet

If you're used to Helmet on Express, the Hapi API will feel familiar.

Express + Helmet

const express = require('express');
const helmet = require('helmet');

const app = express();
app.use(helmet());

Hapi + hapi-aegis

const Hapi = require('@hapi/hapi');
const Aegis = require('hapi-aegis');

const server = Hapi.server();
await server.register(Aegis);

Custom CSP looks nearly identical:

Express + Helmet

app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            scriptSrc: ["'self'", 'https://cdn.example.com']
        }
    }
}));

Hapi + hapi-aegis

await server.register({
    plugin: Aegis,
    options: {
        contentSecurityPolicy: {
            directives: {
                scriptSrc: ["'self'", 'https://cdn.example.com']
            }
        }
    }
});

See Also

For per-request CSP variations (nonces, hashes, anything that depends on the request), use the Dynamic per-request directives feature above — function-valued directives are resolved during onPreResponse with the Hapi request. If you'd prefer a dedicated plugin for that use case, blankie is an alternative; you can run it alongside hapi-aegis by setting contentSecurityPolicy: false in the hapi-aegis options and letting blankie handle CSP while hapi-aegis handles the other headers.

FAQ

Does this work with Boom error responses?

Yes. The plugin attaches an onPreResponse extension that detects Boom responses (response.isBoom) and applies headers to response.output.headers. A 400 or 500 response gets the same security headers as a 200.

Can I use this alongside other Hapi auth/validation plugins?

Yes. hapi-aegis only reads and writes response headers in an onPreResponse extension; it doesn't touch routing, authentication, validation, or the request lifecycle. Register it alongside @hapi/jwt, @hapi/bell, joi-based validation, and so on without conflict.

Why is X-XSS-Protection set to 0?

The legacy X-XSS-Protection filter has known bypasses and can be used as an XSS vector in itself. Modern browsers have removed or deprecated it. The safe default — matching Helmet — is to emit 0 so any residual browser behaviour is explicitly disabled, and to rely on Content-Security-Policy for XSS mitigation.

Is expectCt deprecated?

Yes. The Expect-CT header is deprecated by browsers. The middleware is included for legacy compatibility and sets max-age=0 by default, which is effectively a no-op. Disable it entirely with expectCt: false if you have no use for it.

Performance

hapi-aegis runs ~17 small middleware functions on each response via onPreResponse. The benchmarks/ directory contains autocannon-based scenarios that compare a bare Hapi server against hapi-aegis with defaults, with CSP disabled, and with only three middlewares enabled.

A local Node 22 run on a no-op route (handler returns the string "ok") measured ~24% lower req/s with defaults vs. bare (43.4k → 33.0k req/s after the 1.3.1 hot-path caching, down from ~36% pre-1.3.1) with p99 latency steady at 2–3 ms. Most of the remaining gap is bytes-on-wire — the default header set adds roughly 870 bytes to every response, so on no-op routes the network has more to push for each request. Routes returning real payloads see proportionally smaller relative overhead. See benchmarks/README.md for methodology and the full table. Run npm run bench locally, or trigger the manual Benchmarks GitHub Actions workflow to capture results across Node 18, 20, and 22.

Contributing

  1. Fork the repo and create a feature branch.
  2. npm install to pull dev dependencies.
  3. Make your change. Keep the middleware pattern (pure function, { header, value } out) and prefix all thrown errors with hapi-aegis:.
  4. Add tests. Run npm test — the suite must pass with ≥95% coverage.
  5. Run npm run lint.
  6. Open a pull request. Use conventional commits for commit messages (e.g. feat(hsts): …, fix(core): …).

Acknowledgements

hapi-aegis's option shapes, middleware scope, and sensible defaults are modeled after Helmet, the Express security-headers middleware. The implementation is independent — hapi's request lifecycle, Boom error handling, and route-level plugin configuration are all hapi-native — but the API similarity is intentional to make the plugin feel familiar to developers coming from Express.

License

MIT © 2026 Matt Rosenlund — see LICENSE.