amalgm 0.1.238 → 0.1.240

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 (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -14,12 +14,17 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { AMALGM_DIR } = require('../config');
16
16
  const { appendStateEvent } = require('../state/events');
17
- const { originContains } = require('./git');
17
+ const durable = require('./durable');
18
18
  const transition = require('./transition');
19
+ const {
20
+ CHUNK_BYTES,
21
+ describeBytes,
22
+ manifestChecksum,
23
+ sha256,
24
+ } = require('./content');
19
25
 
20
- const CONTRACT = 'git-bundle-chunks@1';
21
- const PAYLOAD_CONTRACT = 'opaque-chunks@1';
22
- const CHUNK_BYTES = 512 * 1024;
26
+ const CONTRACT = 'git-bundle-chunks@2';
27
+ const PAYLOAD_CONTRACT = 'opaque-chunks@2';
23
28
  const ROOT = path.join(AMALGM_DIR, 'state', 'repo-objects');
24
29
  const MANIFESTS = path.join(ROOT, 'manifests');
25
30
  const PAYLOADS = path.join(ROOT, 'payloads');
@@ -27,8 +32,16 @@ const CHUNKS = path.join(ROOT, 'chunks');
27
32
  const REQUESTS = path.join(ROOT, 'requests');
28
33
  const preparing = new Map();
29
34
 
30
- function sha256(bytes) {
31
- return crypto.createHash('sha256').update(bytes).digest('hex');
35
+ function isProjectId(value) {
36
+ return require('./tree/identity').isUuid(String(value || ''));
37
+ }
38
+
39
+ function locatorChecksum(chunks) {
40
+ return manifestChecksum(chunks);
41
+ }
42
+
43
+ function locatorBytes(chunks) {
44
+ return chunks.reduce((total, chunk) => total + chunk.bytes, 0);
32
45
  }
33
46
 
34
47
  function ensureDirs() {
@@ -56,8 +69,7 @@ function readJson(file) {
56
69
  }
57
70
 
58
71
  function writeJson(file, value) {
59
- fs.mkdirSync(path.dirname(file), { recursive: true });
60
- fs.writeFileSync(file, `${JSON.stringify(value)}\n`, { mode: 0o600 });
72
+ durable.writeJson(file, value);
61
73
  }
62
74
 
63
75
  function publicLocator(manifest) {
@@ -83,14 +95,18 @@ function publicPayloadLocator(manifest) {
83
95
  };
84
96
  }
85
97
 
86
- function writeChunks(bytes) {
87
- const chunks = [];
88
- for (let offset = 0; offset < bytes.length; offset += CHUNK_BYTES) {
89
- const part = bytes.subarray(offset, Math.min(offset + CHUNK_BYTES, bytes.length));
90
- const digest = sha256(part);
91
- const chunkFile = path.join(CHUNKS, `${digest}.bin`);
92
- if (!fs.existsSync(chunkFile)) fs.writeFileSync(chunkFile, part, { mode: 0o600 });
93
- chunks.push({ sha256: digest, bytes: part.length, uploaded: false });
98
+ function describeChunks(bytes) {
99
+ return describeBytes(bytes).map((chunk) => ({ ...chunk, uploaded: false }));
100
+ }
101
+
102
+ function writeChunks(bytes, chunks = describeChunks(bytes)) {
103
+ let offset = 0;
104
+ for (const chunk of chunks) {
105
+ const part = bytes.subarray(offset, offset + chunk.bytes);
106
+ const chunkFile = path.join(CHUNKS, `${chunk.sha256}.bin`);
107
+ const held = fs.existsSync(chunkFile) ? fs.readFileSync(chunkFile) : null;
108
+ if (!held || sha256(held) !== chunk.sha256) durable.writeFile(chunkFile, part);
109
+ offset += chunk.bytes;
94
110
  }
95
111
  return chunks;
96
112
  }
@@ -113,8 +129,8 @@ async function prepareBundle(worktreeRoot, repoKey, resourceId, oid, file) {
113
129
  repoKey,
114
130
  oid,
115
131
  ref,
116
- checksum: sha256(bundle),
117
- bytes: bundle.length,
132
+ checksum: locatorChecksum(chunks),
133
+ bytes: locatorBytes(chunks),
118
134
  chunks,
119
135
  createdAt: new Date().toISOString(),
120
136
  });
@@ -128,23 +144,29 @@ function publishPayload(key, bytesInput, repoKey) {
128
144
  ensureDirs();
129
145
  const bytes = Buffer.isBuffer(bytesInput) ? bytesInput : Buffer.from(bytesInput);
130
146
  const project = require('./wall').projectForRepo(repoKey);
131
- const checksum = sha256(bytes);
132
147
  if (!project?.resourceId) {
133
- return { kind: 'pending', contract: PAYLOAD_CONTRACT, checksum };
148
+ return { kind: 'pending', contract: PAYLOAD_CONTRACT };
134
149
  }
150
+ const chunks = describeChunks(bytes);
151
+ const checksum = locatorChecksum(chunks);
135
152
  // Payload content is its identity. A later checkpoint that still contains
136
153
  // the same large file must reference the already-uploaded bytes instead of
137
154
  // creating another upload merely because the surrounding checkpoint moved.
138
155
  const file = payloadFile(project.resourceId, checksum);
139
156
  let manifest = readJson(file);
140
- if (!manifest || manifest.checksum !== checksum) {
157
+ if (
158
+ !manifest
159
+ || manifest.contract !== PAYLOAD_CONTRACT
160
+ || manifest.checksum !== checksum
161
+ ) {
162
+ writeChunks(bytes, chunks);
141
163
  manifest = {
142
164
  contract: PAYLOAD_CONTRACT,
143
165
  resourceId: project.resourceId,
144
166
  repoKey,
145
167
  checksum,
146
- bytes: bytes.length,
147
- chunks: writeChunks(bytes),
168
+ bytes: locatorBytes(chunks),
169
+ chunks,
148
170
  createdAt: new Date().toISOString(),
149
171
  };
150
172
  writeJson(file, manifest);
@@ -182,7 +204,6 @@ async function bundleSourceFor(worktreeRoot, repoKey, oid) {
182
204
 
183
205
  async function objectSourceFor(worktreeRoot, repoKey, oid) {
184
206
  if (!oid || !repoKey) return null;
185
- if (await originContains(worktreeRoot, oid)) return { kind: 'remote' };
186
207
  return bundleSourceFor(worktreeRoot, repoKey, oid);
187
208
  }
188
209
 
@@ -190,10 +211,22 @@ function claimUpload() {
190
211
  ensureDirs();
191
212
  for (const directory of [MANIFESTS, PAYLOADS]) {
192
213
  for (const name of fs.readdirSync(directory).sort()) {
193
- const manifest = readJson(path.join(directory, name));
214
+ const file = path.join(directory, name);
215
+ const manifest = readJson(file);
194
216
  const chunk = manifest?.chunks?.find((entry) => !entry.uploaded);
195
217
  if (!chunk) continue;
196
- const bytes = fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`));
218
+ const chunkFile = path.join(CHUNKS, `${chunk.sha256}.bin`);
219
+ const bytes = fs.existsSync(chunkFile) ? fs.readFileSync(chunkFile) : null;
220
+ if (
221
+ !bytes
222
+ || bytes.length !== chunk.bytes
223
+ || sha256(bytes) !== chunk.sha256
224
+ ) {
225
+ fs.rmSync(chunkFile, { force: true });
226
+ fs.rmSync(file, { force: true });
227
+ wake('delete', chunk.sha256);
228
+ continue;
229
+ }
197
230
  return {
198
231
  resourceId: manifest.resourceId,
199
232
  sha256: chunk.sha256,
@@ -243,17 +276,20 @@ function validatePayloadLocator(locator) {
243
276
  if (
244
277
  locator?.kind !== 'chunks'
245
278
  || locator?.contract !== PAYLOAD_CONTRACT
246
- || !/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(locator.resourceId || ''))
279
+ || !isProjectId(locator.resourceId)
247
280
  || !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
248
281
  || !Number.isSafeInteger(locator.bytes)
249
- || locator.bytes < 0
282
+ || locator.bytes < 1
250
283
  || !Array.isArray(locator.chunks)
284
+ || locator.chunks.length < 1
251
285
  || locator.chunks.some((chunk) => (
252
286
  !/^[0-9a-f]{64}$/.test(String(chunk?.sha256 || ''))
253
287
  || !Number.isSafeInteger(chunk?.bytes)
254
- || chunk.bytes < 0
288
+ || chunk.bytes < 1
255
289
  || chunk.bytes > CHUNK_BYTES
256
290
  ))
291
+ || locatorBytes(locator.chunks) !== locator.bytes
292
+ || locatorChecksum(locator.chunks) !== locator.checksum
257
293
  ) throw new Error('invalid checkpoint payload locator');
258
294
  }
259
295
 
@@ -263,7 +299,11 @@ function readPayload(locator) {
263
299
  let waiting = false;
264
300
  for (const chunk of locator.chunks) {
265
301
  const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
266
- if (fs.existsSync(file) && fs.statSync(file).size === chunk.bytes) continue;
302
+ if (fs.existsSync(file)) {
303
+ const held = fs.readFileSync(file);
304
+ if (held.length === chunk.bytes && sha256(held) === chunk.sha256) continue;
305
+ fs.rmSync(file, { force: true });
306
+ }
267
307
  writeJson(requestFile(locator.resourceId, chunk.sha256), {
268
308
  resourceId: locator.resourceId,
269
309
  sha256: chunk.sha256,
@@ -282,7 +322,7 @@ function readPayload(locator) {
282
322
  return bytes;
283
323
  });
284
324
  const bytes = Buffer.concat(parts);
285
- if (bytes.length !== locator.bytes || sha256(bytes) !== locator.checksum) {
325
+ if (bytes.length !== locator.bytes) {
286
326
  throw new Error('checkpoint payload verification failed');
287
327
  }
288
328
  writeJson(payloadFile(locator.resourceId, locator.checksum), {
@@ -303,21 +343,29 @@ function requestFile(resourceId, digest) {
303
343
 
304
344
  function claimDownload() {
305
345
  ensureDirs();
306
- const name = fs.readdirSync(REQUESTS).filter((entry) => entry.endsWith('.json')).sort()[0];
307
- if (!name) return null;
308
- const request = readJson(path.join(REQUESTS, name));
309
- return request?.sha256 ? request : null;
346
+ for (const name of fs.readdirSync(REQUESTS).filter((entry) => entry.endsWith('.json')).sort()) {
347
+ const file = path.join(REQUESTS, name);
348
+ const request = readJson(file);
349
+ if (
350
+ request
351
+ && Object.keys(request).length === 2
352
+ && isProjectId(request.resourceId)
353
+ && /^[0-9a-f]{64}$/.test(String(request.sha256 || ''))
354
+ ) return request;
355
+ fs.rmSync(file, { force: true });
356
+ }
357
+ return null;
310
358
  }
311
359
 
312
360
  function receiveDownload(resourceId, digest, dataBase64) {
313
- if (!/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(resourceId || ''))) {
361
+ if (!isProjectId(resourceId)) {
314
362
  throw new Error('invalid repo object project');
315
363
  }
316
364
  if (!/^[0-9a-f]{64}$/.test(String(digest || ''))) throw new Error('invalid repo object checksum');
317
365
  ensureDirs();
318
366
  const bytes = Buffer.from(String(dataBase64 || ''), 'base64');
319
367
  if (sha256(bytes) !== digest) throw new Error('repo object checksum mismatch');
320
- fs.writeFileSync(path.join(CHUNKS, `${digest}.bin`), bytes, { mode: 0o600 });
368
+ durable.writeFile(path.join(CHUNKS, `${digest}.bin`), bytes);
321
369
  fs.rmSync(requestFile(resourceId, digest), { force: true });
322
370
  wake('update', digest);
323
371
  require('./reducer').reduceNow().catch(() => {});
@@ -326,17 +374,24 @@ function receiveDownload(resourceId, digest, dataBase64) {
326
374
 
327
375
  function validateLocator(locator) {
328
376
  if (
329
- locator?.contract !== CONTRACT
330
- || !/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(locator.resourceId || ''))
331
- || !/^[0-9a-f]{40,64}$/.test(String(locator.oid || ''))
377
+ locator?.kind !== 'bundle'
378
+ || locator?.contract !== CONTRACT
379
+ || !isProjectId(locator.resourceId)
380
+ || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(locator.oid || ''))
332
381
  || !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
382
+ || locator.ref !== `refs/amalgm/exports/${locator.oid}`
383
+ || !Number.isSafeInteger(locator.bytes)
384
+ || locator.bytes < 1
333
385
  || !Array.isArray(locator.chunks)
386
+ || locator.chunks.length < 1
334
387
  || locator.chunks.some((chunk) => (
335
388
  !/^[0-9a-f]{64}$/.test(String(chunk?.sha256 || ''))
336
389
  || !Number.isSafeInteger(chunk?.bytes)
337
- || chunk.bytes < 0
390
+ || chunk.bytes < 1
338
391
  || chunk.bytes > CHUNK_BYTES
339
392
  ))
393
+ || locatorBytes(locator.chunks) !== locator.bytes
394
+ || locatorChecksum(locator.chunks) !== locator.checksum
340
395
  ) throw new Error('invalid git bundle locator');
341
396
  }
342
397
 
@@ -346,7 +401,11 @@ function prepareBundleImport(locator) {
346
401
  let waiting = false;
347
402
  for (const chunk of locator.chunks) {
348
403
  const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
349
- if (fs.existsSync(file) && fs.statSync(file).size === chunk.bytes) continue;
404
+ if (fs.existsSync(file)) {
405
+ const held = fs.readFileSync(file);
406
+ if (held.length === chunk.bytes && sha256(held) === chunk.sha256) continue;
407
+ fs.rmSync(file, { force: true });
408
+ }
350
409
  writeJson(requestFile(locator.resourceId, chunk.sha256), {
351
410
  resourceId: locator.resourceId,
352
411
  sha256: chunk.sha256,
@@ -359,19 +418,24 @@ function prepareBundleImport(locator) {
359
418
  }
360
419
 
361
420
  const bundleFile = path.join(ROOT, `${locator.checksum}.bundle`);
362
- if (!fs.existsSync(bundleFile)) {
363
- const parts = locator.chunks.map((chunk) => {
364
- const bytes = fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`));
365
- if (bytes.length !== chunk.bytes || sha256(bytes) !== chunk.sha256) {
366
- throw new Error('git bundle chunk verification failed');
367
- }
368
- return bytes;
369
- });
370
- const bundle = Buffer.concat(parts);
371
- if (bundle.length !== locator.bytes || sha256(bundle) !== locator.checksum) {
372
- throw new Error('git bundle verification failed');
421
+ const parts = locator.chunks.map((chunk) => (
422
+ fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`))
423
+ ));
424
+ const bundleMatches = fs.existsSync(bundleFile) && (() => {
425
+ const held = fs.readFileSync(bundleFile);
426
+ let offset = 0;
427
+ for (let index = 0; index < locator.chunks.length; index += 1) {
428
+ const chunk = locator.chunks[index];
429
+ const part = held.subarray(offset, offset + chunk.bytes);
430
+ if (part.length !== chunk.bytes || sha256(part) !== chunk.sha256) return false;
431
+ offset += chunk.bytes;
373
432
  }
374
- fs.writeFileSync(bundleFile, bundle, { mode: 0o600 });
433
+ return offset === held.length && held.length === locator.bytes;
434
+ })();
435
+ if (!bundleMatches) {
436
+ const bundle = Buffer.concat(parts);
437
+ if (bundle.length !== locator.bytes) throw new Error('git bundle verification failed');
438
+ durable.writeFile(bundleFile, bundle);
375
439
  }
376
440
  return { state: 'ready', file: bundleFile };
377
441
  }
@@ -387,6 +451,7 @@ module.exports = {
387
451
  CHUNK_BYTES,
388
452
  CONTRACT,
389
453
  PAYLOAD_CONTRACT,
454
+ _test: { describeChunks, locatorBytes, locatorChecksum },
390
455
  acknowledgeUpload,
391
456
  bundleSourceFor,
392
457
  claimDownload,