@modelprofile.com/authswitch 6.4.0 → 7.0.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 (98) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +302 -6
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-runtime-contract.d.ts +15 -0
  5. package/dist_ts/authorityrehearsal.child.d.ts +1 -0
  6. package/dist_ts/authorityrehearsal.child.js +395 -0
  7. package/dist_ts/classes.accountlist.js +4 -3
  8. package/dist_ts/classes.authoritybackup.d.ts +134 -0
  9. package/dist_ts/classes.authoritybackup.js +769 -0
  10. package/dist_ts/classes.authoritybroker.d.ts +29 -6
  11. package/dist_ts/classes.authoritybroker.js +413 -122
  12. package/dist_ts/classes.authoritycli.d.ts +24 -0
  13. package/dist_ts/classes.authoritycli.js +675 -0
  14. package/dist_ts/classes.authorityclient.d.ts +34 -9
  15. package/dist_ts/classes.authorityclient.js +136 -38
  16. package/dist_ts/classes.authoritydaemon.d.ts +47 -0
  17. package/dist_ts/classes.authoritydaemon.js +705 -16
  18. package/dist_ts/classes.authoritydatabase.d.ts +182 -8
  19. package/dist_ts/classes.authoritydatabase.js +1188 -41
  20. package/dist_ts/classes.authoritymodels.d.ts +247 -1
  21. package/dist_ts/classes.authoritymodels.js +870 -6
  22. package/dist_ts/classes.authoritypreuse.d.ts +47 -0
  23. package/dist_ts/classes.authoritypreuse.js +278 -0
  24. package/dist_ts/classes.authorityregistry.d.ts +24 -0
  25. package/dist_ts/classes.authorityregistry.js +88 -0
  26. package/dist_ts/classes.authorityrehearsal.d.ts +127 -0
  27. package/dist_ts/classes.authorityrehearsal.js +632 -0
  28. package/dist_ts/classes.authorityusage.d.ts +160 -0
  29. package/dist_ts/classes.authorityusage.js +429 -0
  30. package/dist_ts/classes.claudeauthority.d.ts +66 -0
  31. package/dist_ts/classes.claudeauthority.js +532 -0
  32. package/dist_ts/classes.claudecodeharness.js +3 -2
  33. package/dist_ts/classes.claudecodelocks.d.ts +13 -0
  34. package/dist_ts/classes.claudecodelocks.js +75 -3
  35. package/dist_ts/classes.claudenative.d.ts +137 -0
  36. package/dist_ts/classes.claudenative.js +731 -0
  37. package/dist_ts/classes.claudestatus.d.ts +9 -0
  38. package/dist_ts/classes.claudestatus.js +26 -9
  39. package/dist_ts/classes.claudetokenrefresh.d.ts +15 -1
  40. package/dist_ts/classes.claudetokenrefresh.js +74 -21
  41. package/dist_ts/classes.codexharness.js +4 -2
  42. package/dist_ts/classes.codexmanaged.d.ts +91 -0
  43. package/dist_ts/classes.codexmanaged.js +404 -0
  44. package/dist_ts/classes.codexpreuse.d.ts +8 -0
  45. package/dist_ts/classes.codexpreuse.js +24 -6
  46. package/dist_ts/classes.fileharness.d.ts +2 -0
  47. package/dist_ts/classes.fileharness.js +6 -4
  48. package/dist_ts/classes.limits.js +12 -7
  49. package/dist_ts/classes.opencodeharness.js +2 -2
  50. package/dist_ts/claudehttp.d.ts +10 -0
  51. package/dist_ts/claudehttp.js +29 -3
  52. package/dist_ts/index.d.ts +2 -0
  53. package/dist_ts/index.js +8 -1
  54. package/dist_ts/interfaces.harness.d.ts +6 -0
  55. package/dist_ts/interfaces.list.d.ts +9 -3
  56. package/dist_ts/plugins.d.ts +7 -3
  57. package/dist_ts/plugins.js +9 -4
  58. package/dist_ts/ts_migration/0001_authority_meta.d.ts +3 -0
  59. package/dist_ts/ts_migration/0001_authority_meta.js +32 -0
  60. package/dist_ts/ts_migration/index.d.ts +3 -0
  61. package/dist_ts/ts_migration/index.js +6 -0
  62. package/package.json +12 -2
  63. package/readme.md +205 -13
  64. package/ts/00_commitinfo_data.ts +1 -1
  65. package/ts/authority-contract.ts +264 -9
  66. package/ts/authority-runtime-contract.ts +21 -1
  67. package/ts/authorityrehearsal.child.ts +330 -0
  68. package/ts/classes.accountlist.ts +3 -2
  69. package/ts/classes.authoritybackup.ts +780 -0
  70. package/ts/classes.authoritybroker.ts +410 -120
  71. package/ts/classes.authoritycli.ts +719 -0
  72. package/ts/classes.authorityclient.ts +164 -38
  73. package/ts/classes.authoritydaemon.ts +614 -17
  74. package/ts/classes.authoritydatabase.ts +1229 -44
  75. package/ts/classes.authoritymodels.ts +579 -4
  76. package/ts/classes.authoritypreuse.ts +297 -0
  77. package/ts/classes.authorityregistry.ts +118 -0
  78. package/ts/classes.authorityrehearsal.ts +625 -0
  79. package/ts/classes.authorityusage.ts +527 -0
  80. package/ts/classes.claudeauthority.ts +541 -0
  81. package/ts/classes.claudecodeharness.ts +2 -1
  82. package/ts/classes.claudecodelocks.ts +70 -1
  83. package/ts/classes.claudenative.ts +816 -0
  84. package/ts/classes.claudestatus.ts +38 -9
  85. package/ts/classes.claudetokenrefresh.ts +82 -19
  86. package/ts/classes.codexharness.ts +3 -1
  87. package/ts/classes.codexmanaged.ts +402 -0
  88. package/ts/classes.codexpreuse.ts +33 -5
  89. package/ts/classes.fileharness.ts +7 -3
  90. package/ts/classes.limits.ts +11 -6
  91. package/ts/classes.opencodeharness.ts +1 -1
  92. package/ts/claudehttp.ts +31 -2
  93. package/ts/index.ts +7 -0
  94. package/ts/interfaces.harness.ts +6 -0
  95. package/ts/interfaces.list.ts +9 -3
  96. package/ts/plugins.ts +9 -3
  97. package/ts/ts_migration/0001_authority_meta.ts +33 -0
  98. package/ts/ts_migration/index.ts +7 -0
@@ -1,28 +1,81 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import {
3
- AuthSwitchAuthorityAccountModel, AuthSwitchAuthorityBindingModel, AuthSwitchAuthorityEnrollmentModel,
3
+ AuthSwitchAuthorityAccountModel, AuthSwitchAuthorityBindingModel, AuthSwitchAuthorityClaudeHandoffModel,
4
+ AuthSwitchAuthorityClaudeHomeModel, AuthSwitchAuthorityCodexHomeModel, AuthSwitchAuthorityEnrollmentModel,
4
5
  AuthSwitchAuthorityEventModel, AuthSwitchAuthorityGrantModel, AuthSwitchAuthorityHandoffModel,
5
- AuthSwitchAuthorityMetaModel, AuthSwitchAuthorityMigrationLedgerModel, assertStoredAuthorityAccount,
6
- assertStoredAuthorityBinding, assertStoredAuthorityEnrollment, assertStoredAuthorityGrant,
7
- assertStoredAuthorityHandoff, assertStoredAuthorityMigrationLedger,
8
- type IStoredAuthorityAccount, type IStoredAuthorityBinding, type IStoredAuthorityEnrollment,
6
+ AuthSwitchAuthorityMetaModel, AuthSwitchAuthorityMigrationLedgerModel, AuthSwitchAuthorityOperationModel,
7
+ AuthSwitchAuthorityUsageModel, authorityUsageId, assertStoredAuthorityAccount,
8
+ assertStoredAuthorityBinding, assertStoredAuthorityClaudeHandoff, assertStoredAuthorityClaudeHome,
9
+ assertStoredAuthorityEnrollment, assertStoredAuthorityGrant,
10
+ assertStoredAuthorityHandoff, assertStoredAuthorityMigrationLedger, assertStoredAuthorityOperation,
11
+ assertStoredAuthorityUsage,
12
+ type IStoredAuthorityAccount, type IStoredAuthorityBinding, type IStoredAuthorityClaudeHandoff,
13
+ type IStoredAuthorityClaudeHome, type IStoredAuthorityCodexHome, type IStoredAuthorityEnrollment,
9
14
  type IStoredAuthorityEvent, type IStoredAuthorityGrant, type IStoredAuthorityHandoff,
10
- type IStoredAuthorityMeta, type IStoredAuthorityMigrationLedger,
15
+ type IStoredAuthorityDeviceOperation, type IStoredAuthorityMeta, type IStoredAuthorityMigrationLedger,
16
+ type IStoredAuthorityOperation, type IStoredAuthorityPreuseOperation, type IStoredAuthorityUsage,
11
17
  } from './classes.authoritymodels.js';
18
+ import type { IAuthSwitchStoredUsage, IAuthSwitchUsageContext } from './classes.authorityusage.js';
19
+ import { authorityModel, authorityModelRegistry, authorityStoredRecords,
20
+ assertAuthorityStoredRecord, type IAuthorityStoredRecord } from './classes.authorityregistry.js';
21
+ import type { IAuthorityBackupSnapshotAttempt, IAuthorityBackupSourceCapsule } from './classes.authoritybackup.js';
22
+ import { runAuthorityMigrations } from './ts_migration/index.js';
12
23
 
13
24
  export interface IAuthSwitchAuthorityDatabaseOptions {
14
25
  dataDirectory: string;
15
26
  socketPath: string;
16
27
  }
17
28
 
29
+ export interface IAuthoritySnapshotCaptureOptions {
30
+ /** Recreate capsules on every transaction retry from a source already held under qualified exclusion. */
31
+ sourceFactory: () => AsyncIterable<IAuthorityBackupSourceCapsule>;
32
+ signal?: AbortSignal;
33
+ pageSize?: number;
34
+ }
35
+
18
36
  type TEventKind = IStoredAuthorityEvent['kind'];
19
37
  type TSession = plugins.smartdata.SmartdataSession;
38
+ type TStoredAuthorityDeviceOperationAdmission = Omit<IStoredAuthorityDeviceOperation, 'expectedGrantGeneration'>;
39
+ type TClaudeHandoffDraft = {
40
+ home: IStoredAuthorityClaudeHome;
41
+ handoff: IStoredAuthorityClaudeHandoff;
42
+ outgoing: IStoredAuthorityGrant;
43
+ incoming: IStoredAuthorityGrant;
44
+ };
45
+
46
+ const usageContext = (meta: IStoredAuthorityMeta, account: IStoredAuthorityAccount,
47
+ grant: IStoredAuthorityGrant): IAuthSwitchUsageContext => ({
48
+ epoch: meta.epoch, accountId: account.id, loginId: grant.id, subjectId: account.subject,
49
+ workspaceId: account.workspaceId, providerId: account.providerId,
50
+ purpose: grant.purpose, owner: grant.owner, grantState: grant.state,
51
+ removed: account.removed, authorizationGeneration: grant.authorizationGeneration,
52
+ grantGeneration: grant.grantGeneration, nativeSource: null,
53
+ });
54
+ const sameUsageContext = (left: IAuthSwitchUsageContext, right: IAuthSwitchUsageContext): boolean =>
55
+ left.epoch === right.epoch && left.accountId === right.accountId && left.loginId === right.loginId
56
+ && left.subjectId === right.subjectId && left.workspaceId === right.workspaceId
57
+ && left.providerId === right.providerId
58
+ && left.purpose === right.purpose && left.owner === right.owner && left.grantState === right.grantState
59
+ && left.removed === right.removed && left.authorizationGeneration === right.authorizationGeneration
60
+ && left.grantGeneration === right.grantGeneration;
61
+ const sameUsageSource = (left: IAuthSwitchUsageContext, right: IAuthSwitchUsageContext): boolean =>
62
+ sameUsageContext(left, right) && (left.nativeSource === null ? right.nativeSource === null
63
+ : right.nativeSource !== null && left.nativeSource.homeId === right.nativeSource.homeId
64
+ && left.nativeSource.credentialDigest === right.nativeSource.credentialDigest
65
+ && left.nativeSource.configDigest === right.nativeSource.configDigest
66
+ && left.nativeSource.accessTokenDigest === right.nativeSource.accessTokenDigest);
67
+ const storedOperation = (stored: Parameters<typeof AuthSwitchAuthorityOperationModel.exact.toPersisted>[0]): IStoredAuthorityOperation => {
68
+ const operation = AuthSwitchAuthorityOperationModel.exact.toPersisted(stored);
69
+ assertStoredAuthorityOperation(operation);
70
+ return operation;
71
+ };
20
72
 
