@modelprofile.com/authswitch 7.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +1 -3
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-import-contract.d.ts +207 -0
  5. package/dist_ts/authority-import-contract.js +18 -0
  6. package/dist_ts/classes.authoritybroker.js +1 -5
  7. package/dist_ts/classes.authoritycli.d.ts +4 -0
  8. package/dist_ts/classes.authoritycli.js +149 -12
  9. package/dist_ts/classes.authorityclient.d.ts +7 -0
  10. package/dist_ts/classes.authorityclient.js +13 -1
  11. package/dist_ts/classes.authoritydaemon.d.ts +4 -0
  12. package/dist_ts/classes.authoritydaemon.js +75 -9
  13. package/dist_ts/classes.authoritydatabase.d.ts +10 -26
  14. package/dist_ts/classes.authoritydatabase.js +68 -133
  15. package/dist_ts/classes.authorityimport.d.ts +161 -0
  16. package/dist_ts/classes.authorityimport.js +638 -0
  17. package/dist_ts/classes.authoritymodels.d.ts +39 -43
  18. package/dist_ts/classes.authoritymodels.js +209 -243
  19. package/dist_ts/classes.authoritypreuse.js +1 -2
  20. package/dist_ts/classes.authorityregistry.d.ts +2 -17
  21. package/dist_ts/classes.authorityregistry.js +18 -74
  22. package/dist_ts/classes.authoritysecrets.d.ts +1 -1
  23. package/dist_ts/classes.authoritysecrets.js +2 -2
  24. package/dist_ts/classes.authorityusage.d.ts +3 -3
  25. package/dist_ts/classes.claudeauthority.js +3 -7
  26. package/dist_ts/classes.claudenative.d.ts +1 -8
  27. package/dist_ts/classes.claudenative.js +2 -25
  28. package/dist_ts/classes.codexmanaged.d.ts +1 -1
  29. package/dist_ts/index.d.ts +1 -0
  30. package/dist_ts/index.js +6 -3
  31. package/dist_ts/plugins.d.ts +7 -7
  32. package/dist_ts/plugins.js +8 -8
  33. package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -3
  34. package/dist_ts/ts_migration/0001_authority_meta.js +29 -17
  35. package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
  36. package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
  37. package/dist_ts/ts_migration/index.d.ts +6 -3
  38. package/dist_ts/ts_migration/index.js +10 -4
  39. package/dist_ts/ts_migration/legacysources/authswitchstores.d.ts +2 -0
  40. package/dist_ts/ts_migration/legacysources/authswitchstores.js +244 -0
  41. package/dist_ts/ts_migration/legacysources/index.d.ts +18 -0
  42. package/dist_ts/ts_migration/legacysources/index.js +24 -0
  43. package/dist_ts/ts_migration/legacysources/material.d.ts +37 -0
  44. package/dist_ts/ts_migration/legacysources/material.js +120 -0
  45. package/dist_ts/ts_migration/legacysources/nativestores.d.ts +2 -0
  46. package/dist_ts/ts_migration/legacysources/nativestores.js +207 -0
  47. package/dist_ts/ts_migration/legacysources/shared.d.ts +124 -0
  48. package/dist_ts/ts_migration/legacysources/shared.js +142 -0
  49. package/package.json +6 -5
  50. package/readme.md +68 -26
  51. package/ts/00_commitinfo_data.ts +1 -1
  52. package/ts/authority-contract.ts +1 -3
  53. package/ts/authority-import-contract.ts +216 -0
  54. package/ts/classes.authoritybroker.ts +0 -4
  55. package/ts/classes.authoritycli.ts +165 -11
  56. package/ts/classes.authorityclient.ts +22 -0
  57. package/ts/classes.authoritydaemon.ts +75 -8
  58. package/ts/classes.authoritydatabase.ts +70 -145
  59. package/ts/classes.authorityimport.ts +718 -0
  60. package/ts/classes.authoritymodels.ts +253 -269
  61. package/ts/classes.authoritypreuse.ts +0 -1
  62. package/ts/classes.authorityregistry.ts +20 -91
  63. package/ts/classes.authoritysecrets.ts +1 -1
  64. package/ts/classes.authorityusage.ts +3 -3
  65. package/ts/classes.claudeauthority.ts +2 -6
  66. package/ts/classes.claudenative.ts +1 -32
  67. package/ts/classes.codexmanaged.ts +1 -1
  68. package/ts/index.ts +5 -2
  69. package/ts/plugins.ts +7 -7
  70. package/ts/ts_migration/0001_authority_meta.ts +29 -17
  71. package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
  72. package/ts/ts_migration/index.ts +10 -5
  73. package/ts/ts_migration/legacysources/authswitchstores.ts +211 -0
  74. package/ts/ts_migration/legacysources/index.ts +35 -0
  75. package/ts/ts_migration/legacysources/material.ts +134 -0
  76. package/ts/ts_migration/legacysources/nativestores.ts +204 -0
  77. package/ts/ts_migration/legacysources/shared.ts +220 -0
  78. package/dist_ts/authorityrehearsal.child.d.ts +0 -1
  79. package/dist_ts/authorityrehearsal.child.js +0 -395
  80. package/dist_ts/classes.authoritybackup.d.ts +0 -134
  81. package/dist_ts/classes.authoritybackup.js +0 -769
  82. package/dist_ts/classes.authorityrehearsal.d.ts +0 -127
  83. package/dist_ts/classes.authorityrehearsal.js +0 -632
  84. package/ts/authorityrehearsal.child.ts +0 -330
  85. package/ts/classes.authoritybackup.ts +0 -780
  86. package/ts/classes.authorityrehearsal.ts +0 -625
