@modelprofile.com/authswitch 6.5.0 → 8.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 (95) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +300 -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/classes.accountlist.js +4 -3
  6. package/dist_ts/classes.authoritybroker.d.ts +25 -5
  7. package/dist_ts/classes.authoritybroker.js +388 -123
  8. package/dist_ts/classes.authoritycli.d.ts +24 -0
  9. package/dist_ts/classes.authoritycli.js +675 -0
  10. package/dist_ts/classes.authorityclient.d.ts +19 -7
  11. package/dist_ts/classes.authorityclient.js +51 -14
  12. package/dist_ts/classes.authoritydaemon.d.ts +28 -1
  13. package/dist_ts/classes.authoritydaemon.js +477 -36
  14. package/dist_ts/classes.authoritydatabase.d.ts +155 -11
  15. package/dist_ts/classes.authoritydatabase.js +1037 -69
  16. package/dist_ts/classes.authoritymodels.d.ts +229 -23
  17. package/dist_ts/classes.authoritymodels.js +867 -128
  18. package/dist_ts/classes.authoritypreuse.d.ts +47 -0
  19. package/dist_ts/classes.authoritypreuse.js +277 -0
  20. package/dist_ts/classes.authorityregistry.d.ts +9 -0
  21. package/dist_ts/classes.authorityregistry.js +32 -0
  22. package/dist_ts/classes.authoritysecrets.d.ts +1 -1
  23. package/dist_ts/classes.authoritysecrets.js +2 -2
  24. package/dist_ts/classes.authorityusage.d.ts +160 -0
  25. package/dist_ts/classes.authorityusage.js +429 -0
  26. package/dist_ts/classes.claudeauthority.d.ts +66 -0
  27. package/dist_ts/classes.claudeauthority.js +528 -0
  28. package/dist_ts/classes.claudecodeharness.js +3 -2
  29. package/dist_ts/classes.claudecodelocks.d.ts +13 -0
  30. package/dist_ts/classes.claudecodelocks.js +75 -3
  31. package/dist_ts/classes.claudenative.d.ts +130 -0
  32. package/dist_ts/classes.claudenative.js +708 -0
  33. package/dist_ts/classes.claudestatus.d.ts +9 -0
  34. package/dist_ts/classes.claudestatus.js +26 -9
  35. package/dist_ts/classes.claudetokenrefresh.d.ts +15 -1
  36. package/dist_ts/classes.claudetokenrefresh.js +74 -21
  37. package/dist_ts/classes.codexharness.js +4 -2
  38. package/dist_ts/classes.codexmanaged.d.ts +12 -1
  39. package/dist_ts/classes.codexmanaged.js +75 -14
  40. package/dist_ts/classes.codexpreuse.d.ts +8 -0
  41. package/dist_ts/classes.codexpreuse.js +24 -6
  42. package/dist_ts/classes.fileharness.d.ts +2 -0
  43. package/dist_ts/classes.fileharness.js +6 -4
  44. package/dist_ts/classes.limits.js +12 -7
  45. package/dist_ts/classes.opencodeharness.js +2 -2
  46. package/dist_ts/claudehttp.d.ts +10 -0
  47. package/dist_ts/claudehttp.js +29 -3
  48. package/dist_ts/index.d.ts +1 -0
  49. package/dist_ts/index.js +7 -1
  50. package/dist_ts/interfaces.harness.d.ts +6 -0
  51. package/dist_ts/interfaces.list.d.ts +9 -3
  52. package/dist_ts/plugins.d.ts +6 -3
  53. package/dist_ts/plugins.js +8 -4
  54. package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -0
  55. package/dist_ts/ts_migration/0001_authority_meta.js +44 -0
  56. package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
  57. package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
  58. package/dist_ts/ts_migration/index.d.ts +6 -0
  59. package/dist_ts/ts_migration/index.js +12 -0
  60. package/package.json +3 -3
  61. package/readme.md +177 -14
  62. package/ts/00_commitinfo_data.ts +1 -1
  63. package/ts/authority-contract.ts +262 -9
  64. package/ts/authority-runtime-contract.ts +14 -0
  65. package/ts/classes.accountlist.ts +3 -2
  66. package/ts/classes.authoritybroker.ts +385 -121
  67. package/ts/classes.authoritycli.ts +719 -0
  68. package/ts/classes.authorityclient.ts +75 -18
  69. package/ts/classes.authoritydaemon.ts +444 -36
  70. package/ts/classes.authoritydatabase.ts +1076 -75
  71. package/ts/classes.authoritymodels.ts +642 -144
  72. package/ts/classes.authoritypreuse.ts +296 -0
  73. package/ts/classes.authorityregistry.ts +47 -0
  74. package/ts/classes.authoritysecrets.ts +1 -1
  75. package/ts/classes.authorityusage.ts +527 -0
  76. package/ts/classes.claudeauthority.ts +537 -0
  77. package/ts/classes.claudecodeharness.ts +2 -1
  78. package/ts/classes.claudecodelocks.ts +70 -1
  79. package/ts/classes.claudenative.ts +785 -0
  80. package/ts/classes.claudestatus.ts +38 -9
  81. package/ts/classes.claudetokenrefresh.ts +82 -19
  82. package/ts/classes.codexharness.ts +3 -1
  83. package/ts/classes.codexmanaged.ts +76 -13
  84. package/ts/classes.codexpreuse.ts +33 -5
  85. package/ts/classes.fileharness.ts +7 -3
  86. package/ts/classes.limits.ts +11 -6
  87. package/ts/classes.opencodeharness.ts +1 -1
  88. package/ts/claudehttp.ts +31 -2
  89. package/ts/index.ts +6 -0
  90. package/ts/interfaces.harness.ts +6 -0
  91. package/ts/interfaces.list.ts +9 -3
  92. package/ts/plugins.ts +8 -3
  93. package/ts/ts_migration/0001_authority_meta.ts +45 -0
  94. package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
  95. package/ts/ts_migration/index.ts +12 -0
@@ -1,14 +1,23 @@
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 { authorityModelRegistry } from './classes.authorityregistry.js';
20
+ import { runAuthorityMigrations } from './ts_migration/index.js';
12
21
 