21
73
  /** Separate account, grant, binding and journal records; the only global record is a revision counter. */
22
74
  export class AuthSwitchAuthorityDatabase {
23
75
  private engine?: plugins.smartdb.LocalSmartDb;
24
76
  private database?: plugins.smartdata.SmartdataDb;
25
77
  private started = false;
78
+ private mode: 'normal' | 'closed_restore' | null = null;
26
79
 
27
80
  constructor(private readonly options: IAuthSwitchAuthorityDatabaseOptions) {}
28
81
 
@@ -35,8 +88,10 @@ export class AuthSwitchAuthorityDatabase {
35
88
  await plugins.fs.promises.chmod(pathArg, 0o700);
36
89
  }
37
90
 
38
- public async start(): Promise<void> {
91
+ public async start(options: { mode?: 'normal' | 'closed_restore' } = {}): Promise<void> {
39
92
  if (this.started || this.engine || this.database) throw new Error('Authswitch database is already starting or started.');
93
+ const mode = options.mode ?? 'normal';
94
+ if (mode !== 'normal' && mode !== 'closed_restore') throw new Error('Invalid authority database start mode.');
40
95
  await this.privateDirectory(this.options.dataDirectory);
41
96
  await this.privateDirectory(plugins.path.dirname(this.options.socketPath));
42
97
  const engine = new plugins.smartdb.LocalSmartDb({ folderPath: this.options.dataDirectory, socketPath: this.options.socketPath });
@@ -45,26 +100,41 @@ export class AuthSwitchAuthorityDatabase {
45
100
  const connection = await engine.start();
46
101
  const database = new plugins.smartdata.SmartdataDb({ mongoDbUrl: connection.connectionUri, mongoDbName: 'authswitch' });
47
102
  this.database = database;
48
- for (const model of [AuthSwitchAuthorityMetaModel, AuthSwitchAuthorityAccountModel,
49
- AuthSwitchAuthorityGrantModel, AuthSwitchAuthorityBindingModel, AuthSwitchAuthorityEnrollmentModel,
50
- AuthSwitchAuthorityHandoffModel, AuthSwitchAuthorityMigrationLedgerModel, AuthSwitchAuthorityEventModel]) {
51
- plugins.smartdata.setDefaultManagerForDoc({ db: database }, model);
52
- }
53
103
  await database.init();
54
- await Promise.all([AuthSwitchAuthorityMetaModel.init(), AuthSwitchAuthorityAccountModel.init(),
55
- AuthSwitchAuthorityGrantModel.init(), AuthSwitchAuthorityBindingModel.init(),
56
- AuthSwitchAuthorityEnrollmentModel.init(), AuthSwitchAuthorityHandoffModel.init(),
57
- AuthSwitchAuthorityMigrationLedgerModel.init(), AuthSwitchAuthorityEventModel.init()]);
58
- await Promise.all([AuthSwitchAuthorityMetaModel.exact.prepare(), AuthSwitchAuthorityAccountModel.exact.prepare(),
59
- AuthSwitchAuthorityGrantModel.exact.prepare(), AuthSwitchAuthorityBindingModel.exact.prepare(),
60
- AuthSwitchAuthorityEnrollmentModel.exact.prepare(), AuthSwitchAuthorityHandoffModel.exact.prepare(),
61
- AuthSwitchAuthorityMigrationLedgerModel.exact.prepare(), AuthSwitchAuthorityEventModel.exact.prepare()]);
62
- const initial: IStoredAuthorityMeta = {
63
- id: 'authswitch-authority', epoch: plugins.crypto.randomUUID(), revision: 0,
64
- updateId: plugins.crypto.randomUUID(),
65
- };
66
- const result = await AuthSwitchAuthorityMetaModel.exact.insert(initial);
67
- if (result.status === 'conflict' && result.document.id !== initial.id) throw new Error('Authswitch authority could not initialize.');
104
+ // The LocalSmartDb root lease excludes another engine during this catalog admission.
105
+ const inventory = await database.inspectNamespace({ maxCollections: 1024,
106
+ documentCountUpTo: 0, timeoutMs: 30_000 });
107
+ const names = new Set(authorityModelRegistry.map(entry => entry.name));
108
+ for (const collection of inventory.collections) {
109
+ if (collection.namespaceType !== 'collection' || !names.has(collection.collectionName)) {
110
+ throw new Error('Authswitch authority database contains an unknown collection or namespace.');
111
+ }
112
+ }
113
+ const fresh = inventory.collections.length === 0;
114
+ if (mode === 'closed_restore' && !fresh) {
115
+ throw new Error('Closed authority restore requires an empty database.');
116
+ }
117
+ if (mode === 'normal' && !fresh) {
118
+ if (!inventory.collections.some(item => item.collectionName === 'authswitch_authority_meta')) {
119
+ throw new Error('Existing authswitch authority database has no metadata collection.');
120
+ }
121
+ await runAuthorityMigrations(database);
122
+ }
123
+ await Promise.all(authorityModelRegistry.map(entry => entry.initialize(database)));
124
+ await Promise.all(authorityModelRegistry.map(entry => entry.prepare()));
125
+ if (mode === 'normal') {
126
+ if (fresh) {
127
+ const initial: IStoredAuthorityMeta = {
128
+ id: 'authswitch-authority', schemaVersion: 2, admission: 'open',
129
+ epoch: plugins.crypto.randomUUID(), revision: 0, updateId: plugins.crypto.randomUUID(),
130
+ };
131
+ const result = await AuthSwitchAuthorityMetaModel.exact.insert(initial);
132
+ if (result.status !== 'inserted') throw new Error('Authswitch authority could not initialize.');
133
+ } else if (!await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' })) {
134
+ throw new Error('Existing authswitch authority metadata is missing.');
135
+ }
136
+ }
137
+ this.mode = mode;
68
138
  this.started = true;
69
139
  } catch (error) {
70
140
  try { await this.stop(); } catch { /* Preserve the original startup error. */ }
@@ -86,6 +156,70 @@ export class AuthSwitchAuthorityDatabase {
86
156
  } finally { await session.close(); }
87
157
  }
88
158
 
159
+ /** One repeatable SmartData transaction per capture attempt; portable staging happens inside visit. */
160
+ public async captureBackupSnapshot(options: IAuthoritySnapshotCaptureOptions,
161
+ visit: (attempt: IAuthorityBackupSnapshotAttempt) => Promise<void>): Promise<void> {
162
+ if (typeof options.sourceFactory !== 'function' || typeof visit !== 'function') {
163
+ throw new Error('Authority backup capture requires an exact source and visitor.');
164
+ }
165
+ await this.transaction(async session => {
166
+ options.signal?.throwIfAborted();
167
+ const stored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' },
168
+ { session, signal: options.signal });
169
+ if (!stored) throw new Error('Authority backup metadata is absent.');
170
+ const meta = AuthSwitchAuthorityMetaModel.exact.toPersisted(stored);
171
+ if (meta.admission !== 'open') throw new Error('Closed restored authority cannot publish a backup.');
172
+ let recordsComplete = false;
173
+ let sourcesComplete = false;
174
+ const records = async function* (): AsyncGenerator<IAuthorityStoredRecord> {
175
+ yield* authorityStoredRecords({ session, signal: options.signal, pageSize: options.pageSize });
176
+ recordsComplete = true;
177
+ };
178
+ const sources = async function* (): AsyncGenerator<IAuthorityBackupSourceCapsule> {
179
+ for await (const source of options.sourceFactory()) {
180
+ options.signal?.throwIfAborted();
181
+ yield source;
182
+ }
183
+ sourcesComplete = true;
184
+ };
185
+ await visit({ epoch: meta.epoch, revision: meta.revision, records: records(), sources: sources() });
186
+ if (!recordsComplete || !sourcesComplete) {
187
+ throw new Error('Authority backup visitor did not consume the complete pinned inventory.');
188
+ }
189
+ return true;
190
+ });
191
+ }
192
+
193
+ /** Reopened closed restore readback uses the same exhaustive registry and stable keyset order. */
194
+ public readStoredRecords(options: { signal?: AbortSignal; pageSize?: number } = {}): AsyncIterable<IAuthorityStoredRecord> {
195
+ this.db();
196
+ return authorityStoredRecords(options);
197
+ }
198
+
199
+ public async restoreClosedMeta(meta: IStoredAuthorityMeta): Promise<void> {
200
+ if (this.mode !== 'closed_restore') throw new Error('Authority database is not a closed restore target.');
201
+ const record: IAuthorityStoredRecord = { model: 'authswitch_authority_meta', id: meta.id, value: meta };
202
+ assertAuthorityStoredRecord(record);
203
+ if (meta.admission !== 'closed_restored') throw new Error('Restored authority metadata must remain closed.');
204
+ await authorityModel(record.model).insert(record);
205
+ }
206
+
207
+ public async insertClosedRestoredRecord(record: IAuthorityStoredRecord): Promise<void> {
208
+ if (this.mode !== 'closed_restore' || record.model === 'authswitch_authority_meta') {
209
+ throw new Error('Authority database is not admitting this restored record.');
210
+ }
211
+ if ((await this.readMeta()).admission !== 'closed_restored') {
212
+ throw new Error('Closed restore metadata must be installed before records.');
213
+ }
214
+ assertAuthorityStoredRecord(record);
215
+ await authorityModel(record.model).insert(record);
216
+ }
217
+
218
+ public async assertOpenAdmission(): Promise<void> {
219
+ const meta = await this.readMeta();
220
+ if (meta.admission !== 'open') throw new Error('Restored authority admission is closed pending ownership reconciliation.');
221
+ }
222
+
89
223
  public async readMeta(): Promise<IStoredAuthorityMeta> {
90
224
  this.db();
91
225
  const stored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' });
@@ -105,6 +239,89 @@ export class AuthSwitchAuthorityDatabase {
105
239
  return stored ? AuthSwitchAuthorityBindingModel.exact.toPersisted(stored) : null;
106
240
  }
107
241
 
242
+ public async readCodexHome(id: string): Promise<IStoredAuthorityCodexHome | null> {
243
+ this.db();
244
+ const stored = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id });
245
+ return stored ? AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(stored) : null;
246
+ }
247
+
248
+ /** Durable home claims must be inspected even when the daemon has lost its process map. */
249
+ public async codexHomesForAccount(accountId: string, after: string | null): Promise<IStoredAuthorityCodexHome[]> {
250
+ this.db();
251
+ if (!/^[a-f0-9]{64}$/.test(accountId) || (after !== null && !/^[a-f0-9]{64}$/.test(after))) {
252
+ throw new Error('Invalid managed Codex home selector.');
253
+ }
254
+ const stored = await AuthSwitchAuthorityCodexHomeModel.exact.findStored({
255
+ filter: after ? { accountId, id: { $gt: after } } : { accountId }, sort: { id: 1 }, limit: 128,
256
+ });
257
+ return stored.map(item => AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(item));
258
+ }
259
+
260
+ /** Home path identity is hashed by the caller; no vendor files or raw paths enter SmartData. */
261
+ public async reserveCodexHome(accountId: string, grantId: string, scopeId: string, homeId: string,
262
+ updateId: string): Promise<{ newlyRegistered: boolean }> {
263
+ try {
264
+ return await this.transaction(async session => {
265
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
266
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
267
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
268
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
269
+ if (!account || account.removed || account.providerId !== 'openai' || grant?.owner !== 'daemon'
270
+ || grant.purpose !== 'openai_managed' || grant.accountId !== accountId
271
+ || grant.id !== account.primaryGrantId || grant.state === 'removed') {
272
+ throw new Error('Managed Codex requires a daemon-owned OpenAI account.');
273
+ }
274
+ const used = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId }, { session });
275
+ if (used) {
276
+ if (used.accountId !== accountId || used.grantId !== grantId || used.scopeId !== scopeId) {
277
+ throw new Error('Managed Codex home already belongs to another account or scope.');
278
+ }
279
+ return { newlyRegistered: false };
280
+ }
281
+ const record: IStoredAuthorityCodexHome = { id: homeId, accountId, grantId, scopeId, activeRun: null,
282
+ createdAt: new Date().toISOString(), updateId };
283
+ const inserted = await AuthSwitchAuthorityCodexHomeModel.exact.insert(record, { session });
284
+ if (inserted.status !== 'inserted') throw new Error('Managed Codex home changed during admission.');
285
+ return { newlyRegistered: true };
286
+ });
287
+ } catch (error) {
288
+ if (error instanceof plugins.smartdata.SmartdataExactPersistenceError
289
+ && (error.code === 'ambiguous_write' || error.code === 'unique_conflict')) {
290
+ const assigned = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId });
291
+ if (assigned?.accountId === accountId && assigned.grantId === grantId && assigned.scopeId === scopeId
292
+ && assigned.updateId === updateId) return { newlyRegistered: true };
293
+ throw new Error('Managed Codex home registration changed concurrently; refresh before retrying.');
294
+ }
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ /** Pre-spawn claim is durable; the exact socket lets a later daemon detect a child born before PID readback. */
300
+ public async changeCodexRun(homeId: string, expectedRunId: string | null,
301
+ activeRun: IStoredAuthorityCodexHome['activeRun'], updateId: string): Promise<void> {
302
+ try {
303
+ await this.transaction(async session => {
304
+ const current = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId }, { session });
305
+ if (!current || current.activeRun?.id !== (expectedRunId ?? undefined)) {
306
+ throw new Error('Managed Codex home run changed concurrently.');
307
+ }
308
+ if (activeRun && expectedRunId !== null && activeRun.id !== expectedRunId) {
309
+ throw new Error('Managed Codex run identity cannot change.');
310
+ }
311
+ const draft = { ...AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(current), activeRun, updateId };
312
+ const result = await AuthSwitchAuthorityCodexHomeModel.exact.transition({ current,
313
+ change: model => Object.assign(model, draft) }, { session });
314
+ if (result.status !== 'transitioned') throw new Error('Managed Codex home run changed concurrently.');
315
+ return true;
316
+ });
317
+ } catch (error) {
318
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
319
+ const current = await this.readCodexHome(homeId);
320
+ if (current?.updateId === updateId && JSON.stringify(current.activeRun) === JSON.stringify(activeRun)) return;
321
+ throw new Error('Managed Codex home run outcome is unknown.');
322
+ }
323
+ }
324
+
108
325
  /** One consistent authorization view. Callers must not combine separate binding and grant reads. */
