@blamejs/blamejs-shop 0.3.55 → 0.3.57

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 (85) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +39 -2
  3. package/SECURITY.md +14 -0
  4. package/lib/asset-manifest.json +1 -1
  5. package/lib/email.js +169 -0
  6. package/lib/storefront.js +368 -0
  7. package/lib/vendor/MANIFEST.json +84 -72
  8. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  9. package/lib/vendor/blamejs/README.md +3 -3
  10. package/lib/vendor/blamejs/SECURITY.md +5 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +15 -3
  12. package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
  13. package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
  14. package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
  15. package/lib/vendor/blamejs/lib/archive-read.js +215 -16
  16. package/lib/vendor/blamejs/lib/archive.js +206 -52
  17. package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
  18. package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
  19. package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
  20. package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
  21. package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
  22. package/lib/vendor/blamejs/lib/db-schema.js +120 -3
  23. package/lib/vendor/blamejs/lib/db.js +10 -3
  24. package/lib/vendor/blamejs/lib/error-page.js +93 -9
  25. package/lib/vendor/blamejs/lib/external-db.js +164 -13
  26. package/lib/vendor/blamejs/lib/guard-email.js +36 -3
  27. package/lib/vendor/blamejs/lib/http-client.js +37 -7
  28. package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
  29. package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
  30. package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
  31. package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
  32. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
  33. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
  34. package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
  35. package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
  36. package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
  37. package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
  38. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
  39. package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
  40. package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
  41. package/lib/vendor/blamejs/lib/openapi.js +225 -100
  42. package/lib/vendor/blamejs/lib/problem-details.js +15 -3
  43. package/lib/vendor/blamejs/lib/queue-local.js +148 -38
  44. package/lib/vendor/blamejs/lib/queue.js +41 -11
  45. package/lib/vendor/blamejs/lib/render.js +21 -3
  46. package/lib/vendor/blamejs/lib/router.js +13 -6
  47. package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
  48. package/lib/vendor/blamejs/lib/sse.js +7 -5
  49. package/lib/vendor/blamejs/lib/static.js +46 -17
  50. package/lib/vendor/blamejs/lib/uri-template.js +3 -1
  51. package/lib/vendor/blamejs/package.json +1 -1
  52. package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
  53. package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
  54. package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
  55. package/lib/vendor/blamejs/test/00-primitives.js +151 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
  59. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
  64. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
  65. package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
  67. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
  69. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
  70. package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
  71. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
  72. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
  73. package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
  76. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
  77. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
  78. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
  79. package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
  80. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
  81. package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
  82. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
  83. package/lib/wishlist-alerts.js +22 -0
  84. package/lib/wishlist-digest.js +18 -0
  85. package/package.json +1 -1
@@ -2356,6 +2356,28 @@ function testRenderJson() {
2356
2356
  check("render.json: extra headers merged", c2.headers["x-custom"] === "v");
2357
2357
  }
2358
2358
 
2359
+ function testRenderJsonReplacer() {
2360
+ // Function replacer serializes a BigInt that bare JSON.stringify throws on.
2361
+ var res = _captureRes();
2362
+ b.render.json(res, { total: 9007199254740993n }, {
2363
+ replacer: function (k, v) { return typeof v === "bigint" ? v.toString() : v; },
2364
+ });
2365
+ var c = res._captured();
2366
+ check("render.json: replacer fn serializes BigInt", c.body === '{"total":"9007199254740993"}');
2367
+ check("render.json: replacer Content-Length matches", Number(c.headers["content-length"]) === Buffer.byteLength(c.body));
2368
+
2369
+ // Array replacer acts as a key allowlist.
2370
+ var res2 = _captureRes();
2371
+ b.render.json(res2, { keep: 1, drop: 2 }, { replacer: ["keep"] });
2372
+ check("render.json: array replacer filters keys", res2._captured().body === '{"keep":1}');
2373
+
2374
+ // Bad replacer shape throws at call time (config typo).
2375
+ var threw = false;
2376
+ try { b.render.json(_captureRes(), { a: 1 }, { replacer: "nope" }); }
2377
+ catch (e) { threw = e instanceof TypeError; }
2378
+ check("render.json: non-fn/array replacer throws", threw);
2379
+ }
2380
+
2359
2381
  function testRenderText() {
2360
2382
  var res = _captureRes();
2361
2383
  b.render.text(res, "hello");
@@ -12303,6 +12325,99 @@ function testErrorsPageModeAutoDetectsFromNodeEnv() {
12303
12325
  }
12304
12326
  }
12305
12327
 
