@blamejs/blamejs-shop 0.0.113 → 0.0.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/analytics.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +16 -0
- package/lib/vendor/blamejs/api-snapshot.json +6 -2
- package/lib/vendor/blamejs/lib/archive-wrap.js +58 -0
- package/lib/vendor/blamejs/lib/archive.js +1 -0
- package/lib/vendor/blamejs/lib/backup/index.js +585 -10
- package/lib/vendor/blamejs/lib/safe-archive.js +112 -3
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.13.json +31 -0
- package/lib/vendor/blamejs/release-notes/v0.12.14.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.15.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.12.16.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.17.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.18.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.19.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.20.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-sniff-envelope.test.js +118 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-bundle-info.test.js +279 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-object-store-adapter.test.js +167 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-all-bundles.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-bundle.test.js +186 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-auto-unwrap.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-inspect-unwrap.test.js +89 -0
- package/package.json +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.backup.bundleAdapterStorage#bundleInfo + listBundles
|
|
4
|
+
* format inference + v0.12.17 envelopeKind probe.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var fs = require("node:fs");
|
|
8
|
+
var path = require("node:path");
|
|
9
|
+
var os = require("node:os");
|
|
10
|
+
var b = require("../../index");
|
|
11
|
+
var helpers = require("../helpers");
|
|
12
|
+
var check = helpers.check;
|
|
13
|
+
|
|
14
|
+
async function testBundleInfoTarGzRecipient() {
|
|
15
|
+
var pair = b.crypto.generateEncryptionKeyPair();
|
|
16
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "bi-src-r-"));
|
|
17
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bi-dest-r-"));
|
|
18
|
+
try {
|
|
19
|
+
fs.writeFileSync(path.join(src, "phi.json"), "{\"id\":42}", { mode: 0o600 });
|
|
20
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
21
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
22
|
+
format: "tar.gz",
|
|
23
|
+
cryptoStrategy: "recipient",
|
|
24
|
+
recipient: pair,
|
|
25
|
+
});
|
|
26
|
+
var bid = "2026-05-23T23-00-00-000Z-deadbeef";
|
|
27
|
+
await storage.writeBundle(bid, src);
|
|
28
|
+
var info = await storage.bundleInfo(bid);
|
|
29
|
+
check("bundleInfo: format inferred from storage layout", info.format === "tar.gz");
|
|
30
|
+
check("bundleInfo: envelopeKind probed from payload magic",
|
|
31
|
+
info.envelopeKind === "recipient");
|
|
32
|
+
check("bundleInfo: sizeBytes carries payload length", info.sizeBytes > 0);
|
|
33
|
+
} finally {
|
|
34
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
35
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function testBundleInfoTarPassphrase() {
|
|
40
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "bi-src-p-"));
|
|
41
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bi-dest-p-"));
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(path.join(src, "data.json"), "{\"v\":1}", { mode: 0o600 });
|
|
44
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
45
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
46
|
+
format: "tar",
|
|
47
|
+
cryptoStrategy: "passphrase",
|
|
48
|
+
passphrase: "aLongCorrectHorseBatteryStaple9876!Phrase",
|
|
49
|
+
});
|
|
50
|
+
var bid = "2026-05-23T23-15-00-000Z-cafef00d";
|
|
51
|
+
await storage.writeBundle(bid, src);
|
|
52
|
+
var info = await storage.bundleInfo(bid);
|
|
53
|
+
check("bundleInfo: tar format inferred", info.format === "tar");
|
|
54
|
+
check("bundleInfo: passphrase envelope detected",
|
|
55
|
+
info.envelopeKind === "passphrase");
|
|
56
|
+
} finally {
|
|
57
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
58
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function testBundleInfoPlaintext() {
|
|
63
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "bi-src-n-"));
|
|
64
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bi-dest-n-"));
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(path.join(src, "data.json"), "{\"v\":1}", { mode: 0o600 });
|
|
67
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
68
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
69
|
+
format: "tar.gz",
|
|
70
|
+
});
|
|
71
|
+
var bid = "2026-05-23T23-30-00-000Z-ba5eba11";
|
|
72
|
+
await storage.writeBundle(bid, src);
|
|
73
|
+
var info = await storage.bundleInfo(bid);
|
|
74
|
+
check("bundleInfo: plaintext bundle yields envelopeKind \"none\"",
|
|
75
|
+
info.envelopeKind === "none");
|
|
76
|
+
} finally {
|
|
77
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
78
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function testBundleInfoNotFound() {
|
|
83
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bi-dest-nf-"));
|
|
84
|
+
try {
|
|
85
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
86
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
87
|
+
});
|
|
88
|
+
var refused = null;
|
|
89
|
+
try {
|
|
90
|
+
await storage.bundleInfo("2026-05-23T23-45-00-000Z-feedface");
|
|
91
|
+
} catch (e) { refused = e; }
|
|
92
|
+
check("bundleInfo: nonexistent bundle refused with bundle-not-found",
|
|
93
|
+
refused && /bundle-not-found/.test(refused.code || refused.message));
|
|
94
|
+
} finally {
|
|
95
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function testBundleInfoLargeBundleUsesPartialRead() {
|
|
100
|
+
// Codex P1 on v0.12.17 PR #168 — bundleInfo's envelope probe
|
|
101
|
+
// claimed 5 bytes but read the full payload. With a 1 MiB
|
|
102
|
+
// bundle + readPartial-capable adapter, the probe should consume
|
|
103
|
+
// at most 16 bytes; verify by mocking an adapter whose readFile
|
|
104
|
+
// throws "DO NOT CALL" but whose readPartial returns the magic.
|
|
105
|
+
var calls = { readFile: 0, readPartial: 0 };
|
|
106
|
+
var sealedHead = Buffer.concat([Buffer.from("BAWRP"), Buffer.alloc(11)]); // 16 bytes
|
|
107
|
+
var mockAdapter = {
|
|
108
|
+
writeFile: async function () {},
|
|
109
|
+
readFile: async function () { calls.readFile += 1; throw new Error("readFile MUST NOT be called for the probe"); },
|
|
110
|
+
listKeys: async function () { return []; },
|
|
111
|
+
deleteKey: async function () {},
|
|
112
|
+
hasKey: async function (key) { return /bundle\.tar\.gz$/.test(key); },
|
|
113
|
+
readPartial: async function (key, length) {
|
|
114
|
+
calls.readPartial += 1;
|
|
115
|
+
return sealedHead.slice(0, length);
|
|
116
|
+
},
|
|
117
|
+
statKey: async function () { return { size: 1024 * 1024, mtimeMs: 0 }; },
|
|
118
|
+
};
|
|
119
|
+
var storage = b.backup.bundleAdapterStorage({ adapter: mockAdapter });
|
|
120
|
+
var info = await storage.bundleInfo("2026-05-23T23-00-00-000Z-bbbbbbbb");
|
|
121
|
+
check("bundleInfo: probe routes through readPartial when adapter exposes it",
|
|
122
|
+
calls.readPartial === 1 && calls.readFile === 0);
|
|
123
|
+
check("bundleInfo: envelopeKind correctly identified from partial read",
|
|
124
|
+
info.envelopeKind === "recipient");
|
|
125
|
+
check("bundleInfo: sizeBytes from statKey", info.sizeBytes === 1024 * 1024);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function testListBundlesTarGzWinsOverTar() {
|
|
129
|
+
// Codex P2 on v0.12.17 PR #168 — when both bundle.tar and
|
|
130
|
+
// bundle.tar.gz exist for the same bundleId (e.g. operator
|
|
131
|
+
// migration in progress), listBundles must report tar.gz to
|
|
132
|
+
// align with readBundle's precedence. Verify by injecting both
|
|
133
|
+
// keys via a mock adapter.
|
|
134
|
+
var mockAdapter = {
|
|
135
|
+
writeFile: async function () {},
|
|
136
|
+
readFile: async function () { return Buffer.alloc(0); },
|
|
137
|
+
listKeys: async function () {
|
|
138
|
+
// Return keys in tar-first order; tar.gz should still win.
|
|
139
|
+
return [
|
|
140
|
+
"2026-05-23T22-00-00-000Z-aabbccdd/bundle.tar",
|
|
141
|
+
"2026-05-23T22-00-00-000Z-aabbccdd/bundle.tar.gz",
|
|
142
|
+
];
|
|
143
|
+
},
|
|
144
|
+
deleteKey: async function () {},
|
|
145
|
+
hasKey: async function () { return false; },
|
|
146
|
+
};
|
|
147
|
+
var storage = b.backup.bundleAdapterStorage({ adapter: mockAdapter });
|
|
148
|
+
var list = await storage.listBundles();
|
|
149
|
+
check("listBundles: tar.gz precedence applied when both formats exist",
|
|
150
|
+
list.length === 1 && list[0].format === "tar.gz");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function testListBundlesWithStats() {
|
|
154
|
+
// v0.12.18 — listBundles({ withStats: true }) populates
|
|
155
|
+
// createdAt + size from statKey when the adapter exposes it.
|
|
156
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "lbws-src-"));
|
|
157
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "lbws-dest-"));
|
|
158
|
+
try {
|
|
159
|
+
fs.writeFileSync(path.join(src, "a"), "x", { mode: 0o600 });
|
|
160
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
161
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
162
|
+
format: "tar.gz",
|
|
163
|
+
});
|
|
164
|
+
var bid = "2026-05-24T00-00-00-000Z-99887766";
|
|
165
|
+
await storage.writeBundle(bid, src);
|
|
166
|
+
var noStats = await storage.listBundles();
|
|
167
|
+
check("listBundles: default call leaves size + createdAt null",
|
|
168
|
+
noStats[0].size === null && noStats[0].createdAt === null);
|
|
169
|
+
var withStats = await storage.listBundles({ withStats: true });
|
|
170
|
+
check("listBundles({ withStats: true }): size populated",
|
|
171
|
+
typeof withStats[0].size === "number" && withStats[0].size > 0);
|
|
172
|
+
check("listBundles({ withStats: true }): createdAt is ISO string",
|
|
173
|
+
typeof withStats[0].createdAt === "string" &&
|
|
174
|
+
/^\d{4}-\d{2}-\d{2}T/.test(withStats[0].createdAt));
|
|
175
|
+
} finally {
|
|
176
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
177
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function testBundleInfoDirectoryCreatedAt() {
|
|
182
|
+
// Codex P2 on v0.12.18 PR #169 — directory-format bundles MUST
|
|
183
|
+
// populate createdAt from the manifest.json (parity with
|
|
184
|
+
// listBundles({ withStats })). Previously the stat lookup was
|
|
185
|
+
// gated inside `if (payloadKey !== null)` which skipped
|
|
186
|
+
// directory bundles entirely.
|
|
187
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "bidir-src-"));
|
|
188
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bidir-dest-"));
|
|
189
|
+
try {
|
|
190
|
+
fs.writeFileSync(path.join(src, "a"), "x", { mode: 0o600 });
|
|
191
|
+
fs.writeFileSync(path.join(src, "manifest.json"), "{\"version\":1}", { mode: 0o600 });
|
|
192
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
193
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
194
|
+
format: "directory",
|
|
195
|
+
});
|
|
196
|
+
var bid = "2026-05-24T00-45-00-000Z-aaaa9999";
|
|
197
|
+
await storage.writeBundle(bid, src);
|
|
198
|
+
var info = await storage.bundleInfo(bid);
|
|
199
|
+
check("bundleInfo: directory-format bundle reports createdAt from manifest",
|
|
200
|
+
typeof info.createdAt === "string" && /^\d{4}-\d{2}-\d{2}T/.test(info.createdAt));
|
|
201
|
+
check("bundleInfo: directory-format bundle reports format \"directory\"",
|
|
202
|
+
info.format === "directory");
|
|
203
|
+
var list = await storage.listBundles({ withStats: true });
|
|
204
|
+
check("listBundles+withStats vs bundleInfo: createdAt parity for directory format",
|
|
205
|
+
list[0].createdAt === info.createdAt);
|
|
206
|
+
} finally {
|
|
207
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
208
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function testBundleInfoCreatedAt() {
|
|
213
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "bica-src-"));
|
|
214
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "bica-dest-"));
|
|
215
|
+
try {
|
|
216
|
+
fs.writeFileSync(path.join(src, "a"), "x", { mode: 0o600 });
|
|
217
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
218
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
219
|
+
format: "tar",
|
|
220
|
+
});
|
|
221
|
+
var bid = "2026-05-24T00-30-00-000Z-aabbccdd";
|
|
222
|
+
await storage.writeBundle(bid, src);
|
|
223
|
+
var info = await storage.bundleInfo(bid);
|
|
224
|
+
check("bundleInfo: createdAt is ISO string from statKey.mtimeMs",
|
|
225
|
+
typeof info.createdAt === "string" && /^\d{4}-\d{2}-\d{2}T/.test(info.createdAt));
|
|
226
|
+
} finally {
|
|
227
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
228
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function testListBundlesCarriesFormat() {
|
|
233
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "lb-src-"));
|
|
234
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "lb-dest-"));
|
|
235
|
+
try {
|
|
236
|
+
fs.writeFileSync(path.join(src, "data.json"), "{\"v\":1}", { mode: 0o600 });
|
|
237
|
+
var tarStorage = b.backup.bundleAdapterStorage({
|
|
238
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
239
|
+
format: "tar",
|
|
240
|
+
});
|
|
241
|
+
await tarStorage.writeBundle("2026-05-23T23-50-00-000Z-a1b2c3d4", src);
|
|
242
|
+
var tarGzStorage = b.backup.bundleAdapterStorage({
|
|
243
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
244
|
+
format: "tar.gz",
|
|
245
|
+
});
|
|
246
|
+
await tarGzStorage.writeBundle("2026-05-23T23-55-00-000Z-e5f6a7b8", src);
|
|
247
|
+
var list = await tarStorage.listBundles();
|
|
248
|
+
check("listBundles: returns 2 bundles", list.length === 2);
|
|
249
|
+
var byFormat = {};
|
|
250
|
+
for (var i = 0; i < list.length; i += 1) byFormat[list[i].format] = (byFormat[list[i].format] || 0) + 1;
|
|
251
|
+
check("listBundles: format inferred per bundle (tar + tar.gz)",
|
|
252
|
+
byFormat.tar === 1 && byFormat["tar.gz"] === 1);
|
|
253
|
+
} finally {
|
|
254
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
255
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function run() {
|
|
260
|
+
await testBundleInfoTarGzRecipient();
|
|
261
|
+
await testBundleInfoTarPassphrase();
|
|
262
|
+
await testBundleInfoPlaintext();
|
|
263
|
+
await testBundleInfoNotFound();
|
|
264
|
+
await testBundleInfoLargeBundleUsesPartialRead();
|
|
265
|
+
await testListBundlesTarGzWinsOverTar();
|
|
266
|
+
await testListBundlesCarriesFormat();
|
|
267
|
+
await testListBundlesWithStats();
|
|
268
|
+
await testBundleInfoCreatedAt();
|
|
269
|
+
await testBundleInfoDirectoryCreatedAt();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { run: run };
|
|
273
|
+
|
|
274
|
+
if (require.main === module) {
|
|
275
|
+
run().then(
|
|
276
|
+
function () { console.log("[backup-bundle-info] OK — " + helpers.getChecks() + " checks passed"); },
|
|
277
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.backup.bundleAdapterStorage.objectStoreAdapter +
|
|
4
|
+
* end-to-end round-trip via b.objectStore local backend +
|
|
5
|
+
* combined with v0.12.10 recipient + v0.12.11 passphrase wrap.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var fs = require("node:fs");
|
|
9
|
+
var path = require("node:path");
|
|
10
|
+
var os = require("node:os");
|
|
11
|
+
var b = require("../../index");
|
|
12
|
+
var helpers = require("../helpers");
|
|
13
|
+
var check = helpers.check;
|
|
14
|
+
|
|
15
|
+
function _mkSrc(name, contents) {
|
|
16
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-os-src-"));
|
|
17
|
+
fs.writeFileSync(path.join(dir, name), contents);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function testObjectStoreAdapterRoundTrip() {
|
|
22
|
+
var rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-os-root-"));
|
|
23
|
+
var src = _mkSrc("data.json", "{\"v\":1}");
|
|
24
|
+
var verify = path.join(os.tmpdir(), "bjs-os-verify-" + Date.now());
|
|
25
|
+
try {
|
|
26
|
+
var client = b.objectStore.buildBackend({ protocol: "local", rootDir: rootDir });
|
|
27
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
28
|
+
adapter: b.backup.bundleAdapterStorage.objectStoreAdapter(client, { prefix: "test-prefix" }),
|
|
29
|
+
format: "tar.gz",
|
|
30
|
+
});
|
|
31
|
+
var bundleId = "2026-05-23T22-00-00-000Z-aaaa1111";
|
|
32
|
+
await storage.writeBundle(bundleId, src);
|
|
33
|
+
check("objectStoreAdapter: hasBundle true after write",
|
|
34
|
+
await storage.hasBundle(bundleId));
|
|
35
|
+
await storage.readBundle(bundleId, verify);
|
|
36
|
+
check("objectStoreAdapter: bundle round-trips after fs-backed objectStore put + get",
|
|
37
|
+
fs.readFileSync(path.join(verify, "data.json"), "utf-8") === "{\"v\":1}");
|
|
38
|
+
var diskKey = path.join(rootDir, "test-prefix", bundleId, "bundle.tar.gz");
|
|
39
|
+
check("objectStoreAdapter: prefix applied — key lands under operator-specified root",
|
|
40
|
+
fs.existsSync(diskKey));
|
|
41
|
+
} finally {
|
|
42
|
+
try { fs.rmSync(rootDir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
43
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
44
|
+
try { fs.rmSync(verify, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function testObjectStoreAdapterWithRecipient() {
|
|
49
|
+
var rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "bjs-os-root-r-"));
|
|
50
|
+
var src = _mkSrc("phi.json", "{\"patient\":42}");
|
|
51
|
+
var verify = path.join(os.tmpdir(), "bjs-os-verify-r-" + Date.now());
|
|
52
|
+
try {
|
|
53
|
+
var pair = b.crypto.generateEncryptionKeyPair();
|
|
54
|
+
var client = b.objectStore.buildBackend({ protocol: "local", rootDir: rootDir });
|
|
55
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
56
|
+
adapter: b.backup.bundleAdapterStorage.objectStoreAdapter(client),
|
|
57
|
+
format: "tar.gz",
|
|
58
|
+
cryptoStrategy: "recipient",
|
|
59
|
+
recipient: pair,
|
|
60
|
+
});
|
|
61
|
+
var bundleId = "2026-05-23T22-15-00-000Z-aaaa2222";
|
|
62
|
+
await storage.writeBundle(bundleId, src);
|
|
63
|
+
var sealed = fs.readFileSync(path.join(rootDir, bundleId, "bundle.tar.gz"));
|
|
64
|
+
check("objectStoreAdapter + recipient: bundle carries BAWRP envelope magic on disk",
|
|
65
|
+
sealed.slice(0, 5).toString("ascii") === "BAWRP");
|
|
66
|
+
await storage.readBundle(bundleId, verify);
|
|
67
|
+
check("objectStoreAdapter + recipient: round-trips through unwrap + gunzip + untar",
|
|
68
|
+
fs.readFileSync(path.join(verify, "phi.json"), "utf-8") === "{\"patient\":42}");
|
|
69
|
+
} finally {
|
|
70
|
+
try { fs.rmSync(rootDir, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
71
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
72
|
+
try { fs.rmSync(verify, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function testObjectStoreAdapterRefusesBadClient() {
|
|
77
|
+
var refused = null;
|
|
78
|
+
try {
|
|
79
|
+
b.backup.bundleAdapterStorage.objectStoreAdapter({}); // missing put/get/etc
|
|
80
|
+
} catch (e) { refused = e; }
|
|
81
|
+
check("objectStoreAdapter: missing methods refused upfront",
|
|
82
|
+
refused && /missing method/.test(refused.message || ""));
|
|
83
|
+
var refused2 = null;
|
|
84
|
+
try {
|
|
85
|
+
b.backup.bundleAdapterStorage.objectStoreAdapter(null);
|
|
86
|
+
} catch (e) { refused2 = e; }
|
|
87
|
+
check("objectStoreAdapter: null client refused upfront",
|
|
88
|
+
refused2 && /client is required/.test(refused2.message || ""));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function testObjectStoreAdapterPagination() {
|
|
92
|
+
// Codex P1 on v0.12.13 PR #164 — listKeys must follow
|
|
93
|
+
// truncated / continuationToken pages. Mock a client that
|
|
94
|
+
// returns 3 pages then exhausts.
|
|
95
|
+
var pages = [
|
|
96
|
+
{ items: [{ key: "p/a" }, { key: "p/b" }], truncated: true, continuationToken: "tok1" },
|
|
97
|
+
{ items: [{ key: "p/c" }, { key: "p/d" }], truncated: true, continuationToken: "tok2" },
|
|
98
|
+
{ items: [{ key: "p/e" }], truncated: false, continuationToken: null },
|
|
99
|
+
];
|
|
100
|
+
var calls = [];
|
|
101
|
+
var client = {
|
|
102
|
+
put: async function () { return { size: 0 }; },
|
|
103
|
+
get: async function () { return Buffer.alloc(0); },
|
|
104
|
+
head: async function () { return { size: 0 }; },
|
|
105
|
+
delete: async function () { return true; },
|
|
106
|
+
list: async function (prefix, opts) {
|
|
107
|
+
calls.push({ prefix: prefix, token: (opts && opts.continuationToken) || null });
|
|
108
|
+
return pages.shift();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
var adapter = b.backup.bundleAdapterStorage.objectStoreAdapter(client, { prefix: "p" });
|
|
112
|
+
var keys = await adapter.listKeys("");
|
|
113
|
+
check("objectStoreAdapter.listKeys: walks all paginated pages",
|
|
114
|
+
keys.length === 5 && keys.join(",") === "a,b,c,d,e");
|
|
115
|
+
check("objectStoreAdapter.listKeys: forwarded continuationToken across calls",
|
|
116
|
+
calls.length === 3 && calls[1].token === "tok1" && calls[2].token === "tok2");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function testObjectStoreAdapterPaginationRunaway() {
|
|
120
|
+
// A misbehaving backend that returns truncated:true forever
|
|
121
|
+
// must trip the safety cap rather than spin.
|
|
122
|
+
var client = {
|
|
123
|
+
put: async function () { return { size: 0 }; },
|
|
124
|
+
get: async function () { return Buffer.alloc(0); },
|
|
125
|
+
head: async function () { return { size: 0 }; },
|
|
126
|
+
delete: async function () { return true; },
|
|
127
|
+
list: async function () {
|
|
128
|
+
return { items: [{ key: "k" }], truncated: true, continuationToken: "ever" };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
var adapter = b.backup.bundleAdapterStorage.objectStoreAdapter(client);
|
|
132
|
+
var refused = null;
|
|
133
|
+
try { await adapter.listKeys(""); } catch (e) { refused = e; }
|
|
134
|
+
check("objectStoreAdapter.listKeys: runaway pagination refused with typed error",
|
|
135
|
+
refused && /list-pagination-runaway/.test(refused.code || refused.message));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function testObjectStoreAdapterPrefixTraversalRefused() {
|
|
139
|
+
var client = b.objectStore.buildBackend({
|
|
140
|
+
protocol: "local",
|
|
141
|
+
rootDir: fs.mkdtempSync(path.join(os.tmpdir(), "bjs-os-trav-")),
|
|
142
|
+
});
|
|
143
|
+
var refused = null;
|
|
144
|
+
try {
|
|
145
|
+
b.backup.bundleAdapterStorage.objectStoreAdapter(client, { prefix: "../escape" });
|
|
146
|
+
} catch (e) { refused = e; }
|
|
147
|
+
check("objectStoreAdapter: prefix with traversal segment refused upfront",
|
|
148
|
+
refused && /traversal/.test(refused.message || ""));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function run() {
|
|
152
|
+
await testObjectStoreAdapterRoundTrip();
|
|
153
|
+
await testObjectStoreAdapterWithRecipient();
|
|
154
|
+
await testObjectStoreAdapterRefusesBadClient();
|
|
155
|
+
await testObjectStoreAdapterPagination();
|
|
156
|
+
await testObjectStoreAdapterPaginationRunaway();
|
|
157
|
+
await testObjectStoreAdapterPrefixTraversalRefused();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { run: run };
|
|
161
|
+
|
|
162
|
+
if (require.main === module) {
|
|
163
|
+
run().then(
|
|
164
|
+
function () { console.log("[backup-object-store-adapter] OK — " + helpers.getChecks() + " checks passed"); },
|
|
165
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.backup.bundleAdapterStorage#verifyBundle integrity
|
|
4
|
+
* check across plaintext + recipient + passphrase + directory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var fs = require("node:fs");
|
|
8
|
+
var path = require("node:path");
|
|
9
|
+
var os = require("node:os");
|
|
10
|
+
var b = require("../../index");
|
|
11
|
+
var helpers = require("../helpers");
|
|
12
|
+
var check = helpers.check;
|
|
13
|
+
|
|
14
|
+
function _mkSrcDir(name, contents) {
|
|
15
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "vb-src-"));
|
|
16
|
+
fs.writeFileSync(path.join(dir, name), contents, { mode: 0o600 });
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function testVerifyTarGzRecipientOk() {
|
|
21
|
+
var pair = b.crypto.generateEncryptionKeyPair();
|
|
22
|
+
var src = _mkSrcDir("a.json", "{\"x\":1}");
|
|
23
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dest-"));
|
|
24
|
+
try {
|
|
25
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
26
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
27
|
+
format: "tar.gz",
|
|
28
|
+
cryptoStrategy: "recipient",
|
|
29
|
+
recipient: pair,
|
|
30
|
+
});
|
|
31
|
+
var bid = "2026-05-24T01-00-00-000Z-aaaa1111";
|
|
32
|
+
await storage.writeBundle(bid, src);
|
|
33
|
+
var v = await storage.verifyBundle(bid);
|
|
34
|
+
check("verifyBundle: tar.gz + recipient bundle reports ok=true",
|
|
35
|
+
v.ok === true);
|
|
36
|
+
check("verifyBundle: format reported", v.format === "tar.gz");
|
|
37
|
+
check("verifyBundle: envelopeKind reported", v.envelopeKind === "recipient");
|
|
38
|
+
check("verifyBundle: entryCount populated", v.entryCount === 1);
|
|
39
|
+
check("verifyBundle: errors array empty", v.errors.length === 0);
|
|
40
|
+
} finally {
|
|
41
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
42
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function testVerifyTarPassphraseOk() {
|
|
47
|
+
var src = _mkSrcDir("a.json", "{\"x\":1}");
|
|
48
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dest-p-"));
|
|
49
|
+
try {
|
|
50
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
51
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
52
|
+
format: "tar",
|
|
53
|
+
cryptoStrategy: "passphrase",
|
|
54
|
+
passphrase: "aLongCorrectHorseBatteryStaple9876!Phrase",
|
|
55
|
+
});
|
|
56
|
+
var bid = "2026-05-24T01-15-00-000Z-bbbb2222";
|
|
57
|
+
await storage.writeBundle(bid, src);
|
|
58
|
+
var v = await storage.verifyBundle(bid);
|
|
59
|
+
check("verifyBundle: tar + passphrase ok",
|
|
60
|
+
v.ok === true && v.envelopeKind === "passphrase" && v.entryCount === 1);
|
|
61
|
+
} finally {
|
|
62
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
63
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function testVerifyPlaintextTarOk() {
|
|
68
|
+
var src = _mkSrcDir("a.json", "{\"x\":1}");
|
|
69
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dest-pt-"));
|
|
70
|
+
try {
|
|
71
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
72
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
73
|
+
format: "tar",
|
|
74
|
+
});
|
|
75
|
+
var bid = "2026-05-24T01-30-00-000Z-cccc3333";
|
|
76
|
+
await storage.writeBundle(bid, src);
|
|
77
|
+
var v = await storage.verifyBundle(bid);
|
|
78
|
+
check("verifyBundle: plaintext tar ok",
|
|
79
|
+
v.ok === true && v.envelopeKind === "none" && v.entryCount === 1);
|
|
80
|
+
} finally {
|
|
81
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
82
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function testVerifyWrongPassphraseFails() {
|
|
87
|
+
var src = _mkSrcDir("a.json", "{\"x\":1}");
|
|
88
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dest-wp-"));
|
|
89
|
+
try {
|
|
90
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
91
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
92
|
+
format: "tar",
|
|
93
|
+
cryptoStrategy: "passphrase",
|
|
94
|
+
passphrase: "aLongCorrectHorseBatteryStaple9876!Phrase",
|
|
95
|
+
});
|
|
96
|
+
var bid = "2026-05-24T01-45-00-000Z-dddd4444";
|
|
97
|
+
await storage.writeBundle(bid, src);
|
|
98
|
+
var v = await storage.verifyBundle(bid, {
|
|
99
|
+
passphrase: "wrongPassphrase!CompletelyDifferent987654321",
|
|
100
|
+
});
|
|
101
|
+
check("verifyBundle: wrong passphrase reports ok=false with error",
|
|
102
|
+
v.ok === false && v.errors.length > 0);
|
|
103
|
+
} finally {
|
|
104
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
105
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function testVerifyCorruptedTarFails() {
|
|
110
|
+
// Write a tar bundle, corrupt the tar header, verify reports failure.
|
|
111
|
+
var src = _mkSrcDir("a.json", "{\"x\":1}");
|
|
112
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dest-cor-"));
|
|
113
|
+
try {
|
|
114
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
115
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
116
|
+
format: "tar",
|
|
117
|
+
});
|
|
118
|
+
var bid = "2026-05-24T02-00-00-000Z-eeee5555";
|
|
119
|
+
await storage.writeBundle(bid, src);
|
|
120
|
+
var tarPath = path.join(dest, bid, "bundle.tar");
|
|
121
|
+
// Corrupt the first 257 bytes (header field) so ustar magic + chksum break.
|
|
122
|
+
var bytes = fs.readFileSync(tarPath);
|
|
123
|
+
bytes[257] = 0x42; bytes[258] = 0x42; bytes[259] = 0x42;
|
|
124
|
+
fs.writeFileSync(tarPath, bytes, { mode: 0o600 });
|
|
125
|
+
var v = await storage.verifyBundle(bid);
|
|
126
|
+
check("verifyBundle: corrupted tar reports ok=false with error",
|
|
127
|
+
v.ok === false && v.errors.length > 0);
|
|
128
|
+
} finally {
|
|
129
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
130
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function testVerifyDirectoryOk() {
|
|
135
|
+
var src = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dir-src-"));
|
|
136
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dir-dest-"));
|
|
137
|
+
try {
|
|
138
|
+
fs.writeFileSync(path.join(src, "manifest.json"), "{\"v\":1}", { mode: 0o600 });
|
|
139
|
+
fs.writeFileSync(path.join(src, "a"), "x", { mode: 0o600 });
|
|
140
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
141
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
142
|
+
format: "directory",
|
|
143
|
+
});
|
|
144
|
+
var bid = "2026-05-24T02-15-00-000Z-ffff6666";
|
|
145
|
+
await storage.writeBundle(bid, src);
|
|
146
|
+
var v = await storage.verifyBundle(bid);
|
|
147
|
+
check("verifyBundle: directory format reports ok=true (manifest existence is the verification)",
|
|
148
|
+
v.ok === true && v.format === "directory" && v.entryCount === null);
|
|
149
|
+
} finally {
|
|
150
|
+
try { fs.rmSync(src, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
151
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function testVerifyMissingBundleFails() {
|
|
156
|
+
var dest = fs.mkdtempSync(path.join(os.tmpdir(), "vb-dest-mb-"));
|
|
157
|
+
try {
|
|
158
|
+
var storage = b.backup.bundleAdapterStorage({
|
|
159
|
+
adapter: b.backup.bundleAdapterStorage.fsAdapter({ root: dest }),
|
|
160
|
+
});
|
|
161
|
+
var v = await storage.verifyBundle("2026-05-24T02-30-00-000Z-99999999");
|
|
162
|
+
check("verifyBundle: missing bundle reports ok=false with bundle-not-found",
|
|
163
|
+
v.ok === false && /bundle-not-found/.test(v.errors[0] || ""));
|
|
164
|
+
} finally {
|
|
165
|
+
try { fs.rmSync(dest, { recursive: true, force: true }); } catch (_e) { /* ignore */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function run() {
|
|
170
|
+
await testVerifyTarGzRecipientOk();
|
|
171
|
+
await testVerifyTarPassphraseOk();
|
|
172
|
+
await testVerifyPlaintextTarOk();
|
|
173
|
+
await testVerifyWrongPassphraseFails();
|
|
174
|
+
await testVerifyCorruptedTarFails();
|
|
175
|
+
await testVerifyDirectoryOk();
|
|
176
|
+
await testVerifyMissingBundleFails();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { run: run };
|
|
180
|
+
|
|
181
|
+
if (require.main === module) {
|
|
182
|
+
run().then(
|
|
183
|
+
function () { console.log("[backup-verify-bundle] OK — " + helpers.getChecks() + " checks passed"); },
|
|
184
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -5646,6 +5646,34 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
5646
5646
|
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.",
|
|
5647
5647
|
},
|
|
5648
5648
|
|
|
5649
|
+
{
|
|
5650
|
+
// Codex P1 on v0.12.13 PR #164 — listKeys against a paginated
|
|
5651
|
+
// object-store backend dropped every key past the first page
|
|
5652
|
+
// because the call sent only one client.list() and ignored
|
|
5653
|
+
// the `truncated` / `continuationToken` contract. Detector
|
|
5654
|
+
// locks the shape: any lib/ call to `client.list(...)` paired
|
|
5655
|
+
// with `truncated` consumption MUST also walk the
|
|
5656
|
+
// continuationToken loop, OR the call site must carry an
|
|
5657
|
+
// `allow:list-without-pagination` marker explaining why
|
|
5658
|
+
// single-page is sufficient (typically: the caller already
|
|
5659
|
+
// bounds the prefix or already passes a maxResults that's
|
|
5660
|
+
// known to be larger than the universe).
|
|
5661
|
+
id: "object-store-list-without-pagination",
|
|
5662
|
+
primitive: "object-store list calls in lib/ MUST walk truncated / continuationToken pages — single-shot list silently truncates at the backend's page cap (1000 by default). The runtime symptom is silent data loss in listBundles / deleteBundle. Either follow the pagination loop or carry the `allow:list-without-pagination` marker with the bound reason.",
|
|
5663
|
+
regex: /\bclient\.list\s*\(/,
|
|
5664
|
+
requires: /continuationToken|truncated|allow:list-without-pagination/,
|
|
5665
|
+
skipCommentLines: true,
|
|
5666
|
+
allowlist: [
|
|
5667
|
+
// backup/index.js IS the runtime pagination site (walks the
|
|
5668
|
+
// loop with the runaway-cap defence). Allowlisted because
|
|
5669
|
+
// the inline `client.list` calls there are inside the
|
|
5670
|
+
// walker itself; the detector would false-positive on the
|
|
5671
|
+
// call inside the do-while.
|
|
5672
|
+
"lib/backup/index.js",
|
|
5673
|
+
],
|
|
5674
|
+
reason: "Codex P1 on v0.12.13 PR #164 — objectStoreAdapter.listKeys called client.list once and never followed the truncated/continuationToken pagination contract. The fix walks the loop with a PAGINATION_CAP safety net. Detector locks the shape so a future caller of client.list can't silently drop pagination.",
|
|
5675
|
+
},
|
|
5676
|
+
|
|
5649
5677
|
{
|
|
5650
5678
|
// Codex P1A on v0.12.12 PR #163 — "on-request" placement
|
|
5651
5679
|
// semantics collapsed into "always" when shouldEmit didn't
|