@modelprofile.com/authswitch 6.5.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 +13 -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 +25 -5
  11. package/dist_ts/classes.authoritybroker.js +392 -123
  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 +19 -7
  15. package/dist_ts/classes.authorityclient.js +51 -14
  16. package/dist_ts/classes.authoritydaemon.d.ts +28 -1
  17. package/dist_ts/classes.authoritydaemon.js +482 -36
  18. package/dist_ts/classes.authoritydatabase.d.ts +175 -10
  19. package/dist_ts/classes.authoritydatabase.js +1100 -52
  20. package/dist_ts/classes.authoritymodels.d.ts +223 -1
  21. package/dist_ts/classes.authoritymodels.js +798 -9
  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 +11 -0
  43. package/dist_ts/classes.codexmanaged.js +75 -14
  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 +1 -0
  53. package/dist_ts/index.js +7 -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 +5 -2
  57. package/dist_ts/plugins.js +7 -3
  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 +4 -1
  63. package/readme.md +181 -14
  64. package/ts/00_commitinfo_data.ts +1 -1
  65. package/ts/authority-contract.ts +264 -9
  66. package/ts/authority-runtime-contract.ts +14 -0
  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 +389 -121
  71. package/ts/classes.authoritycli.ts +719 -0
  72. package/ts/classes.authorityclient.ts +75 -18
  73. package/ts/classes.authoritydaemon.ts +449 -36
  74. package/ts/classes.authoritydatabase.ts +1147 -55
  75. package/ts/classes.authoritymodels.ts +541 -6
  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 +75 -12
  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 +6 -0
  94. package/ts/interfaces.harness.ts +6 -0
  95. package/ts/interfaces.list.ts +9 -3
  96. package/ts/plugins.ts +7 -2
  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, AuthSwitchAuthorityCodexHomeModel, 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 IStoredAuthorityCodexHome, 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, AuthSwitchAuthorityCodexHomeModel, 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(), AuthSwitchAuthorityCodexHomeModel.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(), AuthSwitchAuthorityCodexHomeModel.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' });
@@ -111,28 +245,40 @@ export class AuthSwitchAuthorityDatabase {
111
245
  return stored ? AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(stored) : null;
112
246
  }
113
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
+
114
260
  /** Home path identity is hashed by the caller; no vendor files or raw paths enter SmartData. */
115
- public async reserveCodexHome(accountId: string, scopeId: string, homeId: string,
261
+ public async reserveCodexHome(accountId: string, grantId: string, scopeId: string, homeId: string,
116
262
  updateId: string): Promise<{ newlyRegistered: boolean }> {
117
263
  try {
118
264
  return await this.transaction(async session => {
119
265
  const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
120
266
  const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
121
- const grantStored = account?.primaryGrantId
122
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
267
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
123
268
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
124
269
  if (!account || account.removed || account.providerId !== 'openai' || grant?.owner !== 'daemon'
125
- || grant.purpose !== 'openai_managed' || grant.accountId !== accountId || grant.state === 'removed') {
270
+ || grant.purpose !== 'openai_managed' || grant.accountId !== accountId
271
+ || grant.id !== account.primaryGrantId || grant.state === 'removed') {
126
272
  throw new Error('Managed Codex requires a daemon-owned OpenAI account.');
127
273
  }
128
274
  const used = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId }, { session });
129
275
  if (used) {
130
- if (used.accountId !== accountId || used.scopeId !== scopeId) {
276
+ if (used.accountId !== accountId || used.grantId !== grantId || used.scopeId !== scopeId) {
131
277
  throw new Error('Managed Codex home already belongs to another account or scope.');
132
278
  }
133
279
  return { newlyRegistered: false };
134
280
  }
135
- const record: IStoredAuthorityCodexHome = { id: homeId, accountId, scopeId, activeRun: null,
281
+ const record: IStoredAuthorityCodexHome = { id: homeId, accountId, grantId, scopeId, activeRun: null,
136
282
  createdAt: new Date().toISOString(), updateId };
137
283
  const inserted = await AuthSwitchAuthorityCodexHomeModel.exact.insert(record, { session });
138
284
  if (inserted.status !== 'inserted') throw new Error('Managed Codex home changed during admission.');
@@ -142,7 +288,7 @@ export class AuthSwitchAuthorityDatabase {
142
288
  if (error instanceof plugins.smartdata.SmartdataExactPersistenceError
143
289
  && (error.code === 'ambiguous_write' || error.code === 'unique_conflict')) {
144
290
  const assigned = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId });
145
- if (assigned?.accountId === accountId && assigned.scopeId === scopeId
291
+ if (assigned?.accountId === accountId && assigned.grantId === grantId && assigned.scopeId === scopeId
146
292
  && assigned.updateId === updateId) return { newlyRegistered: true };
147
293
  throw new Error('Managed Codex home registration changed concurrently; refresh before retrying.');
148
294
  }
@@ -188,10 +334,10 @@ export class AuthSwitchAuthorityDatabase {
188
334
  const accountStored = binding
189
335
  ? await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: binding.accountId }, { session }) : null;
190
336
  const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
191
- const grantStored = binding && account?.primaryGrantId === binding.grantId
337
+ const grantStored = binding && account && !account.removed
192
338
  ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: binding.grantId }, { session }) : null;
193
339
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
194
- return { binding, account, grant };
340
+ return { binding, account, grant: grant?.accountId === account?.id ? grant : null };
195
341
  });
