amalgm 0.1.175 → 0.1.176
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/lib/shared-realtime-tunnel.js +28 -12
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/index.js +1 -1
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +5 -1
- package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +4 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +13 -6
- package/runtime/scripts/amalgm-mcp/state/migrations.js +0 -4
- package/runtime/scripts/amalgm-mcp/state/mutations.js +13 -1
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +25 -1
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +9 -0
- package/runtime/scripts/amalgm-mcp/tests/state-promotion-ownership.test.js +30 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +4 -5
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +37 -11
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +10 -2
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +14 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +324 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +36 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +81 -48
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -1
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +9 -21
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +187 -122
- package/runtime/scripts/amalgm-mcp/workspace/git.js +71 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +119 -2
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +49 -17
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +582 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +15 -1
- package/runtime/scripts/amalgm-mcp/workspace/store.js +8 -18
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +67 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +3 -4
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +331 -180
- package/runtime/scripts/amalgm-mcp/tests/workspace-reconcile.test.js +0 -359
- package/runtime/scripts/amalgm-mcp/workspace/ledger.js +0 -96
- package/runtime/scripts/amalgm-mcp/workspace/reconcile.js +0 -452
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* goes through transition.applyMergedFiles so it lands in the journal.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
const crypto = require('crypto');
|
|
30
31
|
const fs = require('fs');
|
|
31
32
|
const os = require('os');
|
|
32
33
|
const path = require('path');
|
|
@@ -102,6 +103,21 @@ async function treeOfPatch(worktreeRoot, baseOid, diff) {
|
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/** The exact binary patch that turns a commit into a tree. */
|
|
107
|
+
async function diffOfTree(worktreeRoot, baseOid, treeOid) {
|
|
108
|
+
const { stdout } = await run(
|
|
109
|
+
['-c', 'core.quotepath=false', 'diff', '--binary', '--full-index', baseOid, treeOid],
|
|
110
|
+
worktreeRoot,
|
|
111
|
+
);
|
|
112
|
+
return stdout;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The exact tree currently represented by Git's index. */
|
|
116
|
+
async function indexTree(worktreeRoot) {
|
|
117
|
+
const { stdout } = await run(['write-tree'], worktreeRoot);
|
|
118
|
+
return stdout.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
/** Paths that differ between two trees, no rename detection — exact adds/deletes. */
|
|
106
122
|
async function changedPaths(worktreeRoot, treeA, treeB) {
|
|
107
123
|
const { stdout } = await run(
|
|
@@ -158,7 +174,7 @@ function isText(buffer) {
|
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
/** Content-level three-way merge; { ok, content } — never writes markers to disk. */
|
|
161
|
-
async function mergeFileContents(ours, base, theirs) {
|
|
177
|
+
async function mergeFileContents(ours, base, theirs, labels = {}) {
|
|
162
178
|
return withScratch(async (dir) => {
|
|
163
179
|
const oursFile = path.join(dir, 'ours');
|
|
164
180
|
const baseFile = path.join(dir, 'base');
|
|
@@ -168,7 +184,13 @@ async function mergeFileContents(ours, base, theirs) {
|
|
|
168
184
|
await fs.promises.writeFile(theirsFile, theirs);
|
|
169
185
|
const conflictCodes = Array.from({ length: 127 }, (_, i) => i + 1);
|
|
170
186
|
const result = await run(
|
|
171
|
-
[
|
|
187
|
+
[
|
|
188
|
+
'merge-file', '-p',
|
|
189
|
+
'-L', labels.ours || 'current',
|
|
190
|
+
'-L', labels.base || 'base',
|
|
191
|
+
'-L', labels.theirs || 'incoming',
|
|
192
|
+
oursFile, baseFile, theirsFile,
|
|
193
|
+
],
|
|
172
194
|
dir,
|
|
173
195
|
{ okCodes: conflictCodes, binary: true },
|
|
174
196
|
);
|
|
@@ -176,6 +198,98 @@ async function mergeFileContents(ours, base, theirs) {
|
|
|
176
198
|
});
|
|
177
199
|
}
|
|
178
200
|
|
|
201
|
+
function sameEntry(left, right) {
|
|
202
|
+
return left === null
|
|
203
|
+
? right === null
|
|
204
|
+
: right !== null && left.mode === right.mode && left.type === right.type && left.oid === right.oid;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function storedBlob(worktreeRoot, content, scratchDir, name) {
|
|
208
|
+
const file = path.join(scratchDir, name);
|
|
209
|
+
await fs.promises.writeFile(file, content);
|
|
210
|
+
const { stdout } = await run(['hash-object', '-w', file], worktreeRoot);
|
|
211
|
+
return stdout.trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function conflictCopyPath(relPath, label) {
|
|
215
|
+
const digest = crypto.createHash('sha256').update(`${label}\0${relPath}`).digest('hex').slice(0, 16);
|
|
216
|
+
return `.amalgm-conflicts/${digest}/${relPath}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Pure checkpoint reduction. Apply one published transition
|
|
221
|
+
* (`baseTree → targetTree`) to the currently reduced tree. No working file
|
|
222
|
+
* is read or written. Disjoint changes combine, identical changes fold, and
|
|
223
|
+
* overlaps produce the same conflict bytes and paths on every machine.
|
|
224
|
+
*/
|
|
225
|
+
async function reduceTree(worktreeRoot, currentTree, baseTree, targetTree, { label = 'rail' } = {}) {
|
|
226
|
+
const updates = [];
|
|
227
|
+
const conflicts = [];
|
|
228
|
+
|
|
229
|
+
await withScratch(async (dir) => {
|
|
230
|
+
let stored = 0;
|
|
231
|
+
for (const relPath of await changedPaths(worktreeRoot, baseTree, targetTree)) {
|
|
232
|
+
if (unsafePath(relPath)) {
|
|
233
|
+
conflicts.push({ path: relPath, kind: 'unsafe-path', label });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const base = await entryAt(worktreeRoot, baseTree, relPath);
|
|
237
|
+
const current = await entryAt(worktreeRoot, currentTree, relPath);
|
|
238
|
+
const target = await entryAt(worktreeRoot, targetTree, relPath);
|
|
239
|
+
|
|
240
|
+
if (sameEntry(current, target) || sameEntry(base, target)) continue;
|
|
241
|
+
if (sameEntry(current, base)) {
|
|
242
|
+
updates.push({ path: relPath, entry: target });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const baseBytes = base?.type === 'blob' ? await readBlob(worktreeRoot, base.oid) : null;
|
|
247
|
+
const currentBytes = current?.type === 'blob' ? await readBlob(worktreeRoot, current.oid) : null;
|
|
248
|
+
const targetBytes = target?.type === 'blob' ? await readBlob(worktreeRoot, target.oid) : null;
|
|
249
|
+
const mergeable = base && current && target
|
|
250
|
+
&& base.type === 'blob' && current.type === 'blob' && target.type === 'blob'
|
|
251
|
+
&& base.mode !== '120000' && current.mode !== '120000' && target.mode !== '120000'
|
|
252
|
+
&& isText(baseBytes) && isText(currentBytes) && isText(targetBytes);
|
|
253
|
+
|
|
254
|
+
if (mergeable) {
|
|
255
|
+
const merged = await mergeFileContents(currentBytes, baseBytes, targetBytes, {
|
|
256
|
+
ours: 'current',
|
|
257
|
+
base: 'shared-base',
|
|
258
|
+
theirs: `rail-${label}`,
|
|
259
|
+
});
|
|
260
|
+
const oid = await storedBlob(worktreeRoot, merged.content, dir, `merged-${stored++}`);
|
|
261
|
+
updates.push({ path: relPath, entry: { mode: current.mode, type: 'blob', oid } });
|
|
262
|
+
if (!merged.ok) conflicts.push({ path: relPath, kind: 'content-overlap', label });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Binary/delete/add conflicts retain the current path and, when the
|
|
267
|
+
// incoming side has bytes, preserve those bytes at one deterministic
|
|
268
|
+
// conflict path. The reducer's conflict record preserves delete intent.
|
|
269
|
+
const kind = !target
|
|
270
|
+
? 'delete-vs-change'
|
|
271
|
+
: !current
|
|
272
|
+
? 'change-vs-delete'
|
|
273
|
+
: !base
|
|
274
|
+
? 'add-add'
|
|
275
|
+
: 'binary-overlap';
|
|
276
|
+
const conflict = { path: relPath, kind, label };
|
|
277
|
+
if (targetBytes) {
|
|
278
|
+
const copyPath = conflictCopyPath(relPath, label);
|
|
279
|
+
const oid = await storedBlob(worktreeRoot, targetBytes, dir, `conflict-${stored++}`);
|
|
280
|
+
updates.push({ path: copyPath, entry: { mode: '100644', type: 'blob', oid } });
|
|
281
|
+
conflict.copyPath = copyPath;
|
|
282
|
+
}
|
|
283
|
+
conflicts.push(conflict);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
tree: updates.length ? await updateHeldTree(worktreeRoot, currentTree, updates) : currentTree,
|
|
289
|
+
conflicts,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
179
293
|
/** heldTree with the given entries replaced/removed — via a scratch index. */
|
|
180
294
|
async function updateHeldTree(worktreeRoot, heldTree, applied) {
|
|
181
295
|
return withScratch(async (dir) => {
|
|
@@ -284,7 +398,10 @@ async function advance(worktreeRoot, heldTree, targetTree, { label } = {}) {
|
|
|
284
398
|
|
|
285
399
|
module.exports = {
|
|
286
400
|
advance,
|
|
401
|
+
diffOfTree,
|
|
287
402
|
hasTree,
|
|
403
|
+
indexTree,
|
|
404
|
+
reduceTree,
|
|
288
405
|
treeOf,
|
|
289
406
|
treeOfPatch,
|
|
290
407
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A card names one content-addressed bundle when origin cannot supply its
|
|
7
7
|
* commit. The bundle is split into fixed-size opaque chunks; the tunnel
|
|
8
|
-
* stores and retrieves those chunks under the
|
|
8
|
+
* stores and retrieves those chunks under the authorized code project. Chunking
|
|
9
9
|
* changes transport size, never the two-value repo model.
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -63,6 +63,7 @@ function writeJson(file, value) {
|
|
|
63
63
|
function publicLocator(manifest) {
|
|
64
64
|
return {
|
|
65
65
|
contract: CONTRACT,
|
|
66
|
+
resourceId: manifest.resourceId,
|
|
66
67
|
oid: manifest.oid,
|
|
67
68
|
ref: manifest.ref,
|
|
68
69
|
checksum: manifest.checksum,
|
|
@@ -75,6 +76,7 @@ function publicPayloadLocator(manifest) {
|
|
|
75
76
|
return {
|
|
76
77
|
kind: 'chunks',
|
|
77
78
|
contract: PAYLOAD_CONTRACT,
|
|
79
|
+
resourceId: manifest.resourceId,
|
|
78
80
|
checksum: manifest.checksum,
|
|
79
81
|
bytes: manifest.bytes,
|
|
80
82
|
chunks: manifest.chunks.map(({ sha256: digest, bytes }) => ({ sha256: digest, bytes })),
|
|
@@ -97,7 +99,7 @@ function wake(op, id) {
|
|
|
97
99
|
appendStateEvent({ resource: 'repo-objects', op, id, source: 'repo-objects' });
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
async function prepareBundle(worktreeRoot, repoKey, oid, file) {
|
|
102
|
+
async function prepareBundle(worktreeRoot, repoKey, resourceId, oid, file) {
|
|
101
103
|
ensureDirs();
|
|
102
104
|
const bundleFile = path.join(ROOT, `${crypto.randomBytes(12).toString('hex')}.bundle`);
|
|
103
105
|
const ref = `refs/amalgm/exports/${oid}`;
|
|
@@ -107,6 +109,7 @@ async function prepareBundle(worktreeRoot, repoKey, oid, file) {
|
|
|
107
109
|
const chunks = writeChunks(bundle);
|
|
108
110
|
writeJson(file, {
|
|
109
111
|
contract: CONTRACT,
|
|
112
|
+
resourceId,
|
|
110
113
|
repoKey,
|
|
111
114
|
oid,
|
|
112
115
|
ref,
|
|
@@ -121,15 +124,21 @@ async function prepareBundle(worktreeRoot, repoKey, oid, file) {
|
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
function publishPayload(key, bytesInput) {
|
|
127
|
+
function publishPayload(key, bytesInput, repoKey) {
|
|
125
128
|
ensureDirs();
|
|
126
129
|
const bytes = Buffer.isBuffer(bytesInput) ? bytesInput : Buffer.from(bytesInput);
|
|
127
130
|
const file = payloadFile(key);
|
|
128
131
|
let manifest = readJson(file);
|
|
129
132
|
const checksum = sha256(bytes);
|
|
130
133
|
if (!manifest || manifest.checksum !== checksum) {
|
|
134
|
+
const project = require('./wall').projectForRepo(repoKey);
|
|
135
|
+
if (!project?.resourceId) {
|
|
136
|
+
return { kind: 'pending', contract: PAYLOAD_CONTRACT, checksum };
|
|
137
|
+
}
|
|
131
138
|
manifest = {
|
|
132
139
|
contract: PAYLOAD_CONTRACT,
|
|
140
|
+
resourceId: project.resourceId,
|
|
141
|
+
repoKey,
|
|
133
142
|
checksum,
|
|
134
143
|
bytes: bytes.length,
|
|
135
144
|
chunks: writeChunks(bytes),
|
|
@@ -143,10 +152,8 @@ function publishPayload(key, bytesInput) {
|
|
|
143
152
|
: { kind: 'pending', contract: PAYLOAD_CONTRACT, checksum };
|
|
144
153
|
}
|
|
145
154
|
|
|
146
|
-
async function
|
|
155
|
+
async function bundleSourceFor(worktreeRoot, repoKey, oid) {
|
|
147
156
|
if (!oid || !repoKey) return null;
|
|
148
|
-
if (await originContains(worktreeRoot, oid)) return { kind: 'remote' };
|
|
149
|
-
|
|
150
157
|
const file = manifestFile(repoKey, oid);
|
|
151
158
|
const manifest = readJson(file);
|
|
152
159
|
if (manifest?.contract === CONTRACT && manifest.oid === oid) {
|
|
@@ -155,8 +162,10 @@ async function objectSourceFor(worktreeRoot, repoKey, oid) {
|
|
|
155
162
|
: { kind: 'pending' };
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
const project = require('./wall').projectForRepo(repoKey);
|
|
166
|
+
if (!project?.resourceId) return { kind: 'pending' };
|
|
158
167
|
if (!preparing.has(file)) {
|
|
159
|
-
const promise = prepareBundle(worktreeRoot, repoKey, oid, file)
|
|
168
|
+
const promise = prepareBundle(worktreeRoot, repoKey, project.resourceId, oid, file)
|
|
160
169
|
.catch((error) => console.warn(`[RepoObjects] bundle ${oid} failed:`, error.message))
|
|
161
170
|
.finally(() => {
|
|
162
171
|
preparing.delete(file);
|
|
@@ -168,6 +177,12 @@ async function objectSourceFor(worktreeRoot, repoKey, oid) {
|
|
|
168
177
|
return { kind: 'pending' };
|
|
169
178
|
}
|
|
170
179
|
|
|
180
|
+
async function objectSourceFor(worktreeRoot, repoKey, oid) {
|
|
181
|
+
if (!oid || !repoKey) return null;
|
|
182
|
+
if (await originContains(worktreeRoot, oid)) return { kind: 'remote' };
|
|
183
|
+
return bundleSourceFor(worktreeRoot, repoKey, oid);
|
|
184
|
+
}
|
|
185
|
+
|
|
171
186
|
function claimUpload() {
|
|
172
187
|
ensureDirs();
|
|
173
188
|
for (const directory of [MANIFESTS, PAYLOADS]) {
|
|
@@ -176,13 +191,18 @@ function claimUpload() {
|
|
|
176
191
|
const chunk = manifest?.chunks?.find((entry) => !entry.uploaded);
|
|
177
192
|
if (!chunk) continue;
|
|
178
193
|
const bytes = fs.readFileSync(path.join(CHUNKS, `${chunk.sha256}.bin`));
|
|
179
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
resourceId: manifest.resourceId,
|
|
196
|
+
sha256: chunk.sha256,
|
|
197
|
+
bytes: bytes.length,
|
|
198
|
+
dataBase64: bytes.toString('base64'),
|
|
199
|
+
};
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
return null;
|
|
183
203
|
}
|
|
184
204
|
|
|
185
|
-
function acknowledgeUpload(digest) {
|
|
205
|
+
function acknowledgeUpload(resourceId, digest) {
|
|
186
206
|
if (!/^[0-9a-f]{64}$/.test(String(digest || ''))) return { changed: false };
|
|
187
207
|
ensureDirs();
|
|
188
208
|
let changed = false;
|
|
@@ -191,7 +211,7 @@ function acknowledgeUpload(digest) {
|
|
|
191
211
|
for (const name of fs.readdirSync(directory)) {
|
|
192
212
|
const file = path.join(directory, name);
|
|
193
213
|
const manifest = readJson(file);
|
|
194
|
-
if (!manifest?.chunks) continue;
|
|
214
|
+
if (manifest?.resourceId !== resourceId || !manifest?.chunks) continue;
|
|
195
215
|
let touched = false;
|
|
196
216
|
for (const chunk of manifest.chunks) {
|
|
197
217
|
if (chunk.sha256 === digest && !chunk.uploaded) {
|
|
@@ -220,6 +240,7 @@ function validatePayloadLocator(locator) {
|
|
|
220
240
|
if (
|
|
221
241
|
locator?.kind !== 'chunks'
|
|
222
242
|
|| locator?.contract !== PAYLOAD_CONTRACT
|
|
243
|
+
|| !/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(locator.resourceId || ''))
|
|
223
244
|
|| !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
|
|
224
245
|
|| !Number.isSafeInteger(locator.bytes)
|
|
225
246
|
|| locator.bytes < 0
|
|
@@ -240,7 +261,10 @@ function readPayload(locator) {
|
|
|
240
261
|
for (const chunk of locator.chunks) {
|
|
241
262
|
const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
|
|
242
263
|
if (fs.existsSync(file) && fs.statSync(file).size === chunk.bytes) continue;
|
|
243
|
-
writeJson(requestFile(chunk.sha256), {
|
|
264
|
+
writeJson(requestFile(locator.resourceId, chunk.sha256), {
|
|
265
|
+
resourceId: locator.resourceId,
|
|
266
|
+
sha256: chunk.sha256,
|
|
267
|
+
});
|
|
244
268
|
waiting = true;
|
|
245
269
|
}
|
|
246
270
|
if (waiting) {
|
|
@@ -261,8 +285,8 @@ function readPayload(locator) {
|
|
|
261
285
|
return { state: 'ready', bytes };
|
|
262
286
|
}
|
|
263
287
|
|
|
264
|
-
function requestFile(digest) {
|
|
265
|
-
return path.join(REQUESTS, `${digest}.json`);
|
|
288
|
+
function requestFile(resourceId, digest) {
|
|
289
|
+
return path.join(REQUESTS, `${sha256(Buffer.from(`${resourceId}\n${digest}`))}.json`);
|
|
266
290
|
}
|
|
267
291
|
|
|
268
292
|
function claimDownload() {
|
|
@@ -273,21 +297,25 @@ function claimDownload() {
|
|
|
273
297
|
return request?.sha256 ? request : null;
|
|
274
298
|
}
|
|
275
299
|
|
|
276
|
-
function receiveDownload(digest, dataBase64) {
|
|
300
|
+
function receiveDownload(resourceId, digest, dataBase64) {
|
|
301
|
+
if (!/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(resourceId || ''))) {
|
|
302
|
+
throw new Error('invalid repo object project');
|
|
303
|
+
}
|
|
277
304
|
if (!/^[0-9a-f]{64}$/.test(String(digest || ''))) throw new Error('invalid repo object checksum');
|
|
278
305
|
ensureDirs();
|
|
279
306
|
const bytes = Buffer.from(String(dataBase64 || ''), 'base64');
|
|
280
307
|
if (sha256(bytes) !== digest) throw new Error('repo object checksum mismatch');
|
|
281
308
|
fs.writeFileSync(path.join(CHUNKS, `${digest}.bin`), bytes, { mode: 0o600 });
|
|
282
|
-
fs.rmSync(requestFile(digest), { force: true });
|
|
309
|
+
fs.rmSync(requestFile(resourceId, digest), { force: true });
|
|
283
310
|
wake('update', digest);
|
|
284
|
-
require('./
|
|
311
|
+
require('./reducer').reduceNow().catch(() => {});
|
|
285
312
|
return { stored: true, sha256: digest, bytes: bytes.length };
|
|
286
313
|
}
|
|
287
314
|
|
|
288
315
|
function validateLocator(locator) {
|
|
289
316
|
if (
|
|
290
317
|
locator?.contract !== CONTRACT
|
|
318
|
+
|| !/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(locator.resourceId || ''))
|
|
291
319
|
|| !/^[0-9a-f]{40,64}$/.test(String(locator.oid || ''))
|
|
292
320
|
|| !/^[0-9a-f]{64}$/.test(String(locator.checksum || ''))
|
|
293
321
|
|| !Array.isArray(locator.chunks)
|
|
@@ -307,7 +335,10 @@ async function importFromLocator(worktreeRoot, locator) {
|
|
|
307
335
|
for (const chunk of locator.chunks) {
|
|
308
336
|
const file = path.join(CHUNKS, `${chunk.sha256}.bin`);
|
|
309
337
|
if (fs.existsSync(file) && fs.statSync(file).size === chunk.bytes) continue;
|
|
310
|
-
writeJson(requestFile(chunk.sha256), {
|
|
338
|
+
writeJson(requestFile(locator.resourceId, chunk.sha256), {
|
|
339
|
+
resourceId: locator.resourceId,
|
|
340
|
+
sha256: chunk.sha256,
|
|
341
|
+
});
|
|
311
342
|
waiting = true;
|
|
312
343
|
}
|
|
313
344
|
if (waiting) {
|
|
@@ -339,6 +370,7 @@ module.exports = {
|
|
|
339
370
|
CONTRACT,
|
|
340
371
|
PAYLOAD_CONTRACT,
|
|
341
372
|
acknowledgeUpload,
|
|
373
|
+
bundleSourceFor,
|
|
342
374
|
claimDownload,
|
|
343
375
|
claimUpload,
|
|
344
376
|
importFromLocator,
|