@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
package/lib/auth/jwt.js CHANGED
@@ -147,6 +147,19 @@ async function sign(claims, opts) {
147
147
  if (typeof opts.expiresInSec === "number" && payload.exp === undefined) {
148
148
  payload.exp = nowSec + opts.expiresInSec;
149
149
  }
150
+ // Auto-mint jti when the token has an expiry but no operator-set
151
+ // jti. The replay-defense path on verify() requires every replay-
152
+ // protected token to carry a jti; without auto-mint, an operator
153
+ // who configured replayStore on verify but forgot to set jti on
154
+ // sign produces tokens that never replay-protect — and the
155
+ // failure surfaces only at first replay attempt (via the
156
+ // verifier's "missing-jti" throw). Auto-mint closes the silent
157
+ // hole; operators who explicitly want a deterministic jti pass
158
+ // opts.jti themselves.
159
+ if (payload.exp !== undefined && payload.jti === undefined) {
160
+ var fwCryptoJti = require("../crypto"); // allow:inline-require — circular-load defense (crypto imports jwt? no — but use lazy form to keep parity)
161
+ payload.jti = fwCryptoJti.generateBytes(C.BYTES.bytes(16)).toString("base64url");
162
+ }
150
163
  if (typeof opts.notBeforeSec === "number" && payload.nbf === undefined) {
151
164
  payload.nbf = nowSec + opts.notBeforeSec;
152
165
  }
@@ -227,12 +227,25 @@ function create(opts) {
227
227
  } catch (_e) { /* audit best-effort */ }
228
228
  }
229
229
 
230
+ // Cache failures fail-OPEN by design (per the framework's
231
+ // documented brute-force-lockout posture — rather than crash the
232
+ // request, allow the attempt). The signal MUST land somewhere
233
+ // visible regardless of operator wiring: observability picks it up
234
+ // when wired, and audit picks it up when wired. Without the audit
235
+ // path a deployment running with no observability + a degraded
236
+ // cache silently gets brute-force-protection-disabled.
237
+ function _signalCacheError(op) {
238
+ _emitObs("auth.lockout.cache_error", { namespace: namespace, op: op });
239
+ _emitAudit("auth.lockout.cache_error", "<system>", "failure",
240
+ { namespace: namespace, op: op }, null);
241
+ }
242
+
230
243
  async function _readState(key) {
231
244
  try {
232
245
  var raw = await cache.get(_scopedKey(key));
233
246
  return raw || null;
234
247
  } catch (_e) {
235
- _emitObs("auth.lockout.cache_error", { namespace: namespace, op: "get" });
248
+ _signalCacheError("get");
236
249
  return null;
237
250
  }
238
251
  }
@@ -241,7 +254,7 @@ function create(opts) {
241
254
  try {
242
255
  await cache.set(_scopedKey(key), state, { ttlMs: ttlMs });
243
256
  } catch (_e) {
244
- _emitObs("auth.lockout.cache_error", { namespace: namespace, op: "set" });
257
+ _signalCacheError("set");
245
258
  }
246
259
  }
247
260
 
@@ -249,7 +262,7 @@ function create(opts) {
249
262
  try {
250
263
  await cache.del(_scopedKey(key));
251
264
  } catch (_e) {
252
- _emitObs("auth.lockout.cache_error", { namespace: namespace, op: "del" });
265
+ _signalCacheError("del");
253
266
  }
254
267
  }
255
268
 
package/lib/auth/oauth.js CHANGED
@@ -482,6 +482,20 @@ function create(opts) {
482
482
  throw new OAuthError("auth-oauth/no-verifier",
483
483
  "exchangeCode: opts.verifier is required when PKCE is on (default)");
484
484
  }