12328
+ function testErrorsPageProblemDetailsEnvelope() {
12329
+ var handler = b.errorPage.create({ mode: "prod", audit: false, problemDetails: true });
12330
+ var req = { method: "POST", url: "/api/x", headers: { accept: "application/json" } };
12331
+ var res = _makeFakeRes();
12332
+ handler(Object.assign(new Error("bad input"), {
12333
+ isAppError: true, statusCode: 400, code: "VALIDATION_ERROR",
12334
+ }), req, res);
12335
+ check("problemDetails: 400 status", res.statusCode === 400);
12336
+ check("problemDetails: application/problem+json content type",
12337
+ /application\/problem\+json/.test(res.headers["Content-Type"]));
12338
+ var doc = JSON.parse(res.body);
12339
+ check("problemDetails: status field set", doc.status === 400);
12340
+ check("problemDetails: title from reason phrase", doc.title === "Bad Request");
12341
+ check("problemDetails: detail carries 4xx message", doc.detail === "bad input");
12342
+ check("problemDetails: code extension preserved", doc.code === "VALIDATION_ERROR");
12343
+ // 500 generic still hides operator-private message under problem+json.
12344
+ var res2 = _makeFakeRes();
12345
+ handler(new Error("DB pwd: hunter2"), req, res2);
12346
+ var doc2 = JSON.parse(res2.body);
12347
+ check("problemDetails: 500 hides operator message",
12348
+ doc2.status === 500 && doc2.detail.indexOf("hunter2") === -1 &&
12349
+ doc2.detail === "Internal Server Error");
12350
+ }
12351
+
12352
+ function testErrorsPageJsonFormatterTakesOverBody() {
12353
+ var seen = [];
12354
+ var handler = b.errorPage.create({
12355
+ mode: "prod", audit: false,
12356
+ jsonFormatter: function (info, req) {
12357
+ seen.push({ status: info.status, code: info.code, url: req && req.url });
12358
+ return { contentType: "application/vnd.api+json", body: JSON.stringify({ errors: [{ status: String(info.status) }] }) };
12359
+ },
12360
+ });
12361
+ var req = { method: "POST", url: "/api/x", headers: { accept: "application/json" } };
12362
+ var res = _makeFakeRes();
12363
+ handler(Object.assign(new Error("nope"), { isAppError: true, statusCode: 422, code: "X" }), req, res);
12364
+ check("jsonFormatter: hook ran with classified info",
12365
+ seen.length === 1 && seen[0].status === 422 && seen[0].url === "/api/x");
12366
+ check("jsonFormatter: status preserved", res.statusCode === 422);
12367
+ check("jsonFormatter: custom content type honored",
12368
+ res.headers["Content-Type"] === "application/vnd.api+json");
12369
+ check("jsonFormatter: custom body written verbatim",
12370
+ res.body === '{"errors":[{"status":"422"}]}');
12371
+
12372
+ // A throwing formatter falls back to the default envelope, never masks
12373
+ // the original error.
12374
+ var fallbackHandler = b.errorPage.create({
12375
+ mode: "prod", audit: false,
12376
+ jsonFormatter: function () { throw new Error("formatter broke"); },
12377
+ });
12378
+ var res2 = _makeFakeRes();
12379
+ fallbackHandler(Object.assign(new Error("bad"), { isAppError: true, statusCode: 400, code: "VALIDATION_ERROR" }), req, res2);
12380
+ var payload = JSON.parse(res2.body);
12381
+ check("jsonFormatter: throw falls back to default envelope",
12382
+ res2.statusCode === 400 && payload.error.code === "VALIDATION_ERROR" &&
12383
+ /application\/json/.test(res2.headers["Content-Type"]));
12384
+ }
12385
+
12386
+ function testErrorsPageRenderHtmlHook() {
12387
+ var handler = b.errorPage.create({
12388
+ mode: "prod", audit: false,
12389
+ renderHtml: function (info) { return "<custom>" + info.status + "</custom>"; },
12390
+ });
12391
+ var req = { method: "GET", url: "/x", headers: { accept: "text/html" } };
12392
+ var res = _makeFakeRes();
12393
+ handler(new Error("boom"), req, res);
12394
+ check("renderHtml: status preserved", res.statusCode === 500);
12395
+ check("renderHtml: still text/html", /text\/html/.test(res.headers["Content-Type"]));
12396
+ check("renderHtml: custom body written", res.body === "<custom>500</custom>");
12397
+
12398
+ // A throwing hook falls back to the built-in prod page.
12399
+ var fallbackHandler = b.errorPage.create({
12400
+ mode: "prod", audit: false,
12401
+ renderHtml: function () { throw new Error("render broke"); },
12402
+ });
12403
+ var res2 = _makeFakeRes();
12404
+ fallbackHandler(new Error("boom"), req, res2);
12405
+ check("renderHtml: throw falls back to built-in page",
12406
+ res2.statusCode === 500 && res2.body.indexOf("Internal Server Error") !== -1);
12407
+ }
12408
+
12409
+ function testErrorsPageHookOptsRejectNonFunction() {
12410
+ var threwJson = false;
12411
+ try { b.errorPage.create({ jsonFormatter: "nope" }); }
12412
+ catch (e) { threwJson = e instanceof TypeError; }
12413
+ check("jsonFormatter non-function throws at config time", threwJson);
12414
+
12415
+ var threwHtml = false;
12416
+ try { b.errorPage.create({ renderHtml: 42 }); }
12417
+ catch (e) { threwHtml = e instanceof TypeError; }
12418
+ check("renderHtml non-function throws at config time", threwHtml);
12419
+ }
12420
+
12306
12421
  // ---- log ----
12307
12422
  //
12308
12423
  // Each test creates an instance with a captured-buffer destination so