13
22
  export interface IAuthSwitchAuthorityDatabaseOptions {
14
23
  dataDirectory: string;
@@ -16,12 +25,46 @@ export interface IAuthSwitchAuthorityDatabaseOptions {
16
25
  }
17
26
 
18
27
  type TEventKind = IStoredAuthorityEvent['kind'];
19
- type TSession = plugins.smartdata.SmartdataSession;
28
+ type TSession = plugins.nosqldb.SmartdataSession;
29
+ type TStoredAuthorityDeviceOperationAdmission = Omit<IStoredAuthorityDeviceOperation, 'expectedGrantGeneration'>;
30
+ type TClaudeHandoffDraft = {
31
+ home: IStoredAuthorityClaudeHome;
32
+ handoff: IStoredAuthorityClaudeHandoff;
33
+ outgoing: IStoredAuthorityGrant;
34
+ incoming: IStoredAuthorityGrant;
35
+ };
36
+
37
+ const usageContext = (meta: IStoredAuthorityMeta, account: IStoredAuthorityAccount,
38
+ grant: IStoredAuthorityGrant): IAuthSwitchUsageContext => ({
39
+ epoch: meta.epoch, accountId: account.id, loginId: grant.id, subjectId: account.subject,
40
+ workspaceId: account.workspaceId, providerId: account.providerId,
41
+ purpose: grant.purpose, owner: grant.owner, grantState: grant.state,
42
+ removed: account.removed, authorizationGeneration: grant.authorizationGeneration,
43
+ grantGeneration: grant.grantGeneration, nativeSource: null,
44
+ });
45
+ const sameUsageContext = (left: IAuthSwitchUsageContext, right: IAuthSwitchUsageContext): boolean =>
46
+ left.epoch === right.epoch && left.accountId === right.accountId && left.loginId === right.loginId
47
+ && left.subjectId === right.subjectId && left.workspaceId === right.workspaceId
48
+ && left.providerId === right.providerId
49
+ && left.purpose === right.purpose && left.owner === right.owner && left.grantState === right.grantState
50
+ && left.removed === right.removed && left.authorizationGeneration === right.authorizationGeneration
51
+ && left.grantGeneration === right.grantGeneration;
52
+ const sameUsageSource = (left: IAuthSwitchUsageContext, right: IAuthSwitchUsageContext): boolean =>
53
+ sameUsageContext(left, right) && (left.nativeSource === null ? right.nativeSource === null
54
+ : right.nativeSource !== null && left.nativeSource.homeId === right.nativeSource.homeId
55
+ && left.nativeSource.credentialDigest === right.nativeSource.credentialDigest
56
+ && left.nativeSource.configDigest === right.nativeSource.configDigest
57
+ && left.nativeSource.accessTokenDigest === right.nativeSource.accessTokenDigest);
58
+ const storedOperation = (stored: Parameters<typeof AuthSwitchAuthorityOperationModel.exact.toPersisted>[0]): IStoredAuthorityOperation => {
59
+ const operation = AuthSwitchAuthorityOperationModel.exact.toPersisted(stored);
60
+ assertStoredAuthorityOperation(operation);
61
+ return operation;
62
+ };
20
63
 
21
64
  /** Separate account, grant, binding and journal records; the only global record is a revision counter. */
22
65
  export class AuthSwitchAuthorityDatabase {
23
- private engine?: plugins.smartdb.LocalSmartDb;
24
- private database?: plugins.smartdata.SmartdataDb;
66
+ private engine?: plugins.nosqldbEngine.LocalNoSqlDb;
67
+ private database?: plugins.nosqldb.SmartdataDb;
25
68
  private started = false;
26
69
 
27
70
  constructor(private readonly options: IAuthSwitchAuthorityDatabaseOptions) {}
@@ -39,32 +82,41 @@ export class AuthSwitchAuthorityDatabase {
39
82
  if (this.started || this.engine || this.database) throw new Error('Authswitch database is already starting or started.');
40
83
  await this.privateDirectory(this.options.dataDirectory);
41
84
  await this.privateDirectory(plugins.path.dirname(this.options.socketPath));
42
- const engine = new plugins.smartdb.LocalSmartDb({ folderPath: this.options.dataDirectory, socketPath: this.options.socketPath });
85
+ const engine = new plugins.nosqldbEngine.LocalNoSqlDb({ folderPath: this.options.dataDirectory, socketPath: this.options.socketPath });
43
86
  this.engine = engine;
44
87
  try {
45
88
  const connection = await engine.start();
46
- const database = new plugins.smartdata.SmartdataDb({ mongoDbUrl: connection.connectionUri, mongoDbName: 'authswitch' });
89
+ const database = new plugins.nosqldb.SmartdataDb({ mongoDbUrl: connection.connectionUri, mongoDbName: 'authswitch' });
47
90
  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
91
  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.');
92
+ // The LocalNoSqlDb root lease excludes another engine during this catalog admission.
93
+ const inventory = await database.inspectNamespace({ maxCollections: 1024,
94
+ documentCountUpTo: 0, timeoutMs: 30_000 });
95
+ const names = new Set(authorityModelRegistry.map(entry => entry.name));
96
+ for (const collection of inventory.collections) {
97
+ if (collection.namespaceType !== 'collection' || !names.has(collection.collectionName)) {
98
+ throw new Error('Authswitch authority database contains an unknown collection or namespace.');
99
+ }
100
+ }
101
+ const fresh = inventory.collections.length === 0;
102
+ if (!fresh) {
103
+ if (!inventory.collections.some(item => item.collectionName === 'authswitch_authority_meta')) {
104
+ throw new Error('Existing authswitch authority database has no metadata collection.');
105
+ }
106
+ await runAuthorityMigrations(database);
107
+ }
108
+ await Promise.all(authorityModelRegistry.map(entry => entry.initialize(database)));
109
+ await Promise.all(authorityModelRegistry.map(entry => entry.prepare()));
110
+ if (fresh) {
111
+ const initial: IStoredAuthorityMeta = {
112
+ id: 'authswitch-authority',
113
+ epoch: plugins.crypto.randomUUID(), revision: 0, updateId: plugins.crypto.randomUUID(),
114
+ };
115
+ const result = await AuthSwitchAuthorityMetaModel.exact.insert(initial);
116
+ if (result.status !== 'inserted') throw new Error('Authswitch authority could not initialize.');
117
+ } else if (!await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' })) {
118
+ throw new Error('Existing authswitch authority metadata is missing.');
119
+ }
68
120
  this.started = true;
69
121
  } catch (error) {
70
122
  try { await this.stop(); } catch { /* Preserve the original startup error. */ }
@@ -72,7 +124,7 @@ export class AuthSwitchAuthorityDatabase {
72
124
  }
73
125
  }
74
126
 
75
- private db(): plugins.smartdata.SmartdataDb {
127
+ private db(): plugins.nosqldb.SmartdataDb {
76
128
  if (!this.started || !this.database) throw new Error('Authswitch database is unavailable.');
77
129
  return this.database;
78
130
  }
@@ -111,38 +163,50 @@ export class AuthSwitchAuthorityDatabase {
111
163
  return stored ? AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(stored) : null;
112
164
  }
113
165
 
114
- /** 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,
166
+ /** Durable home claims must be inspected even when the daemon has lost its process map. */
167
+ public async codexHomesForAccount(accountId: string, after: string | null): Promise<IStoredAuthorityCodexHome[]> {
168
+ this.db();
169
+ if (!/^[a-f0-9]{64}$/.test(accountId) || (after !== null && !/^[a-f0-9]{64}$/.test(after))) {
170
+ throw new Error('Invalid managed Codex home selector.');
171
+ }
172
+ const stored = await AuthSwitchAuthorityCodexHomeModel.exact.findStored({
173
+ filter: after ? { accountId, id: { $gt: after } } : { accountId }, sort: { id: 1 }, limit: 128,
174
+ });
175
+ return stored.map(item => AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(item));
176
+ }
177
+
178
+ /** Home path identity is hashed by the caller; no vendor files or raw paths enter NoSQLDB. */
179
+ public async reserveCodexHome(accountId: string, grantId: string, scopeId: string, homeId: string,
116
180
  updateId: string): Promise<{ newlyRegistered: boolean }> {
117
181
  try {
118
182
  return await this.transaction(async session => {
119
183
  const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
120
184
  const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
121
- const grantStored = account?.primaryGrantId
122
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
185
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
123
186
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
124
187
  if (!account || account.removed || account.providerId !== 'openai' || grant?.owner !== 'daemon'
125
- || grant.purpose !== 'openai_managed' || grant.accountId !== accountId || grant.state === 'removed') {
188
+ || grant.purpose !== 'openai_managed' || grant.accountId !== accountId
189
+ || grant.id !== account.primaryGrantId || grant.state === 'removed') {
126
190
  throw new Error('Managed Codex requires a daemon-owned OpenAI account.');
127
191
  }
128
192
  const used = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId }, { session });
129
193
  if (used) {
130
- if (used.accountId !== accountId || used.scopeId !== scopeId) {
194
+ if (used.accountId !== accountId || used.grantId !== grantId || used.scopeId !== scopeId) {
131
195
  throw new Error('Managed Codex home already belongs to another account or scope.');
132
196
  }
133
197
  return { newlyRegistered: false };
134
198
  }
135
- const record: IStoredAuthorityCodexHome = { id: homeId, accountId, scopeId, activeRun: null,
199
+ const record: IStoredAuthorityCodexHome = { id: homeId, accountId, grantId, scopeId, activeRun: null,
136
200
  createdAt: new Date().toISOString(), updateId };
137
201
  const inserted = await AuthSwitchAuthorityCodexHomeModel.exact.insert(record, { session });
138
202
  if (inserted.status !== 'inserted') throw new Error('Managed Codex home changed during admission.');
139
203
  return { newlyRegistered: true };
140
204
  });
141
205
  } catch (error) {
142
- if (error instanceof plugins.smartdata.SmartdataExactPersistenceError
206
+ if (error instanceof plugins.nosqldb.SmartdataExactPersistenceError
143
207
  && (error.code === 'ambiguous_write' || error.code === 'unique_conflict')) {
144
208
  const assigned = await AuthSwitchAuthorityCodexHomeModel.exact.findStoredOne({ id: homeId });
145
- if (assigned?.accountId === accountId && assigned.scopeId === scopeId
209
+ if (assigned?.accountId === accountId && assigned.grantId === grantId && assigned.scopeId === scopeId
146
210
  && assigned.updateId === updateId) return { newlyRegistered: true };
147
211
  throw new Error('Managed Codex home registration changed concurrently; refresh before retrying.');
148
212
  }
@@ -169,7 +233,7 @@ export class AuthSwitchAuthorityDatabase {
169
233
  return true;
170
234
  });
171
235
  } catch (error) {
172
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
236
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
173
237
  const current = await this.readCodexHome(homeId);
174
238
  if (current?.updateId === updateId && JSON.stringify(current.activeRun) === JSON.stringify(activeRun)) return;
175
239
  throw new Error('Managed Codex home run outcome is unknown.');
@@ -188,10 +252,10 @@ export class AuthSwitchAuthorityDatabase {
188
252
  const accountStored = binding
189
253
  ? await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: binding.accountId }, { session }) : null;
190
254
  const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
191
- const grantStored = binding && account?.primaryGrantId === binding.grantId
255
+ const grantStored = binding && account && !account.removed
192
256
  ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: binding.grantId }, { session }) : null;
193
257
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
194
- return { binding, account, grant };
258
+ return { binding, account, grant: grant?.accountId === account?.id ? grant : null };
195
259
  });
196
260
  }
197
261
 
@@ -219,6 +283,286 @@ export class AuthSwitchAuthorityDatabase {
219
283
  return stored ? AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(stored) : null;
220
284
  }
221
285
 
286
+ public async readOperation(id: string): Promise<IStoredAuthorityOperation | null> {
287
+ this.db();
288
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id });
289
+ return stored ? storedOperation(stored) : null;
290
+ }
291
+
292
+ public async operationsPage(after: string | null, limit = 128): Promise<{ operations: IStoredAuthorityDeviceOperation[]; nextCursor: string | null }> {
293
+ this.db();
294
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid operation page size.');
295
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStored({
296
+ filter: after
297
+ ? { kind: { $in: ['add_openai', 'reauth_openai'] }, id: { $gt: after } }
298
+ : { kind: { $in: ['add_openai', 'reauth_openai'] } },
299
+ sort: { id: 1 }, limit: limit + 1,
300
+ });
301
+ const operations = stored.slice(0, limit).map(storedOperation)
302
+ .filter((item): item is IStoredAuthorityDeviceOperation => item.kind !== 'preuse_openai');
303
+ return { operations, nextCursor: stored.length > limit ? operations.at(-1)!.id : null };
304
+ }
305
+
306
+ public async operationsByState(state: IStoredAuthorityDeviceOperation['state'], after: string | null,
307
+ limit = 128): Promise<IStoredAuthorityDeviceOperation[]> {
308
+ this.db();
309
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid operation page size.');
310
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStored({
311
+ filter: after ? { state, id: { $gt: after } } : { state }, sort: { id: 1 }, limit,
312
+ });
313
+ return stored.map(storedOperation)
314
+ .filter((item): item is IStoredAuthorityDeviceOperation => item.kind !== 'preuse_openai');
315
+ }
316
+
317
+ public async preuseOperationsByState(state: IStoredAuthorityPreuseOperation['state'], after: string | null,
318
+ limit = 128): Promise<IStoredAuthorityPreuseOperation[]> {
319
+ this.db();
320
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid operation page size.');
321
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStored({
322
+ filter: after ? { kind: 'preuse_openai', state, id: { $gt: after } } : { kind: 'preuse_openai', state },
323
+ sort: { id: 1 }, limit,
324
+ });
325
+ return stored.map(storedOperation)
326
+ .filter((item): item is IStoredAuthorityPreuseOperation => item.kind === 'preuse_openai');
327
+ }
328
+
329
+ /** Insert-or-read admission for one exact device-login intent. Existing receipts are never replayed. */
330
+ public async createOperation(intent: TStoredAuthorityDeviceOperationAdmission): Promise<{
331
+ operation: IStoredAuthorityDeviceOperation; created: boolean;
332
+ }> {
333
+ this.db();
334
+ assertStoredAuthorityOperation({ ...intent,
335
+ expectedGrantGeneration: intent.kind === 'reauth_openai' ? 1 : null });
336
+ const sameRequest = (current: IStoredAuthorityOperation): current is IStoredAuthorityDeviceOperation =>
337
+ current.kind !== 'preuse_openai' && current.kind === intent.kind
338
+ && current.accountId === intent.accountId && current.grantId === intent.grantId
339
+ && current.purpose === intent.purpose;
340
+ try {
341
+ return await this.transaction(async session => {
342
+ const existingStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne(
343
+ { id: intent.id }, { session });
344
+ if (existingStored) {
345
+ const existing = storedOperation(existingStored);
346
+ if (!sameRequest(existing)) throw new Error('Account operation ID belongs to a different request.');
347
+ return { operation: existing, created: false };
348
+ }
349
+ let expectedGrantGeneration: number | null = null;
350
+ if (intent.kind === 'reauth_openai') {
351
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne(
352
+ { id: intent.accountId! }, { session });
353
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne(
354
+ { id: intent.grantId! }, { session });
355
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
356
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
357
+ if (!account || account.removed || account.providerId !== 'openai'
358
+ || account.primaryGrantId !== intent.grantId || !grant || grant.accountId !== intent.accountId
359
+ || grant.providerId !== 'openai' || grant.purpose !== 'openai_managed'
360
+ || grant.owner !== 'daemon' || ['removed', 'exchange_may_have_been_sent'].includes(grant.state)) {
361
+ throw new Error('Selected OpenAI login is unavailable for reauthentication.');
362
+ }
363
+ expectedGrantGeneration = grant.grantGeneration;
364
+ }
365
+ const operation: IStoredAuthorityDeviceOperation = { ...intent, expectedGrantGeneration };
366
+ assertStoredAuthorityOperation(operation);
367
+ const result = await AuthSwitchAuthorityOperationModel.exact.insert(operation, { session });
368
+ if (result.status !== 'inserted') throw new Error('Account operation ID already exists.');
369
+ return { operation, created: true };
370
+ });
371
+ } catch (error) {
372
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
373
+ const stored = await this.readOperation(intent.id);
374
+ if (stored && sameRequest(stored)) {
375
+ return { operation: stored, created: stored.updateId === intent.updateId };
376
+ }
377
+ throw new Error('Account operation creation outcome is unknown; no provider login was started.');
378
+ }
379
+ }
380
+
381
+ /** Revalidate the durable owner and exact reauthentication generation after mutable runtime preparation. */
382
+ public async confirmOperationMayStart(operationId: string): Promise<IStoredAuthorityDeviceOperation> {
383
+ return this.transaction(async session => {
384
+ const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne(
385
+ { id: operationId }, { session });
386
+ if (!operationStored) throw new Error('Account operation was not found.');
387
+ const operation = storedOperation(operationStored);
388
+ if (operation.kind === 'preuse_openai' || operation.state !== 'starting') {
389
+ throw new Error('Account operation is no longer starting.');
390
+ }
391
+ if (operation.kind === 'reauth_openai') {
392
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne(
393
+ { id: operation.accountId! }, { session });
394
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne(
395
+ { id: operation.grantId! }, { session });
396
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
397
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
398
+ if (!account || account.removed || account.providerId !== 'openai'
399
+ || account.primaryGrantId !== operation.grantId || !grant || grant.accountId !== operation.accountId
400
+ || grant.providerId !== 'openai' || grant.purpose !== 'openai_managed'
401
+ || grant.owner !== 'daemon' || ['removed', 'exchange_may_have_been_sent'].includes(grant.state)
402
+ || grant.grantGeneration !== operation.expectedGrantGeneration) {
403
+ throw new Error('Selected OpenAI login changed before reauthentication could start.');
404
+ }
405
+ }
406
+ return operation;
407
+ });
408
+ }
409
+
410
+ public async changeOperation(updateId: string, operationId: string,
411
+ change: (operation: IStoredAuthorityDeviceOperation) => IStoredAuthorityDeviceOperation): Promise<IStoredAuthorityDeviceOperation> {
412
+ this.db();
413
+ try {
414
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId });
415
+ if (!stored) throw new Error('Account operation was not found.');
416
+ const current = storedOperation(stored);
417
+ if (current.kind === 'preuse_openai') throw new Error('Account operation is not a device login.');
418
+ const draft = change(structuredClone(current));
419
+ const allowed: Record<IStoredAuthorityDeviceOperation['state'], IStoredAuthorityDeviceOperation['state'][]> = {
420
+ starting: ['pending', 'failed', 'cancelled', 'interrupted'],
421
+ pending: ['committing', 'failed', 'cancelled', 'interrupted'],
422
+ committing: ['complete', 'outcome_unknown'],
423
+ complete: [], failed: [], cancelled: [], interrupted: [], outcome_unknown: [],
424
+ };
425
+ if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
426
+ || draft.accountId !== current.accountId || draft.grantId !== current.grantId
427
+ || draft.expectedGrantGeneration !== current.expectedGrantGeneration
428
+ || draft.createdAt !== current.createdAt || draft.updateId !== updateId
429
+ || draft.revision !== current.revision + 1 || !allowed[current.state].includes(draft.state)) {
430
+ throw new Error('Account operation transition is invalid.');
431
+ }
432
+ assertStoredAuthorityOperation(draft);
433
+ const result = await AuthSwitchAuthorityOperationModel.exact.transition({
434
+ current: stored, change: model => Object.assign(model, draft),
435
+ });
436
+ if (result.status !== 'transitioned') throw new Error('Account operation changed concurrently.');
437
+ return draft;
438
+ } catch (error) {
439
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
440
+ const stored = await this.readOperation(operationId);
441
+ if (stored?.kind !== 'preuse_openai' && stored?.updateId === updateId) return stored;
442
+ throw new Error('Account operation transition outcome is unknown.');
443
+ }
444
+ }
445
+
446
+ /** Insert-or-read idempotency for one exact preuse request. Existing records are never resumed here. */
447
+ public async createPreuseOperation(operation: IStoredAuthorityPreuseOperation): Promise<{
448
+ operation: IStoredAuthorityPreuseOperation; created: boolean;
449
+ }> {
450
+ this.db();
451
+ assertStoredAuthorityOperation(operation);
452
+ const sameRequest = (current: IStoredAuthorityOperation): current is IStoredAuthorityPreuseOperation =>
453
+ current.kind === 'preuse_openai' && current.accountId === operation.accountId
454
+ && current.grantId === operation.grantId && current.purpose === operation.purpose
455
+ && current.requestHash === operation.requestHash && current.requestedModel === operation.requestedModel;
456
+ try {
457
+ return await this.transaction(async session => {
458
+ const existingStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operation.id }, { session });
459
+ if (existingStored) {
460
+ const existing = storedOperation(existingStored);
461
+ if (!sameRequest(existing)) throw new Error('Preuse operation ID belongs to a different request.');
462
+ return { operation: existing, created: false };
463
+ }
464
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: operation.accountId }, { session });
465
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: operation.grantId }, { session });
466
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
467
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
468
+ if (!account || account.removed || account.providerId !== 'openai' || account.primaryGrantId !== operation.grantId
469
+ || !grant || grant.accountId !== operation.accountId || grant.providerId !== 'openai'
470
+ || grant.purpose !== 'openai_managed' || grant.owner !== 'daemon'
471
+ || !['ready', 'retry_wait'].includes(grant.state)
472
+ || grant.authorizationGeneration !== operation.authorizationGeneration) {
473
+ throw new Error('Selected OpenAI login is unavailable for preuse.');
474
+ }
475
+ const inserted = await AuthSwitchAuthorityOperationModel.exact.insert(operation, { session });
476
+ if (inserted.status !== 'inserted') throw new Error('Preuse operation ID already exists.');
477
+ return { operation, created: true };
478
+ });
479
+ } catch (error) {
480
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
481
+ const stored = await this.readOperation(operation.id);
482
+ if (stored && sameRequest(stored) && stored.updateId === operation.updateId) {
483
+ return { operation: stored, created: true };
484
+ }
485
+ throw new Error('Preuse operation creation outcome is unknown; no provider request was started.');
486
+ }
487
+ }
488
+
489
+ /** The committed marker precedes the only quota-consuming provider call. */
490
+ public async markPreuseRequestMayHaveStarted(operationId: string, updateId: string, model: string,
491
+ grantGeneration: number, now: string): Promise<IStoredAuthorityPreuseOperation> {
492
+ try {
493
+ return await this.transaction(async session => {
494
+ const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId }, { session });
495
+ if (!operationStored) throw new Error('Preuse operation was not found.');
496
+ const current = storedOperation(operationStored);
497
+ if (current.kind !== 'preuse_openai' || current.state !== 'preparing') {
498
+ throw new Error('Preuse operation is no longer preparing.');
499
+ }
500
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: current.accountId }, { session });
501
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: current.grantId }, { session });
502
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
503
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
504
+ if (!account || account.removed || account.providerId !== 'openai' || account.primaryGrantId !== current.grantId
505
+ || !grant || grant.accountId !== current.accountId || grant.providerId !== 'openai'
506
+ || grant.purpose !== current.purpose || grant.owner !== 'daemon'
507
+ || !['ready', 'retry_wait'].includes(grant.state)
508
+ || grant.authorizationGeneration !== current.authorizationGeneration
509
+ || grant.grantGeneration !== grantGeneration) {
510
+ throw new Error('Preuse login changed before its request marker was committed.');
511
+ }
512
+ const draft: IStoredAuthorityPreuseOperation = { ...current, model, grantGeneration,
513
+ state: 'request_may_have_started', updatedAt: now, revision: current.revision + 1, updateId };
514
+ assertStoredAuthorityOperation(draft);
515
+ const transitioned = await AuthSwitchAuthorityOperationModel.exact.transition({
516
+ current: operationStored, change: stored => Object.assign(stored, draft),
517
+ }, { session });
518
+ if (transitioned.status !== 'transitioned') throw new Error('Preuse operation changed concurrently.');
519
+ return draft;
520
+ });
521
+ } catch (error) {
522
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
523
+ const stored = await this.readOperation(operationId);
524
+ if (stored?.kind === 'preuse_openai' && stored.updateId === updateId
525
+ && stored.state === 'request_may_have_started') return stored;
526
+ throw new Error('Preuse request-marker outcome is unknown; no provider request was started.');
527
+ }
528
+ }
529
+
530
+ public async changePreuseOperation(updateId: string, operationId: string,
531
+ change: (operation: IStoredAuthorityPreuseOperation) => IStoredAuthorityPreuseOperation): Promise<IStoredAuthorityPreuseOperation> {
532
+ this.db();
533
+ try {
534
+ const stored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId });
535
+ if (!stored) throw new Error('Preuse operation was not found.');
536
+ const current = storedOperation(stored);
537
+ if (current.kind !== 'preuse_openai') throw new Error('Operation is not a preuse request.');
538
+ const draft = change(structuredClone(current));
539
+ const allowed: Record<IStoredAuthorityPreuseOperation['state'], IStoredAuthorityPreuseOperation['state'][]> = {
540
+ preparing: ['request_may_have_started', 'not_sent'],
541
+ request_may_have_started: ['complete', 'outcome_unknown'],
542
+ complete: [], not_sent: [], outcome_unknown: [],
543
+ };
544
+ if (draft.id !== current.id || draft.kind !== current.kind || draft.purpose !== current.purpose
545
+ || draft.accountId !== current.accountId || draft.grantId !== current.grantId
546
+ || draft.requestHash !== current.requestHash || draft.requestedModel !== current.requestedModel
547
+ || draft.authorizationGeneration !== current.authorizationGeneration
548
+ || draft.createdAt !== current.createdAt || draft.updateId !== updateId
549
+ || draft.revision !== current.revision + 1 || !allowed[current.state].includes(draft.state)) {
550
+ throw new Error('Preuse operation transition is invalid.');
551
+ }
552
+ assertStoredAuthorityOperation(draft);
553
+ const result = await AuthSwitchAuthorityOperationModel.exact.transition({
554
+ current: stored, change: model => Object.assign(model, draft),
555
+ });
556
+ if (result.status !== 'transitioned') throw new Error('Preuse operation changed concurrently.');
557
+ return draft;
558
+ } catch (error) {
559
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
560
+ const stored = await this.readOperation(operationId);
561
+ if (stored?.kind === 'preuse_openai' && stored.updateId === updateId) return stored;
562
+ throw new Error('Preuse operation transition outcome is unknown.');
563
+ }
564
+ }
565
+
222
566
  public async migrationLedgerByStatus(status: IStoredAuthorityMigrationLedger['status'],
223
567
  after: string | null, limit = 128): Promise<IStoredAuthorityMigrationLedger[]> {
224
568
  this.db();
@@ -241,10 +585,153 @@ export class AuthSwitchAuthorityDatabase {
241
585
  });
