@alxia/secure-headers 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +3 -3
- package/dist/secure-headers.d.ts +3 -5
- package/dist/secure-headers.d.ts.map +1 -1
- package/docs/guide.md +3 -8
- package/docs/roadmap.md +1 -1
- package/docs/troubleshooting.md +28 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -103,7 +103,6 @@ value is refused at startup. `nonce` is off by default.
|
|
|
103
103
|
| `NonceContext` | what `nonce: true` adds to the context: `nonce`, a string |
|
|
104
104
|
| `SecureHeaders` | what `secureHeaders()` returns: a middleware that adds nothing to the context |
|
|
105
105
|
| `NonceMiddleware` | what `secureHeaders({ nonce: true })` returns: a middleware adding `NonceContext` |
|
|
106
|
-
| `NoncePlugin` | deprecated: the former name of `NonceMiddleware` |
|
|
107
106
|
|
|
108
107
|
## Traps
|
|
109
108
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { NONCE, type NonceContext } from './nonce';
|
|
2
|
-
export { type NonceMiddleware, type
|
|
2
|
+
export { type NonceMiddleware, type SecureHeaders, type SecureHeadersOptions, type Setting, secureHeaders, } from './secure-headers';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,OAAO,EACZ,aAAa,GACb,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ function freshNonce() {
|
|
|
19
19
|
// src/secure-headers.ts
|
|
20
20
|
import {
|
|
21
21
|
defineMiddleware,
|
|
22
|
+
markFactory,
|
|
22
23
|
settle,
|
|
23
24
|
withHeaders
|
|
24
25
|
} from "@alxia/core";
|
|
@@ -59,7 +60,9 @@ function secureHeaders(options = {}) {
|
|
|
59
60
|
throw new TypeError("secureHeaders: contentSecurityPolicy names NONCE, but nonce is off: give nonce: true");
|
|
60
61
|
}
|
|
61
62
|
const set = setter(headers, hide);
|
|
62
|
-
return defineMiddleware(async (ctx, next)
|
|
63
|
+
return defineMiddleware(async function secureHeaders(ctx, next) {
|
|
64
|
+
return set(await settle(ctx, next()));
|
|
65
|
+
});
|
|
63
66
|
}
|
|
64
67
|
if (policy === undefined) {
|
|
65
68
|
throw new TypeError("secureHeaders: nonce is on, but contentSecurityPolicy is false: a nonce is only read through the content-security-policy header");
|
|
@@ -67,7 +70,7 @@ function secureHeaders(options = {}) {
|
|
|
67
70
|
headers.delete("content-security-policy");
|
|
68
71
|
const withNonce = policyWithNonce(policy);
|
|
69
72
|
const set = setter(headers, hide);
|
|
70
|
-
return defineMiddleware(async (ctx, next)
|
|
73
|
+
return defineMiddleware(async function secureHeaders(ctx, next) {
|
|
71
74
|
const added = { nonce: freshNonce() };
|
|
72
75
|
const response = await settle(ctx, next(added));
|
|
73
76
|
return set(response, withNonce(added.nonce));
|
|
@@ -101,10 +104,11 @@ function setter(headers, hide) {
|
|
|
101
104
|
}
|
|
102
105
|
});
|
|
103
106
|
}
|
|
107
|
+
markFactory(secureHeaders);
|
|
104
108
|
export {
|
|
105
109
|
NONCE,
|
|
106
110
|
secureHeaders
|
|
107
111
|
};
|
|
108
112
|
|
|
109
|
-
//# debugId=
|
|
113
|
+
//# debugId=DA6BBAE79DDA945364756E2164756E21
|
|
110
114
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/nonce.ts", "../src/secure-headers.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * The per-request nonce of `secureHeaders({ nonce: true })`: where it goes\n * in the policy, and how one is made.\n */\n\n/**\n * Where the nonce goes in `contentSecurityPolicy`: each occurrence becomes\n * `'nonce-<value>'`, the request's own value. Without it, `nonce: true`\n * adds the nonce to `script-src` and `script-src-elem`.\n *\n * ```ts\n * secureHeaders({\n * nonce: true,\n * contentSecurityPolicy: `default-src 'self'; style-src 'self' ${NONCE}`,\n * });\n * ```\n */\nexport const NONCE = \"'nonce-{alxia}'\";\n\n/** What `secureHeaders({ nonce: true })` adds to the context of the routes after it. */\nexport interface NonceContext {\n\t/**\n\t * This request's nonce, base64, the one its `Content-Security-Policy`\n\t * allows: what a page puts on its `<script nonce>`.\n\t */\n\treadonly nonce: string;\n}\n\n/** The directives the nonce is added to when the policy does not place it. */\nconst SCRIPT_DIRECTIVES = new Set(['script-src', 'script-src-elem']);\n\n/**\n * The policy as a function of the nonce: split where `NONCE` stands, or\n * where it is added to the script directives. Throws when there is\n * nowhere to put it.\n */\nexport function policyWithNonce(policy: string): (nonce: string) => string {\n\tconst placed = policy.includes(NONCE)\n\t\t? policy\n\t\t: policy\n\t\t\t\t.split(';')\n\t\t\t\t.map((directive) => {\n\t\t\t\t\tconst name = directive.trim().split(/\\s+/)[0]?.toLowerCase() ?? '';\n\t\t\t\t\treturn SCRIPT_DIRECTIVES.has(name)\n\t\t\t\t\t\t? `${directive.trimEnd()} ${NONCE}`\n\t\t\t\t\t\t: directive;\n\t\t\t\t})\n\t\t\t\t.join(';');\n\tif (!placed.includes(NONCE)) {\n\t\tthrow new TypeError(\n\t\t\t`secureHeaders: nonce is on, but the content-security-policy has no script-src to add it to: write one, or place NONCE where the nonce goes (the policy is \"${policy}\")`,\n\t\t);\n\t}\n\tconst parts = placed.split(NONCE);\n\treturn (nonce) => parts.join(`'nonce-${nonce}'`);\n}\n\n/** 128 random bits, base64: a fresh nonce. */\nexport function freshNonce(): string {\n\tconst bytes = crypto.getRandomValues(new Uint8Array(16));\n\treturn btoa(String.fromCharCode(...bytes));\n}\n",
|
|
6
|
-
"import {\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\
|
|
6
|
+
"import {\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Next,\n\tsettle,\n\twithHeaders,\n} from '@alxia/core';\nimport { freshNonce, NONCE, type NonceContext, policyWithNonce } from './nonce';\n\n/** A header's value, or `false` to leave it out. An empty value is refused. */\nexport type Setting = string | false;\n\nexport interface SecureHeadersOptions {\n\t/** Defaults to a policy for an API: nothing loads, nothing frames it. */\n\treadonly contentSecurityPolicy?: Setting;\n\treadonly strictTransportSecurity?: Setting;\n\treadonly xContentTypeOptions?: Setting;\n\treadonly xFrameOptions?: Setting;\n\treadonly referrerPolicy?: Setting;\n\treadonly crossOriginOpenerPolicy?: Setting;\n\treadonly crossOriginResourcePolicy?: Setting;\n\treadonly crossOriginEmbedderPolicy?: Setting;\n\treadonly originAgentCluster?: Setting;\n\treadonly xDnsPrefetchControl?: Setting;\n\treadonly xPermittedCrossDomainPolicies?: Setting;\n\t/** Off by default: a policy is the app's to write. */\n\treadonly permissionsPolicy?: Setting;\n\t/** Whether `X-Powered-By` and `Server` are removed. On by default. */\n\treadonly hidePoweredBy?: boolean;\n}\n\n/**\n * The nonce switch, beside `SecureHeadersOptions` rather than in it, so that\n * options typed by that interface still pick the plugin without a nonce.\n */\ninterface NonceOption {\n\t/**\n\t * A fresh nonce for each request, in `contentSecurityPolicy` and on the\n\t * context as `nonce`. Placed where the policy names `NONCE`, or added to\n\t * its `script-src`. Off by default.\n\t */\n\treadonly nonce?: boolean;\n}\n\n/**\n * What `secureHeaders()` returns: a middleware, for `app.use`, that sets\n * the headers on every response after it.\n */\nexport type SecureHeaders = Middleware<Empty, Promise<Response>>;\n\n/**\n * What `secureHeaders({ nonce: true })` returns: a middleware, for\n * `app.use`, whose routes after it read `nonce`, and which sets the header\n * with the same one.\n */\nexport type NonceMiddleware = Middleware<Empty, Promise<Next<NonceContext>>>;\n\nconst DEFAULTS = {\n\t'content-security-policy':\n\t\t\"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n\t'strict-transport-security': 'max-age=31536000; includeSubDomains',\n\t'x-content-type-options': 'nosniff',\n\t'x-frame-options': 'DENY',\n\t'referrer-policy': 'no-referrer',\n\t'cross-origin-opener-policy': 'same-origin',\n\t'cross-origin-resource-policy': 'same-origin',\n\t'cross-origin-embedder-policy': false,\n\t'origin-agent-cluster': '?1',\n\t'x-dns-prefetch-control': 'off',\n\t'x-permitted-cross-domain-policies': 'none',\n\t'permissions-policy': false,\n} as const satisfies Record<string, Setting>;\n\nconst OPTION: Record<keyof typeof DEFAULTS, keyof SecureHeadersOptions> = {\n\t'content-security-policy': 'contentSecurityPolicy',\n\t'strict-transport-security': 'strictTransportSecurity',\n\t'x-content-type-options': 'xContentTypeOptions',\n\t'x-frame-options': 'xFrameOptions',\n\t'referrer-policy': 'referrerPolicy',\n\t'cross-origin-opener-policy': 'crossOriginOpenerPolicy',\n\t'cross-origin-resource-policy': 'crossOriginResourcePolicy',\n\t'cross-origin-embedder-policy': 'crossOriginEmbedderPolicy',\n\t'origin-agent-cluster': 'originAgentCluster',\n\t'x-dns-prefetch-control': 'xDnsPrefetchControl',\n\t'x-permitted-cross-domain-policies': 'xPermittedCrossDomainPolicies',\n\t'permissions-policy': 'permissionsPolicy',\n};\n\n/**\n * Secure headers on every response, as a middleware: each response of the\n * routes after it and of a request no route matches — a 404, an error —\n * gets them. A header a route set itself is kept: a page that needs its\n * own `Content-Security-Policy` — `@alxia/graphql`'s IDE — sets it.\n *\n * ```ts\n * app.use(secureHeaders({ contentSecurityPolicy: \"default-src 'self'\" }));\n * ```\n *\n * With `nonce: true`, each request gets a nonce of its own, in the policy\n * and on the context of the routes declared after it:\n *\n * ```ts\n * app\n * .use(secureHeaders({ nonce: true, contentSecurityPolicy: \"script-src 'self'\" }))\n * .get('/', ({ nonce, reply }) => reply(200, `<script nonce=\"${nonce}\">…</script>`));\n * ```\n */\nexport function secureHeaders(\n\toptions: SecureHeadersOptions & { readonly nonce: true },\n): NonceMiddleware;\nexport function secureHeaders(\n\toptions?: SecureHeadersOptions & { readonly nonce?: false },\n): SecureHeaders;\nexport function secureHeaders(\n\toptions: SecureHeadersOptions & NonceOption = {},\n): SecureHeaders | NonceMiddleware {\n\tconst headers = fixedHeaders(options);\n\tconst hide = options.hidePoweredBy ?? true;\n\tconst policy = headers.get('content-security-policy');\n\tif (!options.nonce) {\n\t\tif (policy?.includes(NONCE)) {\n\t\t\tthrow new TypeError(\n\t\t\t\t'secureHeaders: contentSecurityPolicy names NONCE, but nonce is off: give nonce: true',\n\t\t\t);\n\t\t}\n\t\tconst set = setter(headers, hide);\n\t\treturn defineMiddleware(async function secureHeaders(ctx, next) {\n\t\t\treturn set(await settle(ctx, next()));\n\t\t});\n\t}\n\tif (policy === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'secureHeaders: nonce is on, but contentSecurityPolicy is false: a nonce is only read through the content-security-policy header',\n\t\t);\n\t}\n\theaders.delete('content-security-policy');\n\tconst withNonce = policyWithNonce(policy);\n\tconst set = setter(headers, hide);\n\treturn defineMiddleware(async function secureHeaders(ctx, next) {\n\t\tconst added: NonceContext = { nonce: freshNonce() };\n\t\tconst response = await settle(ctx, next(added));\n\t\treturn set(response, withNonce(added.nonce)) as typeof response;\n\t});\n}\n\n/** Each header the options leave in, by name, with its value. Throws on an empty one. */\nfunction fixedHeaders(options: SecureHeadersOptions): Map<string, string> {\n\tconst headers = new Map<string, string>();\n\tfor (const [name, fallback] of Object.entries(DEFAULTS) as [\n\t\tkeyof typeof DEFAULTS,\n\t\tSetting,\n\t][]) {\n\t\tconst setting = options[OPTION[name]];\n\t\tif (typeof setting === 'string' && setting.trim() === '') {\n\t\t\t// An empty header says nothing a browser can act on: leave it out.\n\t\t\tthrow new TypeError(\n\t\t\t\t`secureHeaders: ${OPTION[name]} is empty; give false to leave the ${name} header out`,\n\t\t\t);\n\t\t}\n\t\tconst value = setting === undefined ? fallback : setting;\n\t\tif (typeof value === 'string') headers.set(name, value);\n\t}\n\treturn headers;\n}\n\n/** Every header the response lacks, then the policy given per request. */\nfunction setter(headers: ReadonlyMap<string, string>, hide: boolean) {\n\treturn (response: Response, policy?: string) =>\n\t\twithHeaders(response, (current) => {\n\t\t\tfor (const [name, value] of headers) {\n\t\t\t\tif (!current.has(name)) current.set(name, value);\n\t\t\t}\n\t\t\tif (policy !== undefined && !current.has('content-security-policy')) {\n\t\t\t\tcurrent.set('content-security-policy', policy);\n\t\t\t}\n\t\t\tif (hide) {\n\t\t\t\tcurrent.delete('x-powered-by');\n\t\t\t\tcurrent.delete('server');\n\t\t\t}\n\t\t});\n}\n\nmarkFactory(secureHeaders);\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";AAiBO,IAAM,QAAQ;AAYrB,IAAM,oBAAoB,IAAI,IAAI,CAAC,cAAc,iBAAiB,CAAC;AAO5D,SAAS,eAAe,CAAC,QAA2C;AAAA,EAC1E,MAAM,SAAS,OAAO,SAAS,KAAK,IACjC,SACA,OACC,MAAM,GAAG,EACT,IAAI,CAAC,cAAc;AAAA,IACnB,MAAM,OAAO,UAAU,KAAK,EAAE,MAAM,KAAK,EAAE,IAAI,YAAY,KAAK;AAAA,IAChE,OAAO,kBAAkB,IAAI,IAAI,IAC9B,GAAG,UAAU,QAAQ,KAAK,UAC1B;AAAA,GACH,EACA,KAAK,GAAG;AAAA,EACZ,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAAA,IAC5B,MAAM,IAAI,UACT,8JAA8J,UAC/J;AAAA,EACD;AAAA,EACA,MAAM,QAAQ,OAAO,MAAM,KAAK;AAAA,EAChC,OAAO,CAAC,UAAU,MAAM,KAAK,UAAU,QAAQ;AAAA;AAIzC,SAAS,UAAU,GAAW;AAAA,EACpC,MAAM,QAAQ,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AAAA,EACvD,OAAO,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC;AAAA;;AC5D1C;AAAA;AAAA;AAAA;AAAA;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "DA6BBAE79DDA945364756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/secure-headers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Empty, type Middleware, type
|
|
1
|
+
import { type Empty, type Middleware, type Next } from '@alxia/core';
|
|
2
2
|
import { type NonceContext } from './nonce';
|
|
3
3
|
/** A header's value, or `false` to leave it out. An empty value is refused. */
|
|
4
4
|
export type Setting = string | false;
|
|
@@ -24,15 +24,13 @@ export interface SecureHeadersOptions {
|
|
|
24
24
|
* What `secureHeaders()` returns: a middleware, for `app.use`, that sets
|
|
25
25
|
* the headers on every response after it.
|
|
26
26
|
*/
|
|
27
|
-
export type SecureHeaders = Middleware<Empty, Promise<Response
|
|
27
|
+
export type SecureHeaders = Middleware<Empty, Promise<Response>>;
|
|
28
28
|
/**
|
|
29
29
|
* What `secureHeaders({ nonce: true })` returns: a middleware, for
|
|
30
30
|
* `app.use`, whose routes after it read `nonce`, and which sets the header
|
|
31
31
|
* with the same one.
|
|
32
32
|
*/
|
|
33
|
-
export type NonceMiddleware = Middleware<Empty, Promise<Next<NonceContext
|
|
34
|
-
/** @deprecated Renamed `NonceMiddleware`: it is a middleware. */
|
|
35
|
-
export type NoncePlugin = NonceMiddleware;
|
|
33
|
+
export type NonceMiddleware = Middleware<Empty, Promise<Next<NonceContext>>>;
|
|
36
34
|
/**
|
|
37
35
|
* Secure headers on every response, as a middleware: each response of the
|
|
38
36
|
* routes after it and of a request no route matches — a 404, an error —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-headers.d.ts","sourceRoot":"","sources":["../src/secure-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,KAAK,EACV,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"secure-headers.d.ts","sourceRoot":"","sources":["../src/secure-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,IAAI,EAGT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAqB,KAAK,YAAY,EAAmB,MAAM,SAAS,CAAC;AAEhF,+EAA+E;AAC/E,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAErC,MAAM,WAAW,oBAAoB;IACpC,yEAAyE;IACzE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C,QAAQ,CAAC,yBAAyB,CAAC,EAAE,OAAO,CAAC;IAC7C,QAAQ,CAAC,yBAAyB,CAAC,EAAE,OAAO,CAAC;IAC7C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACjD,sDAAsD;IACtD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IACrC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CACjC;AAeD;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEjE;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AAiC7E;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAC5B,OAAO,EAAE,oBAAoB,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GACtD,eAAe,CAAC;AACnB,wBAAgB,aAAa,CAC5B,OAAO,CAAC,EAAE,oBAAoB,GAAG;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACzD,aAAa,CAAC"}
|
package/docs/guide.md
CHANGED
|
@@ -292,12 +292,11 @@ client would get, whatever produced it.
|
|
|
292
292
|
| Response | Headers |
|
|
293
293
|
| --- | --- |
|
|
294
294
|
| a route's reply, `static` and `file` included | yes |
|
|
295
|
-
| the core's 400, 404, 405 and 500, and
|
|
295
|
+
| the core's 400, 404, 405 and 500, and a `try`/`catch` middleware's reply declared after it | yes |
|
|
296
296
|
| a `Response` another middleware returned (a CORS preflight, a redirect) | yes, when that middleware is declared after `secureHeaders` |
|
|
297
297
|
| a route declared before `app.use(secureHeaders())` | no: the middleware does not run for it |
|
|
298
298
|
| a WebSocket upgrade that succeeds (`101`) | no: the upgrade is not a response a middleware can decorate |
|
|
299
299
|
| a page served with `page()` | no: `Bun.serve` serves it, around no middleware |
|
|
300
|
-
| the 500 sent when a deprecated `around` hook itself throws | no |
|
|
301
300
|
|
|
302
301
|
```ts
|
|
303
302
|
const app = alxia().use(secureHeaders());
|
|
@@ -340,16 +339,12 @@ Declare `secureHeaders()` among the observers, first: `logger()`,
|
|
|
340
339
|
`telemetry()`, `secureHeaders()`, `cors()`, `compress()`. An
|
|
341
340
|
error-handling middleware — a `try`/`catch` around `next()`, or
|
|
342
341
|
`janusErrors()` — goes **after** it. `secureHeaders()` settles `next()`: it
|
|
343
|
-
sets the headers on the response the error would be answered with (
|
|
344
|
-
|
|
342
|
+
sets the headers on the response the error would be answered with (its
|
|
343
|
+
`HttpError`'s status, or a 500), then the error
|
|
345
344
|
goes on, so a `try`/`catch` catches it wherever it is declared; declared
|
|
346
345
|
before `secureHeaders()`, its reply does not carry the headers. A guard on the app (`bearer`, a required
|
|
347
346
|
session) declared before it answers its `401` without them.
|
|
348
347
|
|
|
349
|
-
The deprecated `onResponse` and `around` hooks still run outside every
|
|
350
|
-
middleware: an `onResponse` hook sees the headers `secureHeaders` set,
|
|
351
|
-
wherever it is declared.
|
|
352
|
-
|
|
353
348
|
## A header the middleware does not know
|
|
354
349
|
|
|
355
350
|
The middleware sends the twelve headers above and nothing else. Another one —
|
package/docs/roadmap.md
CHANGED
|
@@ -11,7 +11,7 @@ number on it. Every release, with each change it made, is in
|
|
|
11
11
|
first, sets the headers on every response that comes back through it, a
|
|
12
12
|
404's, an error's and a 500's included; `NonceMiddleware` gives `nonce` to the
|
|
13
13
|
routes after it and `SecureHeaders` is the plain one.
|
|
14
|
-
`app.plugin(secureHeaders())
|
|
14
|
+
`app.plugin(secureHeaders())`, the deprecated alias, was removed in 0.5.
|
|
15
15
|
|
|
16
16
|
## Next
|
|
17
17
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -11,14 +11,14 @@ or a header in the wrong place.
|
|
|
11
11
|
- [`Type 'true' is not assignable to type 'Setting | undefined'`](#type-true-is-not-assignable-to-type-setting--undefined)
|
|
12
12
|
- [`No overload matches this call` … `is not assignable to type 'SecureHeadersOptions' with 'exactOptionalPropertyTypes: true'`](#no-overload-matches-this-call--is-not-assignable-to-type-secureheadersoptions-with-exactoptionalpropertytypes-true)
|
|
13
13
|
- [`No overload matches this call` … `Object literal may only specify known properties, and '…' does not exist in type 'SecureHeadersOptions & …'`](#no-overload-matches-this-call--object-literal-may-only-specify-known-properties-and--does-not-exist-in-type-secureheadersoptions--)
|
|
14
|
-
- [`
|
|
14
|
+
- [`Type 'undefined' is not assignable to type 'SecureHeadersOptions & { readonly nonce: true; }'`](#type-undefined-is-not-assignable-to-type-secureheadersoptions---readonly-nonce-true-)
|
|
15
15
|
- [`Property 'nonce' does not exist on type 'RouteBase<…>'`](#property-nonce-does-not-exist-on-type-routebase)
|
|
16
16
|
- [`No overload matches this call` … `Type 'boolean' is not assignable to type 'true'`](#no-overload-matches-this-call--type-boolean-is-not-assignable-to-type-true)
|
|
17
17
|
|
|
18
18
|
**Runtime**
|
|
19
19
|
|
|
20
20
|
- [`TypeError: secureHeaders: … is empty; give false to leave the … header out`](#typeerror-secureheaders--is-empty-give-false-to-leave-the--header-out)
|
|
21
|
-
- [`
|
|
21
|
+
- [`use(): argument 1 looks like a factory (secureHeaders): call it, use(secureHeaders())`](#use-argument-1-looks-like-a-factory-secureheaders-call-it-usesecureheaders)
|
|
22
22
|
- [`TypeError: secureHeaders: nonce is on, but the content-security-policy has no script-src to add it to: …`](#typeerror-secureheaders-nonce-is-on-but-the-content-security-policy-has-no-script-src-to-add-it-to-)
|
|
23
23
|
- [`TypeError: secureHeaders: nonce is on, but contentSecurityPolicy is false: …`](#typeerror-secureheaders-nonce-is-on-but-contentsecuritypolicy-is-false-)
|
|
24
24
|
- [`TypeError: secureHeaders: contentSecurityPolicy names NONCE, but nonce is off: give nonce: true`](#typeerror-secureheaders-contentsecuritypolicy-names-nonce-but-nonce-is-off-give-nonce-true)
|
|
@@ -141,22 +141,23 @@ const app = alxia()
|
|
|
141
141
|
.use(reportOnly);
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
### `
|
|
144
|
+
### `Type 'undefined' is not assignable to type 'SecureHeadersOptions & { readonly nonce: true; }'`
|
|
145
145
|
|
|
146
146
|
**When:** `secureHeaders` is given to `app.use` without being called.
|
|
147
147
|
|
|
148
148
|
```text
|
|
149
|
-
error
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Type '
|
|
154
|
-
|
|
149
|
+
error TS2345: Argument of type '{ (options: SecureHeadersOptions & { readonly nonce: true; }): NonceMiddleware; (options?: (SecureHeadersOptions & { ...; }) | undefined): SecureHeaders; }' is not assignable to parameter of type '{ (options: SecureHeadersOptions & { readonly nonce: true; }): NonceMiddleware; (options?: (SecureHeadersOptions & { ...; }) | undefined): SecureHeaders; } & Step<...>'.
|
|
150
|
+
Type '{ (options: SecureHeadersOptions & { readonly nonce: true; }): NonceMiddleware; (options?: (SecureHeadersOptions & { ...; }) | undefined): SecureHeaders; }' is not assignable to type 'Step<BaseContext & Empty, (SecureHeadersOptions & { readonly nonce?: false; }) | undefined, MiddlewareReturn>'.
|
|
151
|
+
Types of parameters 'options' and 'ctx' are incompatible.
|
|
152
|
+
Type '(SecureHeadersOptions & { readonly nonce?: false; }) | undefined' is not assignable to type 'SecureHeadersOptions & { readonly nonce: true; }'.
|
|
153
|
+
Type 'undefined' is not assignable to type 'SecureHeadersOptions & { readonly nonce: true; }'.
|
|
154
|
+
Type 'undefined' is not assignable to type 'SecureHeadersOptions'.
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
**Why:** `secureHeaders` makes the middleware from its options; the
|
|
158
|
-
middleware is what it returns.
|
|
159
|
-
|
|
158
|
+
middleware is what it returns. Given to `use` as it is, it is read as a
|
|
159
|
+
middleware whose context is its options, and TypeScript says those do not
|
|
160
|
+
fit its first overload, the nonce one.
|
|
160
161
|
|
|
161
162
|
**Fix:** call it, with or without options:
|
|
162
163
|
|
|
@@ -169,7 +170,7 @@ app.use(secureHeaders());
|
|
|
169
170
|
**When:** a route or a middleware reads `nonce` from its context.
|
|
170
171
|
|
|
171
172
|
```text
|
|
172
|
-
error TS2339: Property 'nonce' does not exist on type 'RouteBase<RouteApp<"GET", Empty, ""
|
|
173
|
+
error TS2339: Property 'nonce' does not exist on type 'RouteBase<RouteApp<"GET", Empty, "">, "/">'.
|
|
173
174
|
error TS2339: Property 'nonce' does not exist on type 'BaseContext & Empty'.
|
|
174
175
|
```
|
|
175
176
|
|
|
@@ -236,20 +237,18 @@ Only `false` leaves a header out.
|
|
|
236
237
|
app.use(secureHeaders({ xFrameOptions: false }));
|
|
237
238
|
```
|
|
238
239
|
|
|
239
|
-
### `
|
|
240
|
-
|
|
241
|
-
**When:** the same mistake as above in JavaScript, or past a cast:
|
|
242
|
-
`alxia().use(secureHeaders)`, the factory given uncalled. It throws at
|
|
243
|
-
startup:
|
|
240
|
+
### `use(): argument 1 looks like a factory (secureHeaders): call it, use(secureHeaders())`
|
|
244
241
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
**When:** `alxia().use(secureHeaders)`, the factory given uncalled, in
|
|
243
|
+
JavaScript or past a cast. It throws where the app is declared, since
|
|
244
|
+
alxia 0.5; before, each request was answered with a 500 and the log said
|
|
245
|
+
`GET /: a middleware (secureHeaders) returned function: …`. TypeScript
|
|
246
|
+
refuses it first, with `"this looks like a factory given uncalled: …"` in
|
|
247
|
+
the error.
|
|
248
248
|
|
|
249
|
-
**Why:** `
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
function, not the app.
|
|
249
|
+
**Why:** `use` takes a middleware, a `(ctx, next)` function; `secureHeaders`
|
|
250
|
+
makes one, and is marked as a factory, so `use`, a route and `plugin`
|
|
251
|
+
refuse it.
|
|
253
252
|
|
|
254
253
|
**Fix:**
|
|
255
254
|
|
|
@@ -509,8 +508,7 @@ response.
|
|
|
509
508
|
|
|
510
509
|
**Why:** the middleware deletes them when the response comes back through
|
|
511
510
|
it. A middleware declared **before** `secureHeaders` that adds one sees the
|
|
512
|
-
response after that, and keeps it
|
|
513
|
-
which runs after every middleware. A proxy in front of the app may add its
|
|
511
|
+
response after that, and keeps it. A proxy in front of the app may add its
|
|
514
512
|
own `Server` too.
|
|
515
513
|
|
|
516
514
|
**Fix:** declare `secureHeaders` before the middleware that adds the header,
|
|
@@ -534,10 +532,9 @@ const app = alxia()
|
|
|
534
532
|
**When:** a response has none of the headers, though `secureHeaders` is
|
|
535
533
|
in use.
|
|
536
534
|
|
|
537
|
-
**Why:** the middleware decorates what comes back through it, and
|
|
538
|
-
responses do not: a WebSocket upgrade that succeeds (the `101`), a page
|
|
539
|
-
served with `page()`, which `Bun.serve` answers itself
|
|
540
|
-
when a deprecated `around` hook throws.
|
|
535
|
+
**Why:** the middleware decorates what comes back through it, and two
|
|
536
|
+
responses do not: a WebSocket upgrade that succeeds (the `101`), and a page
|
|
537
|
+
served with `page()`, which `Bun.serve` answers itself.
|
|
541
538
|
|
|
542
539
|
**Fix:** serve a page that needs the headers through a route, `file` or
|
|
543
540
|
`static` rather than `page()` — or give it its policy in its HTML with
|
|
@@ -595,8 +592,7 @@ its own format, is declared before `secureHeaders()`, and its reply comes
|
|
|
595
592
|
back without the security headers.
|
|
596
593
|
|
|
597
594
|
**Why:** `secureHeaders` settles `next()`: it sets the headers on the response
|
|
598
|
-
the route boundary would give (
|
|
599
|
-
500), then the error goes on to the middlewares around it. A `try`/`catch`
|
|
595
|
+
the route boundary would give (the `HttpError`'s status, or a 500), then the error goes on to the middlewares around it. A `try`/`catch`
|
|
600
596
|
declared **before** it catches the error, but its reply is made outside
|
|
601
597
|
`secureHeaders`, so it carries no headers; the 500 inside did.
|
|
602
598
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/secure-headers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Secure HTTP headers for alxia: HSTS, CSP, nosniff and the rest, each one adjustable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/core": "^0.
|
|
44
|
+
"@alxia/core": "^0.5.0",
|
|
45
45
|
"@types/bun": "^1.4.2",
|
|
46
46
|
"zod": "^4.2.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@alxia/core": "^0.
|
|
49
|
+
"@alxia/core": "^0.5.0",
|
|
50
50
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|