@blamejs/core 0.7.107 → 0.8.4

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 (100) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +15 -0
  5. package/lib/asyncapi-bindings.js +160 -0
  6. package/lib/asyncapi-traits.js +143 -0
  7. package/lib/asyncapi.js +531 -0
  8. package/lib/audit-sign.js +1 -1
  9. package/lib/audit.js +68 -2
  10. package/lib/auth/acr-vocabulary.js +265 -0
  11. package/lib/auth/auth-time-tracker.js +111 -0
  12. package/lib/auth/elevation-grant.js +306 -0
  13. package/lib/auth/jwt.js +13 -0
  14. package/lib/auth/lockout.js +16 -3
  15. package/lib/auth/oauth.js +15 -1
  16. package/lib/auth/password.js +22 -2
  17. package/lib/auth/sd-jwt-vc-issuer.js +2 -2
  18. package/lib/auth/sd-jwt-vc.js +7 -2
  19. package/lib/auth/step-up-policy.js +335 -0
  20. package/lib/auth/step-up.js +445 -0
  21. package/lib/break-glass.js +53 -14
  22. package/lib/cache-redis.js +1 -1
  23. package/lib/cache.js +6 -1
  24. package/lib/cli.js +3 -3
  25. package/lib/cluster.js +24 -1
  26. package/lib/compliance-ai-act-logging.js +190 -0
  27. package/lib/compliance-ai-act-prohibited.js +205 -0
  28. package/lib/compliance-ai-act-risk.js +189 -0
  29. package/lib/compliance-ai-act-transparency.js +200 -0
  30. package/lib/compliance-ai-act.js +558 -0
  31. package/lib/compliance.js +12 -2
  32. package/lib/config-drift.js +2 -2
  33. package/lib/crypto-field.js +21 -1
  34. package/lib/crypto.js +114 -1
  35. package/lib/db.js +35 -4
  36. package/lib/dev.js +30 -3
  37. package/lib/dual-control.js +19 -1
  38. package/lib/external-db.js +10 -0
  39. package/lib/file-upload.js +30 -3
  40. package/lib/flag-cache.js +136 -0
  41. package/lib/flag-evaluation-context.js +135 -0
  42. package/lib/flag-providers.js +279 -0
  43. package/lib/flag-targeting.js +210 -0
  44. package/lib/flag.js +284 -0
  45. package/lib/guard-all.js +33 -16
  46. package/lib/guard-csv.js +16 -2
  47. package/lib/guard-html.js +35 -0
  48. package/lib/guard-svg.js +20 -0
  49. package/lib/http-client.js +57 -11
  50. package/lib/inbox.js +391 -0
  51. package/lib/log-stream-syslog.js +8 -0
  52. package/lib/log-stream.js +1 -1
  53. package/lib/mail-arc-sign.js +372 -0
  54. package/lib/mail-auth.js +2 -0
  55. package/lib/mail.js +40 -0
  56. package/lib/middleware/ai-act-disclosure.js +166 -0
  57. package/lib/middleware/asyncapi-serve.js +136 -0
  58. package/lib/middleware/attach-user.js +25 -2
  59. package/lib/middleware/bearer-auth.js +71 -6
  60. package/lib/middleware/body-parser.js +13 -0
  61. package/lib/middleware/cors.js +10 -0
  62. package/lib/middleware/csrf-protect.js +34 -3
  63. package/lib/middleware/dpop.js +3 -3
  64. package/lib/middleware/flag-context.js +76 -0
  65. package/lib/middleware/host-allowlist.js +1 -1
  66. package/lib/middleware/index.js +15 -0
  67. package/lib/middleware/openapi-serve.js +143 -0
  68. package/lib/middleware/require-aal.js +2 -2
  69. package/lib/middleware/require-step-up.js +186 -0
  70. package/lib/middleware/trace-propagate.js +1 -1
  71. package/lib/mtls-ca.js +23 -29
  72. package/lib/mtls-engine-default.js +21 -1
  73. package/lib/network-tls.js +21 -6
  74. package/lib/object-store/sigv4-bucket-ops.js +41 -0
  75. package/lib/observability-otlp-exporter.js +35 -2
  76. package/lib/openapi-paths-builder.js +248 -0
  77. package/lib/openapi-schema-walk.js +192 -0
  78. package/lib/openapi-security.js +169 -0
  79. package/lib/openapi-yaml.js +154 -0
  80. package/lib/openapi.js +443 -0
  81. package/lib/outbox.js +3 -3
  82. package/lib/permissions.js +10 -1
  83. package/lib/pqc-agent.js +22 -1
  84. package/lib/pqc-software.js +195 -0
  85. package/lib/pubsub.js +8 -4
  86. package/lib/redact.js +26 -1
  87. package/lib/retention.js +26 -0
  88. package/lib/router.js +1 -0
  89. package/lib/scheduler.js +57 -1
  90. package/lib/session.js +3 -3
  91. package/lib/ssrf-guard.js +19 -4
  92. package/lib/static.js +12 -0
  93. package/lib/totp.js +16 -0
  94. package/lib/vault/index.js +3 -0
  95. package/lib/vault-aad.js +259 -0
  96. package/lib/vendor/MANIFEST.json +29 -0
  97. package/lib/vendor/noble-post-quantum.cjs +18 -0
  98. package/lib/ws-client.js +978 -0
  99. package/package.json +1 -1
  100. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * asyncapi-serve middleware — expose an AsyncAPI 3.0 document at a
