@blamejs/core 0.8.51 → 0.8.57

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/index.js +8 -0
  3. package/lib/audit.js +4 -0
  4. package/lib/auth/fido-mds3.js +624 -0
  5. package/lib/auth/passkey.js +214 -2
  6. package/lib/auth-bot-challenge.js +1 -1
  7. package/lib/credential-hash.js +2 -2
  8. package/lib/framework-error.js +55 -0
  9. package/lib/guard-cidr.js +2 -1
  10. package/lib/guard-jwt.js +2 -2
  11. package/lib/guard-oauth.js +2 -2
  12. package/lib/http-client-cache.js +916 -0
  13. package/lib/http-client.js +242 -0
  14. package/lib/local-db-thin.js +8 -7
  15. package/lib/mail-arf.js +343 -0
  16. package/lib/mail-auth.js +265 -40
  17. package/lib/mail-bimi.js +948 -33
  18. package/lib/mail-bounce.js +386 -4
  19. package/lib/mail-mdn.js +424 -0
  20. package/lib/mail-unsubscribe.js +265 -25
  21. package/lib/mail.js +403 -21
  22. package/lib/middleware/bearer-auth.js +1 -1
  23. package/lib/middleware/clear-site-data.js +122 -0
  24. package/lib/middleware/dpop.js +1 -1
  25. package/lib/middleware/index.js +9 -0
  26. package/lib/middleware/nel.js +214 -0
  27. package/lib/middleware/security-headers.js +56 -4
  28. package/lib/middleware/speculation-rules.js +323 -0
  29. package/lib/mime-parse.js +198 -0
  30. package/lib/network-dns.js +890 -27
  31. package/lib/network-tls.js +745 -0
  32. package/lib/object-store/sigv4.js +54 -0
  33. package/lib/public-suffix.js +414 -0
  34. package/lib/safe-buffer.js +7 -0
  35. package/lib/safe-json.js +1 -1
  36. package/lib/static.js +120 -0
  37. package/lib/storage.js +11 -0
  38. package/lib/vendor/MANIFEST.json +33 -0
  39. package/lib/vendor/bimi-trust-anchors.pem +33 -0
  40. package/lib/vendor/public-suffix-list.dat +16376 -0
  41. package/package.json +1 -1
  42. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.middleware.clearSiteData
