amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -13,44 +13,22 @@ const crypto = require('crypto');
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { AMALGM_DIR } = require('../config');
16
- const { isSharedResourceId } = require('../state/cloud-resource-id');
17
16
  const { appendStateEvent } = require('../state/events');
18
- const durable = require('./durable');
17
+ const { originContains } = require('./git');
19
18
  const transition = require('./transition');
20
- const {
21
- CHUNK_BYTES,
22
- describeBytes,
23
- manifestChecksum,
24
- sha256,
25
- } = require('./content');
26
19
 
27
- const CONTRACT = 'git-bundle-chunks@2';
28
- const PAYLOAD_CONTRACT = 'opaque-chunks@2';
20
+ const CONTRACT = 'git-bundle-chunks@1';
21
+ const PAYLOAD_CONTRACT = 'opaque-chunks@1';
22
+ const CHUNK_BYTES = 512 * 1024;
29
23
  const ROOT = path.join(AMALGM_DIR, 'state', 'repo-objects');
30
24
  const MANIFESTS = path.join(ROOT, 'manifests');
31
25
  const PAYLOADS = path.join(ROOT, 'payloads');
32
26
  const CHUNKS = path.join(ROOT, 'chunks');
33
27
  const REQUESTS = path.join(ROOT, 'requests');
34
28
  const preparing = new Map();
35
- const UPLOAD_RETRY_BASE_MS = 1_000;
36
- const UPLOAD_RETRY_MAX_MS = 60_000;
37
- const RESOURCE_WIDE_UPLOAD_FAILURES = new Set([
38
- 'invalid_repo_project',
39
- 'private_resource_forbidden',
40
- 'shared_resource_read_forbidden',
41
- 'shared_resource_write_forbidden',
42
- ]);
43
-
44
- function isProjectId(value) {
45
- return require('./tree/identity').isUuid(String(value || ''));
46
- }
47
-
48
- function locatorChecksum(chunks) {
49
- return manifestChecksum(chunks);
50
- }
51
29
 
52
- function locatorBytes(chunks) {
53
- return chunks.reduce((total, chunk) => total + chunk.bytes, 0);
30
+ function sha256(bytes) {
31
+ return crypto.createHash('sha256').update(bytes).digest('hex');
54
32
  }
55
33
 
56
34
  function ensureDirs() {
@@ -78,7 +56,8 @@ function readJson(file) {
78
56
  }
79
57
 
80
58
  function writeJson(file, value) {
81
- durable.writeJson(file, value);
59
+ fs.mkdirSync(path.dirname(file), { recursive: true });
60
+ fs.writeFileSync(file, `${JSON.stringify(value)}\n`, { mode: 0o600 });
82
61
  }
83
62
 
84
63
  function publicLocator(manifest) {
@@ -104,18 +83,14 @@ function publicPayloadLocator(manifest) {
104
83
  };
105
84
  }
106
85
 
107
- function describeChunks(bytes) {
108
- return describeBytes(bytes).map((chunk) => ({ ...chunk, uploaded: false }));
109
- }
110
-
111
- function writeChunks(bytes, chunks = describeChunks(bytes)) {
112
- let offset = 0;
113
- for (const chunk of chunks) {
114
- const part = bytes.subarray(offset, offset + chunk.bytes);
115
- const chunkFile = path.join(CHUNKS, `${chunk.sha256}.bin`);
116
- const held = fs.existsSync(chunkFile) ? fs.readFileSync(chunkFile) : null;
117
- if (!held || sha256(held) !== chunk.sha256) durable.writeFile(chunkFile, part);
118
- offset += chunk.bytes;
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 });
119
94
  }
120
95
  return chunks;
121
96
  }
@@ -138,8 +113,8 @@ async function prepareBundle(worktreeRoot, repoKey, resourceId, oid, file) {
138
113
  repoKey,
139
114
  oid,
140
115
  ref,
141
- checksum: locatorChecksum(chunks),
142
- bytes: locatorBytes(chunks),
116
+ checksum: sha256(bundle),
117
+ bytes: bundle.length,
143
118
  chunks,
144
119
  createdAt: new Date().toISOString(),
145
120
  });
