@blamejs/blamejs-shop 0.3.69 → 0.3.71

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 (92) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +254 -1
  4. package/lib/asset-manifest.json +1 -1
  5. package/lib/vendor/MANIFEST.json +95 -83
  6. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
  7. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +1 -1
  8. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +1 -1
  9. package/lib/vendor/blamejs/.github/workflows/ci.yml +10 -10
  10. package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
  11. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +2 -2
  12. package/lib/vendor/blamejs/.github/workflows/release-container.yml +4 -4
  13. package/lib/vendor/blamejs/.github/workflows/scorecard.yml +2 -2
  14. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
  15. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  16. package/lib/vendor/blamejs/README.md +1 -1
  17. package/lib/vendor/blamejs/SECURITY.md +2 -0
  18. package/lib/vendor/blamejs/api-snapshot.json +108 -4
  19. package/lib/vendor/blamejs/lib/auth/oauth.js +736 -1
  20. package/lib/vendor/blamejs/lib/auth/oid4vci.js +124 -5
  21. package/lib/vendor/blamejs/lib/auth/oid4vp.js +14 -4
  22. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc-holder.js +46 -1
  23. package/lib/vendor/blamejs/lib/break-glass.js +1 -2
  24. package/lib/vendor/blamejs/lib/config.js +28 -31
  25. package/lib/vendor/blamejs/lib/crypto-field.js +274 -17
  26. package/lib/vendor/blamejs/lib/dora.js +8 -5
  27. package/lib/vendor/blamejs/lib/dsr.js +2 -2
  28. package/lib/vendor/blamejs/lib/flag-evaluation-context.js +7 -0
  29. package/lib/vendor/blamejs/lib/guard-html-wcag-aria.js +4 -2
  30. package/lib/vendor/blamejs/lib/guard-html-wcag-forms.js +4 -2
  31. package/lib/vendor/blamejs/lib/guard-html-wcag-tables.js +4 -2
  32. package/lib/vendor/blamejs/lib/guard-html-wcag-tagwalk.js +20 -0
  33. package/lib/vendor/blamejs/lib/guard-html-wcag.js +1 -1
  34. package/lib/vendor/blamejs/lib/honeytoken.js +27 -20
  35. package/lib/vendor/blamejs/lib/mail-auth.js +333 -0
  36. package/lib/vendor/blamejs/lib/mail-deploy.js +1 -1
  37. package/lib/vendor/blamejs/lib/mail-send-deliver.js +13 -4
  38. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +140 -13
  39. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +3 -0
  40. package/lib/vendor/blamejs/lib/middleware/csp-report.js +13 -9
  41. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +115 -14
  42. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +3 -0
  43. package/lib/vendor/blamejs/lib/middleware/scim-server.js +297 -19
  44. package/lib/vendor/blamejs/lib/middleware/security-headers.js +47 -0
  45. package/lib/vendor/blamejs/lib/middleware/security-txt.js +1 -2
  46. package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +1 -2
  47. package/lib/vendor/blamejs/lib/network-smtp-policy.js +4 -4
  48. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +11 -2
  49. package/lib/vendor/blamejs/lib/observability-tracer.js +1 -1
  50. package/lib/vendor/blamejs/lib/observability.js +39 -1
  51. package/lib/vendor/blamejs/lib/problem-details.js +56 -11
  52. package/lib/vendor/blamejs/lib/pubsub-cluster.js +16 -3
  53. package/lib/vendor/blamejs/lib/queue-sqs.js +20 -2
  54. package/lib/vendor/blamejs/lib/redis-client.js +32 -4
  55. package/lib/vendor/blamejs/lib/safe-redirect.js +16 -2
  56. package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +3 -2
  57. package/lib/vendor/blamejs/package.json +1 -1
  58. package/lib/vendor/blamejs/release-notes/v0.14.20.json +73 -0
  59. package/lib/vendor/blamejs/release-notes/v0.14.21.json +98 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +339 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +37 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +22 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +315 -5
  64. package/lib/vendor/blamejs/test/layer-0-primitives/config.test.js +46 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +176 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +86 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +38 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +29 -0
  69. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +236 -1
  70. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +190 -0
  71. package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +23 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/guard-html-wcag.test.js +59 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/honeytoken.test.js +26 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy-tlsrpt.test.js +16 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +108 -0
  77. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +269 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +28 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +39 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +37 -0
  81. package/lib/vendor/blamejs/test/layer-0-primitives/problem-details.test.js +79 -0
  82. package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +49 -0
  83. package/lib/vendor/blamejs/test/layer-0-primitives/queue-sqs.test.js +48 -0
  84. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +60 -0
  85. package/lib/vendor/blamejs/test/layer-0-primitives/safe-redirect.test.js +118 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +259 -0
  87. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +46 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +113 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/security-txt.test.js +111 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +62 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/smtp-policy.test.js +39 -0
  92. package/package.json +1 -1