@@ -14136,6 +14251,30 @@ function testBufferSafeBoundedChunkCollector() {
14136
14251
  _expectBadArg("null", { maxBytes: null });
14137
14252
  }
14138
14253
 
14254
+ async function testBufferSafeCollectStream() {
14255
+ var bs = b.safeBuffer;
14256
+ var Readable = require("stream").Readable;
14257
+ check("safeBuffer.collectStream is a function", typeof bs.collectStream === "function");
14258
+
14259
+ // Happy path — pump a Readable fully into one Buffer.
14260
+ var buf = await bs.collectStream(
14261
+ Readable.from([Buffer.from("hello "), Buffer.from("world")]), { maxBytes: 100 });
14262
+ check("collectStream: joins chunks",
14263
+ Buffer.isBuffer(buf) && buf.toString("utf8") === "hello world");
14264
+
14265
+ // Cap enforced — a stream over maxBytes rejects instead of buffering it all.
14266
+ var overflowed = false;
14267
+ try { await bs.collectStream(Readable.from([Buffer.alloc(8), Buffer.alloc(5)]), { maxBytes: 10 }); }
14268
+ catch (e) { overflowed = e.code === "buffer/too-large"; }
14269
+ check("collectStream: rejects when stream exceeds maxBytes", overflowed);
14270
+
14271
+ // Bad maxBytes rejects (not a synchronous throw, so callers can rely on .catch).
14272
+ var badArg = false;
14273
+ try { await bs.collectStream(Readable.from([Buffer.from("x")]), { maxBytes: Infinity }); }
14274
+ catch (e) { badArg = e.code === "buffer/bad-arg"; }
14275
+ check("collectStream: bad maxBytes rejects", badArg);
14276
+ }
14277
+
14139
14278
  // ---- url-safe ----
14140
14279
 