485
+ // Nonce enforcement on OIDC paths. authorizationUrl() always
486
+ // emits a nonce when isOidc; if the operator forgot to thread it
487
+ // through to exchangeCode, _normalizeTokens silently skipped the
488
+ // nonce check on the ID token and a captured token from another
489
+ // browser session could be replayed without detection. Throw
490
+ // loudly so the operator sees the bug at config time, not at
491
+ // first-replay-attempt time.
492
+ if (isOidc && eopts.nonce === undefined && eopts.skipNonceCheck !== true) {
493
+ throw new OAuthError("auth-oauth/no-nonce",
494
+ "exchangeCode: nonce is required on OIDC flows. Pass the " +
495
+ "value returned from authorizationUrl() through to exchangeCode " +
496
+ "({ code, state, verifier, nonce }). Operators with a deliberate " +
497
+ "no-nonce flow must pass `skipNonceCheck: true` (audited reason).");
498
+ }
485
499
  var endpoint = await _resolveEndpoint("tokenEndpoint");
486
500
  var body = new URLSearchParams();
487
501
  body.set("grant_type", "authorization_code");
@@ -492,7 +506,7 @@ function create(opts) {
492
506
  if (eopts.verifier) body.set("code_verifier", eopts.verifier);
493
507
 
494
508
  var tokens = await _postForm(endpoint, body);
495
- return await _normalizeTokens(tokens, { nonce: eopts.nonce });
509
+ return await _normalizeTokens(tokens, { nonce: eopts.nonce, skipNonceCheck: eopts.skipNonceCheck });
496
510
  }
497
511
 
498
512
  async function refreshAccessToken(refreshToken) {
@@ -448,20 +448,40 @@ function policy(opts) {
448
448
  }
449
449
  var bodyText = Buffer.isBuffer(resp.body) ? resp.body.toString("utf8") : String(resp.body);
450
450
  var lines = bodyText.split(/\r?\n/);
451
+ var goodLines = 0;
452
+ var badLines = 0;
451
453
  for (var li = 0; li < lines.length; li++) {
452
454
  var line = lines[li].trim();
453
455
  if (line.length === 0) continue;
454
456
  var colon = line.indexOf(":");
455
- if (colon < 0) continue;
457
+ if (colon < 0) { badLines += 1; continue; }
456
458
  var hashSuffix = line.slice(0, colon).toUpperCase();
457
459
  var count = parseInt(line.slice(colon + 1), 10);
460
+ if (!isFinite(count)) { badLines += 1; continue; }
461
+ goodLines += 1;
458
462
  if (timingSafeEqual(Buffer.from(hashSuffix, "utf8"), Buffer.from(suffix, "utf8")) &&
459
- isFinite(count) && count >= p.breachThreshold) {
463
+ count >= p.breachThreshold) {
460
464
  return _fail("breached",
461
465
  "plaintext appears in HaveIBeenPwned with count " + count +
462
466
  " (threshold " + p.breachThreshold + ")");
463
467
  }
464
468
  }
469
+ // If a hostile / poisoned mirror returned a response shaped like
470
+ // HIBP but with mostly-unparseable counts, the original loop
471
+ // skipped them silently and reported breachCheckCount=0 — i.e.
472
+ // the operator saw "looks fine" against a body that was never
473
+ // actually verifiable. When more than half the lines fail to
474
+ // parse, treat the response as unverifiable and apply the
475
+ // operator's fail-closed posture.
476
+ if (goodLines + badLines > 0 && badLines * 2 > goodLines) {
477
+ if (p.failClosed) {
478
+ return _fail("breach-check-failed",
479
+ "HIBP response was mostly-unparseable (good=" + goodLines +
480
+ ", bad=" + badLines + ") — possible poisoned mirror");
481
+ }
482
+ return _ok({ breachCheckSkipped: true,
483
+ breachCheckSkipReason: "hibp-response-mostly-unparseable" });
484
+ }
465
485
  return _ok({ breachCheckCount: 0 });
466
486
  }
467
487
  return _ok();
@@ -95,7 +95,7 @@ function create(opts) {
95
95
  "issuer.create: activeKid \"" + activeKid + "\" is not in the keys array");
96
96
  }
97
97
  var defaultTtlMs = opts.defaultTtlMs || C.TIME.days(90);