@@ -1144,6 +1144,338 @@ async function testApiEncryptObservabilityCounters() {
1144
1144
  check("observability emits apiEncrypt.session.created on bootstrap", hasCreated);
1145
1145
  }
1146
1146
 
1147
+ // _clusterStyleSessionStore — models a multi-replica session backend
1148
+ // where every get() deserialises a FRESH copy of the stored row (as a
1149
+ // real cluster store does — each call returns its own object, never a
1150
+ // shared reference) and get/set/delete are genuinely async (yield to the
1151
+ // microtask queue). This is the shape that exposes a non-atomic
1152
+ // read-modify-write counter guard: two concurrent requests both read the
1153
+ // same lastReqCtr from independent copies and both pass a copy-local
1154
+ // check. `gets` counts reads so the test can confirm both requests
1155
+ // actually raced through get() before either set() landed.
1156
+ function _clusterStyleSessionStore() {
1157
+ var rows = Object.create(null);
1158
+ var gets = 0;
1159
+ return {
1160
+ get: async function (sid) {
1161
+ gets += 1;
1162
+ await Promise.resolve(); // force a microtask yield
1163
+ var raw = rows[sid];
1164
+ if (raw === undefined) return null;
1165
+ return JSON.parse(raw); // fresh deserialised copy
1166
+ },
1167
+ set: async function (sid, row) {
1168
+ await Promise.resolve();
1169
+ rows[sid] = JSON.stringify(row);
1170
+ },
1171
+ delete: async function (sid) {
1172
+ await Promise.resolve();
1173
+ delete rows[sid];
1174
+ },
1175
+ _gets: function () { return gets; },
1176
+ };
1177
+ }
1178
+
1179
+ async function testApiEncryptCreateRejectsBadReplayWindow() {
1180
+ var keypair = _serverKeypair();
1181
+ var threw = null;
1182
+ try {
1183
+ b.middleware.apiEncrypt({ keypair: keypair, audit: false, replayWindowMs: "5m" });
1184
+ } catch (e) { threw = e; }
1185
+ check("apiEncrypt: create() rejects string replayWindowMs at boot",
1186
+ threw && /replayWindowMs/.test(threw.message));
1187
+ threw = null;
1188
+ try {
1189
+ b.middleware.apiEncrypt({ keypair: keypair, audit: false, replayWindowMs: 0 });
1190
+ } catch (e) { threw = e; }
1191
+ check("apiEncrypt: create() rejects replayWindowMs=0 (disables staleness defense)",
1192
+ threw && /replayWindowMs/.test(threw.message));
1193
+ threw = null;
1194
+ try {
1195
+ b.middleware.apiEncrypt({ keypair: keypair, audit: false, maxDecryptedBytes: "lots" });
1196
+ } catch (e) { threw = e; }
1197
+ check("apiEncrypt: create() rejects non-numeric maxDecryptedBytes",
1198
+ threw && /maxDecryptedBytes/.test(threw.message));
1199
+ threw = null;
1200
+ try {
1201
+ b.middleware.apiEncrypt({ keypair: keypair, audit: false, pruneIntervalMs: "soon" });
1202
+ } catch (e) { threw = e; }
1203
+ check("apiEncrypt: create() rejects non-numeric pruneIntervalMs",
1204
+ threw && /pruneIntervalMs/.test(threw.message));
1205
+ // Sanity: a valid numeric replayWindowMs still builds.
1206
+ var ok = b.middleware.apiEncrypt({ keypair: keypair, audit: false, replayWindowMs: 30000 });
1207
+ check("apiEncrypt: create() accepts numeric replayWindowMs", typeof ok === "function");
1208
+ }
1209
+
1210
+ async function testApiEncryptClientRejectsBadMaxDecryptedBytes() {
1211
+ var keypair = _serverKeypair();
1212
+ var threw = null;
1213
+ try {
1214
+ b.middleware.apiEncrypt.client({ pubkey: keypair, maxDecryptedBytes: "5m" });
1215
+ } catch (e) { threw = e; }
1216
+ check("apiEncrypt.client: rejects string maxDecryptedBytes at boot",
1217
+ threw && /maxDecryptedBytes/.test(threw.message));
1218
+ }
1219
+
1220
+ async function testApiEncryptHttpClientRejectsBadMaxDecryptedBytes() {
1221
+ var keypair = _serverKeypair();
1222
+ var threw = null;
1223
+ try {
1224
+ b.middleware.apiEncrypt.httpClient({ pubkey: keypair, maxDecryptedBytes: "5m" });
1225
+ } catch (e) { threw = e; }
1226
+ check("apiEncrypt.httpClient: rejects string maxDecryptedBytes at boot",
1227
+ threw && /maxDecryptedBytes/.test(threw.message));
1228
+ }
1229
+
1230
+ async function testApiEncryptPerSessionConcurrentCtrExecutesOnce() {
1231
+ var keypair = _serverKeypair();
1232
+ var store = _clusterStyleSessionStore();
1233
+ var mw = b.middleware.apiEncrypt({
1234
+ keypair: keypair,
1235
+ audit: false,
1236
+ keying: "per-session",
1237
+ sessionStore: store,
1238
+ });
1239
+ var clientCtx = b.middleware.apiEncrypt.client({
1240
+ pubkey: keypair, keying: "per-session",
1241
+ });
1242
+
1243
+ // Bootstrap the session (ctr=1) so a row exists in the store.
1244
+ var first = clientCtx.encryptRequest({ n: 1 });
1245
+ var req1 = _bodyReq("POST", { "content-type": "application/json" }, "");
1246
+ req1.body = first.body;
1247
+ var res1 = _mkRes();
1248
+ var fin1 = _newFinish(res1);
1249
+ await mw(req1, res1, function () { res1.json({ ok: 1 }); });
1250
+ await fin1;
1251
+ check("concurrent-ctr: bootstrap 200", res1._endedStatus === 200);
1252
+
1253
+ // Build ONE subsequent request (ctr=2), then submit two byte-identical
1254
+ // copies of it concurrently — the double-execution replay an attacker
1255
+ // (or a buggy retry) lands on a clustered store.
1256
+ var second = clientCtx.encryptRequest({ n: 2 });
1257
+ var execCount = 0;
1258
+
1259
+ function _submit() {
1260
+ var req = _bodyReq("POST", { "content-type": "application/json" }, "");
1261
+ req.body = JSON.parse(JSON.stringify(second.body)); // distinct copies
1262
+ var res = _mkRes();
1263
+ var fin = _newFinish(res);
1264
+ var p = mw(req, res, function () {
1265
+ execCount += 1;
1266
+ res.json({ ok: 2 });
1267
+ });
1268
+ return { done: Promise.all([p, fin]), res: res };
1269
+ }
1270
+
1271
+ // Fire both BEFORE awaiting either, so they interleave at the async
1272
+ // get()/set() yield points.
1273
+ var a = _submit();
1274
+ var c = _submit();
1275
+ await a.done;
1276
+ await c.done;
1277
+
1278
+ await helpers.waitUntil(function () {
1279
+ return a.res._endedStatus !== null && c.res._endedStatus !== null;
1280
+ }, { timeoutMs: 5000, label: "concurrent-ctr: both responses finished" });
1281
+
1282
+ check("concurrent-ctr: both requests read a fresh copy (raced through get)",
1283
+ store._gets() >= 2);
1284
+ check("concurrent-ctr: handler executed EXACTLY once", execCount === 1);
1285
+ var statuses = [a.res._endedStatus, c.res._endedStatus].sort();
1286
+ check("concurrent-ctr: exactly one 200 and one 400",
1287
+ statuses[0] === 200 && statuses[1] === 400);
1288
+ var rejected = a.res._endedStatus === 400 ? a.res : c.res;
1289
+ check("concurrent-ctr: the loser is refused with the replay shape",
1290
+ /encrypted-payload-rejected/.test(rejected._captured));
1291
+ }
1292
+
1293
+ async function testApiEncryptPerSessionSequentialCounterStillWorks() {
1294
+ // Success path: the atomic gate must NOT break normal monotonic flow.
1295
+ var keypair = _serverKeypair();
1296
+ var store = _clusterStyleSessionStore();
1297
+ var mw = b.middleware.apiEncrypt({
1298
+ keypair: keypair,
1299
+ audit: false,
1300
+ keying: "per-session",
1301
+ sessionStore: store,
1302
+ });
1303
+ var clientCtx = b.middleware.apiEncrypt.client({
1304
+ pubkey: keypair, keying: "per-session",
1305
+ });
1306
+
1307
+ var reached = [];
1308
+ // Three sequential requests, ctr 1 → 2 → 3, each must reach next() and 200.
1309
+ for (var i = 1; i <= 3; i++) {
1310
+ var call = clientCtx.encryptRequest({ n: i });
1311
+ var req = _bodyReq("POST", { "content-type": "application/json" }, "");
1312
+ req.body = call.body;
1313
+ var res = _mkRes();
1314
+ var fin = _newFinish(res);
1315
+ await mw(req, res, (function (n, rq, r) {
1316
+ return function () {
1317
+ reached.push(n);
1318
+ // middleware replaced rq.body in-place with the cleartext payload.
1319
+ check("seq-ctr: req " + n + " body decrypted", rq.body && rq.body.n === n);
1320
+ r.json({ ok: n });
1321
+ };
1322
+ })(i, req, res));
1323
+ await fin;
1324
+ check("seq-ctr: req " + i + " returns 200", res._endedStatus === 200);
1325
+ var resp = JSON.parse(res._captured);
1326
+ var plain = call.decryptResponse(resp);
1327
+ check("seq-ctr: req " + i + " response decrypts", plain.ok === i);
1328
+ }
1329
+ check("seq-ctr: all three sequential requests reached the handler",
1330
+ reached.length === 3 && reached[0] === 1 && reached[1] === 2 && reached[2] === 3);
1331
+ }
1332
+
1333
+ async function testApiEncryptCtrClaimLifetimeAndSetFailure() {
1334
+ // The (sid, ctr) claim must live until session.expiresAt, not just
1335
+ // replayWindowMs: the post-handler session write is best-effort, so a
1336
+ // failed write leaves lastReqCtr stale in the store, and _ts is
1337
+ // plaintext envelope metadata — a window-scoped claim would let the
1338
+ // same captured body replay after the window and execute twice.
1339
+ var keypair = _serverKeypair();
1340
+ var store = _clusterStyleSessionStore();
1341
+ var failSets = false;
1342
+ var innerSet = store.set;
1343
+ store.set = async function (sid, row, opts) {
1344
+ if (failSets) throw new Error("store write rejected");
1345
+ return innerSet.call(store, sid, row, opts);
1346
+ };
1347
+ // Wrap the memory nonce store to record every claim's expireAt.
1348
+ var innerNonce = b.nonceStore.create({ backend: "memory" });
1349
+ var claims = [];
1350
+ var nonceStore = {
1351
+ checkAndInsert: function (key, expireAt) {
1352
+ claims.push({ key: key, expireAt: expireAt });
1353
+ return innerNonce.checkAndInsert(key, expireAt);
1354
+ },
1355
+ purgeExpired: function () { return innerNonce.purgeExpired(); },
1356
+ close: function () { return innerNonce.close(); },
1357
+ };
1358
+ var mw = b.middleware.apiEncrypt({
1359
+ keypair: keypair, audit: false, keying: "per-session",
1360
+ sessionStore: store, nonceStore: nonceStore,
1361
+ replayWindowMs: 30000,
1362
+ });
1363
+ var clientCtx = b.middleware.apiEncrypt.client({
1364
+ pubkey: keypair, keying: "per-session",
1365
+ });
1366
+
1367
+ // Bootstrap (ctr=1) — session row persists (writes still succeed).
1368
+ var first = clientCtx.encryptRequest({ n: 1 });
1369
+ var req1 = _bodyReq("POST", { "content-type": "application/json" }, "");
1370
+ req1.body = first.body;
1371
+ var res1 = _mkRes();
1372
+ var fin1 = _newFinish(res1);
1373
+ await mw(req1, res1, function () { res1.json({ ok: 1 }); });
1374
+ await fin1;
1375
+ check("ctr-claim: bootstrap 200", res1._endedStatus === 200);
1376
+
1377
+ // Subsequent request (ctr=2) with the post-handler session write
1378
+ // FAILING best-effort — the request still succeeds, but lastReqCtr
1379
+ // stays stale (1) in the store.
1380
+ failSets = true;
1381
+ var second = clientCtx.encryptRequest({ n: 2 });
1382
+ var execCount = 0;
1383
+ var req2 = _bodyReq("POST", { "content-type": "application/json" }, "");
1384
+ req2.body = JSON.parse(JSON.stringify(second.body));
1385
+ var res2 = _mkRes();
1386
+ var fin2 = _newFinish(res2);
1387
+ await mw(req2, res2, function () { execCount += 1; res2.json({ ok: 2 }); });
1388
+ await fin2;
1389
+ check("ctr-claim: request succeeds despite failed session write",
1390
+ res2._endedStatus === 200 && execCount === 1);
1391
+
1392
+ // The recorded ctr claim expires with the SESSION, not the window.
1393
+ var ctrClaims = claims.filter(function (c) { return /^ctr:/.test(c.key); });
1394
+ check("ctr-claim: exactly one ctr claim recorded", ctrClaims.length === 1);
1395
+ var key = ctrClaims[0].key;
1396
+ var sid = key.slice(4, key.lastIndexOf(":"));
1397
+ failSets = false;
1398
+ var row = await store.get(sid);
1399
+ check("ctr-claim: claim lives until session.expiresAt (not the staleness window)",
1400
+ row !== null && ctrClaims[0].expireAt === row.expiresAt &&
1401
+ ctrClaims[0].expireAt > Date.now() + 30000);
1402
+
1403
+ // REPLAY the same captured body — must refuse even though the failed
1404
+ // store write left lastReqCtr stale at 1 (the monotonic check alone
1405
+ // would pass ctr=2 again; the atomic claim is what holds).
1406
+ var req3 = _bodyReq("POST", { "content-type": "application/json" }, "");
1407
+ req3.body = JSON.parse(JSON.stringify(second.body));
1408
+ var res3 = _mkRes();
1409
+ var fin3 = _newFinish(res3);
1410
+ await mw(req3, res3, function () { execCount += 1; res3.json({ ok: 3 }); });
1411
+ await fin3;
1412
+ check("ctr-claim: replay of the captured body is refused",
1413
+ res3._endedStatus === 400 && /encrypted-payload-rejected/.test(res3._captured));
1414
+ check("ctr-claim: handler did not execute twice", execCount === 1);
1415
+ }
1416
+
1417
+ async function testApiEncryptEnvelopeMetadataAadBound() {
1418
+ // The envelope's plaintext fields are AEAD-bound: a captured request
1419
+ // cannot be replayed with a rewritten _ts (the staleness-defeat
1420
+ // vector), and a captured response cannot be replayed to the client
1421
+ // under a bumped _ctr (the monotonic check reads plaintext).
1422
+ var keypair = _serverKeypair();
1423
+
1424
+ // -- Request side: per-request mode, tampered _ts refused.
1425
+ var mw = b.middleware.apiEncrypt({ keypair: keypair, audit: false });
1426
+ var clientCtx = b.middleware.apiEncrypt.client({ pubkey: keypair });
1427
+ var call = clientCtx.encryptRequest({ n: 1 });
1428
+
1429
+ var good = _bodyReq("POST", { "content-type": "application/json" }, "");
1430
+ good.body = JSON.parse(JSON.stringify(call.body));
1431
+ var goodRes = _mkRes();
1432
+ var goodFin = _newFinish(goodRes);
1433
+ var goodRan = 0;
1434
+ await mw(good, goodRes, function () { goodRan += 1; goodRes.json({ ok: 1 }); });
1435
+ await goodFin;
1436
+ check("aad: untampered request succeeds", goodRes._endedStatus === 200 && goodRan === 1);
1437
+
1438
+ var tampered = _bodyReq("POST", { "content-type": "application/json" }, "");
1439
+ tampered.body = JSON.parse(JSON.stringify(call.body));
1440
+ tampered.body._ts = tampered.body._ts + 1; // rewrite freshness field
1441
+ tampered.body._nonce = b.crypto.generateBytes(16).toString("hex"); // fresh nonce to clear the replay claim
1442
+ var tampRes = _mkRes();
1443
+ var tampFin = _newFinish(tampRes);
1444
+ var tampRan = 0;
1445
+ await mw(tampered, tampRes, function () { tampRan += 1; tampRes.json({ ok: 1 }); });
1446
+ await tampFin;
1447
+ check("aad: rewritten _ts refused (AEAD tag mismatch)",
1448
+ tampRes._endedStatus === 400 && tampRan === 0 &&
1449
+ /encrypted-payload-rejected/.test(tampRes._captured));
1450
+
1451
+ // -- Response side: per-session mode, bumped response _ctr refused typed.
1452
+ var store = _clusterStyleSessionStore();
1453
+ var mw2 = b.middleware.apiEncrypt({
1454
+ keypair: keypair, audit: false, keying: "per-session", sessionStore: store,
1455
+ });
1456
+ var ctx2 = b.middleware.apiEncrypt.client({ pubkey: keypair, keying: "per-session" });
1457
+ var c1 = ctx2.encryptRequest({ n: 1 });
1458
+ var r1 = _bodyReq("POST", { "content-type": "application/json" }, "");
1459
+ r1.body = c1.body;
1460
+ var rs1 = _mkRes();
1461
+ var rf1 = _newFinish(rs1);
1462
+ await mw2(r1, rs1, function () { rs1.json({ ok: 1 }); });
1463
+ await rf1;
1464
+ var resp1 = JSON.parse(rs1._captured);
1465
+ // Replaying the response under a bumped counter passes the client's
1466
+ // plaintext monotonic check; the AAD binding is what refuses it.
1467
+ var forged = JSON.parse(JSON.stringify(resp1));
1468
+ forged._ctr = forged._ctr + 1;
1469
+ var threw = null;
1470
+ try { c1.decryptResponse(forged); } catch (e) { threw = e; }
1471
+ check("aad: response replay under a bumped _ctr refused typed",
1472
+ threw !== null && threw.code === "CLIENT_RESPONSE_TAMPERED");
1473
+ // The refused forgery must NOT have advanced the monotonic counter —
1474
+ // the genuine response still decrypts after the attack attempt.
1475
+ var plain = c1.decryptResponse(resp1);
1476
+ check("aad: genuine response still decrypts after refused forgery", plain.ok === 1);
1477
+ }
1478
+
1147
1479
  async function run() {
1148
1480
  await testNonceStoreSurface();
1149
1481
  await testNonceStoreMemoryBasics();
@@ -1155,6 +1487,9 @@ async function run() {
1155
1487
  await testNonceStoreUnknownBackend();
1156
1488
 
1157
1489
  await testApiEncryptKeypairValidated();
1490
+ await testApiEncryptCreateRejectsBadReplayWindow();
1491
+ await testApiEncryptClientRejectsBadMaxDecryptedBytes();
1492
+ await testApiEncryptHttpClientRejectsBadMaxDecryptedBytes();
1158
1493
  await testApiEncryptRoundTrip();
1159
1494
  await testApiEncryptRejectsMissingShape();
1160
1495
  await testApiEncryptRejectsStaleTimestamp();
@@ -1183,6 +1518,10 @@ async function run() {
1183
1518
  await testApiEncryptPerSessionBootstrapAndReuse();
1184
1519
  await testApiEncryptPerSessionUnknownSid();
1185
1520
  await testApiEncryptPerSessionCounterReplay();
1521
+ await testApiEncryptPerSessionConcurrentCtrExecutesOnce();
1522
+ await testApiEncryptPerSessionSequentialCounterStillWorks();
1523
+ await testApiEncryptCtrClaimLifetimeAndSetFailure();
1524
+ await testApiEncryptEnvelopeMetadataAadBound();
1186
1525
  await testApiEncryptPerSessionExpiry();
1187
1526
  await testApiEncryptPerSessionMaxResponses();
1188
1527
  await testApiEncryptPerSessionResponseCounterMonotonic();
@@ -516,6 +516,43 @@ function run() {
516
516
 
517
517
  check("asyncapiServe.forceRebuild is fn", typeof serve.forceRebuild === "function");
518
518
 
519
+ // HEAD carries the GET response headers (incl. Content-Length) with an
520
+ // EMPTY body (RFC 9110 §9.3.2). GET unchanged: it still returns the
521
+ // body. Asserts the head-suppression against both JSON and YAML mounts.
522
+ var sentHeadJson = { status: null, headers: null, body: undefined, ended: false };
523
+ serve({ method: "HEAD", url: "/asyncapi.json", pathname: "/asyncapi.json", headers: {} },
524
+ { writeHead: function (s, h) { sentHeadJson.status = s; sentHeadJson.headers = h; },
525
+ end: function (bdy) { sentHeadJson.body = bdy; sentHeadJson.ended = true; } },
526
+ function () {});
527
+ check("asyncapiServe HEAD JSON: 200", sentHeadJson.status === 200);
528
+ check("asyncapiServe HEAD JSON: Content-Length set like GET",
529
+ sentHeadJson.headers["Content-Length"] === sentJson.headers["Content-Length"]);
530
+ check("asyncapiServe HEAD JSON: Content-Type set like GET",
531
+ sentHeadJson.headers["Content-Type"] === sentJson.headers["Content-Type"]);
532
+ check("asyncapiServe HEAD JSON: empty body",
533
+ sentHeadJson.ended === true && (sentHeadJson.body === undefined || sentHeadJson.body == null));
534
+
535
+ var sentHeadYaml = { status: null, headers: null, body: undefined, ended: false };
536
+ serve({ method: "HEAD", url: "/asyncapi.yaml", pathname: "/asyncapi.yaml", headers: {} },
537
+ { writeHead: function (s, h) { sentHeadYaml.status = s; sentHeadYaml.headers = h; },
538
+ end: function (bdy) { sentHeadYaml.body = bdy; sentHeadYaml.ended = true; } },
539
+ function () {});
540
+ check("asyncapiServe HEAD YAML: 200", sentHeadYaml.status === 200);
541
+ check("asyncapiServe HEAD YAML: Content-Length set like GET",
542
+ sentHeadYaml.headers["Content-Length"] === sentYaml.headers["Content-Length"]);
543
+ check("asyncapiServe HEAD YAML: empty body",
544
+ sentHeadYaml.ended === true && (sentHeadYaml.body === undefined || sentHeadYaml.body == null));
545
+
546
+ // GET still returns the body after the HEAD path was added.
547
+ var sentGetAfter = { status: null, headers: null, body: null };
548
+ serve({ method: "GET", url: "/asyncapi.json", pathname: "/asyncapi.json", headers: {} },
549
+ { writeHead: function (s, h) { sentGetAfter.status = s; sentGetAfter.headers = h; },
550
+ end: function (bdy) { sentGetAfter.body = bdy; } },
551
+ function () {});
552
+ check("asyncapiServe GET still returns body",
553
+ sentGetAfter.status === 200 && typeof sentGetAfter.body === "string" &&
554
+ sentGetAfter.body.length > 0);
555
+
519
556
  // ---- operation messages with $ref form ----
520
557
  aapi.operation("publishWithRef", {
521
558
  action: "send",
@@ -64,6 +64,27 @@ function testSurface() {
64
64
  check("breakGlass.BreakGlassError is class", typeof b.breakGlass.BreakGlassError === "function");
65
65
  }
66
66
 
67
+ // ---- init opts validation ----
68
+
69
+ function testInitOptsValidation() {
70
+ // The `now` knob was documented as a Date.now override but nothing
71
+ // consumed it (every time read is a direct Date.now()). It is removed
72
+ // from the init allowlist, so passing it is now a config-time error.
73
+ var threwNow = false;
74
+ try { b.breakGlass.init({ now: 123 }); } catch (_e) { threwNow = true; }
75
+ check("init: removed `now` opt throws", threwNow);
76
+
77
+ // trustProxy is still honored — default behavior unchanged.
78
+ var threwTrustProxy = false;
79
+ try { b.breakGlass.init({ trustProxy: true }); } catch (_e) { threwTrustProxy = true; }
80
+ check("init: trustProxy still accepted", !threwTrustProxy);
81
+
82
+ // bare init() with no opts still works.
83
+ var threwBare = false;
84
+ try { b.breakGlass.init(); } catch (_e) { threwBare = true; }
85
+ check("init: no-opts init still works", !threwBare);
86
+ }
87
+
67
88
  // ---- Policy CRUD ----
68
89
 
69
90
  async function testPolicyCRUD() {
@@ -825,6 +846,7 @@ async function testListActiveAllAndRevokeAll() {
825
846
 
826
847
  async function run() {
827
848
  testSurface();
849
+ testInitOptsValidation();
828
850
  await testPolicyCRUD();
829
851
  await testPolicyValidation();
830
852
  await testGrantHappyPath();