@cortexkit/antigravity-auth-core 3.0.14 → 3.0.16

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.
@@ -59,14 +59,94 @@ export function decodeAccountStorePointer(value, legacyPath) {
59
59
  throw new AccountMigrationError('pointer directory basename differs from its derived generation');
60
60
  return { schemaVersion: 1, id, directoryBasename: basename(paths.storeDir) };
61
61
  }
62
- async function readPointer(legacyPath) {
62
+ /** Decodes either pointer schema; schema 1 keeps `decodeAccountStorePointer`'s exact rules. */
63
+ export function decodeAccountStorePointerRecord(value, legacyPath) {
64
+ if (object(value, 'pointer').schemaVersion !== 2)
65
+ return decodeAccountStorePointer(value, legacyPath);
66
+ const raw = exact(value, ['schemaVersion', 'id', 'directoryBasename', 'relocatedTo'], 'forward');
67
+ const id = text(raw.id, UUID, 'forward source generation');
68
+ const paths = generationPaths(legacyPath, id);
69
+ if (raw.directoryBasename !== basename(paths.storeDir))
70
+ throw new AccountMigrationError('forward directory basename differs from its derived generation');
71
+ const target = exact(raw.relocatedTo, ['legacyPath', 'id'], 'forward target');
72
+ if (typeof target.legacyPath !== 'string')
73
+ throw new AccountMigrationError('forward target path is invalid');
74
+ const targetId = text(target.id, UUID, 'forward target generation');
75
+ const targetPaths = generationPaths(target.legacyPath, targetId);
76
+ if (sameFilesystemPath(targetPaths.legacyPath, legacyPath))
77
+ throw new AccountMigrationError('forward names its own anchor');
78
+ return {
79
+ schemaVersion: 2,
80
+ id,
81
+ directoryBasename: basename(paths.storeDir),
82
+ relocatedTo: {
83
+ legacyPath: targetPaths.legacyPath,
84
+ id: targetId,
85
+ },
86
+ };
87
+ }
88
+ async function readPointerRecord(legacyPath) {
63
89
  const logical = resolveAccountStorePaths(legacyPath);
64
90
  if (await exists(logical.storeDir))
65
91
  throw new AccountMigrationError('foreign logical store directory is not pointer authority');
66
92
  const path = accountStorePointerPath(legacyPath);
67
93
  if (!(await exists(path)))
68
94
  return undefined;
69
- return decodeAccountStorePointer(jsonBytes(await secureRead(path)), legacyPath);
95
+ return decodeAccountStorePointerRecord(jsonBytes(await secureRead(path)), legacyPath);
96
+ }
97
+ /**
98
+ * The schema-1 pointer for operations that work on the generation published
99
+ * at this anchor. A forward refuses them: this build only reads a moved
100
+ * store (through `readAccountStoreBinding`) and never migrates, initializes,
101
+ * publishes over or writes beside one.
102
+ */
103
+ async function readPointer(legacyPath) {
104
+ const record = await readPointerRecord(legacyPath);
105
+ if (record?.schemaVersion === 2)
106
+ throw new AccountMigrationError(`the account store moved to ${record.relocatedTo.legacyPath}; this operation does not follow a moved store`);
107
+ return record;
108
+ }
109
+ /**
110
+ * The anchor a forward leads to. The new anchor's pointer must publish
111
+ * exactly the generation the forward names; anything else (no pointer, a
112
+ * different generation, a second forward) refuses, because binding there
113
+ * would either serve the wrong store or report "no accounts" and let a login
114
+ * create a second store.
115
+ */
116
+ async function forwardTarget(legacyPath, forward) {
117
+ const target = forward.relocatedTo.legacyPath;
118
+ const published = await readPointerRecord(target);
119
+ if (published?.schemaVersion !== 1 || published.id !== forward.relocatedTo.id)
120
+ throw new AccountMigrationError(`the forwarding pointer ${accountStorePointerPath(legacyPath)} names generation ${forward.relocatedTo.id}, which ${accountStorePointerPath(target)} does not publish`);
121
+ return target;
122
+ }
123
+ /**
124
+ * Resolves the caller's anchor to the one that holds the store: follows a
125
+ * schema-2 forward one step to the anchor it names, after checking that
126
+ * anchor still publishes the forwarded generation. Reads only; it writes
127
+ * nothing at either anchor.
128
+ */
129
+ export async function resolveAccountStoreAnchor(legacyPath, options = {}) {
130
+ resolveAccountStorePaths(legacyPath);
131
+ const pointer = accountStorePointerPath(legacyPath);
132
+ for (const alternative of options.alternatives ?? []) {
133
+ if (sameFilesystemPath(alternative, legacyPath))
134
+ continue;
135
+ const other = accountStorePointerPath(alternative);
136
+ if ((await exists(pointer)) && (await exists(other)))
137
+ throw new AccountMigrationError(`account store pointers exist at both ${pointer} and ${other}; refusing to choose between them`);
138
+ }
139
+ const record = await readPointerRecord(legacyPath);
140
+ if (record?.schemaVersion !== 2)
141
+ return { legacyPath };
142
+ const named = record.relocatedTo.legacyPath;
143
+ if (options.relocatedAnchor !== undefined &&
144
+ !sameFilesystemPath(options.relocatedAnchor, named))
145
+ throw new AccountMigrationError(`the forwarding pointer ${pointer} names ${named}, but this process keeps the account store at ${options.relocatedAnchor}`);
146
+ return {
147
+ legacyPath: await forwardTarget(legacyPath, record),
148
+ forwardedFrom: legacyPath,
149
+ };
70
150
  }
71
151
  export async function resolvePublishedAccountStorePaths(legacyPath) {
72
152
  const pointer = await readPointer(legacyPath);
@@ -354,7 +434,9 @@ export function decodeAccountStoreGeneration(value) {
354
434
  ], 'generation');
355
435
  if (raw.schemaVersion !== 1 ||
356
436
  typeof raw.legacyPath !== 'string' ||
357
- (raw.sourceKind !== 'file' && raw.sourceKind !== 'absent'))
437
+ (raw.sourceKind !== 'file' &&
438
+ raw.sourceKind !== 'absent' &&
439
+ raw.sourceKind !== 'generation'))
358
440
  throw new AccountMigrationError('generation schema is invalid');
