@blamejs/blamejs-shop 0.3.56 → 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 (79) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +84 -72
  4. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  5. package/lib/vendor/blamejs/README.md +3 -3
  6. package/lib/vendor/blamejs/SECURITY.md +5 -0
  7. package/lib/vendor/blamejs/api-snapshot.json +15 -3
  8. package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
  9. package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
  10. package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
  11. package/lib/vendor/blamejs/lib/archive-read.js +215 -16
  12. package/lib/vendor/blamejs/lib/archive.js +206 -52
  13. package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
  14. package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
  15. package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
  16. package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
  17. package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
  18. package/lib/vendor/blamejs/lib/db-schema.js +120 -3
  19. package/lib/vendor/blamejs/lib/db.js +10 -3
  20. package/lib/vendor/blamejs/lib/error-page.js +93 -9
  21. package/lib/vendor/blamejs/lib/external-db.js +164 -13
  22. package/lib/vendor/blamejs/lib/guard-email.js +36 -3
  23. package/lib/vendor/blamejs/lib/http-client.js +37 -7
  24. package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
  25. package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
  26. package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
  27. package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
  28. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
  29. package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
  30. package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
  31. package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
  32. package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
  33. package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
  34. package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
  35. package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
  36. package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
  37. package/lib/vendor/blamejs/lib/openapi.js +225 -100
  38. package/lib/vendor/blamejs/lib/problem-details.js +15 -3
  39. package/lib/vendor/blamejs/lib/queue-local.js +148 -38
  40. package/lib/vendor/blamejs/lib/queue.js +41 -11
  41. package/lib/vendor/blamejs/lib/render.js +21 -3
  42. package/lib/vendor/blamejs/lib/router.js +13 -6
  43. package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
  44. package/lib/vendor/blamejs/lib/sse.js +7 -5
  45. package/lib/vendor/blamejs/lib/static.js +46 -17
  46. package/lib/vendor/blamejs/lib/uri-template.js +3 -1
  47. package/lib/vendor/blamejs/package.json +1 -1
  48. package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
  49. package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
  50. package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
  51. package/lib/vendor/blamejs/test/00-primitives.js +151 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
  59. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
  60. package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
  61. package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
  63. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
  65. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
  66. package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
  67. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
  68. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
  69. package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
  70. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
  71. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
  72. package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
  73. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
  74. package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
  75. package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
  76. package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
  77. package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
  78. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
  79. package/package.json +1 -1