242
586
  }
243
587
 
244
- public async page(accountAfter: string | null, bindingAfter: string | null, limit: number): Promise<{
245
- meta: IStoredAuthorityMeta; accounts: Array<{ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant | null }>;
588
+ public async readAccountAndGrant(accountId: string, grantId: string): Promise<{
589
+ account: IStoredAuthorityAccount | null; grant: IStoredAuthorityGrant | null;
590
+ }> {
591
+ return this.transaction(async session => {
592
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
593
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
594
+ const account = accountStored ? AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored) : null;
595
+ const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
596
+ return { account, grant: grant?.accountId === accountId ? grant : null };
597
+ });
598
+ }
599
+
600
+ /** Epoch, account and exact login are read from one authority snapshot. */
601
+ public async readUsageContext(accountId: string, loginId: string): Promise<IAuthSwitchUsageContext | null> {
602
+ return (await this.readUsageAccessView(accountId, loginId))?.context ?? null;
603
+ }
604
+
605
+ /** Backend-only view used to validate a usage token before and after unsealing. */
606
+ public async readUsageAccessView(accountId: string, loginId: string): Promise<{
607
+ context: IAuthSwitchUsageContext; account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant;
608
+ } | null> {
609
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
610
+ throw new Error('Invalid authority usage identity.');
611
+ }
612
+ return this.transaction(async session => {
613
+ const meta = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
614
+ const account = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
615
+ const grant = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: loginId }, { session });
616
+ if (!meta || !account || !grant || grant.accountId !== accountId) return null;
617
+ const accountRecord = AuthSwitchAuthorityAccountModel.exact.toPersisted(account);
618
+ const grantRecord = AuthSwitchAuthorityGrantModel.exact.toPersisted(grant);
619
+ return { context: usageContext(AuthSwitchAuthorityMetaModel.exact.toPersisted(meta), accountRecord, grantRecord),
620
+ account: accountRecord, grant: grantRecord };
621
+ });
622
+ }
623
+
624
+ /** Coherent native source ownership, including the home handoff fence. */
625
+ public async readClaudeNativeUsageView(homeId: string, accountId: string, loginId: string): Promise<{
626
+ context: IAuthSwitchUsageContext; account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant;
627
+ home: IStoredAuthorityClaudeHome;
628
+ } | null> {
629
+ if (![homeId, accountId, loginId].every(id => /^[a-f0-9]{64}$/.test(id))) {
630
+ throw new Error('Invalid Claude native usage selector.');
631
+ }
632
+ return this.transaction(async session => {
633
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
634
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
635
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: loginId }, { session });
636
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
637
+ if (!metaStored || !accountStored || !grantStored || !homeStored
638
+ || grantStored.accountId !== accountId || accountStored.removed
639
+ || accountStored.providerId !== 'anthropic' || grantStored.providerId !== 'anthropic'
640
+ || grantStored.purpose !== 'claude_host_native' || grantStored.owner !== 'claude_native'
641
+ || grantStored.state !== 'native' || homeStored.status !== 'ready'
642
+ || homeStored.pendingOperationId !== null || homeStored.activeAccountId !== accountId
643
+ || homeStored.activeGrantId !== loginId) return null;
644
+ const account = AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored);
645
+ const grant = AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored);
646
+ return { context: usageContext(AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored), account, grant),
647
+ account, grant, home: AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(homeStored) };
648
+ });
649
+ }
650
+
651
+ public async readUsage(accountId: string, loginId: string): Promise<IAuthSwitchStoredUsage | null> {
652
+ this.db();
653
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
654
+ throw new Error('Invalid authority usage identity.');
655
+ }
656
+ const stored = await AuthSwitchAuthorityUsageModel.exact.findStoredOne({ id: authorityUsageId(accountId, loginId) });
657
+ if (!stored) return null;
658
+ const { id: _id, accountId: _accountId, loginId: _loginId, ...record } =
659
+ AuthSwitchAuthorityUsageModel.exact.toPersisted(stored);
660
+ return record;
661
+ }
662
+
663
+ /** Publish one credential-free usage result only if its authority and predecessor still match. */
664
+ public async publishUsageIfCurrent(context: IAuthSwitchUsageContext,
665
+ previousUpdateId: string | null, next: IAuthSwitchStoredUsage): Promise<boolean> {
666
+ const id = authorityUsageId(context.accountId, context.loginId);
667
+ const draft: IStoredAuthorityUsage = { id, accountId: context.accountId,
668
+ loginId: context.loginId, ...next };
669
+ assertStoredAuthorityUsage(draft);
670
+ if (!sameUsageSource(context, next.context)
671
+ || (previousUpdateId !== null && !/^[a-f0-9-]{36}$/.test(previousUpdateId))) {
672
+ throw new Error('Authority usage publication has an invalid context or predecessor.');
673
+ }
674
+ try {
675
+ return await this.transaction(async session => {
676
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
677
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: context.accountId }, { session });
678
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: context.loginId }, { session });
679
+ if (!metaStored || !accountStored || !grantStored || grantStored.accountId !== context.accountId) return false;
680
+ const actual = usageContext(AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
681
+ AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored),
682
+ AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored));
683
+ if (!sameUsageContext(context, actual) || actual.removed) return false;
684
+ if (actual.providerId === 'openai') {
685
+ if (actual.owner !== 'daemon' || actual.purpose !== 'openai_managed'
686
+ || context.nativeSource !== null || !['ready', 'retry_wait'].includes(actual.grantState)) return false;
687
+ } else if (actual.providerId === 'anthropic' && actual.purpose === 'claude_host_native') {
688
+ if (actual.owner === 'daemon') {
689
+ if (context.nativeSource !== null || !['ready', 'retry_wait'].includes(actual.grantState)) return false;
690
+ } else if (actual.owner === 'claude_native' && actual.grantState === 'native'
691
+ && context.nativeSource !== null) {
692
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({
693
+ id: context.nativeSource.homeId }, { session });
694
+ if (!homeStored || homeStored.status !== 'ready' || homeStored.pendingOperationId !== null
695
+ || homeStored.activeAccountId !== context.accountId
696
+ || homeStored.activeGrantId !== context.loginId) return false;
697
+ } else return false;
698
+ } else return false;
699
+ const current = await AuthSwitchAuthorityUsageModel.exact.findStoredOne({ id }, { session });
700
+ if (current) {
701
+ const prior = AuthSwitchAuthorityUsageModel.exact.toPersisted(current);
702
+ if (sameUsageSource(prior.context, context)
703
+ ? prior.updateId !== previousUpdateId : previousUpdateId !== null) return false;
704
+ const changed = await AuthSwitchAuthorityUsageModel.exact.transition({ current,
705
+ change: model => Object.assign(model, draft) }, { session });
706
+ if (changed.status !== 'transitioned') return false;
707
+ } else {
708
+ if (previousUpdateId !== null) return false;
709
+ const inserted = await AuthSwitchAuthorityUsageModel.exact.insert(draft, { session });
710
+ if (inserted.status !== 'inserted') return false;
711
+ }
712
+ await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
713
+ next.updateId, 'account', context.accountId);
714
+ return true;
715
+ });
716
+ } catch (error) {
717
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError)) throw error;
718
+ if (error.code === 'unique_conflict') return false;
719
+ if (error.code !== 'ambiguous_write') throw error;
720
+ const [stored, event] = await Promise.all([
721
+ AuthSwitchAuthorityUsageModel.exact.findStoredOne({ id }),
722
+ AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: next.updateId }),
723
+ ]);
724
+ if (stored?.updateId === next.updateId && event?.epoch === context.epoch
725
+ && event.kind === 'account' && event.accountId === context.accountId
726
+ && sameUsageSource(stored.context, context)) return true;
727
+ throw new Error('Authority usage publication outcome is unknown; the provider query must not be replayed.');
728
+ }
729
+ }
730
+
731
+ public async page(accountAfter: string | null, grantAfter: string | null, bindingAfter: string | null, limit: number): Promise<{
732
+ meta: IStoredAuthorityMeta; accounts: IStoredAuthorityAccount[]; grants: IStoredAuthorityGrant[];
246
733
  bindings: IStoredAuthorityBinding[];
247
- nextAccountCursor: string | null; nextBindingCursor: string | null;
734
+ nextAccountCursor: string | null; nextGrantCursor: string | null; nextBindingCursor: string | null;
248
735
  }> {
249
736
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid authority page size.');
250
737
  return this.transaction(async session => {
@@ -254,28 +741,128 @@ export class AuthSwitchAuthorityDatabase {
254
741
  filter: accountAfter ? { id: { $gt: accountAfter } } : {},
255
742
  sort: { id: 1 }, limit: limit + 1, session,
256
743
  });
744
+ const grants = await AuthSwitchAuthorityGrantModel.exact.findStored({
745
+ filter: grantAfter ? { id: { $gt: grantAfter } } : {},
746
+ sort: { id: 1 }, limit: limit + 1, session,
747
+ });
257
748
  const bindings = await AuthSwitchAuthorityBindingModel.exact.findStored({
258
749
  filter: bindingAfter ? { id: { $gt: bindingAfter } } : {},
259
750
  sort: { id: 1 }, limit: limit + 1, session,
260
751
  });
261
752
  const accountPage = accounts.slice(0, limit).map(item => AuthSwitchAuthorityAccountModel.exact.toPersisted(item));
753
+ const grantPage = grants.slice(0, limit).map(item => AuthSwitchAuthorityGrantModel.exact.toPersisted(item));
262
754
  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
755
  return {
271
756
  meta: AuthSwitchAuthorityMetaModel.exact.toPersisted(meta),
272
- accounts: withGrants, bindings: bindingPage,
757
+ accounts: accountPage, grants: grantPage, bindings: bindingPage,
273
758
  nextAccountCursor: accounts.length > limit ? accountPage.at(-1)!.id : null,
759
+ nextGrantCursor: grants.length > limit ? grantPage.at(-1)!.id : null,
274
760
  nextBindingCursor: bindings.length > limit ? bindingPage.at(-1)!.id : null,
275
761
  };
276
762
  });