359
441
  const id = text(raw.id, UUID, 'generation id');
360
442
  const paths = generationPaths(raw.legacyPath, id);
@@ -462,6 +544,7 @@ export function decodeAccountMigrationJournal(value) {
462
544
  'retiredSha256',
463
545
  'rollback',
464
546
  'cancelled',
547
+ 'relocation',
465
548
  ], 'journal');
466
549
  if (raw.schemaVersion !== 1)
467
550
  throw new AccountMigrationError('unsupported journal schema');
@@ -481,12 +564,16 @@ export function decodeAccountMigrationJournal(value) {
481
564
  if (phase === undefined)
482
565
  throw new AccountMigrationError('journal phase is invalid');
483
566
  const sourceKind = raw.sourceKind;
484
- if (sourceKind !== 'file' && sourceKind !== 'absent')
567
+ if (sourceKind !== 'file' &&
568
+ sourceKind !== 'absent' &&
569
+ sourceKind !== 'generation')
485
570
  throw new AccountMigrationError('journal source kind is invalid');
486
- const sourceSha256 = sourceKind === 'file'
571
+ // A moved store keeps the same two hashes as a pool-file source: its
572
+ // snapshot of the old generation's files and its capture file.
573
+ const sourceSha256 = sourceKind !== 'absent'
487
574
  ? text(raw.sourceSha256, SHA256, 'journal.sourceSha256')
488
575
  : undefined;
489
- const captureSha256 = sourceKind === 'file'
576
+ const captureSha256 = sourceKind !== 'absent'
490
577
  ? text(raw.captureSha256, SHA256, 'journal.captureSha256')
491
578
  : undefined;
492
579
  if (sourceKind === 'absent' && Object.hasOwn(raw, 'captureSha256'))
@@ -511,7 +598,7 @@ export function decodeAccountMigrationJournal(value) {
511
598
  sourceVersion !== 4)
512
599
  throw new AccountMigrationError('manifest source version is invalid');
513
600
  if ((sourceKind === 'file' && sourceVersion === null) ||
514
- (sourceKind === 'absent' && sourceVersion !== null))
601
+ (sourceKind !== 'file' && sourceVersion !== null))
515
602
  throw new AccountMigrationError('manifest version differs from source kind');