@@ -128,6 +128,185 @@ async function run() {
128
128
  var emptyBuf = b.archive.zip().toBuffer();
129
129
  check("empty archive: just EOCD (22 bytes)", emptyBuf.length === 22);
130
130
  check("empty archive: EOCD signature", _readU32LE(emptyBuf, 0) === 0x06054b50);
131
+
132
+ await testZip64Write();
133
+ }
134
+
135
+ // ZIP64 write (APPNOTE 6.3.10 §4.3.14 EOCD64 / §4.3.15 locator / §4.4.8
136
+ // size fields / §4.5.3 extended-information extra). The writer emits
137
+ // ZIP64 only when needed; small archives stay classic byte-for-byte, and
138
+ // what it emits round-trips through the v0.14.18 random-access reader.
139
+ var SIG_EOCD64 = Buffer.from([0x50, 0x4b, 0x06, 0x06]);
140
+ var SIG_EOCD64_LOCATOR = Buffer.from([0x50, 0x4b, 0x06, 0x07]);
141
+ var METHOD_STORE = 0;
142
+
143
+ async function testZip64Write() {
144
+ // ---- Classic archives carry no ZIP64 trailer or per-entry extra ----
145
+ var classic = b.archive.zip();
146
+ classic.addFile("readme.txt", "Hello, world!\n");
147
+ classic.addFile("data/users.csv", "id,name\n1,alice\n2,bob\n");
148
+ var classicBuf = classic.toBuffer();
149
+ check("zip64-write: small archive has no ZIP64 EOCD",
150
+ classicBuf.indexOf(SIG_EOCD64) === -1);
151
+ check("zip64-write: small archive has no ZIP64 locator",
152
+ classicBuf.indexOf(SIG_EOCD64_LOCATOR) === -1);
153
+ check("zip64-write: small archive ends in classic 22-byte EOCD",
154
+ _readU32LE(classicBuf, classicBuf.length - 22) === 0x06054b50);
155
+ check("zip64-write: first LFH carries no extra field",
156
+ _readU16LE(classicBuf, 28) === 0);
157
+
158
+ // ---- Byte-stable: same inputs + mtimes emit identical bytes ----
159
+ var fixedMtime = new Date(Date.UTC(2026, 0, 1, 0, 0, 0));
160
+ function buildFixed() {
161
+ var z = b.archive.zip();
162
+ z.addFile("a.txt", "alpha", { mtime: fixedMtime });
163
+ z.addFile("b.txt", "beta", { mtime: fixedMtime });
164
+ return z.toBuffer();
165
+ }
166
+ check("zip64-write: classic output is byte-stable",
167
+ buildFixed().equals(buildFixed()));
168
+
169
+ // ---- >65535 entries forces the ZIP64 EOCD record + locator, and the
170
+ // classic EOCD entry-count fields carry the 0xFFFF sentinel. The
171
+ // reader resolves the true count from the ZIP64 EOCD record. ----
172
+ var many = b.archive.zip();
173
+ var manyCount = 65536;
174
+ for (var i = 0; i < manyCount; i++) many.addFile("f" + i + ".txt", "x");
175
+ check("zip64-write: entryCount tracks 65536 entries", many.entryCount === manyCount);
176
+ var manyBuf = many.toBuffer();
177
+ check("zip64-write: >65535 entries emits ZIP64 EOCD record",
178
+ manyBuf.indexOf(SIG_EOCD64) !== -1);
179
+ check("zip64-write: >65535 entries emits ZIP64 EOCD locator",
180
+ manyBuf.indexOf(SIG_EOCD64_LOCATOR) !== -1);
181
+ check("zip64-write: classic EOCD entry count is 0xFFFF sentinel",
182
+ _readU16LE(manyBuf, manyBuf.length - 22 + 10) === 0xffff);
183
+
184
+ var manyReader = b.archive.read.zip(b.archive.adapters.buffer(manyBuf), {
185
+ bombPolicy: { maxEntries: 2000000 },
186
+ });
187
+ var manyEntries = await manyReader.inspect();
188
+ check("zip64-write: reader resolves true count from ZIP64 EOCD",
189
+ manyEntries.length === manyCount);
190
+ check("zip64-write: first/last names round-trip",
191
+ manyEntries[0].name === "f0.txt" &&
192
+ manyEntries[manyCount - 1].name === "f65535.txt");
193
+
194
+ // Round-trip recovers byte-identical content for a sampled entry.
195
+ var firstBytes = null;
196
+ for await (var rEnt of manyReader.extractEntries({ allowEncrypted: false })) {
197
+ firstBytes = rEnt.bytes;
198
+ break;
199
+ }
200
+ check("zip64-write: extractEntries recovers content from a ZIP64 archive",
201
+ firstBytes && firstBytes.equals(Buffer.from("x")));
202
+
203
+ // ---- Per-entry ZIP64 extended-information extra field (§4.5.3) ----
204
+ // Real >4 GiB entries are impractical in a unit test; the writer's
205
+ // extra-field builder is exercised at logical sizes/offsets above the
206
+ // 32-bit limit, and the produced extra is fed to the v0.14.18 reader,
207
+ // which resolves the full 64-bit value back. The entry-count + EOCD64
208
+ // paths above already round-trip the EOCD64/locator end to end.
209
+ var z64 = b.archive._zip64ForTest;
210
+ check("zip64-write: U32_MAX is NOT an overflow (boundary)",
211
+ z64.entryNeedsZip64(z64.U32_MAX, 0, 0) === false);
212
+ check("zip64-write: size above U32_MAX overflows",
213
+ z64.entryNeedsZip64(0, z64.U32_MAX + 1, 0) === true);
214
+ check("zip64-write: offset above U32_MAX overflows",
215
+ z64.entryNeedsZip64(0, 0, z64.U32_MAX + 1) === true);
216
+
217
+ // No overflow → empty extra (classic stays classic).
218
+ check("zip64-write: no-overflow extra is empty",
219
+ z64.buildExtra(100, 100, 100, true).length === 0);
220
+
221
+ // CD extra (offset included) at overflowing usize+csize → 16-byte data
222
+ // block (uncompressedSize, compressedSize), header id 0x0001.
223
+ var big = z64.U32_MAX + 1234;
224
+ var cdExtra = z64.buildExtra(big, big, 0, true);
225
+ check("zip64-write: CD extra header id is 0x0001",
226
+ cdExtra.readUInt16LE(0) === z64.EXTRA_HEADER_ID);
227
+ check("zip64-write: CD extra data length is 16 (usize+csize)",
228
+ cdExtra.readUInt16LE(2) === 16);
229
+ check("zip64-write: CD extra carries usize then csize (APPNOTE order)",
230
+ cdExtra.readBigUInt64LE(4) === BigInt(big) &&
231
+ cdExtra.readBigUInt64LE(12) === BigInt(big));
232
+
233
+ // Offset overflow too → 24-byte data block (usize, csize, offset).
234
+ var bigOff = z64.U32_MAX + 7;
235
+ var cdExtraOff = z64.buildExtra(big, big, bigOff, true);
236
+ check("zip64-write: CD extra widens to 24 bytes when offset overflows",
237
+ cdExtraOff.readUInt16LE(2) === 24 &&
238
+ cdExtraOff.readBigUInt64LE(20) === BigInt(bigOff));
239
+
240
+ // LFH extra never carries the offset (§4.5.3) — 16 bytes even when the
241
+ // offset overflows.
242
+ var lfhExtra = z64.buildExtra(big, big, bigOff, false);
243
+ check("zip64-write: LFH extra omits the offset field",
244
+ lfhExtra.readUInt16LE(2) === 16);
245
+
246
+ // End to end: a synthetic single-entry archive whose LFH + CD ZIP64
247
+ // extra blocks come from the writer's builder, with the classic size
248
+ // fields set to the sentinel, resolves through the real reader to the
249
+ // full 64-bit logical size.
250
+ var payload = Buffer.from("ZIP64 writer-extra resolution payload.\n", "utf8");
251
+ var name = "z64w.txt";
252
+ var nameBuf = Buffer.from(name, "utf8");
253
+ var crc = b.archive._crc32ForTest(payload);
254
+ var logical = z64.U32_MAX + payload.length; // > 4 GiB
255
+
256
+ var lfhExtraBig = z64.buildExtra(logical, logical, 0, false);
257
+ var lfh = Buffer.alloc(30);
258
+ lfh.writeUInt32LE(0x04034b50, 0);
259
+ lfh.writeUInt16LE(45, 4);
260
+ lfh.writeUInt16LE(0, 6);
261
+ lfh.writeUInt16LE(METHOD_STORE, 8);
262
+ lfh.writeUInt16LE(0, 10);
263
+ lfh.writeUInt16LE(0x21, 12);
264
+ lfh.writeUInt32LE(crc >>> 0, 14);
265
+ lfh.writeUInt32LE(z64.U32_SENTINEL, 18);
266
+ lfh.writeUInt32LE(z64.U32_SENTINEL, 22);
267
+ lfh.writeUInt16LE(nameBuf.length, 26);
268
+ lfh.writeUInt16LE(lfhExtraBig.length, 28);
269
+
270
+ var cdExtraBig = z64.buildExtra(logical, logical, 0, true);
271
+ var cd = Buffer.alloc(46);
272
+ cd.writeUInt32LE(0x02014b50, 0);
273
+ cd.writeUInt16LE(45, 4);
274
+ cd.writeUInt16LE(45, 6);
275
+ cd.writeUInt16LE(0, 8);
276
+ cd.writeUInt16LE(METHOD_STORE, 10);
277
+ cd.writeUInt16LE(0, 12);
278
+ cd.writeUInt16LE(0x21, 14);
279
+ cd.writeUInt32LE(crc >>> 0, 16);
280
+ cd.writeUInt32LE(z64.U32_SENTINEL, 20);
281
+ cd.writeUInt32LE(z64.U32_SENTINEL, 24);
282
+ cd.writeUInt16LE(nameBuf.length, 28);
283
+ cd.writeUInt16LE(cdExtraBig.length, 30);
284
+ cd.writeUInt16LE(0, 32);
285
+ cd.writeUInt16LE(0, 34);
286
+ cd.writeUInt16LE(0, 36);
287
+ cd.writeUInt32LE(0, 38);
288
+ cd.writeUInt32LE(0, 42);
289
+
290
+ var localPart = Buffer.concat([lfh, nameBuf, lfhExtraBig, payload]);
291
+ var cdOffset = localPart.length;
292
+ var cdRecord = Buffer.concat([cd, nameBuf, cdExtraBig]);
293
+ var eocd = Buffer.alloc(22);
294
+ eocd.writeUInt32LE(0x06054b50, 0);
295
+ eocd.writeUInt16LE(1, 8);
296
+ eocd.writeUInt16LE(1, 10);
297
+ eocd.writeUInt32LE(cdRecord.length, 12);
298
+ eocd.writeUInt32LE(cdOffset, 16);
299
+ var synthetic = Buffer.concat([localPart, cdRecord, eocd]);
300
+
301
+ var z64Reader = b.archive.read.zip(b.archive.adapters.buffer(synthetic), {
302
+ bombPolicy: {
303
+ maxEntryDecompressedBytes: logical + 16,
304
+ maxTotalDecompressedBytes: logical + 16,
305
+ },
306
+ });
307
+ var z64Entries = await z64Reader.inspect();
308
+ check("zip64-write: reader resolves writer-built extra to 64-bit size",
309
+ z64Entries.length === 1 && z64Entries[0].size === logical);
131
310
  }