277
763
  }
278
764
 
765
+ /**
766
+ * One bounded, repeatable-read diagnostic page. Its metadata revision is authority state,
767
+ * not a complete change clock for every native-home record.
768
+ */
769
+ public async doctorPage(cursors: {
770
+ accountAfter: string | null;
771
+ loginAfter: string | null;
772
+ codexHomeAfter: string | null;
773
+ claudeHomeAfter: string | null;
774
+ claudeHandoffAfter: string | null;
775
+ nativeHandoffAfter: string | null;
776
+ migrationAfter: string | null;
777
+ }, limit: number): Promise<{
778
+ observedAt: string;
779
+ meta: IStoredAuthorityMeta;
780
+ accounts: IStoredAuthorityAccount[];
781
+ logins: IStoredAuthorityGrant[];
782
+ codexHomes: IStoredAuthorityCodexHome[];
783
+ claudeHomes: IStoredAuthorityClaudeHome[];
784
+ claudeHandoffs: IStoredAuthorityClaudeHandoff[];
785
+ nativeHandoffs: IStoredAuthorityHandoff[];
786
+ migrations: IStoredAuthorityMigrationLedger[];
787
+ nextAccountCursor: string | null;
788
+ nextLoginCursor: string | null;
789
+ nextCodexHomeCursor: string | null;
790
+ nextClaudeHomeCursor: string | null;
791
+ nextClaudeHandoffCursor: string | null;
792
+ nextNativeHandoffCursor: string | null;
793
+ nextMigrationCursor: string | null;
794
+ }> {
795
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 128) {
796
+ throw new Error('Invalid authority doctor page size.');
797
+ }
798
+ const hashCursor = (value: string | null): boolean => value === null || /^[a-f0-9]{64}$/.test(value);
799
+ if (![cursors.accountAfter, cursors.loginAfter, cursors.codexHomeAfter, cursors.claudeHomeAfter,
800
+ cursors.nativeHandoffAfter, cursors.migrationAfter].every(hashCursor)
801
+ || (cursors.claudeHandoffAfter !== null && !/^[a-f0-9-]{36}$/.test(cursors.claudeHandoffAfter))) {
802
+ throw new Error('Invalid authority doctor cursor.');
803
+ }
804
+ return this.transaction(async session => {
805
+ const meta = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
806
+ if (!meta) throw new Error('Authswitch authority metadata is missing.');
807
+ const accounts = await AuthSwitchAuthorityAccountModel.exact.findStored({
808
+ filter: cursors.accountAfter ? { id: { $gt: cursors.accountAfter } } : {},
809
+ sort: { id: 1 }, limit: limit + 1, session,
810
+ });
811
+ const logins = await AuthSwitchAuthorityGrantModel.exact.findStored({
812
+ filter: cursors.loginAfter ? { id: { $gt: cursors.loginAfter } } : {},
813
+ sort: { id: 1 }, limit: limit + 1, session,
814
+ });
815
+ const codexHomes = await AuthSwitchAuthorityCodexHomeModel.exact.findStored({
816
+ filter: cursors.codexHomeAfter ? { id: { $gt: cursors.codexHomeAfter } } : {},
817
+ sort: { id: 1 }, limit: limit + 1, session,
818
+ });
819
+ const claudeHomes = await AuthSwitchAuthorityClaudeHomeModel.exact.findStored({
820
+ filter: cursors.claudeHomeAfter ? { id: { $gt: cursors.claudeHomeAfter } } : {},
821
+ sort: { id: 1 }, limit: limit + 1, session,
822
+ });
823
+ const claudeHandoffs = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStored({
824
+ filter: cursors.claudeHandoffAfter ? { id: { $gt: cursors.claudeHandoffAfter } } : {},
825
+ sort: { id: 1 }, limit: limit + 1, session,
826
+ });
827
+ const nativeHandoffs = await AuthSwitchAuthorityHandoffModel.exact.findStored({
828
+ filter: cursors.nativeHandoffAfter ? { id: { $gt: cursors.nativeHandoffAfter } } : {},
829
+ sort: { id: 1 }, limit: limit + 1, session,
830
+ });
831
+ const migrations = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
832
+ filter: cursors.migrationAfter ? { id: { $gt: cursors.migrationAfter } } : {},
833
+ sort: { id: 1 }, limit: limit + 1, session,
834
+ });
835
+ const accountPage = accounts.slice(0, limit).map(item => AuthSwitchAuthorityAccountModel.exact.toPersisted(item));
836
+ const loginPage = logins.slice(0, limit).map(item => AuthSwitchAuthorityGrantModel.exact.toPersisted(item));
837
+ const codexHomePage = codexHomes.slice(0, limit).map(item => AuthSwitchAuthorityCodexHomeModel.exact.toPersisted(item));
838
+ const claudeHomePage = claudeHomes.slice(0, limit).map(item => AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(item));
839
+ const claudeHandoffPage = claudeHandoffs.slice(0, limit)
840
+ .map(item => AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(item));
841
+ const nativeHandoffPage = nativeHandoffs.slice(0, limit)
842
+ .map(item => AuthSwitchAuthorityHandoffModel.exact.toPersisted(item));
843
+ const migrationPage = migrations.slice(0, limit)
844
+ .map(item => AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(item));
845
+ return {
846
+ observedAt: new Date().toISOString(),
847
+ meta: AuthSwitchAuthorityMetaModel.exact.toPersisted(meta),
848
+ accounts: accountPage,
849
+ logins: loginPage,
850
+ codexHomes: codexHomePage,
851
+ claudeHomes: claudeHomePage,
852
+ claudeHandoffs: claudeHandoffPage,
853
+ nativeHandoffs: nativeHandoffPage,
854
+ migrations: migrationPage,
855
+ nextAccountCursor: accounts.length > limit ? accountPage.at(-1)!.id : null,
856
+ nextLoginCursor: logins.length > limit ? loginPage.at(-1)!.id : null,
857
+ nextCodexHomeCursor: codexHomes.length > limit ? codexHomePage.at(-1)!.id : null,
858
+ nextClaudeHomeCursor: claudeHomes.length > limit ? claudeHomePage.at(-1)!.id : null,
859
+ nextClaudeHandoffCursor: claudeHandoffs.length > limit ? claudeHandoffPage.at(-1)!.id : null,
860
+ nextNativeHandoffCursor: nativeHandoffs.length > limit ? nativeHandoffPage.at(-1)!.id : null,
861
+ nextMigrationCursor: migrations.length > limit ? migrationPage.at(-1)!.id : null,
862
+ };
863
+ });
864
+ }
865
+
279
866
  public async grantsByState(state: IStoredAuthorityGrant['state'], after: string | null, limit = 128): Promise<IStoredAuthorityGrant[]> {
280
867
  this.db();
281
868
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid authority scan size.');
@@ -338,18 +925,27 @@ export class AuthSwitchAuthorityDatabase {
338
925
  }
339
926
 
340
927
  private async persistGrant(session: TSession, current: Awaited<ReturnType<typeof AuthSwitchAuthorityGrantModel.exact.findStoredOne>>,
341
- draft: IStoredAuthorityGrant): Promise<void> {
928
+ draft: IStoredAuthorityGrant, allowClaudeHandoff = false): Promise<void> {
342
929
  assertStoredAuthorityGrant(draft);
343
930
  if (current) {
344
931
  const prior = AuthSwitchAuthorityGrantModel.exact.toPersisted(current);
345
932
  if (draft.id !== prior.id || draft.accountId !== prior.accountId
346
933
  || draft.providerId !== prior.providerId || draft.purpose !== prior.purpose
347
934
  || draft.audience !== prior.audience) throw new Error('Grant identity cannot change.');
935
+ if (!allowClaudeHandoff && draft.purpose === 'claude_host_native'
936
+ && (['handoff_pending', 'handoff_quarantined'].includes(draft.state)
937
+ || ['handoff_pending', 'handoff_quarantined'].includes(prior.state)
938
+ || draft.owner !== prior.owner)) {
939
+ throw new Error('Claude native ownership requires the two-grant handoff transaction.');
940
+ }
348
941
  const result = await AuthSwitchAuthorityGrantModel.exact.transition({
349
942
  current, change: model => Object.assign(model, draft),
350
943
  }, { session });
351
944
  if (result.status !== 'transitioned') throw new Error('Grant changed concurrently.');
352
945
  } else {
946
+ if (!allowClaudeHandoff && ['handoff_pending', 'handoff_quarantined'].includes(draft.state)) {
947
+ throw new Error('Claude handoff cannot begin with an unassigned grant.');
948
+ }
353
949
  const result = await AuthSwitchAuthorityGrantModel.exact.insert(draft, { session });
354
950
  if (result.status !== 'inserted') throw new Error('Grant was added concurrently.');
355
951
  }
@@ -388,7 +984,7 @@ export class AuthSwitchAuthorityDatabase {
388
984
  return { meta, ...draft };
389
985
  });
390
986
  } catch (error) {
391
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
987
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
392
988
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
393
989
  const { account, grant } = await this.readAccountAndPrimaryGrant(accountId);
394
990
  if (event && account?.updateId === updateId && grant?.updateId === updateId) {
@@ -398,6 +994,75 @@ export class AuthSwitchAuthorityDatabase {
398
994
  }
399
995
  }
400
996
 
997
+ /** Device-login result, account and grant are one commit; a lost RPC response remains discoverable by operation ID. */
998
+ public async commitLoginOperation(updateId: string, operationId: string, accountId: string, grantId: string,
999
+ change: (account: IStoredAuthorityAccount | null, grant: IStoredAuthorityGrant | null) => {
1000
+ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant;
1001
+ }): Promise<{ account: IStoredAuthorityAccount; grant: IStoredAuthorityGrant; operation: IStoredAuthorityDeviceOperation }> {
1002
+ try {
1003
+ return await this.transaction(async session => {
1004
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1005
+ const operationStored = await AuthSwitchAuthorityOperationModel.exact.findStoredOne({ id: operationId }, { session });
1006
+ if (!metaStored || !operationStored) throw new Error('Device operation is unavailable.');
1007
+ const operation = storedOperation(operationStored);
1008
+ if (operation.kind === 'preuse_openai' || operation.state !== 'committing' || operation.commitId !== updateId
1009
+ || (operation.kind === 'reauth_openai' && (operation.accountId !== accountId || operation.grantId !== grantId))) {
1010
+ throw new Error('Device operation is not admitted for this login.');
1011
+ }
1012
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1013
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
1014
+ const oldGrant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
1015
+ if (operation.kind === 'reauth_openai' && oldGrant?.grantGeneration !== operation.expectedGrantGeneration) {
1016
+ throw new Error('Login changed while device reauthentication was pending.');
1017
+ }
1018
+ const draft = change(
1019
+ accountStored ? structuredClone(AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored)) : null,
1020
+ oldGrant ? structuredClone(oldGrant) : null);
1021
+ if (draft.account.id !== accountId || draft.grant.id !== grantId || draft.grant.accountId !== accountId
1022
+ || draft.account.primaryGrantId !== grantId || draft.account.providerId !== draft.grant.providerId
1023
+ || draft.account.updateId !== updateId || draft.grant.updateId !== updateId) {
1024
+ throw new Error('Device login account/grant change has an invalid identity.');
1025
+ }
1026
+ if (accountStored && accountStored.primaryGrantId !== grantId) {
1027
+ throw new Error('Primary grant identity cannot be replaced.');
1028
+ }
1029
+ if (grantStored && ['legacy_native', 'claude_native'].includes(grantStored.owner)
1030
+ && draft.grant.owner === 'daemon') {
1031
+ throw new Error('Native ownership requires a verified handoff.');
1032
+ }
1033
+ const completed: IStoredAuthorityDeviceOperation = {
1034
+ ...operation, state: 'complete', prompt: null,
1035
+ result: { accountId, grantId, accountRevision: draft.account.revision,
1036
+ grantGeneration: draft.grant.grantGeneration },
1037
+ error: null, updatedAt: draft.account.statusObservedAt, finishedAt: draft.account.statusObservedAt,
1038
+ revision: operation.revision + 1, updateId,
1039
+ };
1040
+ assertStoredAuthorityOperation(completed);
1041
+ await this.persistAccount(session, accountStored, draft.account);
1042
+ await this.persistGrant(session, grantStored, draft.grant);
1043
+ const transitioned = await AuthSwitchAuthorityOperationModel.exact.transition({
1044
+ current: operationStored, change: model => Object.assign(model, completed),
1045
+ }, { session });
1046
+ if (transitioned.status !== 'transitioned') throw new Error('Device operation changed concurrently.');
1047
+ await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
1048
+ updateId, 'account', accountId);
1049
+ return { ...draft, operation: completed };
1050
+ });
1051
+ } catch (error) {
1052
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1053
+ const [event, operation, pair] = await Promise.all([
1054
+ AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId }),
1055
+ this.readOperation(operationId), this.readAccountAndPrimaryGrant(accountId),
1056
+ ]);
1057
+ if (event && operation?.kind !== 'preuse_openai' && operation?.state === 'complete' && operation.updateId === updateId
1058
+ && pair.account?.updateId === updateId && pair.grant?.updateId === updateId
1059
+ && operation.result?.accountId === accountId && operation.result.grantId === grantId) {
1060
+ return { account: pair.account, grant: pair.grant, operation };
1061
+ }
1062
+ throw new Error('Device login commit outcome is unknown; inspect the durable operation before retrying.');
1063
+ }
1064
+ }
1065
+
401
1066
  /** Grant transition and public account revision share a transaction and one event. */
