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.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- 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
|
|
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@
|
|
28
|
-
const PAYLOAD_CONTRACT = 'opaque-chunks@
|
|
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
|
|
53
|
-
return
|
|
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
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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:
|
|
142
|
-
bytes:
|
|
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
|
|
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:
|
|
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
|
|
225
|
-
const
|
|
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
|
|
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
|
-
||
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 (
|
|
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
|
-
|
|
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?.
|
|
465
|
-
|| locator
|
|
466
|
-
||
|
|
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 <
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
525
|
-
|
|
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,
|