132
311
 
133
312
  module.exports = { run: run };
@@ -495,6 +495,25 @@ function run() {
495
495
  function () {});
496
496
  check("asyncapiServe: same-origin omits CORS", sentSO.headers["Access-Control-Allow-Origin"] == null);
497
497
 
498
+ // accessControl={ allowOrigin } echoes one validated origin + Vary: Origin
499
+ var serveAO = b.middleware.asyncapiServe({
500
+ document: realApi, accessControl: { allowOrigin: "https://docs.example.com" }, audit: false,
501
+ });
502
+ var sentAO = { headers: null };
503
+ serveAO({ method: "GET", url: "/asyncapi.json", pathname: "/asyncapi.json", headers: {} },
504
+ { writeHead: function (s, h) { sentAO.headers = h; }, end: function () {} },
505
+ function () {});
506
+ check("asyncapiServe: allowOrigin echoed", sentAO.headers["Access-Control-Allow-Origin"] === "https://docs.example.com");
507
+ check("asyncapiServe: allowOrigin sets Vary", sentAO.headers["Vary"] === "Origin");
508
+
509
+ // A bad allowOrigin throws at config time
510
+ rejects("asyncapiServe: allowOrigin with path rejected",
511
+ function () { b.middleware.asyncapiServe({ document: realApi, accessControl: { allowOrigin: "https://x.example.com/docs" } }); },
512
+ /bare origin/);
513
+ rejects("asyncapiServe: allowOrigin CRLF rejected",
514
+ function () { b.middleware.asyncapiServe({ document: realApi, accessControl: { allowOrigin: "https://x.example.com\r\nX-Evil: 1" } }); },
515
+ /valid|bare origin/);
516
+
498
517
  check("asyncapiServe.forceRebuild is fn", typeof serve.forceRebuild === "function");