109
326
  public async readBindingContext(id: string): Promise<{
110
327
  binding: IStoredAuthorityBinding | null;
@@ -117,10 +334,10 @@ export class AuthSwitchAuthorityDatabase {
117
334
  const accountStored = binding
118
335
  ? await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: binding.accountId }, { session }) : null;
119
336
  const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
120
- const grantStored = binding && account?.primaryGrantId === binding.grantId
337
+ const grantStored = binding && account && !account.removed
121
338
  ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: binding.grantId }, { session }) : null;
122
339
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
123
- return { binding, account, grant };
340
+ return { binding, account, grant: grant?.accountId === account?.id ? grant : null };
124
341
  });
125
342
  }
126
343
 
@@ -148,6 +365,296 @@ export class AuthSwitchAuthorityDatabase {
148
365
  return stored ? AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(stored) : null;
149
366
  }
150
367
 
368
+ public async readOperation(id: string): Promise<IStoredAuthorityOperation | null> {
369
+ this.db();
370
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id });
371
+ return stored ? storedOperation(stored) : null;
372
+ }
373
+
374
+ public async operationsPage(after: string | null, limit = 128): Promise<{ operations: IStoredAuthorityDeviceOperation[]; nextCursor: string | null }> {
375
+ this.db();
376
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid operation page size.');
377
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStored({
378
+ filter: after
379
+ ? { kind: { $in: ['add_openai', 'reauth_openai'] }, id: { $gt: after } }
380
+ : { kind: { $in: ['add_openai', 'reauth_openai'] } },
381
+ sort: { id: 1 }, limit: limit + 1,
382
+ });
383
+ const operations = stored.slice(0, limit).map(storedOperation)
384
+ .filter((item): item is IStoredAuthorityDeviceOperation => item.kind !== 'preuse_openai');
385
+ return { operations, nextCursor: stored.length > limit ? operations.at(-1)!.id : null };
386
+ }
387
+
388
+ public async operationsByState(state: IStoredAuthorityDeviceOperation['state'], after: string | null,
389
+ limit = 128): Promise<IStoredAuthorityDeviceOperation[]> {
390
+ this.db();
391
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid operation page size.');
392
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStored({
393
+ filter: after ? { state, id: { $gt: after } } : { state }, sort: { id: 1 }, limit,
394
+ });
395
+ return stored.map(storedOperation)
396
+ .filter((item): item is IStoredAuthorityDeviceOperation => item.kind !== 'preuse_openai');
397
+ }
398
+
399
+ public async preuseOperationsByState(state: IStoredAuthorityPreuseOperation['state'], after: string | null,
400
+ limit = 128): Promise<IStoredAuthorityPreuseOperation[]> {
401
+ this.db();
402
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid operation page size.');
403
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStored({
404
+ filter: after ? { kind: 'preuse_openai', state, id: { $gt: after } } : { kind: 'preuse_openai', state },
405
+ sort: { id: 1 }, limit,
406
+ });
407
+ return stored.map(storedOperation)
408
+ .filter((item): item is IStoredAuthorityPreuseOperation => item.kind === 'preuse_openai');
409
+ }
410
+
411
+ /** Insert-or-read admission for one exact device-login intent. Existing receipts are never replayed. */
412
+ public async createOperation(intent: TStoredAuthorityDeviceOperationAdmission): Promise<{
413
+ operation: IStoredAuthorityDeviceOperation; created: boolean;
414
+ }> {
415
+ this.db();
416
+ assertStoredAuthorityOperation({ ...intent,
417
+ expectedGrantGeneration: intent.kind === 'reauth_openai' ? 1 : null });
418
+ const sameRequest = (current: IStoredAuthorityOperation): current is IStoredAuthorityDeviceOperation =>
419
+ current.kind !== 'preuse_openai' && current.kind === intent.kind
420
+ && current.accountId === intent.accountId && current.grantId === intent.grantId
421
+ && current.purpose === intent.purpose;
422
+ try {
423
+ return await this.transaction(async session => {
424
+ const existingStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne(
425
+ { id: intent.id }, { session });
426
+ if (existingStored) {
427
+ const existing = storedOperation(existingStored);
428
+ if (!sameRequest(existing)) throw new Error('Account operation ID belongs to a different request.');
429
+ return { operation: existing, created: false };
430
+ }
431
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne(
432
+ { id: 'authswitch-authority' }, { session });
433
+ if (!metaStored || AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored).admission !== 'open') {
434
+ throw new Error('Authswitch authority admission is closed.');
435
+ }
436
+ let expectedGrantGeneration: number | null = null;
437
+ if (intent.kind === 'reauth_openai') {
438
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne(
439
+ { id: intent.accountId! }, { session });
440
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne(
441
+ { id: intent.grantId! }, { session });
442
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
443
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
444
+ if (!account || account.removed || account.providerId !== 'openai'
445
+ || account.primaryGrantId !== intent.grantId || !grant || grant.accountId !== intent.accountId
446
+ || grant.providerId !== 'openai' || grant.purpose !== 'openai_managed'
447
+ || grant.owner !== 'daemon' || ['removed', 'exchange_may_have_been_sent'].includes(grant.state)) {
448
+ throw new Error('Selected OpenAI login is unavailable for reauthentication.');
449
+ }
450
+ expectedGrantGeneration = grant.grantGeneration;
451
+ }
452
+ const operation: IStoredAuthorityDeviceOperation = { ...intent, expectedGrantGeneration };
453
+ assertStoredAuthorityOperation(operation);
454
+ const result = await AuthSwitchAuthorityOperationModel.exact.insert(operation, { session });
455
+ if (result.status !== 'inserted') throw new Error('Account operation ID already exists.');
456
+ return { operation, created: true };
457
+ });
458
+ } catch (error) {
459
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
460
+ const stored = await this.readOperation(intent.id);
461
+ if (stored && sameRequest(stored)) {
462
+ return { operation: stored, created: stored.updateId === intent.updateId };
463
+ }
464
+ throw new Error('Account operation creation outcome is unknown; no provider login was started.');
465
+ }
466
+ }
467
+
468
+ /** Revalidate the durable owner and exact reauthentication generation after mutable runtime preparation. */
469
+ public async confirmOperationMayStart(operationId: string): Promise<IStoredAuthorityDeviceOperation> {
470
+ return this.transaction(async session => {
471
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne(
472
+ { id: 'authswitch-authority' }, { session });
473
+ const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne(
474
+ { id: operationId }, { session });
475
+ if (!metaStored || AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored).admission !== 'open') {
476
+ throw new Error('Authswitch authority admission is closed.');
477
+ }
478
+ if (!operationStored) throw new Error('Account operation was not found.');
479
+ const operation = storedOperation(operationStored);
480
+ if (operation.kind === 'preuse_openai' || operation.state !== 'starting') {
481
+ throw new Error('Account operation is no longer starting.');
482
+ }
483
+ if (operation.kind === 'reauth_openai') {
484
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne(
485
+ { id: operation.accountId! }, { session });
486
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne(
487
+ { id: operation.grantId! }, { session });
488
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
489
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
490
+ if (!account || account.removed || account.providerId !== 'openai'
491
+ || account.primaryGrantId !== operation.grantId || !grant || grant.accountId !== operation.accountId
492
+ || grant.providerId !== 'openai' || grant.purpose !== 'openai_managed'
493
+ || grant.owner !== 'daemon' || ['removed', 'exchange_may_have_been_sent'].includes(grant.state)
494
+ || grant.grantGeneration !== operation.expectedGrantGeneration) {
495
+ throw new Error('Selected OpenAI login changed before reauthentication could start.');
496
+ }
497
+ }
498
+ return operation;
499
+ });
500
+ }
501
+
502
+ public async changeOperation(updateId: string, operationId: string,
503
+ change: (operation: IStoredAuthorityDeviceOperation) => IStoredAuthorityDeviceOperation): Promise<IStoredAuthorityDeviceOperation> {
504
+ this.db();
505
+ try {
506
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId });
507
+ if (!stored) throw new Error('Account operation was not found.');
508
+ const current = storedOperation(stored);
509
+ if (current.kind === 'preuse_openai') throw new Error('Account operation is not a device login.');
510
+ const draft = change(structuredClone(current));
511
+ const allowed: Record<IStoredAuthorityDeviceOperation['state'], IStoredAuthorityDeviceOperation['state'][]> = {
512
+ starting: ['pending', 'failed', 'cancelled', 'interrupted'],
513
+ pending: ['committing', 'failed', 'cancelled', 'interrupted'],
514
+ committing: ['complete', 'outcome_unknown'],
515
+ complete: [], failed: [], cancelled: [], interrupted: [], outcome_unknown: [],
516
+ };
517
+ if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
518
+ || draft.accountId !== current.accountId || draft.grantId !== current.grantId
519
+ || draft.expectedGrantGeneration !== current.expectedGrantGeneration
520
+ || draft.createdAt !== current.createdAt || draft.updateId !== updateId
521
+ || draft.revision !== current.revision + 1 || !allowed[current.state].includes(draft.state)) {
522
+ throw new Error('Account operation transition is invalid.');
523
+ }
524
+ assertStoredAuthorityOperation(draft);
525
+ const result = await AuthSwitchAuthorityOperationModel.exact.transition({
526
+ current: stored, change: model => Object.assign(model, draft),
527
+ });
528
+ if (result.status !== 'transitioned') throw new Error('Account operation changed concurrently.');
529
+ return draft;
530
+ } catch (error) {
531
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
532
+ const stored = await this.readOperation(operationId);
533
+ if (stored?.kind !== 'preuse_openai' && stored?.updateId === updateId) return stored;
534
+ throw new Error('Account operation transition outcome is unknown.');
535
+ }
536
+ }
537
+
538
+ /** Insert-or-read idempotency for one exact preuse request. Existing records are never resumed here. */
539
+ public async createPreuseOperation(operation: IStoredAuthorityPreuseOperation): Promise<{
540
+ operation: IStoredAuthorityPreuseOperation; created: boolean;
541
+ }> {
542
+ this.db();
543
+ assertStoredAuthorityOperation(operation);
544
+ const sameRequest = (current: IStoredAuthorityOperation): current is IStoredAuthorityPreuseOperation =>
545
+ current.kind === 'preuse_openai' && current.accountId === operation.accountId
546
+ && current.grantId === operation.grantId && current.purpose === operation.purpose
547
+ && current.requestHash === operation.requestHash && current.requestedModel === operation.requestedModel;
548
+ try {
549
+ return await this.transaction(async session => {
550
+ const existingStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operation.id }, { session });
551
+ if (existingStored) {
552
+ const existing = storedOperation(existingStored);
553
+ if (!sameRequest(existing)) throw new Error('Preuse operation ID belongs to a different request.');
554
+ return { operation: existing, created: false };
555
+ }
556
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: operation.accountId }, { session });
557
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: operation.grantId }, { session });
558
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
559
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
560
+ if (!account || account.removed || account.providerId !== 'openai' || account.primaryGrantId !== operation.grantId
561
+ || !grant || grant.accountId !== operation.accountId || grant.providerId !== 'openai'
562
+ || grant.purpose !== 'openai_managed' || grant.owner !== 'daemon'
563
+ || !['ready', 'retry_wait'].includes(grant.state)
564
+ || grant.authorizationGeneration !== operation.authorizationGeneration) {
565
+ throw new Error('Selected OpenAI login is unavailable for preuse.');
566
+ }
567
+ const inserted = await AuthSwitchAuthorityOperationModel.exact.insert(operation, { session });
568
+ if (inserted.status !== 'inserted') throw new Error('Preuse operation ID already exists.');
569
+ return { operation, created: true };
570
+ });
571
+ } catch (error) {
572
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
573
+ const stored = await this.readOperation(operation.id);
574
+ if (stored && sameRequest(stored) && stored.updateId === operation.updateId) {
575
+ return { operation: stored, created: true };
576
+ }
577
+ throw new Error('Preuse operation creation outcome is unknown; no provider request was started.');
578
+ }
579
+ }
580
+
581
+ /** The committed marker precedes the only quota-consuming provider call. */
582
+ public async markPreuseRequestMayHaveStarted(operationId: string, updateId: string, model: string,
583
+ grantGeneration: number, now: string): Promise<IStoredAuthorityPreuseOperation> {
584
+ try {
585
+ return await this.transaction(async session => {
586
+ const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId }, { session });
587
+ if (!operationStored) throw new Error('Preuse operation was not found.');
588
+ const current = storedOperation(operationStored);
589
+ if (current.kind !== 'preuse_openai' || current.state !== 'preparing') {
590
+ throw new Error('Preuse operation is no longer preparing.');
591
+ }
592
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: current.accountId }, { session });
593
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: current.grantId }, { session });
594
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
595
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
596
+ if (!account || account.removed || account.providerId !== 'openai' || account.primaryGrantId !== current.grantId
597
+ || !grant || grant.accountId !== current.accountId || grant.providerId !== 'openai'
598
+ || grant.purpose !== current.purpose || grant.owner !== 'daemon'
599
+ || !['ready', 'retry_wait'].includes(grant.state)
600
+ || grant.authorizationGeneration !== current.authorizationGeneration
601
+ || grant.grantGeneration !== grantGeneration) {
602
+ throw new Error('Preuse login changed before its request marker was committed.');
603
+ }
604
+ const draft: IStoredAuthorityPreuseOperation = { ...current, model, grantGeneration,
605
+ state: 'request_may_have_started', updatedAt: now, revision: current.revision + 1, updateId };
606
+ assertStoredAuthorityOperation(draft);
607
+ const transitioned = await AuthSwitchAuthorityOperationModel.exact.transition({
608
+ current: operationStored, change: stored => Object.assign(stored, draft),
609
+ }, { session });
610
+ if (transitioned.status !== 'transitioned') throw new Error('Preuse operation changed concurrently.');
611
+ return draft;
612
+ });
613
+ } catch (error) {
614
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
615
+ const stored = await this.readOperation(operationId);
616
+ if (stored?.kind === 'preuse_openai' && stored.updateId === updateId
617
+ && stored.state === 'request_may_have_started') return stored;
618
+ throw new Error('Preuse request-marker outcome is unknown; no provider request was started.');
619
+ }
620
+ }
621
+
622
+ public async changePreuseOperation(updateId: string, operationId: string,
623
+ change: (operation: IStoredAuthorityPreuseOperation) => IStoredAuthorityPreuseOperation): Promise<IStoredAuthorityPreuseOperation> {
624
+ this.db();
625
+ try {
626
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId });
627
+ if (!stored) throw new Error('Preuse operation was not found.');
628
+ const current = storedOperation(stored);
629
+ if (current.kind !== 'preuse_openai') throw new Error('Operation is not a preuse request.');
630
+ const draft = change(structuredClone(current));
631
+ const allowed: Record<IStoredAuthorityPreuseOperation['state'], IStoredAuthorityPreuseOperation['state'][]> = {
632
+ preparing: ['request_may_have_started', 'not_sent'],
633
+ request_may_have_started: ['complete', 'outcome_unknown'],
634
+ complete: [], not_sent: [], outcome_unknown: [],
635
+ };
636
+ if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
637
+ || draft.accountId !== current.accountId || draft.grantId !== current.grantId
638
+ || draft.requestHash !== current.requestHash || draft.requestedModel !== current.requestedModel
639
+ || draft.authorizationGeneration !== current.authorizationGeneration
640
+ || draft.createdAt !== current.createdAt || draft.updateId !== updateId
641
+ || draft.revision !== current.revision + 1 || !allowed[current.state].includes(draft.state)) {
642
+ throw new Error('Preuse operation transition is invalid.');
643
+ }
644
+ assertStoredAuthorityOperation(draft);
645
+ const result = await AuthSwitchAuthorityOperationModel.exact.transition({
646
+ current: stored, change: model => Object.assign(model, draft),
647
+ });
648
+ if (result.status !== 'transitioned') throw new Error('Preuse operation changed concurrently.');
649
+ return draft;
650
+ } catch (error) {
651
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
652
+ const stored = await this.readOperation(operationId);
653
+ if (stored?.kind === 'preuse_openai' && stored.updateId === updateId) return stored;
654
+ throw new Error('Preuse operation transition outcome is unknown.');
655
+ }
656
+ }
657
+
151
658
  public async migrationLedgerByStatus(status: IStoredAuthorityMigrationLedger['status'],
152
659
  after: string | null, limit = 128): Promise<IStoredAuthorityMigrationLedger[]> {
153
660
  this.db();
@@ -170,10 +677,153 @@ export class AuthSwitchAuthorityDatabase {
170
677
  });
