@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.
- package/dist/account-migration.d.ts +92 -3
- package/dist/account-migration.d.ts.map +1 -1
- package/dist/account-migration.js +340 -53
- package/dist/account-migration.js.map +1 -1
- package/dist/vault-account-source.d.ts +10 -2
- package/dist/vault-account-source.d.ts.map +1 -1
- package/dist/vault-account-source.js +15 -1
- package/dist/vault-account-source.js.map +1 -1
- package/dist/vault-provider-state.d.ts +68 -0
- package/dist/vault-provider-state.d.ts.map +1 -1
- package/dist/vault-provider-state.js +164 -0
- package/dist/vault-provider-state.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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' &&
|
|
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' &&
|
|
567
|
+
if (sourceKind !== 'file' &&
|
|
568
|
+
sourceKind !== 'absent' &&
|
|
569
|
+
sourceKind !== 'generation')
|
|
485
570
|
throw new AccountMigrationError('journal source kind is invalid');
|
|
486
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
|
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') {
|