499
518
 
500
519
  // ---- operation messages with $ref form ----
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * attach-user — configurable Authorization scheme + token extractor.
4
+ *
5
+ * Covers the bearerScheme / tokenExtractor escape hatches: the default
6
+ * "Bearer" scheme (RFC 6750 §2.1), operator-configured schemes
7
+ * ("Token", "DPoP" per RFC 9449), regex-metacharacter-safe matching,
8
+ * config-time opt validation, and the tokenExtractor override that
9
+ * fully owns header extraction.
10
+ *
11
+ * Run standalone: `node test/layer-0-primitives/attach-user-bearer-scheme.test.js`
12
+ * Or via smoke: `node test/smoke.js`
13
+ */
14
+
15
+ var helpers = require("../helpers");
16
+ var b = helpers.b;
17
+ var check = helpers.check;
18
+
19
+ var attachUser = b.middleware._modules.attachUser;
20
+
21
+ async function _drive(mw, req) {
22
+ // attachUser always calls next() — synchronously for no-token paths,
23
+ // after session.verify resolves otherwise. Poll the next()-called flag
24
+ // instead of racing a fixed sleep that drifts under runner contention.
25
+ var called = false;
26
+ mw(req, {}, function () { called = true; });
27
+ await helpers.waitUntil(function () { return called; }, {
28
+ timeoutMs: 5000,
29
+ label: "attach-user-bearer-scheme: middleware called next()",
30
+ });
31
+ }
32
+
33
+ function _req(headers) {
34
+ return {
35
+ headers: headers || {},
36
+ socket: { remoteAddress: "127.0.0.1" },
37
+ connection: { remoteAddress: "127.0.0.1" },
38
+ };
39
+ }
40
+
41
+ function testReadBearerDefaultScheme() {
42
+ check("default scheme extracts after Bearer",
43
+ attachUser._readBearer("Bearer abc123") === "abc123");
44
+ check("default scheme is case-insensitive (RFC 9110 §11.1)",
45
+ attachUser._readBearer("bearer abc123") === "abc123");
46
+ check("default scheme ignores a Token header",
47
+ attachUser._readBearer("Token abc123") === null);
48
+ check("no header → null",
49
+ attachUser._readBearer(undefined) === null);
50
+ }
51
+
52
+ function testReadBearerCustomScheme() {
53
+ check("custom Token scheme extracts the credential",
54
+ attachUser._readBearer("Token abc123", "Token") === "abc123");
55
+ check("DPoP scheme (RFC 9449) extracts the credential",
56
+ attachUser._readBearer("DPoP xyz", "DPoP") === "xyz");
57
+ // A scheme containing regex metacharacters must match literally, not
58
+ // as an injected pattern — "a.b" must not match "axb".
59
+ check("regex-meta scheme matches literally",
60
+ attachUser._readBearer("a.b qqq", "a.b") === "qqq");
61
+ check("regex-meta scheme does NOT match a wildcard expansion",
62
+ attachUser._readBearer("axb qqq", "a.b") === null);
63
+ }
64
+
65
+ function testCreateValidatesOpts() {
66
+ function threw(fn) { try { fn(); return null; } catch (e) { return e; } }
67
+ var loader = async function () { return null; };
68
+
69
+ var e1 = threw(function () {
70
+ attachUser.create({ userLoader: loader, bearerScheme: "" });
71
+ });
72
+ check("empty bearerScheme → throws", e1 && /bearerScheme/.test(e1.message));
73
+
74
+ var e2 = threw(function () {
75
+ attachUser.create({ userLoader: loader, bearerScheme: 42 });
76
+ });
77
+ check("non-string bearerScheme → throws", e2 && /bearerScheme/.test(e2.message));
78
+
79
+ var e3 = threw(function () {
80
+ attachUser.create({ userLoader: loader, tokenExtractor: "nope" });
81
+ });
82
+ check("non-function tokenExtractor → throws", e3 && /tokenExtractor/.test(e3.message));
83
+
84
+ var ok = threw(function () {
85
+ attachUser.create({ userLoader: loader, bearerScheme: "Token", tokenExtractor: function () { return null; } });
86
+ });
87
+ check("valid bearerScheme + tokenExtractor accepted", ok === null);
88
+ }
89
+
90
+ async function testCustomSchemeHeaderConsumed() {
91
+ // With bearerScheme "Token", a "Token <x>" header is read; a "Bearer
92
+ // <x>" header is ignored. A bogus token fails session.verify
93
+ // gracefully (no throw) → req.user stays null and next() runs.
94
+ var mw = attachUser.create({
95
+ userLoader: async function (s) { return { id: s.userId }; },
96
+ tokenFrom: "header",
97
+ bearerScheme: "Token",
98
+ audit: false,
99
+ });
100
+ var req = _req({ authorization: "Token bogus-session-id" });
101
+ await _drive(mw, req);
102
+ check("custom scheme: middleware ran, attached req.user (null for bogus token)",
103
+ req.user === null && req.session === null);
104
+ }
105
+
106
+ async function testTokenExtractorOwnsExtraction() {
107
+ // tokenExtractor fully owns header extraction: it is invoked and its
108
+ // result is the token handed to session.verify.
109
+ var calls = 0;
110
+ var seenReq = null;
111
+ var mw = attachUser.create({
112
+ userLoader: async function (s) { return { id: s.userId }; },
113
+ tokenFrom: "header",
114
+ tokenExtractor: function (req) { calls += 1; seenReq = req; return "extractor-token"; },
115
+ audit: false,
116
+ });
117
+ var req = _req({ authorization: "Bearer should-be-ignored" });
118
+ await _drive(mw, req);
119
+ check("tokenExtractor was invoked exactly once", calls === 1);
120
+ check("tokenExtractor received the request object", seenReq === req);
121
+ check("tokenExtractor path: bogus token → req.user null + next() ran", req.user === null);
122
+ }
123
+
124
+ async function testTokenExtractorNullSkipsAuth() {
125
+ // tokenExtractor returning null/undefined means "no token" — the
126
+ // middleware short-circuits to next() with req.user = null.
127
+ var mw = attachUser.create({
128
+ userLoader: async function () { throw new Error("userLoader must not run with no token"); },
129
+ tokenFrom: "header",
130
+ tokenExtractor: function () { return null; },
131
+ audit: false,
132
+ });
133
+ var req = _req({ authorization: "Bearer ignored" });
134
+ await _drive(mw, req);
135
+ check("tokenExtractor null → req.user null, no userLoader call", req.user === null);
136
+ }
137
+
138
+ async function run() {
139
+ testReadBearerDefaultScheme();
140
+ testReadBearerCustomScheme();
141
+ testCreateValidatesOpts();
142
+ await testCustomSchemeHeaderConsumed();
143
+ await testTokenExtractorOwnsExtraction();
144
+ await testTokenExtractorNullSkipsAuth();
145
+ }
146
+
147
+ module.exports = { run: run };
148
+
149
+ if (require.main === module) {
150
+ run().then(
151
+ function () { console.log("[attach-user-bearer-scheme] OK — " + helpers.getChecks() + " checks passed"); },
152
+ function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
153
+ );
154
+ }
@@ -100,27 +100,29 @@ async function testInvalidEofStateRefused() {
100
100
  rv.headers["connection"] === "close"));