171
678
  }
172
679
 
173
- public async page(accountAfter: string | null, bindingAfter: string | null, limit: number): Promise<{
174
- meta: IStoredAuthorityMeta; accounts: Array<{ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant | null }>;
680
+ public async readAccountAndGrant(accountId: string, grantId: string): Promise<{
681
+ account: IStoredAuthorityAccount | null; grant: IStoredAuthorityGrant | null;
682
+ }> {
683
+ return this.transaction(async session => {
684
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
685
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
686
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
687
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
688
+ return { account, grant: grant?.accountId === accountId ? grant : null };
689
+ });
690
+ }
691
+
692
+ /** Epoch, account and exact login are read from one authority snapshot. */
693
+ public async readUsageContext(accountId: string, loginId: string): Promise<IAuthSwitchUsageContext | null> {
694
+ return (await this.readUsageAccessView(accountId, loginId))?.context ?? null;
695
+ }
696
+
697
+ /** Backend-only view used to validate a usage token before and after unsealing. */
698
+ public async readUsageAccessView(accountId: string, loginId: string): Promise<{
699
+ context: IAuthSwitchUsageContext; account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant;
700
+ } | null> {
701
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
702
+ throw new Error('Invalid authority usage identity.');
703
+ }
704
+ return this.transaction(async session => {
705
+ const meta = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
706
+ const account = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
707
+ const grant = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: loginId }, { session });
708
+ if (!meta || !account || !grant || grant.accountId !== accountId) return null;
709
+ const accountRecord = AuthSwitchAuthorityAccountModel.exact.toPersisted(account);
710
+ const grantRecord = AuthSwitchAuthorityGrantModel.exact.toPersisted(grant);
711
+ return { context: usageContext(AuthSwitchAuthorityMetaModel.exact.toPersisted(meta), accountRecord, grantRecord),
712
+ account: accountRecord, grant: grantRecord };
713
+ });
714
+ }
715
+
716
+ /** Coherent native source ownership, including the home handoff fence. */
717
+ public async readClaudeNativeUsageView(homeId: string, accountId: string, loginId: string): Promise<{
718
+ context: IAuthSwitchUsageContext; account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant;
719
+ home: IStoredAuthorityClaudeHome;
720
+ } | null> {
721
+ if (![homeId, accountId, loginId].every(id => /^[a-f0-9]{64}$/.test(id))) {
722
+ throw new Error('Invalid Claude native usage selector.');
723
+ }
724
+ return this.transaction(async session => {
725
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
726
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
727
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: loginId }, { session });
728
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
729
+ if (!metaStored || !accountStored || !grantStored || !homeStored
730
+ || grantStored.accountId !== accountId || accountStored.removed
731
+ || accountStored.providerId !== 'anthropic' || grantStored.providerId !== 'anthropic'
732
+ || grantStored.purpose !== 'claude_host_native' || grantStored.owner !== 'claude_native'
733
+ || grantStored.state !== 'native' || homeStored.status !== 'ready'
734
+ || homeStored.pendingOperationId !== null || homeStored.activeAccountId !== accountId
735
+ || homeStored.activeGrantId !== loginId) return null;
736
+ const account = AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored);
737
+ const grant = AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored);
738
+ return { context: usageContext(AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored), account, grant),
739
+ account, grant, home: AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(homeStored) };
740
+ });
741
+ }
742
+
743
+ public async readUsage(accountId: string, loginId: string): Promise<IAuthSwitchStoredUsage | null> {
744
+ this.db();
745
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
746
+ throw new Error('Invalid authority usage identity.');
747
+ }
748
+ const stored = await AuthSwitchAuthorityUsageModel.exact.findStoredOne({ id: authorityUsageId(accountId, loginId) });
749
+ if (!stored) return null;
750
+ const { id: _id, accountId: _accountId, loginId: _loginId, ...record } =
751
+ AuthSwitchAuthorityUsageModel.exact.toPersisted(stored);
752
+ return record;
753
+ }
754
+
755
+ /** Publish one credential-free usage result only if its authority and predecessor still match. */
756
+ public async publishUsageIfCurrent(context: IAuthSwitchUsageContext,
757
+ previousUpdateId: string | null, next: IAuthSwitchStoredUsage): Promise<boolean> {
758
+ const id = authorityUsageId(context.accountId, context.loginId);
759
+ const draft: IStoredAuthorityUsage = { id, accountId: context.accountId,
760
+ loginId: context.loginId, ...next };
761
+ assertStoredAuthorityUsage(draft);
762
+ if (!sameUsageSource(context, next.context)
763
+ || (previousUpdateId !== null && !/^[a-f0-9-]{36}$/.test(previousUpdateId))) {
764
+ throw new Error('Authority usage publication has an invalid context or predecessor.');
765
+ }
766
+ try {
767
+ return await this.transaction(async session => {
768
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
769
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: context.accountId }, { session });
770
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: context.loginId }, { session });
771
+ if (!metaStored || !accountStored || !grantStored || grantStored.accountId !== context.accountId) return false;
772
+ const actual = usageContext(AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
773
+ AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored),
774
+ AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored));
775
+ if (!sameUsageContext(context, actual) || actual.removed) return false;
776
+ if (actual.providerId === 'openai') {
777
+ if (actual.owner !== 'daemon' || actual.purpose !== 'openai_managed'
778
+ || context.nativeSource !== null || !['ready', 'retry_wait'].includes(actual.grantState)) return false;
779
+ } else if (actual.providerId === 'anthropic' && actual.purpose === 'claude_host_native') {
780
+ if (actual.owner === 'daemon') {
781
+ if (context.nativeSource !== null || !['ready', 'retry_wait'].includes(actual.grantState)) return false;
782
+ } else if (actual.owner === 'claude_native' && actual.grantState === 'native'
783
+ && context.nativeSource !== null) {
784
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({
785
+ id: context.nativeSource.homeId }, { session });
786
+ if (!homeStored || homeStored.status !== 'ready' || homeStored.pendingOperationId !== null
787
+ || homeStored.activeAccountId !== context.accountId
788
+ || homeStored.activeGrantId !== context.loginId) return false;
789
+ } else return false;
790
+ } else return false;
791
+ const current = await AuthSwitchAuthorityUsageModel.exact.findStoredOne({ id }, { session });
792
+ if (current) {
793
+ const prior = AuthSwitchAuthorityUsageModel.exact.toPersisted(current);
794
+ if (sameUsageSource(prior.context, context)
795
+ ? prior.updateId !== previousUpdateId : previousUpdateId !== null) return false;
796
+ const changed = await AuthSwitchAuthorityUsageModel.exact.transition({ current,
797
+ change: model => Object.assign(model, draft) }, { session });
798
+ if (changed.status !== 'transitioned') return false;
799
+ } else {
800
+ if (previousUpdateId !== null) return false;
801
+ const inserted = await AuthSwitchAuthorityUsageModel.exact.insert(draft, { session });
802
+ if (inserted.status !== 'inserted') return false;
803
+ }
804
+ await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
805
+ next.updateId, 'account', context.accountId);
806
+ return true;
807
+ });
808
+ } catch (error) {
809
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError)) throw error;
810
+ if (error.code === 'unique_conflict') return false;
811
+ if (error.code !== 'ambiguous_write') throw error;
812
+ const [stored, event] = await Promise.all([
813
+ AuthSwitchAuthorityUsageModel.exact.findStoredOne({ id }),
814
+ AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: next.updateId }),
815
+ ]);
816
+ if (stored?.updateId === next.updateId && event?.epoch === context.epoch
817
+ && event.kind === 'account' && event.accountId === context.accountId
818
+ && sameUsageSource(stored.context, context)) return true;
819
+ throw new Error('Authority usage publication outcome is unknown; the provider query must not be replayed.');
820
+ }
821
+ }
822
+
823
+ public async page(accountAfter: string | null, grantAfter: string | null, bindingAfter: string | null, limit: number): Promise<{
824
+ meta: IStoredAuthorityMeta; accounts: IStoredAuthorityAccount[]; grants: IStoredAuthorityGrant[];
175
825
  bindings: IStoredAuthorityBinding[];
176
- nextAccountCursor: string | null; nextBindingCursor: string | null;
826
+ nextAccountCursor: string | null; nextGrantCursor: string | null; nextBindingCursor: string | null;
177
827
  }> {
178
828
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid authority page size.');
179
829
  return this.transaction(async session => {
@@ -183,28 +833,128 @@ export class AuthSwitchAuthorityDatabase {
183
833
  filter: accountAfter ? { id: { $gt: accountAfter } } : {},
184
834
  sort: { id: 1 }, limit: limit + 1, session,
185
835
  });
836
+ const grants = await AuthSwitchAuthorityGrantModel.exact.findStored({
837
+ filter: grantAfter ? { id: { $gt: grantAfter } } : {},
838
+ sort: { id: 1 }, limit: limit + 1, session,
839
+ });
186
840
  const bindings = await AuthSwitchAuthorityBindingModel.exact.findStored({
187
841
  filter: bindingAfter ? { id: { $gt: bindingAfter } } : {},
188
842
  sort: { id: 1 }, limit: limit + 1, session,
189
843
  });
190
844
  const accountPage = accounts.slice(0, limit).map(item => AuthSwitchAuthorityAccountModel.exact.toPersisted(item));
845
+ const grantPage = grants.slice(0, limit).map(item => AuthSwitchAuthorityGrantModel.exact.toPersisted(item));
191
846
  const bindingPage = bindings.slice(0, limit).map(item => AuthSwitchAuthorityBindingModel.exact.toPersisted(item));
192
- const withGrants: Array<{ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant | null }> = [];
193
- for (const account of accountPage) {
194
- const stored = account.primaryGrantId
195
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
196
- if (account.primaryGrantId && !stored) throw new Error('Authority account primary grant is missing.');
197
- withGrants.push({ account, grant: stored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(stored) : null });
198
- }
199
847
  return {
200
848
  meta: AuthSwitchAuthorityMetaModel.exact.toPersisted(meta),
201
- accounts: withGrants, bindings: bindingPage,
849
+ accounts: accountPage, grants: grantPage, bindings: bindingPage,
202
850
  nextAccountCursor: accounts.length > limit ? accountPage.at(-1)!.id : null,
851
+ nextGrantCursor: grants.length > limit ? grantPage.at(-1)!.id : null,
203
852
  nextBindingCursor: bindings.length > limit ? bindingPage.at(-1)!.id : null,
204
853
  };
205
854
  });