516
603
  if (!finite(m.normalizationClock) || !finite(m.effectiveActiveIndex))
517
604
  throw new AccountMigrationError('manifest clock or cursor is invalid');
@@ -549,6 +636,15 @@ export function decodeAccountMigrationJournal(value) {
549
636
  }) ||
550
637
  manifest.effectiveActiveIndex !== 0))
551
638
  throw new AccountMigrationError('absent-source manifest must be explicitly empty with no invented cursors');
639
+ // A move copies the store's own files; its journal carries no rows and so
640
+ // no refresh token. The moved rows are listed in `relocation.rows`.
641
+ if (sourceKind === 'generation' &&
642
+ (accounts.length ||
643
+ !isDeepStrictEqual(encodeRoutingSettings(manifest.routing), {
644
+ schemaVersion: 1,
645
+ }) ||
646
+ manifest.effectiveActiveIndex !== 0))
647
+ throw new AccountMigrationError('moved-store manifest must be explicitly empty');
552
648
  const effective = accounts.length
553
649
  ? Math.max(0, Math.min(manifest.routing.activeIndex ?? 0, accounts.length - 1))
554
650
  : 0;
@@ -606,6 +702,33 @@ export function decodeAccountMigrationJournal(value) {
606
702
  throw new AccountMigrationError('absent source cannot claim retired bytes');
607
703
  if (retiredSha256 !== undefined && retiredSha256 !== sourceSha256)
608
704
  throw new AccountMigrationError('retired bytes are not associated with source');
705
+ const relocation = decodeRelocation(raw, sourceKind, paths.legacyPath);
706
+ if (sourceKind === 'generation') {
707
+ // The moved store's retirement is the forward at the old anchor, not a
708
+ // retired pool-file copy or a checked absence.
709
+ if (retiredSha256 !== undefined)
710
+ throw new AccountMigrationError('moved source cannot claim retired pool-file bytes');
711
+ if (relocation === undefined)
712
+ throw new AccountMigrationError('moved source lacks its relocation evidence');
713
+ if (operation === 'rollback')
714
+ throw new AccountMigrationError('a moved account store is not rolled back; move it back first with `antigravity-auth store relocate --back --offline`');
715
+ if (relocation.sourceForwarded && !relocation.sourceTombstoned)
716
+ throw new AccountMigrationError('source forward precedes its management tombstone');
717
+ if ((relocation.sourceTombstoned || relocation.sourceForwarded) &&
718
+ (!verification || (phase !== 'retire' && phase !== 'activate')))
719
+ throw new AccountMigrationError('source tombstone or forward precedes durable verification');
720
+ if (status === 'active' && !relocation.sourceForwarded)
721
+ throw new AccountMigrationError('active moved store lacks its source forward');
722
+ if (phase === 'activate' && !relocation.sourceForwarded)
723
+ throw new AccountMigrationError('moved-store activation lacks its source forward');
724
+ }
725
+ // What retired the source: the retired pool-file copy, the checked
726
+ // absence of a pool file, or the forward that replaced the old pointer.
727
+ const retirementRecorded = sourceKind === 'file'
728
+ ? retiredSha256 !== undefined
729
+ : sourceKind === 'absent'
730
+ ? raw.sourceAbsenceVerified === true
731
+ : relocation?.sourceForwarded === true;
609
732
  let rollback;
610
733
  if (Object.hasOwn(raw, 'rollback')) {
611
734
  const r = exact(raw.rollback, ['id', 'snapshotSha256', 'exportSha256', 'rows', 'successorInactive'], 'rollback');
@@ -643,18 +766,12 @@ export function decodeAccountMigrationJournal(value) {
643
766
  (retiredSha256 !== undefined || raw.sourceAbsenceVerified === true) &&
644
767
  (!verification || (phase !== 'retire' && phase !== 'activate')))
645
768
  throw new AccountMigrationError('retirement association precedes durable verification');
646
- if (operation === 'migrate' &&
647
- phase === 'activate' &&
648
- (sourceKind === 'file'
649
- ? retiredSha256 === undefined
650
- : raw.sourceAbsenceVerified !== true))
769
+ if (operation === 'migrate' && phase === 'activate' && !retirementRecorded)
651
770
  throw new AccountMigrationError('activation lacks its actual source retirement association');
652
771
  if (status === 'active' &&
653
772
  (operation !== 'migrate' ||
654
773
  phase !== 'activate' ||
655
- (sourceKind === 'file'
656
- ? retiredSha256 === undefined
657
- : raw.sourceAbsenceVerified !== true) ||
774
+ !retirementRecorded ||
658
775
  rollback !== undefined ||
659
776
  cancelled))
660
777
  throw new AccountMigrationError('active receipt is incomplete');
@@ -662,10 +779,7 @@ export function decodeAccountMigrationJournal(value) {
662
779
  throw new AccountMigrationError('rollback lacks its current-credential snapshot');
663
780
  if (operation === 'rollback' &&
664
781
  !cancelled &&
665
- (verification === undefined ||
666
- (sourceKind === 'file'
667
- ? retiredSha256 === undefined
668
- : raw.sourceAbsenceVerified !== true)))
782
+ (verification === undefined || !retirementRecorded))
669
783
  throw new AccountMigrationError('rollback lacks the original verified retirement');
670
784
  if (status === 'inactive' &&
671
785
  !cancelled &&
@@ -697,6 +811,56 @@ export function decodeAccountMigrationJournal(value) {
697
811
  ...(retiredSha256 === undefined ? {} : { retiredSha256 }),
698
812
  ...(rollback === undefined ? {} : { rollback }),
699
813
  ...(cancelled ? { cancelled: true } : {}),
814
+ ...(relocation === undefined ? {} : { relocation }),
815
+ };
816
+ }
817
+ /** Decodes `relocation`, which only a moved store's journal may carry. */
818
+ function decodeRelocation(raw, sourceKind, legacyPath) {
819
+ if (!Object.hasOwn(raw, 'relocation'))
820
+ return undefined;
821
+ if (sourceKind !== 'generation')
822
+ throw new AccountMigrationError('only a moved store records relocation evidence');
823
+ const r = exact(raw.relocation, ['source', 'rows', 'sourceTombstoned', 'sourceForwarded'], 'relocation');
824
+ const s = exact(r.source, [
825
+ 'legacyPath',
826
+ 'storeDir',
827
+ 'journalId',
828
+ 'configSha256',
829
+ 'stateSha256',
830
+ 'statePresent',
831
+ ], 'relocation source');
832
+ if (typeof s.legacyPath !== 'string')
833
+ throw new AccountMigrationError('relocation source path is invalid');
834
+ const journalId = text(s.journalId, UUID, 'relocation source journal');
835
+ const source = generationPaths(s.legacyPath, journalId);
836
+ if (s.storeDir !== source.storeDir)
837
+ throw new AccountMigrationError('relocation source association differs');
838
+ if (sameFilesystemPath(source.legacyPath, legacyPath))
839
+ throw new AccountMigrationError('relocation source and target share one anchor');
840
+ if (typeof s.statePresent !== 'boolean')
841
+ throw new AccountMigrationError('relocation state presence is invalid');
842
+ if (!s.statePresent && Object.hasOwn(s, 'stateSha256'))
843
+ throw new AccountMigrationError('absent source state cannot claim a file hash');
844
+ const rows = list(r.rows, 'relocation rows').map((id) => text(id, UUID, 'relocation row id'));
845
+ if (new Set(rows).size !== rows.length)
846
+ throw new AccountMigrationError('relocation rows are duplicated');
847
+ if (typeof r.sourceTombstoned !== 'boolean' ||
848
+ typeof r.sourceForwarded !== 'boolean')
849
+ throw new AccountMigrationError('relocation progress flags are invalid');
850
+ return {
851
+ source: {
852
+ legacyPath: source.legacyPath,
853
+ storeDir: source.storeDir,
854
+ journalId,
855
+ configSha256: text(s.configSha256, SHA256, 'relocation config hash'),
856
+ ...(s.statePresent
857
+ ? { stateSha256: text(s.stateSha256, SHA256, 'relocation state hash') }
858
+ : {}),
859
+ statePresent: s.statePresent,
860
+ },
861
+ rows,
862
+ sourceTombstoned: r.sourceTombstoned,
863
+ sourceForwarded: r.sourceForwarded,
700
864
  };
701
865
  }
702
866
  function pendingLockFailure(modules, error) {
@@ -1046,6 +1210,8 @@ async function verifyCopies(paths, journal, retired) {
1046
1210
  if (digest(capture) !== journal.captureSha256 ||
1047
1211
  !isDeepStrictEqual(jsonBytes(capture), captureJson(journal)))
1048
1212
  throw new AccountMigrationError('journal differs from its exclusive capture snapshot');
1213
+ if (journal.sourceKind === 'generation')
1214
+ return verifyRelocationSnapshot(backup, journal);
1049
1215
  // Original backup JSON and the captured time reproduce the expected import.
1050
1216
  // Compare that value with the journal without changing its selected rows, clock,
1051
1217
  // source positions or UUID mapping.
@@ -1068,8 +1234,67 @@ function captureJson(journal) {
1068
1234
  sourceSha256: journal.sourceSha256,
1069
1235
  manifest: manifestJson(journal.manifest),
1070
1236
  mapping: journal.mapping,
1237
+ ...(journal.relocation === undefined
1238
+ ? {}
1239
+ : { relocation: { source: journal.relocation.source } }),
1240
+ };
1241
+ }
1242
+ function base64Bytes(value, path) {
1243
+ if (typeof value !== 'string')
1244
+ throw new AccountMigrationError(`${path} is invalid`);
1245
+ const bytes = Buffer.from(value, 'base64');
1246
+ // Only canonical base64 round-trips; anything else could hide extra bytes.
1247
+ if (bytes.toString('base64') !== value)
1248
+ throw new AccountMigrationError(`${path} is not canonical base64`);
1249
+ return bytes;
1250
+ }
1251
+ export function decodeRelocationSnapshot(value) {
1252
+ const raw = exact(value, ['config', 'state', 'journal'], 'moved snapshot');
1253
+ return {
1254
+ config: base64Bytes(raw.config, 'moved snapshot config'),
1255
+ ...(Object.hasOwn(raw, 'state')
1256
+ ? { state: base64Bytes(raw.state, 'moved snapshot state') }
1257
+ : {}),
1258
+ journal: base64Bytes(raw.journal, 'moved snapshot journal'),
1071
1259
  };
1072
1260
  }
1261
+ /**
1262
+ * Ties a moved store's source snapshot to its journal: the copied files hash
1263
+ * to `relocation.source`, the copied journal is the active source generation
1264
+ * it names, and the copied config carries that generation's own settings
1265
+ * key. The live old generation is not compared: it gains a management
1266
+ * tombstone after the copy, and it is no longer served.
1267
+ *
1268
+ * Binding a copy at another anchor is sound because the store's files hold
1269
+ * no location. The common-auth "Store portability" contract (common-auth
1270
+ * `docs/adoption-inventory.md` at 672f6265, pinned by its
1271
+ * `test/store/store-portability.test.ts`) states that `config.json` and
1272
+ * `state.json` record no path, folder or file identity, or host identity,
1273
+ * and that a byte copy of both taken under the store's save locks opens
1274
+ * elsewhere as the same pool. Lock and sidecar files are not part of that
1275
+ * contract, and neither is any in-memory state kept by the process that
1276
+ * opened the old files; nothing here relies on either.
1277
+ */
1278
+ function verifyRelocationSnapshot(backup, journal) {
1279
+ const source = journal.relocation?.source;
1280
+ if (source === undefined)
1281
+ throw new AccountMigrationError('moved source lacks its relocation evidence');
1282
+ const snapshot = decodeRelocationSnapshot(jsonBytes(backup));
1283
+ if (digest(snapshot.config) !== source.configSha256 ||
1284
+ (snapshot.state !== undefined) !== source.statePresent ||
1285
+ (snapshot.state !== undefined &&
1286
+ digest(snapshot.state) !== source.stateSha256))
1287
+ throw new AccountMigrationError('moved source snapshot differs from its recorded hashes');
1288
+ const copied = decodeAccountMigrationJournal(jsonBytes(snapshot.journal));
1289
+ if (copied.id !== source.journalId ||
1290
+ copied.legacyPath !== source.legacyPath ||
1291
+ copied.storeDir !== source.storeDir ||
1292
+ copied.status !== 'active')
1293
+ throw new AccountMigrationError('moved source snapshot is not the active source generation');
1294
+ const config = object(jsonBytes(snapshot.config), 'moved source config');
1295
+ if (!isDeepStrictEqual(decodeAccountStoreGeneration(config[ACCOUNT_STORE_GENERATION_SETTINGS_KEY]), generationJson(copied)))
1296
+ throw new AccountMigrationError('moved source snapshot belongs to another generation');
1297
+ }
1073
1298
  async function synchronizedSnapshot(paths, boundary) {
1074
1299
  await syncFile(paths.configPath);
1075
1300
  const statePresent = await exists(paths.statePath);
@@ -1153,7 +1378,7 @@ function receipt(journal) {
1153
1378
  storeDir: journal.storeDir,
1154
1379
  sourceKind: journal.sourceKind,
1155
1380
  durability: journal.verification?.durability ?? accountMigrationDurability(),
1156
- restartRequired: journal.sourceKind === 'file' || journal.operation === 'rollback',
1381
+ restartRequired: journal.sourceKind !== 'absent' || journal.operation === 'rollback',
1157
1382
  };
1158
1383
  }
1159
1384
  async function currentSuccessor(store, paths, journal) {
@@ -1246,40 +1471,15 @@ async function trustedCurrentGeneration(store, paths, journal) {
1246
1471
  /** A bound result establishes completed-generation ownership for repository recovery, not readiness to select or route credentials. */
1247
1472
  export async function readAccountStoreBinding(legacyPath, modules, now, expectedGeneration) {
1248
1473
  try {
1249
- const paths = await resolvePublishedAccountStorePaths(legacyPath);
1250
- if (!paths)
1251
- return (await exists(legacyPath))
1252
- ? { status: 'pending' }
1253
- : { status: 'initialization-required' };
1254
- const journal = await readJournal(paths);
1255
- if (!journal)
1256
- throw new AccountMigrationError('published binding journal missing');
1257
- if (expectedGeneration !== undefined && journal.id !== expectedGeneration)
1258
- throw new AccountMigrationError('bound repository generation differs from pointer');
1259
- if (journal.status === 'pending')
1260
- return {
1261
- status: 'pending',
1262
- phase: journal.phase,
1263
- operation: journal.operation,
1264
- };
1265
- if (journal.status === 'inactive')
1266
- return { status: 'inactive', id: journal.id };
1267
- if (await exists(legacyPath))
1268
- throw new AccountMigrationError('legacy source was recreated after activation');
1269
- await verifyCopies(paths, journal, true);
1270
- const settings = await trustedCurrentGeneration(openAdmissionStore(modules, paths, now), paths, journal);
1271
- const pending = settings[MANAGEMENT_SETTINGS_KEY];
1272
- if (pending !== undefined) {
1273
- const record = object(pending, 'repository management');
1274
- if (record.kind === 'migration')
1275
- return { status: 'pending', phase: 'activate', operation: 'migrate' };
1276
- if (record.kind !== 'clear' && record.kind !== 'replace-pool')
1277
- throw new AccountMigrationError('unknown management owner prevents binding');
1278
- // Ownership permits opening these physical paths. The repository must then
1279
- // validate and resume its own strict clear/replace journal; this binding
1280
- // neither validates that operation nor grants credential-serving readiness.
1474
+ // A forward at this anchor leads one step to the anchor the store moved
1475
+ // to; the bound paths are then the moved generation's own.
1476
+ const record = await readPointerRecord(legacyPath);
1477
+ if (record?.schemaVersion === 2) {
1478
+ if (await exists(legacyPath))
1479
+ throw new AccountMigrationError(`a pool file reappeared at ${legacyPath} after the account store moved to ${record.relocatedTo.legacyPath}`);
1480
+ return await bindPublishedGeneration(await forwardTarget(legacyPath, record), modules, now, expectedGeneration, legacyPath);
1281
1481
  }
1282
- return { status: 'bound', receipt: receipt(journal), paths };
1482
+ return await bindPublishedGeneration(legacyPath, modules, now, expectedGeneration);
1283
1483
  }
1284
1484
  catch (error) {
1285
1485
  return {
@@ -1290,6 +1490,84 @@ export async function readAccountStoreBinding(legacyPath, modules, now, expected
1290
1490
  };
1291
1491
  }
1292
1492
  }
1493
+ /**
1494
+ * A moved generation binds only while the old anchor still forwards to
1495
+ * exactly it. A pointer set back to schema 1 there, a missing forward, or a
1496
+ * forward to another generation refuses: either store could then be the
1497
+ * one a build writes to, and neither is chosen by age.
1498
+ */
1499
+ async function assertSourceForwarded(journal, forwardedFrom) {
1500
+ const source = journal.relocation?.source;
1501
+ if (source === undefined)
1502
+ throw new AccountMigrationError('moved source lacks its relocation evidence');
1503
+ const pointer = accountStorePointerPath(source.legacyPath);
1504
+ if (forwardedFrom !== undefined && forwardedFrom !== source.legacyPath)
1505
+ throw new AccountMigrationError(`the forwarding pointer ${accountStorePointerPath(forwardedFrom)} leads to a store moved from ${source.legacyPath}`);
1506
+ if (await exists(source.legacyPath))
1507
+ throw new AccountMigrationError(`a pool file reappeared at ${source.legacyPath} after the account store moved to ${journal.legacyPath}`);
1508
+ const record = await readPointerRecord(source.legacyPath);
1509
+ if (record === undefined)
1510
+ throw new AccountMigrationError(`the forwarding pointer ${pointer} to the moved account store at ${journal.storeDir} is missing; restore it`);
1511
+ if (record.schemaVersion === 1)
1512
+ throw new AccountMigrationError(`the account store in ${dirname(source.legacyPath)} was reactivated: ${pointer} publishes generation ${record.id} while the moved store at ${journal.storeDir} is active; restore the forwarding pointer, or run \`antigravity-auth store relocate\``);
1513
+ if (record.id !== source.journalId ||
1514
+ record.relocatedTo.legacyPath !== journal.legacyPath ||
1515
+ record.relocatedTo.id !== journal.id)
1516
+ throw new AccountMigrationError(`the forwarding pointer ${pointer} names another store than the moved account store at ${journal.storeDir}`);
1517
+ }
1518
+ async function bindPublishedGeneration(legacyPath, modules, now, expectedGeneration, forwardedFrom) {
1519
+ const paths = await resolvePublishedAccountStorePaths(legacyPath);
1520
+ if (!paths) {
1521
+ if (forwardedFrom !== undefined)
1522
+ throw new AccountMigrationError('forwarded anchor publishes no generation');
1523
+ return (await exists(legacyPath))
1524
+ ? { status: 'pending' }
1525
+ : { status: 'initialization-required' };
1526
+ }
1527
+ const journal = await readJournal(paths);
1528
+ if (!journal)
1529
+ throw new AccountMigrationError('published binding journal missing');
1530
+ if (expectedGeneration !== undefined && journal.id !== expectedGeneration)
1531
+ throw new AccountMigrationError('bound repository generation differs from pointer');
1532
+ if (forwardedFrom !== undefined && journal.sourceKind !== 'generation')
1533
+ throw new AccountMigrationError(`the forwarding pointer ${accountStorePointerPath(forwardedFrom)} leads to a store that was not moved there`);
1534
+ // An unfinished move is finished by the build that started it; this build
1535
+ // neither resumes it nor serves either store meanwhile.
1536
+ if (journal.sourceKind === 'generation' && journal.status === 'pending')
1537
+ throw new AccountMigrationError(`a move of the account store to ${journal.storeDir} is unfinished; finish it with the version that started it`);
1538
+ if (journal.status === 'pending')
1539
+ return {
1540
+ status: 'pending',
1541
+ phase: journal.phase,
1542
+ operation: journal.operation,
1543
+ };
1544
+ if (journal.status === 'inactive')
1545
+ return { status: 'inactive', id: journal.id };
1546
+ if (await exists(legacyPath))
1547
+ throw new AccountMigrationError('legacy source was recreated after activation');
1548
+ await verifyCopies(paths, journal, true);
1549
+ if (journal.sourceKind === 'generation')
1550
+ await assertSourceForwarded(journal, forwardedFrom);
1551
+ const settings = await trustedCurrentGeneration(openAdmissionStore(modules, paths, now), paths, journal);
1552
+ const pending = settings[MANAGEMENT_SETTINGS_KEY];
1553
+ if (pending !== undefined) {
1554
+ const record = object(pending, 'repository management');
1555
+ // Only a store move writes a migration record owned by another
1556
+ // generation: it blocks the old generation once the copy is taken.
1557
+ // Reaching it here means this anchor's pointer was set back from the
1558
+ // forward to the old generation.
1559
+ if (record.kind === 'migration' && record.id !== journal.id)
1560
+ throw new AccountMigrationError(`the account store at ${paths.storeDir} was moved to generation ${String(record.id)}, but ${accountStorePointerPath(legacyPath)} publishes the old store again; restore the forwarding pointer, or run \`antigravity-auth store relocate\``);
1561
+ if (record.kind === 'migration')
1562
+ return { status: 'pending', phase: 'activate', operation: 'migrate' };
1563
+ if (record.kind !== 'clear' && record.kind !== 'replace-pool')
1564
+ throw new AccountMigrationError('unknown management owner prevents binding');
1565
+ // Ownership permits opening these physical paths. The repository must then
1566
+ // validate and resume its own strict clear/replace journal; this binding
1567
+ // neither validates that operation nor grants credential-serving readiness.
1568
+ }
1569
+ return { status: 'bound', receipt: receipt(journal), paths };
1570
+ }
1293
1571
  /** Serving requires healthy current rows and no repository management work. Binding alone is insufficient. */
1294
1572
  export async function readAccountStoreAdmission(legacyPath, modules, now, expectedGeneration) {
1295
1573
  const binding = await readAccountStoreBinding(legacyPath, modules, now, expectedGeneration);
@@ -1623,6 +1901,8 @@ export function createAccountMigrationFactory(modules) {
1623
1901
  const existing = await readJournal(paths);
1624
1902
  if (!existing)
1625
1903
  throw new AccountMigrationError('published generation journal missing');
1904
+ if (existing.sourceKind === 'generation')
1905
+ throw new AccountMigrationError('a moved account store is finished by `antigravity-auth store relocate`, not by migrate');
1626
1906
  if (existing.status === 'inactive') {
1627
1907
  previous = existing;
1628
1908
  await modules.fs.withLock(paths.configPath, {
@@ -1812,6 +2092,11 @@ export function createAccountRollbackFactory(modules) {
1812
2092
  throw new AccountMigrationError('stopped processes and timers must be explicitly confirmed');
1813
2093
  try {
1814
2094
  return await withParentTopology(modules, options.legacyPath, async () => {
2095
+ // A moved store is rolled back from its pool file only after it was
2096
+ // moved back to this anchor; this build never does either.
2097
+ const record = await readPointerRecord(options.legacyPath);
2098
+ if (record?.schemaVersion === 2)
2099
+ throw new AccountMigrationError(`the account store moved to ${record.relocatedTo.legacyPath}; move it back with \`antigravity-auth store relocate --back --offline\` before rolling it back`);
1815
2100
  const paths = await resolvePublishedAccountStorePaths(options.legacyPath);
1816
2101
  if (!paths)
1817
2102
  throw new AccountMigrationError('rollback requires a published generation');
@@ -1824,6 +2109,8 @@ export function createAccountRollbackFactory(modules) {
1824
2109
  const initialJournal = await readJournal(paths);
1825
2110
  if (!initialJournal)
1826
2111
  throw new AccountMigrationError('rollback requires an owned migration generation');
2112
+ if (initialJournal.sourceKind === 'generation')
2113
+ throw new AccountMigrationError(`this account store was moved here from ${initialJournal.relocation?.source.legacyPath}; move it back with \`antigravity-auth store relocate --back --offline\` before rolling it back`);
1827
2114
  let journal = initialJournal;
1828
2115
  const store = openMigrationStore(modules, paths, options.now, writeObserver(paths, lease, options.onBoundary));
1829
2116
  if (journal.status === 'inactive') {