@@ -1,5 +1,8 @@
1
1
  import type { IAuthSwitchDoctorPage, IAuthSwitchOperation, IAuthSwitchPreuseOperation,
2
2
  IAuthSwitchSnapshot, IReq_AuthSwitchDoctor, IReq_AuthSwitchStartPreuse } from './authority-contract.js';
3
+ import { isAuthSwitchImportRefusal } from './authority-import-contract.js';
4
+ import type { IAuthSwitchImportInventory, IAuthSwitchImportResult, IAuthSwitchImportStatus,
5
+ IReq_AuthSwitchImportStatus, TAuthSwitchImportAction } from './authority-import-contract.js';
3
6
  import { AuthSwitchClient } from './classes.authorityclient.js';
4
7
  import { resolveAuthSwitchAuthorityPaths } from './classes.authorityservice.js';
5
8
  import { defaultPreusePrompt, validatePreuseOptions } from './preuse.js';
@@ -9,6 +12,9 @@ import * as plugins from './plugins.js';
9
12
  export interface IAuthSwitchAuthorityCliClient {
10
13
  snapshotAll(): Promise<IAuthSwitchSnapshot>;
11
14
  doctorPage(request: IReq_AuthSwitchDoctor['request']): Promise<IAuthSwitchDoctorPage>;
15
+ importInventory(): Promise<IAuthSwitchImportInventory>;
16
+ importStatus(request: IReq_AuthSwitchImportStatus['request']): Promise<IAuthSwitchImportStatus>;
17
+ submitImport(sourceId: string, sourceDigest: string): Promise<IAuthSwitchImportResult>;
12
18
  beginAddOpenAi(operationId: string): Promise<IAuthSwitchOperation>;
13
19
  beginReauthOpenAi(operationId: string, accountId: string, loginId: string,
14
20
  purpose: 'openai_managed'): Promise<IAuthSwitchOperation>;
@@ -33,6 +39,9 @@ export interface IAuthSwitchAuthorityCliOptions {
33
39
  type TAuthorityCliCommand =
34
40
  | { kind: 'list'; json: boolean }
35
41
  | { kind: 'doctor'; json: boolean }
42
+ | { kind: 'importInventory'; json: boolean }
43
+ | { kind: 'importStatus'; json: boolean }
44
+ | { kind: 'importSubmit'; sourceId: string; sourceDigest: string; json: boolean }
36
45
  | { kind: 'add'; json: boolean }
37
46
  | { kind: 'reauth'; accountId: string; loginId: string; json: boolean }
38
47
  | { kind: 'get'; operationId: string; json: boolean }
@@ -51,9 +60,11 @@ class AuthorityCliFailure extends Error {
51
60
  }
52
61
  }
53
62
 
54
- const usage = 'Usage: authswitch account list [--json] | authswitch account add openai [--json] | authswitch account reauth <account-id> <login-id> [--json] | authswitch account operation get|cancel|resume <operation-id> [--json] | authswitch account preuse <account-id> <login-id>|--all [--prompt <text>] [--model <id>] [--json] | authswitch account preuse operation get|cancel|resume <operation-id> [--json] | authswitch authority doctor [--json]';
63
+ const usage = 'Usage: authswitch account list [--json] | authswitch account add openai [--json] | authswitch account reauth <account-id> <login-id> [--json] | authswitch account operation get|cancel|resume <operation-id> [--json] | authswitch account preuse <account-id> <login-id>|--all [--prompt <text>] [--model <id>] [--json] | authswitch account preuse operation get|cancel|resume <operation-id> [--json] | authswitch authority doctor [--json] | authswitch authority import inventory [--json] | authswitch authority import status [--json] | authswitch authority import submit <source-id> --digest <source-digest> [--json]';
55
64
  const safe = (value: string | number | null): string => plainText(value === null ? 'none' : String(value));
56
65
  const isId = (value: string | undefined): value is string => Boolean(value && !value.startsWith('-'));
66
+ const isHash = (value: string | undefined): value is string =>
67
+ typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
57
68
 
58
69
  const parsePreuse = (argv: readonly string[]): TAuthorityCliCommand | null => {
59
70
  if (argv[2] === 'operation') {
@@ -109,6 +120,21 @@ const parse = (argv: readonly string[]): TAuthorityCliCommand | null => {
109
120
  && (argv.length === 2 || (argv.length === 3 && argv[2] === '--json'))) {
110
121
  return { kind: 'doctor', json: argv[2] === '--json' };
111
122
  }
123
+ if (argv[0] === 'authority' && argv[1] === 'import' && argv[2] === 'inventory'
124
+ && (argv.length === 3 || (argv.length === 4 && argv[3] === '--json'))) {
125
+ return { kind: 'importInventory', json: argv[3] === '--json' };
126
+ }
127
+ if (argv[0] === 'authority' && argv[1] === 'import' && argv[2] === 'status'
128
+ && (argv.length === 3 || (argv.length === 4 && argv[3] === '--json'))) {
129
+ return { kind: 'importStatus', json: argv[3] === '--json' };
130
+ }
131
+ if (argv[0] === 'authority' && argv[1] === 'import' && argv[2] === 'submit') {
132
+ // The digest the owner read in the inventory is part of the command: the daemon refuses a source whose
133
+ // bytes have changed since, so approving one source can never import a different one.
134
+ if (!isHash(argv[3]) || argv[4] !== '--digest' || !isHash(argv[5])
135
+ || (argv.length !== 6 && (argv.length !== 7 || argv[6] !== '--json'))) return null;
136
+ return { kind: 'importSubmit', sourceId: argv[3], sourceDigest: argv[5], json: argv[6] === '--json' };
137
+ }
112
138
  if (argv[0] === 'account' && argv[1] === 'add' && argv[2] === 'openai'
113
139
  && (argv.length === 3 || (argv.length === 4 && argv[3] === '--json'))) {
114
140
  return { kind: 'add', json: argv[3] === '--json' };
@@ -143,6 +169,18 @@ class ScopedAuthorityCliClient implements IAuthSwitchAuthorityCliClient {
143
169
  return this.client.doctorPage(request, this.controller.signal);
144
170
  }
145
171
 
172
+ public importInventory(): Promise<IAuthSwitchImportInventory> {
173
+ return this.client.importInventory(this.controller.signal);
174
+ }
175
+
176
+ public importStatus(request: IReq_AuthSwitchImportStatus['request']): Promise<IAuthSwitchImportStatus> {
177
+ return this.client.importStatus(request, this.controller.signal);
178
+ }
179
+
180
+ public submitImport(sourceId: string, sourceDigest: string): Promise<IAuthSwitchImportResult> {
181
+ return this.client.submitImport({ kind: 'local', sourceId, sourceDigest }, this.controller.signal);
182
+ }
183
+
146
184
  public beginAddOpenAi(operationId: string): Promise<IAuthSwitchOperation> {
147
185
  return this.client.beginAddOpenAi(operationId, this.controller.signal);
148
186
  }
@@ -201,6 +239,91 @@ const renderList = (snapshot: IAuthSwitchSnapshot): string => {
201
239
  return lines.join('\n') + '\n';
202
240
  };
203
241
 
242
+ /**
243
+ * The legacy-source report.
244
+ *
245
+ * The notice comes first and is never abbreviated: an import is a one-way transfer of ownership, and this is
246
+ * the only place an owner reads what that costs before approving it.
247
+ */
248
+ const renderInventory = (inventory: IAuthSwitchImportInventory): string => {
249
+ const lines = [`Legacy account sources — generated ${safe(inventory.generatedAt)}, environment ${safe(inventory.environmentId)}`,
250
+ 'Before you import:'];
251
+ for (const notice of inventory.notice) lines.push(` - ${safe(notice)}`);
252
+ lines.push('');
253
+ if (inventory.sources.length === 0) lines.push('No legacy credential source was found.');
254
+ for (const source of inventory.sources) {
255
+ lines.push(`${safe(source.label ?? source.email ?? source.sourceKind)} [${safe(source.providerId)}]`,
256
+ ` source ${safe(source.id)}; kind ${safe(source.sourceKind)}; treatment ${safe(source.treatment)}; proof ${safe(source.proof)}`,
257
+ ` account ${safe(source.accountId)}; plan ${safe(source.plan)}; access token expires ${safe(source.accessExpiresAt)}`,
258
+ ` enrollments ${source.enrollmentCount === null ? 'unknown' : source.enrollmentCount}; ledger ${safe(source.ledgerStatus)}; digest ${safe(source.sourceDigest)}`);
259
+ for (const problem of source.problems) lines.push(` problem: ${safe(problem)}`);
260
+ }
261
+ if (inventory.problems.length) {
262
+ lines.push('', 'Locations that could not be read:');
263
+ for (const problem of inventory.problems) lines.push(` - ${safe(problem)}`);
264
+ }
265
+ return lines.join('\n') + '\n';
266
+ };
267
+
268
+ /**
269
+ * What the owner does next with a source, in words rather than a contract value. An action this version does
270
+ * not recognise is read as the careful one: never invite a second submission on a guess.
271
+ */
272
+ const actionText = (action: TAuthSwitchImportAction): string =>
273
+ action === 'none' ? 'nothing to do'
274
+ : action === 'resume' ? 'submit this source again to continue where it stopped'
275
+ : 'sign in to this account again; this source cannot be imported a second time';
276
+
277
+ /**
278
+ * Every page of the ledger, as one report.
279
+ *
280
+ * The owner acts on this read, so it must not stop at a page boundary and leave a quarantined source
281
+ * unmentioned. A cursor that does not advance is a broken answer rather than a reason to keep asking.
282
+ */
283
+ const collectImportStatus = async (client: IAuthSwitchAuthorityCliClient): Promise<IAuthSwitchImportStatus> => {
284
+ const first = await client.importStatus({ limit: 128 });
285
+ const entries = [...first.entries];
286
+ let cursor = first.nextCursor;
287
+ while (cursor !== null) {
288
+ const page = await client.importStatus({ after: cursor, limit: 128 });
289
+ if (page.entries.length === 0 || page.nextCursor === cursor) throw new AuthorityCliFailure('response');
290
+ entries.push(...page.entries);
291
+ cursor = page.nextCursor;
292
+ }
293
+ return { ...first, entries, nextCursor: null };
294
+ };
295
+
296
+ const renderImportStatus = (status: IAuthSwitchImportStatus): string => {
297
+ const lines = [`Import status — generated ${safe(status.generatedAt)}`];
298
+ if (status.entries.length === 0) {
299
+ lines.push('No source has been submitted yet. Run `authswitch authority import inventory` to see what this host holds.');
300
+ }
301
+ for (const entry of status.entries) {
302
+ lines.push(`${safe(entry.sourceKind)} ${safe(entry.sourceId)}`,
303
+ ` state ${safe(entry.status)}; refresher ${safe(entry.owner)}; recorded ${safe(entry.statusObservedAt)}`,
304
+ ` account ${safe(entry.accountId)}; login ${safe(entry.loginId)}`,
305
+ ` next: ${actionText(entry.action)}`);
306
+ }
307
+ // A source that was never submitted has no ledger row, so it is the inventory's subject, not this read's.
308
+ lines.push('Sources that were never submitted are listed by `authswitch authority import inventory`.');
309
+ return lines.join('\n') + '\n';
310
+ };
311
+
312
+ const renderImportResult = (result: IAuthSwitchImportResult): string => {
313
+ const settled = result.status === 'complete';
314
+ const lines = [`${settled ? 'Imported' : 'Import did not settle'}: ${safe(result.sourceKind)} ${safe(result.sourceId)}`,
315
+ ` treatment ${safe(result.treatment)}; state ${safe(result.status)}`,
316
+ ` account ${safe(result.accountId)}; login ${safe(result.loginId)}`];
317
+ for (const problem of result.problems) lines.push(` problem: ${safe(problem)}`);
318
+ if (settled && result.treatment === 'import') {
319
+ lines.push(' The authority now refreshes this login. Its legacy copy holds a token the provider has rotated away.');
320
+ }
321
+ if (settled && result.treatment === 'project') {
322
+ lines.push(' The native tool still refreshes this login. The authority holds no copy of it.');
323
+ }
324
+ return lines.join('\n') + '\n';
325
+ };
326
+
204
327
  const terminalOperation = (operation: IAuthSwitchOperation): boolean =>
205
328
  operation.state === 'complete' || operation.state === 'failed' || operation.state === 'cancelled'
206
329
  || operation.state === 'interrupted' || operation.state === 'outcome_unknown';
@@ -289,7 +412,7 @@ const renderDoctorPage = (page: IAuthSwitchDoctorPage, pageNumber: number): stri
289
412
  const lines = [
290
413
  `Authority doctor page ${pageNumber} — observed ${safe(page.observedAt)}`,
291
414
  ` daemon=${safe(page.daemon.state)}; Claude native authority=${safe(page.daemon.claudeNativeAuthority)}; observed=${safe(page.daemon.observedAt)}`,
292
- ` database=${safe(page.database.state)}; admission=${safe(page.database.admission)}; schema=${page.database.schemaVersion}; revision=${page.database.revision}; observed=${safe(page.database.observedAt)}`,
415
+ ` database=${safe(page.database.state)}; revision=${page.database.revision}; observed=${safe(page.database.observedAt)}`,
293
416
  ` accounts: ${page.accounts.length} stored evidence record(s); live provider check not performed`,
294
417
  ` logins: ${page.logins.length} stored evidence record(s); live provider check not performed`,
295
418
  ];
@@ -443,6 +566,7 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
443
566
  let preuseHint = false;
444
567
  let beginAttempted = false;
445
568
  let beginResponded = false;
569
+ let submitAttempted = false;
446
570
  const operationController = new AbortController();
447
571
  const closeClient = (reason?: Error): void => {
448
572
  if (clientClosed || !client) return;
@@ -579,6 +703,24 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
579
703
  await runDoctor(activeClient, command.json, writeOutput);
580
704
  return 0;
581
705
  }
706
+ if (command.kind === 'importInventory') {
707
+ const inventory = await activeClient.importInventory();
708
+ await writeOutput(command.json ? JSON.stringify(inventory) + '\n' : renderInventory(inventory));
709
+ return 0;
710
+ }
711
+ if (command.kind === 'importStatus') {
712
+ const status = await collectImportStatus(activeClient);
713
+ await writeOutput(command.json ? JSON.stringify(status) + '\n' : renderImportStatus(status));
714
+ return 0;
715
+ }
716
+ if (command.kind === 'importSubmit') {
717
+ // A lost response leaves the outcome unknown to this process but never to the authority, so the
718
+ // failure path below points at the status read rather than inviting a second submission.
719
+ submitAttempted = true;
720
+ const result = await activeClient.submitImport(command.sourceId, command.sourceDigest);
721
+ await writeOutput(command.json ? JSON.stringify(result) + '\n' : renderImportResult(result));
722
+ return result.status === 'complete' ? 0 : 1;
723
+ }
582
724
  if (command.kind === 'preuseGet' || command.kind === 'preuseCancel') {
583
725
  const operation = validatePreuseOperation(command.kind === 'preuseGet'
584
726
  ? await activeClient.getPreuse(command.operationId)
@@ -692,15 +834,27 @@ export const runAuthSwitchAuthorityCli = async (argv: readonly string[],
692
834
  }
693
835
  return 0;
694
836
  } catch (error) {
695
- let message = cancelled ? 'Authswitch authority request was cancelled.\n'
696
- : timedOut ? 'Authswitch authority operation timed out.\n'
697
- : beginAttempted && !beginResponded
698
- ? 'Authswitch authority operation start response was lost.\n'
699
- : error instanceof AuthorityCliFailure && error.kind === 'output'
700
- ? 'Authswitch authority output failed.\n'
701
- : error instanceof AuthorityCliFailure && error.kind === 'response'
702
- ? 'Authswitch authority daemon returned an invalid response.\n'
703
- : 'Authswitch authority daemon is unavailable.\n';
837
+ // A marked refusal is the daemon's answer to the command, not a fault: it names what the owner does
838
+ // instead and nothing was left half-done, so it is shown as written rather than replaced by a generic
839
+ // line. Any other failure -- including the transport's own sanitised text -- stays a diagnostic.
840
+ const refusal = error instanceof Error && isAuthSwitchImportRefusal(error) && !cancelled && !timedOut
841
+ ? safe(error.message) : null;
842
+ let message = refusal !== null ? `${refusal}\n`
843
+ : cancelled ? 'Authswitch authority request was cancelled.\n'
844
+ : timedOut ? 'Authswitch authority operation timed out.\n'
845
+ : beginAttempted && !beginResponded
846
+ ? 'Authswitch authority operation start response was lost.\n'
847
+ : error instanceof AuthorityCliFailure && error.kind === 'output'
848
+ ? 'Authswitch authority output failed.\n'
849
+ : error instanceof AuthorityCliFailure && error.kind === 'response'
850
+ ? 'Authswitch authority daemon returned an invalid response.\n'
851
+ : 'Authswitch authority daemon is unavailable.\n';
852
+ if (submitAttempted && refusal === null) {
853
+ // The authority may have settled this source even though this process never learned the answer; never
854
+ // submit it again blindly, because a rotating refresh token is sent at most once.
855
+ message += 'The outcome of this import is unknown to this command. Run `authswitch authority import '
856
+ + 'status` before doing anything else with this source.\n';
857
+ }
704
858
  if (operationIdForHint !== null) {
705
859
  const resume = preuseHint ? preuseResumeCommand(operationIdForHint, command.json)
706
860
  : resumeCommand(operationIdForHint, command.json);
@@ -11,6 +11,8 @@ import type {
11
11
  } from './authority-contract.js';
12
12
  import type { IReq_AuthSwitchBindAccount, IReq_AuthSwitchReleaseBinding,
13
13
  IReq_AuthSwitchResolveAccess } from './authority-runtime-contract.js';
14
+ import type { IReq_AuthSwitchImportInventory, IReq_AuthSwitchImportStatus,
15
+ IReq_AuthSwitchImportSubmit } from './authority-import-contract.js';
14
16
  import { AuthSwitchAuthorityFrameReader, authSwitchAuthorityFrameBytes,
15
17
  maxAuthSwitchAuthorityFrameBytes } from './classes.authorityframing.js';
16
18
 
@@ -97,6 +99,26 @@ export class AuthSwitchClient {
97
99
  return (await this.request<IReq_AuthSwitchDoctor>('authswitch.authority.doctor', options, 35_000, false, signal)).page;
98
100
  }
99
101
 
102
+ /** The credential-free legacy-source report of the one-time import. It changes nothing. */
103
+ public async importInventory(signal?: AbortSignal): Promise<IReq_AuthSwitchImportInventory['response']['inventory']> {
104
+ return (await this.request<IReq_AuthSwitchImportInventory>('authswitch.authority.import.inventory',
105
+ {}, 35_000, false, signal)).inventory;
106
+ }
107
+
108
+ /** Where every source the authority knows about stands. Durable records only; no store is opened. */
109
+ public async importStatus(request: IReq_AuthSwitchImportStatus['request'] = {},
110
+ signal?: AbortSignal): Promise<IReq_AuthSwitchImportStatus['response']['status']> {
111
+ return (await this.request<IReq_AuthSwitchImportStatus>('authswitch.authority.import.status',
112
+ request, 35_000, false, signal)).status;
113
+ }
114
+
115
+ /** Backend-only. One source, one ownership decision; both acknowledgements are required. */
116
+ public async submitImport(submission: IReq_AuthSwitchImportSubmit['request']['submission'],
117
+ signal?: AbortSignal): Promise<IReq_AuthSwitchImportSubmit['response']['result']> {
118
+ return (await this.request<IReq_AuthSwitchImportSubmit>('authswitch.authority.import.submit',
119
+ { submission, callerQuiescent: true, acknowledgeRunOrder: true }, 120_000, true, signal)).result;
120
+ }
121
+
100
122
  public async getUsage(accountId: string, loginId: string, force = false): Promise<IReq_AuthSwitchUsage['response']['usage']> {
101
123
  return (await this.request<IReq_AuthSwitchUsage>('authswitch.authority.usage',
102
124
  { accountId, loginId, force })).usage;
@@ -10,6 +10,11 @@ import type {
10
10
  } from './authority-contract.js';
11
11
  import type { IReq_AuthSwitchBindAccount, IReq_AuthSwitchReleaseBinding,
12
12
  IReq_AuthSwitchResolveAccess } from './authority-runtime-contract.js';
13
+ import { authSwitchImportRefusalReason } from './authority-import-contract.js';
14
+ import type { IReq_AuthSwitchImportInventory, IReq_AuthSwitchImportStatus, IReq_AuthSwitchImportSubmit,
15
+ TAuthSwitchImportSubmission } from './authority-import-contract.js';
16
+ import { AuthSwitchAuthorityImport, AuthSwitchImportRefusal,
17
+ type IAuthSwitchAuthorityImportOptions } from './classes.authorityimport.js';
13
18
  import { authSwitchBindingCapabilityHash, AuthSwitchAuthorityBroker,
14
19
  type IAuthSwitchAuthorityBrokerOptions } from './classes.authoritybroker.js';
15
20
  import { AuthSwitchAuthorityDatabase, type IAuthSwitchAuthorityDatabaseOptions } from './classes.authoritydatabase.js';
@@ -32,6 +37,8 @@ export interface IAuthSwitchAuthorityDaemonOptions extends IAuthSwitchAuthorityD
32
37
  claudeStatus?: ClaudeAccountStatus;
33
38
  /** Activated only after a verified backend source receipt has registered the native home. */
34
39
  claudeNative?: IClaudeNativeAuthorityOptions;
40
+ /** Where the one-time import reads the legacy stores. Defaults to this user's own locations. */
41
+ legacyImport?: IAuthSwitchAuthorityImportOptions;
35
42
  }
36
43
 
37
44
  const hasKeys = (value: unknown, keys: readonly string[]): value is Record<string, unknown> =>
@@ -41,6 +48,28 @@ const invalid = (): never => { throw new plugins.typedrequest.TypedResponseError
41
48
  const validBindingId = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
42
49
  const validBindingCapability = (value: unknown): value is string => typeof value === 'string'
43
50
  && /^[A-Za-z0-9_-]{43}$/.test(value);
51
+ const validHash = (value: unknown): value is string => typeof value === 'string'
52
+ && /^[a-f0-9]{64}$/.test(value);
53
+ const boundedToken = (value: unknown): value is string => typeof value === 'string'
54
+ && value.length > 0 && value.length <= 8192;
55
+
56
+ /** A submission names a local source by id and digest, or carries one external store's own login. */
57
+ const validImportSubmission = (value: unknown): value is TAuthSwitchImportSubmission => {
58
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
59
+ const submission = value as Record<string, unknown>;
60
+ if (submission.kind === 'local') {
61
+ return hasKeys(submission, ['kind', 'sourceId', 'sourceDigest'])
62
+ && validHash(submission.sourceId) && validHash(submission.sourceDigest);
63
+ }
64
+ if (submission.kind !== 'external'
65
+ || !hasKeys(submission, ['kind', 'sourceKind', 'sourcePathHash', 'sourceDigest', 'credential'])
66
+ || submission.sourceKind !== 'agl_flex' || !validHash(submission.sourcePathHash)
67
+ || !validHash(submission.sourceDigest)) return false;
68
+ const credential = submission.credential;
69
+ return hasKeys(credential, ['providerId', 'accessToken', 'refreshToken', 'idToken'])
70
+ && credential.providerId === 'openai' && boundedToken(credential.accessToken)
71
+ && boundedToken(credential.refreshToken) && boundedToken(credential.idToken);
72
+ };
44
73
 
45
74
  interface IPreuseAccountReservation {
46
75
  operationId: string;
@@ -52,6 +81,7 @@ export class AuthSwitchAuthorityDaemon {
52
81
  public readonly broker: AuthSwitchAuthorityBroker;
53
82
  public readonly usage: AuthSwitchAuthorityUsage;
54
83
  public readonly preuse: AuthSwitchAuthorityPreuse;
84
+ public readonly legacyImport: AuthSwitchAuthorityImport;
55
85
  public readonly claudeNative?: ClaudeNativeAuthority;
56
86
  private readonly managementRouter = new plugins.typedrequest.TypedRouter();
57
87
  private readonly runtimeRouter = new plugins.typedrequest.TypedRouter();
@@ -79,10 +109,7 @@ export class AuthSwitchAuthorityDaemon {
79
109
  this.broker = new AuthSwitchAuthorityBroker(this.database, { ...options.broker, provider });
80
110
  this.usage = new AuthSwitchAuthorityUsage({
81
111
  ...options.usage,
82
- readContext: async (accountId, loginId) => {
83
- await this.database.assertOpenAdmission();
84
- return this.database.readUsageContext(accountId, loginId);
85
- },
112
+ readContext: (accountId, loginId) => this.database.readUsageContext(accountId, loginId),
86
113
  resolveAccess: (context, rejectedGrantGeneration, minValidityMs) =>
87
114
  this.broker.resolveUsageAccess(context, rejectedGrantGeneration, minValidityMs),
88
115
  resolveClaudeLogin: (context, rejectedGrantGeneration, minValidityMs) => {
@@ -105,6 +132,7 @@ export class AuthSwitchAuthorityDaemon {
105
132
  provider,
106
133
  });
107
134
  this.preuse = new AuthSwitchAuthorityPreuse(this.database, this.broker, options.preuse);
135
+ this.legacyImport = new AuthSwitchAuthorityImport(this.database, options.legacyImport);
108
136
  if (options.claudeNative) {
109
137
  this.claudeNative = new ClaudeNativeAuthority(this.database, {
110
138
  ...options.claudeNative, onChanged: () => this.broker.notifyChanged(),
@@ -251,6 +279,25 @@ export class AuthSwitchAuthorityDaemon {
251
279
  || request.limit < 1 || request.limit > 128))) invalid();
252
280
  return { page: await this.doctorPage(request) };
253
281
  }));
282
+ this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchImportInventory>(
283
+ 'authswitch.authority.import.inventory', async request => {
284
+ // A read with no parameters: the inventory is the whole host, and a caller cannot narrow it into
285
+ // believing a source does not exist.
286
+ if (request === null || typeof request !== 'object' || Array.isArray(request)
287
+ || Object.keys(request).length !== 0) invalid();
288
+ if (this.closing) throw new Error('Authswitch authority is shutting down.');
289
+ return { inventory: await this.legacyImport.inventory() };
290
+ }));
291
+ this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchImportStatus>(
292
+ 'authswitch.authority.import.status', async request => {
293
+ if (request === null || typeof request !== 'object' || Array.isArray(request)
294
+ || Object.keys(request).some(key => !['after', 'limit'].includes(key))
295
+ || (request.after !== undefined && !validHash(request.after))
296
+ || (request.limit !== undefined && (!Number.isSafeInteger(request.limit)
297
+ || request.limit < 1 || request.limit > 128))) invalid();
298
+ if (this.closing) throw new Error('Authswitch authority is shutting down.');
299
+ return { status: await this.legacyImport.status(request) };
300
+ }));
254
301
  this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchEvents>(
255
302
  'authswitch.authority.events', async (request, tools) => {
256
303
  if (!hasKeys(request, ['epoch', 'afterRevision', 'waitMs'])) invalid();
@@ -384,6 +431,28 @@ export class AuthSwitchAuthorityDaemon {
384
431
  if (this.closing) throw new Error('Authswitch authority is closing.');
385
432
  return this.releaseRuntimeBinding(request.bindingId, request.capability);
386
433
  }));
434
+ this.runtimeRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchImportSubmit>(
435
+ 'authswitch.authority.import.submit', async request => {
436
+ // Backend-only: an external submission carries the material of a store this package cannot read.
437
+ if (!hasKeys(request, ['submission', 'callerQuiescent', 'acknowledgeRunOrder'])
438
+ || request.callerQuiescent !== true || request.acknowledgeRunOrder !== true
439
+ || !validImportSubmission(request.submission)) invalid();
440
+ if (this.closing) throw new Error('Authswitch authority is closing.');
441
+ try {
442
+ return { result: await this.legacyImport.submit({ submission: request.submission,
443
+ callerQuiescent: true, acknowledgeRunOrder: true }) };
444
+ } catch (error) {
445
+ // A refusal is an answer, not a fault: it names what the owner does instead, and only a
446
+ // TypedResponseError survives the wire with its text. The marker is what lets the caller tell
447
+ // that instruction apart from the transport's sanitised text for an unexpected failure, whose
448
+ // import outcome is unknown. Everything else stays sanitised and unmarked.
449
+ if (error instanceof AuthSwitchImportRefusal) {
450
+ throw new plugins.typedrequest.TypedResponseError(error.message,
451
+ { reason: authSwitchImportRefusalReason });
452
+ }
453
+ throw error;
454
+ }
455
+ }));
387
456
  }
388
457
 
389
458
  private async doctorPage(request: IReq_AuthSwitchDoctor['request']): Promise<IAuthSwitchDoctorPage> {
@@ -450,8 +519,8 @@ export class AuthSwitchAuthorityDaemon {
450
519
  observedAt,
451
520
  daemon: { state: this.closing ? 'closing' : 'ready',
452
521
  claudeNativeAuthority: this.claudeNative ? 'configured' : 'not_configured', observedAt },
453
- database: { state: 'available', observedAt: stored.observedAt, schemaVersion: stored.meta.schemaVersion,
454
- admission: stored.meta.admission, epoch: stored.meta.epoch, revision: stored.meta.revision },
522
+ database: { state: 'available', observedAt: stored.observedAt,
523
+ epoch: stored.meta.epoch, revision: stored.meta.revision },
455
524
  accounts: stored.accounts.map(account => ({ id: account.id, providerId: account.providerId,
456
525
  removed: account.removed, statusObservedAt: account.statusObservedAt,
457
526
  evidence: 'stored', liveCheck: 'not_checked' })),
@@ -491,7 +560,6 @@ export class AuthSwitchAuthorityDaemon {
491
560
  if (this.managementServer || this.runtimeServer) throw new Error('Authswitch authority daemon is already started.');
492
561
  await this.database.start();
493
562
  try {
494
- await this.database.assertOpenAdmission();
495
563
  await this.broker.start();
496
564
  await this.preuse.start();
497
565
  if (this.claudeNative) await this.claudeNative.start();
@@ -566,7 +634,6 @@ export class AuthSwitchAuthorityDaemon {
566
634
  let runtime: AuthSwitchManagedCodex | undefined;
567
635
  let claimed = false;
568
636
  try {
569
- await this.database.assertOpenAdmission();
570
637
  const home = input.codexHomeDirectory;
571
638
  const parent = plugins.path.dirname(home);
572
639
  const parentStat = await plugins.fs.promises.lstat(parent);