98
- var defaultHashAlg = opts.defaultHashAlg || "sha-256";
98
+ var defaultHashAlg = opts.defaultHashAlg || "sha3-512";
99
99
  var auditOn = opts.auditOn !== false;
100
100
 
101
101
  var stats = {
@@ -137,7 +137,7 @@ function create(opts) {
137
137
  claims: spec.claims || {},
138
138
  selectivelyDisclosed: spec.selectivelyDisclosed || [],
139
139
  issuerKey: key.privateKey,
140
- algorithm: key.algorithm || "ES256",
140
+ algorithm: key.algorithm || "ML-DSA-87",
141
141
  hashAlg: spec.hashAlg || defaultHashAlg,
142
142
  ttlMs: spec.ttlMs || defaultTtlMs,
143
143
  holderKey: spec.holderKey || null,
@@ -80,8 +80,13 @@ var SUPPORTED_HASH_ALGS = Object.freeze({
80
80
  "sha3-512": "sha3-512",
81
81
  });
82
82
 
83
- var DEFAULT_ALG = "ES256";
84
- var DEFAULT_HASH_ALG = "sha-256";
83
+ // Defaults are PQC-first per the framework's hard rule §2 — operators
84
+ // who must interop with ES256-only verifiers today opt in via the
85
+ // `compatibilityProfile: "spec-default"` shape on the issuer/holder
86
+ // surfaces, OR pass `algorithm: "ES256"` + `hashAlg: "sha-256"`
87
+ // explicitly with an audited reason.
88
+ var DEFAULT_ALG = "ML-DSA-87";
89
+ var DEFAULT_HASH_ALG = "sha3-512";
85
90
 
86
91
  function _b64uEncode(str) {
87
92
  return Buffer.from(str, "utf8").toString("base64url");
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ /**
3
+ * Step-up policy DSL — compose RFC 9470 step-up requirements as a
4
+ * fluent expression rather than a flat object. Useful for routes that
5
+ * need "ACR >= loa3 OR a fresh hardware-key auth within the last 5 min".
6
+ *
7
+ * var policy = b.auth.stepUp.policy
8
+ * .acr("loa3")
9
+ * .and(b.auth.stepUp.policy.maxAge(300))
10
+ * .or(
11
+ * b.auth.stepUp.policy.amr(["hwk", "pop"])
12
+ * .and(b.auth.stepUp.policy.maxAge(120))
13
+ * );
14
+ *
15
+ * var result = policy.evaluate(claims);
16
+ *
17
+ * var middleware = policy.middleware({ realm: "billing-api" });
18
+ *
19
+ * The policy compiles down to RFC 9470 challenges by extracting the
20
+ * outermost-feasible (acr, maxAge, amr, ...) tuple. When an operator's
21
+ * policy is genuinely or-of-and-of-..., the challenge emitted is the
22
+ * UNION of individually-required atoms (so the IdP knows what to ask
23
+ * for) plus an `error_description` hint that there are alternatives.
24
+ *
25
+ * The DSL is pure. All node types are immutable once constructed;
26
+ * chaining returns a new policy object.
27
+ */
28
+
29
+ var lazyRequire = require("../lazy-require");
30
+ var validateOpts = require("../validate-opts");
31
+ var { AuthError } = require("../framework-error");
32
+
33
+ var stepUp = lazyRequire(function () { return require("./step-up"); });
34
+ var requireStepUp = lazyRequire(function () { return require("../middleware/require-step-up"); });
35
+
36
+ function _mkNode(spec) {
37
+ spec.evaluate = function (claims) {
38
+ return spec._run(claims);
39
+ };
40
+ spec.toRequirement = function () {
41
+ return spec._toReq();
42
+ };
43
+ spec.and = function (other) {
44
+ return _and(spec, other);
45
+ };
46
+ spec.or = function (other) {
47
+ return _or(spec, other);
48
+ };
49
+ spec.not = function () {
50
+ return _not(spec);
51
+ };
52
+ spec.middleware = function (opts) {
53
+ opts = opts || {};
54
+ return requireStepUp().create(Object.assign({}, opts, {
55
+ requirement: spec._toReq(),
56
+ }));
57
+ };
58
+ return Object.freeze(spec);
59
+ }
60
+
61
+ function acr(value) {
62
+ validateOpts.requireNonEmptyString(value, "policy.acr: value", AuthError, "auth-stepUp/bad-acr");
63
+ return _mkNode({
64
+ kind: "acr",
65
+ value: value,
66
+ _run: function (claims) {
67
+ var result = stepUp().evaluate({
68
+ claims: claims,
69
+ requirement: { acr: value },
70
+ });
71
+ return { ok: result.ok === true, atom: "acr:" + value, reason: result.reason || null };
72
+ },
73
+ _toReq: function () { return { acr: value }; },
74
+ });
75
+ }
76
+
77
+ function acrAny(values) {
78
+ if (!Array.isArray(values) || values.length === 0) {
79
+ throw new AuthError("auth-stepUp/bad-policy",
80
+ "policy.acrAny: values must be a non-empty array");
81
+ }
82
+ for (var i = 0; i < values.length; i += 1) {
83
+ validateOpts.requireNonEmptyString(values[i],
84
+ "policy.acrAny: values[" + i + "]", AuthError, "auth-stepUp/bad-acr");
85
+ }
86
+ var copy = values.slice();
87
+ return _mkNode({
88
+ kind: "acrAny",
89
+ values: copy,
90
+ _run: function (claims) {
91
+ var result = stepUp().evaluate({
92
+ claims: claims,
93
+ requirement: { acrValues: copy },
94
+ });
95
+ return { ok: result.ok === true, atom: "acrAny:" + copy.join(","), reason: result.reason || null };
96
+ },
97
+ _toReq: function () { return { acrValues: copy }; },
98
+ });
99
+ }
100
+
101
+ function amr(required) {
102
+ if (!Array.isArray(required) || required.length === 0) {
103
+ throw new AuthError("auth-stepUp/bad-policy",
104
+ "policy.amr: required must be a non-empty array");
105
+ }
106
+ for (var i = 0; i < required.length; i += 1) {
107
+ validateOpts.requireNonEmptyString(required[i],
108
+ "policy.amr: required[" + i + "]", AuthError, "auth-stepUp/bad-amr");
109
+ }
110
+ var copy = required.slice();
111
+ return _mkNode({
112
+ kind: "amr",
113
+ required: copy,
114
+ _run: function (claims) {
115
+ var result = stepUp().evaluate({
116
+ claims: claims,
117
+ requirement: { requiredAmr: copy },
118
+ });
119
+ return { ok: result.ok === true, atom: "amr:" + copy.join("+"), reason: result.reason || null };
120
+ },
121
+ _toReq: function () { return { requiredAmr: copy }; },
122
+ });
123
+ }
124
+
125
+ function phishingResistant() {
126
+ return _mkNode({
127
+ kind: "phishingResistant",
128
+ _run: function (claims) {
129
+ var result = stepUp().evaluate({
130
+ claims: claims,
131
+ requirement: { phishingResistant: true },
132
+ });
133
+ return { ok: result.ok === true, atom: "phr", reason: result.reason || null };
134
+ },
135
+ _toReq: function () { return { phishingResistant: true }; },
136
+ });
137
+ }
138
+
139
+ function maxAge(seconds) {
140
+ if (typeof seconds !== "number" || !isFinite(seconds) || seconds < 0) {
141
+ throw new AuthError("auth-stepUp/bad-policy",
142
+ "policy.maxAge: seconds must be a finite number >= 0 — got " +
143
+ JSON.stringify(seconds));
144
+ }
145
+ return _mkNode({
146
+ kind: "maxAge",
147
+ seconds: seconds,
148
+ _run: function (claims) {
149
+ var result = stepUp().evaluate({
150
+ claims: claims,
151
+ requirement: { maxAge: seconds },
152
+ });
153
+ return { ok: result.ok === true, atom: "maxAge:" + seconds, reason: result.reason || null };
154
+ },
155
+ _toReq: function () { return { maxAge: seconds }; },
156
+ });
157
+ }
158
+
159
+ function custom(name, fn) {
160
+ validateOpts.requireNonEmptyString(name, "policy.custom: name", AuthError, "auth-stepUp/bad-policy");
161
+ if (typeof fn !== "function") {
162
+ throw new AuthError("auth-stepUp/bad-policy",
163
+ "policy.custom: fn must be a function — got " + typeof fn);
164
+ }
165
+ return _mkNode({
166
+ kind: "custom",
167
+ name: name,
168
+ _run: function (claims) {
169
+ var ok = false;
170
+ try { ok = fn(claims) === true; } catch (_e) { ok = false; }
171
+ return { ok: ok, atom: "custom:" + name, reason: ok ? null : "custom predicate '" + name + "' returned false" };
172
+ },
173
+ _toReq: function () {
174
+ throw new AuthError("auth-stepUp/policy-no-challenge",
175
+ "policy.custom: cannot translate to RFC 9470 challenge — wrap in .or() with a translatable atom or use .middleware({ requirement: ... })");
176
+ },
177
+ });
178
+ }
179
+
180
+ function _and(left, right) {
181
+ return _mkNode({
182
+ kind: "and",
183
+ left: left,
184
+ right: right,
185
+ _run: function (claims) {
186
+ var l = left._run(claims);
187
+ if (!l.ok) return { ok: false, atom: "and(" + l.atom + ")", reason: l.reason };
188
+ var r = right._run(claims);
189
+ if (!r.ok) return { ok: false, atom: "and(" + l.atom + "," + r.atom + ")", reason: r.reason };
190
+ return { ok: true, atom: "and(" + l.atom + "," + r.atom + ")", reason: null };
191
+ },
192
+ _toReq: function () {
193
+ var lr = left._toReq();
194
+ var rr = right._toReq();
195
+ return _mergeAnd(lr, rr);
196
+ },
197
+ });
198
+ }
199
+
200
+ function _or(left, right) {
201
+ return _mkNode({
202
+ kind: "or",
203
+ left: left,
204
+ right: right,
205
+ _run: function (claims) {
206
+ var l = left._run(claims);
207
+ if (l.ok) return { ok: true, atom: "or(" + l.atom + ")", reason: null };
208
+ var r = right._run(claims);
209
+ if (r.ok) return { ok: true, atom: "or(" + r.atom + ")", reason: null };
210
+ return {
211
+ ok: false,
212
+ atom: "or(" + l.atom + "," + r.atom + ")",
213
+ reason: l.reason + " AND " + r.reason,
214
+ };
215
+ },
216
+ _toReq: function () {
217
+ // RFC 9470 doesn't support OR semantics in WWW-Authenticate. We
218
+ // pick the LEFT branch and emit its challenge — operator can
219
+ // override via .middleware({ requirement }).
220
+ return left._toReq();
221
+ },
222
+ });
223
+ }
224
+
225
+ function _not(inner) {
226
+ return _mkNode({
227
+ kind: "not",
228
+ inner: inner,
229
+ _run: function (claims) {
230
+ var i = inner._run(claims);
231
+ return { ok: !i.ok, atom: "not(" + i.atom + ")", reason: i.ok ? "not(" + i.atom + ") matched" : null };
232
+ },
233
+ _toReq: function () {
234
+ throw new AuthError("auth-stepUp/policy-no-challenge",
235
+ "policy.not: cannot translate to RFC 9470 challenge");
236
+ },
237
+ });
238
+ }
239
+
240
+ function _mergeAnd(a, b) {
241
+ var out = {};
242
+ if (a.acr || b.acr) {
243
+ if (a.acr && b.acr && a.acr !== b.acr) {
244
+ throw new AuthError("auth-stepUp/policy-conflict",
245
+ "policy.and: conflicting acr requirements " +
246
+ JSON.stringify(a.acr) + " and " + JSON.stringify(b.acr));
247
+ }
248
+ out.acr = a.acr || b.acr;
249
+ }
250
+ if (a.acrValues || b.acrValues) {
251
+ out.acrValues = (a.acrValues || []).concat(b.acrValues || []);
252
+ }
253
+ if (a.maxAge != null && b.maxAge != null) {
254
+ out.maxAge = Math.min(a.maxAge, b.maxAge); // tighter wins
255
+ } else if (a.maxAge != null) {
256
+ out.maxAge = a.maxAge;
257
+ } else if (b.maxAge != null) {
258
+ out.maxAge = b.maxAge;
259
+ }
260
+ if (a.requiredAmr || b.requiredAmr) {
261
+ var combined = (a.requiredAmr || []).concat(b.requiredAmr || []);
262
+ var seen = Object.create(null);
263
+ out.requiredAmr = [];
264
+ for (var i = 0; i < combined.length; i += 1) {
265
+ if (!seen[combined[i]]) {
266
+ seen[combined[i]] = true;
267
+ out.requiredAmr.push(combined[i]);
268
+ }
269
+ }
270
+ }
271
+ if (a.phishingResistant === true || b.phishingResistant === true) {
272
+ out.phishingResistant = true;
273
+ }
274
+ return out;
275
+ }
276
+
277
+ // ---- Common preset policies operators reach for ----
278
+
279
+ var C = require("../constants");
280
+ var SEC_5_MIN = C.TIME.minutes(5) / C.TIME.seconds(1);
281
+ var SEC_2_MIN = C.TIME.minutes(2) / C.TIME.seconds(1);
282
+ var SEC_15_MIN = C.TIME.minutes(15) / C.TIME.seconds(1);
283
+ var SEC_1_MIN = C.TIME.minutes(1) / C.TIME.seconds(1);
284
+
285
+ var PRESETS = {
286
+ // Sensitive write: ACR >= loa2 + auth_time within 5 min
287
+ sensitiveWrite: function () {
288
+ return acr("loa2").and(maxAge(SEC_5_MIN));
289
+ },
290
+ // Admin bulk: ACR >= loa3 + max_age 5 min + phishing-resistant
291
+ adminBulk: function () {
292
+ return acr("loa3").and(maxAge(SEC_5_MIN)).and(phishingResistant());
293
+ },
294
+ // Financial: phr-class hardware + max_age 2 min
295
+ financial: function () {
296
+ return acr("loa3").and(amr(["hwk"])).and(maxAge(SEC_2_MIN));
297
+ },
298
+ // PHI read: ACR >= loa2 + max_age 15 min
299
+ phiRead: function () {
300
+ return acr("loa2").and(maxAge(SEC_15_MIN));
301
+ },
302
+ // PHI write: ACR >= loa3 + max_age 5 min + phishing-resistant
303
+ phiWrite: function () {
304
+ return acr("loa3").and(maxAge(SEC_5_MIN)).and(phishingResistant());
305
+ },
306
+ // Account-recovery: phishing-resistant + max_age 1 min
307
+ accountRecovery: function () {
308
+ return phishingResistant().and(maxAge(SEC_1_MIN));
309
+ },
310
+ };
311
+
312
+ function preset(name) {
313
+ if (!Object.prototype.hasOwnProperty.call(PRESETS, name)) {
314
+ throw new AuthError("auth-stepUp/bad-preset",
315
+ "policy.preset: unknown preset " + JSON.stringify(name) +
316
+ " — valid presets: " + Object.keys(PRESETS).join(", "));
317
+ }
318
+ return PRESETS[name]();
319
+ }
320
+
321
+ function listPresets() {
322
+ return Object.keys(PRESETS).slice();
323
+ }
324
+
325
+ module.exports = {
326
+ acr: acr,
327
+ acrAny: acrAny,
328
+ amr: amr,
329
+ phishingResistant: phishingResistant,
330
+ maxAge: maxAge,
331
+ custom: custom,
332
+ preset: preset,
333
+ listPresets: listPresets,
334
+ PRESETS: PRESETS,
335
+ };