101
101
  }
102
102
 
103
- async function testNonChunkedErrorPreservesNoCloseHeader() {
104
- // Generic parser error (not chunked-malformed) body parser still
105
- // returns 400 but does NOT add Connection: close (the smuggling
106
- // socket-reuse path only applies to the chunked-decode case).
103
+ async function testNonChunkedErrorAlsoClosesConnection() {
104
+ // A generic body-parse 4xx (here a read-abort routed through the
105
+ // generic _writeError) also sets Connection: close defense in depth
106
+ // against an upstream proxy reusing a socket whose request stream the
107
+ // parser abandoned mid-body (RFC 9112 §9.6), matching the chunked
108
+ // writers above rather than leaving the generic path uncovered.
107
109
  var err = new Error("read aborted");
108
110
  err.code = "ECONNRESET";
109
111
  var rv = await _runWithError(
110
112
  { "content-type": "application/json", "content-length": "5" },
111
113
  err
112
114
  );
113
- check("non-chunked error -> 400 without Connection: close",
115
+ check("non-chunked 4xx error -> 400 with Connection: close",
114
116
  rv.next === false && rv.status === 400 &&
115
- rv.headers && rv.headers["Connection"] !== "close" &&
116
- rv.headers["connection"] !== "close");
117
+ rv.headers && (rv.headers["Connection"] === "close" ||
118
+ rv.headers["connection"] === "close"));
117
119
  }
118
120
 
119
121
  async function run() {
120
122
  await testInvalidChunkSizeRefused();
121
123
  await testInvalidTransferEncodingRefused();
122
124
  await testInvalidEofStateRefused();
123
- await testNonChunkedErrorPreservesNoCloseHeader();
125
+ await testNonChunkedErrorAlsoClosesConnection();
124
126
  }
125
127
 
126
128
  module.exports = { run: run };
@@ -15,27 +15,24 @@ var check = helpers.check;
15
15
  var _bodyReq = helpers._bodyReq;
16
16
  var _bodyRes = helpers._bodyRes;
17
17
 
18
- function _runMiddleware(req, res) {
19
- var bp = b.middleware.bodyParser();
20
- return new Promise(function (resolve) {
21
- var settled = false;
22
- bp(req, res, function () {
23
- if (settled) return;
24
- settled = true;
25
- resolve({ next: true, status: res._endedStatus });
26
- });
27
- res.on("finish", function () {
28
- if (settled) return;
29
- settled = true;
30
- resolve({ next: false, status: res._endedStatus });
31
- });
32
- // Safety timeout so a missing finish/next doesn't hang the suite.
33
- setTimeout(function () {
34
- if (settled) return;
35
- settled = true;
36
- resolve({ next: false, status: res._endedStatus, timeout: true });
37
- }, 1500);
18
+ async function _runMiddleware(req, res, bp) {
19
+ bp = bp || b.middleware.bodyParser();
20
+ // bodyParser settles by calling next() (parse succeeded) or by writing
21
+ // the error response (res.writeHead/end → _endedStatus set, "finish"
22
+ // emitted). Both can happen synchronously inside bp(), so the next()
23
+ // flag is captured by callback and the response-write path is read off
24
+ // res._endedStatus rather than relying on catching the finish event.
25
+ // Poll the settled condition instead of racing a fixed sleep that
26
+ // drifts under runner contention.
27
+ var nextCalled = false;
28
+ bp(req, res, function () { nextCalled = true; });
29
+ await helpers.waitUntil(function () {
30
+ return nextCalled || res._endedStatus !== null;
31
+ }, {
32
+ timeoutMs: 5000,
33
+ label: "body-parser-smuggling: bodyParser settled (next or response written)",
38
34
  });
35
+ return { next: nextCalled, status: res._endedStatus };
39
36
  }
40
37
 
41
38
  async function testTeAndClConflictRejected() {
@@ -102,12 +99,94 @@ async function testCleanRequestPasses() {
102
99
  rv.next === true);
103
100
  }
104
101
 
102
+ // A malformed-JSON 400 flows through the generic _writeError path (not
103
+ // the smuggling/chunked inline writers). It must also carry Connection:
104
+ // close so an upstream proxy can't reuse a socket whose request stream
105
+ // the parser abandoned mid-body (RFC 9112 §9.6).
106
+ async function testGenericErrorSetsConnectionClose() {
107
+ var body = "{ not valid json";
108
+ var req = _bodyReq("POST", {
109
+ "content-length": String(Buffer.byteLength(body)),
110
+ "content-type": "application/json",
111
+ }, body);
112
+ var res = _bodyRes();
113
+ var rv = await _runMiddleware(req, res);
114
+ check("malformed JSON → 400 via _writeError",
115
+ rv.next === false && rv.status === 400);
116
+ check("malformed JSON 400 → Connection: close set",
117
+ res._headers["Connection"] === "close" ||
118
+ res._headers["connection"] === "close");
119
+ }
120
+
121
+ // RFC 5987 / 2231 filename* charset gating. utf-8 is always decoded; a
122
+ // `filename*=ISO-8859-1''...` part is refused by default (falls back to
123
+ // the legacy `filename=` companion) and decoded only when the operator
124
+ // opts iso-8859-1 into multipart.filenameCharsets.
125
+ function _multipartIso8859Body(boundary) {
126
+ // filename* uses ISO-8859-1 percent-encoding: r%E9sum%E9 → "résumé".
127
+ // A legacy filename= companion provides the default-path fallback.
128
+ return Buffer.from(
129
+ "--" + boundary + "\r\n" +
130
+ "Content-Disposition: form-data; name=\"doc\"; " +
131
+ "filename=\"fallback.txt\"; filename*=ISO-8859-1''r%E9sum%E9.txt\r\n" +
132
+ "Content-Type: text/plain\r\n" +
133
+ "\r\n" +
134
+ "hello\r\n" +
135
+ "--" + boundary + "--\r\n",
136
+ "latin1"
137
+ );
138
+ }
139
+
140
+ async function _runMultipart(bp, boundary, body) {
141
+ var req = _bodyReq("POST", {
142
+ "content-type": "multipart/form-data; boundary=" + boundary,
143
+ "content-length": String(body.length),
144
+ }, body);
145
+ var res = _bodyRes();
146
+ // bodyParser settles by calling next() (parse succeeded) or by writing
147
+ // the error response (res "finish"). Poll the settled flag instead of
148
+ // racing a fixed sleep that drifts under runner contention.
149
+ var settled = false;
150
+ bp(req, res, function () { settled = true; });
151
+ res.on("finish", function () { settled = true; });
152
+ await helpers.waitUntil(function () { return settled; }, {
153
+ timeoutMs: 5000,
154
+ label: "body-parser-smuggling: bodyParser settled (next or response finish)",
155
+ });
156
+ return req;
157
+ }
158
+
159
+ async function testFilenameCharsetsDefaultRefusesIso8859() {
160
+ var boundary = "bptest1";
161
+ var bp = b.middleware.bodyParser({ multipart: { storage: "memory" } });
162
+ var req = await _runMultipart(bp, boundary, _multipartIso8859Body(boundary));
163
+ check("default: a file part was parsed",
164
+ Array.isArray(req.files) && req.files.length === 1);
165
+ check("default: iso-8859-1 filename* refused → legacy filename= wins",
166
+ req.files[0].filename === "fallback.txt");
167
+ }
168
+
169
+ async function testFilenameCharsetsOptInDecodesIso8859() {
170
+ var boundary = "bptest2";
171
+ var bp = b.middleware.bodyParser({
172
+ multipart: { storage: "memory", filenameCharsets: ["utf-8", "iso-8859-1"] },
173
+ });
174
+ var req = await _runMultipart(bp, boundary, _multipartIso8859Body(boundary));
175
+ check("opt-in: a file part was parsed",
176
+ Array.isArray(req.files) && req.files.length === 1);
177
+ check("opt-in: iso-8859-1 filename* decoded to résumé.txt",
178
+ req.files[0].filename === "résumé.txt");
179
+ }
180
+
105
181
  async function run() {
106
182
  await testTeAndClConflictRejected();
107
183
  await testMultipleContentLengthRejected();
108
184
  await testTeNotChunkedRejected();
109
185
  await testTeDuplicateChunkedRejected();
110
186
  await testCleanRequestPasses();
187
+ await testGenericErrorSetsConnectionClose();
188
+ await testFilenameCharsetsDefaultRefusesIso8859();
189
+ await testFilenameCharsetsOptInDecodesIso8859();
111
190
  }
112
191
 
113
192
  module.exports = { run: run };
@@ -34,5 +34,90 @@ var check = helpers.check;
34
34
  check("after both filings, none pending", reporter.pending(rec.id).length === 0);
35
35
  check("breach closed after all filed", reporter.get(rec.id).closedAt !== null);
36
36
 
37
+ // ---- running clock (composes incident.report.createDeadlineClock) ----
38
+ // Injected clock so escalation timing is deterministic, no wall-clock sleep.
39
+ var detectedAt = 0;
40
+ var clockNow = detectedAt;
41
+ var events = [];
42
+
43
+ var clockReporter = b.breach.report.create({ audit: false, now: function () { return clockNow; } });
44
+ var clockRec = clockReporter.open({
45
+ detectedAt: detectedAt,
46
+ affectedStates: ["CA", "TX"], // CA = asap-ceiling 60d, TX = hard 60d
47
+ impact: { individualsAffected: 9000 },
48
+ });
49
+
50
+ var clock = b.breach.deadline.createClock({
51
+ audit: false,
52
+ autoStart: false,
53
+ approachThresholds: [0.5, 0.9],
54
+ notify: { send: function (p) { events.push(p); } },
55
+ now: function () { return clockNow; },
56
+ });
57
+
58
+ var trackedId = clock.trackReport(clockRec);
59
+ check("trackReport returns the breach id", trackedId === clockRec.id);
60
+ check("clock tracks both states", clock.status().tracked === 2);
61
+ check("clock counts one breach", clock.status().breaches === 1);
62
+
63
+ // Day 0: nothing has elapsed, no escalation.
64
+ clock.tick();
65
+ check("no escalation at detection time", events.length === 0);
66
+
67
+ // Advance past the 50% threshold of the 60-day window (31 days).
68
+ clockNow = detectedAt + 31 * 24 * 60 * 60 * 1000;
69
+ clock.tick();
70
+ var approaching = events.filter(function (e) { return e.kind === "deadline_approaching"; });
71
+ check("approaching fired for both states at 50%", approaching.length === 2);
72
+
73
+ // Re-ticking at the same proportion must NOT re-fire (once per phase).
74
+ var beforeReTick = events.length;
75
+ clock.tick();
76
+ check("approaching does not re-fire on repeat tick", events.length === beforeReTick);
77
+
78
+ // Acknowledge CA's filing — CA must go silent even past its deadline.
79
+ clock.acknowledgeSubmission(clockRec.id, "ca");
80
+
81
+ // Advance past the deadline (61 days). TX should fire "passed"; CA must not.
82
+ clockNow = detectedAt + 61 * 24 * 60 * 60 * 1000;
83
+ clock.tick();
84
+ var passed = events.filter(function (e) { return e.kind === "deadline_passed"; });
85
+ check("exactly one state fired passed (TX, not acked CA)", passed.length === 1);
86
+ check("passed carries the statute regime", typeof passed[0].regime === "string" && passed[0].regime.length > 0);
87
+
88
+ var ackUnknownThrew = false;
89
+ try { clock.acknowledgeSubmission(clockRec.id, "NY"); }
90
+ catch (e) { ackUnknownThrew = e.code === "breach-clock/unknown-tracked-state"; }
91
+ check("acknowledgeSubmission refuses an untracked state", ackUnknownThrew);
92
+
93
+ var badReportThrew = false;
94
+ try { clock.trackReport({ id: 42 }); }
95
+ catch (e) { badReportThrew = e.code === "breach-clock/bad-report"; }
96
+ check("trackReport refuses a non-record", badReportThrew);
97
+
98
+ check("untrack removes the breach", clock.untrack(clockRec.id) === true);
99
+ check("clock empty after untrack", clock.status().tracked === 0);
100
+
101
+ // Auto-start timer path: poll the notify sink (no fixed-budget sleep).
102
+ var autoEvents = [];
103
+ var autoNow = 0;
104
+ var autoReporter = b.breach.report.create({ audit: false, now: function () { return autoNow; } });
105
+ var autoRec = autoReporter.open({ detectedAt: 0, affectedStates: ["TX"], impact: {} });
106
+ autoNow = 61 * 24 * 60 * 60 * 1000; // already past TX's 60-day wall
107
+ var autoClock = b.breach.deadline.createClock({
108
+ audit: false,
109
+ autoStart: true,
110
+ intervalMs: 10,
111
+ notify: { send: function (p) { autoEvents.push(p); } },
112
+ now: function () { return autoNow; },
113
+ });
114
+ autoClock.trackReport(autoRec);
115
+ await helpers.waitUntil(function () {
116
+ return autoEvents.some(function (e) { return e.kind === "deadline_passed"; });
117
+ }, { timeoutMs: 5000, label: "breach-clock: auto-tick fires deadline_passed" });
118
+ check("auto-tick timer fired the passed alert", true);
119
+ autoClock.stop();
120
+ check("clock stops cleanly", autoClock.status().running === false);
121
+
37
122
  console.log("OK — breach-deadline tests");
38
123
  })().catch(function (e) { console.error(e); process.exit(1); });