206
855
  }
207
856
 
857
+ /**
858
+ * One bounded, repeatable-read diagnostic page. Its metadata revision is authority state,
859
+ * not a complete change clock for every native-home record.
860
+ */
861
+ public async doctorPage(cursors: {
862
+ accountAfter: string | null;
863
+ loginAfter: string | null;
864
+ codexHomeAfter: string | null;
865
+ claudeHomeAfter: string | null;
866
+ claudeHandoffAfter: string | null;
867
+ nativeHandoffAfter: string | null;
868
+ migrationAfter: string | null;
869
+ }, limit: number): Promise<{
870
+ observedAt: string;
871
+ meta: IStoredAuthorityMeta;
872
+ accounts: IStoredAuthorityAccount[];
873
+ logins: IStoredAuthorityGrant[];
874
+ codexHomes: IStoredAuthorityCodexHome[];
875
+ claudeHomes: IStoredAuthorityClaudeHome[];
876
+ claudeHandoffs: IStoredAuthorityClaudeHandoff[];
877
+ nativeHandoffs: IStoredAuthorityHandoff[];
878
+ migrations: IStoredAuthorityMigrationLedger[];
879
+ nextAccountCursor: string | null;
880
+ nextLoginCursor: string | null;
881
+ nextCodexHomeCursor: string | null;
882
+ nextClaudeHomeCursor: string | null;
883
+ nextClaudeHandoffCursor: string | null;
884
+ nextNativeHandoffCursor: string | null;
885
+ nextMigrationCursor: string | null;
886
+ }> {
887
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 128) {
888
+ throw new Error('Invalid authority doctor page size.');
889
+ }
890
+ const hashCursor = (value: string | null): boolean => value === null || /^[a-f0-9]{64}$/.test(value);
891
+ if (![cursors.accountAfter, cursors.loginAfter, cursors.codexHomeAfter, cursors.claudeHomeAfter,
892
+ cursors.nativeHandoffAfter, cursors.migrationAfter].every(hashCursor)
893
+ || (cursors.claudeHandoffAfter !== null && !/^[a-f0-9-]{36}$/.test(cursors.claudeHandoffAfter))) {
894
+ throw new Error('Invalid authority doctor cursor.');
895
+ }
896
+ return this.transaction(async session => {
897
+ const meta = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
898
+ if (!meta) throw new Error('Authswitch authority metadata is missing.');
899
+ const accounts = await AuthSwitchAuthorityAccountModel.exact.findStored({
900
+ filter: cursors.accountAfter ? { id: { $gt: cursors.accountAfter } } : {},
901
+ sort: { id: 1 }, limit: limit + 1, session,
902
+ });
903
+ const logins = await AuthSwitchAuthorityGrantModel.exact.findStored({
904
+ filter: cursors.loginAfter ? { id: { $gt: cursors.loginAfter } } : {},
905
+ sort: { id: 1 }, limit: limit + 1, session,
906
+ });
907
+ const codexHomes = await AuthSwitchAuthorityCodexHomeModel.exact.findStored({
908
+ filter: cursors.codexHomeAfter ? { id: { $gt: cursors.codexHomeAfter } } : {},
909
+ sort: { id: 1 }, limit: limit + 1, session,
910
+ });
911
+ const claudeHomes = await AuthSwitchAuthorityClaudeHomeModel.exact.findStored({
912
+ filter: cursors.claudeHomeAfter ? { id: { $gt: cursors.claudeHomeAfter } } : {},
913
+ sort: { id: 1 }, limit: limit + 1, session,
914
+ });
915
+ const claudeHandoffs = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStored({
916
+ filter: cursors.claudeHandoffAfter ? { id: { $gt: cursors.claudeHandoffAfter } } : {},
917
+ sort: { id: 1 }, limit: limit + 1, session,
918
+ });
919
+ const nativeHandoffs = await AuthSwitchAuthorityHandoffModel.exact.findStored({
920
+ filter: cursors.nativeHandoffAfter ? { id: { $gt: cursors.nativeHandoffAfter } } : {},
921
+ sort: { id: 1 }, limit: limit + 1, session,
922
+ });
923
+ const migrations = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
924
+ filter: cursors.migrationAfter ? { id: { $gt: cursors.migrationAfter } } : {},
925
+ sort: { id: 1 }, limit: limit + 1, session,
926
+ });
927
+ const accountPage = accounts.slice(0, limit).map(item => AuthSwitchAuthorityAccountModel.exact.toPersisted(item));
928
+ const loginPage = logins.slice(0, limit).map(item => AuthSwitchAuthorityGrantModel.exact.toPersisted(item));
929
+ const codexHomePage = codexHomes.slice(0, limit).map(item => AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(item));
930
+ const claudeHomePage = claudeHomes.slice(0, limit).map(item => AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(item));
931
+ const claudeHandoffPage = claudeHandoffs.slice(0, limit)
932
+ .map(item => AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(item));
933
+ const nativeHandoffPage = nativeHandoffs.slice(0, limit)
934
+ .map(item => AuthSwitchAuthorityHandoffModel.exact.toPersisted(item));
935
+ const migrationPage = migrations.slice(0, limit)
936
+ .map(item => AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(item));
937
+ return {
938
+ observedAt: new Date().toISOString(),
939
+ meta: AuthSwitchAuthorityMetaModel.exact.toPersisted(meta),
940
+ accounts: accountPage,
941
+ logins: loginPage,
942
+ codexHomes: codexHomePage,
943
+ claudeHomes: claudeHomePage,
944
+ claudeHandoffs: claudeHandoffPage,
945
+ nativeHandoffs: nativeHandoffPage,
946
+ migrations: migrationPage,
947
+ nextAccountCursor: accounts.length > limit ? accountPage.at(-1)!.id : null,
948
+ nextLoginCursor: logins.length > limit ? loginPage.at(-1)!.id : null,
949
+ nextCodexHomeCursor: codexHomes.length > limit ? codexHomePage.at(-1)!.id : null,
950
+ nextClaudeHomeCursor: claudeHomes.length > limit ? claudeHomePage.at(-1)!.id : null,
951
+ nextClaudeHandoffCursor: claudeHandoffs.length > limit ? claudeHandoffPage.at(-1)!.id : null,
952
+ nextNativeHandoffCursor: nativeHandoffs.length > limit ? nativeHandoffPage.at(-1)!.id : null,
953
+ nextMigrationCursor: migrations.length > limit ? migrationPage.at(-1)!.id : null,
954
+ };
955
+ });
956
+ }
957
+
208
958
  public async grantsByState(state: IStoredAuthorityGrant['state'], after: string | null, limit = 128): Promise<IStoredAuthorityGrant[]> {
209
959
  this.db();
210
960
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid authority scan size.');
@@ -267,18 +1017,27 @@ export class AuthSwitchAuthorityDatabase {
267
1017
  }
268
1018
 
269
1019
  private async persistGrant(session: TSession, current: Awaited<ReturnType<typeof AuthSwitchAuthorityGrantModel.exact.findStoredOne>>,
270
- draft: IStoredAuthorityGrant): Promise<void> {
1020
+ draft: IStoredAuthorityGrant, allowClaudeHandoff = false): Promise<void> {
271
1021
  assertStoredAuthorityGrant(draft);
272
1022
  if (current) {
273
1023
  const prior = AuthSwitchAuthorityGrantModel.exact.toPersisted(current);
274
1024
  if (draft.id !== prior.id || draft.accountId !== prior.accountId
275
1025
  || draft.providerId !== prior.providerId || draft.purpose !== prior.purpose
276
1026
  || draft.audience !== prior.audience) throw new Error('Grant identity cannot change.');
1027
+ if (!allowClaudeHandoff && draft.purpose === 'claude_host_native'
1028
+ && (['handoff_pending', 'handoff_quarantined'].includes(draft.state)
1029
+ || ['handoff_pending', 'handoff_quarantined'].includes(prior.state)
1030
+ || draft.owner !== prior.owner)) {
1031
+ throw new Error('Claude native ownership requires the two-grant handoff transaction.');
1032
+ }
277
1033
  const result = await AuthSwitchAuthorityGrantModel.exact.transition({
278
1034
  current, change: model => Object.assign(model, draft),
279
1035
  }, { session });
280
1036
  if (result.status !== 'transitioned') throw new Error('Grant changed concurrently.');
281
1037
  } else {
1038
+ if (!allowClaudeHandoff && ['handoff_pending', 'handoff_quarantined'].includes(draft.state)) {
1039
+ throw new Error('Claude handoff cannot begin with an unassigned grant.');
1040
+ }
282
1041
  const result = await AuthSwitchAuthorityGrantModel.exact.insert(draft, { session });
283
1042
  if (result.status !== 'inserted') throw new Error('Grant was added concurrently.');
284
1043
  }
@@ -327,6 +1086,75 @@ export class AuthSwitchAuthorityDatabase {
327
1086
  }
328
1087
  }
329
1088
 
1089
+ /** Device-login result, account and grant are one commit; a lost RPC response remains discoverable by operation ID. */
1090
+ public async commitLoginOperation(updateId: string, operationId: string, accountId: string, grantId: string,
1091
+ change: (account: IStoredAuthorityAccount | null, grant: IStoredAuthorityGrant | null) => {
1092
+ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant;
1093
+ }): Promise<{ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant; operation: IStoredAuthorityDeviceOperation }> {
1094
+ try {
1095
+ return await this.transaction(async session => {
1096
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1097
+ const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId }, { session });
1098
+ if (!metaStored || !operationStored) throw new Error('Device operation is unavailable.');
1099
+ const operation = storedOperation(operationStored);
1100
+ if (operation.kind === 'preuse_openai' || operation.state !== 'committing' || operation.commitId !== updateId
1101
+ || (operation.kind === 'reauth_openai' && (operation.accountId !== accountId || operation.grantId !== grantId))) {
1102
+ throw new Error('Device operation is not admitted for this login.');
1103
+ }
1104
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1105
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
1106
+ const oldGrant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
1107
+ if (operation.kind === 'reauth_openai' && oldGrant?.grantGeneration !== operation.expectedGrantGeneration) {
1108
+ throw new Error('Login changed while device reauthentication was pending.');
1109
+ }
1110
+ const draft = change(
1111
+ accountStored ? structuredClone(AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored)) : null,
1112
+ oldGrant ? structuredClone(oldGrant) : null);
1113
+ if (draft.account.id !== accountId || draft.grant.id !== grantId || draft.grant.accountId !== accountId
1114
+ || draft.account.primaryGrantId !== grantId || draft.account.providerId !== draft.grant.providerId
1115
+ || draft.account.updateId !== updateId || draft.grant.updateId !== updateId) {
1116
+ throw new Error('Device login account/grant change has an invalid identity.');
1117
+ }
1118
+ if (accountStored && accountStored.primaryGrantId !== grantId) {
1119
+ throw new Error('Primary grant identity cannot be replaced.');
1120
+ }
1121
+ if (grantStored && ['legacy_native', 'claude_native'].includes(grantStored.owner)
1122
+ && draft.grant.owner === 'daemon') {
1123
+ throw new Error('Native ownership requires a verified handoff.');
1124
+ }
1125
+ const completed: IStoredAuthorityDeviceOperation = {
1126
+ ...operation, state: 'complete', prompt: null,
1127
+ result: { accountId, grantId, accountRevision: draft.account.revision,
1128
+ grantGeneration: draft.grant.grantGeneration },
1129
+ error: null, updatedAt: draft.account.statusObservedAt, finishedAt: draft.account.statusObservedAt,
1130
+ revision: operation.revision + 1, updateId,
1131
+ };
1132
+ assertStoredAuthorityOperation(completed);
1133
+ await this.persistAccount(session, accountStored, draft.account);
1134
+ await this.persistGrant(session, grantStored, draft.grant);
1135
+ const transitioned = await AuthSwitchAuthorityOperationModel.exact.transition({
1136
+ current: operationStored, change: model => Object.assign(model, completed),
1137
+ }, { session });
1138
+ if (transitioned.status !== 'transitioned') throw new Error('Device operation changed concurrently.');
1139
+ await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
1140
+ updateId, 'account', accountId);
1141
+ return { ...draft, operation: completed };
1142
+ });
1143
+ } catch (error) {
1144
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1145
+ const [event, operation, pair] = await Promise.all([
1146
+ AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId }),
1147
+ this.readOperation(operationId), this.readAccountAndPrimaryGrant(accountId),
1148
+ ]);
1149
+ if (event && operation?.kind !== 'preuse_openai' && operation?.state === 'complete' && operation.updateId === updateId
1150
+ && pair.account?.updateId === updateId && pair.grant?.updateId === updateId
1151
+ && operation.result?.accountId === accountId && operation.result.grantId === grantId) {
1152
+ return { account: pair.account, grant: pair.grant, operation };
1153
+ }
1154
+ throw new Error('Device login commit outcome is unknown; inspect the durable operation before retrying.');
1155
+ }
1156
+ }
1157
+
330
1158
  /** Grant transition and public account revision share a transaction and one event. */
