@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.
- package/CHANGELOG.md +41 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +15 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit-sign.js +1 -1
- package/lib/audit.js +68 -2
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/jwt.js +13 -0
- package/lib/auth/lockout.js +16 -3
- package/lib/auth/oauth.js +15 -1
- package/lib/auth/password.js +22 -2
- package/lib/auth/sd-jwt-vc-issuer.js +2 -2
- package/lib/auth/sd-jwt-vc.js +7 -2
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/break-glass.js +53 -14
- package/lib/cache-redis.js +1 -1
- package/lib/cache.js +6 -1
- package/lib/cli.js +3 -3
- package/lib/cluster.js +24 -1
- package/lib/compliance-ai-act-logging.js +190 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +12 -2
- package/lib/config-drift.js +2 -2
- package/lib/crypto-field.js +21 -1
- package/lib/crypto.js +114 -1
- package/lib/db.js +35 -4
- package/lib/dev.js +30 -3
- package/lib/dual-control.js +19 -1
- package/lib/external-db.js +10 -0
- package/lib/file-upload.js +30 -3
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/guard-all.js +33 -16
- package/lib/guard-csv.js +16 -2
- package/lib/guard-html.js +35 -0
- package/lib/guard-svg.js +20 -0
- package/lib/http-client.js +57 -11
- package/lib/inbox.js +391 -0
- package/lib/log-stream-syslog.js +8 -0
- package/lib/log-stream.js +1 -1
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/mail.js +40 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/attach-user.js +25 -2
- package/lib/middleware/bearer-auth.js +71 -6
- package/lib/middleware/body-parser.js +13 -0
- package/lib/middleware/cors.js +10 -0
- package/lib/middleware/csrf-protect.js +34 -3
- package/lib/middleware/dpop.js +3 -3
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/host-allowlist.js +1 -1
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-aal.js +2 -2
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/middleware/trace-propagate.js +1 -1
- package/lib/mtls-ca.js +23 -29
- package/lib/mtls-engine-default.js +21 -1
- package/lib/network-tls.js +21 -6
- package/lib/object-store/sigv4-bucket-ops.js +41 -0
- package/lib/observability-otlp-exporter.js +35 -2
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/outbox.js +3 -3
- package/lib/permissions.js +10 -1
- package/lib/pqc-agent.js +22 -1
- package/lib/pqc-software.js +195 -0
- package/lib/pubsub.js +8 -4
- package/lib/redact.js +26 -1
- package/lib/retention.js +26 -0
- package/lib/router.js +1 -0
- package/lib/scheduler.js +57 -1
- package/lib/session.js +3 -3
- package/lib/ssrf-guard.js +19 -4
- package/lib/static.js +12 -0
- package/lib/totp.js +16 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +978 -0
- package/package.json +1 -1
- 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
|
|
66
|
+
if (typeof h !== "string" || h.length === 0) return { state: "absent" };
|
|
60
67
|
var prefix = scheme + " ";
|
|
61
|
-
if (h.length <= prefix.length) return
|
|
62
|
-
if (h.slice(0, prefix.length).toLowerCase() !== prefix.toLowerCase())
|
|
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
|
-
|
|
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
|
|
108
|
-
if (
|
|
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.
|
package/lib/middleware/cors.js
CHANGED
|
@@ -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
|
-
|
|
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.");
|
package/lib/middleware/dpop.js
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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 };
|
package/lib/middleware/index.js
CHANGED
|
@@ -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,
|