196
342
  }
197
343
 
@@ -219,6 +365,296 @@ export class AuthSwitchAuthorityDatabase {
219
365
  return stored ? AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(stored) : null;
220
366
  }
221
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
+
222
658
  public async migrationLedgerByStatus(status: IStoredAuthorityMigrationLedger['status'],
223
659
  after: string | null, limit = 128): Promise<IStoredAuthorityMigrationLedger[]> {
224
660
  this.db();
@@ -241,10 +677,153 @@ export class AuthSwitchAuthorityDatabase {
241
677
  });
242
678
  }
243
679
 
244
- public async page(accountAfter: string | null, bindingAfter: string | null, limit: number): Promise<{
245
- 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[];
246
825
  bindings: IStoredAuthorityBinding[];
247
- nextAccountCursor: string | null; nextBindingCursor: string | null;
826
+ nextAccountCursor: string | null; nextGrantCursor: string | null; nextBindingCursor: string | null;
248
827
  }> {
249
828
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid authority page size.');
250
829
  return this.transaction(async session => {
@@ -254,28 +833,128 @@ export class AuthSwitchAuthorityDatabase {
254
833
  filter: accountAfter ? { id: { $gt: accountAfter } } : {},
255
834
  sort: { id: 1 }, limit: limit + 1, session,
256
835
  });
836
+ const grants = await AuthSwitchAuthorityGrantModel.exact.findStored({
837
+ filter: grantAfter ? { id: { $gt: grantAfter } } : {},
838
+ sort: { id: 1 }, limit: limit + 1, session,
839
+ });
257
840
  const bindings = await AuthSwitchAuthorityBindingModel.exact.findStored({
258
841
  filter: bindingAfter ? { id: { $gt: bindingAfter } } : {},
259
842
  sort: { id: 1 }, limit: limit + 1, session,
260
843
  });
261
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));
262
846
  const bindingPage = bindings.slice(0, limit).map(item => AuthSwitchAuthorityBindingModel.exact.toPersisted(item));
263
- const withGrants: Array<{ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant | null }> = [];
264
- for (const account of accountPage) {
265
- const stored = account.primaryGrantId
266
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
267
- if (account.primaryGrantId && !stored) throw new Error('Authority account primary grant is missing.');
268
- withGrants.push({ account, grant: stored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(stored) : null });
269
- }
270
847
  return {
271
848
  meta: AuthSwitchAuthorityMetaModel.exact.toPersisted(meta),
272
- accounts: withGrants, bindings: bindingPage,
849
+ accounts: accountPage, grants: grantPage, bindings: bindingPage,
273
850
  nextAccountCursor: accounts.length > limit ? accountPage.at(-1)!.id : null,
851
+ nextGrantCursor: grants.length > limit ? grantPage.at(-1)!.id : null,
274
852
  nextBindingCursor: bindings.length > limit ? bindingPage.at(-1)!.id : null,
275
853
  };