@@ -153,29 +128,20 @@ function publishPayload(key, bytesInput, repoKey) {
153
128
  ensureDirs();
154
129
  const bytes = Buffer.isBuffer(bytesInput) ? bytesInput : Buffer.from(bytesInput);
155
130
  const project = require('./wall').projectForRepo(repoKey);
131
+ const checksum = sha256(bytes);
156
132
  if (!project?.resourceId) {
157
- return { kind: 'pending', contract: PAYLOAD_CONTRACT };
133
+ return { kind: 'pending', contract: PAYLOAD_CONTRACT, checksum };
158
134
  }
159
- const chunks = describeChunks(bytes);
160
- const checksum = locatorChecksum(chunks);
161
- // Payload content is its identity. A later checkpoint that still contains
162
- // the same large file must reference the already-uploaded bytes instead of
163
- // creating another upload merely because the surrounding checkpoint moved.
164
- const file = payloadFile(project.resourceId, checksum);
135
+ const file = payloadFile(project.resourceId, key);
165
136
  let manifest = readJson(file);
166
- if (
167
- !manifest
168
- || manifest.contract !== PAYLOAD_CONTRACT
169
- || manifest.checksum !== checksum
170
- ) {
171
- writeChunks(bytes, chunks);
137
+ if (!manifest || manifest.checksum !== checksum) {
172
138
  manifest = {
173
139
  contract: PAYLOAD_CONTRACT,
174
140
  resourceId: project.resourceId,
175
141
  repoKey,
176
142
  checksum,
177
- bytes: locatorBytes(chunks),
178
- chunks,
143
+ bytes: bytes.length,
144
+ chunks: writeChunks(bytes),
179
145
  createdAt: new Date().toISOString(),
180
146
  };
181
147
  writeJson(file, manifest);
@@ -213,38 +179,18 @@ async function bundleSourceFor(worktreeRoot, repoKey, oid) {
213
179
 
214
180
  async function objectSourceFor(worktreeRoot, repoKey, oid) {
215
181
  if (!oid || !repoKey) return null;
182
+ if (await originContains(worktreeRoot, oid)) return { kind: 'remote' };
216
183
  return bundleSourceFor(worktreeRoot, repoKey, oid);
217
184
  }
218
185
 
219
186
  function claimUpload() {
220
187
  ensureDirs();
221
- const now = Date.now();
222
188
  for (const directory of [MANIFESTS, PAYLOADS]) {
223
189
  for (const name of fs.readdirSync(directory).sort()) {
224
- const file = path.join(directory, name);
225
- const manifest = readJson(file);
226
- const chunk = manifest?.chunks?.find((entry) => (
227
- !entry.uploaded
228
- && !entry.uploadTerminal
229
- && (
230
- !entry.uploadRetryAfter
231
- || !Number.isFinite(Date.parse(entry.uploadRetryAfter))
232
- || Date.parse(entry.uploadRetryAfter) <= now
233
- )
234
- ));
190
+ const manifest = readJson(path.join(directory, name));
191
+ const chunk = manifest?.chunks?.find((entry) => !entry.uploaded);
235
192
  if (!chunk) continue;
236
- const chunkFile = path.join(CHUNKS, `${chunk.sha256}.bin`);
237
- const bytes = fs.existsSync(chunkFile) ? fs.readFileSync(chunkFile) : null;
238
- if (
239
- !bytes
240
- || bytes.length !== chunk.bytes
241
- || sha256(bytes) !== chunk.sha256
242
- ) {
243
- fs.rmSync(chunkFile, { force: true });
244
- fs.rmSync(file, { force: true });
245
- wake('delete', chunk.sha256);
246
- continue;
247
- }
193
+ const bytes = fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`));
248
194
  return {
249
195
  resourceId: manifest.resourceId,
250
196
  sha256: chunk.sha256,
@@ -270,10 +216,6 @@ function acknowledgeUpload(resourceId, digest) {
270
216
  for (const chunk of manifest.chunks) {
271
217
  if (chunk.sha256 === digest && !chunk.uploaded) {
272
218
  chunk.uploaded = true;
273
- delete chunk.uploadAttempts;
274
- delete chunk.uploadLastError;
275
- delete chunk.uploadRetryAfter;
276
- delete chunk.uploadTerminal;
277
219
  touched = true;
278
220
  changed = true;
279
221
  }
@@ -294,89 +236,21 @@ function acknowledgeUpload(resourceId, digest) {
294
236
  return { changed, completed };
295
237
  }
296
238
 
297
- function failUpload(resourceId, digest, failure = {}) {
298
- // Upload work is durable across protocol generations. Match the exact
299
- // identity that was accepted when the work was queued, including legacy
300
- // `shr_` rails, so stale historical work can reach a terminal state instead
301
- // of monopolizing every newer code project behind it.
302
- if (!isSharedResourceId(resourceId) || !/^[0-9a-f]{64}$/.test(String(digest || ''))) {
303
- return { changed: false, terminal: false };
304
- }
305
- ensureDirs();
306
- const failedAt = new Date().toISOString();
307
- const retryable = failure?.retryable !== false;
308
- const failureCode = typeof failure?.code === 'string'
309
- ? failure.code
310
- : 'repo_object_upload_failed';
311
- const resourceWide = RESOURCE_WIDE_UPLOAD_FAILURES.has(failureCode);
312
- let changed = false;
313
- let terminal = false;
314
- let retryAfter = null;
315
- let attempts = 0;
316
- for (const directory of [MANIFESTS, PAYLOADS]) {
317
- for (const name of fs.readdirSync(directory)) {
318
- const file = path.join(directory, name);
319
- const manifest = readJson(file);
320
- if (manifest?.resourceId !== resourceId || !manifest?.chunks) continue;
321
- const failedChunks = manifest.chunks.filter((entry) => (
322
- !entry.uploaded && (resourceWide || entry.sha256 === digest)
323
- ));
324
- if (failedChunks.length < 1) continue;
325
- for (const chunk of failedChunks) {
326
- const chunkAttempts = Number(chunk.uploadAttempts || 0) + 1;
327
- attempts = Math.max(attempts, chunkAttempts);
328
- chunk.uploadAttempts = chunkAttempts;
329
- chunk.uploadLastError = {
330
- code: failureCode,
331
- message: typeof failure?.message === 'string'
332
- ? failure.message
333
- : 'repo object upload failed',
334
- retryable,
335
- at: failedAt,
336
- };
337
- if (retryable) {
338
- const delayMs = Math.min(
339
- UPLOAD_RETRY_MAX_MS,
340
- UPLOAD_RETRY_BASE_MS * (2 ** Math.min(chunkAttempts - 1, 16)),
341
- );
342
- const chunkRetryAfter = new Date(Date.now() + delayMs).toISOString();
343
- retryAfter = retryAfter && Date.parse(retryAfter) > Date.parse(chunkRetryAfter)
344
- ? retryAfter
345
- : chunkRetryAfter;
346
- chunk.uploadRetryAfter = chunkRetryAfter;
347
- delete chunk.uploadTerminal;
348
- } else {
349
- chunk.uploadTerminal = { ...chunk.uploadLastError };
350
- delete chunk.uploadRetryAfter;
351
- terminal = true;
352
- }
353
- }
354
- writeJson(file, manifest);
355
- changed = true;
356
- }
357
- }
358
- if (changed) wake('update', digest);
359
- return { changed, terminal, retryAfter, attempts };
360
- }
361
-
362
239
  function validatePayloadLocator(locator) {
363
240
  if (
364
241
  locator?.kind !== 'chunks'
365
242
  || locator?.contract !== PAYLOAD_CONTRACT
366
- || !isProjectId(locator.resourceId)
243
+ || !/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(locator.resourceId || ''))
367
244
  || !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
368
245
  || !Number.isSafeInteger(locator.bytes)
369
- || locator.bytes < 1
246
+ || locator.bytes < 0
370
247
  || !Array.isArray(locator.chunks)
371
- || locator.chunks.length < 1
372
248
  || locator.chunks.some((chunk) => (
373
249
  !/^[0-9a-f]{64}$/.test(String(chunk?.sha256 || ''))
374
250
  || !Number.isSafeInteger(chunk?.bytes)
375
- || chunk.bytes < 1
251
+ || chunk.bytes < 0
376
252
  || chunk.bytes > CHUNK_BYTES
377
253
  ))
378
- || locatorBytes(locator.chunks) !== locator.bytes
379
- || locatorChecksum(locator.chunks) !== locator.checksum
380
254
  ) throw new Error('invalid checkpoint payload locator');
381
255
  }
382
256
 
@@ -386,11 +260,7 @@ function readPayload(locator) {
386
260
  let waiting = false;
387
261
  for (const chunk of locator.chunks) {
388
262
  const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
389
- if (fs.existsSync(file)) {
390
- const held = fs.readFileSync(file);
391
- if (held.length === chunk.bytes && sha256(held) === chunk.sha256) continue;
392
- fs.rmSync(file, { force: true });
393
- }
263
+ if (fs.existsSync(file) && fs.statSync(file).size === chunk.bytes) continue;
394
264
  writeJson(requestFile(locator.resourceId, chunk.sha256), {
395
265
  resourceId: locator.resourceId,
396
266
  sha256: chunk.sha256,
@@ -409,18 +279,9 @@ function readPayload(locator) {
409
279
  return bytes;
410
280
  });
411
281
  const bytes = Buffer.concat(parts);
412
- if (bytes.length !== locator.bytes) {
282
+ if (bytes.length !== locator.bytes || sha256(bytes) !== locator.checksum) {
413
283
  throw new Error('checkpoint payload verification failed');
414
284
  }
415
- writeJson(payloadFile(locator.resourceId, locator.checksum), {
416
- contract: PAYLOAD_CONTRACT,
417
- resourceId: locator.resourceId,
418
- repoKey: null,
419
- checksum: locator.checksum,
420
- bytes: locator.bytes,
421
- chunks: locator.chunks.map((chunk) => ({ ...chunk, uploaded: true })),
422
- createdAt: new Date().toISOString(),
423
- });
424
285
  return { state: 'ready', bytes };
425
286
  }
426
287
 
@@ -430,29 +291,21 @@ function requestFile(resourceId, digest) {
430
291
 
431
292
  function claimDownload() {
432
293
  ensureDirs();
433
- for (const name of fs.readdirSync(REQUESTS).filter((entry) => entry.endsWith('.json')).sort()) {
434
- const file = path.join(REQUESTS, name);
435
- const request = readJson(file);
436
- if (
437
- request
438
- && Object.keys(request).length === 2
439
- && isProjectId(request.resourceId)
440
- && /^[0-9a-f]{64}$/.test(String(request.sha256 || ''))
441
- ) return request;
442
- fs.rmSync(file, { force: true });
443
- }
444
- return null;
294
+ const name = fs.readdirSync(REQUESTS).filter((entry) => entry.endsWith('.json')).sort()[0];
295
+ if (!name) return null;
296
+ const request = readJson(path.join(REQUESTS, name));
297
+ return request?.sha256 ? request : null;
445
298
  }
446
299
 
447
300
  function receiveDownload(resourceId, digest, dataBase64) {
448
- if (!isProjectId(resourceId)) {
301
+ if (!/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(resourceId || ''))) {
449
302
  throw new Error('invalid repo object project');
450
303
  }
451
304
  if (!/^[0-9a-f]{64}$/.test(String(digest || ''))) throw new Error('invalid repo object checksum');
452
305
  ensureDirs();
453
306
  const bytes = Buffer.from(String(dataBase64 || ''), 'base64');
454
307
  if (sha256(bytes) !== digest) throw new Error('repo object checksum mismatch');
455
- durable.writeFile(path.join(CHUNKS, `${digest}.bin`), bytes);
308
+ fs.writeFileSync(path.join(CHUNKS, `${digest}.bin`), bytes, { mode: 0o600 });
456
309
  fs.rmSync(requestFile(resourceId, digest), { force: true });
457
310
  wake('update', digest);
458
311
  require('./reducer').reduceNow().catch(() => {});
@@ -461,24 +314,17 @@ function receiveDownload(resourceId, digest, dataBase64) {
461
314
 
462
315
  function validateLocator(locator) {
463
316
  if (
464
- locator?.kind !== 'bundle'
465
- || locator?.contract !== CONTRACT
466
- || !isProjectId(locator.resourceId)
467
- || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(locator.oid || ''))
317
+ locator?.contract !== CONTRACT
318
+ || !/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(locator.resourceId || ''))
319
+ || !/^[0-9a-f]{40,64}$/.test(String(locator.oid || ''))
468
320
  || !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
469
- || locator.ref !== `refs/amalgm/exports/${locator.oid}`
470
- || !Number.isSafeInteger(locator.bytes)
471
- || locator.bytes < 1
472
321
  || !Array.isArray(locator.chunks)
473
- || locator.chunks.length < 1
474
322
  || locator.chunks.some((chunk) => (
475
323
  !/^[0-9a-f]{64}$/.test(String(chunk?.sha256 || ''))
476
324
  || !Number.isSafeInteger(chunk?.bytes)
477
- || chunk.bytes < 1
325
+ || chunk.bytes < 0
478
326
  || chunk.bytes > CHUNK_BYTES
479
327
  ))
480
- || locatorBytes(locator.chunks) !== locator.bytes
481
- || locatorChecksum(locator.chunks) !== locator.checksum
482
328
  ) throw new Error('invalid git bundle locator');
483
329
  }
484
330
 
@@ -488,11 +334,7 @@ function prepareBundleImport(locator) {
488
334
  let waiting = false;
489
335
  for (const chunk of locator.chunks) {
490
336
  const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
491
- if (fs.existsSync(file)) {
492
- const held = fs.readFileSync(file);
493
- if (held.length === chunk.bytes && sha256(held) === chunk.sha256) continue;
494
- fs.rmSync(file, { force: true });
495
- }
337
+ if (fs.existsSync(file) && fs.statSync(file).size === chunk.bytes) continue;
496
338
  writeJson(requestFile(locator.resourceId, chunk.sha256), {
497
339
  resourceId: locator.resourceId,
498
340
  sha256: chunk.sha256,
@@ -505,24 +347,19 @@ function prepareBundleImport(locator) {
505
347
  }
506
348
 
507
349
  const bundleFile = path.join(ROOT, `${locator.checksum}.bundle`);
508
- const parts = locator.chunks.map((chunk) => (
509
- fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`))
510
- ));
511
- const bundleMatches = fs.existsSync(bundleFile) && (() => {
512
- const held = fs.readFileSync(bundleFile);
513
- let offset = 0;
514
- for (let index = 0; index < locator.chunks.length; index += 1) {
515
- const chunk = locator.chunks[index];
516
- const part = held.subarray(offset, offset + chunk.bytes);
517
- if (part.length !== chunk.bytes || sha256(part) !== chunk.sha256) return false;
518
- offset += chunk.bytes;
519
- }
520
- return offset === held.length && held.length === locator.bytes;
521
- })();
522
- if (!bundleMatches) {
350
+ if (!fs.existsSync(bundleFile)) {
351
+ const parts = locator.chunks.map((chunk) => {
352
+ const bytes = fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`));
353
+ if (bytes.length !== chunk.bytes || sha256(bytes) !== chunk.sha256) {
354
+ throw new Error('git bundle chunk verification failed');
355
+ }
356
+ return bytes;
357
+ });
523
358
  const bundle = Buffer.concat(parts);
524
- if (bundle.length !== locator.bytes) throw new Error('git bundle verification failed');
525
- durable.writeFile(bundleFile, bundle);
359
+ if (bundle.length !== locator.bytes || sha256(bundle) !== locator.checksum) {
360
+ throw new Error('git bundle verification failed');
361
+ }
362
+ fs.writeFileSync(bundleFile, bundle, { mode: 0o600 });
526
363
  }
527
364
  return { state: 'ready', file: bundleFile };
528
365
  }
@@ -538,12 +375,10 @@ module.exports = {
538
375
  CHUNK_BYTES,
539
376
  CONTRACT,
540
377
  PAYLOAD_CONTRACT,
541
- _test: { describeChunks, locatorBytes, locatorChecksum },
542
378
  acknowledgeUpload,
543
379
  bundleSourceFor,
544
380
  claimDownload,
545
381
  claimUpload,
546
- failUpload,
547
382
  importFromLocator,
548
383
  objectSourceFor,
549
384
  prepareBundleImport,