amalgm 0.1.254 → 0.1.256
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +27 -10
- package/runtime/scripts/amalgm-mcp/registration/service.js +88 -22
- package/runtime/scripts/amalgm-mcp/registry/index.js +61 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +57 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +7 -4
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +21 -14
- package/runtime/scripts/amalgm-mcp/tests/entity-content.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +92 -2
package/package.json
CHANGED
|
@@ -424,14 +424,23 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Create the first private entity-resource snapshot from the *already
|
|
429
|
+
* verified* bootstrap graph. This deliberately accepts records rather than
|
|
430
|
+
* a registry: login owns the declared user-home graph, but an older device
|
|
431
|
+
* can also hold unrelated local registrations which have never had cloud
|
|
432
|
+
* authority. Publishing those as an implicit local-wins merge would make
|
|
433
|
+
* sign-in a hidden `register` command.
|
|
434
|
+
*/
|
|
435
|
+
function bootstrap(records) {
|
|
428
436
|
const currentIdentity = identity();
|
|
429
437
|
if (!currentIdentity.enabled) return currentIdentity;
|
|
430
438
|
const existing = replica();
|
|
431
439
|
if (existing?.state === 'active') {
|
|
432
440
|
throw new Error('entity cloud registry already exists locally; cloud lookup must not create it again');
|
|
433
441
|
}
|
|
434
|
-
|
|
442
|
+
if (!Array.isArray(records)) throw new Error('entity cloud bootstrap requires verified records');
|
|
443
|
+
const snapshot = cloudSnapshot(records);
|
|
435
444
|
const cutoff = db.prepare('SELECT COALESCE(MAX(sequence), 0) AS sequence FROM entity_cloud_mutations').get().sequence;
|
|
436
445
|
saveReplica({
|
|
437
446
|
resourceId: currentIdentity.resourceId,
|
|
@@ -471,7 +480,6 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
471
480
|
|| !Number.isInteger(Number(headVersion)) || Number(headVersion) < 0) {
|
|
472
481
|
throw new Error('entity cloud registry authority is invalid');
|
|
473
482
|
}
|
|
474
|
-
const local = cloudSnapshot(registry.syncRecords());
|
|
475
483
|
const current = replica();
|
|
476
484
|
const bootstrapping = current?.state === 'bootstrapping';
|
|
477
485
|
const reconciling = current?.state === BOOTSTRAP_RECONCILIATION_STATE;
|
|
@@ -486,7 +494,12 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
486
494
|
// exactly the already-verified declared user home. This is not a general
|
|
487
495
|
// local-wins merge: user-selected ground remains refused until an active
|
|
488
496
|
// authoritative snapshot exists.
|
|
489
|
-
|
|
497
|
+
// The first entity resource contains only declared user-home truth. An
|
|
498
|
+
// old installation may also have local, pre-cloud user ground; that is
|
|
499
|
+
// deliberately excluded here and will join only through an explicit
|
|
500
|
+
// later `amalgm register`. The empty cloud resource is therefore repaired
|
|
501
|
+
// from the trusted bootstrap records, never from the whole local registry.
|
|
502
|
+
if (emptyCloudGraph && trustedBootstrap && (!current || bootstrapping)) {
|
|
490
503
|
const next = {
|
|
491
504
|
resourceId,
|
|
492
505
|
deviceId: currentIdentity.deviceId,
|
|
@@ -515,8 +528,8 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
515
528
|
}
|
|
516
529
|
|
|
517
530
|
if (reconciling) {
|
|
518
|
-
if (!trustedBootstrap
|
|
519
|
-
throw new Error('entity bootstrap reconciliation
|
|
531
|
+
if (!trustedBootstrap) {
|
|
532
|
+
throw new Error('entity bootstrap reconciliation requires the verified user-home graph');
|
|
520
533
|
}
|
|
521
534
|
if (current.resourceId !== resourceId || Number(authorityEpoch) !== current.authorityEpoch
|
|
522
535
|
|| Number(headVersion) < current.headVersion || !recordsAreSubset(snapshot.records, trustedBootstrap)) {
|
|
@@ -547,13 +560,17 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
|
|
|
547
560
|
reconciling: !complete,
|
|
548
561
|
};
|
|
549
562
|
}
|
|
550
|
-
if (bootstrapping
|
|
551
|
-
|
|
563
|
+
if (bootstrapping) {
|
|
564
|
+
const expected = trustedBootstrap ?? cloudSnapshot(registry.syncRecords()).records;
|
|
565
|
+
if (!sameRecords(expected, snapshot.records)) {
|
|
566
|
+
throw new Error('first cloud entity snapshot does not match the locally bootstrapped identity graph');
|
|
567
|
+
}
|
|
552
568
|
}
|
|
553
569
|
if (!bootstrapping) {
|
|
554
570
|
const cloudByUuid = new Map(snapshot.records.map((record) => [record.uuid, record]));
|
|
555
|
-
for (const
|
|
556
|
-
|
|
571
|
+
for (const local of registry.syncRecords()) {
|
|
572
|
+
const remote = cloudByUuid.get(local.uuid);
|
|
573
|
+
if (remote && !sameRecord(cloudRecord(local), remote)) {
|
|
557
574
|
throw new Error('cloud entity graph conflicts with locally bound ground; resolve the local mutation before importing a new cloud head');
|
|
558
575
|
}
|
|
559
576
|
}
|
|
@@ -230,6 +230,24 @@ function createRegistrationService({
|
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/** A bootstrap rekey is safe only before this installation has accepted
|
|
234
|
+
* ordinary entity-cloud state. After that point a UUID graph has an
|
|
235
|
+
* independent authority and an operator must resolve any disagreement
|
|
236
|
+
* explicitly; a login handshake never chooses a winner for it. */
|
|
237
|
+
function assertBootstrapRekeyAvailable() {
|
|
238
|
+
const state = identityDb.prepare(`
|
|
239
|
+
SELECT
|
|
240
|
+
(SELECT COUNT(*) FROM entity_cloud_replica) AS replicas,
|
|
241
|
+
(SELECT COUNT(*) FROM entity_cloud_mutations) AS mutations,
|
|
242
|
+
(SELECT COUNT(*) FROM entity_cloud_catalog_state) AS catalog_states,
|
|
243
|
+
(SELECT COUNT(*) FROM entity_cloud_catalog) AS catalog_records,
|
|
244
|
+
(SELECT COUNT(*) FROM entity_materializations) AS materializations
|
|
245
|
+
`).get();
|
|
246
|
+
if (Object.values(state).some((count) => Number(count) !== 0)) {
|
|
247
|
+
throw new Error('cannot adopt cloud user-home UUIDs after ordinary entity-cloud state exists');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
233
251
|
/**
|
|
234
252
|
* Bootstrap a logged-in person's declared Amalgm home through the SAME
|
|
235
253
|
* operation as `amalgm register`. `ensureUserHome` only creates the safe shape;
|
|
@@ -243,23 +261,18 @@ function createRegistrationService({
|
|
|
243
261
|
async function bootstrapUserHome({ identitySnapshot = null } = {}) {
|
|
244
262
|
const declaration = ensureUserHome(homeDir);
|
|
245
263
|
const seeds = identitySeeds(identitySnapshot, declaration);
|
|
246
|
-
const roots = [];
|
|
247
264
|
const addedRoots = boundary.current.addMany(declaration.roots.map((entry) => ({
|
|
248
265
|
targetPath: entry.path,
|
|
249
266
|
identityAt: seeds?.forRoot(entry.key) ?? null,
|
|
250
267
|
})));
|
|
251
|
-
|
|
252
|
-
const entry = declaration.roots[index];
|
|
268
|
+
let roots = declaration.roots.map((entry, index) => {
|
|
253
269
|
const added = addedRoots[index];
|
|
254
|
-
|
|
255
|
-
throw new Error(`local user-home root ${entry.key} does not match its cloud UUID`);
|
|
256
|
-
}
|
|
257
|
-
roots.push({
|
|
270
|
+
return {
|
|
258
271
|
key: entry.key,
|
|
259
272
|
path: added.rootPath,
|
|
260
273
|
record: added.record,
|
|
261
|
-
}
|
|
262
|
-
}
|
|
274
|
+
};
|
|
275
|
+
});
|
|
263
276
|
|
|
264
277
|
const system = roots.find((entry) => entry.key === 'system');
|
|
265
278
|
if (!system) throw new Error('declared user home has no system root');
|
|
@@ -275,15 +288,12 @@ function createRegistrationService({
|
|
|
275
288
|
return record;
|
|
276
289
|
};
|
|
277
290
|
|
|
278
|
-
|
|
291
|
+
let exactManaged = declaration.exactManaged.map((entry) => {
|
|
279
292
|
const root = roots.find((candidate) => candidate.key === entry.rootKey);
|
|
280
293
|
const isRoot = entry.relativePath === entry.rootKey;
|
|
281
294
|
const relativePath = isRoot ? '' : entry.relativePath.slice(entry.rootKey.length + 1);
|
|
282
295
|
const record = isRoot ? root?.record : recordAt(relativePath);
|
|
283
296
|
if (!record) throw new Error(`declared user-home managed target was not registered: ${entry.relativePath}`);
|
|
284
|
-
if (seeds && record.uuid !== seeds.managed(entry.key).uuid) {
|
|
285
|
-
throw new Error(`local user-home managed target ${entry.key} does not match its cloud UUID`);
|
|
286
|
-
}
|
|
287
297
|
return {
|
|
288
298
|
key: entry.key,
|
|
289
299
|
rootKey: entry.rootKey,
|
|
@@ -294,6 +304,65 @@ function createRegistrationService({
|
|
|
294
304
|
};
|
|
295
305
|
});
|
|
296
306
|
|
|
307
|
+
if (seeds) {
|
|
308
|
+
const bindings = [
|
|
309
|
+
...roots.map((entry) => ({
|
|
310
|
+
key: entry.key,
|
|
311
|
+
local: entry.record,
|
|
312
|
+
authoritative: seeds.root(entry.key),
|
|
313
|
+
})),
|
|
314
|
+
...exactManaged.map((entry) => ({
|
|
315
|
+
key: entry.key,
|
|
316
|
+
local: entry.record,
|
|
317
|
+
authoritative: seeds.managed(entry.key),
|
|
318
|
+
})),
|
|
319
|
+
];
|
|
320
|
+
for (const binding of bindings) {
|
|
321
|
+
const remote = binding.authoritative;
|
|
322
|
+
const local = binding.local;
|
|
323
|
+
if (!remote || remote.type !== local.type || remote.name !== local.name || remote.status !== local.status) {
|
|
324
|
+
throw new Error(`cloud user-home node ${binding.key} does not match its declared local shape`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (bindings.some(({ local, authoritative }) => local.uuid !== authoritative.uuid)) {
|
|
328
|
+
assertBootstrapRekeyAvailable();
|
|
329
|
+
// The six roots appear once more in `exactManaged` to name their
|
|
330
|
+
// governance. They are the same nodes, not a second rekey.
|
|
331
|
+
const uniqueBindings = [...new Map(bindings.map((binding) => [binding.local.uuid, binding])).values()];
|
|
332
|
+
boundary.current.registry.rebindBootstrapIdentities(uniqueBindings.map(({ local, authoritative }) => ({
|
|
333
|
+
oldUUID: local.uuid,
|
|
334
|
+
newUUID: authoritative.uuid,
|
|
335
|
+
newParentUUID: authoritative.parentUUID,
|
|
336
|
+
type: authoritative.type,
|
|
337
|
+
name: authoritative.name,
|
|
338
|
+
})));
|
|
339
|
+
roots = roots.map((entry) => ({
|
|
340
|
+
...entry,
|
|
341
|
+
record: boundary.current.registry.syncRecord(seeds.root(entry.key).uuid),
|
|
342
|
+
}));
|
|
343
|
+
const reboundSystem = roots.find((entry) => entry.key === 'system');
|
|
344
|
+
const reboundTree = boundary.current.tree(reboundSystem.path);
|
|
345
|
+
if (!reboundTree?.root()) throw new Error('cloud user-home rekey lost the declared system root');
|
|
346
|
+
exactManaged = exactManaged.map((entry) => {
|
|
347
|
+
const remote = seeds.managed(entry.key);
|
|
348
|
+
return {
|
|
349
|
+
...entry,
|
|
350
|
+
record: boundary.current.registry.syncRecord(remote.uuid),
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (const entry of roots) {
|
|
355
|
+
if (entry.record.uuid !== seeds.root(entry.key).uuid) {
|
|
356
|
+
throw new Error(`local user-home root ${entry.key} does not match its cloud UUID`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const entry of exactManaged) {
|
|
360
|
+
if (entry.record.uuid !== seeds.managed(entry.key).uuid) {
|
|
361
|
+
throw new Error(`local user-home managed target ${entry.key} does not match its cloud UUID`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
297
366
|
const cloudSnapshot = {
|
|
298
367
|
schemaVersion: USER_HOME_CLOUD_SCHEMA_VERSION,
|
|
299
368
|
kind: 'amalgm-user-home',
|
|
@@ -312,10 +381,10 @@ function createRegistrationService({
|
|
|
312
381
|
};
|
|
313
382
|
}
|
|
314
383
|
|
|
315
|
-
/** The only local graph
|
|
316
|
-
* the exact home that the user-home handshake has already verified.
|
|
317
|
-
*
|
|
318
|
-
*
|
|
384
|
+
/** The only local graph eligible to repair an old empty entity resource is
|
|
385
|
+
* the exact home that the user-home handshake has already verified. Extra
|
|
386
|
+
* local entities are deliberately excluded rather than treated as a login
|
|
387
|
+
* merge: they become cloud-backed only through a later explicit register. */
|
|
319
388
|
async function exactUserHomeCloudRecords() {
|
|
320
389
|
const home = await bootstrapUserHome();
|
|
321
390
|
const expected = new Map();
|
|
@@ -326,12 +395,9 @@ function createRegistrationService({
|
|
|
326
395
|
// containers, with the registry's sync representation.
|
|
327
396
|
expected.set(entry.record.uuid, boundary.current.registry.syncRecord(entry.record.uuid));
|
|
328
397
|
}
|
|
329
|
-
const records =
|
|
330
|
-
if (records.length !== expected.size) {
|
|
331
|
-
throw new Error('local entity graph contains ground outside the declared user home');
|
|
332
|
-
}
|
|
398
|
+
const records = [...expected.values()];
|
|
333
399
|
for (const record of records) {
|
|
334
|
-
const declared =
|
|
400
|
+
const declared = boundary.current.registry.syncRecord(record.uuid);
|
|
335
401
|
if (!declared || ![
|
|
336
402
|
'uuid', 'type', 'parentUUID', 'name', 'status', 'version', 'payloadVersion',
|
|
337
403
|
].every((field) => record[field] === declared[field])) {
|
|
@@ -684,6 +684,67 @@ function createRegistry(options) {
|
|
|
684
684
|
});
|
|
685
685
|
},
|
|
686
686
|
|
|
687
|
+
/**
|
|
688
|
+
* First cloud bootstrap may meet a pre-cloud local safe home. Its six
|
|
689
|
+
* roots and three exact managed nodes are platform truth, so they may
|
|
690
|
+
* adopt the authenticated cloud UUIDs without changing any user-owned
|
|
691
|
+
* descendant identity. This is one atomic graph rekey, never a delete or
|
|
692
|
+
* local-wins merge, and callers must prove that no ordinary entity-cloud
|
|
693
|
+
* history has begun before asking for it.
|
|
694
|
+
*/
|
|
695
|
+
rebindBootstrapIdentities(bindings) {
|
|
696
|
+
if (!Array.isArray(bindings) || bindings.length === 0) {
|
|
697
|
+
throw new Error('bootstrap identity rebind requires one or more bindings');
|
|
698
|
+
}
|
|
699
|
+
return transaction(() => {
|
|
700
|
+
const byOld = new Map();
|
|
701
|
+
const byNew = new Map();
|
|
702
|
+
const mappings = bindings.map((binding) => {
|
|
703
|
+
if (!binding || typeof binding.oldUUID !== 'string' || typeof binding.newUUID !== 'string'
|
|
704
|
+
|| !UUID.test(binding.oldUUID) || !UUID.test(binding.newUUID)
|
|
705
|
+
|| (binding.newParentUUID !== null && (!UUID.test(binding.newParentUUID)))) {
|
|
706
|
+
throw new Error('bootstrap identity rebind has an invalid UUID binding');
|
|
707
|
+
}
|
|
708
|
+
const oldUuid = binding.oldUUID.toLowerCase();
|
|
709
|
+
const newUuid = binding.newUUID.toLowerCase();
|
|
710
|
+
if (byOld.has(oldUuid) || byNew.has(newUuid)) {
|
|
711
|
+
throw new Error('bootstrap identity rebind repeats an identity');
|
|
712
|
+
}
|
|
713
|
+
const row = mustGet(oldUuid);
|
|
714
|
+
if (row.status !== 'active' || row.type !== binding.type || row.name !== binding.name) {
|
|
715
|
+
throw new Error(`bootstrap identity ${oldUuid} does not match its declared local node`);
|
|
716
|
+
}
|
|
717
|
+
if (newUuid !== oldUuid && store.byUuid(newUuid)) {
|
|
718
|
+
throw new Error(`bootstrap identity ${newUuid} is already registered locally`);
|
|
719
|
+
}
|
|
720
|
+
const mapping = {
|
|
721
|
+
oldUuid,
|
|
722
|
+
newUuid,
|
|
723
|
+
newParentUuid: binding.newParentUUID === null ? null : binding.newParentUUID.toLowerCase(),
|
|
724
|
+
type: row.type,
|
|
725
|
+
name: row.name,
|
|
726
|
+
status: row.status,
|
|
727
|
+
payloadVersion: row.payloadVersion,
|
|
728
|
+
oldParentUuid: row.parentUuid,
|
|
729
|
+
};
|
|
730
|
+
byOld.set(oldUuid, mapping);
|
|
731
|
+
byNew.set(newUuid, mapping);
|
|
732
|
+
return mapping;
|
|
733
|
+
});
|
|
734
|
+
for (const mapping of mappings) {
|
|
735
|
+
const translatedParent = mapping.oldParentUuid === null
|
|
736
|
+
? null
|
|
737
|
+
: (byOld.get(mapping.oldParentUuid)?.newUuid ?? mapping.oldParentUuid);
|
|
738
|
+
if (translatedParent !== mapping.newParentUuid) {
|
|
739
|
+
throw new Error(`bootstrap identity ${mapping.oldUuid} has a different authoritative parent`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
store.rebindIdentities(mappings);
|
|
743
|
+
membershipHashes.clear();
|
|
744
|
+
return mappings.map((mapping) => syncRecord(mustGet(mapping.newUuid)));
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
|
|
687
748
|
record: (uuid) => record(mustGet(uuid)),
|
|
688
749
|
syncRecord: (uuid) => syncRecord(mustGet(uuid)),
|
|
689
750
|
/** Full logical graph for cloud snapshots. Machine bindings and evidence
|
|
@@ -300,6 +300,63 @@ function createStore(db) {
|
|
|
300
300
|
WHERE uuid = ?
|
|
301
301
|
`).run(type, parentUuid, name, status, payloadVersion, uuid);
|
|
302
302
|
},
|
|
303
|
+
/**
|
|
304
|
+
* Rekey a small, already-validated identity set in one invisible SQLite
|
|
305
|
+
* transition. This is deliberately a store primitive, not a generic
|
|
306
|
+
* public CRUD verb: trusted bootstrap migration is the only caller.
|
|
307
|
+
*
|
|
308
|
+
* The old row is retired just long enough to free its active slot, a new
|
|
309
|
+
* row is born with the authoritative UUID, every direct child follows the
|
|
310
|
+
* new parent, then the retired row is removed. Nothing outside the
|
|
311
|
+
* transaction can observe the temporary state. UUID references held by
|
|
312
|
+
* local evidence, Git state, and managed references follow as well.
|
|
313
|
+
*/
|
|
314
|
+
rebindIdentities(mappings) {
|
|
315
|
+
const retired = mappings.filter((mapping) => mapping.oldUuid !== mapping.newUuid);
|
|
316
|
+
const retire = db.prepare("UPDATE entities SET status = 'trashed' WHERE uuid = ?");
|
|
317
|
+
const insert = db.prepare(`
|
|
318
|
+
INSERT INTO entities (uuid, type, parent_uuid, name, status, payload_version)
|
|
319
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
320
|
+
`);
|
|
321
|
+
const reparent = db.prepare('UPDATE entities SET parent_uuid = ? WHERE parent_uuid = ?');
|
|
322
|
+
const rebindChannels = db.prepare('UPDATE channel_bindings SET entity_uuid = ? WHERE entity_uuid = ?');
|
|
323
|
+
const rebindClaims = db.prepare('UPDATE contested_ground SET claimant_uuid = ? WHERE claimant_uuid = ?');
|
|
324
|
+
const rebindRepoState = db.prepare('UPDATE repo_states SET entity_uuid = ? WHERE entity_uuid = ?');
|
|
325
|
+
const rebindReference = db.prepare("UPDATE entities SET payload_version = ? WHERE type = 'reference' AND payload_version = ?");
|
|
326
|
+
const remove = db.prepare('DELETE FROM entities WHERE uuid = ?');
|
|
327
|
+
|
|
328
|
+
db.transaction(() => {
|
|
329
|
+
for (const mapping of retired) retire.run(mapping.oldUuid);
|
|
330
|
+
|
|
331
|
+
const remaining = new Map(retired.map((mapping) => [mapping.newUuid, mapping]));
|
|
332
|
+
while (remaining.size > 0) {
|
|
333
|
+
let inserted = false;
|
|
334
|
+
for (const mapping of [...remaining.values()]) {
|
|
335
|
+
if (remaining.has(mapping.newParentUuid)) continue;
|
|
336
|
+
insert.run(
|
|
337
|
+
mapping.newUuid,
|
|
338
|
+
mapping.type,
|
|
339
|
+
mapping.newParentUuid,
|
|
340
|
+
mapping.name,
|
|
341
|
+
mapping.status,
|
|
342
|
+
mapping.payloadVersion,
|
|
343
|
+
);
|
|
344
|
+
remaining.delete(mapping.newUuid);
|
|
345
|
+
inserted = true;
|
|
346
|
+
}
|
|
347
|
+
if (!inserted) throw new Error('identity rebind has a cyclic authoritative parent map');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const mapping of retired) reparent.run(mapping.newUuid, mapping.oldUuid);
|
|
351
|
+
for (const mapping of retired) {
|
|
352
|
+
rebindChannels.run(mapping.newUuid, mapping.oldUuid);
|
|
353
|
+
rebindClaims.run(mapping.newUuid, mapping.oldUuid);
|
|
354
|
+
rebindRepoState.run(mapping.newUuid, mapping.oldUuid);
|
|
355
|
+
rebindReference.run(mapping.newUuid, mapping.oldUuid);
|
|
356
|
+
}
|
|
357
|
+
for (const mapping of retired) remove.run(mapping.oldUuid);
|
|
358
|
+
})();
|
|
359
|
+
},
|
|
303
360
|
/**
|
|
304
361
|
* One address permutation as one write. Movers may hold each other's
|
|
305
362
|
* slots, and the live unique index cannot be threaded one row at a
|
|
@@ -135,9 +135,11 @@ async function handleEntityRoutes(ctx) {
|
|
|
135
135
|
return true;
|
|
136
136
|
}
|
|
137
137
|
// A missing entity resource is first created from the same declared
|
|
138
|
-
// user-home graph that login owns. This is not an alternate registrar
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// user-home graph that login owns. This is not an alternate registrar:
|
|
139
|
+
// pre-cloud user registrations are deliberately excluded until their
|
|
140
|
+
// caller explicitly registers them after the bootstrap head is active.
|
|
141
|
+
const bootstrapRecords = await service.exactUserHomeCloudRecords();
|
|
142
|
+
ctx.sendJson(200, service.entityCloud.bootstrap(bootstrapRecords));
|
|
141
143
|
return true;
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -147,7 +149,8 @@ async function handleEntityRoutes(ctx) {
|
|
|
147
149
|
const service = registrationService();
|
|
148
150
|
try {
|
|
149
151
|
const existing = service.entityCloud.replica();
|
|
150
|
-
let requiresBootstrapProof = existing?.state === '
|
|
152
|
+
let requiresBootstrapProof = existing?.state === 'bootstrapping'
|
|
153
|
+
|| existing?.state === 'reconciling-bootstrap';
|
|
151
154
|
try {
|
|
152
155
|
const snapshot = JSON.parse(Buffer.from(String(body?.snapshotBase64 || ''), 'base64').toString('utf8'));
|
|
153
156
|
requiresBootstrapProof ||= Number(body?.headVersion) === 0
|
|
@@ -25,7 +25,7 @@ test('the entity cloud snapshot establishes identity, then each later entity com
|
|
|
25
25
|
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
|
|
26
26
|
'initial bootstrap identity is carried by the first immutable snapshot, not an unscoped tail');
|
|
27
27
|
|
|
28
|
-
const bootstrap = cloud.bootstrap(registry);
|
|
28
|
+
const bootstrap = cloud.bootstrap(registry.syncRecords());
|
|
29
29
|
const snapshot = parseSnapshot(Buffer.from(bootstrap.snapshotBase64, 'base64'));
|
|
30
30
|
assert.deepEqual(snapshot.records.map((record) => record.uuid), [root.uuid]);
|
|
31
31
|
cloud.install({
|
|
@@ -64,7 +64,7 @@ test('a signed-in registration is refused until the cloud entity resource is act
|
|
|
64
64
|
const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
|
|
65
65
|
assert.throws(() => cloud.assertRegistrationReady(), /entity cloud registry is not ready/);
|
|
66
66
|
registry.enrollRoot({ type: 'workspace', name: 'bootstrap-only' });
|
|
67
|
-
const bootstrap = cloud.bootstrap(registry);
|
|
67
|
+
const bootstrap = cloud.bootstrap(registry.syncRecords());
|
|
68
68
|
cloud.install({
|
|
69
69
|
registry,
|
|
70
70
|
resourceId: bootstrap.resourceId,
|
|
@@ -81,7 +81,7 @@ test('a large registration replaces the complete graph in one cloud operation an
|
|
|
81
81
|
const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
|
|
82
82
|
const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
|
|
83
83
|
const root = registry.enrollRoot({ type: 'workspace', name: 'large-ground' });
|
|
84
|
-
const bootstrap = cloud.bootstrap(registry);
|
|
84
|
+
const bootstrap = cloud.bootstrap(registry.syncRecords());
|
|
85
85
|
cloud.install({
|
|
86
86
|
registry,
|
|
87
87
|
resourceId: bootstrap.resourceId,
|
|
@@ -126,7 +126,7 @@ test('a large registration replaces the complete graph in one cloud operation an
|
|
|
126
126
|
recipientDb.close();
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
test('
|
|
129
|
+
test('a legacy empty entity resource repairs only the verified home and excludes unrelated local ground', () => {
|
|
130
130
|
const db = new Database(':memory:');
|
|
131
131
|
const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
|
|
132
132
|
const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
|
|
@@ -172,21 +172,28 @@ test('only an exact verified bootstrap graph can repair a legacy empty entity re
|
|
|
172
172
|
assert.equal(cloud.replica().state, 'active');
|
|
173
173
|
assert.doesNotThrow(() => cloud.assertRegistrationReady());
|
|
174
174
|
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const safeRoot =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
const legacyDb = new Database(':memory:');
|
|
176
|
+
const legacyCloud = createEntityCloud({ db: legacyDb, userId: 'user-1', deviceId: 'legacy-device' });
|
|
177
|
+
const legacyRegistry = createRegistry({ db: legacyDb, ephemeral: true, emit: () => {}, journal: legacyCloud.journal });
|
|
178
|
+
const safeRoot = legacyRegistry.enrollRoot({ type: 'workspace', name: 'agents' });
|
|
179
|
+
const legacyRepo = legacyRegistry.create({ type: 'repo.git', parentUUID: safeRoot.uuid, name: 'old-local-repo' });
|
|
180
|
+
const verifiedHome = legacyRegistry.syncRecords().filter((record) => record.uuid === safeRoot.uuid);
|
|
181
|
+
const repaired = legacyCloud.install({
|
|
182
|
+
registry: legacyRegistry,
|
|
182
183
|
resourceId,
|
|
183
184
|
authorityEpoch: 1,
|
|
184
185
|
headVersion: 0,
|
|
185
186
|
snapshotBase64: empty.toString('base64'),
|
|
186
187
|
snapshotChecksum: require('crypto').createHash('sha256').update(empty).digest('hex'),
|
|
187
|
-
bootstrapRecords:
|
|
188
|
-
})
|
|
189
|
-
|
|
188
|
+
bootstrapRecords: verifiedHome,
|
|
189
|
+
});
|
|
190
|
+
assert.equal(repaired.reconciling, true);
|
|
191
|
+
const repair = legacyCloud.claim();
|
|
192
|
+
assert.equal(repair.length, 1);
|
|
193
|
+
assert.equal(repair[0].operation.record.uuid, safeRoot.uuid,
|
|
194
|
+
'only the verified home node is queued into the automatic cloud graph');
|
|
195
|
+
assert.equal(legacyRegistry.syncRecord(legacyRepo.uuid).uuid, legacyRepo.uuid,
|
|
196
|
+
'unrelated local ground remains local until its caller explicitly registers it');
|
|
190
197
|
});
|
|
191
198
|
|
|
192
199
|
test('a compacted remote snapshot enters the cloud catalog, not locally watched identity, and add claims its immutable content', () => {
|
|
@@ -32,7 +32,7 @@ test('file bytes are captured as bounded immutable chunks, then acknowledged bef
|
|
|
32
32
|
const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1', content });
|
|
33
33
|
const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
|
|
34
34
|
const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
|
|
35
|
-
const bootstrap = cloud.bootstrap(registry);
|
|
35
|
+
const bootstrap = cloud.bootstrap(registry.syncRecords());
|
|
36
36
|
cloud.install({
|
|
37
37
|
registry,
|
|
38
38
|
resourceId: bootstrap.resourceId,
|
|
@@ -344,7 +344,7 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
|
|
|
344
344
|
cloudUserId: 'user-1', cloudDeviceId: 'target-device',
|
|
345
345
|
});
|
|
346
346
|
try {
|
|
347
|
-
const bootstrap = source.entityCloud.bootstrap(source.registration.registry);
|
|
347
|
+
const bootstrap = source.entityCloud.bootstrap(source.registration.registry.syncRecords());
|
|
348
348
|
source.entityCloud.install({
|
|
349
349
|
registry: source.registration.registry, resourceId: bootstrap.resourceId,
|
|
350
350
|
authorityEpoch: 1, headVersion: 0, snapshotBase64: bootstrap.snapshotBase64,
|
|
@@ -233,6 +233,40 @@ test('user-home bootstrap creates only declared roots and registers them through
|
|
|
233
233
|
);
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
+
test('entity-cloud bootstrap publishes only the verified user home when an old local repo exists', async () => {
|
|
237
|
+
const service = createRegistrationService({
|
|
238
|
+
dir: path.join(tempRoot, 'state-user-home-cloud-boundary'),
|
|
239
|
+
homeDir: path.join(tempRoot, 'user-home-cloud-boundary'),
|
|
240
|
+
cloudUserId: 'user-home-cloud-boundary',
|
|
241
|
+
cloudDeviceId: 'computer-user-home-cloud-boundary',
|
|
242
|
+
});
|
|
243
|
+
services.push(service);
|
|
244
|
+
const home = await service.bootstrapUserHome();
|
|
245
|
+
const workspaces = home.roots.find((entry) => entry.key === 'workspaces').record;
|
|
246
|
+
const oldRepo = service.registration.registry.create({
|
|
247
|
+
type: 'repo.git', parentUUID: workspaces.uuid, name: 'old-local-repo', payloadVersion: null,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const records = await service.exactUserHomeCloudRecords();
|
|
251
|
+
assert.equal(records.length, 9);
|
|
252
|
+
assert.equal(records.some((record) => record.uuid === oldRepo.uuid), false,
|
|
253
|
+
'login never turns existing user-selected ground into an implicit cloud registration');
|
|
254
|
+
|
|
255
|
+
const bootstrap = service.entityCloud.bootstrap(records);
|
|
256
|
+
const snapshot = JSON.parse(Buffer.from(bootstrap.snapshotBase64, 'base64').toString('utf8'));
|
|
257
|
+
assert.equal(snapshot.records.length, 9);
|
|
258
|
+
service.entityCloud.install({
|
|
259
|
+
registry: service.registration.registry,
|
|
260
|
+
resourceId: bootstrap.resourceId,
|
|
261
|
+
authorityEpoch: 1,
|
|
262
|
+
headVersion: 1,
|
|
263
|
+
snapshotBase64: bootstrap.snapshotBase64,
|
|
264
|
+
snapshotChecksum: bootstrap.snapshotChecksum,
|
|
265
|
+
bootstrapRecords: records,
|
|
266
|
+
});
|
|
267
|
+
assert.doesNotThrow(() => service.entityCloud.assertRegistrationReady());
|
|
268
|
+
});
|
|
269
|
+
|
|
236
270
|
test('a fresh machine binds existing user-home UUIDs before it starts watching', async () => {
|
|
237
271
|
const first = createRegistrationService({
|
|
238
272
|
dir: path.join(tempRoot, 'state-user-home-origin'),
|
|
@@ -262,6 +296,62 @@ test('a fresh machine binds existing user-home UUIDs before it starts watching',
|
|
|
262
296
|
'only the bound identities enter Watch');
|
|
263
297
|
});
|
|
264
298
|
|
|
299
|
+
test('an older pre-cloud home adopts authenticated platform UUIDs without changing user descendants', async () => {
|
|
300
|
+
const authority = createRegistrationService({
|
|
301
|
+
dir: path.join(tempRoot, 'state-user-home-authority'),
|
|
302
|
+
homeDir: path.join(tempRoot, 'user-home-authority'),
|
|
303
|
+
});
|
|
304
|
+
const local = createRegistrationService({
|
|
305
|
+
dir: path.join(tempRoot, 'state-user-home-pre-cloud'),
|
|
306
|
+
homeDir: path.join(tempRoot, 'user-home-pre-cloud'),
|
|
307
|
+
});
|
|
308
|
+
services.push(authority, local);
|
|
309
|
+
const authoritative = await authority.bootstrapUserHome();
|
|
310
|
+
const before = await local.bootstrapUserHome();
|
|
311
|
+
const oldWorkspaces = before.roots.find((entry) => entry.key === 'workspaces').record;
|
|
312
|
+
const userProject = local.registration.registry.create({
|
|
313
|
+
type: 'folder', parentUUID: oldWorkspaces.uuid, name: 'user-project',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const snapshot = JSON.parse(JSON.stringify(authoritative.cloudSnapshot));
|
|
317
|
+
const rootIds = new Map();
|
|
318
|
+
for (const root of snapshot.roots) {
|
|
319
|
+
root.record.uuid = crypto.randomUUID();
|
|
320
|
+
rootIds.set(root.key, root.record.uuid);
|
|
321
|
+
}
|
|
322
|
+
for (const managed of snapshot.exactManaged) {
|
|
323
|
+
if (managed.relativePath === managed.key.replace(/-root$/, '')) {
|
|
324
|
+
managed.record.uuid = rootIds.get(managed.key.replace(/-root$/, ''));
|
|
325
|
+
managed.record.parentUUID = null;
|
|
326
|
+
} else {
|
|
327
|
+
managed.record.uuid = crypto.randomUUID();
|
|
328
|
+
managed.record.parentUUID = rootIds.get(managed.relativePath.split('/')[0]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const rebound = await local.bootstrapUserHome({ identitySnapshot: snapshot });
|
|
333
|
+
const reboundWorkspaces = rebound.roots.find((entry) => entry.key === 'workspaces').record;
|
|
334
|
+
assert.equal(reboundWorkspaces.uuid, rootIds.get('workspaces'));
|
|
335
|
+
assert.equal(local.registration.registry.syncRecord(userProject.uuid).uuid, userProject.uuid,
|
|
336
|
+
'the user project keeps its permanent identity');
|
|
337
|
+
assert.equal(local.registration.registry.syncRecord(userProject.uuid).parentUUID, reboundWorkspaces.uuid,
|
|
338
|
+
'only its parent now names the cloud-authoritative platform root');
|
|
339
|
+
assert.deepEqual(
|
|
340
|
+
rebound.exactManaged.map((entry) => entry.record.uuid),
|
|
341
|
+
snapshot.exactManaged.map((entry) => entry.record.uuid),
|
|
342
|
+
'the three exact system nodes and six roots now use the signed cloud identities',
|
|
343
|
+
);
|
|
344
|
+
const db = new Database(path.join(local.dir, 'identity.db'), { readonly: true });
|
|
345
|
+
try {
|
|
346
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entities WHERE uuid = ?').get(oldWorkspaces.uuid).count, 0,
|
|
347
|
+
'the superseded platform root is not retained as a second active identity');
|
|
348
|
+
} finally {
|
|
349
|
+
db.close();
|
|
350
|
+
}
|
|
351
|
+
assert.equal(local.observer.status().every((root) => root.watching && !root.degraded), true,
|
|
352
|
+
'rekeying identity preserves every existing watcher');
|
|
353
|
+
});
|
|
354
|
+
|
|
265
355
|
test('a signed-in register call writes every entity delivery intent in identity.db before Watch begins', async () => {
|
|
266
356
|
const service = createRegistrationService({
|
|
267
357
|
dir: path.join(tempRoot, 'state-cloud-register'),
|
|
@@ -270,7 +360,7 @@ test('a signed-in register call writes every entity delivery intent in identity.
|
|
|
270
360
|
cloudDeviceId: 'computer-cloud-register',
|
|
271
361
|
});
|
|
272
362
|
const home = await service.bootstrapUserHome();
|
|
273
|
-
const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
|
|
363
|
+
const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
|
|
274
364
|
service.entityCloud.install({
|
|
275
365
|
registry: service.registration.registry,
|
|
276
366
|
resourceId: bootstrap.resourceId,
|
|
@@ -310,7 +400,7 @@ test('a large signed-in register call publishes one complete cloud graph before
|
|
|
310
400
|
cloudUserId: 'user-cloud-register-large',
|
|
311
401
|
cloudDeviceId: 'computer-cloud-register-large',
|
|
312
402
|
});
|
|
313
|
-
const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
|
|
403
|
+
const bootstrap = service.entityCloud.bootstrap(service.registration.registry.syncRecords());
|
|
314
404
|
service.entityCloud.install({
|
|
315
405
|
registry: service.registration.registry,
|
|
316
406
|
resourceId: bootstrap.resourceId,
|