@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
|
@@ -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
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
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
|
|
115
|
+
check("non-chunked 4xx error -> 400 with Connection: close",
|
|
114
116
|
rv.next === false && rv.status === 400 &&
|
|
115
|
-
rv.headers && rv.headers["Connection"]
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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); });
|