@modelprofile.com/authswitch 7.0.0 → 8.1.0

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.
Files changed (86) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +1 -3
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-import-contract.d.ts +207 -0
  5. package/dist_ts/authority-import-contract.js +18 -0
  6. package/dist_ts/classes.authoritybroker.js +1 -5
  7. package/dist_ts/classes.authoritycli.d.ts +4 -0
  8. package/dist_ts/classes.authoritycli.js +149 -12
  9. package/dist_ts/classes.authorityclient.d.ts +7 -0
  10. package/dist_ts/classes.authorityclient.js +13 -1
  11. package/dist_ts/classes.authoritydaemon.d.ts +4 -0
  12. package/dist_ts/classes.authoritydaemon.js +75 -9
  13. package/dist_ts/classes.authoritydatabase.d.ts +10 -26
  14. package/dist_ts/classes.authoritydatabase.js +68 -133
  15. package/dist_ts/classes.authorityimport.d.ts +161 -0
  16. package/dist_ts/classes.authorityimport.js +638 -0
  17. package/dist_ts/classes.authoritymodels.d.ts +39 -43
  18. package/dist_ts/classes.authoritymodels.js +209 -243
  19. package/dist_ts/classes.authoritypreuse.js +1 -2
  20. package/dist_ts/classes.authorityregistry.d.ts +2 -17
  21. package/dist_ts/classes.authorityregistry.js +18 -74
  22. package/dist_ts/classes.authoritysecrets.d.ts +1 -1
  23. package/dist_ts/classes.authoritysecrets.js +2 -2
  24. package/dist_ts/classes.authorityusage.d.ts +3 -3
  25. package/dist_ts/classes.claudeauthority.js +3 -7
  26. package/dist_ts/classes.claudenative.d.ts +1 -8
  27. package/dist_ts/classes.claudenative.js +2 -25
  28. package/dist_ts/classes.codexmanaged.d.ts +1 -1
  29. package/dist_ts/index.d.ts +1 -0
  30. package/dist_ts/index.js +6 -3
  31. package/dist_ts/plugins.d.ts +7 -7
  32. package/dist_ts/plugins.js +8 -8
  33. package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -3
  34. package/dist_ts/ts_migration/0001_authority_meta.js +29 -17
  35. package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
  36. package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
  37. package/dist_ts/ts_migration/index.d.ts +6 -3
  38. package/dist_ts/ts_migration/index.js +10 -4
  39. package/dist_ts/ts_migration/legacysources/authswitchstores.d.ts +2 -0
  40. package/dist_ts/ts_migration/legacysources/authswitchstores.js +244 -0
  41. package/dist_ts/ts_migration/legacysources/index.d.ts +18 -0
  42. package/dist_ts/ts_migration/legacysources/index.js +24 -0
  43. package/dist_ts/ts_migration/legacysources/material.d.ts +37 -0
  44. package/dist_ts/ts_migration/legacysources/material.js +120 -0
  45. package/dist_ts/ts_migration/legacysources/nativestores.d.ts +2 -0
  46. package/dist_ts/ts_migration/legacysources/nativestores.js +207 -0
  47. package/dist_ts/ts_migration/legacysources/shared.d.ts +124 -0
  48. package/dist_ts/ts_migration/legacysources/shared.js +142 -0
  49. package/package.json +6 -5
  50. package/readme.md +68 -26
  51. package/ts/00_commitinfo_data.ts +1 -1
  52. package/ts/authority-contract.ts +1 -3
  53. package/ts/authority-import-contract.ts +216 -0
  54. package/ts/classes.authoritybroker.ts +0 -4
  55. package/ts/classes.authoritycli.ts +165 -11
  56. package/ts/classes.authorityclient.ts +22 -0
  57. package/ts/classes.authoritydaemon.ts +75 -8
  58. package/ts/classes.authoritydatabase.ts +70 -145
  59. package/ts/classes.authorityimport.ts +718 -0
  60. package/ts/classes.authoritymodels.ts +253 -269
  61. package/ts/classes.authoritypreuse.ts +0 -1
  62. package/ts/classes.authorityregistry.ts +20 -91
  63. package/ts/classes.authoritysecrets.ts +1 -1
  64. package/ts/classes.authorityusage.ts +3 -3
  65. package/ts/classes.claudeauthority.ts +2 -6
  66. package/ts/classes.claudenative.ts +1 -32
  67. package/ts/classes.codexmanaged.ts +1 -1
  68. package/ts/index.ts +5 -2
  69. package/ts/plugins.ts +7 -7
  70. package/ts/ts_migration/0001_authority_meta.ts +29 -17
  71. package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
  72. package/ts/ts_migration/index.ts +10 -5
  73. package/ts/ts_migration/legacysources/authswitchstores.ts +211 -0
  74. package/ts/ts_migration/legacysources/index.ts +35 -0
  75. package/ts/ts_migration/legacysources/material.ts +134 -0
  76. package/ts/ts_migration/legacysources/nativestores.ts +204 -0
  77. package/ts/ts_migration/legacysources/shared.ts +220 -0
  78. package/dist_ts/authorityrehearsal.child.d.ts +0 -1
  79. package/dist_ts/authorityrehearsal.child.js +0 -395
  80. package/dist_ts/classes.authoritybackup.d.ts +0 -134
  81. package/dist_ts/classes.authoritybackup.js +0 -769
  82. package/dist_ts/classes.authorityrehearsal.d.ts +0 -127
  83. package/dist_ts/classes.authorityrehearsal.js +0 -632
  84. package/ts/authorityrehearsal.child.ts +0 -330
  85. package/ts/classes.authoritybackup.ts +0 -780
  86. package/ts/classes.authorityrehearsal.ts +0 -625
