@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,228 @@
1
+ "use strict";
2
+ /**
3
+ * Layer 0 — b.archive.tar write + b.archive.read.tar +
4
+ * b.guardArchive.tarEntryPolicy + b.backup.migrate.
5
+ *
6
+ * Round-trip coverage: write tar via b.archive.tar(), read back via
7
+ * b.archive.read.tar(buffer adapter), extract via b.safeArchive.extract
8
+ * (format auto-detect lands on tar). Refusal: oversize entry, dangerous
9
+ * typeflag refusal by default. Migration: write a directory bundle in
10
+ * one storage, migrate to tar bundle in another, verify content.
11
+ */
12
+
13
+ var helpers = require("../helpers");
14
+ var check = helpers.check;
15
+ var b = helpers.b;
16
+ var os = require("node:os");
17
+ var path = require("node:path");
18
+ var fs = require("node:fs");
19
+
20
+ async function testTarRoundTrip() {
21
+ var t = b.archive.tar();
22
+ t.addFile("readme.txt", "Hello, tar!\n");
23
+ t.addFile("data/numbers.csv", "n,sq\n1,1\n2,4\n");
24
+ t.addDirectory("docs/");
25
+ t.addFile("docs/nested/deep.txt", "payload\n");
26
+ var bytes = t.toBuffer();
27
+
28
+ check("archive.tar: tar bytes round to 512", bytes.length % 512 === 0);
29
+ check("archive.tar: ustar magic at offset 257",
30
+ bytes.slice(257, 263).toString().indexOf("ustar") === 0);
31
+
32
+ var reader = b.archive.read.tar(b.archive.adapters.buffer(bytes));
33
+ var entries = await reader.inspect();
34
+ check("archive.read.tar.inspect: 4 entries", entries.length === 4);
35
+ check("archive.read.tar.inspect: file type", entries[0].entryType === "file");
36
+ check("archive.read.tar.inspect: directory type", entries[2].entryType === "directory");
37
+ }
38
+
39
+ async function testSafeArchiveTarExtract() {
40
+ var t = b.archive.tar();
41
+ t.addFile("a.txt", "alpha");
42
+ t.addFile("nested/b.txt", "beta");
43
+ var bytes = t.toBuffer();
44
+
45
+ var dest = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-tar-extract-"));
46
+ try {
47
+ var result = await b.safeArchive.extract({
48
+ source: bytes,
49
+ destination: dest,
50
+ guardProfile: "balanced",
51
+ });
52
+ check("safeArchive.extract: format=tar autodetected", result.format === "tar");
53
+ check("safeArchive.extract: 2 entries written", result.entries.length === 2);
54
+ var a = fs.readFileSync(path.join(dest, "a.txt"), "utf8");
55
+ var bb = fs.readFileSync(path.join(dest, "nested", "b.txt"), "utf8");
56
+ check("safeArchive.extract: a.txt contents", a === "alpha");
57
+ check("safeArchive.extract: nested b.txt contents", bb === "beta");
58
+ } finally {
59
+ fs.rmSync(dest, { recursive: true, force: true });
60
+ }
61
+ }
62
+
63
+ function testTarEntryPolicy() {
64
+ var p = b.guardArchive.tarEntryPolicy({ symlinks: true });
65
+ check("tarEntryPolicy: symlinks opted in", p.symlinks === true);
66
+ check("tarEntryPolicy: hardlinks default off", p.hardlinks === false);
67
+ check("tarEntryPolicy: devices default off", p.devices === false);
68
+ check("tarEntryPolicy: fifos default off", p.fifos === false);
69
+ check("tarEntryPolicy: sockets default off", p.sockets === false);
70
+ }
71
+
72
+ async function testTarChecksumDetection() {
73
+ // Build a valid tar, corrupt the chksum field, verify reader refuses.
74
+ var t = b.archive.tar();
75
+ t.addFile("payload.txt", "hello\n");
76
+ var bytes = t.toBuffer();
77
+ // Corrupt the checksum field of the first header (offset 148, 8 bytes).
78
+ bytes[148] = 0x39; // change first chksum digit
79
+ var refused = null;
80
+ try {
81
+ var reader = b.archive.read.tar(b.archive.adapters.buffer(bytes));
82
+ await reader.inspect();
83
+ } catch (e) { refused = e; }
84
+ check("archive.read.tar: corrupted chksum refused",
85
+ refused && /chksum|bad-octal/.test(refused.code || refused.message));
86
+ check("archive.read.tar: chksum refusal is a b.archive.TarError",
87
+ refused instanceof b.archive.TarError);
88
+ }
89
+
90
+ async function testBackupMigrateDirectoryToTar() {
91
+ var fromRoot = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-from-"));
92
+ var toRoot = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-to-"));
93
+ var srcDir = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-src-"));
94
+ var verifyDir = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-verify-"));
95
+ fs.rmSync(verifyDir, { recursive: true }); // backend wants non-existent dest
96
+ try {
97
+ fs.writeFileSync(path.join(srcDir, "manifest.json"), JSON.stringify({ v: 1 }));
98
+ fs.mkdirSync(path.join(srcDir, "files"));
99
+ fs.writeFileSync(path.join(srcDir, "files", "blob.bin"), Buffer.from([1, 2, 3]));
100
+
101
+ var from = b.backup.bundleAdapterStorage({
102
+ adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: fromRoot }),
103
+ format: "directory",
104
+ });
105
+ var to = b.backup.bundleAdapterStorage({
106
+ adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: toRoot }),
107
+ format: "tar",
108
+ });
109
+
110
+ var bundleId = "2026-05-23T15-00-00-000Z-deadbeef";
111
+ await from.writeBundle(bundleId, srcDir);
112
+ check("migrate: source-bundle exists pre-migrate", await from.hasBundle(bundleId));
113
+ check("migrate: dest-bundle absent pre-migrate", !(await to.hasBundle(bundleId)));
114
+
115
+ var report = await b.backup.migrate({ from: from, to: to });
116
+ check("migrate: 1 migrated", report.migrated === 1 && report.skipped === 0);
117
+ check("migrate: dest-bundle present post-migrate", await to.hasBundle(bundleId));
118
+
119
+ // Idempotency: second run skips.
120
+ var second = await b.backup.migrate({ from: from, to: to });
121
+ check("migrate: idempotent (1 skipped)", second.migrated === 0 && second.skipped === 1);
122
+
123
+ // Read back from destination, verify file contents.
124
+ await to.readBundle(bundleId, verifyDir);
125
+ var manifest = JSON.parse(fs.readFileSync(path.join(verifyDir, "manifest.json"), "utf8"));
126
+ check("migrate: manifest round-tripped", manifest.v === 1);
127
+ var blob = fs.readFileSync(path.join(verifyDir, "files", "blob.bin"));
128
+ check("migrate: blob round-tripped",
129
+ blob.length === 3 && blob[0] === 1 && blob[2] === 3);
130
+ } finally {
131
+ try { fs.rmSync(fromRoot, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
132
+ try { fs.rmSync(toRoot, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
133
+ try { fs.rmSync(srcDir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
134
+ try { fs.rmSync(verifyDir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
135
+ }
136
+ }
137
+
138
+ async function testBackupTarFormatDefault() {
139
+ // Write a bundle via the v0.12.8-default format (tar) + verify the
140
+ // tar key landed on disk.
141
+ var root = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-tar-default-"));
142
+ var src = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-tar-src-"));
143
+ try {
144
+ fs.writeFileSync(path.join(src, "manifest.json"), JSON.stringify({ v: 1 }));
145
+ var storage = b.backup.bundleAdapterStorage({
146
+ adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: root }),
147
+ // format defaults to "tar" in v0.12.8
148
+ });
149
+ var bid = "2026-05-23T15-30-00-000Z-cafebabe";
150
+ await storage.writeBundle(bid, src);
151
+ var tarKeyPresent = fs.existsSync(path.join(root, bid, "bundle.tar"));
152
+ check("bundleAdapterStorage default format: tar key on disk", tarKeyPresent);
153
+ check("bundleAdapterStorage default format: hasBundle true",
154
+ await storage.hasBundle(bid));
155
+ } finally {
156
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
157
+ try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
158
+ }
159
+ }
160
+
161
+ async function testTarTruncationRefused() {
162
+ // Codex P1 on v0.12.8 PR #159 — declare 11 bytes in the header
163
+ // but truncate the buffer to 8 payload bytes. The walker must
164
+ // refuse upfront rather than emitting a partial entry.
165
+ var t = b.archive.tar();
166
+ t.addFile("payload.txt", Buffer.from("hello world")); // 11 bytes
167
+ var bytes = t.toBuffer();
168
+ // First header block is at offset 0-511; first data block at
169
+ // 512-1023 (padded to 512 bytes from the declared 11). Truncate to
170
+ // 768 bytes: header survives intact but the data block ends
171
+ // halfway — walker must refuse before slicing partial bytes as
172
+ // a "complete" file.
173
+ var truncated = bytes.slice(0, 768);
174
+ var refused = null;
175
+ try {
176
+ var reader = b.archive.read.tar(b.archive.adapters.buffer(truncated));
177
+ await reader.extract({
178
+ destination: fs.mkdtempSync(path.join(os.tmpdir(), "bjs-tar-trunc-")),
179
+ });
180
+ } catch (e) { refused = e; }
181
+ check("archive.read.tar: truncated entry refused with typed error",
182
+ refused && /truncated-entry/.test(refused.code || refused.message));
183
+ }
184
+
185
+ async function testBackupBundleTooLargeRefused() {
186
+ // Codex P2 on v0.12.8 PR #159 — bundleAdapterStorage with a
187
+ // tight maxBundleBytes cap must refuse oversized payloads upfront
188
+ // rather than OOM during in-memory tar materialization.
189
+ var srcDir = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-bulk-"));
190
+ var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-bk-bulk-dest-"));
191
+ try {
192
+ fs.writeFileSync(path.join(srcDir, "big.bin"), Buffer.alloc(64 * 1024));
193
+ var storage = b.backup.bundleAdapterStorage({
194
+ adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
195
+ format: "tar",
196
+ maxBundleBytes: 16 * 1024,
197
+ });
198
+ var refused = null;
199
+ try {
200
+ await storage.writeBundle("2026-05-23T00-00-00-000Z-abcdef12", srcDir);
201
+ } catch (e) { refused = e; }
202
+ check("backup: bundle exceeding maxBundleBytes refused upfront",
203
+ refused && /bundle-too-large/.test(refused.code || refused.message));
204
+ } finally {
205
+ try { fs.rmSync(srcDir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
206
+ try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
207
+ }
208
+ }
209
+
210
+ async function run() {
211
+ await testTarRoundTrip();
212
+ await testSafeArchiveTarExtract();
213
+ testTarEntryPolicy();
214
+ await testTarChecksumDetection();
215
+ await testTarTruncationRefused();
216
+ await testBackupMigrateDirectoryToTar();
217
+ await testBackupTarFormatDefault();
218
+ await testBackupBundleTooLargeRefused();
219
+ }
220
+
221
+ module.exports = { run: run };
222
+
223
+ if (require.main === module) {
224
+ run().then(
225
+ function () { console.log("[archive-tar] OK — " + helpers.getChecks() + " checks passed"); },
226
+ function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
227
+ );
228
+ }
@@ -2185,6 +2185,76 @@ async function testNoDuplicateCodeBlocks() {
2185
2185
  files: ["lib/api-key.js:issue", "lib/db-query.js:<top>", "lib/session.js:create"],
2186
2186
  reason: "Generic JS array helper / lambda shape — Object.keys(...).map(fn) + similar functional idioms appearing in any code that walks a column-or-key list.",
2187
2187
  },
2188
+ {
2189
+ mode: "family-subset",
2190
+ files: [
2191
+ "lib/archive-read.js:_emitAudit",
2192
+ "lib/archive-tar-read.js:_emitAudit",
2193
+ "lib/archive.js:_emitAudit",
2194
+ "lib/http-client.js:_emitAudit",
2195
+ ],
2196
+ reason: "v0.12.7 + v0.12.8 — Per-module `_emitAudit(opts, action, outcome, metadata)` shape repeats across primitives that drop-silently emit to opts.audit.safeEmit if present. Each module's audit events carry a primitive-specific `action:` namespace (archive.read.*, archive.zip.*, archive.read.tar.*, http-client.*) + per-primitive metadata fields; consolidating would lose the namespace + force every consumer to import the same audit helper. Four-file repetition is the expected shape per `feedback_audit_safeEmit_per_module_emitAudit_shape`. archive-tar.js (write) does NOT carry _emitAudit — the read side lives in sibling archive-tar-read.js so the @primitive validator can pair both `b.archive.tar` (write) and `b.archive.read.tar` (read) cleanly.",
2197
+ },
2198
+ {
2199
+ mode: "family-subset",
2200
+ files: [
2201
+ "lib/archive-adapters.js:fs",
2202
+ "lib/archive-adapters.js:http",
2203
+ "lib/network-smtp-policy.js:mtaStsFetch",
2204
+ "lib/parsers/safe-env.js:readVar",
2205
+ ],
2206
+ reason: "v0.12.7 — `if (typeof <opt> !== \"string\" || <opt>.length === 0) throw new <Error>(...)` shape repeats across primitives validating REQUIRED string opts. validateOpts.requireNonEmptyString covers most call sites; the four flagged here are inline because they each carry a primitive-specific error CODE (adapter/bad-arg, smtp-policy/bad-arg, safe-env/bad-arg) that the helper's caller-error-class shape doesn't compose cleanly across — each primitive's typed-error class is module-local + the message string names the local opt. The duplicated shape is the symptom, not the cause; the cause is that JS doesn't have a way to throw an instance of caller-namespaced ErrorClass without the local closure.",
2207
+ },
2208
+ {
2209
+ mode: "family-subset",
2210
+ files: [
2211
+ "lib/archive-read.js:extract",
2212
+ "lib/archive-tar-read.js:extract",
2213
+ "lib/auth/ciba.js:pollToken",
2214
+ "lib/auth/oid4vci.js:exchangePreAuthorizedCode",
2215
+ "lib/auth/oid4vci.js:issueCredential",
2216
+ ],
2217
+ reason: "v0.12.7 + v0.12.8 — `try { ... await ... } catch (e) { /* per-step cleanup */ throw e; }` shape repeats across primitives doing multi-step async work with per-step rollback. archive-read.extract + archive-tar.extract both clean up partial-extract files; ciba.pollToken cleans up rate-limit + retry state; oid4vci.exchange/issueCredential clean up partial credential-state. Each catch body is primitive-specific (the cleanup it does is the primitive's responsibility) — extraction would require a generic transaction-style helper which is itself a v1.0+ surface decision. Five-file repetition with primitive-specific cleanup bodies stays as the documented exception.",
2218
+ },
2219
+ {
2220
+ mode: "family-subset",
2221
+ files: [
2222
+ "lib/archive-tar-read.js:_classifyTypeflag",
2223
+ "lib/archive-tar-read.js:inspect",
2224
+ "lib/auth/ciba.js:_registerInitialInterval",
2225
+ "lib/auth/oauth.js:exchangeToken",
2226
+ "lib/auth/oauth.js:pollDeviceCode",
2227
+ "lib/auth/oid4vci.js:createCredentialOffer",
2228
+ "lib/auth/oid4vci.js:exchangePreAuthorizedCode",
2229
+ "lib/restore-rollback.js:swap",
2230
+ ],
2231
+ reason: "v0.12.8 — Compact branching helpers (`if (x === A) return ...; if (x === B) return ...;` switch-style) repeat across primitives that map operator-supplied enum values to internal labels. archive-tar-read._classifyTypeflag maps single-char tar typeflags (0/1/2/3/4/5/6/7/x/g) to entry-type labels (file/symlink/hardlink/device/fifo/directory/etc.); archive-tar-read.inspect dispatches on the same typeflag set per-entry — distinct vocabulary from oauth.exchangeToken / oid4vci.createCredentialOffer / etc. which dispatch on grant_type / credential_format / step. The match is shape (chain of if-equals-return), not semantic. Extraction would require a generic enum-dispatch helper for trivially-different enums — that's an obscured abstraction. Each call site's enum + label set is primitive-specific.",
2232
+ },
2233
+ {
2234
+ mode: "family-subset",
2235
+ files: [
2236
+ "lib/archive-read.js:_normalizeEntryTypePolicy",
2237
+ "lib/archive-tar-read.js:_normalizeEntryTypePolicy",
2238
+ "lib/archive.js:writeTo",
2239
+ ],
2240
+ reason: "v0.12.8 — `_normalizeEntryTypePolicy` shape is genuinely duplicated between archive-read.js + archive-tar-read.js — both copy DEFAULT_ENTRY_TYPE_POLICY and merge with operator opts. Could extract to a shared lib/_archive-policy.js helper in a future patch; for v0.12.8 keeping the duplication so the format-specific entry-type vocabulary (zip's external-attrs vs tar's typeflag) stays close to the reader that uses it. archive.js:writeTo is the unrelated third file in the dup cluster — its toBuffer + writeFileSync shape happens to share the 50-token shingle by coincidence (writeTo is the legacy ZIP write-to-path helper, not policy-related).",
2241
+ },
2242
+ {
2243
+ mode: "family-subset",
2244
+ files: [
2245
+ "lib/agent-idempotency.js:_checkArgs",
2246
+ "lib/agent-tenant.js:_sealField",
2247
+ "lib/atomic-file.js:copyDirRecursive",
2248
+ "lib/ddl-change-control.js:approve",
2249
+ "lib/ddl-change-control.js:reject",
2250
+ "lib/deprecate.js:alias",
2251
+ "lib/guard-filename.js:verifyExtractionPath",
2252
+ "lib/jose-jwe-experimental.js:decrypt",
2253
+ "lib/mail-deploy.js:_validateTlsRptReport",
2254
+ "lib/totp.js:uri",
2255
+ ],
2256
+ reason: "v0.12.7 — Generic string-argument validation shape: `if (typeof X !== \"string\" || X.length === 0) throw new <ErrorClass>(<code>, ...)` repeats across primitives validating REQUIRED non-empty string opts. Each call site emits a primitive-specific typed error class (BackupError, GuardFilenameError, IdempotencyError, AgentTenantError, AtomicFileError, DdlError, DeprecateError, JoseError, MailDeployError, TotpError) so extracting to a shared helper would lose the per-primitive error namespace. validateOpts.requireNonEmptyString covers most call sites where the caller's typed-error class composes with the helper's caller-error-class shape; the 9 file paths here are inline because each one's typed error has a primitive-local code namespace + message string the helper can't compose cleanly. Same shape as the v0.10.16 client-hints/csp/sandbox family-subset reason (inline for per-primitive typed errors). 5/9/3-file subsets at smaller token windows are the same family — one entry covers all of them.",
2257
+ },
2188
2258
  {
2189
2259
  mode: "family-subset",
2190
2260
  files: [
@@ -5391,7 +5461,10 @@ var KNOWN_ANTIPATTERNS = [
5391
5461
  "lib/observability-otlp-exporter.js", // byResource grouping (Map<resKey, bucket>) — object-literal factory
5392
5462
  "lib/otel-export.js", // counters / observations (2 sites) — object-literal factory
5393
5463
  "lib/pubsub.js", // exactSubs (Map<channel, Set<sub>>) — Set factory
5464
+ "lib/backup/index.js", // bundleAdapterStorage.listBundles: byBundle (Map<bundleId, stats>) — object-literal factory
5394
5465
  ],
5466
+ // Strong-dup allowlists added with v0.12.7 archive substrate
5467
+ // — see KNOWN_CLUSTERS additions below for structural reasons.
5395
5468
  reason: "Node 26 ships Map.prototype.getOrInsertComputed(key, factory) — a single-lookup get-or-insert that replaces the two-step `var v = m.get(k); if (!v) { v = factory(); m.set(k, v); }` pattern. The sweep is deferred to the Node 26 floor-bump (eligible Oct 2026); engines.node is `>=24` today. Allowlist above is the survey ground truth from memory/specs/node-26-map-getorinsert-migration.md. New code post-this-patch trips the detector — either wait for the floor bump, or add the call site to BOTH the allowlist AND the migration spec in the same patch. When the floor moves, the bump commit walks the allowlist, rewrites each call site, drops the allowlist + flips the detector to enforce.",
5396
5469
  },
5397
5470
  {
@@ -5512,6 +5585,113 @@ var KNOWN_ANTIPATTERNS = [
5512
5585
  reason: "Codex P1 on v0.12.6 PR #157 — `_anyValueToProto` negative-int path wrapped a varint payload in `embeddedMessage` (wire-type 2) instead of using `int64` (wire-type 0 varint). The wire-type mismatch poisons the whole OTLP batch; the `v >>> 0` truncation also dropped sign + high bits. Fixed by adding `pb.int64` + `pb.sint64` to the encoder + routing the negative-int branch through `pb.int64`. Detector locks the shape: `embeddedMessage(N, _writeVarint(...))` cannot recur.",
5513
5586
  },
5514
5587
 
5588
+ {
5589
+ // Codex P1 on v0.12.7 PR #158 — archive-read.extract's rollback
5590
+ // cleanup deleted PRE-EXISTING destination files when a later
5591
+ // entry failed. The renameSync(tmpPath, resolvedPath) silently
5592
+ // overwrote operator files at the destination, then on abort the
5593
+ // catch-block rmSync wiped them out — permanent data loss
5594
+ // disguised as atomic rollback. Fix: refuse to write when the
5595
+ // destination path already exists; force operators to extract
5596
+ // into a fresh / empty subtree.
5597
+ //
5598
+ // Detector scope: any lib/archive*.js or lib/safe-archive.js file
5599
+ // that calls renameSync into a path it ALSO tracks for cleanup
5600
+ // MUST refuse overwrite up-front. Codify as a file-scoped invariant:
5601
+ // archive-read.js must contain "destination-exists" refusal code.
5602
+ id: "archive-extract-overwrite-without-refusal",
5603
+ primitive: "extract loops in lib/archive-read.js MUST refuse to write to a destination path that already exists — atomic rollback via tmp-rename + tracked-path cleanup is only safe when every tracked path was newly created. Pre-existing files at the destination + catch-block rmSync = data loss.",
5604
+ // File-scoped: only fires on archive-read.js / safe-archive.js
5605
+ // shape. The pattern is renameSync of a tmpPath onto resolvedPath
5606
+ // (the canonical destination variable) — atomic-file.js's
5607
+ // operator-file rename is a different shape (operator already
5608
+ // owns the destination context); http-client.js's atomic-tmp
5609
+ // rename writes operator-supplied paths under operator-supplied
5610
+ // tmp dirs, also a different concern.
5611
+ regex: /written\.push\s*\(\s*\{[^}]*path:\s*resolvedPath/,
5612
+ requires: /destination-exists/,
5613
+ skipCommentLines: true,
5614
+ allowlist: [],
5615
+ reason: "Codex P1 on v0.12.7 PR #158 — archive-read.extract used renameSync to atomically place each decompressed entry at its canonical destination + tracked written[].path for catch-block cleanup. When the destination directory was non-empty, the rename silently overwrote operator files; on extract abort, the cleanup deleted them. Fix: refuse upfront if destination path exists, force operators to use a fresh / empty subtree. Detector locks the shape: any extract code that tracks resolvedPath for catch-block cleanup MUST carry a `destination-exists` refusal in the same file.",
5616
+ },
5617
+
5618
+ {
5619
+ // Codex P1 + P2 on v0.12.9 PR #160 — backup readBundle's
5620
+ // tar.gz restore path inherited archive.read.gz defaults (1 GiB
5621
+ // output / 100× ratio), which made the SAME primitive write
5622
+ // bundles it couldn't read back. The detector enforces the
5623
+ // write/read contract for self-authored gzip payloads: any
5624
+ // lib/ call to `archive.read.gz(...)` from a context that has
5625
+ // its own size budget (paired with a `maxBundleBytes` /
5626
+ // `maxOutputBytes` / `maxPayloadBytes` opt) MUST propagate
5627
+ // that budget to read.gz via `maxDecompressedBytes` AND
5628
+ // disable the ratio cap (`maxExpansionRatio: 0`) — bombs in
5629
+ // self-authored payloads are already prevented at write time.
5630
+ id: "archive-read-gz-without-self-authored-budget",
5631
+ primitive: "callers of archive.read.gz from a context that gates its own writes on a size cap (maxBundleBytes / similar) must pass maxDecompressedBytes + maxExpansionRatio:0 so the write/read contract is symmetric. Bomb defenses live at the upstream cap; the gz layer just decompresses.",
5632
+ // File-scoped: only fires on backup/index.js shapes for now.
5633
+ // archive.read.gz called with no opts is fine in operator code
5634
+ // (adversarial-input case); the antipattern is when the caller
5635
+ // also writes payloads under its own size cap.
5636
+ regex: /archive(?:Lazy\(\))?\.read\.gz\s*\([^)]*\)\s*[^,{]/,
5637
+ requires: /maxDecompressedBytes/,
5638
+ skipCommentLines: true,
5639
+ allowlist: [
5640
+ // archive-gz.js IS the read.gz primitive itself.
5641
+ "lib/archive-gz.js",
5642
+ ],
5643
+ reason: "Codex P1/P2 on v0.12.9 PR #160 — backup readBundle's tar.gz restore inherited the 100× ratio + 1 GiB output defaults, breaking restore for zero-filled DB dumps + ~1-8 GiB bundles that writeBundle accepts. Fix: every archive.read.gz call from a primitive with its own size budget propagates that budget. Detector locks the symmetry.",
5644
+ },
5645
+
5646
+ {
5647
+ // v0.12.9 — Direct node:zlib gunzip calls in lib/ must compose
5648
+ // b.safeDecompress (1 GiB output / 100× ratio default caps) so a
5649
+ // hostile gzip stream can't OOM or expand-bomb the host. Mirrors
5650
+ // the v0.11.5 must-compose pattern. lib/archive-gz.js IS the
5651
+ // canonical gunzip site (it wires safeDecompress in directly);
5652
+ // every other lib/ call to zlib.gunzipSync / zlib.createGunzip
5653
+ // must either route through b.safeDecompress OR carry a marker
5654
+ // explaining why it's safe to bypass (e.g. the caller already
5655
+ // applied `maxOutputLength` AND the input is operator-controlled).
5656
+ id: "archive-gz-without-safedecompress",
5657
+ primitive: "every lib/ call to zlib.gunzipSync / zlib.createGunzip / gunzip MUST either go through lib/archive-gz.js (which composes b.safeDecompress) OR carry an `allow:archive-gz-without-safedecompress` marker with the reason the bomb gate is bypassed (typically: `maxOutputLength` is already enforced + the input is operator-trusted).",
5658
+ regex: /zlib\.(?:gunzipSync|createGunzip)\b/,
5659
+ requires: /safeDecompress|maxOutputLength|allow:archive-gz-without-safedecompress/,
5660
+ skipCommentLines: true,
5661
+ allowlist: [
5662
+ // archive-gz.js is the canonical gunzip site — it directly
5663
+ // imports safeDecompress and routes every call through it.
5664
+ // Listed here so the detector doesn't false-positive against
5665
+ // its own enforcement file.
5666
+ "lib/archive-gz.js",
5667
+ ],
5668
+ reason: "v0.12.9 — b.archive.read.gz is the framework's gzip read primitive and composes b.safeDecompress for every gunzip. Direct lib/ zlib.gunzipSync / zlib.createGunzip calls must either route through b.archive.read.gz, compose b.safeDecompress inline, OR carry an explicit `maxOutputLength` cap with the bypass marker. The detector locks the contract so v0.13+ primitives that handle a gzip-wrapped payload can't quietly drop the bomb cap.",
5669
+ },
5670
+
5671
+ {
5672
+ // Codex P1 on v0.12.8 PR #159 — archive-tar-read.js's walker
5673
+ // advanced `pos` by the declared padded block size without
5674
+ // checking that those bytes existed in the buffer. A truncated
5675
+ // archive (header says 11 bytes, buffer holds 8) silently
5676
+ // produced an entry whose extract() sliced the 8-byte prefix
5677
+ // and wrote it as if it were the complete file. Fix: refuse
5678
+ // upfront with a `truncated-entry` typed error when
5679
+ // `bodyStart + paddedSize > bytes.length`. Same shape applies
5680
+ // to the pax-extended-header path (its `bodyEnd` advance was
5681
+ // the same uncapped arithmetic).
5682
+ id: "archive-tar-walker-without-truncation-check",
5683
+ primitive: "tar walkers in lib/archive-tar-read.js MUST verify that the declared block size fits within the remaining buffer before advancing `pos` — a header that claims more bytes than the buffer holds is a truncated archive, not a valid entry. The refusal carries `truncated-entry` code so operators can distinguish wire-format-bad input from policy-bad input.",
5684
+ // File-scoped: only fires on archive-tar-read.js. The walker
5685
+ // advances pos by paddedSize (Math.ceil(hdr.size / BLOCK_SIZE)
5686
+ // * BLOCK_SIZE) — any code that adds paddedSize to pos without
5687
+ // a preceding bounds check is the smell.
5688
+ regex: /pos\s*\+=\s*paddedSize/,
5689
+ requires: /truncated-entry/,
5690
+ skipCommentLines: true,
5691
+ allowlist: [],
5692
+ reason: "Codex P1 on v0.12.8 PR #159 — archive-tar-read.js's tar walker recorded each entry and advanced pos by paddedSize without verifying the declared bytes existed in the buffer. A truncated archive silently produced a partial-content entry on extract — exact reproducer in the Codex thread: declared 11-byte file backed by 8 bytes of buffer produced an 8-byte output. Fix: refuse upfront with `archive-tar/truncated-entry` typed error. Detector locks the shape: any code path that advances pos by paddedSize in archive-tar-read.js MUST carry a `truncated-entry` refusal in the same file.",
5693
+ },
5694
+
5515
5695
  {
5516
5696
  // Codex P2 on v0.11.22 PR #126 — `b.cert.create`'s SNI dispatch
5517
5697
  // wildcard-matched `*.example.com` against `foo.bar.example.com`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.0.83",
3
+ "version": "0.0.98",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -35,7 +35,7 @@
35
35
  "type": "git",
36
36
  "url": "git+https://github.com/blamejs/blamejs.shop.git"
37
37
  },
38
- "homepage": "https://github.com/blamejs/blamejs.shop",
38
+ "homepage": "https://blamejs.shop",
39
39
  "devDependencies": {
40
40
  "@cloudflare/containers": "^0.3.4",
41
41
  "wrangler": "^4.93.0"