402
1067
  public async changeGrant(updateId: string, grantId: string,
403
1068
  change: (grant: IStoredAuthorityGrant, account: IStoredAuthorityAccount) => IStoredAuthorityGrant): Promise<{
@@ -416,6 +1081,11 @@ export class AuthSwitchAuthorityDatabase {
416
1081
  if (draft.id !== grantId || draft.accountId !== account.id || draft.updateId !== updateId) {
417
1082
  throw new Error('Grant change has an invalid identity.');
418
1083
  }
1084
+ if (grant.purpose === 'claude_host_native' && draft.state === 'exchange_may_have_been_sent'
1085
+ && grant.state !== 'exchange_may_have_been_sent'
1086
+ && !(await this.hasVerifiedClaudeGrantReceipt(account.id, grant.id, session))) {
1087
+ throw new Error('Claude refresh requires a verified import receipt.');
1088
+ }
419
1089
  const accountDraft: IStoredAuthorityAccount = { ...account, revision: account.revision + 1,
420
1090
  statusObservedAt: draft.statusObservedAt, updateId };
421
1091
  await this.persistGrant(session, grantStored, draft);
@@ -425,7 +1095,7 @@ export class AuthSwitchAuthorityDatabase {
425
1095
  return { meta, account: accountDraft, grant: draft };
426
1096
  });
427
1097
  } catch (error) {
428
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1098
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
429
1099
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
430
1100
  const grant = await this.readGrant(grantId);
431
1101
  const account = grant ? await this.readAccount(grant.accountId) : null;
@@ -463,7 +1133,7 @@ export class AuthSwitchAuthorityDatabase {
463
1133
  return { account: updatedAccount, grant: draft };
464
1134
  });