276
854
  });
277
855
  }
278
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
+
279
958
  public async grantsByState(state: IStoredAuthorityGrant['state'], after: string | null, limit = 128): Promise<IStoredAuthorityGrant[]> {
280
959
  this.db();
281
960
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid authority scan size.');
@@ -338,18 +1017,27 @@ export class AuthSwitchAuthorityDatabase {
338
1017
  }
339
1018
 
340
1019
  private async persistGrant(session: TSession, current: Awaited<ReturnType<typeof AuthSwitchAuthorityGrantModel.exact.findStoredOne>>,
341
- draft: IStoredAuthorityGrant): Promise<void> {
1020
+ draft: IStoredAuthorityGrant, allowClaudeHandoff = false): Promise<void> {
342
1021
  assertStoredAuthorityGrant(draft);
343
1022
  if (current) {
344
1023
  const prior = AuthSwitchAuthorityGrantModel.exact.toPersisted(current);
345
1024
  if (draft.id !== prior.id || draft.accountId !== prior.accountId
346
1025
  || draft.providerId !== prior.providerId || draft.purpose !== prior.purpose
347
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
+ }
348
1033
  const result = await AuthSwitchAuthorityGrantModel.exact.transition({
349
1034
  current, change: model => Object.assign(model, draft),
350
1035
  }, { session });
351
1036
  if (result.status !== 'transitioned') throw new Error('Grant changed concurrently.');
352
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
+ }
353
1041
  const result = await AuthSwitchAuthorityGrantModel.exact.insert(draft, { session });
354
1042
  if (result.status !== 'inserted') throw new Error('Grant was added concurrently.');
355
1043
  }
@@ -398,6 +1086,75 @@ export class AuthSwitchAuthorityDatabase {
398
1086
  }
399
1087
  }
400
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
+
401
1158
  /** Grant transition and public account revision share a transaction and one event. */