4
+ * @nav HTTP
5
+ * @title Clear-Site-Data
6
+ * @order 120
7
+ * @card RFC 9527 Clear-Site-Data middleware — wipe browser-side
8
+ * state (cookies, storage, cache, executionContexts) when
9
+ * a session ends. Mount on logout/erase routes; the
10
+ * header tells the UA to drop everything before navigating
11
+ * away so the next request starts clean.
12
+ *
13
+ * @intro
14
+ * The framework's logout primitive should not just delete the
15
+ * server-side session — it should tell the user-agent to drop every
16
+ * browser-side trace too. RFC 9527 Clear-Site-Data is the header
17
+ * that does it: the UA sees the response and synchronously evicts
18
+ * the named state types BEFORE running any subsequent navigation
19
+ * code, so a stale tab doesn't leak post-logout requests carrying
20
+ * the previous user's cookies.
21
+ *
22
+ * Common shape on a logout endpoint:
23
+ *
24
+ * app.post("/logout", [
25
+ * b.middleware.requireAuth(),
26
+ * async function (req, res) {
27
+ * await req.session.destroy();
28
+ * b.middleware.clearSiteData()(req, res, function () {});
29
+ * res.redirect("/");
30
+ * },
31
+ * ]);
32
+ *
33
+ * Or as drop-in middleware on every route under a path prefix:
34
+ *
35
+ * app.use("/account/erase", b.middleware.clearSiteData());
36
+ *
37
+ * Default types: `cookies`, `storage`, `cache`, `executionContexts`.
38
+ * Operators wanting a narrower wipe (e.g. only `cache`) pass
39
+ * `{ types: ["cache"] }`. Wildcard `"*"` is supported but discouraged
40
+ * — it tells the UA to wipe the whole origin including cross-tab
41
+ * service workers, which often surprises operators.
42
+ */
43
+
44
+ var validateOpts = require("../validate-opts");
45
+
46
+ // RFC 9527 §3 — the canonical token set. `clientHints` was added in
47
+ // the 2024 revision; `executionContexts` reloads any documents the
48
+ // origin currently has open (closes XSS-style hijacked tabs).
49
+ var KNOWN_TYPES = {
50
+ "cookies": true,
51
+ "storage": true,
52
+ "cache": true,
53
+ "executionContexts": true,
54
+ "clientHints": true,
55
+ "*": true,
56
+ };
57
+
58
+ var DEFAULT_TYPES = ["cookies", "storage", "cache", "executionContexts"];
59
+
60
+ /**
61
+ * @primitive b.middleware.clearSiteData
62
+ * @signature b.middleware.clearSiteData(req, res, next)
63
+ * @since 0.8.53
64
+ * @status stable
65
+ * @related b.middleware.securityHeaders, b.session
66
+ *
67
+ * Builds middleware that emits an RFC 9527 Clear-Site-Data response
68
+ * header. Mount on logout / account-erase / consent-revoke routes
69
+ * so the user-agent wipes browser-side state synchronously before
70
+ * the next navigation. Without this header, a logged-out tab can
71
+ * still carry cookies and cached responses past the server-side
72
+ * session destruction, leaking post-logout requests.
73
+ *
74
+ * @opts
75
+ * {
76
+ * types: Array<"cookies"|"storage"|"cache"|"executionContexts"|"clientHints"|"*">,
77
+ * }
78
+ *
79
+ * @example
80
+ * var b = require("@blamejs/core");
81
+ * var app = b.router.create();
82
+ * app.post("/logout", [
83
+ * b.middleware.clearSiteData(),
84
+ * async function (req, res) {
85
+ * await req.session.destroy();
86
+ * res.redirect("/");
87
+ * },
88
+ * ]);
89
+ */
90
+ function create(opts) {
91
+ opts = opts || {};
92
+ validateOpts(opts, ["types"], "middleware.clearSiteData");
93
+ var types = opts.types === undefined ? DEFAULT_TYPES : opts.types;
94
+ if (!Array.isArray(types) || types.length === 0) {
95
+ throw new TypeError("middleware.clearSiteData: opts.types must be a non-empty array");
96
+ }
97
+ for (var i = 0; i < types.length; i += 1) {
98
+ var t = types[i];
99
+ if (typeof t !== "string" || !KNOWN_TYPES[t]) {
100
+ throw new TypeError(
101
+ "middleware.clearSiteData: unknown type '" + t +
102
+ "' (expected one of: " + Object.keys(KNOWN_TYPES).join(", ") + ")");
103
+ }
104
+ }
105
+ // Header value is a comma-separated list of double-quoted tokens
106
+ // per RFC 9527 §3 (Structured Field Value List of Strings). Build
107
+ // once at construction time — runtime cost is one setHeader call.
108
+ var headerValue = types.map(function (t) { return '"' + t + '"'; }).join(", ");
109
+
110
+ return function clearSiteData(req, res, next) {
111
+ if (typeof res.setHeader === "function") {
112
+ res.setHeader("Clear-Site-Data", headerValue);
113
+ }
114
+ next();
115
+ };
116
+ }
117
+
118
+ module.exports = {
119
+ create: create,
120
+ KNOWN_TYPES: Object.keys(KNOWN_TYPES),
121
+ DEFAULT_TYPES: DEFAULT_TYPES,
122
+ };
@@ -121,7 +121,7 @@ function _reconstructHtu(req) {
121
121
  * @primitive b.middleware.dpop
122
122
  * @signature b.middleware.dpop(opts)
123
123
  * @since 0.1.0
124
- * @related b.middleware.bearerAuth, b.auth.jwt
124
+ * @related b.middleware.bearerAuth
125
125
  *
126
126
  * RFC 9449 Demonstrating Proof of Possession (DPoP). Verifies the
127
127
  * `DPoP` header on inbound requests, attaches `req.dpop = { header,
@@ -25,6 +25,7 @@ var assetlinks = require("./assetlinks");
25
25
  var attachUser = require("./attach-user");
26
26
  var bearerAuth = require("./bearer-auth");
27
27
  var bodyParser = require("./body-parser");
28
+ var clearSiteData = require("./clear-site-data");
28
29
  var botDisclose = require("./bot-disclose");
29
30
  var botGuard = require("./bot-guard");
30
31
  var compression = require("./compression");
@@ -42,8 +43,10 @@ var gpc = require("./gpc");
42
43
  var headers = require("./headers");
43
44
  var health = require("./health");
44
45
  var hostAllowlist = require("./host-allowlist");
46
+ var nel = require("./nel");
45
47
  var networkAllowlist = require("./network-allowlist");
46
48
  var rateLimit = require("./rate-limit");
49
+ var speculationRules = require("./speculation-rules");
47
50
  var requestId = require("./request-id");
48
51
  var requestLog = require("./request-log");
49
52
  var requireAal = require("./require-aal");
@@ -108,6 +111,9 @@ module.exports = {
108
111
  tracePropagate: tracePropagate.create,
109
112
  tusUpload: tusUpload.create,
110
113
  webAppManifest: webAppManifest.create,
114
+ clearSiteData: clearSiteData.create,
115
+ nel: nel.create,
116
+ speculationRules: speculationRules.create,
111
117
 
112
118
  // Module exports for advanced use (constants, raw factory access)
113
119
  _modules: {
@@ -152,6 +158,9 @@ module.exports = {
152
158
  tracePropagate: tracePropagate,
153
159
  tusUpload: tusUpload,
154
160
  webAppManifest: webAppManifest,
161
+ clearSiteData: clearSiteData,
162
+ nel: nel,
163
+ speculationRules: speculationRules,
155
164
  },
156
165
  };
157
166
 
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.middleware.nel
4
+ * @nav HTTP
5
+ * @title Network Error Logging
6
+ * @order 125
7
+ * @card W3C Network Error Logging — emits the `NEL` and companion
8
+ * `Report-To` headers so user-agents post network-failure
9
+ * reports (DNS failures, TLS handshake errors, TCP resets,
10
+ * HTTP-error class samples) back to an operator-controlled
11
+ * collector. Pair with `b.middleware.cspReport` for a
12
+ * unified browser-side telemetry channel.
13
+ *
14
+ * @intro
15
+ * Network Error Logging (W3C draft) is the browser's native channel
16
+ * for surfacing failures the server never sees: TLS handshake
17
+ * collapse before the request body, DNS lookup misses, CDN routing
18
+ * resets, premature TCP teardown mid-response. The user-agent
19
+ * buffers these and POSTs JSON reports to a configured collector,
20
+ * keyed by the `report-to` group named in the `NEL` header.
21
+ *
22
+ * The middleware emits two response headers on every request it
23
+ * sees:
24
+ *
25
+ * Report-To: { "group": "default", "max_age": 86400, "endpoints":
26
+ * [ { "url": "https://collector.example.com/nel" } ] }
27
+ * NEL: { "report_to": "default", "max_age": 86400,
28
+ * "include_subdomains": false, "success_fraction": 0,
29
+ * "failure_fraction": 1 }
30
+ *
31
+ * Both header values are JSON dictionaries; the framework refuses
32
+ * any operator-supplied collector URL containing CR/LF/NUL so a
33
+ * typo can't smuggle a header-injection payload into the wire
34
+ * format.
35
+ *
36
+ * Mount AFTER `securityHeaders` (so the response writeHead order
37
+ * stays predictable) and BEFORE business middleware. Pair with
38
+ * `b.middleware.cspReport` so a single collector receives both NEL
39
+ * and CSP reports — operators commonly point both at the same
40
+ * `/_telemetry` endpoint.
41
+ *
42
+ * app.use(b.middleware.requestId());
43
+ * app.use(b.middleware.securityHeaders());
44
+ * app.use(b.middleware.nel({
45
+ * reportTo: "default",
46
+ * collectorUrl: "https://collector.example.com/nel",
47
+ * maxAge: 86400,
48
+ * includeSubdomains: false,
49
+ * successFraction: 0,
50
+ * failureFraction: 1,
51
+ * }));
52
+ *
53
+ * The `successFraction` defaults to 0 because reporting every
54
+ * successful request is a billing surprise on busy origins;
55
+ * operators tune it up (0.001, 0.01) when sampling success
56
+ * distribution intentionally.
57
+ */
58
+
59
+ var validateOpts = require("../validate-opts");
60
+ var C = require("../constants");
61
+
62
+ // Per W3C draft + the practical browser implementations. successFraction
63
+ // = 1.0 reports every request — fine for a low-traffic admin surface,
64
+ // catastrophic on a high-traffic CDN. failureFraction = 1.0 is the
65
+ // security-correct default; operators only lower it when they have a
66
+ // downstream rate-limit on the collector.
67
+ var DEFAULT_REPORT_GROUP = "default";
68
+ var DEFAULT_MAX_AGE = C.TIME.hours(24) / C.TIME.seconds(1); // NEL header takes seconds
69
+ var DEFAULT_SUCCESS_FRACTION = 0;
70
+ var DEFAULT_FAILURE_FRACTION = 1;
71
+
72
+ // Header injection defense — every operator-supplied string that
73
+ // reaches a header value is screened for CR/LF/NUL. The collector
74
+ // URL flows into JSON inside Report-To; a CR there would let an
75
+ // attacker forge an arbitrary follow-up header on stacks that
76
+ // concatenate header lines naively.
77
+ var INJECTION_RE = /[\r\n\0]/;
78
+
79
+ function _refuseInjection(value, label) {
80
+ if (typeof value !== "string") return;
81
+ if (INJECTION_RE.test(value)) { // allow:regex-no-length-cap — CR/LF/NUL injection check, length bounded by caller
82
+ throw new TypeError(
83
+ "middleware.nel: " + label + " contains CR/LF/NUL — refused as a " +
84
+ "header-injection vector");
85
+ }
86
+ }
87
+
88
+ /**
89
+ * @primitive b.middleware.nel
90
+ * @signature b.middleware.nel(req, res, next)
91
+ * @since 0.8.53
92
+ * @status stable
93
+ * @related b.middleware.cspReport, b.middleware.securityHeaders
94
+ *
95
+ * Builds middleware that emits the W3C Network Error Logging `NEL`
96
+ * and companion `Report-To` headers so user-agents post failure
97
+ * telemetry back to an operator-controlled collector. Mount near the
98
+ * top of the chain (after `requestId` and `securityHeaders`) so every
99
+ * response carries the headers — NEL is a long-lived browser policy,
100
+ * not a per-route concern.
101
+ *
102
+ * The two header bodies are JSON dictionaries built once at construct
103
+ * time. Operator-supplied strings flow through a CR/LF/NUL refusal
104
+ * check so a typo in `collectorUrl` can't smuggle additional headers
105
+ * onto the wire.
106
+ *
107
+ * @opts
108
+ * {
109
+ * reportTo: string, // group name (default "default")
110
+ * collectorUrl: string, // required — collector POST URL
111
+ * maxAge: number, // policy lifetime in seconds (default 86400)
112
+ * includeSubdomains: boolean, // default false
113
+ * successFraction: number, // 0..1, default 0
114
+ * failureFraction: number, // 0..1, default 1
115
+ * }
116
+ *
117
+ * @example
118
+ * var b = require("@blamejs/core");
119
+ * var app = b.router.create();
120
+ * app.use(b.middleware.requestId());
121
+ * app.use(b.middleware.securityHeaders());
122
+ * app.use(b.middleware.nel({
123
+ * collectorUrl: "https://collector.example.com/nel",
124
+ * maxAge: 86400,
125
+ * successFraction: 0,
126
+ * failureFraction: 1,
127
+ * }));
128
+ */
129
+ function create(opts) {
130
+ opts = opts || {};
131
+ validateOpts(opts, [
132
+ "reportTo", "collectorUrl", "maxAge",
133
+ "includeSubdomains", "successFraction", "failureFraction",
134
+ ], "middleware.nel");
135
+
136
+ // Per-file allowlist in test/layer-0-primitives/codebase-patterns.test.js
137
+ // for inline-require-non-empty-string-validation — the operator-readable
138
+ // "collectorUrl is required" prose is part of the public test contract;
139
+ // validateOpts.requireNonEmptyString would emit a generic
140
+ // "validate-opts/missing-non-empty-string" message instead.
141
+ if (typeof opts.collectorUrl !== "string" || opts.collectorUrl.length === 0) {
142
+ throw new TypeError(
143
+ "middleware.nel: opts.collectorUrl is required (the URL the user-agent " +
144
+ "POSTs network-failure reports to)");
145
+ }
146
+ _refuseInjection(opts.collectorUrl, "opts.collectorUrl");
147
+
148
+ // The collector URL must be an https:// scheme — browsers only
149
+ // honor secure-origin report endpoints. Refusing at config-time so
150
+ // an operator typo (`http://`) surfaces at boot, not as silent
151
+ // never-fires-in-production.
152
+ if (opts.collectorUrl.slice(0, 8) !== "https://") { // allow:raw-byte-literal — string-prefix length, not bytes
153
+ throw new TypeError(
154
+ "middleware.nel: opts.collectorUrl must be https:// (browsers " +
155
+ "ignore non-secure NEL collectors); got " + opts.collectorUrl);
156
+ }
157
+
158
+ var reportTo = opts.reportTo === undefined ? DEFAULT_REPORT_GROUP : opts.reportTo;
159
+ if (typeof reportTo !== "string" || reportTo.length === 0) {
160
+ throw new TypeError("middleware.nel: opts.reportTo must be a non-empty string");
161
+ }
162
+ _refuseInjection(reportTo, "opts.reportTo");
163
+
164
+ var maxAge = opts.maxAge === undefined ? DEFAULT_MAX_AGE : opts.maxAge;
165
+ if (typeof maxAge !== "number" || !isFinite(maxAge) || maxAge < 0) {
166
+ throw new TypeError("middleware.nel: opts.maxAge must be a non-negative finite number (seconds)");
167
+ }
168
+
169
+ var includeSubdomains = opts.includeSubdomains === undefined ? false : !!opts.includeSubdomains;
170
+
171
+ var successFraction = opts.successFraction === undefined ? DEFAULT_SUCCESS_FRACTION : opts.successFraction;
172
+ if (typeof successFraction !== "number" || !isFinite(successFraction) ||
173
+ successFraction < 0 || successFraction > 1) {
174
+ throw new TypeError("middleware.nel: opts.successFraction must be a number in [0, 1]");
175
+ }
176
+
177
+ var failureFraction = opts.failureFraction === undefined ? DEFAULT_FAILURE_FRACTION : opts.failureFraction;
178
+ if (typeof failureFraction !== "number" || !isFinite(failureFraction) ||
179
+ failureFraction < 0 || failureFraction > 1) {
180
+ throw new TypeError("middleware.nel: opts.failureFraction must be a number in [0, 1]");
181
+ }
182
+
183
+ // Build the two header values once at construct time. JSON.stringify
184
+ // produces the canonical compact form; the property ordering is
185
+ // stable per V8 spec.
186
+ var reportToHeader = JSON.stringify({
187
+ group: reportTo,
188
+ max_age: maxAge,
189
+ endpoints: [{ url: opts.collectorUrl }],
190
+ });
191
+ var nelHeader = JSON.stringify({
192
+ report_to: reportTo,
193
+ max_age: maxAge,
194
+ include_subdomains: includeSubdomains,
195
+ success_fraction: successFraction,
196
+ failure_fraction: failureFraction,
197
+ });
198
+
199
+ return function nel(req, res, next) {
200
+ if (typeof res.setHeader === "function") {
201
+ res.setHeader("Report-To", reportToHeader);
202
+ res.setHeader("NEL", nelHeader);
203
+ }
204
+ next();
205
+ };
206
+ }
207
+
208
+ module.exports = {
209
+ create: create,
210
+ DEFAULT_REPORT_GROUP: DEFAULT_REPORT_GROUP,
211
+ DEFAULT_MAX_AGE: DEFAULT_MAX_AGE,
212
+ DEFAULT_SUCCESS_FRACTION: DEFAULT_SUCCESS_FRACTION,
213
+ DEFAULT_FAILURE_FRACTION: DEFAULT_FAILURE_FRACTION,
214
+ };
@@ -76,12 +76,51 @@ var DEFAULT_CSP =
76
76
  "font-src 'self'; " +