@@ -1,780 +0,0 @@
1
- import * as plugins from './plugins.js';
2
- import {
3
- assertStoredAuthorityBinding, assertStoredAuthorityClaudeHandoff,
4
- assertStoredAuthorityClaudeHome, assertStoredAuthorityCodexHome, assertStoredAuthorityEnrollment,
5
- assertStoredAuthorityGrant, assertStoredAuthorityHandoff,
6
- assertStoredAuthorityMeta, assertStoredAuthorityMigrationLedger, assertStoredAuthorityOperation,
7
- restoreStoredAuthorityOperation,
8
- type IStoredAuthorityBinding, type IStoredAuthorityCodexHome,
9
- type IStoredAuthorityEnrollment, type IStoredAuthorityGrant, type IStoredAuthorityMeta,
10
- } from './classes.authoritymodels.js';
11
- import { assertAuthorityStoredRecord, type IAuthorityStoredRecord } from './classes.authorityregistry.js';
12
- import type { IAuthSwitchSecretCodec } from './classes.authoritysecrets.js';
13
- import type { IRemoteControlEnrollment } from './interfaces.js';
14
-
15
- const maxPlainChunkBytes = 8 * 1024 * 1024;
16
- const maxEncryptedObjectBytes = 12 * 1024 * 1024;
17
- // The manifest is still one object. A paged authenticated index is required for unlimited inventory.
18
- const maxRecordBytes = 1024 * 1024;
19
- const maxSourceBytes = 4 * 1024 * 1024;
20
- const hash = (bytes: Uint8Array | string): string => plugins.crypto.createHash('sha256').update(bytes).digest('hex');
21
- const uuid = (): string => plugins.crypto.randomUUID();
22
- const isHash = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
23
- const isName = (value: unknown): value is string => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,256}$/.test(value);
24
- const isDigestField = (value: unknown): value is IAuthorityBackupSourceCapsule['linkedDigestField'] =>
25
- value === 'sourceDigest' || value === 'before.credentialDigest' || value === 'before.configDigest';
26
- function fail(message: string): never { throw new Error(message); }
27
- const recordKey = (model: string, id: string): string => `${model}\u0000${id}`;
28
- const context = (...parts: Array<string | number>): Uint8Array => Buffer.from(JSON.stringify(['authswitch-backup-v1', ...parts]));
29
- const object = (value: unknown): value is Record<string, unknown> => value !== null
30
- && typeof value === 'object' && !Array.isArray(value)
31
- && [Object.prototype, null].includes(Object.getPrototypeOf(value));
32
- const exactKeys = (value: Record<string, unknown>, keys: readonly string[]): boolean =>
33
- Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
34
-
35
- const assertSnapshotRecord = assertAuthorityStoredRecord;
36
-
37
- /** This ID preserves Codex's complete three-column enrollment key, without email matching. */
38
- export const authorityEnrollmentId = (row: IRemoteControlEnrollment): string =>
39
- hash(JSON.stringify(['authswitch-enrollment-v1', row.websocketUrl, row.accountId, row.appServerClientName]));
40
-
41
- const parseEnrollment = (bytes: Uint8Array, id: string): IRemoteControlEnrollment => {
42
- const value: unknown = JSON.parse(Buffer.from(bytes).toString('utf8'));
43
- if (!object(value) || !exactKeys(value, ['websocketUrl', 'accountId', 'appServerClientName',
44
- 'serverId', 'environmentId', 'serverName', 'updatedAt', 'remoteControlEnabled'])
45
- || !['websocketUrl', 'accountId', 'appServerClientName', 'serverId', 'environmentId', 'serverName']
46
- .every(key => typeof value[key] === 'string')
47
- || !Number.isSafeInteger(value.updatedAt) || Number(value.updatedAt) < 0
48
- || ![null, 0, 1].includes(value.remoteControlEnabled as null | number)) {
49
- throw new Error('Backup enrollment has an invalid or incomplete Codex row.');
50
- }
51
- const row = value as unknown as IRemoteControlEnrollment;
52
- if (!row.websocketUrl || !row.accountId
53
- || authorityEnrollmentId(row) !== id) throw new Error('Backup enrollment composite identity changed.');
54
- return row;
55
- };
56
-
57
- const assertJson = (value: unknown, depth = 0): void => {
58
- if (depth > 24) fail('Backup record exceeds the JSON depth limit.');
59
- if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
60
- if (typeof value === 'number' && Number.isFinite(value)) return;
61
- if (Array.isArray(value)) { for (const item of value) assertJson(item, depth + 1); return; }
62
- if (object(value)) { for (const item of Object.values(value)) assertJson(item, depth + 1); return; }
63
- fail('Backup record contains a non-JSON value.');
64
- };
65
-
66
- /** SmartData readback may reorder object fields; arrays and native source bytes remain exact. */
67
- const canonicalJson = (value: unknown): string => {
68
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
69
- if (object(value)) return `{${Object.keys(value).sort().map(key =>
70
- `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
71
- assertJson(value);
72
- return JSON.stringify(value);
73
- };
74
-
75
- export type IAuthorityBackupRecord = IAuthorityStoredRecord;
76
- export interface IAuthorityBackupSourceCapsule {
77
- kind: string;
78
- id: string;
79
- linkedModel: string;
80
- linkedId: string;
81
- linkedDigestField: 'sourceDigest' | 'before.credentialDigest' | 'before.configDigest';
82
- /** Exact bytes re-read under the native owner's exclusion; the caller retains and wipes its buffer. */
83
- bytes: Uint8Array;
84
- }
85
- export interface IAuthorityBackupSnapshotAttempt {
86
- epoch: string;
87
- revision: number;
88
- records: AsyncIterable<IAuthorityBackupRecord>;
89
- sources: AsyncIterable<IAuthorityBackupSourceCapsule>;
90
- }
91
- /** Implement with one SmartData session.withTransaction; each retry invokes visit anew. */
92
- export interface IAuthorityBackupSnapshotSource {
93
- capture(visit: (attempt: IAuthorityBackupSnapshotAttempt) => Promise<void>): Promise<void>;
94
- }
95
- export interface IAuthorityBackupObjectStore {
96
- prepare(signal?: AbortSignal): Promise<void>;
97
- putExact(path: string, bytes: Uint8Array, signal?: AbortSignal): Promise<void>;
98
- get(path: string, maxBytes: number, signal?: AbortSignal): Promise<Uint8Array>;
99
- }
100
-
101
- /** Thin adapter over SmartBucket's provider-probed, create-only exact upload API. */
102
- export class AuthoritySmartBucketStore implements IAuthorityBackupObjectStore {
103
- private capability?: plugins.smartbucket.IExactUploadCapability;
104
- constructor(private readonly bucket: plugins.smartbucket.Bucket) {}
105
-
106
- public async prepare(signal?: AbortSignal): Promise<void> {
107
- const result = await this.bucket.probeExactUploadCapability({ signal });
108
- if (!result.supported) throw new Error(`Exact backup uploads unavailable: ${result.reason}`);
109
- this.capability = result.capability;
110
- }
111
-
112
- public async putExact(path: string, bytes: Uint8Array, signal?: AbortSignal): Promise<void> {
113
- if (!this.capability) throw new Error('Exact upload capability has not been probed.');
114
- const source = plugins.stream.Readable.from((function* () {
115
- for (let offset = 0; offset < bytes.byteLength; offset += 1024 * 1024) {
116
- yield Buffer.from(bytes.subarray(offset, offset + 1024 * 1024));
117
- }
118
- })());
119
- try {
120
- await this.bucket.fastPutStreamExact({ capability: this.capability, path, readableStream: source,
121
- contentLength: bytes.byteLength, nativeObjectProperties: { contentType: 'application/json' }, signal });
122
- } finally { source.destroy(); }
123
- }
124
-
125
- public async get(path: string, maxBytes: number, signal?: AbortSignal): Promise<Uint8Array> {
126
- const source = await this.bucket.fastGetStream({ path, signal });
127
- const chunks: Buffer[] = [];
128
- let length = 0;
129
- try {
130
- for await (const chunk of source) {
131
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);
132
- length += bytes.byteLength;
133
- if (length > maxBytes) throw new Error('Backup object exceeds its bounded read limit.');
134
- chunks.push(bytes);
135
- }
136
- return Buffer.concat(chunks, length);
137
- } finally { source.destroy(); }
138
- }
139
- }
140
-
141
- export interface IAuthorityRestoredRecord extends IAuthorityBackupRecord {}
142
- export interface IAuthorityRestoredSource {
143
- kind: 'source'; sourceKind: string; sourceId: string; sourceDigest: string;
144
- linkedModel: string; linkedId: string;
145
- linkedDigestField: IAuthorityBackupSourceCapsule['linkedDigestField']; bytes: Uint8Array;
146
- }
147
- export type TAuthorityRestoredEntry = IAuthorityRestoredRecord | IAuthorityRestoredSource;
148
- export interface IAuthorityBackupRehearsalReadback {
149
- records: AsyncIterable<IAuthorityRestoredRecord>;
150
- sources: AsyncIterable<IAuthorityRestoredSource>;
151
- /** Stops the isolated database and child, then removes every disposable artifact. */
152
- close(): Promise<void>;
153
- }
154
- /** Must restore into an isolated, runtime-closed target and read its stored postimage back. */
155
- export interface IAuthorityBackupRehearsal {
156
- restoreClosed(entries: AsyncIterable<TAuthorityRestoredEntry>): Promise<IAuthorityBackupRehearsalReadback>;
157
- }
158
-
159
- interface IChunk { sequence: number; path: string; encryptedDigest: string; plaintextDigest: string; length: number; entries: number }
160
- export interface IAuthorityBackupSourceProof {
161
- kind: string; id: string; linkedModel: string; linkedId: string;
162
- linkedDigestField: IAuthorityBackupSourceCapsule['linkedDigestField']; digest: string;
163
- }
164
- interface IManifest {
165
- schemaVersion: 1; backupId: string; epoch: string; revision: number; recipientKeyId: string;
166
- chunks: IChunk[]; recordCount: number; sources: IAuthorityBackupSourceProof[]; snapshotDigest: string;
167
- }
168
- function assertManifest(value: unknown, backupId: string, prefix: string): asserts value is IManifest {
169
- if (!object(value) || !exactKeys(value, ['schemaVersion', 'backupId', 'epoch', 'revision',
170
- 'recipientKeyId', 'chunks', 'recordCount', 'sources', 'snapshotDigest'])
171
- || value.schemaVersion !== 1 || value.backupId !== backupId
172
- || typeof value.epoch !== 'string' || !/^[a-f0-9-]{36}$/.test(value.epoch)
173
- || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0
174
- || !isName(value.recipientKeyId) || !Number.isSafeInteger(value.recordCount)
175
- || Number(value.recordCount) < 1 || !isHash(value.snapshotDigest)
176
- || !Array.isArray(value.chunks) || !Array.isArray(value.sources)) {
177
- fail('Invalid authenticated backup manifest.');
178
- }
179
- for (let index = 0; index < value.chunks.length; index++) {
180
- const item: unknown = value.chunks[index];
181
- if (!object(item) || !exactKeys(item, ['sequence', 'path', 'encryptedDigest',
182
- 'plaintextDigest', 'length', 'entries']) || item.sequence !== index
183
- || item.path !== `${prefix}/${backupId}/chunk-${String(index).padStart(5, '0')}.json`
184
- || !isHash(item.encryptedDigest) || !isHash(item.plaintextDigest)
185
- || !Number.isSafeInteger(item.length) || Number(item.length) < 1
186
- || Number(item.length) > maxEncryptedObjectBytes
187
- || !Number.isSafeInteger(item.entries) || Number(item.entries) < 1) {
188
- fail('Invalid authenticated backup chunk index.');
189
- }
190
- }
191
- const sourceIds = new Set<string>();
192
- for (const item of value.sources) {
193
- if (!object(item) || !exactKeys(item, ['kind', 'id', 'linkedModel', 'linkedId', 'linkedDigestField', 'digest'])
194
- || !isName(item.kind) || !isName(item.id) || !isName(item.linkedModel)
195
- || !isName(item.linkedId) || !isDigestField(item.linkedDigestField) || !isHash(item.digest)) {
196
- fail('Invalid authenticated backup source index.');
197
- }
198
- const sourceKey = recordKey(item.kind, item.id);
199
- if (sourceIds.has(sourceKey)) fail('Duplicate authenticated backup source proof.');
200
- sourceIds.add(sourceKey);
201
- }
202
- }
203
- interface IRecordLine { kind: 'record'; model: string; id: string; value: unknown; secret: string | null }
204
- interface ISourceLine { kind: 'source'; sourceKind: string; sourceId: string; linkedModel: string;
205
- linkedId: string; linkedDigestField: IAuthorityBackupSourceCapsule['linkedDigestField'];
206
- sourceDigest: string; payload: string }
207
- type TLine = IRecordLine | ISourceLine;
208
- interface IStagedAttempt { dir: string; manifest: IManifest }
209
-
210
- /** Factual portable proof only. Production admission must additionally bind configured bucket and key custody. */
211
- export interface IAuthorityBackupVerification {
212
- backupId: string; manifestPath: string; manifestDigest: string; snapshotDigest: string; sourceEpoch: string;
213
- sourceRevision: number; restoredEpoch: string; recordCount: number; sources: IAuthorityBackupSourceProof[];
214
- rehearsalDigest: string; verifiedAt: string;
215
- /** Portable recovery proves data integrity, never that a surviving native writer has stopped. */
216
- restoreAdmission: 'closed_pending_codex_and_native_reconciliation';
217
- }
218
-
219
- /** An object locator and digest only; this carries no recoverability or deletion authority. */
220
- export interface IAuthorityBackupPublication {
221
- backupId: string;
222
- manifestPath: string;
223
- manifestDigest: string;
224
- sourceEpoch: string;
225
- sourceRevision: number;
226
- snapshotDigest: string;
227
- }
228
-
229
- export class AuthorityBackupPublicationUnknownError extends Error {
230
- constructor(public readonly path: string, cause: unknown) {
231
- super('Backup exact upload outcome could not be reconciled by readback.', { cause });
232
- }
233
- }
234
-
235
- interface IAuthorityBackupCommonOptions {
236
- store: IAuthorityBackupObjectStore;
237
- secrets: IAuthSwitchSecretCodec;
238
- prefix: string;
239
- recipientKeyId: string;
240
- signal?: AbortSignal;
241
- }
242
- export interface IAuthorityBackupPublisherOptions extends IAuthorityBackupCommonOptions {
243
- source: IAuthorityBackupSnapshotSource;
244
- recipientPublicKey: Uint8Array;
245
- }
246
- export interface IAuthorityBackupVerifierOptions extends IAuthorityBackupCommonOptions {
247
- rehearsal: IAuthorityBackupRehearsal;
248
- recipientPrivateKey: Uint8Array;
249
- }
250
- export type TAuthorityBackupOptions = IAuthorityBackupPublisherOptions | IAuthorityBackupVerifierOptions;
251
-
252
- /** Portable backup codec; the daemon's SmartData repository supplies the snapshot and persists the verified receipt. */
253
- export class AuthSwitchAuthorityBackup {
254
- constructor(private readonly options: TAuthorityBackupOptions) {}
255
-
256
- private publisher(): IAuthorityBackupPublisherOptions {
257
- if (!('source' in this.options)) fail('Backup publisher requires a committed snapshot source.');
258
- return this.options;
259
- }
260
-
261
- private verifier(): IAuthorityBackupVerifierOptions {
262
- if (!('rehearsal' in this.options)) fail('Backup verifier requires an isolated restore target.');
263
- return this.options;
264
- }
265
-
266
- private async secretFor(record: IAuthorityBackupRecord): Promise<string | null> {
267
- const value = record.value;
268
- let id: string | null = null;
269
- let sealed: string | null = null;
270
- if (record.model === 'authswitch_authority_grants') {
271
- assertStoredAuthorityGrant(value);
272
- id = value.id; sealed = value.ciphertext;
273
- } else if (record.model === 'authswitch_authority_enrollments') {
274
- assertStoredAuthorityEnrollment(value);
275
- id = value.id; sealed = value.ciphertext;
276
- } else if (record.model === 'authswitch_authority_claude_handoffs' && object(value)
277
- && typeof value.outgoingGrantId === 'string' && typeof value.sealedOutgoing === 'string') {
278
- id = value.outgoingGrantId; sealed = value.sealedOutgoing;
279
- }
280
- if (!id || !sealed) return null;
281
- const bytes = await this.options.secrets.unseal(id, sealed);
282
- try {
283
- if (record.model === 'authswitch_authority_enrollments') parseEnrollment(bytes, record.id);
284
- return Buffer.from(bytes).toString('base64url');
285
- } finally { bytes.fill(0); }
286
- }
287
-
288
- private sourceDigestInRecord(record: IAuthorityBackupRecord,
289
- field: IAuthorityBackupSourceCapsule['linkedDigestField']): string | null {
290
- if (!object(record.value)) return null;
291
- if (field === 'sourceDigest') return isHash(record.value.sourceDigest) ? record.value.sourceDigest : null;
292
- if (object(record.value.before)) {
293
- const digest = field === 'before.credentialDigest'
294
- ? record.value.before.credentialDigest : record.value.before.configDigest;
295
- return isHash(digest) ? digest : null;
296
- }
297
- return null;
298
- }
299
-
300
- private assertPreparedClaudeSourceCoverage(record: IAuthorityBackupRecord,
301
- sources: readonly IAuthorityBackupSourceProof[]): void {
302
- if (record.model !== 'authswitch_authority_claude_handoffs') return;
303
- assertStoredAuthorityClaudeHandoff(record.value);
304
- if (record.value.phase !== 'prepared') return;
305
- for (const field of ['before.credentialDigest', 'before.configDigest'] as const) {
306
- const digest = this.sourceDigestInRecord(record, field);
307
- if (digest !== null && !sources.some(source =>
308
- source.linkedDigestField === field && source.digest === digest)) {
309
- fail('Prepared Claude native source bytes are missing from the backup.');
310
- }
311
- }
312
- }
313
-
314
- private async stage(backupId: string, attempt: IAuthorityBackupSnapshotAttempt): Promise<IStagedAttempt> {
315
- const publisher = this.publisher();
316
- if (typeof attempt.epoch !== 'string' || !/^[a-f0-9-]{36}$/.test(attempt.epoch)
317
- || !Number.isSafeInteger(attempt.revision) || attempt.revision < 0) fail('Invalid backup snapshot identity.');
318
- const dir = await plugins.fs.promises.mkdtemp(plugins.path.join(plugins.os.tmpdir(), 'authswitch-backup-'));
319
- await plugins.fs.promises.chmod(dir, 0o700);
320
- const sources: Array<IAuthorityBackupSourceProof & { seen: boolean }> = [];
321
- const sourceIds = new Set<string>();
322
- const sourceLinks = new Map<string, Array<IAuthorityBackupSourceProof & { seen: boolean }>>();
323
- const lines: string[] = [];
324
- const chunks: IChunk[] = [];
325
- const snapshotHash = plugins.crypto.createHash('sha256');
326
- let chunkBytes = 0;
327
- let recordCount = 0;
328
- let metaCount = 0;
329
- let lastRecordKey = '';
330
- const append = async (line: TLine): Promise<void> => {
331
- const encoded = `${JSON.stringify(line)}\n`;
332
- const length = Buffer.byteLength(encoded);
333
- if (length > maxPlainChunkBytes) fail('Backup entry exceeds the encrypted chunk limit.');
334
- if (chunkBytes + length > maxPlainChunkBytes) await flush();
335
- lines.push(encoded); chunkBytes += length; snapshotHash.update(encoded);
336
- };
337
- const flush = async (): Promise<void> => {
338
- if (lines.length === 0) return;
339
- const sequence = chunks.length;
340
- const plaintext = Buffer.from(lines.join(''));
341
- const plaintextDigest = hash(plaintext);
342
- try {
343
- const envelope = await plugins.smartcrypto.sealX25519Envelope({ plaintext,
344
- recipientPublicKey: publisher.recipientPublicKey, recipientKeyId: this.options.recipientKeyId,
345
- context: context('chunk', backupId, attempt.epoch, attempt.revision, sequence, plaintextDigest) });
346
- const encrypted = Buffer.from(JSON.stringify(envelope));
347
- if (encrypted.byteLength > maxEncryptedObjectBytes) fail('Encrypted backup chunk exceeds its object bound.');
348
- const path = `${this.options.prefix}/${backupId}/chunk-${String(sequence).padStart(5, '0')}.json`;
349
- await plugins.fs.promises.writeFile(plugins.path.join(dir, `${sequence}.enc`), encrypted, { flag: 'wx', mode: 0o600 });
350
- chunks.push({ sequence, path, encryptedDigest: hash(encrypted), plaintextDigest,
351
- length: encrypted.byteLength, entries: lines.length });
352
- lines.length = 0; chunkBytes = 0;
353
- } finally { plaintext.fill(0); }
354
- };
355
- try {
356
- for await (const source of attempt.sources) {
357
- this.options.signal?.throwIfAborted();
358
- if (!isName(source.kind) || !isName(source.id)
359
- || !isName(source.linkedModel) || !isName(source.linkedId)
360
- || !isDigestField(source.linkedDigestField)
361
- || !(source.bytes instanceof Uint8Array) || source.bytes.byteLength === 0
362
- || source.bytes.byteLength > maxSourceBytes) fail('Invalid or oversized exact-source capsule.');
363
- const sourceKey = recordKey(source.kind, source.id);
364
- if (sourceIds.has(sourceKey)) fail('Duplicate exact-source capsule.');
365
- const bytes = Uint8Array.from(source.bytes);
366
- try {
367
- const proof = { kind: source.kind, id: source.id, linkedModel: source.linkedModel,
368
- linkedId: source.linkedId, linkedDigestField: source.linkedDigestField,
369
- digest: hash(bytes), seen: false };
370
- await append({ kind: 'source', sourceKind: proof.kind, sourceId: proof.id,
371
- linkedModel: proof.linkedModel, linkedId: proof.linkedId,
372
- linkedDigestField: proof.linkedDigestField,
373
- sourceDigest: proof.digest, payload: Buffer.from(bytes).toString('base64url') });
374
- sources.push(proof);
375
- sourceIds.add(sourceKey);
376
- const linkKey = recordKey(proof.linkedModel, proof.linkedId);
377
- const linked = sourceLinks.get(linkKey) ?? [];
378
- linked.push(proof);
379
- sourceLinks.set(linkKey, linked);
380
- } finally { bytes.fill(0); }
381
- }
382
- for await (const record of attempt.records) {
383
- this.options.signal?.throwIfAborted();
384
- if (!isName(record.model) || !isName(record.id) || !object(record.value)
385
- || record.value.id !== record.id) fail('Invalid backup record identity.');
386
- const key = recordKey(record.model, record.id);
387
- if (key <= lastRecordKey) fail('Backup records must be sorted by model and stable ID.');
388
- lastRecordKey = key;
389
- assertJson(record.value);
390
- if (Buffer.byteLength(JSON.stringify(record.value)) > maxRecordBytes) fail('Backup record exceeds its size bound.');
391
- assertSnapshotRecord(record);
392
- if (record.model === 'authswitch_authority_meta') {
393
- assertStoredAuthorityMeta(record.value);
394
- if (record.value.epoch !== attempt.epoch || record.value.revision !== attempt.revision) {
395
- fail('Backup metadata does not match the pinned snapshot.');
396
- }
397
- metaCount++;
398
- }
399
- for (const source of sourceLinks.get(key) ?? []) {
400
- if (this.sourceDigestInRecord(record, source.linkedDigestField) !== source.digest) {
401
- fail('Exact-source bytes differ from their linked persisted record.');
402
- }
403
- source.seen = true;
404
- }
405
- this.assertPreparedClaudeSourceCoverage(record, sourceLinks.get(key) ?? []);
406
- await append({ kind: 'record', model: record.model, id: record.id,
407
- value: record.value, secret: await this.secretFor(record) });
408
- recordCount++;
409
- }
410
- if (metaCount !== 1 || sources.some(source => !source.seen)) {
411
- fail('Backup is missing metadata or an exact-source linkage.');
412
- }
413
- await flush();
414
- return { dir, manifest: { schemaVersion: 1, backupId, epoch: attempt.epoch,
415
- revision: attempt.revision, recipientKeyId: this.options.recipientKeyId, chunks,
416
- recordCount, sources: sources.map(source => ({ kind: source.kind, id: source.id,
417
- linkedModel: source.linkedModel, linkedId: source.linkedId,
418
- linkedDigestField: source.linkedDigestField, digest: source.digest })),
419
- snapshotDigest: snapshotHash.digest('hex') } };
420
- } catch (error) {
421
- await plugins.fs.promises.rm(dir, { recursive: true, force: true });
422
- throw error;
423
- }
424
- }
425
-
426
- private async publish(path: string, bytes: Uint8Array): Promise<void> {
427
- try { await this.options.store.putExact(path, bytes, this.options.signal); }
428
- catch (error) {
429
- if (!(error instanceof plugins.smartbucket.ExactUploadError)
430
- || error.state.destinationPublication !== 'ambiguous') throw error;
431
- let found: Uint8Array;
432
- try { found = await this.options.store.get(path, bytes.byteLength, AbortSignal.timeout(30_000)); }
433
- catch (readError) { throw new AuthorityBackupPublicationUnknownError(path, readError); }
434
- if (hash(found) !== hash(bytes) || found.byteLength !== bytes.byteLength) {
435
- throw new AuthorityBackupPublicationUnknownError(path, error);
436
- }
437
- }
438
- const readback = await this.options.store.get(path, bytes.byteLength, this.options.signal);
439
- if (readback.byteLength !== bytes.byteLength || hash(readback) !== hash(bytes)) {
440
- fail('Exact backup object readback differs from the published bytes.');
441
- }
442
- }
443
-
444
- public async publishSnapshot(): Promise<IAuthorityBackupPublication> {
445
- const { source, recipientPublicKey } = this.publisher();
446
- const { store, prefix, recipientKeyId } = this.options;
447
- if (!/^[a-zA-Z0-9/_-]{1,256}$/.test(prefix) || prefix.startsWith('/') || prefix.endsWith('/')
448
- || prefix.includes('//') || !isName(recipientKeyId)
449
- || recipientPublicKey.byteLength !== 32) {
450
- fail('Invalid backup destination or recovery-key reference.');
451
- }
452
- const backupId = uuid();
453
- const stageHolder: { current: IStagedAttempt | null } = { current: null };
454
- try {
455
- await source.capture(async attempt => {
456
- if (stageHolder.current) await plugins.fs.promises.rm(stageHolder.current.dir, { recursive: true, force: true });
457
- stageHolder.current = null;
458
- stageHolder.current = await this.stage(backupId, attempt);
459
- });
460
- const completed = stageHolder.current;
461
- if (!completed) fail('Snapshot source committed without a backup attempt.');
462
- const manifestBytes = Buffer.from(JSON.stringify(completed.manifest));
463
- if (manifestBytes.byteLength > maxRecordBytes) fail('Backup manifest exceeds its size bound.');
464
- const manifestEnvelope = await plugins.smartcrypto.sealX25519Envelope({
465
- plaintext: manifestBytes, recipientPublicKey, recipientKeyId, context: context('manifest', backupId),
466
- });
467
- manifestBytes.fill(0);
468
- const encryptedManifest = Buffer.from(JSON.stringify(manifestEnvelope));
469
- const manifestPath = `${prefix}/${backupId}/manifest.json`;
470
- await store.prepare(this.options.signal);
471
- for (const chunk of completed.manifest.chunks) {
472
- this.options.signal?.throwIfAborted();
473
- const bytes = await plugins.fs.promises.readFile(plugins.path.join(completed.dir, `${chunk.sequence}.enc`));
474
- if (bytes.byteLength !== chunk.length || hash(bytes) !== chunk.encryptedDigest) {
475
- fail('Disposable encrypted backup staging changed before publication.');
476
- }
477
- await this.publish(chunk.path, bytes);
478
- }
479
- await this.publish(manifestPath, encryptedManifest);
480
- return { backupId, manifestPath, manifestDigest: hash(encryptedManifest),
481
- sourceEpoch: completed.manifest.epoch, sourceRevision: completed.manifest.revision,
482
- snapshotDigest: completed.manifest.snapshotDigest };
483
- } finally {
484
- if (stageHolder.current) await plugins.fs.promises.rm(stageHolder.current.dir, { recursive: true, force: true });
485
- }
486
- }
487
-
488
- /** Rehearse a previously published object set; no caller-supplied success flag is accepted. */
489
- public async rehearsePublished(input: IAuthorityBackupPublication): Promise<IAuthorityBackupVerification> {
490
- if (!/^[a-f0-9-]{36}$/.test(input.backupId) || !isHash(input.manifestDigest)
491
- || !/^[a-f0-9-]{36}$/.test(input.sourceEpoch)
492
- || !Number.isSafeInteger(input.sourceRevision) || input.sourceRevision < 0
493
- || !isHash(input.snapshotDigest)
494
- || input.manifestPath !== `${this.options.prefix}/${input.backupId}/manifest.json`) {
495
- fail('Invalid published backup reference.');
496
- }
497
- return this.verifyPublished(input);
498
- }
499
-
500
- private async verifyPublished(input: IAuthorityBackupPublication): Promise<IAuthorityBackupVerification> {
501
- const verifier = this.verifier();
502
- const encrypted = await this.options.store.get(input.manifestPath, maxEncryptedObjectBytes, this.options.signal);
503
- if (hash(encrypted) !== input.manifestDigest) fail('Backup manifest readback digest changed.');
504
- const opened = await plugins.smartcrypto.openX25519Envelope({
505
- envelope: JSON.parse(Buffer.from(encrypted).toString('utf8')),
506
- recipientPrivateKey: verifier.recipientPrivateKey,
507
- expectedRecipientKeyId: this.options.recipientKeyId,
508
- context: context('manifest', input.backupId),
509
- });
510
- let manifestValue: unknown;
511
- try {
512
- if (opened.byteLength > maxRecordBytes) fail('Authenticated backup manifest exceeds its size bound.');
513
- manifestValue = JSON.parse(Buffer.from(opened).toString('utf8'));
514
- }
515
- finally { opened.fill(0); }
516
- assertManifest(manifestValue, input.backupId, this.options.prefix);
517
- const manifest = manifestValue;
518
- if (manifest.recipientKeyId !== this.options.recipientKeyId
519
- || manifest.epoch !== input.sourceEpoch || manifest.revision !== input.sourceRevision
520
- || manifest.snapshotDigest !== input.snapshotDigest) {
521
- fail('Authenticated backup manifest does not match the committed snapshot.');
522
- }
523
- const sourceLinks = new Map<string, Array<{ proof: IAuthorityBackupSourceProof; index: number }>>();
524
- manifest.sources.forEach((proof, index) => {
525
- const key = recordKey(proof.linkedModel, proof.linkedId);
526
- const linked = sourceLinks.get(key) ?? [];
527
- linked.push({ proof, index });
528
- sourceLinks.set(key, linked);
529
- });
530
- const linkedSourceIndexes = new Set<number>();
531
- const restoredEpoch = uuid();
532
- const snapshotHash = plugins.crypto.createHash('sha256');
533
- const expectedRecordHash = plugins.crypto.createHash('sha256');
534
- const expectedSourceHash = plugins.crypto.createHash('sha256');
535
- let recordCount = 0;
536
- let sourceCount = 0;
537
- let expectedCount = 0;
538
- let expectedSourceCount = 0;
539
- let lastKey = '';
540
- let recordsStarted = false;
541
- let fullyConsumed = false;
542
- const self = this;
543
- const entries = async function* (): AsyncGenerator<TAuthorityRestoredEntry> {
544
- for (const chunk of manifest.chunks) {
545
- self.options.signal?.throwIfAborted();
546
- const encryptedChunk = await self.options.store.get(chunk.path, maxEncryptedObjectBytes, self.options.signal);
547
- if (encryptedChunk.byteLength !== chunk.length || hash(encryptedChunk) !== chunk.encryptedDigest) {
548
- fail('Published backup chunk differs from its authenticated manifest.');
549
- }
550
- const plaintext = await plugins.smartcrypto.openX25519Envelope({
551
- envelope: JSON.parse(Buffer.from(encryptedChunk).toString('utf8')),
552
- recipientPrivateKey: verifier.recipientPrivateKey,
553
- expectedRecipientKeyId: manifest.recipientKeyId,
554
- context: context('chunk', manifest.backupId, manifest.epoch, manifest.revision,
555
- chunk.sequence, chunk.plaintextDigest),
556
- });
557
- try {
558
- if (plaintext.byteLength > maxPlainChunkBytes || hash(plaintext) !== chunk.plaintextDigest) {
559
- fail('Published backup chunk plaintext digest changed.');
560
- }
561
- const serialized = Buffer.from(plaintext).toString('utf8');
562
- if (!serialized.endsWith('\n')) fail('Backup chunk ended with a truncated entry.');
563
- const lines = serialized.slice(0, -1).split('\n');
564
- if (lines.length !== chunk.entries) fail('Backup chunk entry count changed.');
565
- snapshotHash.update(serialized);
566
- for (const line of lines) {
567
- const entry: unknown = JSON.parse(line);
568
- if (!object(entry)) fail('Invalid backup entry.');
569
- if (entry.kind === 'source') {
570
- if (recordsStarted) fail('Exact-source capsules must precede backup records.');
571
- if (!isName(entry.sourceKind) || !isName(entry.sourceId)
572
- || !isName(entry.linkedModel) || !isName(entry.linkedId)
573
- || !isDigestField(entry.linkedDigestField)
574
- || !isHash(entry.sourceDigest) || typeof entry.payload !== 'string') {
575
- fail('Invalid exact-source backup capsule.');
576
- }
577
- const bytes = Buffer.from(entry.payload, 'base64url');
578
- if (bytes.byteLength > maxSourceBytes || hash(bytes) !== entry.sourceDigest) {
579
- fail('Exact-source capsule digest changed.');
580
- }
581
- const proof = manifest.sources[sourceCount];
582
- if (!proof || proof.kind !== entry.sourceKind || proof.id !== entry.sourceId
583
- || proof.linkedModel !== entry.linkedModel || proof.linkedId !== entry.linkedId
584
- || proof.linkedDigestField !== entry.linkedDigestField
585
- || proof.digest !== entry.sourceDigest) fail('Exact-source capsule order or coverage changed.');
586
- sourceCount++;
587
- const restored: IAuthorityRestoredSource = { kind: 'source', sourceKind: entry.sourceKind,
588
- sourceId: entry.sourceId, linkedModel: entry.linkedModel, linkedId: entry.linkedId,
589
- linkedDigestField: entry.linkedDigestField, sourceDigest: entry.sourceDigest, bytes };
590
- expectedSourceHash.update(canonicalJson({ ...restored,
591
- bytes: Buffer.from(bytes).toString('base64url') }) + '\n');
592
- expectedSourceCount++;
593
- yield restored;
594
- } else if (entry.kind === 'record') {
595
- recordsStarted = true;
596
- if (!isName(entry.model) || !isName(entry.id) || !object(entry.value)
597
- || entry.value.id !== entry.id || (entry.secret !== null && typeof entry.secret !== 'string')) {
598
- fail('Invalid backup record entry.');
599
- }
600
- const key = recordKey(entry.model, entry.id);
601
- if (key <= lastKey) fail('Backup record order changed.');
602
- lastKey = key; recordCount++;
603
- const originalRecord = { model: entry.model, id: entry.id, value: entry.value };
604
- assertSnapshotRecord(originalRecord);
605
- const linked = sourceLinks.get(key) ?? [];
606
- for (const { proof, index } of linked) {
607
- if (self.sourceDigestInRecord(originalRecord, proof.linkedDigestField) !== proof.digest) {
608
- fail('Authenticated source proof is not linked to its exact record digest.');
609
- }
610
- linkedSourceIndexes.add(index);
611
- }
612
- self.assertPreparedClaudeSourceCoverage(originalRecord, linked.map(item => item.proof));
613
- const restored = await self.restoreRecord({ model: entry.model, id: entry.id,
614
- value: entry.value }, entry.secret, restoredEpoch);
615
- if (restored) {
616
- expectedRecordHash.update(canonicalJson(restored) + '\n'); expectedCount++;
617
- yield restored;
618
- }
619
- } else fail('Unknown backup entry kind.');
620
- }
621
- } finally { plaintext.fill(0); }
622
- }
623
- if (linkedSourceIndexes.size !== manifest.sources.length) {
624
- fail('Authenticated source proof lacks a linked persisted record.');
625
- }
626
- if (recordCount !== manifest.recordCount || sourceCount !== manifest.sources.length
627
- || snapshotHash.digest('hex') !== manifest.snapshotDigest) {
628
- fail('Published backup snapshot is incomplete.');
629
- }
630
- fullyConsumed = true;
631
- };
632
- const readback = await verifier.rehearsal.restoreClosed(entries());
633
- let rehearsalDigest: string | undefined;
634
- let verificationFailure: unknown;
635
- try {
636
- if (!fullyConsumed) fail('Restore rehearsal did not consume the complete backup.');
637
- const actualRecordHash = plugins.crypto.createHash('sha256');
638
- const actualSourceHash = plugins.crypto.createHash('sha256');
639
- let actualCount = 0;
640
- let actualSourceCount = 0;
641
- let actualLastKey = '';
642
- for await (const record of readback.records) {
643
- if (!isName(record.model) || !isName(record.id) || !object(record.value)
644
- || record.value.id !== record.id) fail('Rehearsal readback returned an invalid record.');
645
- const key = recordKey(record.model, record.id);
646
- if (key <= actualLastKey) fail('Rehearsal readback order changed.');
647
- actualLastKey = key;
648
- actualRecordHash.update(canonicalJson(record) + '\n'); actualCount++;
649
- }
650
- for await (const source of readback.sources) {
651
- if (source.kind !== 'source' || !isHash(source.sourceDigest)
652
- || hash(source.bytes) !== source.sourceDigest) fail('Rehearsal source readback changed.');
653
- actualSourceHash.update(canonicalJson({ ...source,
654
- bytes: Buffer.from(source.bytes).toString('base64url') }) + '\n');
655
- actualSourceCount++;
656
- }
657
- const expectedRecordDigest = expectedRecordHash.digest('hex');
658
- const expectedSourceDigest = expectedSourceHash.digest('hex');
659
- if (actualCount !== expectedCount || actualSourceCount !== expectedSourceCount
660
- || actualRecordHash.digest('hex') !== expectedRecordDigest
661
- || actualSourceHash.digest('hex') !== expectedSourceDigest) {
662
- fail('Isolated restore readback differs from expected safe state.');
663
- }
664
- rehearsalDigest = hash(JSON.stringify([expectedRecordDigest, expectedSourceDigest]));
665
- } catch (error) {
666
- verificationFailure = error;
667
- }
668
- let closeFailure: unknown;
669
- // A matching postimage is not a receipt while the isolated owner still has a live database or child.
670
- try { await readback.close(); } catch (error) { closeFailure = error; }
671
- if (verificationFailure && closeFailure) {
672
- throw new AggregateError([verificationFailure, closeFailure],
673
- 'Restore rehearsal verification and cleanup both failed.');
674
- }
675
- if (verificationFailure) throw verificationFailure;
676
- if (closeFailure) throw closeFailure;
677
- if (!rehearsalDigest) fail('Restore rehearsal did not produce an exact postimage digest.');
678
- return { backupId: manifest.backupId, manifestPath: input.manifestPath,
679
- manifestDigest: input.manifestDigest, snapshotDigest: manifest.snapshotDigest, sourceEpoch: manifest.epoch,
680
- sourceRevision: manifest.revision, restoredEpoch, recordCount: manifest.recordCount,
681
- sources: manifest.sources, rehearsalDigest, verifiedAt: new Date().toISOString(),
682
- restoreAdmission: 'closed_pending_codex_and_native_reconciliation' };
683
- }
684
-
685
- private async restoreRecord(record: IAuthorityBackupRecord, secret: string | null,
686
- restoredEpoch: string): Promise<IAuthorityRestoredRecord | null> {
687
- const value = record.value;
688
- if (!object(value)) fail('Invalid stored backup record.');
689
- assertSnapshotRecord(record);
690
- if (record.model === 'authswitch_authority_events') return null;
691
- if (record.model === 'authswitch_authority_usage') return null;
692
- if (record.model === 'authswitch_authority_meta') {
693
- assertStoredAuthorityMeta(value);
694
- const restored: IStoredAuthorityMeta = { ...value, admission: 'closed_restored',
695
- epoch: restoredEpoch, revision: 0, updateId: uuid() };
696
- return { ...record, value: restored };
697
- }
698
- if (record.model === 'authswitch_authority_grants') {
699
- assertStoredAuthorityGrant(value);
700
- if ((value.ciphertext !== null) !== (secret !== null)) fail('Grant recovery payload is incomplete.');
701
- if (value.state === 'removed') return record;
702
- const native = ['claude_host_native', 'opencode_native'].includes(value.purpose);
703
- const restored: IStoredAuthorityGrant = { ...value,
704
- state: native ? 'legacy_native_pending' : 'needs_reauth',
705
- owner: native ? 'legacy_native' : 'daemon', ciphertext: null, accessExpiresAt: null,
706
- attemptId: null, retryAt: null, retryCount: 0,
707
- problem: native ? 'native_owner' : 'exchange_uncertain',
708
- grantGeneration: value.grantGeneration + 1,
709
- authorizationGeneration: value.authorizationGeneration + 1,
710
- revision: value.revision + 1, statusObservedAt: new Date().toISOString(), updateId: uuid() };
711
- assertStoredAuthorityGrant(restored);
712
- return { ...record, value: restored };
713
- }
714
- if (record.model === 'authswitch_authority_bindings') {
715
- assertStoredAuthorityBinding(value);
716
- const restored: IStoredAuthorityBinding = { ...value,
717
- capabilityHash: hash(plugins.crypto.randomBytes(32)), revision: value.revision + 1,
718
- updateId: uuid() };
719
- return { ...record, value: restored };
720
- }
721
- if (record.model === 'authswitch_authority_enrollments') {
722
- assertStoredAuthorityEnrollment(value);
723
- if (secret === null) fail('Enrollment recovery payload is missing.');
724
- const plaintext = Buffer.from(secret, 'base64url');
725
- try {
726
- const row = parseEnrollment(plaintext, value.id);
727
- const sealed = await this.options.secrets.seal(value.id, plaintext);
728
- const opened = await this.options.secrets.unseal(value.id, sealed);
729
- try {
730
- if (Buffer.compare(Buffer.from(opened), plaintext) !== 0) fail('Destination enrollment reseal did not verify.');
731
- } finally { opened.fill(0); }
732
- const restored: IStoredAuthorityEnrollment = { ...value, ciphertext: sealed,
733
- revision: value.revision + 1, updateId: uuid() };
734
- assertStoredAuthorityEnrollment(restored);
735
- if (authorityEnrollmentId(row) !== restored.id) fail('Enrollment changed during destination reseal.');
736
- return { ...record, value: restored };
737
- } finally { plaintext.fill(0); }
738
- }
739
- if (record.model === 'authswitch_authority_codex_homes') {
740
- assertStoredAuthorityCodexHome(value);
741
- const restored: IStoredAuthorityCodexHome = { ...value, activeRun: null, updateId: uuid() };
742
- return { ...record, value: restored };
743
- }
744
- if (record.model === 'authswitch_authority_claude_homes') {
745
- const restored = { ...value, status: 'quarantined', pendingOperationId: uuid(),
746
- revision: Number(value.revision) + 1, updateId: uuid() };
747
- assertStoredAuthorityClaudeHome(restored);
748
- return { ...record, value: restored };
749
- }
750
- if (record.model === 'authswitch_authority_claude_handoffs') {
751
- const restored = { ...value, phase: 'quarantined',
752
- problem: 'native_uncertain', sealedOutgoing: null,
753
- revision: Number(value.revision) + 1, updateId: uuid() };
754
- assertStoredAuthorityClaudeHandoff(restored);
755
- return { ...record, value: restored };
756
- }
757
- if (record.model === 'authswitch_authority_handoffs') {
758
- const restored = { ...value, phase: 'needs_reauth',
759
- backupDigest: null, verifiedSourceDigest: null,
760
- revision: Number(value.revision) + 1, updateId: uuid() };
761
- assertStoredAuthorityHandoff(restored);
762
- return { ...record, value: restored };
763
- }
764
- if (record.model === 'authswitch_authority_migration_ledger') {
765
- const restored = { ...value, status: 'prepared', backupDigest: null,
766
- revision: Number(value.revision) + 1, updateId: uuid() };
767
- assertStoredAuthorityMigrationLedger(restored);
768
- return { ...record, value: restored };
769
- }
770
- if (record.model === 'authswitch_authority_operations') {
771
- if (secret !== null) fail('Unexpected executable secret in backup operation.');
772
- assertStoredAuthorityOperation(value);
773
- const restored = restoreStoredAuthorityOperation(value,
774
- { restoredAt: new Date().toISOString(), updateId: uuid() });
775
- return restored === value ? record : { ...record, value: restored };
776
- }
777
- if (secret !== null) fail('Unexpected executable secret in backup record.');
778
- return record;
779
- }
780
- }