@blamejs/blamejs-shop 0.0.83 → 0.0.98

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 (33) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/lib/admin.js +11 -7
  3. package/lib/customer-import.js +1 -1
  4. package/lib/email-campaigns.js +1 -1
  5. package/lib/pwa-manifest.js +1 -0
  6. package/lib/vendor/MANIFEST.json +2 -2
  7. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +8 -0
  8. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  9. package/lib/vendor/blamejs/README.md +1 -1
  10. package/lib/vendor/blamejs/SECURITY.md +1 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +167 -2
  12. package/lib/vendor/blamejs/fuzz/safe-archive.fuzz.js +37 -0
  13. package/lib/vendor/blamejs/index.js +15 -1
  14. package/lib/vendor/blamejs/lib/archive-adapters.js +629 -0
  15. package/lib/vendor/blamejs/lib/archive-gz.js +229 -0
  16. package/lib/vendor/blamejs/lib/archive-read.js +781 -0
  17. package/lib/vendor/blamejs/lib/archive-tar-read.js +418 -0
  18. package/lib/vendor/blamejs/lib/archive-tar.js +571 -0
  19. package/lib/vendor/blamejs/lib/archive.js +24 -2
  20. package/lib/vendor/blamejs/lib/audit.js +22 -7
  21. package/lib/vendor/blamejs/lib/backup/index.js +469 -0
  22. package/lib/vendor/blamejs/lib/guard-archive.js +180 -0
  23. package/lib/vendor/blamejs/lib/guard-filename.js +205 -0
  24. package/lib/vendor/blamejs/lib/safe-archive.js +309 -0
  25. package/lib/vendor/blamejs/package.json +1 -1
  26. package/lib/vendor/blamejs/release-notes/v0.12.7.json +86 -0
  27. package/lib/vendor/blamejs/release-notes/v0.12.8.json +81 -0
  28. package/lib/vendor/blamejs/release-notes/v0.12.9.json +61 -0
  29. package/lib/vendor/blamejs/test/layer-0-primitives/archive-gz.test.js +159 -0
  30. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +247 -0
  31. package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar.test.js +228 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +180 -0
  33. package/package.json +2 -2