331
1159
  public async changeGrant(updateId: string, grantId: string,
332
1160
  change: (grant: IStoredAuthorityGrant, account: IStoredAuthorityAccount) => IStoredAuthorityGrant): Promise<{
@@ -345,6 +1173,11 @@ export class AuthSwitchAuthorityDatabase {
345
1173
  if (draft.id !== grantId || draft.accountId !== account.id || draft.updateId !== updateId) {
346
1174
  throw new Error('Grant change has an invalid identity.');
347
1175
  }
1176
+ if (grant.purpose === 'claude_host_native' && draft.state === 'exchange_may_have_been_sent'
1177
+ && grant.state !== 'exchange_may_have_been_sent'
1178
+ && !(await this.hasVerifiedClaudeGrantReceipt(account.id, grant.id, session))) {
1179
+ throw new Error('Claude refresh requires a verified import and backup receipt.');
1180
+ }
348
1181
  const accountDraft: IStoredAuthorityAccount = { ...account, revision: account.revision + 1,
349
1182
  statusObservedAt: draft.statusObservedAt, updateId };
350
1183
  await this.persistGrant(session, grantStored, draft);
@@ -433,6 +1266,9 @@ export class AuthSwitchAuthorityDatabase {
433
1266
  if (grant.state === 'exchange_may_have_been_sent') {
434
1267
  throw new Error('Wait for the account refresh to finish before removing it.');
435
1268
  }
1269
+ if (['handoff_pending', 'handoff_quarantined'].includes(grant.state)) {
1270
+ throw new Error('Resolve the Claude native handoff before removing this account.');
1271
+ }
436
1272
  });
437
1273
  await scan(async (grant, stored) => {
438
1274
  if (grant.state === 'removed') return;
@@ -513,7 +1349,7 @@ export class AuthSwitchAuthorityDatabase {
513
1349
  }
514
1350
  }
515
1351
 
516
- public async changeBinding(updateId: string, bindingId: string, accountId: string,
1352
+ public async changeBinding(updateId: string, bindingId: string, accountId: string, grantId: string,
517
1353
  change: (draft: IStoredAuthorityBinding | null, account: IStoredAuthorityAccount,
518
1354
  grant: IStoredAuthorityGrant | null) => IStoredAuthorityBinding): Promise<{
519
1355
  meta: IStoredAuthorityMeta; binding: IStoredAuthorityBinding;
@@ -524,13 +1360,14 @@ export class AuthSwitchAuthorityDatabase {
524
1360
  const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
525
1361
  if (!metaStored || !accountStored) throw new Error('Account is not available for this runtime.');
526
1362
  const account = AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored);
527
- const grantStored = account.primaryGrantId
528
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
1363
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
529
1364
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
530
1365
  const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
531
1366
  const draft = change(current ? structuredClone(AuthSwitchAuthorityBindingModel.exact.toPersisted(current)) : null, account, grant);
532
1367
  if (draft.id !== bindingId || draft.accountId !== accountId || draft.updateId !== updateId) throw new Error('Binding change has an invalid identity.');
533
- if (draft.grantId !== grant?.id) throw new Error('Binding grant does not belong to the account.');
1368
+ if (draft.grantId !== grantId || grant?.accountId !== accountId) {
1369
+ throw new Error('Binding login does not belong to the account.');
1370
+ }
534
1371
  if (current && (draft.runtime !== current.runtime || draft.scopeId !== current.scopeId)) {
535
1372
  throw new Error('Runtime binding identity cannot change.');
536
1373
  }
@@ -557,6 +1394,42 @@ export class AuthSwitchAuthorityDatabase {
557
1394
  }
558
1395
  }
559
1396
 
1397
+ private async revokeBindingExact(updateId: string, bindingId: string, capabilityHash: string,
1398
+ externalOnly: boolean): Promise<boolean> {
1399
+ try {
1400
+ return await this.transaction(async session => {
1401
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1402
+ if (!metaStored) throw new Error('Authswitch authority metadata is missing.');
1403
+ const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
1404
+ if (!current || current.capabilityHash !== capabilityHash
1405
+ || (externalOnly && !['flex', 'opencode'].includes(current.runtime))) return false;
1406
+ const result = await AuthSwitchAuthorityBindingModel.exact.delete({ current }, { session });
1407
+ if (result.status !== 'deleted') throw new Error('Runtime binding changed during revocation.');
1408
+ await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
1409
+ updateId, 'binding', current.accountId);
1410
+ return true;
1411
+ });
1412
+ } catch (error) {
1413
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1414
+ const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
1415
+ const binding = await this.readBinding(bindingId);
1416
+ // Every bind mints a fresh random capability. An event plus absence or a different
1417
+ // capability therefore proves that this exact deletion committed before any successor.
1418
+ if (event && (!binding || binding.capabilityHash !== capabilityHash)) return true;
1419
+ throw new Error('Authswitch binding revocation outcome is unknown.');
1420
+ }
1421
+ }
1422
+
1423
+ /** Closing an authority-owned runtime revokes its exact capability after owned cleanup. */
1424
+ public revokeBinding(updateId: string, bindingId: string, capabilityHash: string): Promise<boolean> {
1425
+ return this.revokeBindingExact(updateId, bindingId, capabilityHash, false);
1426
+ }
1427
+
1428
+ /** External callers can release only Flex/OpenCode bindings; managed Codex cleanup stays daemon-owned. */
1429
+ public releaseExternalBinding(updateId: string, bindingId: string, capabilityHash: string): Promise<boolean> {
1430
+ return this.revokeBindingExact(updateId, bindingId, capabilityHash, true);
1431
+ }
1432
+
560
1433
  /** Enrollment metadata is sealed by the caller and cannot become detached from an account. */
561
1434
  public async changeEnrollment(updateId: string, enrollmentId: string, accountId: string,
562
1435
  change: (current: IStoredAuthorityEnrollment | null) => IStoredAuthorityEnrollment): Promise<IStoredAuthorityEnrollment> {
@@ -676,6 +1549,317 @@ export class AuthSwitchAuthorityDatabase {
676
1549
  }
677
1550
  }
678
1551
 
1552
+ public async readClaudeHome(homeId: string): Promise<IStoredAuthorityClaudeHome | null> {
1553
+ this.db();
1554
+ if (!/^[a-f0-9]{64}$/.test(homeId)) throw new Error('Invalid Claude home selector.');
1555
+ const stored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId });
1556
+ return stored ? AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(stored) : null;
1557
+ }
1558
+
1559
+ public async readClaudeHandoff(operationId: string): Promise<IStoredAuthorityClaudeHandoff | null> {
1560
+ this.db();
1561
+ if (!/^[a-f0-9-]{36}$/.test(operationId)) throw new Error('Invalid Claude handoff selector.');
1562
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStoredOne({ id: operationId });
1563
+ return stored ? AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(stored) : null;
1564
+ }
1565
+
1566
+ /** A backed-up backend import receipt, not an account snapshot, authorizes an inactive Claude grant. */
1567
+ public async hasVerifiedClaudeGrantReceipt(accountId: string, grantId: string,
1568
+ session?: TSession): Promise<boolean> {
1569
+ this.db();
1570
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(grantId)) {
1571
+ throw new Error('Invalid Claude grant receipt selector.');
1572
+ }
1573
+ for (const status of ['verified', 'complete'] as const) {
1574
+ const stored = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
1575
+ filter: { grantId, status }, sort: { id: 1 }, limit: 128, session,
1576
+ });
1577
+ if (stored.some(item => item.accountId === accountId
1578
+ && item.backupDigest !== null
1579
+ && ['authswitch_stash', 'authswitch_backup', 'claude_native'].includes(item.sourceKind))) return true;
1580
+ if (stored.length === 128) throw new Error('Claude grant has too many migration receipts to verify.');
1581
+ }
1582
+ return false;
1583
+ }
1584
+
1585
+ public async claudeHandoffsByPhase(phase: IStoredAuthorityClaudeHandoff['phase'],
1586
+ after: string | null): Promise<IStoredAuthorityClaudeHandoff[]> {
1587
+ this.db();
1588
+ if (!['reserved', 'prepared', 'quarantined'].includes(phase)
1589
+ || (after !== null && !/^[a-f0-9-]{36}$/.test(after))) throw new Error('Invalid Claude handoff scan.');
1590
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStored({
1591
+ filter: after === null ? { phase } : { phase, id: { $gt: after } },
1592
+ sort: { id: 1 }, limit: 128,
1593
+ });
1594
+ return stored.map(item => AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(item));
1595
+ }
1596
+
1597
+ public async claudeHandoffsPage(after: string | null, limit = 128): Promise<{
1598
+ handoffs: IStoredAuthorityClaudeHandoff[]; nextCursor: string | null;
1599
+ }> {
1600
+ this.db();
1601
+ if ((after !== null && !/^[a-f0-9-]{36}$/.test(after))
1602
+ || !Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid Claude handoff page.');
1603
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStored({
1604
+ filter: after === null ? {} : { id: { $gt: after } }, sort: { id: 1 }, limit: limit + 1,
1605
+ });
1606
+ const handoffs = stored.slice(0, limit).map(item => AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(item));
1607
+ return { handoffs, nextCursor: stored.length > limit ? handoffs.at(-1)!.id : null };
1608
+ }
1609
+
1610
+ /** Only a verified backend import receipt may adopt a native home. No startup autodiscovery exists. */
1611
+ public async registerClaudeHomeFromReceipt(input: { homeId: string; accountId: string; grantId: string;
1612
+ receiptId: string; sourceDigest: string; accountUuid: string; organizationUuid: string;
1613
+ updateId: string; now: string }): Promise<IStoredAuthorityClaudeHome> {
1614
+ const { homeId, accountId, grantId, receiptId, sourceDigest, updateId } = input;
1615
+ for (const value of [homeId, accountId, grantId, receiptId, sourceDigest]) {
1616
+ if (!/^[a-f0-9]{64}$/.test(value)) throw new Error('Invalid verified Claude home identity.');
1617
+ }
1618
+ if (!/^[a-f0-9-]{36}$/.test(updateId)) throw new Error('Invalid Claude home update identity.');
1619
+ try {
1620
+ return await this.transaction(async session => {
1621
+ const receiptStored = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStoredOne({ id: receiptId }, { session });
1622
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1623
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
1624
+ const current = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
1625
+ if (!receiptStored || !['verified', 'complete'].includes(receiptStored.status)
1626
+ || receiptStored.backupDigest === null
1627
+ || receiptStored.sourceKind !== 'claude_native' || receiptStored.sourcePathHash !== homeId
1628
+ || receiptStored.sourceDigest !== sourceDigest || receiptStored.accountId !== accountId
1629
+ || receiptStored.grantId !== grantId || !accountStored || accountStored.removed
1630
+ || accountStored.providerId !== 'anthropic' || accountStored.subject !== input.accountUuid
1631
+ || accountStored.workspaceId !== input.organizationUuid || !grantStored
1632
+ || grantStored.accountId !== accountId || grantStored.purpose !== 'claude_host_native'
1633
+ || grantStored.owner !== 'claude_native' || grantStored.state !== 'native'
1634
+ || grantStored.ciphertext !== null) {
1635
+ throw new Error('Verified Claude native source mapping is unavailable.');
1636
+ }
1637
+ if (current) {
1638
+ if (current.activeAccountId !== accountId || current.activeGrantId !== grantId) {
1639
+ throw new Error('Claude native home belongs to another grant.');
1640
+ }
1641
+ return AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(current);
1642
+ }
1643
+ const home: IStoredAuthorityClaudeHome = { id: homeId, activeAccountId: accountId,
1644
+ activeGrantId: grantId, pendingOperationId: null, status: 'ready', revision: 1,
1645
+ createdAt: input.now, updateId };
1646
+ assertStoredAuthorityClaudeHome(home);
1647
+ const inserted = await AuthSwitchAuthorityClaudeHomeModel.exact.insert(home, { session });
1648
+ if (inserted.status !== 'inserted') throw new Error('Claude native home changed concurrently.');
1649
+ return home;
1650
+ });
1651
+ } catch (error) {
1652
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError)
1653
+ || !['ambiguous_write', 'unique_conflict'].includes(error.code)) throw error;
1654
+ const home = await this.readClaudeHome(homeId);
1655
+ if (home?.updateId === updateId && home.activeAccountId === accountId && home.activeGrantId === grantId) return home;
1656
+ throw new Error('Claude native home registration outcome is unknown.');
1657
+ }
1658
+ }
1659
+
1660
+ public async readClaudeHandoffContext(operationId: string): Promise<{
1661
+ handoff: IStoredAuthorityClaudeHandoff; home: IStoredAuthorityClaudeHome;
1662
+ outgoing: IStoredAuthorityGrant; incoming: IStoredAuthorityGrant;
1663
+ outgoingAccount: IStoredAuthorityAccount; incomingAccount: IStoredAuthorityAccount;
1664
+ } | null> {
1665
+ if (!/^[a-f0-9-]{36}$/.test(operationId)) throw new Error('Invalid Claude handoff selector.');
1666
+ return this.transaction(async session => {
1667
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStoredOne({ id: operationId }, { session });
1668
+ if (!stored) return null;
1669
+ const handoff = AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(stored);
1670
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: handoff.homeId }, { session });
1671
+ const outgoingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: handoff.outgoingGrantId }, { session });
1672
+ const incomingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: handoff.incomingGrantId }, { session });
1673
+ const outgoingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: handoff.outgoingAccountId }, { session });
1674
+ const incomingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: handoff.incomingAccountId }, { session });
1675
+ if (!homeStored || !outgoingStored || !incomingStored || !outgoingAccountStored || !incomingAccountStored) {
1676
+ throw new Error('Claude handoff references a missing authority record.');
1677
+ }
1678
+ return { handoff,
1679
+ home: AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(homeStored),
1680
+ outgoing: AuthSwitchAuthorityGrantModel.exact.toPersisted(outgoingStored),
1681
+ incoming: AuthSwitchAuthorityGrantModel.exact.toPersisted(incomingStored),
1682
+ outgoingAccount: AuthSwitchAuthorityAccountModel.exact.toPersisted(outgoingAccountStored),
1683
+ incomingAccount: AuthSwitchAuthorityAccountModel.exact.toPersisted(incomingAccountStored) };
1684
+ });
1685
+ }
1686
+
1687
+ /** One phase advances both grants, home, journal and both account events atomically. */
1688
+ public async changeClaudeHandoff(input: { operationId: string; homeId: string; incomingGrantId: string;
1689
+ updateId: string; eventIds: readonly [string, string];
1690
+ change: (current: {
1691
+ home: IStoredAuthorityClaudeHome; handoff: IStoredAuthorityClaudeHandoff | null;
1692
+ outgoing: IStoredAuthorityGrant; incoming: IStoredAuthorityGrant;
1693
+ outgoingAccount: IStoredAuthorityAccount; incomingAccount: IStoredAuthorityAccount;
1694
+ }) => TClaudeHandoffDraft }): Promise<TClaudeHandoffDraft> {
1695
+ const { operationId, homeId, incomingGrantId, updateId, eventIds } = input;
1696
+ if (!/^[a-f0-9-]{36}$/.test(operationId) || !/^[a-f0-9]{64}$/.test(homeId)
1697
+ || !/^[a-f0-9]{64}$/.test(incomingGrantId)
1698
+ || ![updateId, ...eventIds].every(value => /^[a-f0-9-]{36}$/.test(value))
1699
+ || new Set([updateId, ...eventIds]).size !== 3) throw new Error('Invalid Claude handoff identity.');
1700
+ let expectedPhase: IStoredAuthorityClaudeHandoff['phase'] | null = null;
1701
+ try {
1702
+ return await this.transaction(async session => {
1703
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1704
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
1705
+ const handoffStored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStoredOne({ id: operationId }, { session });
1706
+ if (!metaStored || !homeStored) throw new Error('Verified Claude home is unavailable.');
1707
+ const home = AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(homeStored);
1708
+ const handoff = handoffStored ? AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(handoffStored) : null;
1709
+ if (home.status !== 'ready' || (handoff === null ? home.pendingOperationId !== null
1710
+ : home.pendingOperationId !== operationId || handoff.homeId !== homeId
1711
+ || handoff.incomingGrantId !== incomingGrantId)) {
1712
+ throw new Error('Claude home has another pending or quarantined operation.');
1713
+ }
1714
+ const outgoingId = handoff?.outgoingGrantId ?? home.activeGrantId;
1715
+ if (outgoingId === incomingGrantId) throw new Error('Claude home already owns the selected grant.');
1716
+ const outgoingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: outgoingId }, { session });
1717
+ const incomingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: incomingGrantId }, { session });
1718
+ if (!outgoingStored || !incomingStored) throw new Error('Claude handoff grant is unavailable.');
1719
+ const outgoing = AuthSwitchAuthorityGrantModel.exact.toPersisted(outgoingStored);
1720
+ const incoming = AuthSwitchAuthorityGrantModel.exact.toPersisted(incomingStored);
1721
+ const incomingHome = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({
1722
+ activeGrantId: incomingGrantId,
1723
+ }, { session });
1724
+ if (incomingHome && incomingHome.id !== homeId) {
1725
+ throw new Error('Claude grant is already active in another native home.');
1726
+ }
1727
+ const outgoingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: outgoing.accountId }, { session });
1728
+ const incomingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: incoming.accountId }, { session });
1729
+ if (!outgoingAccountStored || !incomingAccountStored || outgoingAccountStored.removed || incomingAccountStored.removed
1730
+ || outgoing.accountId === incoming.accountId || outgoing.providerId !== 'anthropic'
1731
+ || incoming.providerId !== 'anthropic' || outgoing.purpose !== 'claude_host_native'
1732
+ || incoming.purpose !== 'claude_host_native' || home.activeGrantId !== outgoing.id
1733
+ || home.activeAccountId !== outgoing.accountId) throw new Error('Claude handoff account identity is invalid.');
1734
+ const outgoingAccount = AuthSwitchAuthorityAccountModel.exact.toPersisted(outgoingAccountStored);
1735
+ const incomingAccount = AuthSwitchAuthorityAccountModel.exact.toPersisted(incomingAccountStored);
1736
+ const draft = input.change({ home: structuredClone(home), handoff: handoff ? structuredClone(handoff) : null,
1737
+ outgoing: structuredClone(outgoing), incoming: structuredClone(incoming),
1738
+ outgoingAccount: structuredClone(outgoingAccount), incomingAccount: structuredClone(incomingAccount) });
1739
+ expectedPhase = draft.handoff.phase;
1740
+ assertStoredAuthorityClaudeHome(draft.home);
1741
+ assertStoredAuthorityClaudeHandoff(draft.handoff);
1742
+ if (draft.home.id !== homeId || draft.home.createdAt !== home.createdAt
1743
+ || draft.home.revision !== home.revision + 1 || draft.home.updateId !== updateId
1744
+ || draft.handoff.id !== operationId || draft.handoff.homeId !== homeId
1745
+ || draft.handoff.outgoingAccountId !== outgoing.accountId || draft.handoff.outgoingGrantId !== outgoing.id
1746
+ || draft.handoff.incomingAccountId !== incoming.accountId || draft.handoff.incomingGrantId !== incoming.id
1747
+ || draft.handoff.sourceIdentity.accountUuid !== outgoingAccount.subject
1748
+ || draft.handoff.sourceIdentity.organizationUuid !== outgoingAccount.workspaceId
1749
+ || draft.handoff.targetIdentity.accountUuid !== incomingAccount.subject
1750
+ || draft.handoff.targetIdentity.organizationUuid !== incomingAccount.workspaceId
1751
+ || draft.handoff.revision !== (handoff?.revision ?? 0) + 1 || draft.handoff.updateId !== updateId
1752
+ || draft.outgoing.id !== outgoing.id || draft.incoming.id !== incoming.id
1753
+ || draft.outgoing.accountId !== outgoing.accountId || draft.incoming.accountId !== incoming.accountId
1754
+ || draft.outgoing.updateId !== updateId || draft.incoming.updateId !== updateId
1755
+ || draft.outgoing.revision !== outgoing.revision + 1 || draft.incoming.revision !== incoming.revision + 1) {
1756
+ throw new Error('Claude handoff changed an immutable identity or revision.');
1757
+ }
1758
+ if (handoff && (draft.handoff.startedAt !== handoff.startedAt
1759
+ || JSON.stringify(draft.handoff.sourceIdentity) !== JSON.stringify(handoff.sourceIdentity)
1760
+ || JSON.stringify(draft.handoff.targetIdentity) !== JSON.stringify(handoff.targetIdentity)
1761
+ || (handoff.phase !== 'reserved' && (JSON.stringify(draft.handoff.before) !== JSON.stringify(handoff.before)
1762
+ || JSON.stringify(draft.handoff.after) !== JSON.stringify(handoff.after)
1763
+ || draft.handoff.sourceAccessDigest !== handoff.sourceAccessDigest
1764
+ || draft.handoff.targetAccessDigest !== handoff.targetAccessDigest)))) {
1765
+ throw new Error('Claude handoff proof or identity changed.');
1766
+ }
1767
+ if (!handoff) {
1768
+ let verifiedIncoming = false;
1769
+ for (const status of ['verified', 'complete'] as const) {
1770
+ const receipts = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
1771
+ filter: { grantId: incoming.id, status }, sort: { id: 1 }, limit: 128, session,
1772
+ });
1773
+ if (receipts.length === 128) throw new Error('Claude grant receipt set is unbounded.');
1774
+ verifiedIncoming ||= receipts.some(item => item.accountId === incoming.accountId
1775
+ && item.backupDigest !== null
1776
+ && ['authswitch_stash', 'authswitch_backup', 'claude_native'].includes(item.sourceKind));
1777
+ }
1778
+ if (draft.handoff.phase !== 'reserved' || home.pendingOperationId !== null
1779
+ || !verifiedIncoming
1780
+ || outgoing.state !== 'native' || outgoing.owner !== 'claude_native' || outgoing.ciphertext !== null
1781
+ || incoming.state !== 'ready' || incoming.owner !== 'daemon' || incoming.ciphertext === null
1782
+ || draft.home.activeGrantId !== outgoing.id || draft.home.pendingOperationId !== operationId
1783
+ || draft.home.status !== 'ready' || draft.outgoing.state !== 'native'
1784
+ || draft.outgoing.owner !== 'claude_native' || draft.outgoing.ciphertext !== null
1785
+ || draft.incoming.state !== 'handoff_pending' || draft.incoming.owner !== 'daemon'
1786
+ || draft.incoming.ciphertext !== incoming.ciphertext) throw new Error('Claude handoff reservation is invalid.');
1787
+ } else if (handoff.phase === 'reserved' && draft.handoff.phase === 'prepared') {
1788
+ if (outgoing.state !== 'native' || incoming.state !== 'handoff_pending'
1789
+ || draft.outgoing.state !== 'handoff_pending' || draft.outgoing.owner !== 'claude_native'
1790
+ || draft.outgoing.ciphertext !== null || draft.incoming.state !== 'handoff_pending'
1791
+ || draft.incoming.owner !== 'daemon' || draft.incoming.ciphertext !== incoming.ciphertext
1792
+ || draft.home.activeGrantId !== outgoing.id || draft.home.pendingOperationId !== operationId
1793
+ || draft.home.status !== 'ready') throw new Error('Claude handoff preparation is invalid.');
1794
+ } else if (handoff.phase === 'prepared' && draft.handoff.phase === 'committed') {
1795
+ if (outgoing.state !== 'handoff_pending' || incoming.state !== 'handoff_pending'
1796
+ || draft.outgoing.state !== 'ready' || draft.outgoing.owner !== 'daemon'
1797
+ || draft.outgoing.ciphertext !== handoff.sealedOutgoing
1798
+ || draft.incoming.state !== 'native' || draft.incoming.owner !== 'claude_native'
1799
+ || draft.incoming.ciphertext !== null || draft.home.activeAccountId !== incoming.accountId
1800
+ || draft.home.activeGrantId !== incoming.id || draft.home.pendingOperationId !== null
1801
+ || draft.home.status !== 'ready' || draft.handoff.sealedOutgoing !== null) {
1802
+ throw new Error('Claude handoff completion is invalid.');
1803
+ }
1804
+ } else if (['reserved', 'prepared'].includes(handoff.phase)
1805
+ && draft.handoff.phase === 'aborted') {
1806
+ if (incoming.state !== 'handoff_pending' || draft.incoming.state !== 'ready'
1807
+ || draft.incoming.owner !== 'daemon' || draft.incoming.ciphertext !== incoming.ciphertext
1808
+ || draft.outgoing.state !== 'native' || draft.outgoing.owner !== 'claude_native'
1809
+ || draft.outgoing.ciphertext !== null || draft.home.activeGrantId !== outgoing.id
1810
+ || draft.home.pendingOperationId !== null || draft.home.status !== 'ready'
1811
+ || draft.handoff.sealedOutgoing !== null) throw new Error('Claude handoff abort is invalid.');
1812
+ } else if (['reserved', 'prepared'].includes(handoff.phase)
1813
+ && draft.handoff.phase === 'quarantined') {
1814
+ if (draft.outgoing.state !== 'handoff_quarantined' || draft.outgoing.owner !== 'claude_native'
1815
+ || draft.incoming.state !== 'handoff_quarantined' || draft.incoming.owner !== 'daemon'
1816
+ || draft.home.activeGrantId !== outgoing.id || draft.home.pendingOperationId !== operationId
1817
+ || draft.home.status !== 'quarantined' || draft.handoff.problem === 'none') {
1818
+ throw new Error('Claude handoff quarantine is invalid.');
1819
+ }
1820
+ } else throw new Error('Claude handoff phase transition is invalid.');
1821
+ await this.persistGrant(session, outgoingStored, draft.outgoing, true);
1822
+ await this.persistGrant(session, incomingStored, draft.incoming, true);
1823
+ const homeResult = await AuthSwitchAuthorityClaudeHomeModel.exact.transition({ current: homeStored,
1824
+ change: model => Object.assign(model, draft.home) }, { session });
1825
+ if (homeResult.status !== 'transitioned') throw new Error('Claude home changed concurrently.');
1826
+ if (handoffStored) {
1827
+ const result = await AuthSwitchAuthorityClaudeHandoffModel.exact.transition({ current: handoffStored,
1828
+ change: model => Object.assign(model, draft.handoff) }, { session });
1829
+ if (result.status !== 'transitioned') throw new Error('Claude handoff changed concurrently.');
1830
+ } else {
1831
+ const result = await AuthSwitchAuthorityClaudeHandoffModel.exact.insert(draft.handoff, { session });
1832
+ if (result.status !== 'inserted') throw new Error('Claude handoff operation already exists.');
1833
+ }
1834
+ const outgoingAccountDraft = { ...outgoingAccount, revision: outgoingAccount.revision + 1,
1835
+ statusObservedAt: draft.handoff.updatedAt, updateId };
1836
+ const incomingAccountDraft = { ...incomingAccount, revision: incomingAccount.revision + 1,
1837
+ statusObservedAt: draft.handoff.updatedAt, updateId };
1838
+ await this.persistAccount(session, outgoingAccountStored, outgoingAccountDraft);
1839
+ await this.persistAccount(session, incomingAccountStored, incomingAccountDraft);
1840
+ const first = await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
1841
+ eventIds[0], 'account', outgoing.accountId);
1842
+ await this.appendEvent(session, first, eventIds[1], 'account', incoming.accountId);
1843
+ return draft;
1844
+ });
1845
+ } catch (error) {
1846
+ if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1847
+ const context = await this.readClaudeHandoffContext(operationId);
1848
+ const first = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: eventIds[0] });
1849
+ const second = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: eventIds[1] });
1850
+ if (context && context.handoff.phase === expectedPhase
1851
+ && [context.handoff, context.home, context.outgoing, context.incoming,
1852
+ context.outgoingAccount, context.incomingAccount].every(item => item.updateId === updateId)
1853
+ && first?.accountId === context.outgoing.accountId
1854
+ && second?.accountId === context.incoming.accountId && first.epoch === second.epoch
1855
+ && second.revision === first.revision + 1) {
1856
+ return { handoff: context.handoff, home: context.home,
1857
+ outgoing: context.outgoing, incoming: context.incoming };
1858
+ }
1859
+ throw new Error('Claude handoff database outcome is unknown; native ownership remains blocked.');
1860
+ }
1861
+ }
1862
+
679
1863
  /** Ledger transitions never silently replace an active credential or waive backup proof. */
680
1864
  public async changeMigrationLedger(updateId: string, ledgerId: string,
681
1865
  change: (current: IStoredAuthorityMigrationLedger | null) => IStoredAuthorityMigrationLedger): Promise<IStoredAuthorityMigrationLedger> {
@@ -713,6 +1897,7 @@ export class AuthSwitchAuthorityDatabase {
713
1897
 
714
1898
  public async stop(): Promise<void> {
715
1899
  this.started = false;
1900
+ this.mode = null;
716
1901
  const database = this.database;
717
1902
  const engine = this.engine;
718
1903
  this.database = undefined;