@blamejs/core 0.14.17 → 0.14.19

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/lib/archive.js CHANGED
@@ -33,16 +33,20 @@
33
33
  * null bytes, and `..` segments throw `archive/bad-name`.
34
34
  * - No symlink emission — only regular file entries are produced.
35
35
  * - SHA3-512 fingerprint via `digest()` for operator integrity logs.
36
+ * - ZIP64 (APPNOTE 6.3.10 §4.3.14 / §4.3.15 / §4.4.8 / §4.5.3) is
37
+ * emitted automatically when an archive exceeds 65535 entries or
38
+ * any entry's compressed/uncompressed size or local-header offset
39
+ * exceeds 4 GiB: the classic field carries the 0xFFFF/0xFFFFFFFF
40
+ * sentinel, a ZIP64 extended-information extra field supplies the
41
+ * 64-bit value, and the ZIP64 EOCD record + locator precede the
42
+ * classic EOCD. Archives below those limits stay classic
43
+ * byte-for-byte. `b.archive.read.zip` reads the produced ZIP64
44
+ * form transparently.
36
45
  *
37
46
  * Out of scope (v1):
38
- * - ZIP64 (>4 GiB archives, >65535 files) — `toBuffer` and
39
- * `toStream` throw `archive/too-many-entries` past the limit;
40
- * operators at that scale bring their own toolset.
41
47
  * - ZIP-native password encryption (broken-by-design); operators
42
48
  * wrap the produced bytes via `b.crypto.encryptPacked` for
43
49
  * encryption-at-rest.
44
- * - Reading / extraction — write-only; operators use yauzl or
45
- * `unzip` for read paths.
46
50
  *
47
51
  * @card
48
52
  * ZIP archive creation primitive.
@@ -61,6 +65,30 @@ var ArchiveError = defineClass("ArchiveError", { alwaysPermanent: true });
61
65
  var SIG_LFH = 0x04034b50; // local file header
62
66
  var SIG_CFH = 0x02014b50; // central directory file header
63
67
  var SIG_EOCD = 0x06054b50; // end of central directory
68
+ var SIG_EOCD64 = 0x06064b50; // APPNOTE §4.3.14 ZIP64 EOCD record
69
+ var SIG_EOCD64_LOCATOR = 0x07064b50; // APPNOTE §4.3.15 ZIP64 EOCD locator
70
+
71
+ // ZIP64 sentinels (APPNOTE §4.4 + §4.5.3) — a classic field set to its
72
+ // all-ones value signals that the true value lives in the ZIP64 record /
73
+ // extended-information extra field. 16-bit fields use 0xFFFF, 32-bit
74
+ // fields use 0xFFFFFFFF.
75
+ var ZIP64_U16_SENTINEL = 0xffff;
76
+ var ZIP64_U32_SENTINEL = 0xffffffff;
77
+ // 0xFFFFFFFF as a value boundary: any size/offset > this overflows the
78
+ // classic 32-bit field and must be carried in the ZIP64 extra field.
79
+ var ZIP64_U32_MAX = 0xffffffff;
80
+ // Classic EOCD entry-count field is 16-bit (APPNOTE §4.4.21/§4.4.22);
81
+ // more than 65535 entries forces the ZIP64 EOCD record.
82
+ var ZIP64_MAX_CLASSIC_ENTRIES = 65535;
83
+ // ZIP64 "version needed to extract" — 4.5 (APPNOTE §4.4.3.2).
84
+ var ZIP64_VERSION_NEEDED = 45;
85
+ // ZIP64 extended-information extra field (§4.5.3): 4-byte header
86
+ // (id(2) + dataSize(2)) then up to four fields. Each 64-bit field is
87
+ // 8 bytes; diskStart is a 4-byte dword.
88
+ var ZIP64_EXTRA_HEADER_ID = 0x0001;
89
+ var ZIP64_EXTRA_FIELD_BYTES = 8; // one 64-bit field (uSize / cSize / lfhOffset)
90
+ var ZIP64_EOCD64_BYTES = 56; // §4.3.14 fixed-size record (no extensible-data tail)
91
+ var ZIP64_EOCD64_LOCATOR_BYTES = 20; // §4.3.15 fixed-size locator
64
92
 
65
93
  // Compression methods (APPNOTE 4.4.5 — protocol-fixed method IDs)
66
94
  var METHOD_STORE_ID = 0;
@@ -101,6 +129,52 @@ function _msdosDateTime(date) {
101
129
  return { time: dosTime, date: dosDate };
102
130
  }
103
131
 