@@ -0,0 +1,571 @@
1
+ "use strict";
2
+ /**
3
+ * archive-tar — POSIX pax tar write + read. Sibling of lib/archive.js
4
+ * the way lib/archive-read.js is — the `b.archive.tar` and
5
+ * `b.archive.read.tar` primitives both live in the `b.archive`
6
+ * namespace declared in lib/archive.js; this file just carries the
7
+ * tar-format wire-format implementation.
8
+ *
9
+ * POSIX pax tar format — second archive format in the family, sits
10
+ * alongside `b.archive.zip` (write-only ZIP) + `b.archive.read.zip`
11
+ * (read-side ZIP, v0.12.7).
12
+ *
13
+ * Two output paths mirror the ZIP write side:
14
+ * - `toBuffer()` — whole archive in memory.
15
+ * - `toStream(writable)` — block-by-block streaming.
16
+ * - `toAdapter(adapter)` — write through the v0.12.7 adapter contract.
17
+ *
18
+ * Two input paths for read:
19
+ * - `b.archive.read.tar(adapter)` — random-access OR sequential.
20
+ * Tar has no central directory, so sequential header-by-header
21
+ * walk IS the canonical adversarial-safe path. Trusted-stream
22
+ * adapters are first-class here (unlike ZIP read where they
23
+ * carry a documented trust boundary).
24
+ * - `b.archive.adapters.trustedStream(readable)` — preferred for
25
+ * multi-gibibyte tar streams that don't fit in memory.
26
+ *
27
+ * Format guarantees:
28
+ * - ustar magic + version "00" at byte offset 257 of every header.
29
+ * - Names ≤ 100 chars + sizes ≤ 8 GiB fit in the fixed ustar header.
30
+ * - Longer names + larger sizes get a pax extended header (POSIX.1-
31
+ * 2001 §4.18) preceding the entry: "len key=value\n" records.
32
+ * - Two zero blocks (1024 bytes) terminate the archive.
33
+ * - Path-traversal refused at `addFile` (mirrors ZIP write).
34
+ * - Deterministic insertion order; deterministic mtime opt-in.
35
+ *
36
+ * Entry-type policy (typeflag handling on read):
37
+ * - 0 / '\0' (regular file): extract.
38
+ * - 5 (directory): extract (mkdir -p).
39
+ * - 1 (hardlink): refused by default; `allowDangerous: { hardlinks:
40
+ * true }` opt-in routes link target through
41
+ * `b.guardFilename.verifyExtractionPath`.
42
+ * - 2 (symlink): same as hardlink — refused by default; opt-in
43
+ * with realpath-on-target check.
44
+ * - 3 / 4 / 6 / 7 (char-device / block-device / FIFO / contiguous):
45
+ * refused unconditionally — no use case in application archives.
46
+ * - x (pax extended header): consumed by reader; merged into next
47
+ * entry's metadata.
48
+ * - g (pax global header): consumed; applies to all following.
49
+ *
50
+ * Defends:
51
+ * - CVE-2026-23745 / CVE-2026-24842 (node-tar symlink+hardlink
52
+ * path-resolution divergence between safety check + creation).
53
+ * - CVE-2025-4517 PATH_MAX TOCTOU (carries v0.12.7's
54
+ * `verifyExtractionPath` dual-check).
55
+ * - CVE-2025-11001 / 11002 (7-Zip symlink TOCTOU on extract).
56
+ * - CVE-2024-12905 / CVE-2025-48387 (tar-fs path traversal).
57
+ * - CVE-2025-4138 / 4330 (Python tarfile data filter bypass).
58
+ *
59
+ * Out of scope (v1):
60
+ * - Sparse-file emission (read reconstructs them; write doesn't
61
+ * produce sparse).
62
+ * - Compression (tar.gz lands v0.12.9 via `b.archive.gz`
63
+ * composition).
64
+ * - BSD-tar extensions beyond pax.
65
+ *
66
+ * @card
67
+ * POSIX pax tar archive — write + read with the same defense surface as the ZIP family.
68
+ */
69
+
70
+ var nodeStream = require("node:stream");
71
+ var streamPromises = require("node:stream/promises");
72
+ var C = require("./constants");
73
+ var lazyRequire = require("./lazy-require");
74
+ var { defineClass } = require("./framework-error");
75
+
76
+ var TarError = defineClass("TarError", { alwaysPermanent: true });
77
+
78
+ void streamPromises; void nodeStream;
79
+
80
+ // Lazy because archive-gz lazy-imports archive-tar-read (sibling read
81
+ // module) — a top-of-file `require("./archive-gz")` would create a
82
+ // load-order cycle that depends on file-walk order.
83
+ var archiveGz = lazyRequire(function () { return require("./archive-gz"); });
84
+
85
+ // ---- Wire-format constants -----------------------------------------------
86
+
87
+ var BLOCK_SIZE = C.BYTES.bytes(512); // tar block size (POSIX)
88
+ var USTAR_MAGIC = "ustar\u0000"; // 6 bytes including the trailing NUL
89
+ var USTAR_VERSION = "00"; // 2 bytes
90
+ var NAME_MAX = C.BYTES.bytes(100); // ustar name field cap
91
+ var PREFIX_MAX = C.BYTES.bytes(155); // ustar prefix field cap
92
+ var LINKNAME_MAX = C.BYTES.bytes(100); // ustar linkname field cap
93
+ var USTAR_SIZE_MAX = 0o77777777777; // allow:raw-byte-literal — 11 octal digits = 8 GiB - 1 per ustar size-field width
94
+
95
+ // Header field byte offsets (POSIX.1-1988 ustar; same in POSIX.1-2001 pax)
96
+ var H_NAME = C.BYTES.bytes(0);
97
+ var H_MODE = C.BYTES.bytes(100);
98
+ var H_UID = C.BYTES.bytes(108);
99
+ var H_GID = C.BYTES.bytes(116);
100
+ var H_SIZE = C.BYTES.bytes(124);
101
+ var H_MTIME = C.BYTES.bytes(136);
102
+ var H_CHKSUM = C.BYTES.bytes(148);
103
+ var H_TYPEFLAG = C.BYTES.bytes(156);
104
+ var H_LINKNAME = C.BYTES.bytes(157);
105
+ var H_MAGIC = C.BYTES.bytes(257);
106
+ var H_VERSION = C.BYTES.bytes(263);
107
+ var H_UNAME = C.BYTES.bytes(265);
108
+ var H_GNAME = C.BYTES.bytes(297);
109
+ var H_DEVMAJOR = C.BYTES.bytes(329);
110
+ var H_DEVMINOR = C.BYTES.bytes(337);
111
+ var H_PREFIX = C.BYTES.bytes(345);
112
+
113
+ // Field widths
114
+ var W_MODE = C.BYTES.bytes(8);
115
+ var W_UID = C.BYTES.bytes(8);
116
+ var W_GID = C.BYTES.bytes(8);
117
+ var W_SIZE = C.BYTES.bytes(12);
118
+ var W_MTIME = C.BYTES.bytes(12);
119
+ var W_CHKSUM = C.BYTES.bytes(8);
120
+ var W_UNAME = C.BYTES.bytes(32);
121
+ var W_GNAME = C.BYTES.bytes(32);
122
+
123
+ // Typeflags — write side emits only TF_REGULAR / TF_DIRECTORY /
124
+ // TF_PAX_EXTENDED. The full POSIX typeflag set lives in
125
+ // lib/archive-tar-read.js (the reader has to classify every typeflag
126
+ // the wild produces). H_DEVMAJOR / H_DEVMINOR offsets stay for the
127
+ // shared _parseHeader call site even though the write side never
128
+ // emits typeflag 3/4 (char/block device) entries.
129
+ var TF_REGULAR = "0";
130
+ var TF_DIRECTORY = "5";
131
+ var TF_PAX_EXTENDED = "x";
132
+
133
+ void H_DEVMAJOR; void H_DEVMINOR;
134
+
135
+ // ---- Helpers -------------------------------------------------------------
136
+
137
+ function _padBlock(buf) {
138
+ // Pad buf to the next 512-byte boundary with NUL bytes.
139
+ var rem = buf.length % BLOCK_SIZE;
140
+ if (rem === 0) return buf;
141
+ var pad = Buffer.alloc(BLOCK_SIZE - rem);
142
+ return Buffer.concat([buf, pad]);
143
+ }
144
+
145
+ function _writeOctal(buf, value, offset, width) {
146
+ // ustar octal fields: 6-11 octal digits + space or NUL terminator.
147
+ // For width=8, that's 7 octal digits + terminator. For width=12, 11
148
+ // digits + terminator.
149
+ var digits = width - 1;
150
+ var oct = value.toString(8); // allow:raw-byte-literal — radix=8 for octal stringify per ustar field format
151
+ if (oct.length > digits) {
152
+ throw new TarError("archive-tar/octal-overflow",
153
+ "value " + value + " (octal " + oct + ") exceeds field width " + digits);
154
+ }
155
+ // Left-pad with '0' to fill the digits.
156
+ while (oct.length < digits) oct = "0" + oct;
157
+ buf.write(oct, offset, digits, "ascii");
158
+ buf.writeUInt8(0x20, offset + digits); // allow:raw-byte-literal — ASCII space (' ') terminator per ustar
159
+ }
160
+
161
+ function _writeString(buf, value, offset, width) {
162
+ // ASCII-encoded; truncated if longer than width. NUL terminates if
163
+ // shorter than width.
164
+ var ascii = String(value);
165
+ var bytes = Buffer.from(ascii, "utf8");
166
+ if (bytes.length > width) {
167
+ throw new TarError("archive-tar/field-overflow",
168
+ "string " + JSON.stringify(value) + " (" + bytes.length + " bytes) exceeds field width " + width);
169
+ }
170
+ bytes.copy(buf, offset);
171
+ // Remaining bytes already NUL from Buffer.alloc.
172
+ }
173
+
174
+ function _readOctal(buf, offset, width) {
175
+ // Read an octal-encoded field. Terminator may be space or NUL.
176
+ var s = "";
177
+ for (var i = 0; i < width; i += 1) {
178
+ var c = buf[offset + i];
179
+ if (c === 0x20 || c === 0) break; // allow:raw-byte-literal — ASCII space (0x20) + NUL (0x00) field terminators
180
+ if (c < 0x30 || c > 0x37) { // allow:raw-byte-literal — ASCII '0' (0x30) .. '7' (0x37) octal digits
181
+ throw new TarError("archive-tar/bad-octal",
182
+ "non-octal byte 0x" + c.toString(16) + " at offset " + (offset + i)); // allow:raw-byte-literal — radix=16 for diagnostic hex format
183
+ }
184
+ s += String.fromCharCode(c);
185
+ }
186
+ if (s.length === 0) return 0;
187
+ return parseInt(s, 8); // allow:raw-byte-literal — radix=8 for octal parse per ustar field format
188
+ }
189
+
190
+ function _readString(buf, offset, width) {
191
+ // Read NUL-terminated ASCII / UTF-8 from the field. Truncates at
192
+ // the first NUL byte.
193
+ var end = offset;
194
+ var limit = offset + width;
195
+ while (end < limit && buf[end] !== 0) end += 1;
196
+ return buf.slice(offset, end).toString("utf8");
197
+ }
198
+
199
+ function _computeChecksum(buf) {
200
+ // ustar checksum: sum of every byte in the 512-byte header, with the
201
+ // chksum field itself treated as 8 spaces (0x20). Stored as 6 octal
202
+ // digits + NUL + space (per the spec — historically GNU tar writes
203
+ // it that way; modern parsers accept several variants).
204
+ var sum = 0;
205
+ for (var i = 0; i < BLOCK_SIZE; i += 1) {
206
+ if (i >= H_CHKSUM && i < H_CHKSUM + W_CHKSUM) {
207
+ sum += 0x20; // allow:raw-byte-literal — chksum field treated as 8 spaces per POSIX.1-1988
208
+ } else {
209
+ sum += buf[i];
210
+ }
211
+ }
212
+ return sum;
213
+ }
214
+
215
+ function _writeChecksum(buf) {
216
+ // Write 6 octal digits + NUL + space into the chksum field.
217
+ var sum = _computeChecksum(buf);
218
+ var oct = sum.toString(8); // allow:raw-byte-literal — radix=8 for octal stringify per ustar chksum field format
219
+ while (oct.length < 6) oct = "0" + oct; // allow:raw-byte-literal — chksum field is 6 octal digits per POSIX ustar
220
+ if (oct.length > 6) { // allow:raw-byte-literal — chksum field is 6 octal digits per POSIX ustar
221
+ // Header is corrupt / oversized somewhere; surface a typed error.
222
+ throw new TarError("archive-tar/chksum-overflow",
223
+ "chksum " + sum + " (" + oct + ") exceeds 6 octal digits");
224
+ }
225
+ buf.write(oct, H_CHKSUM, 6, "ascii"); // allow:raw-byte-literal — chksum field is 6 octal digits per POSIX ustar
226
+ buf.writeUInt8(0, H_CHKSUM + 6); // allow:raw-byte-literal — chksum field: 6 digits + NUL + space per POSIX ustar
227
+ buf.writeUInt8(0x20, H_CHKSUM + 7); // allow:raw-byte-literal — chksum field: 6 digits + NUL + space per POSIX ustar
228
+ }
229
+
230
+ function _verifyChecksum(buf) {
231
+ // Parse the stored chksum + compare against recomputed.
232
+ var stored = _readOctal(buf, H_CHKSUM, W_CHKSUM);
233
+ var computed = _computeChecksum(buf);
234
+ return stored === computed;
235
+ }
236
+
237
+ function _normalizeName(name) {
238
+ if (typeof name !== "string" || name.length === 0) {
239
+ throw new TarError("archive-tar/bad-name", "addFile: name must be non-empty string");
240
+ }
241
+ if (name.indexOf("\u0000") !== -1) {
242
+ throw new TarError("archive-tar/bad-name", "addFile: name contains null byte");
243
+ }
244
+ var normalized = name.replace(/\\/g, "/").replace(/^\/+/, "");
245
+ var segs = normalized.split("/");
246
+ for (var i = 0; i < segs.length; i += 1) {
247
+ if (segs[i] === "..") {
248
+ throw new TarError("archive-tar/bad-name", "addFile: name contains '..' segment");
249
+ }
250
+ }
251
+ return normalized;
252
+ }
253
+
254
+ // ---- Pax extended header -----------------------------------------------
255
+
256
+ function _buildPaxRecord(key, value) {
257
+ // POSIX.1-2001 §4.18 — "len key=value\n" where len is the total
258
+ // length of the record (including len itself + space + key + '=' +
259
+ // value + '\n'). Compute len iteratively because len encodes itself.
260
+ var keyVal = key + "=" + value + "\n";
261
+ // Initial guess: len contributes ≤ 3 digits; iterate until stable.
262
+ var lenStr = String(keyVal.length + 1 + 1); // +1 for space, +1 for len digit
263
+ var len = parseInt(lenStr, 10) + keyVal.length;
264
+ while (true) {
265
+ var encoded = String(len) + " " + keyVal;
266
+ if (encoded.length === len) return encoded;
267
+ len = encoded.length;
268
+ }
269
+ }
270
+
271
+ function _buildPaxExtendedHeader(records, prefixName) {
272
+ // records: array of [key, value] pairs.
273
+ // Emit the records as the body, then build a typeflag-'x' ustar
274
+ // header pointing at the body.
275
+ var body = "";
276
+ for (var i = 0; i < records.length; i += 1) {
277
+ body += _buildPaxRecord(records[i][0], records[i][1]);
278
+ }
279
+ var bodyBuf = Buffer.from(body, "utf8");
280
+ var hdr = _buildUstarHeader({
281
+ name: prefixName || "PaxHeader/extended",
282
+ typeflag: TF_PAX_EXTENDED,
283
+ size: bodyBuf.length,
284
+ mtime: 0,
285
+ mode: 0o644,
286
+ });
287
+ return Buffer.concat([hdr, _padBlock(bodyBuf)]);
288
+ }
289
+
290
+ function _parsePaxRecords(buf) {
291
+ // Parse "len key=value\n" records from a pax extended-header body.
292
+ var out = Object.create(null);
293
+ var pos = 0;
294
+ var s = buf.toString("utf8");
295
+ while (pos < s.length) {
296
+ var spaceIdx = s.indexOf(" ", pos);
297
+ if (spaceIdx < 0) {
298
+ throw new TarError("archive-tar/bad-pax-record",
299
+ "pax record at byte " + pos + " missing length-space delimiter");
300
+ }
301
+ var lenStr = s.slice(pos, spaceIdx);
302
+ var len = parseInt(lenStr, 10);
303
+ if (!Number.isFinite(len) || len <= 0) {
304
+ throw new TarError("archive-tar/bad-pax-record",
305
+ "pax record length " + JSON.stringify(lenStr) + " is not a positive integer");
306
+ }
307
+ var record = s.slice(pos, pos + len);
308
+ if (record[record.length - 1] !== "\n") {
309
+ throw new TarError("archive-tar/bad-pax-record",
310
+ "pax record at byte " + pos + " not newline-terminated");
311
+ }
312
+ var eqIdx = record.indexOf("=", spaceIdx - pos + 1);
313
+ if (eqIdx < 0) {
314
+ throw new TarError("archive-tar/bad-pax-record",
315
+ "pax record at byte " + pos + " missing key=value delimiter");
316
+ }
317
+ var key = record.slice(spaceIdx - pos + 1, eqIdx);
318
+ var value = record.slice(eqIdx + 1, record.length - 1);
319
+ out[key] = value;
320
+ pos += len;
321
+ }
322
+ return out;
323
+ }
324
+
325
+ // ---- ustar header build --------------------------------------------------
326
+
327
+ function _buildUstarHeader(entry) {
328
+ var buf = Buffer.alloc(BLOCK_SIZE);
329
+ // name + prefix split for names > 100 chars
330
+ var name = entry.name;
331
+ var prefix = "";
332
+ if (name.length > NAME_MAX) {
333
+ // Try splitting on a '/' so prefix + '/' + name <= 100 + 155
334
+ var splitIdx = name.lastIndexOf("/", NAME_MAX);
335
+ if (splitIdx > 0 && (name.length - splitIdx - 1) <= NAME_MAX &&
336
+ splitIdx <= PREFIX_MAX) {
337
+ prefix = name.slice(0, splitIdx);
338
+ name = name.slice(splitIdx + 1);
339
+ } else {
340
+ // Won't fit in ustar — pax extended header handles it; the
341
+ // ustar header carries a "PaxHeader/data" sentinel name.
342
+ name = "PaxHeader/data";
343
+ }
344
+ }
345
+ _writeString(buf, name, H_NAME, NAME_MAX);
346
+ _writeOctal(buf, entry.mode || 0o644, H_MODE, W_MODE);
347
+ _writeOctal(buf, entry.uid || 0, H_UID, W_UID);
348
+ _writeOctal(buf, entry.gid || 0, H_GID, W_GID);
349
+ _writeOctal(buf, entry.size || 0, H_SIZE, W_SIZE);
350
+ _writeOctal(buf, entry.mtime || 0, H_MTIME, W_MTIME);
351
+ // chksum field — written as 8 spaces during computation, then
352
+ // replaced with the computed value below.
353
+ buf.fill(0x20, H_CHKSUM, H_CHKSUM + W_CHKSUM); // allow:raw-byte-literal — pre-fill chksum field with spaces per POSIX
354
+ buf.write(entry.typeflag || TF_REGULAR, H_TYPEFLAG, 1, "ascii");
355
+ if (entry.linkname) _writeString(buf, entry.linkname, H_LINKNAME, LINKNAME_MAX);
356
+ buf.write(USTAR_MAGIC, H_MAGIC, 6, "ascii"); // allow:raw-byte-literal — ustar magic is 6 bytes per POSIX
357
+ buf.write(USTAR_VERSION, H_VERSION, 2, "ascii"); // allow:raw-byte-literal — ustar version is 2 bytes per POSIX
358
+ if (entry.uname) _writeString(buf, entry.uname, H_UNAME, W_UNAME);
359
+ if (entry.gname) _writeString(buf, entry.gname, H_GNAME, W_GNAME);
360
+ if (prefix) _writeString(buf, prefix, H_PREFIX, PREFIX_MAX);
361
+ _writeChecksum(buf);
362
+ return buf;
363
+ }
364
+
365
+ // ---- Public tar builder ---------------------------------------------------
366
+
367
+ /**
368
+ * @primitive b.archive.tar
369
+ * @signature b.archive.tar()
370
+ * @since 0.12.8
371
+ * @status stable
372
+ * @compliance hipaa, pci-dss, gdpr, soc2
373
+ * @related b.archive.zip, b.archive.read.tar
374
+ *
375
+ * POSIX pax tar archive builder. Mirrors `b.archive.zip()`'s
376
+ * `addFile / addDirectory / toBuffer / toStream / toAdapter / digest`
377
+ * contract.
378
+ *
379
+ * @example
380
+ * var t = b.archive.tar();
381
+ * t.addFile("readme.txt", "Hello\n");
382
+ * t.addFile("data/numbers.csv", "n,sq\n1,1\n2,4\n");
383
+ * var bytes = t.toBuffer();
384
+ * t.entryCount; // → 2
385
+ */
386
+ function tarBuilder() {
387
+ var entries = [];
388
+
389
+ function addFile(name, content, opts) {
390
+ var normalized = _normalizeName(name);
391
+ opts = opts || {};
392
+ var bodyBuf;
393
+ if (Buffer.isBuffer(content)) bodyBuf = content;
394
+ else if (typeof content === "string") bodyBuf = Buffer.from(content, "utf8");
395
+ else throw new TarError("archive-tar/bad-content",
396
+ "addFile: content must be Buffer or string, got " + typeof content);
397
+ entries.push({
398
+ name: normalized,
399
+ kind: "file",
400
+ typeflag: TF_REGULAR,
401
+ body: bodyBuf,
402
+ size: bodyBuf.length,
403
+ mode: opts.mode || 0o644,
404
+ mtime: opts.mtime !== undefined ? opts.mtime
405
+ : Math.floor((opts.fixedMtime || Date.now()) / C.TIME.seconds(1)),
406
+ uid: opts.uid || 0,
407
+ gid: opts.gid || 0,
408
+ uname: opts.uname || "",
409
+ gname: opts.gname || "",
410
+ });
411
+ }
412
+
413
+ function addDirectory(name, opts) {
414
+ var normalized = _normalizeName(name);
415
+ if (normalized[normalized.length - 1] !== "/") normalized = normalized + "/";
416
+ opts = opts || {};
417
+ entries.push({
418
+ name: normalized,
419
+ kind: "directory",
420
+ typeflag: TF_DIRECTORY,
421
+ body: Buffer.alloc(0),
422
+ size: 0,
423
+ mode: opts.mode || 0o755,
424
+ mtime: opts.mtime !== undefined ? opts.mtime : Math.floor(Date.now() / C.TIME.seconds(1)),
425
+ uid: opts.uid || 0,
426
+ gid: opts.gid || 0,
427
+ uname: opts.uname || "",
428
+ gname: opts.gname || "",
429
+ });
430
+ }
431
+
432
+ function _entryBytes(entry) {
433
+ // Determine if we need a pax extended header.
434
+ var paxRecords = [];
435
+ if (entry.name.length > NAME_MAX) {
436
+ // Check if ustar prefix/name split would work.
437
+ var splitIdx = entry.name.lastIndexOf("/", NAME_MAX);
438
+ var fits = splitIdx > 0 &&
439
+ (entry.name.length - splitIdx - 1) <= NAME_MAX &&
440
+ splitIdx <= PREFIX_MAX;
441
+ if (!fits) paxRecords.push(["path", entry.name]);
442
+ }
443
+ if (entry.size > USTAR_SIZE_MAX) {
444
+ paxRecords.push(["size", String(entry.size)]);
445
+ }
446
+ var pieces = [];
447
+ if (paxRecords.length > 0) {
448
+ pieces.push(_buildPaxExtendedHeader(paxRecords, "PaxHeader/" + entry.name.slice(0, 80))); // allow:raw-byte-literal — pax header name fits in ustar 100-char field with 20-char prefix budget
449
+ }
450
+ var hdr = _buildUstarHeader({
451
+ name: entry.name,
452
+ mode: entry.mode,
453
+ uid: entry.uid,
454
+ gid: entry.gid,
455
+ size: Math.min(entry.size, USTAR_SIZE_MAX),
456
+ mtime: entry.mtime,
457
+ typeflag: entry.typeflag,
458
+ linkname: entry.linkname,
459
+ uname: entry.uname,
460
+ gname: entry.gname,
461
+ });
462
+ pieces.push(hdr);
463
+ if (entry.body.length > 0) {
464
+ pieces.push(_padBlock(entry.body));
465
+ }
466
+ return Buffer.concat(pieces);
467
+ }
468
+
469
+ function toBuffer() {
470
+ var pieces = [];
471
+ for (var i = 0; i < entries.length; i += 1) {
472
+ pieces.push(_entryBytes(entries[i]));
473
+ }
474
+ // Two zero blocks terminate the archive (POSIX requirement).
475
+ pieces.push(Buffer.alloc(BLOCK_SIZE * 2)); // allow:raw-byte-literal — POSIX requires 2 trailing zero blocks
476
+ return Buffer.concat(pieces);
477
+ }
478
+
479
+ async function toAdapter(adapter) {
480
+ if (!adapter || typeof adapter.write !== "function") {
481
+ throw new TarError("archive-tar/bad-adapter",
482
+ "toAdapter: adapter must expose a write(bytes) method");
483
+ }
484
+ for (var i = 0; i < entries.length; i += 1) {
485
+ await adapter.write(_entryBytes(entries[i]));
486
+ }
487
+ await adapter.write(Buffer.alloc(BLOCK_SIZE * 2)); // allow:raw-byte-literal — 2 trailing zero blocks
488
+ if (typeof adapter.end === "function") await adapter.end();
489
+ }
490
+
491
+ function toStream(writable) {
492
+ return new Promise(function (resolve, reject) {
493
+ try {
494
+ writable.write(toBuffer());
495
+ writable.end();
496
+ writable.once("finish", resolve);
497
+ writable.once("error", reject);
498
+ } catch (e) { reject(e); }
499
+ });
500
+ }
501
+
502
+ function digest() {
503
+ var nodeCrypto = require("node:crypto");
504
+ return nodeCrypto.createHash("sha3-512").update(toBuffer()).digest("hex");
505
+ }
506
+
507
+ async function toGzip(adapter, gzOpts) {
508
+ // Convenience composition: materialize the tar then wrap through
509
+ // b.archive.gz. archive-gz is lazy-required at module top to break
510
+ // the load-order cycle with archive-tar-read.
511
+ return archiveGz().gz(toBuffer(), gzOpts || {}).toAdapter(adapter);
512
+ }
513
+
514
+ return {
515
+ addFile: addFile,
516
+ addDirectory: addDirectory,
517
+ toBuffer: toBuffer,
518
+ toStream: toStream,
519
+ toAdapter: toAdapter,
520
+ toGzip: toGzip,
521
+ digest: digest,
522
+ get entryCount() { return entries.length; },
523
+ };
524
+ }
525
+
526
+ // ---- Shared read-side helper --------------------------------------------
527
+ //
528
+ // Sibling module lib/archive-tar-read.js imports _parseHeader via
529
+ // these exports. The reader lives in a separate file so the
530
+ // validator can pair the b.archive.read.tar primitive cleanly
531
+ // (sibling shape to lib/archive-read.js for ZIP). _parseHeader
532
+ // lives here so the write side's checksum + header-field encoding
533
+ // has its inverse in the same wire-format module.
534
+
535
+ function _parseHeader(buf) {
536
+ var magic = buf.slice(H_MAGIC, H_MAGIC + C.BYTES.bytes(5)).toString("ascii");
537
+ if (magic !== "ustar") {
538
+ throw new TarError("archive-tar/bad-magic",
539
+ "header magic " + JSON.stringify(magic) + " is not ustar");
540
+ }
541
+ if (!_verifyChecksum(buf)) {
542
+ throw new TarError("archive-tar/bad-chksum",
543
+ "header checksum mismatch");
544
+ }
545
+ var name = _readString(buf, H_NAME, NAME_MAX);
546
+ var prefix = _readString(buf, H_PREFIX, PREFIX_MAX);
547
+ if (prefix.length > 0) name = prefix + "/" + name;
548
+ return {
549
+ name: name,
550
+ mode: _readOctal(buf, H_MODE, W_MODE),
551
+ uid: _readOctal(buf, H_UID, W_UID),
552
+ gid: _readOctal(buf, H_GID, W_GID),
553
+ size: _readOctal(buf, H_SIZE, W_SIZE),
554
+ mtime: _readOctal(buf, H_MTIME, W_MTIME),
555
+ typeflag: String.fromCharCode(buf[H_TYPEFLAG]),
556
+ linkname: _readString(buf, H_LINKNAME, LINKNAME_MAX),
557
+ uname: _readString(buf, H_UNAME, W_UNAME),
558
+ gname: _readString(buf, H_GNAME, W_GNAME),
559
+ };
560
+ }
561
+
562
+ module.exports = {
563
+ tar: tarBuilder,
564
+ TarError: TarError,
565
+ // Exposed for sibling modules + tests
566
+ _buildUstarHeader: _buildUstarHeader,
567
+ _parseHeader: _parseHeader,
568
+ _readOctal: _readOctal,
569
+ _readString: _readString,
570
+ _verifyChecksum: _verifyChecksum,
571
+ };
@@ -538,9 +538,31 @@ function zip() {
538
538
  };
