@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,265 @@
1
+ "use strict";
2
+ /**
3
+ * ACR (Authentication Context Class Reference) vocabulary.
4
+ *
5
+ * The `acr` claim is RFC 9470 §3 + OIDC Core 1.0 §2 + ISO/IEC 29115. It
6
+ * denotes the rigor of the authentication ceremony that backs the
7
+ * current session — operators reason about it the same way they reason
8
+ * about NIST 800-63-4 AAL bands, but ACR carries finer granularity: it
9
+ * also encodes WHICH method achieved the strength.
10
+ *
11
+ * The framework ships a built-in dictionary of well-known ACR values
12
+ * with a strength-rank assigned from the spec authors' intent, but the
13
+ * dictionary is operator-extendable: every ACR your IdP issues should
14
+ * be registered with `register({ value, rank })` so policy decisions
15
+ * can compare what the request carries against what the route requires.
16
+ *
17
+ * Built-in vocabulary (rank ascending):
18
+ *
19
+ * "0" → no authentication (public access)
20
+ * "1" → password / single-factor (OIDC Core)
21
+ * "loa1" / "low" → loa1 / iso-29115 LOA-1 (low confidence)
22
+ * "phr" → phishing-resistant single-factor
23
+ * (RFC 9470 example value)
24
+ * "loa2" / "substantial" → multi-factor, ISO LOA-2
25
+ * "2" → multi-factor (OIDC Core)
26
+ * "phrh" → phishing-resistant + hardware (RFC 9470)
27
+ * "loa3" / "high" → multi-factor + phishing-resistant + hw
28
+ * "loa4" → in-person verified, hardware-bound
29
+ * "urn:mace:incommon:iap:bronze" → InCommon LoA-1
30
+ * "urn:mace:incommon:iap:silver" → InCommon LoA-2
31
+ * "urn:mace:incommon:iap:gold" → InCommon LoA-3
32
+ * "aal1" / "aal2" / "aal3" → NIST 800-63-4 (cross-walked)
33
+ * "ial1" / "ial2" / "ial3" → NIST 800-63A identity (cross-walked)
34
+ * "fal1" / "fal2" / "fal3" → NIST 800-63C federation
35
+ *
36
+ * Operators with a private vocabulary register before evaluation:
37
+ *
38
+ * b.auth.acr.register({ value: "myco:strong", rank: 70 });
39
+ *
40
+ * Ranks are operator-comparable integers in [0, 100]. The framework's
41
+ * built-ins occupy the ranges:
42
+ *
43
+ * 0–9 public / unauthenticated
44
+ * 10–29 single factor
45
+ * 30–49 multi-factor
46
+ * 50–69 phishing-resistant multi-factor
47
+ * 70–89 hardware-bound + phishing-resistant
48
+ * 90–100 in-person identity-proofed + hardware
49
+ *
50
+ * ACR is a STRING value carried in the `acr` JWT claim. Some IdPs emit
51
+ * the value directly; others stuff a JSON-array of ACRs into the
52
+ * `acr_values` parameter and let the policy engine pick. The framework
53
+ * accepts both.
54
+ *
55
+ * AMR (Authentication Methods References) per RFC 8176 is a separate
56
+ * but adjacent vocabulary — it lists WHICH methods were used (e.g.
57
+ * `["pwd", "otp"]` for password+TOTP). The framework's policy engine
58
+ * also evaluates AMR: a route requiring `requiredAmr: ["hwk"]`
59
+ * (hardware-key per RFC 8176) rejects sessions whose AMR lacks `hwk`
60
+ * even when their ACR ranks high enough.
61
+ */
62
+
63
+ var validateOpts = require("../validate-opts");
64
+ var { AuthError } = require("../framework-error");
65
+
66
+ // Core ranks — ascending strength. Operator-extendable via register().
67
+ var BUILTIN_RANKS = {
68
+ // Public / unauthenticated
69
+ "0": 0,
70
+
71
+ // Single factor
72
+ "1": 10,
73
+ "loa1": 10,
74
+ "low": 10,
75
+ "ial1": 10,
76
+ "fal1": 10,
77
+ "aal1": 10,
78
+ "urn:mace:incommon:iap:bronze": 12,
79
+
80
+ // Phishing-resistant single factor (e.g. mTLS)
81
+ "phr": 25,
82
+
83
+ // Multi-factor
84
+ "2": 30,
85
+ "loa2": 30,
86
+ "substantial": 30,
87
+ "ial2": 30,
88
+ "fal2": 30,
89
+ "aal2": 35,
90
+ "urn:mace:incommon:iap:silver": 32, // allow:raw-byte-literal — ACR rank, not bytes
91
+
92
+ // Phishing-resistant multi-factor (passkey UV)
93
+ "phrh": 60, // allow:raw-time-literal — ACR rank, not seconds
94
+
95
+ // Hardware-bound + phishing-resistant + multi-factor
96
+ "loa3": 70,
97
+ "high": 70,
98
+ "ial3": 75,
99
+ "fal3": 75,
100
+ "aal3": 75,
101
+ "urn:mace:incommon:iap:gold": 80, // allow:raw-byte-literal — ACR rank, not bytes
102
+
103
+ // In-person identity-proofed + hardware-bound
104
+ "loa4": 95,
105
+ };
106
+
107
+ // AMR catalog per RFC 8176 — used for requiredAmr policy evaluation.
108
+ // Each entry maps the canonical RFC 8176 short-form to a category that
109
+ // allows operators to evaluate broad classes (e.g. "any phishing-
110
+ // resistant method" → `category: "phishing-resistant"`).
111
+ var BUILTIN_AMR = {
112
+ "face": { category: "biometric", phishingResistant: false },
113
+ "fpt": { category: "biometric", phishingResistant: false },
114
+ "geo": { category: "context", phishingResistant: false },
115
+ "hwk": { category: "hardware", phishingResistant: true },
116
+ "iris": { category: "biometric", phishingResistant: false },
117
+ "kba": { category: "knowledge", phishingResistant: false },
118
+ "mca": { category: "multi-channel", phishingResistant: false },
119
+ "mfa": { category: "composite", phishingResistant: false },
120
+ "otp": { category: "out-of-band", phishingResistant: false },
121
+ "pin": { category: "knowledge", phishingResistant: false },
122
+ "pop": { category: "proof-of-possession", phishingResistant: true },
123
+ "pwd": { category: "knowledge", phishingResistant: false },
124
+ "rba": { category: "context", phishingResistant: false },
125
+ "retina": { category: "biometric", phishingResistant: false },
126
+ "sc": { category: "smart-card", phishingResistant: true },
127
+ "sms": { category: "out-of-band", phishingResistant: false },
128
+ "swk": { category: "software-key", phishingResistant: false },
129
+ "tel": { category: "out-of-band", phishingResistant: false },
130
+ "user": { category: "user-presence", phishingResistant: false },
131
+ "vbm": { category: "biometric", phishingResistant: false },
132
+ "wia": { category: "windows-integrated", phishingResistant: false },
133
+ };
134
+
135
+ // In-process registry — operators may extend (and re-extend) at boot.
136
+ var _registry = Object.create(null);
137
+ for (var k in BUILTIN_RANKS) {
138
+ if (Object.prototype.hasOwnProperty.call(BUILTIN_RANKS, k)) {
139
+ _registry[k] = BUILTIN_RANKS[k];
140
+ }
141
+ }
142
+
143
+ function register(opts) {
144
+ opts = opts || {};
145
+ validateOpts(opts, ["value", "rank"], "auth.acr.register");
146
+ validateOpts.requireNonEmptyString(opts.value, "register: value",
147
+ AuthError, "auth-stepUp/bad-acr");
148
+ if (typeof opts.rank !== "number" || !isFinite(opts.rank)) {
149
+ throw new AuthError("auth-stepUp/bad-rank",
150
+ "auth.acr.register: rank must be a finite number — got " +
151
+ JSON.stringify(opts.rank));
152
+ }
153
+ if (opts.rank < 0 || opts.rank > 100) {
154
+ throw new AuthError("auth-stepUp/bad-rank",
155
+ "auth.acr.register: rank must be in [0, 100] — got " + opts.rank);
156
+ }
157
+ _registry[opts.value] = opts.rank;
158
+ return { value: opts.value, rank: opts.rank };
159
+ }
160
+
161
+ function rankOf(value) {
162
+ if (typeof value !== "string" || value.length === 0) return -1;
163
+ if (Object.prototype.hasOwnProperty.call(_registry, value)) {
164
+ return _registry[value];
165
+ }
166
+ return -1;
167
+ }
168
+
169
+ function isRegistered(value) {
170
+ return rankOf(value) !== -1;
171
+ }
172
+
173
+ function listRegistered() {
174
+ var out = [];
175
+ for (var k in _registry) {
176
+ if (Object.prototype.hasOwnProperty.call(_registry, k)) {
177
+ out.push({ value: k, rank: _registry[k] });
178
+ }
179
+ }
180
+ out.sort(function (a, b) {
181
+ if (a.rank !== b.rank) return a.rank - b.rank;
182
+ if (a.value < b.value) return -1;
183
+ if (a.value > b.value) return 1;
184
+ return 0;
185
+ });
186
+ return out;
187
+ }
188
+
189
+ function meets(presented, required) {
190
+ if (typeof required !== "string") return true;
191
+ if (typeof presented !== "string") return false;
192
+ var rp = rankOf(presented);
193
+ var rr = rankOf(required);
194
+ if (rr === -1) {
195
+ throw new AuthError("auth-stepUp/unknown-acr",
196
+ "auth.acr.meets: required acr is not registered (call b.auth.acr.register first): " +
197
+ JSON.stringify(required));
198
+ }
199
+ if (rp === -1) return false;
200
+ return rp >= rr;
201
+ }
202
+
203
+ function meetsAny(presented, requiredList) {
204
+ if (!Array.isArray(requiredList) || requiredList.length === 0) return true;
205
+ for (var i = 0; i < requiredList.length; i += 1) {
206
+ if (meets(presented, requiredList[i])) return true;
207
+ }
208
+ return false;
209
+ }
210
+
211
+ function _classifyAmr(amrValue) {
212
+ if (typeof amrValue !== "string") return null;
213
+ if (Object.prototype.hasOwnProperty.call(BUILTIN_AMR, amrValue)) {
214
+ return BUILTIN_AMR[amrValue];
215
+ }
216
+ return null;
217
+ }
218
+
219
+ function amrIncludesPhishingResistant(amrList) {
220
+ if (!Array.isArray(amrList)) return false;
221
+ for (var i = 0; i < amrList.length; i += 1) {
222
+ var info = _classifyAmr(amrList[i]);
223
+ if (info && info.phishingResistant) return true;
224
+ }
225
+ return false;
226
+ }
227
+
228
+ function amrSatisfiesRequiredList(presentedAmr, required) {
229
+ if (!Array.isArray(required) || required.length === 0) return true;
230
+ if (!Array.isArray(presentedAmr)) return false;
231
+ var seen = Object.create(null);
232
+ for (var i = 0; i < presentedAmr.length; i += 1) {
233
+ if (typeof presentedAmr[i] === "string") seen[presentedAmr[i]] = true;
234
+ }
235
+ for (var j = 0; j < required.length; j += 1) {
236
+ if (!seen[required[j]]) return false;
237
+ }
238
+ return true;
239
+ }
240
+
241
+ // Reset hook — for tests only. Restores built-in registry.
242
+ function _resetForTests() {
243
+ for (var k in _registry) {
244
+ if (Object.prototype.hasOwnProperty.call(_registry, k)) delete _registry[k];
245
+ }
246
+ for (var bk in BUILTIN_RANKS) {
247
+ if (Object.prototype.hasOwnProperty.call(BUILTIN_RANKS, bk)) {
248
+ _registry[bk] = BUILTIN_RANKS[bk];
249
+ }
250
+ }
251
+ }
252
+
253
+ module.exports = {
254
+ register: register,
255
+ rankOf: rankOf,
256
+ isRegistered: isRegistered,
257
+ listRegistered: listRegistered,
258
+ meets: meets,
259
+ meetsAny: meetsAny,
260
+ amrIncludesPhishingResistant: amrIncludesPhishingResistant,
261
+ amrSatisfiesRequiredList: amrSatisfiesRequiredList,
262
+ BUILTIN_RANKS: BUILTIN_RANKS,
263
+ BUILTIN_AMR: BUILTIN_AMR,
264
+ _resetForTests: _resetForTests,
265
+ };
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * auth_time enforcement helpers per RFC 9470 §3 + OIDC Core 1.0 §2.
4
+ *
5
+ * The `auth_time` JWT claim records the moment the user completed the
6
+ * authentication ceremony — NOT the moment the token was minted. A
7
+ * long-lived session that has been refreshed many times still carries
8
+ * the original `auth_time` until the user re-authenticates.
9
+ *
10
+ * RFC 9470 step-up flows compare the route's `max_age` (seconds since
11
+ * authentication) against the request's `auth_time`:
12
+ *
13
+ * if (now - auth_time > max_age) → challenge with insufficient_user_authentication
14
+ *
15
+ * The framework ships:
16
+ *
17
+ * ageSec(claims, now?) → seconds since auth_time
18
+ * freshEnough(claims, maxAgeSec) → boolean
19
+ * buildClaims({ method, prevAt? }) → { auth_time, amr } scaffold for IdP code
20
+ * readClaims(rawClaims) → normalized { auth_time, acr, amr }
21
+ *
22
+ * This module is method-side: it lives in lib/auth/ and consumes the
23
+ * already-verified JWT claims object (from b.auth.jwt.verify or the
24
+ * external resolver). It does not parse JWTs itself.
25
+ */
26
+
27
+ var validateOpts = require("../validate-opts");
28
+ var C = require("../constants");
29
+ var { AuthError } = require("../framework-error");
30
+
31
+ function _coerceClaim(claim) {
32
+ if (claim == null) return null;
33
+ if (typeof claim === "number" && isFinite(claim)) return claim;
34
+ if (typeof claim === "string") {
35
+ var parsed = parseInt(claim, 10);
36
+ if (isFinite(parsed)) return parsed;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function readClaims(rawClaims) {
42
+ if (!rawClaims || typeof rawClaims !== "object") {
43
+ return { auth_time: null, acr: null, amr: null };
44
+ }
45
+ var authTime = _coerceClaim(rawClaims.auth_time);
46
+ var acr = (typeof rawClaims.acr === "string") ? rawClaims.acr : null;
47
+ var amr = Array.isArray(rawClaims.amr) ? rawClaims.amr.slice() : null;
48
+ return { auth_time: authTime, acr: acr, amr: amr };
49
+ }
50
+
51
+ function ageSec(claims, now) {
52
+ var c = readClaims(claims);
53
+ if (c.auth_time == null) return null;
54
+ var nowSec = (typeof now === "number" && isFinite(now)) ? now : Math.floor(Date.now() / C.TIME.seconds(1));
55
+ if (c.auth_time > nowSec) return 0; // clock skew — treat as fresh
56
+ return nowSec - c.auth_time;
57
+ }
58
+
59
+ function freshEnough(claims, maxAgeSec, now) {
60
+ if (typeof maxAgeSec !== "number" || !isFinite(maxAgeSec) || maxAgeSec < 0) {
61
+ throw new AuthError("auth-stepUp/bad-max-age",
62
+ "auth.authTime.freshEnough: maxAgeSec must be a finite number >= 0 — got " +
63
+ JSON.stringify(maxAgeSec));
64
+ }
65
+ var age = ageSec(claims, now);
66
+ if (age == null) return false;
67
+ return age <= maxAgeSec;
68
+ }
69
+
70
+ function buildClaims(opts) {
71
+ opts = opts || {};
72
+ validateOpts(opts, [
73
+ "method", "prevAt", "now", "amr", "acr",
74
+ ], "auth.authTime.buildClaims");
75
+ var nowSec = (typeof opts.now === "number" && isFinite(opts.now))
76
+ ? opts.now
77
+ : Math.floor(Date.now() / C.TIME.seconds(1));
78
+ var prev = _coerceClaim(opts.prevAt);
79
+ var authTime = nowSec;
80
+ if (typeof opts.method === "string" && opts.method === "refresh" && prev != null) {
81
+ authTime = prev; // refresh preserves prior auth_time
82
+ }
83
+ var out = { auth_time: authTime };
84
+ if (Array.isArray(opts.amr)) out.amr = opts.amr.slice();
85
+ if (typeof opts.acr === "string" && opts.acr.length > 0) out.acr = opts.acr;
86
+ return out;
87
+ }
88
+
89
+ // Operator-side helper: given an existing token's claims and an
90
+ // elevation requirement, return the minimum `max_age` we should allow
91
+ // the IdP to use. Avoids handing the IdP a 0-or-skewed value.
92
+ function recommendMaxAge(opts) {
93
+ opts = opts || {};
94
+ validateOpts(opts, [
95
+ "minSec", "maxSec", "default",
96
+ ], "auth.authTime.recommendMaxAge");
97
+ var min = (typeof opts.minSec === "number") ? opts.minSec : (C.TIME.seconds(60) / C.TIME.seconds(1));
98
+ var max = (typeof opts.maxSec === "number") ? opts.maxSec : (C.TIME.minutes(15) / C.TIME.seconds(1));
99
+ var dflt = (typeof opts.default === "number") ? opts.default : (C.TIME.minutes(5) / C.TIME.seconds(1));
100
+ if (dflt < min) dflt = min;
101
+ if (dflt > max) dflt = max;
102
+ return dflt;
103
+ }
104
+
105
+ module.exports = {
106
+ readClaims: readClaims,
107
+ ageSec: ageSec,
108
+ freshEnough: freshEnough,
109
+ buildClaims: buildClaims,
110
+ recommendMaxAge: recommendMaxAge,
111
+ };
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ /**
3
+ * Step-up elevation grant — short-lived signed tokens that record a
4
+ * successful step-up ceremony. The grant is presented on subsequent
5
+ * sensitive requests (within a tight TTL) so the user does not face a
6
+ * step-up challenge on every action of a multi-step sensitive flow.
7
+ *
8
+ * Tokens are HMAC-SHA3-512 signed (single-key on the operator's side —
9
+ * not asymmetric since these never leave the resource server). The
10
+ * payload binds:
11
+ *
12
+ * subject — the authenticated user / session
13
+ * scope — operator-defined string (e.g. "billing:write",
14
+ * "admin:bulk-update", "phi:read")
15
+ * acr — the achieved ACR value at step-up time
16
+ * amr — the methods that satisfied step-up (RFC 8176)
17
+ * evidence — operator-supplied opaque blob (audit/forensic value
18
+ * only — never trust to drive policy)
19
+ * iat / exp / jti
20
+ *
21
+ * Tokens are revocable: revoke(jti) adds the jti to an in-process
22
+ * deny-set checked by verify(). Operators with multi-node clusters
23
+ * pass `revokedSet` opt to back the deny-set with their own KV.
24
+ *
25
+ * Token format: base64url(JSON-payload) + "." + base64url(HMAC).
26
+ *
27
+ * Per the validation-tier policy: create() throws on bad opts (config-
28
+ * time entry-point); verify() returns structured errors (hot path).
29
+ */
30
+
31
+ var nodeCrypto = require("crypto");
32
+ var validateOpts = require("../validate-opts");
33
+ var lazyRequire = require("../lazy-require");
34
+ var safeJson = require("../safe-json");
35
+ var C = require("../constants");
36
+ var { AuthError } = require("../framework-error");
37
+
38
+ var audit = lazyRequire(function () { return require("../audit"); });
39
+ var fwCrypto = lazyRequire(function () { return require("../crypto"); });
40
+
41
+ var DEFAULT_TTL_SEC = C.TIME.minutes(15) / C.TIME.seconds(1);
42
+ var MAX_TTL_SEC = C.TIME.hours(1) / C.TIME.seconds(1);
43
+ var MIN_TTL_SEC = C.TIME.seconds(30) / C.TIME.seconds(1);
44
+ var DEFAULT_KEY_BYTES = C.BYTES.bytes(64);
45
+ var MAC_BYTES = C.BYTES.bytes(64);
46
+ var MIN_KEY_BYTES = C.BYTES.bytes(32);
47
+
48
+ // In-process state.
49
+ var _signingKey = null;
50
+ var _revokedSet = Object.create(null);
51
+ var _activeGrants = Object.create(null); // jti → { exp, subject } for list()
52
+
53
+ function _ensureSigningKey() {
54
+ if (_signingKey != null) return _signingKey;
55
+ _signingKey = nodeCrypto.randomBytes(DEFAULT_KEY_BYTES);
56
+ return _signingKey;
57
+ }
58
+
59
+ function setSigningKey(keyBuffer) {
60
+ if (!Buffer.isBuffer(keyBuffer) || keyBuffer.length < MIN_KEY_BYTES) {
61
+ throw new AuthError("auth-stepUp/bad-key",
62
+ "auth.stepUp.grant.setSigningKey: keyBuffer must be a Buffer of >= " +
63
+ MIN_KEY_BYTES + " bytes");
64
+ }
65
+ _signingKey = Buffer.from(keyBuffer); // copy — caller may mutate
66
+ }
67
+
68
+ function _b64url(buf) {
69
+ return Buffer.from(buf).toString("base64url");
70
+ }
71
+
72
+ function _b64urlDecode(str) {
73
+ if (typeof str !== "string") return null;
74
+ try { return Buffer.from(str, "base64url"); }
75
+ catch (_e) { return null; }
76
+ }
77
+
78
+ function _macFor(payloadB64) {
79
+ var key = _ensureSigningKey();
80
+ return nodeCrypto.createHmac("sha3-512", key).update(payloadB64).digest();
81
+ }
82
+
83
+ function _timingSafeEqualBuf(a, b) {
84
+ if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) return false;
85
+ return fwCrypto().timingSafeEqual(a, b);
86
+ }
87
+
88
+ function create(opts) {
89
+ opts = opts || {};
90
+ validateOpts(opts, [
91
+ "subject", "scope", "acr", "amr", "evidence",
92
+ "ttlSec", "audience", "now",
93
+ ], "auth.stepUp.grant.create");
94
+ validateOpts.requireNonEmptyString(opts.subject,
95
+ "grant.create: subject", AuthError, "auth-stepUp/bad-grant");
96
+ validateOpts.requireNonEmptyString(opts.scope,
97
+ "grant.create: scope", AuthError, "auth-stepUp/bad-grant");
98
+ if (opts.acr != null) {
99
+ validateOpts.requireNonEmptyString(opts.acr,
100
+ "grant.create: acr", AuthError, "auth-stepUp/bad-grant");
101
+ }
102
+ if (opts.amr != null) {
103
+ if (!Array.isArray(opts.amr)) {
104
+ throw new AuthError("auth-stepUp/bad-grant",
105
+ "grant.create: amr must be an array — got " + typeof opts.amr);
106
+ }
107
+ for (var i = 0; i < opts.amr.length; i += 1) {
108
+ if (typeof opts.amr[i] !== "string") {
109
+ throw new AuthError("auth-stepUp/bad-grant",
110
+ "grant.create: amr[" + i + "] must be a string");
111
+ }
112
+ }
113
+ }
114
+ if (opts.audience != null) {
115
+ validateOpts.requireNonEmptyString(opts.audience,
116
+ "grant.create: audience", AuthError, "auth-stepUp/bad-grant");
117
+ }
118
+ var ttlSec = (typeof opts.ttlSec === "number") ? opts.ttlSec : DEFAULT_TTL_SEC;
119
+ if (!isFinite(ttlSec) || ttlSec < MIN_TTL_SEC) {
120
+ throw new AuthError("auth-stepUp/bad-grant",
121
+ "grant.create: ttlSec must be a finite number >= " +
122
+ MIN_TTL_SEC + " — got " + JSON.stringify(opts.ttlSec));
123
+ }
124
+ if (ttlSec > MAX_TTL_SEC) {
125
+ throw new AuthError("auth-stepUp/bad-grant",
126
+ "grant.create: ttlSec must be <= " + MAX_TTL_SEC +
127
+ " (1h hard ceiling) — got " + ttlSec);
128
+ }
129
+ var nowSec = (typeof opts.now === "number" && isFinite(opts.now))
130
+ ? opts.now : Math.floor(Date.now() / C.TIME.seconds(1));
131
+ var jti = fwCrypto().generateBytes(C.BYTES.bytes(16)).toString("base64url");
132
+ var payload = {
133
+ sub: opts.subject,
134
+ scope: opts.scope,
135
+ acr: opts.acr || null,
136
+ amr: Array.isArray(opts.amr) ? opts.amr.slice() : null,
137
+ aud: opts.audience || null,
138
+ iat: nowSec,
139
+ exp: nowSec + ttlSec,
140
+ jti: jti,
141
+ };
142
+ if (opts.evidence != null) {
143
+ payload.evd = opts.evidence; // opaque pass-through
144
+ }
145
+ var payloadJson = JSON.stringify(payload);
146
+ var payloadB64 = _b64url(payloadJson);
147
+ var mac = _macFor(payloadB64);
148
+ var token = payloadB64 + "." + _b64url(mac);
149
+
150
+ _activeGrants[jti] = { exp: payload.exp, subject: opts.subject, scope: opts.scope };
151
+
152
+ try {
153
+ audit().safeEmit({
154
+ action: "auth.stepup.grant.issued",
155
+ outcome: "success",
156
+ actor: { userId: opts.subject },
157
+ metadata: {
158
+ jti: jti,
159
+ scope: opts.scope,
160
+ acr: opts.acr || null,
161
+ amr: Array.isArray(opts.amr) ? opts.amr.slice() : null,
162
+ ttl: ttlSec,
163
+ },
164
+ });
165
+ } catch (_e) { /* drop-silent */ }
166
+
167
+ return { token: token, expiresAt: payload.exp, jti: jti, payload: payload };
168
+ }
169
+
170
+ function verify(token, opts) {
171
+ opts = opts || {};
172
+ if (typeof token !== "string" || token.length === 0) {
173
+ return { ok: false, error: "no_token", reason: "verify: token must be a non-empty string" };
174
+ }
175
+ validateOpts(opts, [
176
+ "audience", "scope", "subject", "now",
177
+ ], "auth.stepUp.grant.verify");
178
+ var dot = token.indexOf(".");
179
+ if (dot === -1) {
180
+ return { ok: false, error: "malformed", reason: "verify: token missing '.' separator" };
181
+ }
182
+ var payloadB64 = token.slice(0, dot);
183
+ var macB64 = token.slice(dot + 1);
184
+ var presentedMac = _b64urlDecode(macB64);
185
+ if (presentedMac == null || presentedMac.length !== MAC_BYTES) {
186
+ return { ok: false, error: "malformed", reason: "verify: mac decode failed or wrong length" };
187
+ }
188
+ var expectedMac = _macFor(payloadB64);
189
+ if (!_timingSafeEqualBuf(presentedMac, expectedMac)) {
190
+ return { ok: false, error: "bad_mac", reason: "verify: mac mismatch" };
191
+ }
192
+ var payloadBuf = _b64urlDecode(payloadB64);
193
+ if (payloadBuf == null) {
194
+ return { ok: false, error: "malformed", reason: "verify: payload decode failed" };
195
+ }
196
+ var payload;
197
+ try { payload = safeJson.parse(payloadBuf.toString("utf8"), { maxBytes: C.BYTES.kib(8) }); }
198
+ catch (_e) { return { ok: false, error: "malformed", reason: "verify: payload JSON parse failed" }; }
199
+ if (!payload || typeof payload !== "object") {
200
+ return { ok: false, error: "malformed", reason: "verify: payload not an object" };
201
+ }
202
+ var nowSec = (typeof opts.now === "number" && isFinite(opts.now))
203
+ ? opts.now : Math.floor(Date.now() / C.TIME.seconds(1));
204
+ if (typeof payload.exp !== "number" || payload.exp < nowSec) {
205
+ return { ok: false, error: "expired", reason: "verify: token expired (exp=" + payload.exp + ", now=" + nowSec + ")" };
206
+ }
207
+ if (typeof payload.iat !== "number" ||
208
+ payload.iat > nowSec + (C.TIME.seconds(60) / C.TIME.seconds(1))) {
209
+ return { ok: false, error: "future_iat", reason: "verify: iat is in the future" };
210
+ }
211
+ if (opts.audience != null && payload.aud !== opts.audience) {
212
+ return { ok: false, error: "audience_mismatch",
213
+ reason: "verify: audience " + JSON.stringify(payload.aud) +
214
+ " does not match required " + JSON.stringify(opts.audience) };
215
+ }
216
+ if (opts.scope != null && payload.scope !== opts.scope) {
217
+ return { ok: false, error: "scope_mismatch",
218
+ reason: "verify: scope " + JSON.stringify(payload.scope) +
219
+ " does not match required " + JSON.stringify(opts.scope) };
220
+ }
221
+ if (opts.subject != null && payload.sub !== opts.subject) {
222
+ return { ok: false, error: "subject_mismatch",
223
+ reason: "verify: subject " + JSON.stringify(payload.sub) +
224
+ " does not match required " + JSON.stringify(opts.subject) };
225
+ }
226
+ if (typeof payload.jti === "string" && _revokedSet[payload.jti] === true) {
227
+ return { ok: false, error: "revoked", reason: "verify: jti has been revoked" };
228
+ }
229
+
230
+ try {
231
+ audit().safeEmit({
232
+ action: "auth.stepup.grant.consumed",
233
+ outcome: "success",
234
+ actor: { userId: payload.sub },
235
+ metadata: {
236
+ jti: payload.jti || null,
237
+ scope: payload.scope,
238
+ aud: payload.aud || null,
239
+ },
240
+ });
241
+ } catch (_e) { /* drop-silent */ }
242
+
243
+ return { ok: true, payload: payload };
244
+ }
245
+
246
+ function revoke(jti, opts) {
247
+ opts = opts || {};
248
+ validateOpts(opts, ["reason"], "auth.stepUp.grant.revoke");
249
+ if (typeof jti !== "string" || jti.length === 0) {
250
+ throw new AuthError("auth-stepUp/bad-jti",
251
+ "grant.revoke: jti must be a non-empty string — got " + JSON.stringify(jti));
252
+ }
253
+ _revokedSet[jti] = true;
254
+ var prior = _activeGrants[jti];
255
+ delete _activeGrants[jti];
256
+
257
+ try {
258
+ audit().safeEmit({
259
+ action: "auth.stepup.grant.revoked",
260
+ outcome: "success",
261
+ actor: { userId: prior && prior.subject || null },
262
+ metadata: { jti: jti, reason: opts.reason || null },
263
+ });
264
+ } catch (_e) { /* drop-silent */ }
265
+
266
+ return { ok: true, jti: jti };
267
+ }
268
+
269
+ function isRevoked(jti) {
270
+ if (typeof jti !== "string") return false;
271
+ return _revokedSet[jti] === true;
272
+ }
273
+
274
+ function list() {
275
+ var nowSec = Math.floor(Date.now() / C.TIME.seconds(1));
276
+ var out = [];
277
+ for (var jti in _activeGrants) {
278
+ if (Object.prototype.hasOwnProperty.call(_activeGrants, jti)) {
279
+ var entry = _activeGrants[jti];
280
+ if (entry.exp > nowSec && !_revokedSet[jti]) {
281
+ out.push({ jti: jti, subject: entry.subject, scope: entry.scope, exp: entry.exp });
282
+ }
283
+ }
284
+ }
285
+ out.sort(function (a, b) { return a.exp - b.exp; });
286
+ return out;
287
+ }
288
+
289
+ function _resetForTests() {
290
+ _signingKey = null;
291
+ _revokedSet = Object.create(null);
292
+ _activeGrants = Object.create(null);
293
+ }
294
+
295
+ module.exports = {
296
+ create: create,
297
+ verify: verify,
298
+ revoke: revoke,
299
+ isRevoked: isRevoked,
300
+ list: list,
301
+ setSigningKey: setSigningKey,
302
+ DEFAULT_TTL_SEC: DEFAULT_TTL_SEC,
303
+ MAX_TTL_SEC: MAX_TTL_SEC,
304
+ MIN_TTL_SEC: MIN_TTL_SEC,
305
+ _resetForTests: _resetForTests,
306
+ };