@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,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* openapi-serve middleware — expose an OpenAPI 3.1 document as a
|
|
4
|
+
* request-time JSON / YAML resource at a single mount point.
|
|
5
|
+
*
|
|
6
|
+
* var openapi = b.openapi.create({ ... });
|
|
7
|
+
* ...add paths / schemas / security...
|
|
8
|
+
*
|
|
9
|
+
* var serve = b.middleware.openapiServe({
|
|
10
|
+
* document: openapi,
|
|
11
|
+
* pathJson: "/openapi.json",
|
|
12
|
+
* pathYaml: "/openapi.yaml",
|
|
13
|
+
* pretty: true,
|
|
14
|
+
* cacheControl: "public, max-age=300",
|
|
15
|
+
* });
|
|
16
|
+
* router.use(serve);
|
|
17
|
+
*
|
|
18
|
+
* The middleware ONLY responds to GET requests for the configured
|
|
19
|
+
* paths; everything else passes to next() unchanged. ETag is computed
|
|
20
|
+
* from the JSON-string SHA3-512 to allow conditional GET.
|
|
21
|
+
*
|
|
22
|
+
* If `accessControl: "public"` (the default), the middleware emits
|
|
23
|
+
* `Access-Control-Allow-Origin: *` so external doc tooling can fetch.
|
|
24
|
+
* For internal-only docs operators set `accessControl: "same-origin"`
|
|
25
|
+
* which omits the CORS header.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var nodeCrypto = require("crypto");
|
|
29
|
+
var validateOpts = require("../validate-opts");
|
|
30
|
+
var lazyRequire = require("../lazy-require");
|
|
31
|
+
var { defineClass } = require("../framework-error");
|
|
32
|
+
var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
|
|
33
|
+
|
|
34
|
+
var openapiYaml = lazyRequire(function () { return require("../openapi-yaml"); });
|
|
35
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
36
|
+
|
|
37
|
+
function create(opts) {
|
|
38
|
+
opts = opts || {};
|
|
39
|
+
validateOpts(opts, [
|
|
40
|
+
"document", "pathJson", "pathYaml", "pretty",
|
|
41
|
+
"cacheControl", "accessControl", "audit",
|
|
42
|
+
], "middleware.openapiServe");
|
|
43
|
+
if (!opts.document || typeof opts.document.toJson !== "function") {
|
|
44
|
+
throw new OpenApiError("openapi/bad-document",
|
|
45
|
+
"openapiServe: document must be a builder created via b.openapi.create()");
|
|
46
|
+
}
|
|
47
|
+
var pathJson = opts.pathJson || "/openapi.json";
|
|
48
|
+
var pathYaml = opts.pathYaml || "/openapi.yaml";
|
|
49
|
+
var pretty = opts.pretty === true ? 2 : 0;
|
|
50
|
+
var cacheControl = (typeof opts.cacheControl === "string" && opts.cacheControl.length > 0)
|
|
51
|
+
? opts.cacheControl : "public, max-age=300";
|
|
52
|
+
var accessControl = opts.accessControl || "public";
|
|
53
|
+
var auditOn = opts.audit !== false;
|
|
54
|
+
|
|
55
|
+
if (typeof pathJson !== "string" || pathJson.charAt(0) !== "/") {
|
|
56
|
+
throw new OpenApiError("openapi/bad-path",
|
|
57
|
+
"openapiServe: pathJson must start with '/' - got " + JSON.stringify(pathJson));
|
|
58
|
+
}
|
|
59
|
+
if (typeof pathYaml !== "string" || pathYaml.charAt(0) !== "/") {
|
|
60
|
+
throw new OpenApiError("openapi/bad-path",
|
|
61
|
+
"openapiServe: pathYaml must start with '/' - got " + JSON.stringify(pathYaml));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var cachedDoc = null;
|
|
65
|
+
var cachedJsonStr = null;
|
|
66
|
+
var cachedYamlStr = null;
|
|
67
|
+
var cachedJsonEtag = null;
|
|
68
|
+
var cachedYamlEtag = null;
|
|
69
|
+
|
|
70
|
+
function _rebuild() {
|
|
71
|
+
cachedDoc = opts.document.toJson();
|
|
72
|
+
cachedJsonStr = JSON.stringify(cachedDoc, null, pretty);
|
|
73
|
+
cachedYamlStr = openapiYaml().toYaml(cachedDoc);
|
|
74
|
+
cachedJsonEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedJsonStr).digest("base64url").slice(0, 24) + '"';
|
|
75
|
+
cachedYamlEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedYamlStr).digest("base64url").slice(0, 24) + '"';
|
|
76
|
+
}
|
|
77
|
+
_rebuild();
|
|
78
|
+
|
|
79
|
+
function _writeBody(req, res, body, etag, contentType) {
|
|
80
|
+
var requestEtag = (req.headers && req.headers["if-none-match"]) || null;
|
|
81
|
+
if (requestEtag && requestEtag === etag) {
|
|
82
|
+
res.writeHead(304, { "ETag": etag, "Cache-Control": cacheControl }); // allow:raw-byte-literal — HTTP 304
|
|
83
|
+
res.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
var headers = {
|
|
87
|
+
"Content-Type": contentType,
|
|
88
|
+
"Content-Length": Buffer.byteLength(body),
|
|
89
|
+
"Cache-Control": cacheControl,
|
|
90
|
+
"ETag": etag,
|
|
91
|
+
};
|
|
92
|
+
if (accessControl === "public") {
|
|
93
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
94
|
+
}
|
|
95
|
+
res.writeHead(200, headers); // allow:raw-byte-literal — HTTP 200
|
|
96
|
+
res.end(body);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var mw = function (req, res, next) {
|
|
100
|
+
if (typeof res.writeHead !== "function") return next();
|
|
101
|
+
var method = (req.method || "GET").toUpperCase();
|
|
102
|
+
if (method !== "GET" && method !== "HEAD") return next();
|
|
103
|
+
var pathname = req.pathname;
|
|
104
|
+
if (typeof pathname !== "string") {
|
|
105
|
+
var url = req.url || "";
|
|
106
|
+
var qIdx = url.indexOf("?");
|
|
107
|
+
pathname = qIdx === -1 ? url : url.slice(0, qIdx);
|
|
108
|
+
}
|
|
109
|
+
if (pathname === pathJson) {
|
|
110
|
+
_writeBody(req, res, cachedJsonStr, cachedJsonEtag, "application/json; charset=utf-8");
|
|
111
|
+
if (auditOn) {
|
|
112
|
+
try {
|
|
113
|
+
audit().safeEmit({
|
|
114
|
+
action: "openapi.document.served",
|
|
115
|
+
outcome: "success",
|
|
116
|
+
actor: null,
|
|
117
|
+
metadata: { format: "json", path: pathname, bytes: cachedJsonStr.length },
|
|
118
|
+
});
|
|
119
|
+
} catch (_e) { /* drop-silent */ }
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (pathname === pathYaml) {
|
|
124
|
+
_writeBody(req, res, cachedYamlStr, cachedYamlEtag, "application/yaml; charset=utf-8");
|
|
125
|
+
if (auditOn) {
|
|
126
|
+
try {
|
|
127
|
+
audit().safeEmit({
|
|
128
|
+
action: "openapi.document.served",
|
|
129
|
+
outcome: "success",
|
|
130
|
+
actor: null,
|
|
131
|
+
metadata: { format: "yaml", path: pathname, bytes: cachedYamlStr.length },
|
|
132
|
+
});
|
|
133
|
+
} catch (_e) { /* drop-silent */ }
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
return next();
|
|
138
|
+
};
|
|
139
|
+
mw.forceRebuild = _rebuild;
|
|
140
|
+
return mw;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { create: create };
|
|
@@ -76,7 +76,7 @@ function create(opts) {
|
|
|
76
76
|
audit().safeEmit({
|
|
77
77
|
action: "auth.aal.denied",
|
|
78
78
|
actor: { clientIp: requestHelpers.clientIp(req), userId: req.user && req.user.id },
|
|
79
|
-
outcome: "
|
|
79
|
+
outcome: "denied",
|
|
80
80
|
metadata: {
|
|
81
81
|
required: minimum,
|
|
82
82
|
actual: actual || null,
|
|
@@ -93,7 +93,7 @@ function create(opts) {
|
|
|
93
93
|
audit().safeEmit({
|
|
94
94
|
action: "auth.aal.granted",
|
|
95
95
|
actor: { clientIp: requestHelpers.clientIp(req), userId: req.user && req.user.id },
|
|
96
|
-
outcome: "
|
|
96
|
+
outcome: "success",
|
|
97
97
|
metadata: { aal: actual, required: minimum, route: req.url },
|
|
98
98
|
});
|
|
99
99
|
} catch (_ignored) { /* drop-silent */ }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* require-step-up middleware — gate routes per RFC 9470 OAuth 2.0
|
|
4
|
+
* Step-Up Authentication Challenge.
|
|
5
|
+
*
|
|
6
|
+
* Mounted AFTER attachUser / bearerAuth so the request carries the
|
|
7
|
+
* already-verified token claims.
|
|
8
|
+
*
|
|
9
|
+
* var sensitiveStepUp = b.middleware.requireStepUp({
|
|
10
|
+
* requirement: { acr: "high", maxAge: 300 },
|
|
11
|
+
* realm: "billing-api",
|
|
12
|
+
* });
|
|
13
|
+
* router.use("/billing/transfer", sensitiveStepUp);
|
|
14
|
+
*
|
|
15
|
+
* Failure shape (per RFC 9470 §3):
|
|
16
|
+
* 401 Unauthorized
|
|
17
|
+
* WWW-Authenticate: Bearer error="insufficient_user_authentication",
|
|
18
|
+
* error_description="...", acr_values="high", max_age="300"
|
|
19
|
+
* Content-Type: application/json
|
|
20
|
+
* { "error": "insufficient_user_authentication", "error_description": "..." }
|
|
21
|
+
*
|
|
22
|
+
* Operators with their own elevation grants pass `acceptGrant: true`
|
|
23
|
+
* and `grantHeader: "X-Step-Up-Grant"` (default) — the middleware
|
|
24
|
+
* checks for a valid b.auth.stepUp.grant token before evaluating the
|
|
25
|
+
* normal claims-based requirement, so a multi-step flow doesn't get
|
|
26
|
+
* step-up-prompted on every action.
|
|
27
|
+
*
|
|
28
|
+
* Options:
|
|
29
|
+
* {
|
|
30
|
+
* requirement: { acr / acrValues / maxAge / requiredAmr / phishingResistant },
|
|
31
|
+
* getClaims: function(req) { return req.user.claims; },
|
|
32
|
+
* realm: "api",
|
|
33
|
+
* audit: true,
|
|
34
|
+
* acceptGrant: true, // default
|
|
35
|
+
* grantHeader: "X-Step-Up-Grant", // default
|
|
36
|
+
* grantScope: null, // narrow grant by scope
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* NEVER weaken the security default to fix a broken caller. Operators
|
|
40
|
+
* configure their IdP to emit `acr` / `auth_time` / `amr` correctly;
|
|
41
|
+
* the middleware does not silently default these to "good enough" on a
|
|
42
|
+
* missing claim.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
var lazyRequire = require("../lazy-require");
|
|
46
|
+
var validateOpts = require("../validate-opts");
|
|
47
|
+
var requestHelpers = require("../request-helpers");
|
|
48
|
+
var { AuthError } = require("../framework-error");
|
|
49
|
+
|
|
50
|
+
var stepUp = lazyRequire(function () { return require("../auth/step-up"); });
|
|
51
|
+
var elevation = lazyRequire(function () { return require("../auth/elevation-grant"); });
|
|
52
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
53
|
+
|
|
54
|
+
var DEFAULT_GRANT_HEADER = "x-step-up-grant";
|
|
55
|
+
|
|
56
|
+
function _defaultGetClaims(req) {
|
|
57
|
+
if (!req || typeof req !== "object") return null;
|
|
58
|
+
if (req.user && req.user.claims && typeof req.user.claims === "object") {
|
|
59
|
+
return req.user.claims;
|
|
60
|
+
}
|
|
61
|
+
if (req.user && typeof req.user === "object") {
|
|
62
|
+
return req.user;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _writeChallenge(res, challenge, body, statusCode) {
|
|
68
|
+
if (res.headersSent) return;
|
|
69
|
+
var json = JSON.stringify(body);
|
|
70
|
+
res.writeHead(statusCode, { // allow:raw-byte-literal — HTTP status passthrough
|
|
71
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
72
|
+
"Content-Length": Buffer.byteLength(json),
|
|
73
|
+
"WWW-Authenticate": challenge,
|
|
74
|
+
});
|
|
75
|
+
res.end(json);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function create(opts) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
validateOpts(opts, [
|
|
81
|
+
"requirement", "getClaims", "realm", "audit",
|
|
82
|
+
"acceptGrant", "grantHeader", "grantScope", "errorDescription",
|
|
83
|
+
], "middleware.requireStepUp");
|
|
84
|
+
|
|
85
|
+
if (!opts.requirement || typeof opts.requirement !== "object") {
|
|
86
|
+
throw new AuthError("auth-stepUp/bad-requirement",
|
|
87
|
+
"middleware.requireStepUp: opts.requirement must be an object");
|
|
88
|
+
}
|
|
89
|
+
validateOpts.optionalFunction(opts.getClaims,
|
|
90
|
+
"middleware.requireStepUp: getClaims", AuthError, "auth-stepUp/bad-opt");
|
|
91
|
+
|
|
92
|
+
var realm = (typeof opts.realm === "string" && opts.realm.length > 0)
|
|
93
|
+
? opts.realm : "api";
|
|
94
|
+
var auditOn = opts.audit !== false;
|
|
95
|
+
var getClaims = (typeof opts.getClaims === "function")
|
|
96
|
+
? opts.getClaims : _defaultGetClaims;
|
|
97
|
+
var acceptGrant = opts.acceptGrant !== false;
|
|
98
|
+
var grantHeader = (typeof opts.grantHeader === "string" && opts.grantHeader.length > 0)
|
|
99
|
+
? opts.grantHeader.toLowerCase() : DEFAULT_GRANT_HEADER;
|
|
100
|
+
var grantScope = (typeof opts.grantScope === "string" && opts.grantScope.length > 0)
|
|
101
|
+
? opts.grantScope : null;
|
|
102
|
+
var errorDesc = (typeof opts.errorDescription === "string" && opts.errorDescription.length > 0)
|
|
103
|
+
? opts.errorDescription : null;
|
|
104
|
+
|
|
105
|
+
// Pre-validate the requirement so operator typos surface at boot, not
|
|
106
|
+
// on the first hot-path request.
|
|
107
|
+
var probe = stepUp().evaluate({ claims: { acr: "0" }, requirement: opts.requirement });
|
|
108
|
+
if (probe.error === "bad_requirement" || probe.error === "unknown_acr") {
|
|
109
|
+
throw new AuthError("auth-stepUp/bad-requirement",
|
|
110
|
+
"middleware.requireStepUp: " + (probe.reason || probe.error));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return function requireStepUpMiddleware(req, res, next) {
|
|
114
|
+
var headers = req.headers || {};
|
|
115
|
+
|
|
116
|
+
// Path 1: operator-issued elevation grant — short-circuit success.
|
|
117
|
+
if (acceptGrant) {
|
|
118
|
+
var grantToken = headers[grantHeader] || null;
|
|
119
|
+
if (typeof grantToken === "string" && grantToken.length > 0) {
|
|
120
|
+
var verifyOpts = {};
|
|
121
|
+
if (grantScope) verifyOpts.scope = grantScope;
|
|
122
|
+
var grantResult = elevation().verify(grantToken, verifyOpts);
|
|
123
|
+
if (grantResult.ok) {
|
|
124
|
+
if (auditOn) {
|
|
125
|
+
try {
|
|
126
|
+
audit().safeEmit({
|
|
127
|
+
action: "auth.stepup.satisfied",
|
|
128
|
+
outcome: "success",
|
|
129
|
+
actor: { userId: grantResult.payload.sub,
|
|
130
|
+
clientIp: requestHelpers.clientIp(req) },
|
|
131
|
+
metadata: {
|
|
132
|
+
reason: "grant",
|
|
133
|
+
jti: grantResult.payload.jti || null,
|
|
134
|
+
scope: grantResult.payload.scope,
|
|
135
|
+
route: req.url || null,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
} catch (_e) { /* drop-silent */ }
|
|
139
|
+
}
|
|
140
|
+
if (req.user) req.user.stepUp = { byGrant: true, payload: grantResult.payload };
|
|
141
|
+
return next();
|
|
142
|
+
}
|
|
143
|
+
// Invalid grant — fall through to claims-based path; emit signal.
|
|
144
|
+
if (auditOn) {
|
|
145
|
+
try {
|
|
146
|
+
audit().safeEmit({
|
|
147
|
+
action: "auth.stepup.grant.rejected",
|
|
148
|
+
outcome: "denied",
|
|
149
|
+
actor: { clientIp: requestHelpers.clientIp(req) },
|
|
150
|
+
metadata: { error: grantResult.error, reason: grantResult.reason },
|
|
151
|
+
});
|
|
152
|
+
} catch (_e) { /* drop-silent */ }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Path 2: claims-based evaluation.
|
|
158
|
+
var claims = getClaims(req);
|
|
159
|
+
var result = stepUp().evaluate({ claims: claims, requirement: opts.requirement });
|
|
160
|
+
|
|
161
|
+
if (result.ok) {
|
|
162
|
+
if (auditOn) stepUp().emitAuditSatisfied("requireStepUp", opts.requirement, result.presented, req);
|
|
163
|
+
if (req.user) req.user.stepUp = { byClaims: true, presented: result.presented };
|
|
164
|
+
return next();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (auditOn) stepUp().emitAuditRequired("requireStepUp", opts.requirement, result.presented, req);
|
|
168
|
+
|
|
169
|
+
var challenge = stepUp().buildChallenge({
|
|
170
|
+
requirement: opts.requirement,
|
|
171
|
+
realm: realm,
|
|
172
|
+
error: stepUp().INSUFFICIENT_USER_AUTHENTICATION,
|
|
173
|
+
errorDescription: errorDesc || undefined,
|
|
174
|
+
});
|
|
175
|
+
_writeChallenge(res,
|
|
176
|
+
challenge,
|
|
177
|
+
{
|
|
178
|
+
error: stepUp().INSUFFICIENT_USER_AUTHENTICATION,
|
|
179
|
+
error_description: errorDesc || "A higher level of authentication is required",
|
|
180
|
+
},
|
|
181
|
+
401 // allow:raw-byte-literal — HTTP 401
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { create: create };
|
|
@@ -85,7 +85,7 @@ function create(opts) {
|
|
|
85
85
|
try {
|
|
86
86
|
audit().safeEmit({
|
|
87
87
|
action: "system.trace.synthesised",
|
|
88
|
-
outcome: "
|
|
88
|
+
outcome: "success",
|
|
89
89
|
metadata: { route: req.url || "/", traceId: req.trace.traceId },
|
|
90
90
|
});
|
|
91
91
|
} catch (_e) { /* drop-silent — observability sink */ }
|
package/lib/mtls-ca.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* caCert: "ca.crt",
|
|
18
18
|
* },
|
|
19
19
|
* vault: b.vault, // optional; required when sealed
|
|
20
|
-
* caKeySealedMode: "
|
|
20
|
+
* caKeySealedMode: "required", // "required" (default) | "disabled"
|
|
21
21
|
* generation: 1, // current CA generation for OU=CAv{N}
|
|
22
22
|
* engine: myCertEngine, // optional — defaults to b.mtlsEngine
|
|
23
23
|
* });
|
|
@@ -27,10 +27,16 @@
|
|
|
27
27
|
* ca.key CA private key (PEM, plaintext on disk)
|
|
28
28
|
* ca.key.sealed CA private key (vault.seal of PEM bytes)
|
|
29
29
|
*
|
|
30
|
-
* caKeySealedMode:
|
|
31
|
-
* "
|
|
32
|
-
*
|
|
33
|
-
* "disabled" plaintext required; refuse sealed
|
|
30
|
+
* caKeySealedMode (defaults to "required"):
|
|
31
|
+
* "required" sealed file required; refuse plaintext (default — vault
|
|
32
|
+
* must be wired)
|
|
33
|
+
* "disabled" plaintext required; refuse sealed (dev-only opt-out;
|
|
34
|
+
* operator must justify with audited reason)
|
|
35
|
+
*
|
|
36
|
+
* The legacy "auto" mode (load whichever exists, fall back to plaintext
|
|
37
|
+
* when no sealed file is present) was removed; it defaulted to writing
|
|
38
|
+
* plaintext on a fresh install, which is the inverse of the framework's
|
|
39
|
+
* security-defaults-on posture for at-rest key material.
|
|
34
40
|
*
|
|
35
41
|
* Generation tagging: every CA cert issued by the framework embeds a
|
|
36
42
|
* "OU=CAv{N}" RDN in its subject DN. Status reads that back so an
|
|
@@ -114,7 +120,7 @@ var DEFAULT_PATHS = {
|
|
|
114
120
|
crl: "ca.crl",
|
|
115
121
|
};
|
|
116
122
|
|
|
117
|
-
var VALID_SEAL_MODES = {
|
|
123
|
+
var VALID_SEAL_MODES = { required: 1, disabled: 1 };
|
|
118
124
|
|
|
119
125
|
function _resolvePaths(dataDir, paths) {
|
|
120
126
|
var p = Object.assign({}, DEFAULT_PATHS, paths || {});
|
|
@@ -161,10 +167,11 @@ function create(opts) {
|
|
|
161
167
|
}
|
|
162
168
|
var paths = _resolvePaths(opts.dataDir, opts.paths);
|
|
163
169
|
var vault = opts.vault || null;
|
|
164
|
-
var caKeySealedMode = (opts.caKeySealedMode || "
|
|
170
|
+
var caKeySealedMode = (opts.caKeySealedMode || "required").toLowerCase();
|
|
165
171
|
if (!VALID_SEAL_MODES[caKeySealedMode]) {
|
|
166
172
|
throw new MtlsCaError("mtls-ca/bad-mode",
|
|
167
|
-
"caKeySealedMode must be '
|
|
173
|
+
"caKeySealedMode must be 'required' or 'disabled' " +
|
|
174
|
+
"(legacy 'auto' was removed — it defaulted to plaintext-on-disk)");
|
|
168
175
|
}
|
|
169
176
|
var generation = typeof opts.generation === "number" && opts.generation >= 1
|
|
170
177
|
? Math.floor(opts.generation) : 1;
|
|
@@ -230,23 +237,10 @@ function create(opts) {
|
|
|
230
237
|
}
|
|
231
238
|
return Buffer.from(pem, "utf8");
|
|
232
239
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
return fs.readFileSync(paths.caKey);
|
|
239
|
-
}
|
|
240
|
-
// auto: prefer sealed if it exists (defense-in-depth default)
|
|
241
|
-
if (hasSealed) {
|
|
242
|
-
_requireVault("sealed CA key load");
|
|
243
|
-
var sealedBytesA = fs.readFileSync(paths.caKeySealed, "utf8").trim();
|
|
244
|
-
var pemA = vault.unseal(sealedBytesA);
|
|
245
|
-
if (!pemA) {
|
|
246
|
-
throw new MtlsCaError("mtls-ca/unseal-failed",
|
|
247
|
-
"vault.unseal of " + paths.caKeySealed + " returned empty");
|
|
248
|
-
}
|
|
249
|
-
return Buffer.from(pemA, "utf8");
|
|
240
|
+
// disabled: plaintext only.
|
|
241
|
+
if (!hasPlain) {
|
|
242
|
+
throw new MtlsCaError("mtls-ca/plain-required",
|
|
243
|
+
"caKeySealedMode='disabled' but " + paths.caKey + " does not exist");
|
|
250
244
|
}
|
|
251
245
|
return fs.readFileSync(paths.caKey);
|
|
252
246
|
}
|
|
@@ -260,10 +254,10 @@ function create(opts) {
|
|
|
260
254
|
}
|
|
261
255
|
|
|
262
256
|
// Atomic commit: write .tmp + atomic rename for both key and cert.
|
|
263
|
-
// Honors caKeySealedMode — when 'required', the key is
|
|
264
|
-
// before the on-disk write so plaintext PEM never touches
|
|
265
|
-
// filesystem; when 'disabled', it goes to disk as PEM
|
|
266
|
-
//
|
|
257
|
+
// Honors caKeySealedMode — when 'required' (the default), the key is
|
|
258
|
+
// vault-sealed before the on-disk write so plaintext PEM never touches
|
|
259
|
+
// the filesystem; when 'disabled', it goes to disk as PEM with the
|
|
260
|
+
// operator's audited reason on record.
|
|
267
261
|
function commit(opts2) {
|
|
268
262
|
if (!opts2 || typeof opts2.caKeyPem !== "string" || typeof opts2.caCertPem !== "string") {
|
|
269
263
|
throw new MtlsCaError("mtls-ca/bad-commit",
|
|
@@ -134,7 +134,27 @@ async function _selectAlgorithm() {
|
|
|
134
134
|
for (var i = 0; i < ALG_CANDIDATES.length; i++) {
|
|
135
135
|
var c = ALG_CANDIDATES[i];
|
|
136
136
|
var ok = await _probeCandidate(c);
|
|
137
|
-
if (ok) {
|
|
137
|
+
if (ok) {
|
|
138
|
+
_selectedAlg = c;
|
|
139
|
+
// Emit an audit row at first probe so operators see which
|
|
140
|
+
// algorithm landed without having to call b.mtlsCa.status().
|
|
141
|
+
// Pre-PQC ecosystems land on the ECDSA-P384 bridge silently;
|
|
142
|
+
// this puts the choice on the chain so compliance dashboards
|
|
143
|
+
// alert when an operator's deployment hasn't yet picked up the
|
|
144
|
+
// PQ-signed-cert capability the framework would otherwise
|
|
145
|
+
// prefer.
|
|
146
|
+
setImmediate(function () {
|
|
147
|
+
try {
|
|
148
|
+
var auditMod = require("./audit"); // allow:inline-require — circular-load defense
|
|
149
|
+
auditMod.safeEmit({
|
|
150
|
+
action: "mtls.engine.algorithm_selected",
|
|
151
|
+
outcome: "success",
|
|
152
|
+
metadata: { label: c.label, posture: c.posture, candidatesProbed: i + 1 },
|
|
153
|
+
});
|
|
154
|
+
} catch (_e) { /* drop-silent */ }
|
|
155
|
+
});
|
|
156
|
+
return c;
|
|
157
|
+
}
|
|
138
158
|
}
|
|
139
159
|
// Should never happen — ECDSA-P384-SHA384 is universal.
|
|
140
160
|
throw new MtlsEngineError("mtls-engine/no-algorithm",
|
package/lib/network-tls.js
CHANGED
|
@@ -26,7 +26,7 @@ var STATE = {
|
|
|
26
26
|
cas: [],
|
|
27
27
|
systemTrust: false,
|
|
28
28
|
baselineFingerprints: null,
|
|
29
|
-
tlsKeyShares: ["
|
|
29
|
+
tlsKeyShares: ["SecP384r1MLKEM1024", "X25519MLKEM768", "X25519"],
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
function _normalizePem(pem) {
|
|
@@ -301,7 +301,7 @@ function expiryMonitor(opts) {
|
|
|
301
301
|
try {
|
|
302
302
|
audit().safeEmit({
|
|
303
303
|
action: "network.tls.ca.expiring",
|
|
304
|
-
outcome: "
|
|
304
|
+
outcome: "success",
|
|
305
305
|
metadata: {
|
|
306
306
|
count: rows.length,
|
|
307
307
|
labels: rows.map(function (r) { return r.label; }),
|
|
@@ -375,9 +375,9 @@ function applyToContext(opts) {
|
|
|
375
375
|
// resetKeyShares() → restores default
|
|
376
376
|
|
|
377
377
|
var DEFAULT_PQC_KEY_SHARES = Object.freeze([
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
378
|
+
"SecP384r1MLKEM1024", // highest-PQC hybrid (codepoint 0x11ED, draft-kwiatkowski-tls-ecdhe-mlkem-02)
|
|
379
|
+
"X25519MLKEM768", // mid-PQC hybrid (codepoint 0x11EC, IETF/Cloudflare/Chrome interop)
|
|
380
|
+
"X25519", // classical fallback (modern non-PQC peers)
|
|
381
381
|
]);
|
|
382
382
|
|
|
383
383
|
function _validateKeyShare(name) {
|
|
@@ -990,9 +990,24 @@ function buildOcspRequest(opts) {
|
|
|
990
990
|
var serial = _extractLeafSerial(opts.leafCertDer);
|
|
991
991
|
// CertID hashes — SHA-1 per RFC 6960 §4.1.1 (the only universally
|
|
992
992
|
// supported algorithm; SHA-256 in OCSP requests is RFC 6960 §4.3
|
|
993
|
-
// optional and many responders reject).
|
|
993
|
+
// optional and many responders reject). The hash isn't security-
|
|
994
|
+
// critical here — it's a name/key lookup, not an integrity check —
|
|
995
|
+
// but operator compliance dashboards alerting on "anywhere in the
|
|
996
|
+
// framework that touches SHA-1" need a signal. Emit an audit row
|
|
997
|
+
// on every OCSP request build so the algorithm choice is visible
|
|
998
|
+
// in the chain.
|
|
994
999
|
var nameHash = nodeCrypto.createHash("sha1").update(iss.issuerNameDer).digest();
|
|
995
1000
|
var keyHash = nodeCrypto.createHash("sha1").update(iss.issuerKey).digest();
|
|
1001
|
+
setImmediate(function () {
|
|
1002
|
+
try {
|
|
1003
|
+
var auditMod = require("./audit"); // allow:inline-require — circular-load defense (audit imports network-tls)
|
|
1004
|
+
auditMod.safeEmit({
|
|
1005
|
+
action: "network.tls.ocsp.certid_built",
|
|
1006
|
+
outcome: "success",
|
|
1007
|
+
metadata: { hashAlgorithm: "sha1", note: "RFC 6960 §4.1.1 — non-security-critical lookup hash" },
|
|
1008
|
+
});
|
|
1009
|
+
} catch (_e) { /* drop-silent */ }
|
|
1010
|
+
});
|
|
996
1011
|
// hashAlgorithm AlgorithmIdentifier ::= SEQUENCE { algorithm OID, NULL }
|
|
997
1012
|
var algId = asn1.writeSequence([asn1.writeOid(OID_SHA1), asn1.writeNull()]);
|
|
998
1013
|
var certId = asn1.writeSequence([
|
|
@@ -897,6 +897,47 @@ function create(config) {
|
|
|
897
897
|
_validateRetention(opts);
|
|
898
898
|
validateOpts(opts, ["mode", "retainUntil", "bypassGovernance", "req", "actor"],
|
|
899
899
|
"bucketOps.setObjectRetention");
|
|
900
|
+
// COMPLIANCE-mode defense-in-depth: refuse client-side when the
|
|
901
|
+
// operator (or attacker with the s3:PutObjectRetention permission)
|
|
902
|
+
// tries to shorten an existing COMPLIANCE retention or pass
|
|
903
|
+
// bypassGovernance against COMPLIANCE. Real S3 also refuses but
|
|
904
|
+
// MinIO and other S3-compatible backends are implementation-
|
|
905
|
+
// dependent; the framework's job is defense-in-depth, not
|
|
906
|
+
// passthrough. Adds one RTT (the GET) to every PUT — acceptable.
|
|
907
|
+
//
|
|
908
|
+
// The pre-check is a soft gate: when the backend can't surface the
|
|
909
|
+
// existing retention (parse error, no-such-object, etc.), the
|
|
910
|
+
// framework falls through to the PUT and lets the backend's own
|
|
911
|
+
// enforcement handle it. The pre-check is value-add, not
|
|
912
|
+
// load-bearing.
|
|
913
|
+
return getObjectRetention(name, key).then(function (existing) {
|
|
914
|
+
if (existing && existing.mode === "COMPLIANCE") {
|
|
915
|
+
if (opts.bypassGovernance === true) {
|
|
916
|
+
throw new ObjectStoreError("objectstore/compliance-bypass-refused",
|
|
917
|
+
"setObjectRetention: bypassGovernance refused — existing retention mode is COMPLIANCE (cannot be bypassed by anyone, including root)", true);
|
|
918
|
+
}
|
|
919
|
+
if (opts.retainUntil && existing.retainUntil &&
|
|
920
|
+
opts.retainUntil.getTime() < existing.retainUntil.getTime()) {
|
|
921
|
+
throw new ObjectStoreError("objectstore/compliance-shortening-refused",
|
|
922
|
+
"setObjectRetention: cannot shorten COMPLIANCE retention (existing=" +
|
|
923
|
+
existing.retainUntil.toISOString() + ", proposed=" +
|
|
924
|
+
opts.retainUntil.toISOString() + ")", true);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return _doSetRetention(name, key, opts);
|
|
928
|
+
}, function (e) {
|
|
929
|
+
// Re-throw the framework's own COMPLIANCE refusals; everything
|
|
930
|
+
// else (parse errors, transient network errors, malformed
|
|
931
|
+
// backend responses) falls through to the PUT.
|
|
932
|
+
if (e && typeof e.code === "string" &&
|
|
933
|
+
e.code.indexOf("objectstore/compliance-") === 0) {
|
|
934
|
+
throw e;
|
|
935
|
+
}
|
|
936
|
+
return _doSetRetention(name, key, opts);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function _doSetRetention(name, key, opts) {
|
|
900
941
|
var bodyXml = _buildRetentionXml(opts);
|
|
901
942
|
var bodyBuf = Buffer.from(bodyXml, "utf8");
|
|
902
943
|
var url = _objectUrl(name, key, { retention: "" });
|
|
@@ -41,6 +41,33 @@ var { defineClass } = require("./framework-error");
|
|
|
41
41
|
var OtlpExporterError = defineClass("OtlpExporterError", { alwaysPermanent: true });
|
|
42
42
|
|
|
43
43
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
44
|
+
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
45
|
+
|
|
46
|
+
// Default OTLP transport — uses the framework's own b.httpClient
|
|
47
|
+
// (node:https through the PQC-hybrid agent + cert-pinning + SSRF
|
|
48
|
+
// guard) rather than globalThis.fetch. Operators with a sidecar
|
|
49
|
+
// collector that must be addressed via fetch (Cloudflare Workers,
|
|
50
|
+
// Deno, fetch-only edge runtimes) override fetchImpl explicitly.
|
|
51
|
+
// Returning a fetch-shaped { ok, status } so the existing _post
|
|
52
|
+
// path stays the same regardless of which transport ran.
|
|
53
|
+
function _defaultFetchImpl(endpoint, init) {
|
|
54
|
+
var hc = httpClient();
|
|
55
|
+
return hc.request({
|
|
56
|
+
url: endpoint,
|
|
57
|
+
method: init && init.method ? init.method : "POST",
|
|
58
|
+
headers: init && init.headers ? init.headers : {},
|
|
59
|
+
body: init && init.body ? init.body : "",
|
|
60
|
+
timeoutMs: 0,
|
|
61
|
+
responseMode: "always-resolve",
|
|
62
|
+
allowInternal: true,
|
|
63
|
+
}).then(function (res) {
|
|
64
|
+
var status = res && res.statusCode;
|
|
65
|
+
return {
|
|
66
|
+
ok: status >= 200 && status < 300, // allow:raw-byte-literal — HTTP status ranges
|
|
67
|
+
status: status,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
44
71
|
|
|
45
72
|
var DEFAULT_BATCH_SIZE = 200; // allow:raw-byte-literal — OTLP recommended batch
|
|
46
73
|
var DEFAULT_MAX_QUEUE_SIZE = 4096; // allow:raw-byte-literal — operator-side queue cap
|
|
@@ -206,10 +233,16 @@ function create(opts) {
|
|
|
206
233
|
var maxAttempts = opts.maxAttempts || DEFAULT_MAX_ATTEMPTS;
|
|
207
234
|
var backoffInitial = opts.backoffInitialMs || DEFAULT_BACKOFF_INITIAL_MS;
|
|
208
235
|
var backoffMax = opts.backoffMaxMs || DEFAULT_BACKOFF_MAX_MS;
|
|
209
|
-
|
|
236
|
+
// Default transport is the framework's b.httpClient (node:https +
|
|
237
|
+
// PQC-hybrid agent + SSRF guard). globalThis.fetch was the prior
|
|
238
|
+
// default; it leaked an outbound network surface that supply-chain
|
|
239
|
+
// scanners flagged because nothing in the framework's TLS posture
|
|
240
|
+
// wired through it. Operators on fetch-only runtimes still override
|
|
241
|
+
// by passing opts.fetchImpl.
|
|
242
|
+
var fetchImpl = opts.fetchImpl || _defaultFetchImpl;
|
|
210
243
|
if (typeof fetchImpl !== "function") {
|
|
211
244
|
throw new OtlpExporterError("otlp/no-fetch",
|
|
212
|
-
"otlpExporter.create: fetchImpl
|
|
245
|
+
"otlpExporter.create: opts.fetchImpl must be a function (override the framework default)");
|
|
213
246
|
}
|
|
214
247
|
|
|
215
248
|
var queue = [];
|