539
539
  }
540
540
 
541
+ // Read primitive — random-access ZIP reader composes the same wire-
542
+ // format constants as the write side. Lives in a sibling file to keep
543
+ // this module under the line-budget the @primitive validator + the
544
+ // codebase-patterns "single-concern file" pattern prefer.
545
+ var archiveRead = require("./archive-read");
546
+ var archiveTar = require("./archive-tar");
547
+ var archiveTarRead = require("./archive-tar-read");
548
+ var archiveGz = require("./archive-gz");
549
+
541
550
  module.exports = {
542
- zip: zip,
543
- ArchiveError: ArchiveError,
551
+ zip: zip,
552
+ tar: archiveTar.tar,
553
+ gz: archiveGz.gz,
554
+ ArchiveError: ArchiveError,
555
+ TarError: archiveTar.TarError,
556
+ ArchiveGzError: archiveGz.ArchiveGzError,
557
+ read: {
558
+ zip: archiveRead.zip,
559
+ tar: archiveTarRead.tar,
560
+ gz: archiveGz.read.gz,
561
+ fromGzip: archiveGz.read.gz,
562
+ ArchiveReadError: archiveRead.ArchiveReadError,
563
+ DEFAULT_BOMB_POLICY: archiveRead.DEFAULT_BOMB_POLICY,
564
+ DEFAULT_ENTRY_TYPE_POLICY: archiveRead.DEFAULT_ENTRY_TYPE_POLICY,
565
+ },
544
566
  // Test-only export — operators don't call this; it's here for unit-testing
545
567
  // the CRC implementation against known vectors.
546
568
  _crc32ForTest: _crc32,
@@ -1118,17 +1118,28 @@ function _ensureHandler() {
1118
1118
  // items were emitted against. Early-exit drops them; the
1119
1119
  // alternative is silent corruption of the next chain.
1120
1120
  var droppedThisBatch = 0;
1121
+ var firstDropAction = null;
1122
+ var firstDropMessage = null;
1121
1123
  for (var i = 0; i < batch.length; i++) {
1122
1124
  if (ctx && ctx.isShutdown && ctx.isShutdown()) return;
1123
1125
  try { await record(batch[i]); }
1124
1126
  catch (e) {
1125
1127
  droppedThisBatch += 1;
1126
- // Per-item failure shouldn't drop the whole batch; log and
1127
- // continue. The handler's onError gets called for batch-
1128
- // wide failures only.
1129
- log.error("flush dropped event: " +
1130
- (e && e.message ? e.message : String(e)) +
1131
- " (action=" + (batch[i] && batch[i].action) + ")");
1128
+ // Per-item failure shouldn't drop the whole batch; the
1129
+ // signal flows through observability.safeEvent below. The
1130
+ // prior per-drop log.error was noise: boot-phase
1131
+ // audit.emit() racing db.init() fires dozens of these
1132
+ // during normal startup, and operator dashboards
1133
+ // alert-routing on the "error" level read it as a real
1134
+ // failure. The aggregate observability metric is the
1135
+ // documented signal channel; capture the first drop's
1136
+ // action + message in its metadata so operators alerting
1137
+ // on `system.audit.chain_write_dropped` get a
1138
+ // representative sample without per-line log spam.
1139
+ if (firstDropAction === null) {
1140
+ firstDropAction = (batch[i] && batch[i].action) || null;
1141
+ firstDropMessage = (e && e.message) ? e.message : String(e);
1142
+ }
1132
1143
  }
1133
1144
  }
1134
1145
  // Surface chain-write integrity failures via observability so
@@ -1137,7 +1148,11 @@ function _ensureHandler() {
1137
1148
  // broken — so observability is the only sink left.
1138
1149
  if (droppedThisBatch > 0) {
1139
1150
  observability.safeEvent("system.audit.chain_write_dropped",
1140
- droppedThisBatch, { batchSize: batch.length });
1151
+ droppedThisBatch, {
1152
+ batchSize: batch.length,
1153
+ firstDropAction: firstDropAction,
1154
+ firstDropMessage: firstDropMessage,
1155
+ });
1141
1156
  }
1142
1157
  },
1143
1158
  });