77
77
  "connect-src 'self'; " +
78
78
  "frame-ancestors 'none'; " +
79
+ // CSP3 fenced-frame-src: refuse <fencedframe> embeds entirely. The
80
+ // Privacy-Sandbox-era element bypasses traditional frame controls;
81
+ // operators wanting to embed a Privacy-Sandbox vendor opt in by
82
+ // passing their own csp.
83
+ "fenced-frame-src 'none'; " +
79
84
  "base-uri 'self'; " +
80
85
  "form-action 'self'; " +
81
86
  "object-src 'none'; " +
82
87
  "require-trusted-types-for 'script'; " +
83
88
  "trusted-types 'allow-duplicates' default;";
84
89
 
90
+ // Document-Policy default — denies the highest-risk DOM/JS surfaces
91
+ // that aren't otherwise covered by Permissions-Policy. `unsized-media`
92
+ // blocks layout-jank from images without explicit width/height,
93
+ // `oversized-images` caps the served-vs-displayed ratio, and the
94
+ // document-write feature disables the legacy synchronous DOM-injection
95
+ // API. Operators with a third-party widget that needs the legacy API
96
+ // override.
97
+ var DEFAULT_DOCUMENT_POLICY =
98
+ "document-write=?0, " +
99
+ "unsized-media=?0, " +
100
+ "oversized-images=?0";
101
+
102
+ // RFC 9651 (Structured Field Values) Permissions-Policy validation —
103
+ // each policy is `feature=value-list` where value-list is `*` /
104
+ // `(self)` / `(self "https://...")` / `()` (empty = deny). Reject
105
+ // header values that don't conform; operators get a clear refusal at
106
+ // boot rather than a silently-broken header at runtime.
107
+ var PP_POLICY_RE =
108
+ /^[a-z][a-z0-9-]*=(?:\*|\([^)]*\)|self)$/;
109
+ function _validatePermissionsPolicy(value) {
110
+ if (typeof value !== "string" || value.length === 0) return;
111
+ var parts = String(value).split(/\s*,\s*/);
112
+ for (var i = 0; i < parts.length; i += 1) {
113
+ var p = parts[i];
114
+ if (!p) continue;
115
+ if (!PP_POLICY_RE.test(p)) { // allow:regex-no-length-cap — RFC 9651 SF entries are bounded by browser parsers; operator-supplied
116
+ throw new TypeError(
117
+ "middleware.securityHeaders: permissionsPolicy entry '" + p +
118
+ "' is not a valid RFC 9651 structured field (expected " +
119
+ "'feature=*' / 'feature=()' / 'feature=(self ...)')");
120
+ }
121
+ }
122
+ }
123
+
85
124
  /**
86
125
  * @primitive b.middleware.securityHeaders
87
126
  * @signature b.middleware.securityHeaders(req, res, next)
@@ -115,6 +154,9 @@ var DEFAULT_CSP =
115
154
  * originAgentCluster: "?1"|"?0"|false,
116
155
  * dnsPrefetchControl: "off"|"on"|false,
117
156
  * csp: string|false,
157
+ * documentPolicy: string|false,
158
+ * acceptCh: string|false,
159
+ * criticalCh: string|false,
118
160
  * reportingEndpoints: object,
119
161
  * trustProxy: boolean|number,
120
162
  * }
@@ -132,8 +174,11 @@ function create(opts) {
132
174
  "hsts", "contentTypeOptions", "frameOptions", "referrerPolicy",
133
175
  "permissionsPolicy", "coop", "coep", "corp",
134
176
  "originAgentCluster", "dnsPrefetchControl", "csp", "trustProxy",
135
- "reportingEndpoints",
177
+ "reportingEndpoints", "documentPolicy", "criticalCh", "acceptCh",
136
178
  ], "middleware.securityHeaders");
179
+ if (opts.permissionsPolicy && typeof opts.permissionsPolicy === "string") {
180
+ _validatePermissionsPolicy(opts.permissionsPolicy);
181
+ }
137
182
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
138
183
  ? opts.trustProxy : false;
139
184
  var hsts = opts.hsts === undefined ? "max-age=63072000; includeSubDomains; preload" : opts.hsts;
@@ -147,6 +192,9 @@ function create(opts) {
147
192
  var oac = opts.originAgentCluster === undefined ? "?1" : opts.originAgentCluster;
148
193
  var dpc = opts.dnsPrefetchControl === undefined ? "off" : opts.dnsPrefetchControl;
149
194
  var csp = opts.csp === undefined ? DEFAULT_CSP : opts.csp;
195
+ var docPolicy = opts.documentPolicy === undefined ? DEFAULT_DOCUMENT_POLICY : opts.documentPolicy;
196
+ var criticalCh = opts.criticalCh && typeof opts.criticalCh === "string" ? opts.criticalCh : false;
197
+ var acceptCh = opts.acceptCh && typeof opts.acceptCh === "string" ? opts.acceptCh : false;
150
198
  // Reporting-Endpoints (W3C Reporting API) — when operator passes a
151
199
  // map of endpoint-name → URL, we emit `Reporting-Endpoints: name="url",
152
200
  // name2="url2", ...` and (when default CSP is in force) append
@@ -194,13 +242,17 @@ function create(opts) {
194
242
  if (oac) res.setHeader("Origin-Agent-Cluster", oac);
195
243
  if (dpc) res.setHeader("X-DNS-Prefetch-Control", dpc);
196
244
  if (csp) res.setHeader("Content-Security-Policy", csp);
245
+ if (docPolicy) res.setHeader("Document-Policy", docPolicy);
246
+ if (acceptCh) res.setHeader("Accept-CH", acceptCh);
247
+ if (criticalCh) res.setHeader("Critical-CH", criticalCh);
197
248
  if (reportingEndpoints) res.setHeader("Reporting-Endpoints", reportingEndpoints);
198
249
  next();
199
250
  };
200
251
  }
201
252
 
202
253
  module.exports = {
203
- create: create,
204
- DEFAULT_PERMISSIONS: DEFAULT_PERMISSIONS,
205
- DEFAULT_CSP: DEFAULT_CSP,
254
+ create: create,
255
+ DEFAULT_PERMISSIONS: DEFAULT_PERMISSIONS,
256
+ DEFAULT_CSP: DEFAULT_CSP,
257
+ DEFAULT_DOCUMENT_POLICY: DEFAULT_DOCUMENT_POLICY,
206
258
  };