14141
14280
  function testUrlSafeDefaultIsHttpsOnly() {
@@ -17773,6 +17912,7 @@ async function run() {
17773
17912
  // render — response helpers paired with the template engine
17774
17913
  testRenderSurface();
17775
17914
  testRenderJson();
17915
+ testRenderJsonReplacer();
17776
17916
  testRenderText();
17777
17917
  testRenderHtmlString();
17778
17918
  testRenderRedirect();
@@ -18203,6 +18343,10 @@ async function run() {
18203
18343
  testErrorsPageLogsViaInjectedLogger();
18204
18344
  testErrorsPageDevEnvVarsHonorOptIn();
18205
18345
  testErrorsPageModeAutoDetectsFromNodeEnv();
18346
+ testErrorsPageProblemDetailsEnvelope();
18347
+ testErrorsPageJsonFormatterTakesOverBody();
18348
+ testErrorsPageRenderHtmlHook();
18349
+ testErrorsPageHookOptsRejectNonFunction();
18206
18350
  // log — structured JSON logging with request-id correlation
18207
18351
  testLogSurface();
18208
18352
  testLogEmitsJsonLineToStdout();
@@ -18294,6 +18438,7 @@ async function run() {
18294
18438
  testBufferSafeNormalizeText();
18295
18439
  testBufferSafeToBuffer();
18296
18440
  testBufferSafeBoundedChunkCollector();
18441
+ await testBufferSafeCollectStream();
18297
18442
  testBufferSafeSecureZero();
18298
18443
  // logger primitive (per-module log channel)
18299
18444
  testLogger();
@@ -18549,6 +18694,7 @@ module.exports = {
18549
18694
  testTemplateMissingViewsDir: testTemplateMissingViewsDir,
18550
18695
  testRenderSurface: testRenderSurface,
18551
18696
  testRenderJson: testRenderJson,
18697
+ testRenderJsonReplacer: testRenderJsonReplacer,
18552
18698
  testRenderText: testRenderText,
18553
18699
  testRenderHtmlString: testRenderHtmlString,
18554
18700
  testRenderRedirect: testRenderRedirect,
@@ -18898,6 +19044,10 @@ module.exports = {
18898
19044
  testErrorsPageLogsViaInjectedLogger: testErrorsPageLogsViaInjectedLogger,
18899
19045
  testErrorsPageDevEnvVarsHonorOptIn: testErrorsPageDevEnvVarsHonorOptIn,
18900
19046
  testErrorsPageModeAutoDetectsFromNodeEnv: testErrorsPageModeAutoDetectsFromNodeEnv,
19047
+ testErrorsPageProblemDetailsEnvelope: testErrorsPageProblemDetailsEnvelope,
19048
+ testErrorsPageJsonFormatterTakesOverBody: testErrorsPageJsonFormatterTakesOverBody,
19049
+ testErrorsPageRenderHtmlHook: testErrorsPageRenderHtmlHook,
19050
+ testErrorsPageHookOptsRejectNonFunction: testErrorsPageHookOptsRejectNonFunction,
18901
19051
  testLogSurface: testLogSurface,
18902
19052
  testLogEmitsJsonLineToStdout: testLogEmitsJsonLineToStdout,
18903
19053
  testLogRoutesErrorAndFatalToStderr: testLogRoutesErrorAndFatalToStderr,
@@ -18947,6 +19097,7 @@ module.exports = {
18947
19097
  testBufferSafeNormalizeText: testBufferSafeNormalizeText,
18948
19098
  testBufferSafeToBuffer: testBufferSafeToBuffer,
18949
19099
  testBufferSafeBoundedChunkCollector: testBufferSafeBoundedChunkCollector,
19100
+ testBufferSafeCollectStream: testBufferSafeCollectStream,
18950
19101
  testBufferSafeSecureZero: testBufferSafeSecureZero,
18951
19102
  testLogger: testLogger,
18952
19103
  testConstantsReferenceIntegrity: testConstantsReferenceIntegrity,
@@ -377,6 +377,23 @@ async function testDrainQuiesce() {
377
377
  consumers[0].stop = orig;
378
378
  }
379
379
 
380
+ async function testDrainPhaseRegistersWithAppShutdown() {
381
+ // The orchestrator wires its drain into b.appShutdown via addPhase
382
+ // ({ name, run }) — the handle exposes addPhase, not registerPhase, so a
383
+ // registerPhase call silently never registered the phase. Assert the
384
+ // phase is registered AND actually runs (sets ctx.draining) on shutdown.
385
+ var shut = b.appShutdown.create({ graceMs: 1000 });
386
+ var orch = b.agent.orchestrator.create({ appShutdown: shut });
387
+ check("appShutdown wiring: not draining before shutdown", orch._ctx.draining !== true);
388
+
389
+ var result = await shut.shutdown();
390
+ var drainPhase = result.phases.filter(function (p) { return p.name === "agent.orchestrator.drain"; });
391
+ check("appShutdown wiring: drain phase registered", drainPhase.length === 1);
392
+ check("appShutdown wiring: drain phase ran ok", drainPhase.length === 1 && drainPhase[0].ok === true);
393
+ check("appShutdown wiring: drain set ctx.draining", orch._ctx.draining === true);
394
+ shut._resetForTest();
395
+ }
396
+
380
397
  async function testShardForSaltedFnv() {
381
398
  // SUBSTRATE-20 — salted FNV: vault-less fallback still distributes
382
399
  // reasonably. Doesn't assert randomness — just that the same inputs
@@ -538,6 +555,7 @@ async function run() {
538
555
  await testShardForSaltedFnv();
539
556
  await testDrain();
540
557
  await testDrainQuiesce();
558
+ await testDrainPhaseRegistersWithAppShutdown();
541
559
  await testHealth();
542
560
  await testStreamRegistry();
543
561
  await testPermissions();
@@ -722,6 +722,91 @@ async function testApiEncryptClientRejectsBadResponse() {
722
722
  check("client rejects response missing _ct", threw && threw.code === "CLIENT_RESPONSE_SHAPE");
723
723
  }
724
724
 
725
+ async function testApiEncryptEncryptedErrorReadback() {
726
+ // An in-session error must ship in the same { _ct } envelope a success uses
727
+ // (via req.apiEncryptEncode), and the encrypted client in responseMode
728
+ // "passthrough" must read a non-2xx instead of throwing on the shape: an
729
+ // encrypted error decrypts; a plaintext (pre-session-style) error returns
730
+ // verbatim; the default "reject" mode still throws on non-2xx.
731
+ var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-aee-"));
732
+ try {
733
+ await setupTestDb(tmpDir);
734
+ var http = require("http");
735
+ var keypair = _serverKeypair();
736
+ var mw = b.middleware.apiEncrypt({ keypair: keypair, audit: false });
737
+
738
+ var server = http.createServer(function (req, res) {
739
+ res.json = function (data) {
740
+ res.writeHead(res.statusCode || 200, { "Content-Type": "application/json" });
741
+ res.end(JSON.stringify(data));
742
+ };
743
+ req.pathname = req.url.split("?")[0];
744
+ var chunks = [];
745
+ req.on("data", function (c) { chunks.push(c); });
746
+ req.on("end", function () {
747
+ try { req.body = JSON.parse(Buffer.concat(chunks).toString("utf8")); }
748
+ catch (_e) { req.body = null; }
749
+ mw(req, res, function () {
750
+ var want = req.body && req.body.want;
751
+ if (want === "encrypted-error") {
752
+ // Terminal writer bypassing res.json — seal via the request encoder
753
+ // the way error-page / problem-details / deny-response now do.
754
+ check("server: req.apiEncryptEncode set after a valid session",
755
+ typeof req.apiEncryptEncode === "function");
756
+ res.writeHead(403, { "Content-Type": "application/json" });
757
+ res.end(JSON.stringify(req.apiEncryptEncode({ error: "forbidden-detail" })));
758
+ return;
759
+ }
760
+ if (want === "plaintext-error") {
761
+ res.writeHead(400, { "Content-Type": "application/json" });
762
+ res.end(JSON.stringify({ error: "bad-request-plain" }));
763
+ return;
764
+ }
765
+ res.json({ ok: true });
766
+ });
767
+ });
768
+ });
769
+ var port = await helpers.listenOnRandomPort(server);
770
+ var common = { allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL, allowInternal: true };
771
+
772
+ var pass = b.httpClient.encrypted({
773
+ pubkey: keypair, baseUrl: "http://127.0.0.1:" + port, method: "POST",
774
+ responseMode: "passthrough",
775
+ });
776
+
777
+ var enc = await pass.request(Object.assign({ path: "/x", body: { want: "encrypted-error" } }, common));
778
+ check("passthrough: non-2xx status surfaced", enc.statusCode === 403);
779
+ check("passthrough: ok flag false on non-2xx", enc.ok === false);
780
+ check("passthrough: in-session error body decrypts", enc.body && enc.body.error === "forbidden-detail");
781
+
782
+ var plain = await pass.request(Object.assign({ path: "/x", body: { want: "plaintext-error" } }, common));
783
+ check("passthrough: plaintext error status surfaced", plain.statusCode === 400);
784
+ check("passthrough: plaintext error returned verbatim", plain.body && plain.body.error === "bad-request-plain");
785
+
786
+ var ok2xx = await pass.request(Object.assign({ path: "/x", body: { want: "ok" } }, common));
787
+ check("passthrough: 2xx still decrypts + ok true",
788
+ ok2xx.statusCode === 200 && ok2xx.ok === true && ok2xx.body && ok2xx.body.ok === true);
789
+
790
+ var rej = b.httpClient.encrypted({
791
+ pubkey: keypair, baseUrl: "http://127.0.0.1:" + port, method: "POST",
792
+ });
793
+ var threw = null;
794
+ try { await rej.request(Object.assign({ path: "/x", body: { want: "plaintext-error" } }, common)); }
795
+ catch (e) { threw = e; }
796
+ check("default reject: non-2xx still throws (back-compat)", threw !== null);
797
+
798
+ var oneOff = await rej.request(Object.assign(
799
+ { path: "/x", body: { want: "plaintext-error" }, responseMode: "passthrough" }, common));
800
+ check("per-call passthrough override resolves the non-2xx",
801
+ oneOff.statusCode === 400 && oneOff.body && oneOff.body.error === "bad-request-plain");
802
+
803
+ server.close();
804
+ mw.close();
805
+ } finally {
806
+ await teardownTestDb(tmpDir);
807
+ }
808
+ }
809
+
725
810
  // ---- per-session keying mode (v0.7.3) ----
726
811
 
727
812
  async function testApiEncryptPerSessionDefaultIsPerRequest() {
@@ -1088,6 +1173,7 @@ async function run() {
1088
1173
  await testApiEncryptDerivedPruneInterval();
1089
1174
  await testApiEncryptHttpClientHelperShape();
1090
1175
  await testApiEncryptHttpClientRoundTrip();
1176
+ await testApiEncryptEncryptedErrorReadback();
1091
1177
 
1092
1178
  // v0.7.3 — per-session keying mode
1093
1179
  await testApiEncryptPerSessionDefaultIsPerRequest();
@@ -350,6 +350,62 @@ async function testAppShutdownWatchdogForcesExitOnHang() {
350
350
  exited && exited.signal === null && typeof exited.code === "number");
351
351
  }
352
352
 
353
+ function testAppShutdownExitAfterPhasesValidation() {
354
+ var threw = null;
355
+ try { b.appShutdown.create({ exitAfterPhases: "yes" }); }
356
+ catch (e) { threw = e; }
357
+ check("exitAfterPhases: non-boolean → AppShutdownError",
358
+ threw && threw.code === "app-shutdown/bad-exit-after-phases");
359
+ // Default (unset) does NOT exit — a plain shutdown() resolves without
360
+ // terminating the process (the surrounding suite proves this: every
361
+ // prior shutdown() returned to the runner without exiting).
362
+ var o = b.appShutdown.create({ phases: [], exitAfterPhases: false });
363
+ check("exitAfterPhases:false accepted", typeof o.shutdown === "function");
364
+ o._resetForTest();
365
+ }
366
+
367
+ async function testAppShutdownExitAfterPhasesExits() {
368
+ // A non-signal shutdown() with exitAfterPhases:true must terminate the
369
+ // process once phases complete, with an exit code reflecting phase
370
+ // success. Verified in a child process — process.exit would kill the
371
+ // runner. No signal is sent: the child calls shutdown() directly, which
372
+ // distinguishes this from the signal-handler watchdog path.
373
+ var cp = require("node:child_process");
374
+ var repoRoot = require("node:path").resolve(__dirname, "..", "..");
375
+ function _spawnAndExit(phaseBody, label, expectCode) {
376
+ var script =
377
+ "var b = require(" + JSON.stringify(repoRoot) + ");" +
378
+ "var o = b.appShutdown.create({ graceMs: 2000, exitAfterPhases: true," +
379
+ " phases: [{ name: 'p', run: " + phaseBody + " }] });" +
380
+ "o.shutdown().then(function (r) { process.stdout.write('RESOLVED:' + r.ok + '\\n'); });";
381
+ return new Promise(function (resolve) {
382
+ var child = cp.spawn(process.execPath, ["-e", script], { stdio: ["ignore", "pipe", "pipe"] });
383
+ var out = "";
384
+ var done = null;
385
+ child.stdout.on("data", function (d) { out += d.toString(); });
386
+ child.on("exit", function (code, signal) { done = { code: code, signal: signal, out: out }; });
387
+ helpers.waitUntil(function () { return done !== null; },
388
+ { timeoutMs: 30000, label: "app-shutdown exitAfterPhases: " + label })
389
+ .then(function () { resolve(done); }, function () {
390
+ try { child.kill("SIGKILL"); } catch (_e) { /* gone */ }
391
+ resolve(done || { code: -1, signal: null, out: out });
392
+ });
393
+ });
394
+ }
395
+
396
+ var okRun = await _spawnAndExit("function () { return; }", "clean phase", 0);
397
+ check("exitAfterPhases: clean phase resolved before exit",
398
+ okRun && okRun.out.indexOf("RESOLVED:true") !== -1);
399
+ check("exitAfterPhases: clean phase → process.exit(0) (no kill signal)",
400
+ okRun && okRun.code === 0 && okRun.signal === null);
401
+
402
+ var failRun = await _spawnAndExit("function () { throw new Error('boom'); }", "failed phase", 1);
403
+ check("exitAfterPhases: failed phase resolved with ok=false",
404
+ failRun && failRun.out.indexOf("RESOLVED:false") !== -1);
405
+ check("exitAfterPhases: failed phase → process.exit(1)",
406
+ failRun && failRun.code === 1 && failRun.signal === null);
407
+ }
408
+
353
409
  async function run() {
354
410
  testAppShutdownSurface();
355
411
  await testAppShutdownEmpty();
@@ -368,6 +424,8 @@ async function run() {
368
424
  await testAppShutdownSignalHandlersInstall();
369
425
  await testAppShutdownConfigValidation();
370
426
  await testAppShutdownWatchdogForcesExitOnHang();
427
+ testAppShutdownExitAfterPhasesValidation();
428
+ await testAppShutdownExitAfterPhasesExits();
371
429
  }
372
430
 
373
431
  module.exports = { run: run };
@@ -484,7 +484,208 @@ async function testSignalAbort() {
484
484
  check("zip extractEntries with no signal still yields entries", n === 2);
485
485
  }
486
486
 
487
+ // ---- ZIP64 read support ---------------------------------------------------
488
+ // The write side does not emit ZIP64, so we hand-build minimal ZIP64-form
489
+ // archives (infozip / Go archive/zip layout) and assert they decode to the
490
+ // same entry shape a classic reader yields, with bomb/name refusals intact.
491
+ // APPNOTE 6.3.10 §4.3.14 (EOCD64) / §4.3.15 (locator) / §4.5.3 (extra field).
492
+
493
+ var METHOD_STORE = 0;
494
+ var U32_SENTINEL = 0xffffffff;
495
+ var U16_SENTINEL = 0xffff;
496
+
497
+ // Build a single STORE-method ZIP64 archive whose CD entry carries the
498
+ // 0xFFFFFFFF uncompressed/compressed-size sentinels resolved by a §4.5.3
499
+ // extra field, fronted by a real ZIP64 EOCD record + locator and a classic
500
+ // EOCD with sentinel size/offset fields. `crc` comes from the classic
501
+ // archive the framework produced for the same bytes (CRC must agree so the
502
+ // LFH/CD skew check passes). `entryCountSentinel` exercises the EOCD64
503
+ // totalEntries path.
504
+ function buildZip64Store(name, data, crc, opts) {
505
+ opts = opts || {};
506
+ var nameBuf = Buffer.from(name, "utf8");
507
+ var size = data.length;
508
+
509
+ // ZIP64 extra field for the LFH: uncompressedSize(8) + compressedSize(8).
510
+ var lfhExtra = Buffer.alloc(4 + 16);
511
+ lfhExtra.writeUInt16LE(0x0001, 0);
512
+ lfhExtra.writeUInt16LE(16, 2);
513
+ lfhExtra.writeBigUInt64LE(BigInt(size), 4);
514
+ lfhExtra.writeBigUInt64LE(BigInt(size), 12);
515
+
516
+ // Local file header (30 bytes) with sentinel sizes.
517
+ var lfh = Buffer.alloc(30);
518
+ lfh.writeUInt32LE(0x04034b50, 0); // sig
519
+ lfh.writeUInt16LE(45, 4); // version needed = 4.5 (ZIP64)
520
+ lfh.writeUInt16LE(0, 6); // flags
521
+ lfh.writeUInt16LE(METHOD_STORE, 8); // method
522
+ lfh.writeUInt16LE(0, 10); // mod time
523
+ lfh.writeUInt16LE(0x21, 12); // mod date (1980-01-01)
524
+ lfh.writeUInt32LE(crc >>> 0, 14); // crc32
525
+ lfh.writeUInt32LE(U32_SENTINEL, 18); // csize sentinel
526
+ lfh.writeUInt32LE(U32_SENTINEL, 22); // usize sentinel
527
+ lfh.writeUInt16LE(nameBuf.length, 26);
528
+ lfh.writeUInt16LE(lfhExtra.length, 28);
529
+
530
+ var lfhOffset = 0;
531
+
532
+ // ZIP64 extra field for the CD: uncompressedSize(8) + compressedSize(8)
533
+ // (+ localHeaderOffset(8) when opted in).
534
+ var includeOffset = opts.offsetSentinel === true;
535
+ var cdExtraDataLen = includeOffset ? 24 : 16;
536
+ var cdExtra = Buffer.alloc(4 + cdExtraDataLen);
537
+ cdExtra.writeUInt16LE(0x0001, 0);
538
+ cdExtra.writeUInt16LE(cdExtraDataLen, 2);
539
+ cdExtra.writeBigUInt64LE(BigInt(size), 4);
540
+ cdExtra.writeBigUInt64LE(BigInt(size), 12);
541
+ if (includeOffset) cdExtra.writeBigUInt64LE(BigInt(lfhOffset), 20);
542
+
543
+ // Central directory header (46 bytes) with sentinel sizes.
544
+ var cd = Buffer.alloc(46);
545
+ cd.writeUInt32LE(0x02014b50, 0); // sig
546
+ cd.writeUInt16LE(45, 4); // version made by
547
+ cd.writeUInt16LE(45, 6); // version needed
548
+ cd.writeUInt16LE(0, 8); // flags
549
+ cd.writeUInt16LE(METHOD_STORE, 10); // method
550
+ cd.writeUInt16LE(0, 12); // mod time
551
+ cd.writeUInt16LE(0x21, 14); // mod date
552
+ cd.writeUInt32LE(crc >>> 0, 16); // crc32
553
+ cd.writeUInt32LE(U32_SENTINEL, 20); // csize sentinel
554
+ cd.writeUInt32LE(U32_SENTINEL, 24); // usize sentinel
555
+ cd.writeUInt16LE(nameBuf.length, 28);
556
+ cd.writeUInt16LE(cdExtra.length, 30);
557
+ cd.writeUInt16LE(0, 32); // comment len
558
+ cd.writeUInt16LE(0, 34); // disk start
559
+ cd.writeUInt16LE(0, 36); // internal attrs
560
+ cd.writeUInt32LE(0, 38); // external attrs
561
+ cd.writeUInt32LE(includeOffset ? U32_SENTINEL : lfhOffset, 42); // lfh offset (sentinel when opted in)
562
+
563
+ var cdBytesBefore = Buffer.concat([lfh, nameBuf, lfhExtra, data]);
564
+ var cdOffset = cdBytesBefore.length;
565
+ var cdRecord = Buffer.concat([cd, nameBuf, cdExtra]);
566
+ var cdSize = cdRecord.length;
567
+
568
+ // ZIP64 EOCD record (56 bytes through cdOffset).
569
+ var eocd64 = Buffer.alloc(56);
570
+ eocd64.writeUInt32LE(0x06064b50, 0); // sig
571
+ eocd64.writeBigUInt64LE(BigInt(56 - 12), 4); // size of remaining record
572
+ eocd64.writeUInt16LE(45, 12); // version made by
573
+ eocd64.writeUInt16LE(45, 14); // version needed
574
+ eocd64.writeUInt32LE(0, 16); // disk number
575
+ eocd64.writeUInt32LE(0, 20); // cd start disk
576
+ eocd64.writeBigUInt64LE(1n, 24); // entries this disk
577
+ eocd64.writeBigUInt64LE(1n, 32); // total entries
578
+ eocd64.writeBigUInt64LE(BigInt(cdSize), 40); // cd size
579
+ eocd64.writeBigUInt64LE(BigInt(cdOffset), 48);// cd offset
580
+
581
+ var eocd64Offset = cdOffset + cdSize;
582
+
583
+ // ZIP64 EOCD locator (20 bytes).
584
+ var locator = Buffer.alloc(20);
585
+ locator.writeUInt32LE(0x07064b50, 0); // sig
586
+ locator.writeUInt32LE(0, 4); // disk with eocd64
587
+ locator.writeBigUInt64LE(BigInt(eocd64Offset), 8);// eocd64 offset
588
+ locator.writeUInt32LE(1, 16); // total disks
589
+
590
+ // Classic EOCD (22 bytes) with sentinels.
591
+ var entryCountSentinel = opts.entryCountSentinel === true;
592
+ var eocd = Buffer.alloc(22);
593
+ eocd.writeUInt32LE(0x06054b50, 0); // sig
594
+ eocd.writeUInt16LE(0, 4); // disk number
595
+ eocd.writeUInt16LE(0, 6); // cd disk
596
+ eocd.writeUInt16LE(entryCountSentinel ? U16_SENTINEL : 1, 8); // entries this disk
597
+ eocd.writeUInt16LE(entryCountSentinel ? U16_SENTINEL : 1, 10); // total entries
598
+ eocd.writeUInt32LE(U32_SENTINEL, 12);// cd size sentinel
599
+ eocd.writeUInt32LE(U32_SENTINEL, 16);// cd offset sentinel
600
+ eocd.writeUInt16LE(0, 20); // comment len
601
+
602
+ return Buffer.concat([cdBytesBefore, cdRecord, eocd64, locator, eocd]);
603
+ }
604
+
605
+ async function testZip64Read() {
606
+ var payload = Buffer.from("ZIP64 round-trip payload — same bytes a classic reader yields.\n", "utf8");
607
+
608
+ // Get a real CRC32 for the payload by reading it back through a classic
609
+ // STORE archive the framework produced.
610
+ var z = b.archive.zip();
611
+ z.addFile("z64.txt", payload);
612
+ var classicBytes = z.toBuffer();
613
+ var classicReader = b.archive.read.zip(b.archive.adapters.buffer(classicBytes));
614
+ var classicEntries = await classicReader.inspect();
615
+ var crc = classicEntries[0].crc;
616
+
617
+ // Size-sentinel ZIP64 form decodes to the same entry shape.
618
+ var z64 = buildZip64Store("z64.txt", payload, crc);
619
+ var reader = b.archive.read.zip(b.archive.adapters.buffer(z64));
620
+ var entries = await reader.inspect();
621
+ check("zip64: inspect sees 1 entry", entries.length === 1);
622
+ check("zip64: name round-trips", entries[0].name === "z64.txt");
623
+ check("zip64: resolved size matches payload", entries[0].size === payload.length);
624
+
625
+ // extractEntries recovers byte-identical content (LFH/CD ZIP64 skew check
626
+ // passes against resolved sizes).
627
+ var got = null;
628
+ for await (var ent of reader.extractEntries()) { got = ent; }
629
+ check("zip64: extractEntries recovers exact bytes", got && got.bytes.equals(payload));
630
+
631
+ // localHeaderOffset sentinel + ZIP64 extra resolution.
632
+ var z64Off = buildZip64Store("z64.txt", payload, crc, { offsetSentinel: true });
633
+ var readerOff = b.archive.read.zip(b.archive.adapters.buffer(z64Off));
634
+ var gotOff = null;
635
+ for await (var entO of readerOff.extractEntries()) { gotOff = entO; }
636
+ check("zip64: lfhOffset sentinel resolved + bytes recovered",
637
+ gotOff && gotOff.bytes.equals(payload));
638
+
639
+ // EOCD64 totalEntries path — classic EOCD entry-count is the 0xFFFF
640
+ // sentinel; the true count comes from the ZIP64 EOCD record.
641
+ var z64Count = buildZip64Store("z64.txt", payload, crc, { entryCountSentinel: true });
642
+ var readerCount = b.archive.read.zip(b.archive.adapters.buffer(z64Count));
643
+ var entriesCount = await readerCount.inspect();
644
+ check("zip64: EOCD64 totalEntries resolved from sentinel", entriesCount.length === 1);
645
+
646
+ // Bomb cap still fires on the resolved 64-bit size.
647
+ var bombErr = null;
648
+ try {
649
+ var bombReader = b.archive.read.zip(b.archive.adapters.buffer(z64), {
650
+ bombPolicy: { maxEntryDecompressedBytes: 4 },
651
+ });
652
+ await bombReader.inspect();
653
+ } catch (e) { bombErr = e; }
654
+ check("zip64: bomb cap fires on resolved size",
655
+ bombErr && /entry-too-large|total-too-large/.test(bombErr.code || bombErr.message));
656
+
657
+ // Zip-Slip name refusal still fires on a ZIP64-form archive.
658
+ var slip = buildZip64Store("../../etc/passwd", payload, crc);
659
+ var slipDest = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-zip64-slip-"));
660
+ var slipErr = null;
661
+ try {
662
+ var slipReader = b.archive.read.zip(b.archive.adapters.buffer(slip));
663
+ await slipReader.extract({ destination: slipDest });
664
+ } catch (e) { slipErr = e; }
665
+ finally { fs.rmSync(slipDest, { recursive: true, force: true }); }
666
+ check("zip64: zip-slip name still refused", slipErr !== null);
667
+
668
+ // A classic EOCD claiming ZIP64 (sentinel) but missing the ZIP64 trailer
669
+ // is refused, not read with a sentinel taken literally.
670
+ var truncated = z64.slice(0, z64.length - 20 - 56 - 22); // drop eocd64 + locator + classic eocd
671
+ var orphanEocd = Buffer.alloc(22);
672
+ orphanEocd.writeUInt32LE(0x06054b50, 0);
673
+ orphanEocd.writeUInt16LE(U16_SENTINEL, 8);
674
+ orphanEocd.writeUInt16LE(U16_SENTINEL, 10);
675
+ orphanEocd.writeUInt32LE(U32_SENTINEL, 12);
676
+ orphanEocd.writeUInt32LE(U32_SENTINEL, 16);
677
+ var orphan = Buffer.concat([truncated, orphanEocd]);
678
+ var orphanErr = null;
679
+ try {
680
+ var orphanReader = b.archive.read.zip(b.archive.adapters.buffer(orphan));
681
+ await orphanReader.inspect();
682
+ } catch (e) { orphanErr = e; }
683
+ check("zip64: missing ZIP64 trailer refused (no literal-sentinel read)",
684
+ orphanErr && /zip64/.test(orphanErr.code || orphanErr.message));
685
+ }
686
+
487
687
  async function run() {
688
+ await testZip64Read();
488
689
  await testRoundTripExtract();
489
690
  await testExtractEntriesInMemory();
490
691
  await testExtractToMemoryOrchestrator();