@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.
- package/CHANGELOG.md +4 -0
- package/README.md +39 -2
- package/SECURITY.md +14 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/email.js +169 -0
- package/lib/storefront.js +368 -0
- package/lib/vendor/MANIFEST.json +84 -72
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +3 -3
- package/lib/vendor/blamejs/SECURITY.md +5 -0
- package/lib/vendor/blamejs/api-snapshot.json +15 -3
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
- package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
- package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
- package/lib/vendor/blamejs/lib/archive-read.js +215 -16
- package/lib/vendor/blamejs/lib/archive.js +206 -52
- package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
- package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
- package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
- package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
- package/lib/vendor/blamejs/lib/db-schema.js +120 -3
- package/lib/vendor/blamejs/lib/db.js +10 -3
- package/lib/vendor/blamejs/lib/error-page.js +93 -9
- package/lib/vendor/blamejs/lib/external-db.js +164 -13
- package/lib/vendor/blamejs/lib/guard-email.js +36 -3
- package/lib/vendor/blamejs/lib/http-client.js +37 -7
- package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
- package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
- package/lib/vendor/blamejs/lib/openapi.js +225 -100
- package/lib/vendor/blamejs/lib/problem-details.js +15 -3
- package/lib/vendor/blamejs/lib/queue-local.js +148 -38
- package/lib/vendor/blamejs/lib/queue.js +41 -11
- package/lib/vendor/blamejs/lib/render.js +21 -3
- package/lib/vendor/blamejs/lib/router.js +13 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
- package/lib/vendor/blamejs/lib/sse.js +7 -5
- package/lib/vendor/blamejs/lib/static.js +46 -17
- package/lib/vendor/blamejs/lib/uri-template.js +3 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
- package/lib/vendor/blamejs/test/00-primitives.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
- package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
- package/lib/wishlist-alerts.js +22 -0
- package/lib/wishlist-digest.js +18 -0
- 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();
|