465
1135
  } catch (error) {
466
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1136
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
467
1137
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
468
1138
  const account = await this.readAccount(accountId);
469
1139
  const grant = await this.readGrant(grantId);
@@ -504,6 +1174,9 @@ export class AuthSwitchAuthorityDatabase {
504
1174
  if (grant.state === 'exchange_may_have_been_sent') {
505
1175
  throw new Error('Wait for the account refresh to finish before removing it.');
506
1176
  }
1177
+ if (['handoff_pending', 'handoff_quarantined'].includes(grant.state)) {
1178
+ throw new Error('Resolve the Claude native handoff before removing this account.');
1179
+ }
507
1180
  });
508
1181
  await scan(async (grant, stored) => {
509
1182
  if (grant.state === 'removed') return;
@@ -550,7 +1223,7 @@ export class AuthSwitchAuthorityDatabase {
550
1223
  return removed;
551
1224
  });
552
1225
  } catch (error) {
553
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1226
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
554
1227
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
555
1228
  const account = await this.readAccount(accountId);
556
1229
  if (event && account?.removed && account.updateId === updateId) return account;
@@ -576,7 +1249,7 @@ export class AuthSwitchAuthorityDatabase {
576
1249
  return { meta, account: draft };
577
1250
  });
578
1251
  } catch (error) {
579
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1252
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
580
1253
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
581
1254
  const account = await this.readAccount(accountId);
582
1255
  if (event && account?.updateId === updateId) return { meta: await this.readMeta(), account };
@@ -584,7 +1257,7 @@ export class AuthSwitchAuthorityDatabase {
584
1257
  }
585
1258
  }
586
1259
 
587
- public async changeBinding(updateId: string, bindingId: string, accountId: string,
1260
+ public async changeBinding(updateId: string, bindingId: string, accountId: string, grantId: string,
588
1261
  change: (draft: IStoredAuthorityBinding | null, account: IStoredAuthorityAccount,
589
1262
  grant: IStoredAuthorityGrant | null) => IStoredAuthorityBinding): Promise<{
590
1263
  meta: IStoredAuthorityMeta; binding: IStoredAuthorityBinding;
@@ -595,13 +1268,14 @@ export class AuthSwitchAuthorityDatabase {
595
1268
  const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
596
1269
  if (!metaStored || !accountStored) throw new Error('Account is not available for this runtime.');
597
1270
  const account = AuthSwitchAuthorityAccountModel.exact.toPersisted(accountStored);
598
- const grantStored = account.primaryGrantId
599
- ? await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: account.primaryGrantId }, { session }) : null;
1271
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
600
1272
  const grant = grantStored ? AuthSwitchAuthorityGrantModel.exact.toPersisted(grantStored) : null;
601
1273
  const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
602
1274
  const draft = change(current ? structuredClone(AuthSwitchAuthorityBindingModel.exact.toPersisted(current)) : null, account, grant);
603
1275
  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.');
1276
+ if (draft.grantId !== grantId || grant?.accountId !== accountId) {
1277
+ throw new Error('Binding login does not belong to the account.');
1278
+ }
605
1279
  if (current && (draft.runtime !== current.runtime || draft.scopeId !== current.scopeId)) {
606
1280
  throw new Error('Runtime binding identity cannot change.');
607
1281
  }
@@ -620,7 +1294,7 @@ export class AuthSwitchAuthorityDatabase {
620
1294
  return { meta, binding: draft };
621
1295
  });
622
1296
  } catch (error) {
623
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1297
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
624
1298
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
625
1299
  const binding = await this.readBinding(bindingId);
626
1300
  if (event && binding?.updateId === updateId) return { meta: await this.readMeta(), binding };
@@ -628,14 +1302,15 @@ export class AuthSwitchAuthorityDatabase {
628
1302
  }
629
1303
  }
630
1304
 
631
- /** Closing an owned runtime revokes only its exact capability/incarnation. */
632
- public async revokeBinding(updateId: string, bindingId: string, capabilityHash: string): Promise<boolean> {
1305
+ private async revokeBindingExact(updateId: string, bindingId: string, capabilityHash: string,
1306
+ externalOnly: boolean): Promise<boolean> {
633
1307
  try {
634
1308
  return await this.transaction(async session => {
635
1309
  const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
636
1310
  if (!metaStored) throw new Error('Authswitch authority metadata is missing.');
637
1311
  const current = await AuthSwitchAuthorityBindingModel.exact.findStoredOne({ id: bindingId }, { session });
638
- if (!current || current.capabilityHash !== capabilityHash) return false;
1312
+ if (!current || current.capabilityHash !== capabilityHash
1313
+ || (externalOnly && !['flex', 'opencode'].includes(current.runtime))) return false;
639
1314
  const result = await AuthSwitchAuthorityBindingModel.exact.delete({ current }, { session });
640
1315
  if (result.status !== 'deleted') throw new Error('Runtime binding changed during revocation.');
641
1316
  await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
@@ -643,13 +1318,26 @@ export class AuthSwitchAuthorityDatabase {
643
1318
  return true;
644
1319
  });
645
1320
  } catch (error) {
646
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1321
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
647
1322
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
648
- if (event && !(await this.readBinding(bindingId))) return true;
1323
+ const binding = await this.readBinding(bindingId);
1324
+ // Every bind mints a fresh random capability. An event plus absence or a different
1325
+ // capability therefore proves that this exact deletion committed before any successor.
1326
+ if (event && (!binding || binding.capabilityHash !== capabilityHash)) return true;
649
1327
  throw new Error('Authswitch binding revocation outcome is unknown.');
650
1328
  }
651
1329
  }
652
1330
 
1331
+ /** Closing an authority-owned runtime revokes its exact capability after owned cleanup. */
1332
+ public revokeBinding(updateId: string, bindingId: string, capabilityHash: string): Promise<boolean> {
1333
+ return this.revokeBindingExact(updateId, bindingId, capabilityHash, false);
1334
+ }
1335
+
1336
+ /** External callers can release only Flex/OpenCode bindings; managed Codex cleanup stays daemon-owned. */
1337
+ public releaseExternalBinding(updateId: string, bindingId: string, capabilityHash: string): Promise<boolean> {
1338
+ return this.revokeBindingExact(updateId, bindingId, capabilityHash, true);
1339
+ }
1340
+
653
1341
  /** Enrollment metadata is sealed by the caller and cannot become detached from an account. */
654
1342
  public async changeEnrollment(updateId: string, enrollmentId: string, accountId: string,
655
1343
  change: (current: IStoredAuthorityEnrollment | null) => IStoredAuthorityEnrollment): Promise<IStoredAuthorityEnrollment> {
@@ -678,7 +1366,7 @@ export class AuthSwitchAuthorityDatabase {
678
1366
  return draft;
679
1367
  });
680
1368
  } catch (error) {
681
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1369
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
682
1370
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
683
1371
  const enrollment = await this.readEnrollment(enrollmentId);
684
1372
  if (event && enrollment?.updateId === updateId) return enrollment;
@@ -760,7 +1448,7 @@ export class AuthSwitchAuthorityDatabase {
760
1448
  return draft;
761
1449
  });
762
1450
  } catch (error) {
763
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1451
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
764
1452
  const event = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: updateId });
765
1453
  const handoff = await this.readHandoff(handoffId);
766
1454
  const grant = await this.readGrant(grantId);
@@ -769,7 +1457,315 @@ export class AuthSwitchAuthorityDatabase {
769
1457
  }
770
1458
  }
771
1459
 