132
+ // ZIP64 (APPNOTE 6.3.10 §4.3.14 / §4.3.15 / §4.4.8 / §4.5.3) — small
133
+ // archives stay classic byte-for-byte; the ZIP64 trailer + per-entry
134
+ // sentinels only appear once a size/offset overflows the classic 32-bit
135
+ // field (or the entry count exceeds the classic 16-bit cap). The reader
136
+ // in archive-read.js resolves these symmetrically.
137
+
138
+ // True when a single size/offset overflows the classic 32-bit field.
139
+ function _overflows32(n) { return n > ZIP64_U32_MAX; }
140
+
141
+ // Does this entry need a per-record ZIP64 extra block? An entry overflows
142
+ // when its compressed size, uncompressed size, or local-header offset
143
+ // exceeds the 32-bit limit. `lfhOffset` is only known at central-directory
144
+ // build time, so the local-header path passes `lfhOffset = 0` (the LFH
145
+ // extra never carries the offset — §4.5.3).
146
+ function _entryNeedsZip64(csize, usize, lfhOffset) {
147
+ return _overflows32(csize) || _overflows32(usize) || _overflows32(lfhOffset);
148
+ }
149
+
150
+ // Build the ZIP64 extended-information extra field (§4.5.3) carrying ONLY
151
+ // the fields whose classic value overflowed, in APPNOTE order:
152
+ // uncompressedSize, compressedSize, localHeaderOffset, diskStart. Returns
153
+ // an empty buffer when nothing overflowed. `includeOffset` controls
154
+ // whether localHeaderOffset is appended (the LFH variant omits it). The
155
+ // reader keys presence off the matching classic sentinel, so a field is
156
+ // emitted here iff the caller also writes the sentinel into the classic
157
+ // slot. diskStart is never emitted — single-disk archives only.
158
+ function _buildZip64Extra(csize, usize, lfhOffset, includeOffset) {
159
+ var needUsize = _overflows32(usize);
160
+ var needCsize = _overflows32(csize);
161
+ var needOffset = includeOffset && _overflows32(lfhOffset);
162
+ if (!needUsize && !needCsize && !needOffset) return Buffer.alloc(0);
163
+ var fields = 0;
164
+ if (needUsize) fields += 1;
165
+ if (needCsize) fields += 1;
166
+ if (needOffset) fields += 1;
167
+ var dataLen = fields * ZIP64_EXTRA_FIELD_BYTES;
168
+ var extra = Buffer.alloc(C.BYTES.bytes(4 + dataLen));
169
+ extra.writeUInt16LE(ZIP64_EXTRA_HEADER_ID, C.BYTES.bytes(0)); // §4.5.3 extra-field tag
170
+ extra.writeUInt16LE(dataLen, C.BYTES.bytes(2)); // §4.5.1 data size
171
+ var q = 4;
172
+ if (needUsize) { extra.writeBigUInt64LE(BigInt(usize), C.BYTES.bytes(q)); q += ZIP64_EXTRA_FIELD_BYTES; }
173
+ if (needCsize) { extra.writeBigUInt64LE(BigInt(csize), C.BYTES.bytes(q)); q += ZIP64_EXTRA_FIELD_BYTES; }
174
+ if (needOffset) { extra.writeBigUInt64LE(BigInt(lfhOffset), C.BYTES.bytes(q)); q += ZIP64_EXTRA_FIELD_BYTES; }
175
+ return extra;
176
+ }
177
+
104
178
  /**
105
179
  * @primitive b.archive.zip
106
180
  * @signature b.archive.zip()
@@ -223,66 +297,156 @@ function zip() {
223
297
  var nameBuf = Buffer.from(entry.name, "utf8");
224
298
  var dt = _msdosDateTime(entry.mtime);
225
299
  var flags = FLAG_UTF8_NAME | (streaming ? FLAG_DATA_DESCRIPTOR : 0);
300
+ // ZIP64 (§4.3.7 + §4.5.3) applies to the buffer path only — the LFH
301
+ // sizes are written up-front there. Streaming entries carry zeros
302
+ // under the data-descriptor flag (sizes unknown at header time), so
303
+ // they never carry an LFH ZIP64 extra; their 64-bit values ride the
304
+ // data descriptor + central-directory ZIP64 extra. When either size
305
+ // overflows the 32-bit field, the LFH carries the sentinel and a
306
+ // ZIP64 extra block supplies uncompressedSize + compressedSize (the
307
+ // offset is never in the LFH extra — §4.5.3).
308
+ var csize = streaming ? 0 : entry.stored.length;
309
+ var usize = streaming ? 0 : entry.uncompressedSize;
310
+ var zip64 = !streaming && _entryNeedsZip64(csize, usize, 0);
311
+ var zip64Extra = zip64 ? _buildZip64Extra(csize, usize, 0, false) : Buffer.alloc(0);
226
312
  // APPNOTE 4.3.7 — local file header. Offsets are byte positions
227
313
  // within the 30-byte fixed header; each route through C.BYTES.bytes
228
314
  // so the framework's byte-math discipline applies even to format-
229
315
  // fixed offsets.
230
316
  var hdr = Buffer.alloc(C.BYTES.bytes(30));
231
317
  hdr.writeUInt32LE(SIG_LFH, C.BYTES.bytes(0));
232
- hdr.writeUInt16LE(20, C.BYTES.bytes(4)); // version needed
318
+ hdr.writeUInt16LE(zip64 ? ZIP64_VERSION_NEEDED : 20, C.BYTES.bytes(4)); // version needed
233
319
  hdr.writeUInt16LE(flags, C.BYTES.bytes(6)); // flags: bit 11 UTF-8, bit 3 data-descriptor
234
320
  hdr.writeUInt16LE(entry.method, C.BYTES.bytes(0x08));
235
321
  hdr.writeUInt16LE(dt.time, C.BYTES.bytes(10));
236
322
  hdr.writeUInt16LE(dt.date, C.BYTES.bytes(12));
237
323
  hdr.writeUInt32LE(streaming ? 0 : entry.crc, C.BYTES.bytes(14));
238
- hdr.writeUInt32LE(streaming ? 0 : entry.stored.length, C.BYTES.bytes(18));
239
- hdr.writeUInt32LE(streaming ? 0 : entry.uncompressedSize, C.BYTES.bytes(22));
324
+ hdr.writeUInt32LE(_overflows32(csize) ? ZIP64_U32_SENTINEL : csize, C.BYTES.bytes(18));
325
+ hdr.writeUInt32LE(_overflows32(usize) ? ZIP64_U32_SENTINEL : usize, C.BYTES.bytes(22));
240
326
  hdr.writeUInt16LE(nameBuf.length, C.BYTES.bytes(26));
241
- hdr.writeUInt16LE(0, C.BYTES.bytes(28)); // extra field length
242
- return Buffer.concat([hdr, nameBuf]);
327
+ hdr.writeUInt16LE(zip64Extra.length, C.BYTES.bytes(28)); // extra field length
328
+ return Buffer.concat([hdr, nameBuf, zip64Extra]);
243
329
  }
244
330
 
245
331
  function _buildDataDescriptor(crc, csize, usize) {
246
- // APPNOTE 4.3.9 — 16-byte data descriptor (with optional sig dword).
247
- var dd = Buffer.alloc(C.BYTES.bytes(16));
248
- dd.writeUInt32LE(SIG_DATA_DESCRIPTOR, C.BYTES.bytes(0));
249
- dd.writeUInt32LE(crc, C.BYTES.bytes(4));
250
- dd.writeUInt32LE(csize, C.BYTES.bytes(0x08));
251
- dd.writeUInt32LE(usize, C.BYTES.bytes(12));
252
- return dd;
332
+ // APPNOTE 4.3.9 — data descriptor (with optional sig dword). The
333
+ // classic form carries 4-byte csize/usize; §4.3.9.2 widens both to
334
+ // 8 bytes when the entry is ZIP64 (either size overflows the 32-bit
335
+ // field). The central directory carries the authoritative sizes, so
336
+ // the wide form here is for external single-pass extractors.
337
+ var zip64 = _overflows32(csize) || _overflows32(usize);
338
+ if (!zip64) {
339
+ var dd = Buffer.alloc(C.BYTES.bytes(16));
340
+ dd.writeUInt32LE(SIG_DATA_DESCRIPTOR, C.BYTES.bytes(0));
341
+ dd.writeUInt32LE(crc, C.BYTES.bytes(4));
342
+ dd.writeUInt32LE(csize, C.BYTES.bytes(0x08));
343
+ dd.writeUInt32LE(usize, C.BYTES.bytes(12));
344
+ return dd;
345
+ }
346
+ var dd64 = Buffer.alloc(C.BYTES.bytes(24));
347
+ dd64.writeUInt32LE(SIG_DATA_DESCRIPTOR, C.BYTES.bytes(0));
348
+ dd64.writeUInt32LE(crc, C.BYTES.bytes(4));
349
+ dd64.writeBigUInt64LE(BigInt(csize), C.BYTES.bytes(0x08));
350
+ dd64.writeBigUInt64LE(BigInt(usize), C.BYTES.bytes(0x10));
351
+ return dd64;
253
352
  }
254
353
 
255
354
  function _buildCentralDirectoryEntry(entry, lfhOffset) {
256
355
  var nameBuf = Buffer.from(entry.name, "utf8");
257
356
  var dt = _msdosDateTime(entry.mtime);
258
357
  var flags = FLAG_UTF8_NAME | (entry.kind === "stream" ? FLAG_DATA_DESCRIPTOR : 0);
358
+ var csize = entry.stored.length;
359
+ var usize = entry.uncompressedSize;
360
+ // ZIP64 (§4.3.12 + §4.4.8 + §4.5.3): the central-directory entry
361
+ // carries the offset, so its ZIP64 trigger includes localHeaderOffset
362
+ // overflow. Each overflowed field becomes the classic sentinel and is
363
+ // supplied 64-bit in the extra block, in APPNOTE order.
364
+ var zip64 = _entryNeedsZip64(csize, usize, lfhOffset);
365
+ var zip64Extra = zip64 ? _buildZip64Extra(csize, usize, lfhOffset, true) : Buffer.alloc(0);
259
366
  // APPNOTE 4.3.12 — central directory file header (46-byte fixed prefix).
260
367
  var hdr = Buffer.alloc(C.BYTES.bytes(46));
261
368
  hdr.writeUInt32LE(SIG_CFH, C.BYTES.bytes(0));
262
369
  hdr.writeUInt16LE(0x033f, C.BYTES.bytes(4)); // version made by (UNIX | 6.3)
263
- hdr.writeUInt16LE(20, C.BYTES.bytes(6)); // version needed
370
+ hdr.writeUInt16LE(zip64 ? ZIP64_VERSION_NEEDED : 20, C.BYTES.bytes(6)); // version needed
264
371
  hdr.writeUInt16LE(flags, C.BYTES.bytes(0x08)); // flags: bit 11 UTF-8, bit 3 data-descriptor (stream)
265
372
  hdr.writeUInt16LE(entry.method, C.BYTES.bytes(10));
266
373
  hdr.writeUInt16LE(dt.time, C.BYTES.bytes(12));
267
374
  hdr.writeUInt16LE(dt.date, C.BYTES.bytes(14));
268
375
  hdr.writeUInt32LE(entry.crc, C.BYTES.bytes(0x10));
269
- hdr.writeUInt32LE(entry.stored.length, C.BYTES.bytes(20));
270
- hdr.writeUInt32LE(entry.uncompressedSize, C.BYTES.bytes(0x18));
376
+ hdr.writeUInt32LE(_overflows32(csize) ? ZIP64_U32_SENTINEL : csize, C.BYTES.bytes(20));
377
+ hdr.writeUInt32LE(_overflows32(usize) ? ZIP64_U32_SENTINEL : usize, C.BYTES.bytes(0x18));
271
378
  hdr.writeUInt16LE(nameBuf.length, C.BYTES.bytes(28));
272
- hdr.writeUInt16LE(0, C.BYTES.bytes(30)); // extra field length
379
+ hdr.writeUInt16LE(zip64Extra.length, C.BYTES.bytes(30)); // extra field length
273
380
  hdr.writeUInt16LE(0, C.BYTES.bytes(0x20)); // file comment length
274
381
  hdr.writeUInt16LE(0, C.BYTES.bytes(34)); // disk number start
275
382
  hdr.writeUInt16LE(0, C.BYTES.bytes(36)); // internal file attributes
276
383
  hdr.writeUInt32LE(0, C.BYTES.bytes(38)); // external file attributes
277
- hdr.writeUInt32LE(lfhOffset, C.BYTES.bytes(42));
278
- return Buffer.concat([hdr, nameBuf]);
384
+ hdr.writeUInt32LE(_overflows32(lfhOffset) ? ZIP64_U32_SENTINEL : lfhOffset, C.BYTES.bytes(42));
385
+ return Buffer.concat([hdr, nameBuf, zip64Extra]);
279
386
  }
280
387
 
281
- function toBuffer() {
282
- if (entries.length > 65535) {
283
- throw new ArchiveError("archive/too-many-entries",
284
- "ZIP archive cannot contain more than 65535 entries (ZIP64 unsupported in v1)");
388
+ // Build the end-of-central-directory trailer. Returns a Buffer that is
389
+ // just the classic 22-byte EOCD for archives within the classic limits,
390
+ // or the ZIP64 EOCD record (§4.3.14) + ZIP64 EOCD locator (§4.3.15) +
391
+ // the classic EOCD (with sentinels) when the entry count exceeds 65535
392
+ // or the central-directory size/offset exceeds the 32-bit field. The
393
+ // ZIP64 trailer precedes the classic EOCD exactly where the reader's
394
+ // locator-before-classic-EOCD walk expects it.
395
+ function _buildEndOfCentralDirectory(totalEntries, cdSize, cdStart) {
396
+ var needZip64 = totalEntries > ZIP64_MAX_CLASSIC_ENTRIES ||
397
+ _overflows32(cdSize) || _overflows32(cdStart);
398
+ if (!needZip64) {
399
+ // APPNOTE 4.3.16 — end of central directory record (22-byte fixed).
400
+ var eocdClassic = Buffer.alloc(C.BYTES.bytes(22));
401
+ eocdClassic.writeUInt32LE(SIG_EOCD, C.BYTES.bytes(0));
402
+ eocdClassic.writeUInt16LE(0, C.BYTES.bytes(4)); // disk number
403
+ eocdClassic.writeUInt16LE(0, C.BYTES.bytes(6)); // disk where CD starts
404
+ eocdClassic.writeUInt16LE(totalEntries, C.BYTES.bytes(0x08)); // entries on this disk
405
+ eocdClassic.writeUInt16LE(totalEntries, C.BYTES.bytes(10)); // total entries
406
+ eocdClassic.writeUInt32LE(cdSize, C.BYTES.bytes(12)); // size of central directory
407
+ eocdClassic.writeUInt32LE(cdStart, C.BYTES.bytes(0x10)); // offset of central directory
408
+ eocdClassic.writeUInt16LE(0, C.BYTES.bytes(20)); // comment length
409
+ return eocdClassic;
285
410
  }
411
+ // ZIP64 EOCD record (§4.3.14) — fixed 56-byte form, no extensible
412
+ // data tail. The "size of ZIP64 EOCD record" field counts the bytes
413
+ // that FOLLOW it (record total minus the 12-byte sig+size prefix).
414
+ var eocd64 = Buffer.alloc(C.BYTES.bytes(ZIP64_EOCD64_BYTES));
415
+ eocd64.writeUInt32LE(SIG_EOCD64, C.BYTES.bytes(0));
416
+ eocd64.writeBigUInt64LE(BigInt(ZIP64_EOCD64_BYTES - 12), C.BYTES.bytes(4));
417
+ eocd64.writeUInt16LE(0x033f, C.BYTES.bytes(12)); // version made by (UNIX | 6.3)
418
+ eocd64.writeUInt16LE(ZIP64_VERSION_NEEDED, C.BYTES.bytes(14)); // version needed
419
+ eocd64.writeUInt32LE(0, C.BYTES.bytes(16)); // this disk number
420
+ eocd64.writeUInt32LE(0, C.BYTES.bytes(20)); // disk with CD start
421
+ eocd64.writeBigUInt64LE(BigInt(totalEntries), C.BYTES.bytes(24)); // entries on this disk
422
+ eocd64.writeBigUInt64LE(BigInt(totalEntries), C.BYTES.bytes(32)); // total entries
423
+ eocd64.writeBigUInt64LE(BigInt(cdSize), C.BYTES.bytes(40)); // central directory size
424
+ eocd64.writeBigUInt64LE(BigInt(cdStart), C.BYTES.bytes(48)); // central directory offset
425
+ var eocd64Offset = cdStart + cdSize;
426
+ // ZIP64 EOCD locator (§4.3.15) — fixed 20 bytes.
427
+ var locator = Buffer.alloc(C.BYTES.bytes(ZIP64_EOCD64_LOCATOR_BYTES));
428
+ locator.writeUInt32LE(SIG_EOCD64_LOCATOR, C.BYTES.bytes(0));
429
+ locator.writeUInt32LE(0, C.BYTES.bytes(4)); // disk with ZIP64 EOCD
430
+ locator.writeBigUInt64LE(BigInt(eocd64Offset), C.BYTES.bytes(0x08));
431
+ locator.writeUInt32LE(1, C.BYTES.bytes(16)); // total number of disks
432
+ // Classic EOCD (§4.3.16) with ZIP64 sentinels for any overflowed
433
+ // field — readers that don't grok ZIP64 see the sentinel, ZIP64-aware
434
+ // readers follow the locator.
435
+ var eocd = Buffer.alloc(C.BYTES.bytes(22));
436
+ eocd.writeUInt32LE(SIG_EOCD, C.BYTES.bytes(0));
437
+ eocd.writeUInt16LE(0, C.BYTES.bytes(4));
438
+ eocd.writeUInt16LE(0, C.BYTES.bytes(6));
439
+ eocd.writeUInt16LE(totalEntries > ZIP64_MAX_CLASSIC_ENTRIES
440
+ ? ZIP64_U16_SENTINEL : totalEntries, C.BYTES.bytes(0x08));
441
+ eocd.writeUInt16LE(totalEntries > ZIP64_MAX_CLASSIC_ENTRIES
442
+ ? ZIP64_U16_SENTINEL : totalEntries, C.BYTES.bytes(10));
443
+ eocd.writeUInt32LE(_overflows32(cdSize) ? ZIP64_U32_SENTINEL : cdSize, C.BYTES.bytes(12));
444
+ eocd.writeUInt32LE(_overflows32(cdStart) ? ZIP64_U32_SENTINEL : cdStart, C.BYTES.bytes(0x10));
445
+ eocd.writeUInt16LE(0, C.BYTES.bytes(20));
446
+ return Buffer.concat([eocd64, locator, eocd]);
447
+ }
448
+
449
+ function toBuffer() {
286
450
  for (var k = 0; k < entries.length; k++) {
287
451
  if (entries[k].kind === "stream") {
288
452
  throw new ArchiveError("archive/streaming-entry",
@@ -307,17 +471,7 @@ function zip() {
307
471
  pieces.push(cdh);
308
472
  cdSize += cdh.length;
309
473
  }
310
- // APPNOTE 4.3.16 end of central directory record (22-byte fixed).
311
- var eocd = Buffer.alloc(C.BYTES.bytes(22));
312
- eocd.writeUInt32LE(SIG_EOCD, C.BYTES.bytes(0));
313
- eocd.writeUInt16LE(0, C.BYTES.bytes(4)); // disk number
314
- eocd.writeUInt16LE(0, C.BYTES.bytes(6)); // disk where CD starts
315
- eocd.writeUInt16LE(entries.length, C.BYTES.bytes(0x08)); // entries on this disk
316
- eocd.writeUInt16LE(entries.length, C.BYTES.bytes(10)); // total entries
317
- eocd.writeUInt32LE(cdSize, C.BYTES.bytes(12)); // size of central directory
318
- eocd.writeUInt32LE(cdStart, C.BYTES.bytes(0x10)); // offset of central directory
319
- eocd.writeUInt16LE(0, C.BYTES.bytes(20)); // comment length
320
- pieces.push(eocd);
474
+ pieces.push(_buildEndOfCentralDirectory(entries.length, cdSize, cdStart));
321
475
  return Buffer.concat(pieces);
322
476
  }
323
477
 
@@ -451,11 +605,6 @@ function zip() {
451
605
  "toStream: writable must be a Writable (or omit to receive a Readable)");
452
606
  }
453
607
 
454
- if (entries.length > 65535) {
455
- throw new ArchiveError("archive/too-many-entries",
456
- "ZIP archive cannot contain more than 65535 entries (ZIP64 unsupported in v1)");
457
- }
458
-
459
608
  var run = (async function () {
460
609
  var offsets = [];
461
610
  var totalLocalBytes = 0;
@@ -480,15 +629,7 @@ function zip() {
480
629
  await _writeChunk(dest, cdh);
481
630
  cdSize += cdh.length;
482
631
  }
483
- var eocd = Buffer.alloc(C.BYTES.bytes(22));
484
- eocd.writeUInt32LE(SIG_EOCD, C.BYTES.bytes(0));
485
- eocd.writeUInt16LE(0, C.BYTES.bytes(4));
486
- eocd.writeUInt16LE(0, C.BYTES.bytes(6));
487
- eocd.writeUInt16LE(entries.length, C.BYTES.bytes(0x08));
488
- eocd.writeUInt16LE(entries.length, C.BYTES.bytes(10));
489
- eocd.writeUInt32LE(cdSize, C.BYTES.bytes(12));
490
- eocd.writeUInt32LE(cdStart, C.BYTES.bytes(0x10));
491
- eocd.writeUInt16LE(0, C.BYTES.bytes(20));
632
+ var eocd = _buildEndOfCentralDirectory(entries.length, cdSize, cdStart);
492
633
  await _writeChunk(dest, eocd);
493
634
  if (typeof dest.end === "function") dest.end();
494
635
  _emitAudit(opts, "archive.zip.streamed.completed", "success", {
@@ -574,4 +715,17 @@ module.exports = {
574
715
  // Test-only export — operators don't call this; it's here for unit-testing
575
716
  // the CRC implementation against known vectors.
576
717
  _crc32ForTest: _crc32,
718
+ // Test-only export — exercises the per-entry ZIP64 extended-information
719
+ // extra-field builder (§4.5.3) at logical sizes/offsets that exceed the
720
+ // 32-bit field, which the buffer path can only reach with multi-GiB
721
+ // payloads. The entry-count and EOCD64 paths are covered by full
722
+ // round-trips through the random-access reader.
723
+ _zip64ForTest: {
724
+ entryNeedsZip64: _entryNeedsZip64,
725
+ buildExtra: _buildZip64Extra,
726
+ U16_SENTINEL: ZIP64_U16_SENTINEL,
727
+ U32_SENTINEL: ZIP64_U32_SENTINEL,
728
+ U32_MAX: ZIP64_U32_MAX,
729
+ EXTRA_HEADER_ID: ZIP64_EXTRA_HEADER_ID,
730
+ },
577
731
  };
package/lib/auth/oauth.js CHANGED
@@ -497,6 +497,55 @@ function create(opts) {
497
497
  });
498
498
  }
499
499
 
500
+ // PKCE downgrade defense (RFC 9700 §4.13 / OAuth 2.1 §6.2.4 +
501
+ // RFC 7636). The client always sends code_challenge_method=S256 (the
502
+ // plain method and pkce:false are refused). A network attacker who
503
+ // can tamper with discovery metadata can advertise an OP that only
504
+ // supports the `plain` method (or omits S256), nudging a permissive
505
+ // client into a weaker exchange. We don't downgrade — but if the OP's
506
+ // published `code_challenge_methods_supported` is PRESENT and does not
507
+ // list "S256", the redirect we'd build sends an S256 challenge the OP
508
+ // claims it cannot verify, which is the signature of a stripped-S256
509
+ // MITM. Refuse rather than emit an authorization request the metadata
510
+ // says will fail.
511
+ //
512
+ // Back-compat: an OP that does not publish the field at all keeps
513
+ // today's behavior (S256 is still sent — RFC 7636 §4.2 lets the OP
514
+ // accept S256 without advertising it). The check is a non-fetching
515
+ // peek at the already-resolved discovery document: it never forces a
516
+ // network round-trip, so static-endpoint clients (no discovery) are
517
+ // unaffected. Config-time refusal — throw so the operator sees the
518
+ // mismatch instead of a silently-doomed redirect.
519
+ function _assertS256Supported(config) {
520
+ if (!config || typeof config !== "object") return;
521
+ var methods = config.code_challenge_methods_supported;
522
+ if (!Array.isArray(methods)) return; // field absent → keep behavior
523
+ var hasS256 = false;
524
+ for (var i = 0; i < methods.length; i++) {
525
+ if (methods[i] === "S256") { hasS256 = true; break; }
526
+ }
527
+ if (!hasS256) {
528
+ throw new OAuthError("auth-oauth/pkce-downgrade",
529
+ "OP discovery advertises code_challenge_methods_supported " +
530
+ JSON.stringify(methods) + " without 'S256'. The framework sends " +
531
+ "S256 (RFC 7636) and refuses to emit an authorization request the " +
532
+ "OP claims it cannot verify — a stripped-S256 / plain-only " +
533
+ "discovery is the signature of a PKCE downgrade (RFC 9700 §4.13). " +
534
+ "Fix the OP metadata or, on a genuinely S256-incapable IdP, " +
535
+ "front it with a conforming gateway.");
536
+ }
537
+ }
538
+
539
+ // Peek the cached discovery document WITHOUT triggering a fetch, so
540
+ // the PKCE-downgrade gate only inspects metadata the client already
541
+ // resolved on the discovery path. Returns null when no discovery has
542
+ // occurred (static endpoints / non-OIDC) — back-compat preserved.
543
+ async function _peekDiscovery() {
544
+ if (!isOidc || !issuer) return null;
545
+ try { return (await _discoveryCache.get("config")) || null; }
546
+ catch (_e) { return null; }
547
+ }
548
+
500
549
  async function _resolveEndpoint(name) {
501
550
  if (staticEndpoints[name]) return staticEndpoints[name];
502
551
  var config = await _discover();
@@ -530,6 +579,11 @@ function create(opts) {
530
579
  async function authorizationUrl(uopts) {
531
580
  uopts = uopts || {};
532
581
  var endpoint = await _resolveEndpoint("authorizationEndpoint");
582
+ // RFC 9700 §4.13 — refuse an OP whose discovery metadata advertises
583
+ // code_challenge_methods_supported without S256 (PKCE downgrade /
584
+ // stripped-S256 MITM). _resolveEndpoint already populated the
585
+ // discovery cache on the OIDC path; this peek never fetches.
586
+ _assertS256Supported(await _peekDiscovery());
533
587
  // CVE-2026-34511 — PKCE verifier leak via state. The state token is
534
588
  // an opaque CSPRNG output; the PKCE verifier is generated separately
535
589
  // and returned in its own field for the caller to store. The
@@ -1270,6 +1324,10 @@ function create(opts) {
1270
1324
  "pushed_authorization_request_endpoint (set opts.pushedAuthorizationRequestEndpoint " +
1271
1325
  "on create() if the IdP doesn't publish it)");
1272
1326
  }
1327
+ // Same PKCE-downgrade gate as authorizationUrl (RFC 9700 §4.13):
1328
+ // PAR pushes the identical S256 challenge, so an OP advertising
1329
+ // code_challenge_methods_supported without S256 is refused here too.
1330
+ _assertS256Supported(await _peekDiscovery());
1273
1331
  // Build the same param set authorizationUrl would emit, then POST
1274
1332
  // it to PAR instead of putting it in the redirect URL.
1275
1333
  var state = uopts.state || _generateRandomToken(STATE_NONCE_BYTES);
@@ -79,14 +79,15 @@ function _b64uDecodeStr(s) {
79
79
  return Buffer.from(s, "base64url").toString("utf8");
80
80
  }
81
81
 
82
- function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId, supportedAlgs, proofMaxAgeMs) {
82
+ async function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId, supportedAlgs, proofMaxAgeMs, resolveKid) {
83
83
  // OID4VCI §7.2.1.1: the proof JWT MUST:
84
84
  // - typ = "openid4vci-proof+jwt"
85
85
  // - alg in supported list (issuer publishes these)
86
86
  // - aud = credential issuer URL (this issuer's `credential_issuer`)
87
87
  // - iat = recent
88
88
  // - nonce = c_nonce previously issued to the wallet
89
- // - jwk OR kid in header pointing at the key to bind cnf to
89
+ // - jwk (inline) OR kid (resolved via resolveKid) in the header
90
+ // pointing at the holder key to bind cnf to (RFC 7515 §4.1.3/§4.1.4)
90
91
  if (typeof proofJwt !== "string" || proofJwt.length === 0 || proofJwt.length > MAX_PROOF_BYTES) {
91
92
  throw new AuthError("auth-oid4vci/bad-proof",
92
93
  "credential issuance: proof JWT is empty or exceeds " + MAX_PROOF_BYTES + " bytes");
@@ -171,29 +172,78 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
171
172
  "credential issuance: proof JWT iss does not match the access-token client_id");
172
173
  }
173
174
 
174
- // Verify the JWS signature using the key embedded in the header.
175
+ // Resolve the holder key the proof is signed with. Two paths:
176
+ // - inline `jwk` (RFC 7515 §4.1.3) — the wallet ships the public
177
+ // key in the header; bind `cnf` to it directly.
178
+ // - `kid` (RFC 7515 §4.1.4) without inline `jwk` — the wallet
179
+ // references a key by identifier (EUDI-Wallet attested-key flow,
180
+ // OID4VCI §8.2.1.1 `key_attestation` proof). The operator
181
+ // supplies `resolveKid(kid, header)` to map the kid → public key.
182
+ // With no resolver configured the issuer keeps the clear refusal
183
+ // (back-compat): a kid-only proof can't be verified without one.
175
184
  var holderKeyJwk = header.jwk || null;
176
- if (!holderKeyJwk && header.kid) {
177
- // Operators with kid-only proofs supply a resolver; until then,
178
- // require jwk inline. Refuse rather than silently downgrade.
179
- throw new AuthError("auth-oid4vci/kid-resolver-not-supported",
180
- "credential issuance: proof JWT used `kid` without inline `jwk` — supply { jwk } in the header for inline binding (kid-resolver path is operator-side)");
181
- }
182
- if (!holderKeyJwk) {
183
- throw new AuthError("auth-oid4vci/no-jwk-in-header",
184
- "credential issuance: proof JWT must carry `jwk` for inline holder-key binding");
185
- }
186
- // CVE-2026-22817 — cross-check alg/kty before importing the holder
187
- // JWK. Without this an attacker-controlled `alg: "HS256"` against an
188
- // RSA holder JWK would have node:crypto.verify treat the RSA public
189
- // key as an HMAC secret. Routed through the shared helper so every
190
- // JWT verifier in the framework enforces the same check.
191
- jwtExternal._assertAlgKtyMatch(header.alg, holderKeyJwk);
192
185
  var keyObj;
193
- try { keyObj = nodeCrypto.createPublicKey({ key: holderKeyJwk, format: "jwk" }); }
194
- catch (e) {
195
- throw new AuthError("auth-oid4vci/bad-jwk",
196
- "credential issuance: proof JWT jwk is not parseable: " + ((e && e.message) || String(e)));
186
+ if (!holderKeyJwk && header.kid) {
187
+ if (typeof resolveKid !== "function") {
188
+ throw new AuthError("auth-oid4vci/kid-resolver-not-supported",
189
+ "credential issuance: proof JWT used `kid` without inline `jwk` supply { jwk } in the header for inline binding, or configure issuer.create({ resolveKid }) to resolve kid-referenced holder keys");
190
+ }
191
+ var resolved;
192
+ try {
193
+ resolved = await resolveKid(header.kid, header);
194
+ } catch (e) {
195
+ // Wrap a resolver exception in a stable AuthError code so the
196
+ // /credential handler returns a typed refusal instead of an
197
+ // unhandled rejection. resolveKid is operator code, so its own
198
+ // message is allowed through for operator-side debugging.
199
+ throw new AuthError("auth-oid4vci/kid-resolver-failed",
200
+ "credential issuance: resolveKid threw while resolving the proof JWT kid: " + ((e && e.message) || String(e)));
201
+ }
202
+ if (!resolved) {
203
+ throw new AuthError("auth-oid4vci/kid-unresolved",
204
+ "credential issuance: resolveKid returned no key for the proof JWT kid — refused");
205
+ }
206
+ // Normalize to (verify KeyObject) + (cnf JWK). A KeyObject verifies
207
+ // the signature directly; the cnf binding sdJwtIssuer.issue expects
208
+ // a JWK, so a resolved KeyObject is exported to one. A resolved JWK
209
+ // is used for both.
210
+ if (resolved instanceof nodeCrypto.KeyObject) {
211
+ try { holderKeyJwk = resolved.export({ format: "jwk" }); }
212
+ catch (e) {
213
+ throw new AuthError("auth-oid4vci/bad-resolved-key",
214
+ "credential issuance: resolveKid returned a KeyObject that does not export to JWK: " + ((e && e.message) || String(e)));
215
+ }
216
+ } else if (typeof resolved === "object" && typeof resolved.kty === "string") {
217
+ holderKeyJwk = resolved;
218
+ } else {
219
+ throw new AuthError("auth-oid4vci/bad-resolved-key",
220
+ "credential issuance: resolveKid must return a JWK object (with kty) or a node:crypto KeyObject");
221
+ }
222
+ // CVE-2026-22817 — same alg/kty cross-check the inline path applies.
223
+ // A resolver that returns an RSA key for a proof declaring an HMAC
224
+ // alg would otherwise be verified as an HMAC secret.
225
+ jwtExternal._assertAlgKtyMatch(header.alg, holderKeyJwk);
226
+ try { keyObj = nodeCrypto.createPublicKey({ key: holderKeyJwk, format: "jwk" }); }
227
+ catch (e) {
228
+ throw new AuthError("auth-oid4vci/bad-resolved-key",
229
+ "credential issuance: resolveKid-returned key is not importable as a public key: " + ((e && e.message) || String(e)));
230
+ }
231
+ } else {
232
+ if (!holderKeyJwk) {
233
+ throw new AuthError("auth-oid4vci/no-jwk-in-header",
234
+ "credential issuance: proof JWT must carry `jwk` for inline holder-key binding");
235
+ }
236
+ // CVE-2026-22817 — cross-check alg/kty before importing the holder
237
+ // JWK. Without this an attacker-controlled `alg: "HS256"` against an
238
+ // RSA holder JWK would have node:crypto.verify treat the RSA public
239
+ // key as an HMAC secret. Routed through the shared helper so every
240
+ // JWT verifier in the framework enforces the same check.
241
+ jwtExternal._assertAlgKtyMatch(header.alg, holderKeyJwk);
242
+ try { keyObj = nodeCrypto.createPublicKey({ key: holderKeyJwk, format: "jwk" }); }
243
+ catch (e) {
244
+ throw new AuthError("auth-oid4vci/bad-jwk",
245
+ "credential issuance: proof JWT jwk is not parseable: " + ((e && e.message) || String(e)));
246
+ }
197
247
  }
198
248
 
199
249
  var signingInput = parts[0] + "." + parts[1];
@@ -241,6 +291,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
241
291
  * sdJwtIssuer: <b.auth.sdJwtVc.issuer instance>, // mints the SD-JWT VC
242
292
  * supportedCredentials: { [id]: { format, vct, claims, ... } },
243
293
  * proofAlgorithms: string[], // default ["ES256", "ES384", "EdDSA"]
294
+ * resolveKid?: function(kid, header), // resolve a kid-only proof's holder key (JWK | KeyObject); without it, kid-only proofs are refused
244
295
  * preAuthCodeTtlMs?: number, // default 5m
245
296
  * accessTokenTtlMs?: number, // default 15m
246
297
  * cNonceTtlMs?: number, // default 5m
@@ -301,6 +352,12 @@ function create(opts) {
301
352
  var proofAlgs = Array.isArray(opts.proofAlgorithms) && opts.proofAlgorithms.length > 0
302
353
  ? opts.proofAlgorithms : ["ES256", "ES384", "EdDSA"];
303
354
 
355
+ // Optional kid-resolver for kid-only proofs (EUDI-Wallet attested-key
356
+ // flow). Config-time throw if supplied but not a function. Absent →
357
+ // kid-only proofs keep the clear refusal (back-compat).
358
+ var resolveKid = validateOpts.optionalFunction(opts.resolveKid,
359
+ "issuer.create: resolveKid", AuthError, "auth-oid4vci/bad-resolve-kid");
360
+
304
361
  var preAuthTtl = opts.preAuthCodeTtlMs || DEFAULT_PRE_AUTH_TTL_MS;
305
362
  var accessTokenTtl = opts.accessTokenTtlMs || DEFAULT_ACCESS_TOKEN_TTL;
306
363
  var cNonceTtl = opts.cNonceTtlMs || DEFAULT_C_NONCE_TTL_MS;
@@ -466,7 +523,7 @@ function create(opts) {
466
523
  "exchangePreAuthorizedCode: tx_code does not match");
467
524
  }
468
525
  }
469
- await codeStore.delete(eopts.preAuthCode);
526
+ await codeStore.del(eopts.preAuthCode);
470
527
  var accessToken = generateToken(32); // 256-bit access token
471
528
  var cNonce = generateToken(16); // 128-bit c_nonce
472
529
  var record = {
@@ -555,7 +612,7 @@ function create(opts) {
555
612
  }
556
613
 
557
614
  var expectedCNonce = await cNonceStore.get(iopts.accessToken);
558
- var verified = _verifyProofJwt(iopts.proof, opts.credentialIssuerUrl, expectedCNonce, null, proofAlgs, proofMaxAgeMs);
615
+ var verified = await _verifyProofJwt(iopts.proof, opts.credentialIssuerUrl, expectedCNonce, null, proofAlgs, proofMaxAgeMs, resolveKid);
559
616
 
560
617
  if (!iopts.claims || typeof iopts.claims !== "object") {
561
618
  throw new AuthError("auth-oid4vci/no-claims",
@@ -584,8 +641,8 @@ function create(opts) {
584
641
  // explicitly tightens cleanup.
585
642
  if (accessTokenSingleUse) {
586
643
  try {
587
- await atStore.delete(iopts.accessToken);
588
- await cNonceStore.delete(iopts.accessToken);
644
+ await atStore.del(iopts.accessToken);
645
+ await cNonceStore.del(iopts.accessToken);
589
646
  } catch (_e) { /* drop-silent — cleanup is best-effort */ }
590
647
  }
591
648