4
+ * mount point.
5
+ *
6
+ * var aapi = b.asyncapi.create({ ... });
7
+ * ...add channels / operations / schemas / security...
8
+ *
9
+ * router.use(b.middleware.asyncapiServe({
10
+ * document: aapi,
11
+ * pathJson: "/asyncapi.json",
12
+ * pathYaml: "/asyncapi.yaml",
13
+ * pretty: true,
14
+ * accessControl: "public",
15
+ * }));
16
+ *
17
+ * Behaviour matches openapiServe: GET / HEAD only, SHA3-512 ETag with
18
+ * conditional 304, configurable CORS gate, falls through on other
19
+ * paths / methods.
20
+ */
21
+
22
+ var nodeCrypto = require("crypto");
23
+ var validateOpts = require("../validate-opts");
24
+ var lazyRequire = require("../lazy-require");
25
+ var { defineClass } = require("../framework-error");
26
+ var AsyncApiError = defineClass("AsyncApiError", { alwaysPermanent: true });
27
+
28
+ var openapiYaml = lazyRequire(function () { return require("../openapi-yaml"); });
29
+ var audit = lazyRequire(function () { return require("../audit"); });
30
+
31
+ function create(opts) {
32
+ opts = opts || {};
33
+ validateOpts(opts, [
34
+ "document", "pathJson", "pathYaml", "pretty",
35
+ "cacheControl", "accessControl", "audit",
36
+ ], "middleware.asyncapiServe");
37
+ if (!opts.document || typeof opts.document.toJson !== "function") {
38
+ throw new AsyncApiError("asyncapi/bad-document",
39
+ "asyncapiServe: document must be a builder created via b.asyncapi.create()");
40
+ }
41
+ var pathJson = opts.pathJson || "/asyncapi.json";
42
+ var pathYaml = opts.pathYaml || "/asyncapi.yaml";
43
+ var pretty = opts.pretty === true ? 2 : 0;
44
+ var cacheControl = (typeof opts.cacheControl === "string" && opts.cacheControl.length > 0)
45
+ ? opts.cacheControl : "public, max-age=300";
46
+ var accessControl = opts.accessControl || "public";
47
+ var auditOn = opts.audit !== false;
48
+
49
+ if (typeof pathJson !== "string" || pathJson.charAt(0) !== "/") {
50
+ throw new AsyncApiError("asyncapi/bad-path",
51
+ "asyncapiServe: pathJson must start with '/'");
52
+ }
53
+ if (typeof pathYaml !== "string" || pathYaml.charAt(0) !== "/") {
54
+ throw new AsyncApiError("asyncapi/bad-path",
55
+ "asyncapiServe: pathYaml must start with '/'");
56
+ }
57
+
58
+ var cachedJsonStr = null;
59
+ var cachedYamlStr = null;
60
+ var cachedJsonEtag = null;
61
+ var cachedYamlEtag = null;
62
+
63
+ function _rebuild() {
64
+ var doc = opts.document.toJson();
65
+ cachedJsonStr = JSON.stringify(doc, null, pretty);
66
+ cachedYamlStr = openapiYaml().toYaml(doc);
67
+ cachedJsonEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedJsonStr).digest("base64url").slice(0, 24) + '"';
68
+ cachedYamlEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedYamlStr).digest("base64url").slice(0, 24) + '"';
69
+ }
70
+ _rebuild();
71
+
72
+ function _writeBody(req, res, body, etag, contentType) {
73
+ var requestEtag = (req.headers && req.headers["if-none-match"]) || null;
74
+ if (requestEtag && requestEtag === etag) {
75
+ res.writeHead(304, { "ETag": etag, "Cache-Control": cacheControl }); // allow:raw-byte-literal — HTTP 304
76
+ res.end();
77
+ return;
78
+ }
79
+ var headers = {
80
+ "Content-Type": contentType,
81
+ "Content-Length": Buffer.byteLength(body),
82
+ "Cache-Control": cacheControl,
83
+ "ETag": etag,
84
+ };
85
+ if (accessControl === "public") {
86
+ headers["Access-Control-Allow-Origin"] = "*";
87
+ }
88
+ res.writeHead(200, headers); // allow:raw-byte-literal — HTTP 200
89
+ res.end(body);
90
+ }
91
+
92
+ var mw = function (req, res, next) {
93
+ if (typeof res.writeHead !== "function") return next();
94
+ var method = (req.method || "GET").toUpperCase();
95
+ if (method !== "GET" && method !== "HEAD") return next();
96
+ var pathname = req.pathname;
97
+ if (typeof pathname !== "string") {
98
+ var url = req.url || "";
99
+ var qIdx = url.indexOf("?");
100
+ pathname = qIdx === -1 ? url : url.slice(0, qIdx);
101
+ }
102
+ if (pathname === pathJson) {
103
+ _writeBody(req, res, cachedJsonStr, cachedJsonEtag, "application/json; charset=utf-8");
104
+ if (auditOn) {
105
+ try {
106
+ audit().safeEmit({
107
+ action: "asyncapi.document.served",
108
+ outcome: "success",
109
+ actor: null,
110
+ metadata: { format: "json", path: pathname, bytes: cachedJsonStr.length },
111
+ });
112
+ } catch (_e) { /* drop-silent */ }
113
+ }
114
+ return;
115
+ }
116
+ if (pathname === pathYaml) {
117
+ _writeBody(req, res, cachedYamlStr, cachedYamlEtag, "application/yaml; charset=utf-8");
118
+ if (auditOn) {
119
+ try {
120
+ audit().safeEmit({
121
+ action: "asyncapi.document.served",
122
+ outcome: "success",
123
+ actor: null,
124
+ metadata: { format: "yaml", path: pathname, bytes: cachedYamlStr.length },
125
+ });
126
+ } catch (_e) { /* drop-silent */ }
127
+ }
128
+ return;
129
+ }
130
+ return next();
131
+ };
132
+ mw.forceRebuild = _rebuild;
133
+ return mw;
134
+ }
135
+
136
+ module.exports = { create: create };
@@ -67,6 +67,7 @@ function create(opts) {
67
67
  opts = opts || {};
68
68
  validateOpts(opts, [
69
69
  "cookieName", "tokenFrom", "sealed", "vault", "userLoader", "audit",
70
+ "requireFingerprintMatch", "maxAnomalyScore", "scorer",
70
71
  ], "middleware.attachUser");
71
72
  if (typeof opts.userLoader !== "function") {
72
73
  throw new Error("middleware.attachUser: opts.userLoader is required " +
@@ -76,6 +77,17 @@ function create(opts) {
76
77
  var tokenFrom = opts.tokenFrom || "both";
77
78
  var auditOn = opts.audit !== false;
78
79
  var sealed = !!opts.sealed;
80
+ // Fingerprint-drift / IP-UA pin / anomaly-score opts thread through
81
+ // session.verify so the documented session.create({ req,
82
+ // fingerprintFields }) defenses actually engage on every verify
83
+ // through the standard middleware path. Without this they were inert
84
+ // — an operator who set them at session.create only got the signal,
85
+ // not enforcement, when the session was checked through attachUser.
86
+ var verifyOpts = {
87
+ requireFingerprintMatch: opts.requireFingerprintMatch === true,
88
+ maxAnomalyScore: (typeof opts.maxAnomalyScore === "number") ? opts.maxAnomalyScore : null,
89
+ scorer: (typeof opts.scorer === "function") ? opts.scorer : null,
90
+ };
79
91
  if (sealed && (!opts.vault || typeof opts.vault.unseal !== "function")) {
80
92
  throw new Error("middleware.attachUser: opts.sealed requires opts.vault " +
81
93
  "with a .unseal method (typically b.vault)");
@@ -94,14 +106,25 @@ function create(opts) {
94
106
  ? cookieJar.readSealed(req, cookieName)
95
107
  : _readCookie(req.headers && req.headers.cookie, cookieName);
96
108
  }
97
- if (!token && (tokenFrom === "header" || tokenFrom === "both")) {
109
+ if (!token && (tokenFrom === "header" || tokenFrom === "both") &&
110
+ !req._bearerAuthHandled) {
111
+ // bearer-auth (when mounted upstream) sets req._bearerAuthHandled
112
+ // after consuming + verifying the Authorization header. Skipping
113
+ // the header re-read here avoids the duplicate verify and the
114
+ // confusing "session.verify failed" audit row that would land
115
+ // when the bearer token is a JWT or API key, not a session ID.
98
116
  token = _readBearer(req.headers && req.headers.authorization);
99
117
  }
100
118
  if (!token) return next();
101
119
 
102
120
  var verified;
103
121
  try {
104
- verified = await session().verify(token);
122
+ verified = await session().verify(token, {
123
+ req: req,
124
+ requireFingerprintMatch: verifyOpts.requireFingerprintMatch,
125
+ maxAnomalyScore: verifyOpts.maxAnomalyScore,
126
+ scorer: verifyOpts.scorer,
127
+ });
105
128
  } catch (_e) {
106
129
  // session.verify is tolerant — shouldn't normally throw, but if it
107
130
  // does (DB hiccup), don't propagate; treat as "no user" and let
@@ -54,14 +54,28 @@ function _writeUnauthorized(res, scheme, message, realm) {
54
54
  res.end(body);
55
55
  }
56
56
 
57
+ // Three-state extractor: { state: "absent" } when no Authorization
58
+ // header was sent, { state: "malformed" } when one is present but
59
+ // doesn't parse against this middleware's scheme, or { state: "ok",
60
+ // token } on success. The "malformed" case must NOT fall through to
61
+ // downstream auth (cookie-session) — operators relying on bearer-auth
62
+ // expect a 401 when a client deliberately sends `Authorization: ...`
63
+ // even if the value is unparseable.
57
64
  function _extractToken(req, scheme) {
58
65
  var h = req.headers && req.headers.authorization;
59
- if (typeof h !== "string" || h.length === 0) return null;
66
+ if (typeof h !== "string" || h.length === 0) return { state: "absent" };
60
67
  var prefix = scheme + " ";
61
- if (h.length <= prefix.length) return null;
62
- if (h.slice(0, prefix.length).toLowerCase() !== prefix.toLowerCase()) return null;
68
+ if (h.length <= prefix.length) return { state: "malformed" };
69
+ if (h.slice(0, prefix.length).toLowerCase() !== prefix.toLowerCase()) {
70
+ // Authorization header is for a different scheme (Basic, Digest,
71
+ // Negotiate, etc.) — leave the request for the next middleware
72
+ // that handles that scheme. From this middleware's perspective,
73
+ // it's effectively "absent."
74
+ return { state: "absent" };
75
+ }
63
76
  var token = h.slice(prefix.length).trim();
64
- return token.length > 0 ? token : null;
77
+ if (token.length === 0) return { state: "malformed" };
78
+ return { state: "ok", token: token };
65
79
  }
66
80
 
67
81
  function create(opts) {
@@ -80,6 +94,29 @@ function create(opts) {
80
94
  var scheme = opts.scheme || "Bearer";
81
95
  var errorMessage = opts.errorMessage || "Bearer token required.";
82
96
  var realm = opts.realm || null;
97
+ // CRLF-injection defense on operator-supplied realm — without this,
98
+ // a config-fed realm like `api\r\nX-Inject: 1` lands in the
99
+ // WWW-Authenticate response header verbatim. RFC 7235 §2.2 quoted-
100
+ // string excludes CTLs (codepoints < 0x20 and 0x7F) and the literal
101
+ // `"` / `\` characters.
102
+ if (realm !== null) {
103
+ if (typeof realm !== "string") {
104
+ throw new AuthError("auth-bearer/bad-realm",
105
+ "middleware.bearerAuth: realm must be a string");
106
+ }
107
+ for (var ri = 0; ri < realm.length; ri += 1) {
108
+ var rcode = realm.charCodeAt(ri);
109
+ if (rcode < 32 || rcode === 127) { // allow:raw-byte-literal — ASCII control codepoints
110
+ throw new AuthError("auth-bearer/bad-realm",
111
+ "realm contains control character at index " + ri);
112
+ }
113
+ var rchar = realm.charAt(ri);
114
+ if (rchar === '"' || rchar === "\\") {
115
+ throw new AuthError("auth-bearer/bad-realm",
116
+ "realm contains illegal character " + JSON.stringify(rchar) + " at index " + ri);
117
+ }
118
+ }
119
+ }
83
120
  var tokenAttach = opts.tokenAttachKey || "bearerToken";
84
121
  var userAttach = opts.userAttachKey || "user";
85
122
 
@@ -104,12 +141,33 @@ function create(opts) {
104
141
  }
105
142
 
106
143
  return async function bearerAuth(req, res, next) {
107
- var token = _extractToken(req, scheme);
108
- if (!token) {
144
+ var extracted = _extractToken(req, scheme);
145
+ if (extracted.state === "absent") {
109
146
  // No Bearer header — fall through. Cookie-based session middleware
110
147
  // running after this can attach a user via the cookie path.
111
148
  return next();
112
149
  }
150
+ if (extracted.state === "malformed") {
151
+ // Authorization header present but does not parse against this
152
+ // scheme. Refuse with 401 — the request is unambiguously trying
153
+ // to authenticate via bearer, and falling through to cookie-auth
154
+ // would mask the operator's malformed-input bug.
155
+ _emitAudit("auth.bearer.failure", "failure", req, "malformed-authorization");
156
+ _emitObs("auth.bearer.rejected", 1, { reason: "malformed-authorization" });
157
+ if (!res.headersSent) {
158
+ var malformedChallenge = scheme + ' error="invalid_request"' +
159
+ (realm ? ', realm="' + realm + '"' : "");
160
+ var malformedBody = JSON.stringify({ error: errorMessage });
161
+ res.writeHead(401, { // allow:raw-byte-literal — HTTP 401 status
162
+ "Content-Type": "application/json; charset=utf-8",
163
+ "Content-Length": Buffer.byteLength(malformedBody),
164
+ "WWW-Authenticate": malformedChallenge,
165
+ });
166
+ res.end(malformedBody);
167
+ }
168
+ return;
169
+ }
170
+ var token = extracted.token;
113
171
 
114
172
  var user;
115
173
  try {
@@ -143,6 +201,13 @@ function create(opts) {
143
201
 
144
202
  req[tokenAttach] = token;
145
203
  req[userAttach] = user;
204
+ // Signal to attach-user (and any other downstream auth middleware)
205
+ // that this Authorization header has already been consumed and
206
+ // verified — without this flag, attach-user would re-read the
207
+ // header and try to parse it as a session token, producing a
208
+ // confusing "session.verify-tried-and-failed" audit row alongside
209
+ // the successful "auth.bearer.success" we just emitted.
210
+ req._bearerAuthHandled = true;
146
211
  _emitAudit("auth.bearer.success", "success", req, null);
147
212
  _emitObs("auth.bearer.accepted", 1, {});
148
213
  next();
@@ -715,6 +715,19 @@ async function _parseMultipart(req, opts, ctParams) {
715
715
  }
716
716
  return;
717
717
  }
718
+ // Count the per-part header bytes toward totalSize so a
719
+ // burst of small parts can't slip past the request-level
720
+ // cap. Without this, fileCount: 20 + fieldCount: 100
721
+ // gives an attacker ~120 × 16 KiB = ~1.9 MiB of pending
722
+ // header state per request, multiplied across concurrent
723
+ // requests.
724
+ totalRead += headEnd + 4;
725
+ if (totalRead > totalSize) {
726
+ done(new BodyParserError("body-parser/multipart-too-large",
727
+ "multipart total request size exceeds totalSize (" + totalSize + ")",
728
+ true, HTTP_STATUS.PAYLOAD_TOO_LARGE));
729
+ return;
730
+ }
718
731
  currentHeaders = _parseMultipartHeaders(pending.slice(0, headEnd).toString("utf8"));
719
732
  pending = pending.slice(headEnd + 4);
720
733
  // Decode Content-Disposition.
@@ -241,6 +241,16 @@ function create(opts) {
241
241
 
242
242
  var matched = _matchOrigin(origin, origins);
243
243
  if (!matched) {
244
+ // Always append Vary: Origin when the request carried an Origin
245
+ // header — otherwise downstream caches that previously cached a
246
+ // matched-origin response (with ACAO + Vary: Origin set) may
247
+ // serve the wrong cached entry to this unmatched-origin
248
+ // request, OR cache the no-CORS response and replay it for a
249
+ // future matched-origin request. Cheap; matches Fetch-spec
250
+ // discipline.
251
+ if (typeof res.setHeader === "function") {
252
+ try { requestHelpers.appendVary(res, "Origin"); } catch (_e) { /* best-effort */ }
253
+ }
244
254
  if (refuseUnknown) {
245
255
  try {
246
256
  audit().emit({
@@ -165,7 +165,7 @@ function _appendSetCookie(res, value) {
165
165
  // throws on file:// / data:// schemes which would crash the middleware
166
166
  // instead of refusing the request. URL constructor + try/catch is the
167
167
  // right shape for "is this URL well-formed and what's its origin?".
168
- function _checkOriginAllowed(req, allowedOrigins, isHttpsFn) {
168
+ function _checkOriginAllowed(req, allowedOrigins, isHttpsFn, requireOrigin) {
169
169
  var headers = req.headers || {};
170
170
  var origin = headers.origin;
171
171
  var referer = headers.referer;
@@ -175,6 +175,9 @@ function _checkOriginAllowed(req, allowedOrigins, isHttpsFn) {
175
175
  // gate doesn't add to it. Defense-in-depth against a stolen
176
176
  // cookie via a browser-rendered cross-origin fetch IS the value;
177
177
  // headless clients carry their own auth threat model.
178
+ if (requireOrigin === true) {
179
+ return { allowed: false, reason: "missing-origin-and-referer" };
180
+ }
178
181
  return null;
179
182
  }
180
183
 
@@ -229,6 +232,7 @@ function create(opts) {
229
232
  validateOpts(opts, [
230
233
  "cookie", "tokenLookup", "fieldName", "headerName", "methods", "audit",
231
234
  "trustProxy", "checkOrigin", "allowedOrigins", "requireJsonContentType",
235
+ "requireOrigin",
232
236
  ], "middleware.csrfProtect");
233
237
  var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
234
238
  ? opts.trustProxy : false;
@@ -277,6 +281,14 @@ function create(opts) {
277
281
  // SPA + classic form pages) leave this opt-out (default).
278
282
  var requireJsonCt = opts.requireJsonContentType === true;
279
283
 
284
+ // requireOrigin — when true, refuse state-changing requests that
285
+ // carry NO Origin/Referer at all. Default false (back-compat for
286
+ // server-to-server / curl callers). Operators on a browser-only
287
+ // route mount the middleware with `requireOrigin: true` so the
288
+ // documented "no headers = bypass for non-browser" pass-through
289
+ // is opt-in rather than silent.
290
+ var requireOriginOpt = opts.requireOrigin === true;
291
+
280
292
  // Cookie issuance config (only when opts.cookie is set).
281
293
  var cookieCfg = null;
282
294
  if (hasCookie) {
@@ -341,10 +353,29 @@ function create(opts) {
341
353
  var cookieName = _resolveCookieName(req);
342
354
  var cookies = _parseCookieHeader(req.headers && req.headers.cookie);
343
355
  var existing = cookies[cookieName];
344
- if (existing && /^[a-f0-9]{2,}$/.test(existing)) {
356
+ // Strict 64-hex-char check matches the byte-length of every token
357
+ // forms.generateCsrfToken() produces (CSRF_TOKEN_BYTES = 32 bytes
358
+ // → 64 hex chars). The previous {2,} floor accepted any 2-char
359
+ // hex string a sibling-subdomain XSS could plant on plain HTTP
360
+ // (cookie name falls back to `csrf` when the request isn't HTTPS,
361
+ // so the `__Host-` prefix safety doesn't apply). Attacker plants
362
+ // `csrf=ab` then submits matching X-CSRF-Token to bypass the
363
+ // double-submit gate.
364
+ if (existing && /^[a-f0-9]{64}$/.test(existing)) {
345
365
  req.csrfToken = existing;
346
366
  return existing;
347
367
  }
368
+ if (existing && !/^[a-f0-9]{64}$/.test(existing)) {
369
+ // Audit-emit so operators see when a planted/short cookie is
370
+ // refused — surfaces the attack class in compliance logs.
371
+ try {
372
+ audit().safeEmit({
373
+ action: "csrf.bad_cookie_value",
374
+ outcome: "denied",
375
+ metadata: { cookieName: cookieName, length: existing.length },
376
+ });
377
+ } catch (_e) { /* drop-silent */ }
378
+ }
348
379
  var fresh = forms.generateCsrfToken();
349
380
  var setCookie = _formatSetCookie(cookieName, fresh, {
350
381
  path: cookieCfg.path,
@@ -381,7 +412,7 @@ function create(opts) {
381
412
  // requests even when the token is valid (e.g. operator-mistaken
382
413
  // CORS configuration that exposes the cookie).
383
414
  if (checkOrigin) {
384
- var originReason = _checkOriginAllowed(req, allowedOrigins, _isHttps);
415
+ var originReason = _checkOriginAllowed(req, allowedOrigins, _isHttps, requireOriginOpt);
385
416
  if (originReason !== null) {
386
417
  _emitDenied(req, "origin/referer: " + originReason);
387
418
  return _writeReject(res, "CSRF cross-origin request refused.");
@@ -215,7 +215,7 @@ function create(opts) {
215
215
  audit().safeEmit({
216
216
  action: "auth.bearer.failure",
217
217
  actor: { clientIp: requestHelpers.clientIp(req) },
218
- outcome: "fail",
218
+ outcome: "failure",
219
219
  metadata: {
220
220
  method: "dpop",
221
221
  reason: (e && e.code) || "verify-failed",
@@ -245,7 +245,7 @@ function create(opts) {
245
245
  audit().safeEmit({
246
246
  action: "auth.bearer.failure",
247
247
  actor: { clientIp: requestHelpers.clientIp(req) },
248
- outcome: "fail",
248
+ outcome: "failure",
249
249
  metadata: { method: "dpop", reason: "stale-nonce", route: req.url },
250
250
  });
251
251
  } catch (_ignored) { /* drop-silent */ }
@@ -268,7 +268,7 @@ function create(opts) {
268
268
  audit().safeEmit({
269
269
  action: "auth.bearer.success",
270
270
  actor: { clientIp: requestHelpers.clientIp(req) },
271
- outcome: "ok",
271
+ outcome: "success",
272
272
  metadata: { method: "dpop", jkt: result.jkt, route: req.url },
273
273
  });
274
274
  } catch (_ignored) { /* drop-silent */ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * flag-context middleware — extracts an OpenFeature evaluation context
4
+ * onto the request so downstream handlers and multiple flag clients
5
+ * can read a consistent context without re-deriving it per call.
6
+ *
7
+ * var attachCtx = b.middleware.flagContext({
8
+ * userKey: "x-user-id", // header to pull targetingKey from
9
+ * extractAttributes: function (req) { // operator-supplied augmentation
10
+ * return { tenantId: req.tenantId, environment: process.env.NODE_ENV };
11
+ * },
12
+ * });
13
+ * router.use(attachCtx);
14
+ *
15
+ * // Downstream:
16
+ * var ctx = req.flagCtx; // readonly Frozen object
17
+ * var enabled = b.flagClient.getBoolean("foo", ctx);
18
+ *
19
+ * The middleware does NOT evaluate flags — it only constructs the
20
+ * context. Pair with `flag.middleware()` for the request-attached
21
+ * convenience accessor; or pass `req.flagCtx` directly to a flag
22
+ * client method for a more decoupled shape (e.g. when several flag
23
+ * clients with different providers share the same context).
24
+ */
25
+
26
+ var validateOpts = require("../validate-opts");
27
+ var lazyRequire = require("../lazy-require");
28
+ var { defineClass } = require("../framework-error");
29
+ var FlagError = defineClass("FlagError", { alwaysPermanent: true });
30
+
31
+ var contextMod = lazyRequire(function () { return require("../flag-evaluation-context"); });
32
+
33
+ function create(opts) {
34
+ opts = opts || {};
35
+ validateOpts(opts, [
36
+ "userKey", "userKeyHeader", "extractAttributes", "tenantKeyHeader",
37
+ ], "middleware.flagContext");
38
+ if (opts.extractAttributes != null && typeof opts.extractAttributes !== "function") {
39
+ throw new FlagError("flag/bad-opt",
40
+ "flagContext: extractAttributes must be a function");
41
+ }
42
+ var userKeyHeader = (typeof opts.userKeyHeader === "string" && opts.userKeyHeader.length > 0)
43
+ ? opts.userKeyHeader.toLowerCase()
44
+ : null;
45
+ var tenantKeyHeader = (typeof opts.tenantKeyHeader === "string" && opts.tenantKeyHeader.length > 0)
46
+ ? opts.tenantKeyHeader.toLowerCase()
47
+ : null;
48
+ var explicitUserKey = (typeof opts.userKey === "string" && opts.userKey.length > 0)
49
+ ? opts.userKey
50
+ : null;
51
+
52
+ return function flagContextMiddleware(req, res, next) {
53
+ var headers = req.headers || {};
54
+ var headerKey = userKeyHeader && typeof headers[userKeyHeader] === "string"
55
+ ? headers[userKeyHeader]
56
+ : null;
57
+ var fromReqOpts = {};
58
+ if (explicitUserKey) fromReqOpts.userKey = explicitUserKey;
59
+ else if (headerKey) fromReqOpts.userKey = headerKey;
60
+ var augment = {};
61
+ if (typeof opts.extractAttributes === "function") {
62
+ try {
63
+ var extra = opts.extractAttributes(req);
64
+ if (extra && typeof extra === "object") augment = extra;
65
+ } catch (_e) { /* drop-silent on extraction error */ }
66
+ }
67
+ if (tenantKeyHeader && typeof headers[tenantKeyHeader] === "string") {
68
+ augment.tenantId = headers[tenantKeyHeader];
69
+ }
70
+ fromReqOpts.extra = augment;
71
+ req.flagCtx = contextMod().fromRequest(req, fromReqOpts);
72
+ return next();
73
+ };
74
+ }
75
+
76
+ module.exports = { create: create };
@@ -142,7 +142,7 @@ function create(opts) {
142
142
  try {
143
143
  audit().safeEmit({
144
144
  action: "network.host_allowlist.denied",
145
- outcome: "fail",
145
+ outcome: "denied",
146
146
  actor: { clientIp: requestHelpers.clientIp(req) },
147
147
  metadata: {
148
148
  reason: reason,
@@ -16,7 +16,11 @@
16
16
  * 6. (your auth + business middleware + routes)
17
17
  * 7. errorHandler — must be LAST so it catches everything that throws
18
18
  */
19
+ var aiActDisclosure = require("./ai-act-disclosure");
19
20
  var apiEncrypt = require("./api-encrypt");
21
+ var openapiServe = require("./openapi-serve");
22
+ var asyncapiServe = require("./asyncapi-serve");
23
+ var flagContext = require("./flag-context");
20
24
  var assetlinks = require("./assetlinks");
21
25
  var attachUser = require("./attach-user");
22
26
  var bearerAuth = require("./bearer-auth");
@@ -43,6 +47,7 @@ var requireAal = require("./require-aal");
43
47
  var requireAuth = require("./require-auth");
44
48
  var requireContentType = require("./require-content-type");
45
49
  var requireMethods = require("./require-methods");
50
+ var requireStepUp = require("./require-step-up");
46
51
  var securityHeaders = require("./security-headers");
47
52
  var securityTxt = require("./security-txt");
48
53
  var spanHttpServer = require("./span-http-server");
@@ -65,6 +70,7 @@ module.exports = {
65
70
  requireAuth: requireAuth.create,
66
71
  requireContentType: requireContentType.create,
67
72
  requireMethods: requireMethods.create,
73
+ requireStepUp: requireStepUp.create,
68
74
  csrfProtect: csrfProtect.create,
69
75
  fetchMetadata: fetchMetadata.create,
70
76
  gpc: gpc.create,
@@ -78,6 +84,10 @@ module.exports = {
78
84
  sse: sse.create,
79
85
  requestLog: requestLog.create,
80
86
  apiEncrypt: apiEncrypt,
87
+ aiActDisclosure: aiActDisclosure.create,
88
+ openapiServe: openapiServe.create,
89
+ asyncapiServe: asyncapiServe.create,
90
+ flagContext: flagContext.create,
81
91
  assetlinks: assetlinks.create,
82
92
  dbRoleFor: dbRoleFor.create,
83
93
  dpop: dpop.create,
@@ -103,6 +113,7 @@ module.exports = {
103
113
  requireAuth: requireAuth,
104
114
  requireContentType: requireContentType,
105
115
  requireMethods: requireMethods,
116
+ requireStepUp: requireStepUp,
106
117
  csrfProtect: csrfProtect,
107
118
  fetchMetadata: fetchMetadata,
108
119
  bodyParser: bodyParser,
@@ -113,6 +124,10 @@ module.exports = {
113
124
  sse: sse,
114
125
  requestLog: requestLog,
115
126
  apiEncrypt: apiEncrypt,
127
+ aiActDisclosure: aiActDisclosure,
128
+ openapiServe: openapiServe,
129
+ asyncapiServe: asyncapiServe,
130
+ flagContext: flagContext,
116
131
  assetlinks: assetlinks,
117
132
  dbRoleFor: dbRoleFor,
118
133
  dpop: dpop,