772
- /** Ledger transitions never silently replace an active credential or waive backup proof. */
1460
+ public async readClaudeHome(homeId: string): Promise<IStoredAuthorityClaudeHome | null> {
1461
+ this.db();
1462
+ if (!/^[a-f0-9]{64}$/.test(homeId)) throw new Error('Invalid Claude home selector.');
1463
+ const stored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId });
1464
+ return stored ? AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(stored) : null;
1465
+ }
1466
+
1467
+ public async readClaudeHandoff(operationId: string): Promise<IStoredAuthorityClaudeHandoff | null> {
1468
+ this.db();
1469
+ if (!/^[a-f0-9-]{36}$/.test(operationId)) throw new Error('Invalid Claude handoff selector.');
1470
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStoredOne({ id: operationId });
1471
+ return stored ? AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(stored) : null;
1472
+ }
1473
+
1474
+ /** A verified backend import receipt, not an account snapshot, authorizes an inactive Claude grant. */
1475
+ public async hasVerifiedClaudeGrantReceipt(accountId: string, grantId: string,
1476
+ session?: TSession): Promise<boolean> {
1477
+ this.db();
1478
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(grantId)) {
1479
+ throw new Error('Invalid Claude grant receipt selector.');
1480
+ }
1481
+ for (const status of ['verified', 'complete'] as const) {
1482
+ const stored = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
1483
+ filter: { grantId, status }, sort: { id: 1 }, limit: 128, session,
1484
+ });
1485
+ if (stored.some(item => item.accountId === accountId
1486
+ && ['authswitch_stash', 'authswitch_backup', 'claude_native'].includes(item.sourceKind))) return true;
1487
+ if (stored.length === 128) throw new Error('Claude grant has too many migration receipts to verify.');
1488
+ }
1489
+ return false;
1490
+ }
1491
+
1492
+ public async claudeHandoffsByPhase(phase: IStoredAuthorityClaudeHandoff['phase'],
1493
+ after: string | null): Promise<IStoredAuthorityClaudeHandoff[]> {
1494
+ this.db();
1495
+ if (!['reserved', 'prepared', 'quarantined'].includes(phase)
1496
+ || (after !== null && !/^[a-f0-9-]{36}$/.test(after))) throw new Error('Invalid Claude handoff scan.');
1497
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStored({
1498
+ filter: after === null ? { phase } : { phase, id: { $gt: after } },
1499
+ sort: { id: 1 }, limit: 128,
1500
+ });
1501
+ return stored.map(item => AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(item));
1502
+ }
1503
+
1504
+ public async claudeHandoffsPage(after: string | null, limit = 128): Promise<{
1505
+ handoffs: IStoredAuthorityClaudeHandoff[]; nextCursor: string | null;
1506
+ }> {
1507
+ this.db();
1508
+ if ((after !== null && !/^[a-f0-9-]{36}$/.test(after))
1509
+ || !Number.isSafeInteger(limit) || limit < 1 || limit > 256) throw new Error('Invalid Claude handoff page.');
1510
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStored({
1511
+ filter: after === null ? {} : { id: { $gt: after } }, sort: { id: 1 }, limit: limit + 1,
1512
+ });
1513
+ const handoffs = stored.slice(0, limit).map(item => AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(item));
1514
+ return { handoffs, nextCursor: stored.length > limit ? handoffs.at(-1)!.id : null };
1515
+ }
1516
+
1517
+ /** Only a verified backend import receipt may adopt a native home. No startup autodiscovery exists. */
1518
+ public async registerClaudeHomeFromReceipt(input: { homeId: string; accountId: string; grantId: string;
1519
+ receiptId: string; sourceDigest: string; accountUuid: string; organizationUuid: string;
1520
+ updateId: string; now: string }): Promise<IStoredAuthorityClaudeHome> {
1521
+ const { homeId, accountId, grantId, receiptId, sourceDigest, updateId } = input;
1522
+ for (const value of [homeId, accountId, grantId, receiptId, sourceDigest]) {
1523
+ if (!/^[a-f0-9]{64}$/.test(value)) throw new Error('Invalid verified Claude home identity.');
1524
+ }
1525
+ if (!/^[a-f0-9-]{36}$/.test(updateId)) throw new Error('Invalid Claude home update identity.');
1526
+ try {
1527
+ return await this.transaction(async session => {
1528
+ const receiptStored = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStoredOne({ id: receiptId }, { session });
1529
+ const accountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: accountId }, { session });
1530
+ const grantStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: grantId }, { session });
1531
+ const current = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
1532
+ if (!receiptStored || !['verified', 'complete'].includes(receiptStored.status)
1533
+ || receiptStored.sourceKind !== 'claude_native' || receiptStored.sourcePathHash !== homeId
1534
+ || receiptStored.sourceDigest !== sourceDigest || receiptStored.accountId !== accountId
1535
+ || receiptStored.grantId !== grantId || !accountStored || accountStored.removed
1536
+ || accountStored.providerId !== 'anthropic' || accountStored.subject !== input.accountUuid
1537
+ || accountStored.workspaceId !== input.organizationUuid || !grantStored
1538
+ || grantStored.accountId !== accountId || grantStored.purpose !== 'claude_host_native'
1539
+ || grantStored.owner !== 'claude_native' || grantStored.state !== 'native'
1540
+ || grantStored.ciphertext !== null) {
1541
+ throw new Error('Verified Claude native source mapping is unavailable.');
1542
+ }
1543
+ if (current) {
1544
+ if (current.activeAccountId !== accountId || current.activeGrantId !== grantId) {
1545
+ throw new Error('Claude native home belongs to another grant.');
1546
+ }
1547
+ return AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(current);
1548
+ }
1549
+ const home: IStoredAuthorityClaudeHome = { id: homeId, activeAccountId: accountId,
1550
+ activeGrantId: grantId, pendingOperationId: null, status: 'ready', revision: 1,
1551
+ createdAt: input.now, updateId };
1552
+ assertStoredAuthorityClaudeHome(home);
1553
+ const inserted = await AuthSwitchAuthorityClaudeHomeModel.exact.insert(home, { session });
1554
+ if (inserted.status !== 'inserted') throw new Error('Claude native home changed concurrently.');
1555
+ return home;
1556
+ });
1557
+ } catch (error) {
1558
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError)
1559
+ || !['ambiguous_write', 'unique_conflict'].includes(error.code)) throw error;
1560
+ const home = await this.readClaudeHome(homeId);
1561
+ if (home?.updateId === updateId && home.activeAccountId === accountId && home.activeGrantId === grantId) return home;
1562
+ throw new Error('Claude native home registration outcome is unknown.');
1563
+ }
1564
+ }
1565
+
1566
+ public async readClaudeHandoffContext(operationId: string): Promise<{
1567
+ handoff: IStoredAuthorityClaudeHandoff; home: IStoredAuthorityClaudeHome;
1568
+ outgoing: IStoredAuthorityGrant; incoming: IStoredAuthorityGrant;
1569
+ outgoingAccount: IStoredAuthorityAccount; incomingAccount: IStoredAuthorityAccount;
1570
+ } | null> {
1571
+ if (!/^[a-f0-9-]{36}$/.test(operationId)) throw new Error('Invalid Claude handoff selector.');
1572
+ return this.transaction(async session => {
1573
+ const stored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStoredOne({ id: operationId }, { session });
1574
+ if (!stored) return null;
1575
+ const handoff = AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(stored);
1576
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: handoff.homeId }, { session });
1577
+ const outgoingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: handoff.outgoingGrantId }, { session });
1578
+ const incomingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: handoff.incomingGrantId }, { session });
1579
+ const outgoingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: handoff.outgoingAccountId }, { session });
1580
+ const incomingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: handoff.incomingAccountId }, { session });
1581
+ if (!homeStored || !outgoingStored || !incomingStored || !outgoingAccountStored || !incomingAccountStored) {
1582
+ throw new Error('Claude handoff references a missing authority record.');
1583
+ }
1584
+ return { handoff,
1585
+ home: AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(homeStored),
1586
+ outgoing: AuthSwitchAuthorityGrantModel.exact.toPersisted(outgoingStored),
1587
+ incoming: AuthSwitchAuthorityGrantModel.exact.toPersisted(incomingStored),
1588
+ outgoingAccount: AuthSwitchAuthorityAccountModel.exact.toPersisted(outgoingAccountStored),
1589
+ incomingAccount: AuthSwitchAuthorityAccountModel.exact.toPersisted(incomingAccountStored) };
1590
+ });
1591
+ }
1592
+
1593
+ /** One phase advances both grants, home, journal and both account events atomically. */
1594
+ public async changeClaudeHandoff(input: { operationId: string; homeId: string; incomingGrantId: string;
1595
+ updateId: string; eventIds: readonly [string, string];
1596
+ change: (current: {
1597
+ home: IStoredAuthorityClaudeHome; handoff: IStoredAuthorityClaudeHandoff | null;
1598
+ outgoing: IStoredAuthorityGrant; incoming: IStoredAuthorityGrant;
1599
+ outgoingAccount: IStoredAuthorityAccount; incomingAccount: IStoredAuthorityAccount;
1600
+ }) => TClaudeHandoffDraft }): Promise<TClaudeHandoffDraft> {
1601
+ const { operationId, homeId, incomingGrantId, updateId, eventIds } = input;
1602
+ if (!/^[a-f0-9-]{36}$/.test(operationId) || !/^[a-f0-9]{64}$/.test(homeId)
1603
+ || !/^[a-f0-9]{64}$/.test(incomingGrantId)
1604
+ || ![updateId, ...eventIds].every(value => /^[a-f0-9-]{36}$/.test(value))
1605
+ || new Set([updateId, ...eventIds]).size !== 3) throw new Error('Invalid Claude handoff identity.');
1606
+ let expectedPhase: IStoredAuthorityClaudeHandoff['phase'] | null = null;
1607
+ try {
1608
+ return await this.transaction(async session => {
1609
+ const metaStored = await AuthSwitchAuthorityMetaModel.exact.findStoredOne({ id: 'authswitch-authority' }, { session });
1610
+ const homeStored = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({ id: homeId }, { session });
1611
+ const handoffStored = await AuthSwitchAuthorityClaudeHandoffModel.exact.findStoredOne({ id: operationId }, { session });
1612
+ if (!metaStored || !homeStored) throw new Error('Verified Claude home is unavailable.');
1613
+ const home = AuthSwitchAuthorityClaudeHomeModel.exact.toPersisted(homeStored);
1614
+ const handoff = handoffStored ? AuthSwitchAuthorityClaudeHandoffModel.exact.toPersisted(handoffStored) : null;
1615
+ if (home.status !== 'ready' || (handoff === null ? home.pendingOperationId !== null
1616
+ : home.pendingOperationId !== operationId || handoff.homeId !== homeId
1617
+ || handoff.incomingGrantId !== incomingGrantId)) {
1618
+ throw new Error('Claude home has another pending or quarantined operation.');
1619
+ }
1620
+ const outgoingId = handoff?.outgoingGrantId ?? home.activeGrantId;
1621
+ if (outgoingId === incomingGrantId) throw new Error('Claude home already owns the selected grant.');
1622
+ const outgoingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: outgoingId }, { session });
1623
+ const incomingStored = await AuthSwitchAuthorityGrantModel.exact.findStoredOne({ id: incomingGrantId }, { session });
1624
+ if (!outgoingStored || !incomingStored) throw new Error('Claude handoff grant is unavailable.');
1625
+ const outgoing = AuthSwitchAuthorityGrantModel.exact.toPersisted(outgoingStored);
1626
+ const incoming = AuthSwitchAuthorityGrantModel.exact.toPersisted(incomingStored);
1627
+ const incomingHome = await AuthSwitchAuthorityClaudeHomeModel.exact.findStoredOne({
1628
+ activeGrantId: incomingGrantId,
1629
+ }, { session });
1630
+ if (incomingHome && incomingHome.id !== homeId) {
1631
+ throw new Error('Claude grant is already active in another native home.');
1632
+ }
1633
+ const outgoingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: outgoing.accountId }, { session });
1634
+ const incomingAccountStored = await AuthSwitchAuthorityAccountModel.exact.findStoredOne({ id: incoming.accountId }, { session });
1635
+ if (!outgoingAccountStored || !incomingAccountStored || outgoingAccountStored.removed || incomingAccountStored.removed
1636
+ || outgoing.accountId === incoming.accountId || outgoing.providerId !== 'anthropic'
1637
+ || incoming.providerId !== 'anthropic' || outgoing.purpose !== 'claude_host_native'
1638
+ || incoming.purpose !== 'claude_host_native' || home.activeGrantId !== outgoing.id
1639
+ || home.activeAccountId !== outgoing.accountId) throw new Error('Claude handoff account identity is invalid.');
1640
+ const outgoingAccount = AuthSwitchAuthorityAccountModel.exact.toPersisted(outgoingAccountStored);
1641
+ const incomingAccount = AuthSwitchAuthorityAccountModel.exact.toPersisted(incomingAccountStored);
1642
+ const draft = input.change({ home: structuredClone(home), handoff: handoff ? structuredClone(handoff) : null,
1643
+ outgoing: structuredClone(outgoing), incoming: structuredClone(incoming),
1644
+ outgoingAccount: structuredClone(outgoingAccount), incomingAccount: structuredClone(incomingAccount) });
1645
+ expectedPhase = draft.handoff.phase;
1646
+ assertStoredAuthorityClaudeHome(draft.home);
1647
+ assertStoredAuthorityClaudeHandoff(draft.handoff);
1648
+ if (draft.home.id !== homeId || draft.home.createdAt !== home.createdAt
1649
+ || draft.home.revision !== home.revision + 1 || draft.home.updateId !== updateId
1650
+ || draft.handoff.id !== operationId || draft.handoff.homeId !== homeId
1651
+ || draft.handoff.outgoingAccountId !== outgoing.accountId || draft.handoff.outgoingGrantId !== outgoing.id
1652
+ || draft.handoff.incomingAccountId !== incoming.accountId || draft.handoff.incomingGrantId !== incoming.id
1653
+ || draft.handoff.sourceIdentity.accountUuid !== outgoingAccount.subject
1654
+ || draft.handoff.sourceIdentity.organizationUuid !== outgoingAccount.workspaceId
1655
+ || draft.handoff.targetIdentity.accountUuid !== incomingAccount.subject
1656
+ || draft.handoff.targetIdentity.organizationUuid !== incomingAccount.workspaceId
1657
+ || draft.handoff.revision !== (handoff?.revision ?? 0) + 1 || draft.handoff.updateId !== updateId
1658
+ || draft.outgoing.id !== outgoing.id || draft.incoming.id !== incoming.id
1659
+ || draft.outgoing.accountId !== outgoing.accountId || draft.incoming.accountId !== incoming.accountId
1660
+ || draft.outgoing.updateId !== updateId || draft.incoming.updateId !== updateId
1661
+ || draft.outgoing.revision !== outgoing.revision + 1 || draft.incoming.revision !== incoming.revision + 1) {
1662
+ throw new Error('Claude handoff changed an immutable identity or revision.');
1663
+ }
1664
+ if (handoff && (draft.handoff.startedAt !== handoff.startedAt
1665
+ || JSON.stringify(draft.handoff.sourceIdentity) !== JSON.stringify(handoff.sourceIdentity)
1666
+ || JSON.stringify(draft.handoff.targetIdentity) !== JSON.stringify(handoff.targetIdentity)
1667
+ || (handoff.phase !== 'reserved' && (JSON.stringify(draft.handoff.before) !== JSON.stringify(handoff.before)
1668
+ || JSON.stringify(draft.handoff.after) !== JSON.stringify(handoff.after)
1669
+ || draft.handoff.sourceAccessDigest !== handoff.sourceAccessDigest
1670
+ || draft.handoff.targetAccessDigest !== handoff.targetAccessDigest)))) {
1671
+ throw new Error('Claude handoff proof or identity changed.');
1672
+ }
1673
+ if (!handoff) {
1674
+ let verifiedIncoming = false;
1675
+ for (const status of ['verified', 'complete'] as const) {
1676
+ const receipts = await AuthSwitchAuthorityMigrationLedgerModel.exact.findStored({
1677
+ filter: { grantId: incoming.id, status }, sort: { id: 1 }, limit: 128, session,
1678
+ });
1679
+ if (receipts.length === 128) throw new Error('Claude grant receipt set is unbounded.');
1680
+ verifiedIncoming ||= receipts.some(item => item.accountId === incoming.accountId
1681
+ && ['authswitch_stash', 'authswitch_backup', 'claude_native'].includes(item.sourceKind));
1682
+ }
1683
+ if (draft.handoff.phase !== 'reserved' || home.pendingOperationId !== null
1684
+ || !verifiedIncoming
1685
+ || outgoing.state !== 'native' || outgoing.owner !== 'claude_native' || outgoing.ciphertext !== null
1686
+ || incoming.state !== 'ready' || incoming.owner !== 'daemon' || incoming.ciphertext === null
1687
+ || draft.home.activeGrantId !== outgoing.id || draft.home.pendingOperationId !== operationId
1688
+ || draft.home.status !== 'ready' || draft.outgoing.state !== 'native'
1689
+ || draft.outgoing.owner !== 'claude_native' || draft.outgoing.ciphertext !== null
1690
+ || draft.incoming.state !== 'handoff_pending' || draft.incoming.owner !== 'daemon'
1691
+ || draft.incoming.ciphertext !== incoming.ciphertext) throw new Error('Claude handoff reservation is invalid.');
1692
+ } else if (handoff.phase === 'reserved' && draft.handoff.phase === 'prepared') {
1693
+ if (outgoing.state !== 'native' || incoming.state !== 'handoff_pending'
1694
+ || draft.outgoing.state !== 'handoff_pending' || draft.outgoing.owner !== 'claude_native'
1695
+ || draft.outgoing.ciphertext !== null || draft.incoming.state !== 'handoff_pending'
1696
+ || draft.incoming.owner !== 'daemon' || draft.incoming.ciphertext !== incoming.ciphertext
1697
+ || draft.home.activeGrantId !== outgoing.id || draft.home.pendingOperationId !== operationId
1698
+ || draft.home.status !== 'ready') throw new Error('Claude handoff preparation is invalid.');
1699
+ } else if (handoff.phase === 'prepared' && draft.handoff.phase === 'committed') {
1700
+ if (outgoing.state !== 'handoff_pending' || incoming.state !== 'handoff_pending'
1701
+ || draft.outgoing.state !== 'ready' || draft.outgoing.owner !== 'daemon'
1702
+ || draft.outgoing.ciphertext !== handoff.sealedOutgoing
1703
+ || draft.incoming.state !== 'native' || draft.incoming.owner !== 'claude_native'
1704
+ || draft.incoming.ciphertext !== null || draft.home.activeAccountId !== incoming.accountId
1705
+ || draft.home.activeGrantId !== incoming.id || draft.home.pendingOperationId !== null
1706
+ || draft.home.status !== 'ready' || draft.handoff.sealedOutgoing !== null) {
1707
+ throw new Error('Claude handoff completion is invalid.');
1708
+ }
1709
+ } else if (['reserved', 'prepared'].includes(handoff.phase)
1710
+ && draft.handoff.phase === 'aborted') {
1711
+ if (incoming.state !== 'handoff_pending' || draft.incoming.state !== 'ready'
1712
+ || draft.incoming.owner !== 'daemon' || draft.incoming.ciphertext !== incoming.ciphertext
1713
+ || draft.outgoing.state !== 'native' || draft.outgoing.owner !== 'claude_native'
1714
+ || draft.outgoing.ciphertext !== null || draft.home.activeGrantId !== outgoing.id
1715
+ || draft.home.pendingOperationId !== null || draft.home.status !== 'ready'
1716
+ || draft.handoff.sealedOutgoing !== null) throw new Error('Claude handoff abort is invalid.');
1717
+ } else if (['reserved', 'prepared'].includes(handoff.phase)
1718
+ && draft.handoff.phase === 'quarantined') {
1719
+ if (draft.outgoing.state !== 'handoff_quarantined' || draft.outgoing.owner !== 'claude_native'
1720
+ || draft.incoming.state !== 'handoff_quarantined' || draft.incoming.owner !== 'daemon'
1721
+ || draft.home.activeGrantId !== outgoing.id || draft.home.pendingOperationId !== operationId
1722
+ || draft.home.status !== 'quarantined' || draft.handoff.problem === 'none') {
1723
+ throw new Error('Claude handoff quarantine is invalid.');
1724
+ }
1725
+ } else throw new Error('Claude handoff phase transition is invalid.');
1726
+ await this.persistGrant(session, outgoingStored, draft.outgoing, true);
1727
+ await this.persistGrant(session, incomingStored, draft.incoming, true);
1728
+ const homeResult = await AuthSwitchAuthorityClaudeHomeModel.exact.transition({ current: homeStored,
1729
+ change: model => Object.assign(model, draft.home) }, { session });
1730
+ if (homeResult.status !== 'transitioned') throw new Error('Claude home changed concurrently.');
1731
+ if (handoffStored) {
1732
+ const result = await AuthSwitchAuthorityClaudeHandoffModel.exact.transition({ current: handoffStored,
1733
+ change: model => Object.assign(model, draft.handoff) }, { session });
1734
+ if (result.status !== 'transitioned') throw new Error('Claude handoff changed concurrently.');
1735
+ } else {
1736
+ const result = await AuthSwitchAuthorityClaudeHandoffModel.exact.insert(draft.handoff, { session });
1737
+ if (result.status !== 'inserted') throw new Error('Claude handoff operation already exists.');
1738
+ }
1739
+ const outgoingAccountDraft = { ...outgoingAccount, revision: outgoingAccount.revision + 1,
1740
+ statusObservedAt: draft.handoff.updatedAt, updateId };
1741
+ const incomingAccountDraft = { ...incomingAccount, revision: incomingAccount.revision + 1,
1742
+ statusObservedAt: draft.handoff.updatedAt, updateId };
1743
+ await this.persistAccount(session, outgoingAccountStored, outgoingAccountDraft);
1744
+ await this.persistAccount(session, incomingAccountStored, incomingAccountDraft);
1745
+ const first = await this.appendEvent(session, AuthSwitchAuthorityMetaModel.exact.toPersisted(metaStored),
1746
+ eventIds[0], 'account', outgoing.accountId);
1747
+ await this.appendEvent(session, first, eventIds[1], 'account', incoming.accountId);
1748
+ return draft;
1749
+ });
1750
+ } catch (error) {
1751
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1752
+ const context = await this.readClaudeHandoffContext(operationId);
1753
+ const first = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: eventIds[0] });
1754
+ const second = await AuthSwitchAuthorityEventModel.exact.findStoredOne({ id: eventIds[1] });
1755
+ if (context && context.handoff.phase === expectedPhase
1756
+ && [context.handoff, context.home, context.outgoing, context.incoming,
1757
+ context.outgoingAccount, context.incomingAccount].every(item => item.updateId === updateId)
1758
+ && first?.accountId === context.outgoing.accountId
1759
+ && second?.accountId === context.incoming.accountId && first.epoch === second.epoch
1760
+ && second.revision === first.revision + 1) {
1761
+ return { handoff: context.handoff, home: context.home,
1762
+ outgoing: context.outgoing, incoming: context.incoming };
1763
+ }
1764
+ throw new Error('Claude handoff database outcome is unknown; native ownership remains blocked.');
1765
+ }
1766
+ }
1767
+
1768
+ /** Ledger transitions never silently replace an active credential or waive the verified import. */
773
1769
  public async changeMigrationLedger(updateId: string, ledgerId: string,
774
1770
  change: (current: IStoredAuthorityMigrationLedger | null) => IStoredAuthorityMigrationLedger): Promise<IStoredAuthorityMigrationLedger> {
775
1771
  try {
@@ -778,7 +1774,12 @@ export class AuthSwitchAuthorityDatabase {
778
1774
  const draft = change(current ? structuredClone(AuthSwitchAuthorityMigrationLedgerModel.exact.toPersisted(current)) : null);
779
1775
  if (draft.id !== ledgerId || draft.updateId !== updateId) throw new Error('Migration ledger identity is invalid.');
780
1776
  assertStoredAuthorityMigrationLedger(draft);
781
- if (draft.status === 'complete' && draft.backupDigest === null) throw new Error('Migration completion requires verified backup proof.');
1777
+ // A source may only be considered migrated after a verified import bound to an account, a grant and a
1778
+ // source digest. This deliberately tightens a flow that has no production caller and has never run.
1779
+ if (draft.status === 'complete' && (current?.status !== 'verified'
1780
+ || current.accountId !== draft.accountId || current.grantId !== draft.grantId)) {
1781
+ throw new Error('Migration completion requires a verified import of the same account and grant.');
1782
+ }
782
1783
  if (current) {
783
1784
  if (current.status === 'complete' || current.version !== draft.version
784
1785
  || current.sourceKind !== draft.sourceKind || current.sourcePathHash !== draft.sourcePathHash
@@ -797,7 +1798,7 @@ export class AuthSwitchAuthorityDatabase {
797
1798
  return draft;
798
1799
  });
799
1800
  } catch (error) {
800
- if (!(error instanceof plugins.smartdata.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
1801
+ if (!(error instanceof plugins.nosqldb.SmartdataExactPersistenceError) || error.code !== 'ambiguous_write') throw error;
801
1802
  const ledger = await this.readMigrationLedger(ledgerId);
802
1803
  if (ledger?.updateId === updateId) return ledger;
803
1804
  throw new Error('Authswitch migration ledger write outcome is unknown.');