402
1159
  public async changeGrant(updateId: string, grantId: string,
403
1160
  change: (grant: IStoredAuthorityGrant, account: IStoredAuthorityAccount) => IStoredAuthorityGrant): Promise<{
@@ -416,6 +1173,11 @@ export class AuthSwitchAuthorityDatabase {
416
1173
  if (draft.id !== grantId || draft.accountId !== account.id || draft.updateId !== updateId) {
417
1174
  throw new Error('Grant change has an invalid identity.');
418
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
+ }
419
1181
  const accountDraft: IStoredAuthorityAccount = { ...account, revision: account.revision + 1,
420
1182
  statusObservedAt: draft.statusObservedAt, updateId };
421
1183
  await this.persistGrant(session, grantStored, draft);
@@ -504,6 +1266,9 @@ export class AuthSwitchAuthorityDatabase {
504
1266
  if (grant.state === 'exchange_may_have_been_sent') {
505
1267
  throw new Error('Wait for the account refresh to finish before removing it.');
506
1268
  }
1269
+ if (['handoff_pending', 'handoff_quarantined'].includes(grant.state)) {
1270
+ throw new Error('Resolve the Claude native handoff before removing this account.');
1271
+ }
507
1272
  });
508
1273
  await scan(async (grant, stored) => {
509
1274
  if (grant.state === 'removed') return;
@@ -584,7 +1349,7 @@ export class AuthSwitchAuthorityDatabase {
584
1349
  }
585
1350
  }
586
1351
 
587
- public async changeBinding(updateId: string, bindingId: string, accountId: string,
1352
+ public async changeBinding(updateId: string, bindingId: string, accountId: string, grantId: string,
588
1353
  change: (draft: IStoredAuthorityBinding | null, account: IStoredAuthorityAccount,
589
1354
  grant: IStoredAuthorityGrant | null) => IStoredAuthorityBinding): Promise<{
590
1355
  meta: IStoredAuthorityMeta; binding: IStoredAuthorityBinding;
@@ -595,13 +1360,14 @@ export class AuthSwitchAuthorityDatabase {
595
1360
  const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
596
1361
  if (!metaStored || !accountStored) throw new Error('Account is not available for this runtime.');
597
1362
  const account = AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored);
598
- const grantStored = account.primaryGrantId
599
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
1363
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
600
1364
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
601
1365
  const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
602
1366
  const draft = change(current ? structuredClone(AuthSwitchAuthorityBindingModel.exact.toPersisted(current)) : null, account, grant);
603
1367
  if (draft.id !== bindingId || draft.accountId !== accountId || draft.updateId !== updateId) throw new Error('Binding change has an invalid identity.');
604
- 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
+ }
605
1371
  if (current && (draft.runtime !== current.runtime || draft.scopeId !== current.scopeId)) {
606
1372
  throw new Error('Runtime binding identity cannot change.');
607
1373
  }
@@ -628,14 +1394,15 @@ export class AuthSwitchAuthorityDatabase {
628
1394
  }
629
1395
  }
630
1396
 
631
- /** Closing an owned runtime revokes only its exact capability/incarnation. */
632
- public async revokeBinding(updateId: string, bindingId: string, capabilityHash: string): Promise<boolean> {
1397
+ private async revokeBindingExact(updateId: string, bindingId: string, capabilityHash: string,
1398
+ externalOnly: boolean): Promise<boolean> {
633
1399
  try {
634
1400
  return await this.transaction(async session => {
635
1401
  const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
636
1402
  if (!metaStored) throw new Error('Authswitch authority metadata is missing.');
637
1403
  const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
638
- if (!current || current.capabilityHash !== capabilityHash) return false;
1404
+ if (!current || current.capabilityHash !== capabilityHash
1405
+ || (externalOnly && !['flex', 'opencode'].includes(current.runtime))) return false;
639
1406
  const result = await AuthSwitchAuthorityBindingModel.exact.delete({ current }, { session });
640
1407
  if (result.status !== 'deleted') throw new Error('Runtime binding changed during revocation.');
641
1408
  await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
@@ -645,11 +1412,24 @@ export class AuthSwitchAuthorityDatabase {
645
1412
  } catch (error) {
646
1413
  if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
647
1414
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
648
- if (event && !(await this.readBinding(bindingId))) return true;
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;
649
1419
  throw new Error('Authswitch binding revocation outcome is unknown.');
650
1420
  }
651
1421
  }
652
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
+
653
1433
  /** Enrollment metadata is sealed by the caller and cannot become detached from an account. */
654
1434
  public async changeEnrollment(updateId: string, enrollmentId: string, accountId: string,
655
1435
  change: (current: IStoredAuthorityEnrollment | null) => IStoredAuthorityEnrollment): Promise<IStoredAuthorityEnrollment> {
@@ -769,6 +1549,317 @@ export class AuthSwitchAuthorityDatabase {
769
1549
  }
770
1550
  }
771
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
+
772
1863
  /** Ledger transitions never silently replace an active credential or waive backup proof. */
773
1864
  public async changeMigrationLedger(updateId: string, ledgerId: string,
774
1865
  change: (current: IStoredAuthorityMigrationLedger | null) => IStoredAuthorityMigrationLedger): Promise<IStoredAuthorityMigrationLedger> {
@@ -806,6 +1897,7 @@ export class AuthSwitchAuthorityDatabase {
806
1897
 
807
1898
  public async stop(): Promise<void> {
808
1899
  this.started = false;
1900
+ this.mode = null;
809
1901
  const database = this.database;
810
1902
  const engine = this.engine;
811
1903
  this.database = undefined;