@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
package/readme.md CHANGED
@@ -28,27 +28,53 @@ Node.js 24 or newer is required.
28
28
 
29
29
  The package also exports a typed client for a per-user authswitch authority daemon. Its
30
30
  SmartData registry stores account identity separately from sealed refresh grants, runtime
31
- bindings, Codex enrollment records, and native-handoff and migration journals. A daemon
31
+ bindings, durable device-operation receipts, Codex enrollment records, and native-handoff and migration journals. A daemon
32
32
  owns OpenAI device login, targeted reauthentication, and proactive refresh for grants
33
- added through this API. Management snapshots and events contain no credentials.
33
+ added through this API. Management snapshots expose separate account and login rows,
34
+ including each login's purpose, owner, health, problem and available actions; neither
35
+ snapshots nor events contain credentials.
34
36
 
35
37
  ```ts
38
+ import { randomUUID } from 'node:crypto';
36
39
  import { AuthSwitchClient } from '@modelprofile.com/authswitch/authority-client';
37
40
  import { resolveAuthSwitchAuthorityPaths } from '@modelprofile.com/authswitch';
38
41
 
39
42
  const paths = resolveAuthSwitchAuthorityPaths();
40
43
  const client = new AuthSwitchClient(paths.authoritySocketPath, paths.runtimeSocketPath);
41
44
  const snapshot = await client.snapshotAll();
42
- const operation = await client.beginAddOpenAi();
45
+ const operationId = randomUUID();
46
+ const operation = await client.beginAddOpenAi(operationId);
47
+ // Keep operation.id to discover its final result after navigation or reconnect.
48
+ const receipt = await client.getOperation(operation.id);
43
49
  ```
44
50
 
45
51
  Browser code can import the credential-free DTOs from
46
52
  `@modelprofile.com/authswitch/authority-contract`. A backend binds an account to a
47
- runtime incarnation and keeps the returned capability private. The runtime-only socket
53
+ runtime incarnation by exact account ID, login ID and purpose, and keeps the returned
54
+ capability private. Targeted reauthentication likewise requires the exact login; an
55
+ account's presentation default never selects a grant for either action. Completed and
56
+ interrupted operations remain discoverable through `getOperation()` and paged
57
+ `listOperations()`. A completed receipt and its account/grant change commit together, so
58
+ the receipt resolves a lost response or an event that arrives first. Add and reauthentication
59
+ callers supply one UUID for the logical start and reuse it after a lost response. Replaying that
60
+ UUID returns the same matching receipt without starting another provider login; using it for a
61
+ different target is rejected. Cancelling a durable `starting` receipt prevents a late provider
62
+ handle from promoting or overwriting it. Aborting a client request only detaches that request and
63
+ does not cancel the daemon-owned login. The runtime-only socket
48
64
  resolves that binding to a current access token; its directory can be mounted into a
49
65
  container without exposing the management socket. A provider that rejects a specific
50
66
  access-token generation can request a newer one through `rejectedGrantGeneration`; the
51
67
  authority coalesces concurrent rejection callbacks and never replays an uncertain refresh.
68
+ After an external Flex or OpenCode runtime has fenced new work and drained its pending access
69
+ resolutions and provider requests, it can release its exact binding capability through the
70
+ runtime socket. Every bind receives a fresh random capability, so a stale release cannot fence
71
+ or delete a successor even if the caller reused an incarnation label. Release waits for admitted
72
+ server-side resolution handlers, but handler completion does not prove response delivery and
73
+ cannot retract an access token already received or in use; the runtime owner therefore owns that
74
+ drain. Generic release cannot revoke a managed Codex binding, whose daemon-owned stop still closes
75
+ the runtime and clears its durable run before internal revocation. Binding release never transfers
76
+ or changes ownership of the account grant. External-binding recovery after an owner crash remains
77
+ a prerequisite for the full account-mutation cutover.
52
78
 
53
79
  `AuthSwitchClient.subscribe(onSnapshot, onEvent, signal, { onStatus })` reports `current`
54
80
  after a fresh snapshot or verified heartbeat, `unavailable` on disconnect or resync, and
@@ -57,6 +83,111 @@ Every change-event batch supplies a fresh snapshot before `onEvent`, so a consum
57
83
  not need to reconstruct account state from events. `lastVerifiedAt` records the last
58
84
  successful daemon response, including an empty heartbeat.
59
85
 
86
+ `AuthSwitchClient.doctorPage()` reads credential-free diagnostic evidence without unsealing
87
+ a grant, refreshing a token or contacting a provider. Each bounded page separately stamps
88
+ the database observation, daemon state and managed-Codex process observation. Stored account
89
+ and login rows explicitly report `liveCheck: not_checked`; a ready daemon therefore does not
90
+ claim that a stored grant or provider is healthy. Native homes and journals omit paths,
91
+ digests and sealed values, while managed Codex reports durable, in-memory and observed-process
92
+ state independently with fixed problem categories. Continue each independent cursor to inspect
93
+ the complete inventory. Pages are later observations rather than one atomic global snapshot,
94
+ and the authority metadata revision is not a change clock for every native-home record. If the
95
+ management socket is unavailable, an offline caller must leave the database state unknown
96
+ instead of starting the database to inspect it.
97
+
98
+ The authority CLI uses that daemon surface without constructing the legacy native harness adapters:
99
+
100
+ ```bash
101
+ authswitch account list [--json]
102
+ authswitch account add openai [--json]
103
+ authswitch account reauth <account-id> <login-id> [--json]
104
+ authswitch account operation get <operation-id> [--json]
105
+ authswitch account operation cancel <operation-id> [--json]
106
+ authswitch account operation resume <operation-id> [--json]
107
+ authswitch account preuse <account-id> <login-id> [--prompt <text>] [--model <id>] [--json]
108
+ authswitch account preuse --all [--prompt <text>] [--model <id>] [--json]
109
+ authswitch account preuse operation get <operation-id> [--json]
110
+ authswitch account preuse operation cancel <operation-id> [--json]
111
+ authswitch account preuse operation resume <operation-id> [--json]
112
+ authswitch authority doctor [--json]
113
+ ```
114
+
115
+ `account list --json` prints the complete, consistent credential-free authority snapshot.
116
+ Its human form labels login health as stored state and does not imply a live provider check.
117
+ `account add openai` starts one standalone OpenAI device login. `account reauth` starts one
118
+ device login for the exact account and managed login IDs; it never chooses a presentation
119
+ default. Both commands print the durable operation ID, verification URL, user code and changed
120
+ receipts until completion. `account operation resume` polls only the supplied durable ID, while
121
+ `get` performs one read and `cancel` performs one explicit cancellation. Ctrl-C or the local
122
+ 15-minute deadline detaches the CLI and leaves the daemon-owned operation running. If a start
123
+ response is lost, the fixed diagnostic reports the caller-generated operation ID and exact resume
124
+ command; the CLI does not replay the start request.
125
+
126
+ `account preuse` sends one quota-consuming prompt through the exact managed OpenAI account and
127
+ login IDs. The default prompt is `Write 2000 words about strawberries.`; `--prompt` replaces it
128
+ and `--model` requests one exact model. `--all` freezes one consistent authority snapshot, walks
129
+ its canonical daemon-owned OpenAI managed logins in snapshot order, and starts one fresh durable
130
+ operation per login whose stored `canPreuse` capability is true. It does not infer readiness from
131
+ plan, usage, token expiry or binding capability. A login whose snapshot capability is false is
132
+ reported as skipped with the snapshot revision, login revision, stored health, problem and
133
+ observation time; no request or operation ID is created for that skip.
134
+
135
+ Preuse operations run sequentially and are never retried automatically. Every terminal receipt
136
+ advances an `--all` run, including `not_sent` and `outcome_unknown`, while any attempted operation
137
+ that does not complete makes the final exit status nonzero. Snapshot-ineligible skips alone do not
138
+ make an otherwise successful batch fail. Running `--all` again is a new quota-consuming run with
139
+ new operation IDs. Use `account preuse operation resume` to continue observing an existing receipt.
140
+ Only the explicit `cancel` command asks the daemon to cancel one.
141
+
142
+ For `account add`, `account reauth` and `account operation resume`, `--json` writes JSON Lines:
143
+ each line is one complete operation receipt, and unchanged revisions are omitted. `account
144
+ operation get --json` and `account operation cancel --json` each write one JSON object followed by
145
+ a newline. Human output includes the resume command on every unfinished receipt. These device
146
+ commands do not switch the active native login.
147
+
148
+ For one preuse start or resume, `--json` likewise writes each complete changed receipt as one raw
149
+ JSON Line; preuse get and cancel write one raw JSON object. A preuse `--all --json` stream tags each
150
+ line as `operation` or `skipped`. Operation lines contain the complete durable receipt. Skip lines
151
+ contain the exact account and login IDs plus the stored snapshot evidence described above. A lost
152
+ start response, transport or output failure, Ctrl-C, or the local 15-minute observation deadline
153
+ stops the batch before another login starts and reports the current operation ID and resume command.
154
+ It detaches without replaying or implicitly cancelling the daemon operation.
155
+
156
+ `authority doctor --json` incrementally writes one JSON document. Its `pages` array contains
157
+ every independently stamped bounded page; `observationSpan` is written only after all cursors
158
+ complete, and `globallyAtomic` is false. A failure during paging returns nonzero and leaves the
159
+ document incomplete rather than presenting partial evidence as a successful observation span.
160
+ The doctor command imposes no inventory cap. Account listing and doctor do not contact a provider,
161
+ unseal a grant, mutate authority state, start the service, open the database offline or fall back
162
+ to a legacy credential store.
163
+ If the daemon is unavailable before the first response, it fails with a fixed diagnostic on
164
+ stderr and leaves stdout empty. The older native-store commands documented below still coexist until the coordinated
165
+ major-version migration and removal; these authority routes never use them.
166
+
167
+ `AuthSwitchClient.getUsage(accountId, loginId)` reads OpenAI managed limits or a Claude
168
+ host login's profile and usage through the management socket. The tagged, credential-free
169
+ reading preserves Claude's labelled five-hour, weekly and model windows, plan provenance,
170
+ extra usage facts and explicit partial availability. Missing values never become zero.
171
+ The daemon caches one result in SmartData for a bounded current and stale period; a new
172
+ reading emits one account event without changing the account row revision. Concurrent
173
+ reads and rejected-token refresh share the existing provider and authority owners; usage
174
+ adds no polling timer. Native Claude reads hold the qualified vendor-home lease through
175
+ both status requests and publication, and revalidate the exact credential and config
176
+ source even when serving a cached result. Native grants are never refreshed by the daemon.
177
+ Pending, quarantined, setup-token-without-profile-scope and other unsupported contexts
178
+ return explicit unavailable status without a provider request. Production native home
179
+ adoption still requires the separate verified migration cutover.
180
+
181
+ `AuthSwitchClient.startPreuse()` accepts one client-generated operation UUID and one exact
182
+ OpenAI account/login/purpose. The daemon resolves an access-only grant, optionally reads the
183
+ model catalog, then commits `request_may_have_started` immediately before FlexHarness sends one
184
+ inference with retries disabled. Reusing the UUID with the same request returns its durable receipt; changing
185
+ the target, prompt or requested model is rejected. A crash, cancellation or shutdown after the
186
+ marker remains `outcome_unknown` and is never replayed. Before-marker refusals are `not_sent`.
187
+ Only the request hash, selected model, timing and safe token counts are stored; the prompt,
188
+ generated prose and credentials are absent from persistence and management responses. Account
189
+ removal and targeted reauthentication refuse while that account has an active preuse request.
190
+
60
191
  A trusted in-process daemon host can call `AuthSwitchAuthorityDaemon.startManagedCodex()`
61
192
  for a bound account and workspace. It owns a private Codex 0.155.1 app-server, passes only
62
193
  the authority's current access token through Codex's external-token login, answers its
@@ -67,10 +198,16 @@ Callers supply a private, durable `codexHomeDirectory` for Codex's own configura
67
198
  conversation state. SmartData binds that home immutably to one account and scope, and the
68
199
  daemon records its active process/socket identity before launching it. Admission refuses
69
200
  an existing nonempty native home or a surviving app-server owner; a fresh empty home is
70
- created on first use. Account removal drains its managed Codex processes before the
71
- grant is removed, and targeted reauthentication holds new admissions until the device
72
- operation settles. Only the private socket directory is removed on close. Codex's
201
+ created on first use. Account removal and reauthentication refuse while an in-memory or
202
+ durably recorded managed Codex process for that account may still be running, including
203
+ after daemon restart. A recorded run is cleared only when its exact process is proven
204
+ gone; an uncertain launch remains blocked. Callers must explicitly stop owned processes
205
+ after their work finishes. Once reauthentication starts, it holds new admissions until the
206
+ device operation settles. Only the private socket directory is removed on close. Codex's
73
207
  credential store remains ephemeral and does not read or write `auth.json` in that home.
208
+ Managed-Codex process ownership observation is qualified on Linux and reads exact argument
209
+ boundaries from `/proc`; on other hosts doctor reports the process observation as unknown,
210
+ and authority actions that require survivor proof refuse instead of guessing.
74
211
  Codex 0.155.1 does not persist an unused thread created by `thread/start`; resume applies
75
212
  after Codex has committed a rollout. The existing CLI has not yet been moved to this path.
76
213
 
@@ -83,6 +220,30 @@ remain vendor-owned; a container setup-token grant is a separate grant under the
83
220
  account. The one-time migration and deletion of authswitch's old stores are separate
84
221
  cutover steps.
85
222
 
223
+ The backend-only Claude native handoff journals an exact two-file write and transfers
224
+ ownership between two account grants in one SmartData transaction. An inactive Claude
225
+ grant is refreshed by the daemon only after a durable pre-send marker; pending, native,
226
+ and quarantined grants cannot enter that exchange. A native home and incoming grant
227
+ require a verified import receipt: a migration ledger row that binds the exact source
228
+ digest to one account and grant. A source counts as migrated only after such a verified
229
+ import. Nothing in this package writes that ledger yet, so the default daemon does not
230
+ configure a Claude native home or run this path against real credentials. Handoff
231
+ receipts are queryable after a lost response.
232
+ `runningEffectiveAuth: unsupported_effective_auth` means the native store changed, while
233
+ an already-running Claude process's effective login cannot be proven externally;
234
+ qualified live native-store switching still works.
235
+
236
+ The authority database admits exactly the collections of one registry and prepares each
237
+ at startup. Startup rejects unknown collections, unknown metadata schemas and an existing
238
+ store without its metadata document. Published 7.0.0 metadata is upgraded in place to the
239
+ shape 6.4/6.5 already wrote, while account IDs, sealed grants, enrollments and uncertain
240
+ states remain intact. A 7.0.0 store that a restore had marked closed is refused, not opened.
241
+ Startup also removes the backup digest that 6.4.0 to 7.0.0 stored on native handoff and
242
+ migration ledger rows, and refuses a store holding a record only the retired backup and
243
+ restore system could have written.
244
+ The store is deliberately not backed up: its refresh grants are sealed to this host's TPM,
245
+ so a lost store is recovered by one device re-login per account.
246
+
86
247
  ## Usage
87
248
 
88
249
  Run `authswitch`, `authswitch -i`, or `authswitch --interactive` for an interactive guide. Use the arrow keys and
@@ -975,7 +1136,7 @@ arguments to these commands exit with 2.
975
1136
 
976
1137
  `limits --json` emits `IAccountLimits` and `active --json` emits `IActiveAccounts`:
977
1138
  the same rows as the tables, plus the machine-readable fields the tables condense —
978
- `accountId`, `slotId`, `accountType` (`{ plan, source }` with the adapter's plan
1139
+ `accountId`, `providerAccountId`, `slotId`, `accountType` (`{ plan, source }` with the adapter's plan
979
1140
  name, such as `max` or `pro`, sanitised but not display-formatted, and `source` `live`
980
1141
  or `stored`; null when no type is known or the row names no account; both documents
981
1142
  carry it), the limits rows' `billing` (the account's reported `hasActiveSubscription`,
@@ -986,18 +1147,20 @@ it gave none) and `headline` (true for the window the provider picks for a
986
1147
  single-value summary), the ISO `savedAt` beside the human `savedAgo`, the per-row
987
1148
  `unavailableReason` that the footnotes summarise, and `drift`, which carries the
988
1149
  expected and current account ids, their labels, whether the current one is saved,
989
- the ISO `switchedAt` of the switch that was undone, and the same sentence as `reason`. Both carry `schemaVersion: 1`, the
1150
+ the ISO `switchedAt` of the switch that was undone, and the same sentence as `reason`. `accountId` remains the opaque
1151
+ authswitch identifier used for harness operations; `providerAccountId` is the exact provider-native identifier, or null
1152
+ when the credential reports none. Both documents carry `schemaVersion: 2`, the
990
1153
  shared `generatedAt` snapshot every countdown is relative to, and `complete`, which
991
1154
  is false when any account or harness could not be read.
992
1155
 
993
1156
  The exported `IAccountList` contract contains:
994
1157
 
995
- - `schemaVersion: 2`, `generatedAt` (ISO UTC), and `complete`.
1158
+ - `schemaVersion: 3`, `generatedAt` (ISO UTC), and `complete`.
996
1159
  - `harnesses[]`: `id`, `label`, `loginHint`, `saveUnavailableReason`, `accounts`,
997
1160
  `credentialDrift` (slots whose file no longer holds the account the last switch
998
1161
  wrote), and harness-level `problems` (distinguishing failed discovery from an
999
1162
  empty list).
1000
- - Each account's opaque `id`, `label`, `isActive`, verified `isStashed`, `savedAt`,
1163
+ - Each account's opaque `id`, exact provider-native `providerAccountId` (or null), `label`, `isActive`, verified `isStashed`, `savedAt`,
1001
1164
  `details`, and `status` containing all labelled `facts`, `problems`, and optional
1002
1165
  typed `summary` fields. Missing fields remain omitted, not replaced by zero;
1003
1166
  `summary.usageWindows[]` carries `severity` and `headline` only when the provider
@@ -1009,9 +1172,9 @@ harness internals. A partial result still produces valid JSON and exits with 1;
1009
1172
  successful and empty results exit with 0. `complete` describes lookup success,
1010
1173
  not whether the provider exposes every possible metric.
1011
1174
 
1012
- Version 2 of this JSON schema allows `usageWindows[].resetAt` to be null and adds
1013
- optional account `slotId` fields. Consumers migrating from authswitch 1.x must
1014
- accept schema version 2 and handle null reset timestamps. Scripts should qualify
1175
+ Version 3 adds `providerAccountId` without changing the opaque `id` used by mutation commands. Version 2 allowed
1176
+ `usageWindows[].resetAt` to be null and added optional account `slotId` fields. Consumers must
1177
+ accept schema version 3, preserve the two identifiers' distinct meanings and handle null provider IDs and reset timestamps. Scripts should qualify
1015
1178
  mutation commands with `codex`, `opencode` or `claude`, since all three now register
1016
1179
  by default. Saved Codex credentials retain their existing format.
1017
1180
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/authswitch',
6
- version: '6.5.0',
6
+ version: '8.0.0',
7
7
  description: 'Manage Codex, OpenCode and Claude Code accounts with guided switching and live usage status'
8
8
  }
@@ -1,7 +1,10 @@
1
1
  import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
2
+ import type { IAuthSwitchUsageSnapshot } from './classes.authorityusage.js';
3
+ export type { IAuthSwitchUsageSnapshot } from './classes.authorityusage.js';
2
4
 
3
5
  /** Credential-free account management contract. Safe to import in browser code. */
4
- export type TAuthSwitchAccountHealth = 'ready' | 'refreshing' | 'retry_wait' | 'needs_reauth' | 'native' | 'legacy_native_pending' | 'removed';
6
+ export type TAuthSwitchLoginPurpose = 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native';
7
+ export type TAuthSwitchLoginHealth = 'ready' | 'refreshing' | 'retry_wait' | 'needs_reauth' | 'unverified' | 'pending_handoff' | 'handoff_quarantined' | 'removed';
5
8
 
6
9
  export interface IAuthSwitchAccount {
7
10
  id: string;
@@ -9,14 +12,26 @@ export interface IAuthSwitchAccount {
9
12
  label: string;
10
13
  email: string | null;
11
14
  plan: string | null;
12
- health: TAuthSwitchAccountHealth;
15
+ removed: boolean;
16
+ revision: number;
17
+ statusObservedAt: string;
18
+ }
19
+
20
+ /** One independently owned login. Native ownership does not imply observed health. */
21
+ export interface IAuthSwitchLogin {
22
+ id: string;
23
+ accountId: string;
24
+ providerId: string;
25
+ purpose: TAuthSwitchLoginPurpose;
13
26
  owner: 'daemon' | 'claude_native' | 'legacy_native' | 'none';
27
+ health: TAuthSwitchLoginHealth;
28
+ problem: 'none' | 'provider_unavailable' | 'exchange_uncertain' | 'provider_rejected' | 'native_owner';
14
29
  grantGeneration: number;
15
30
  revision: number;
16
31
  accessExpiresAt: string | null;
17
32
  retryAt: string | null;
18
- problem: 'none' | 'provider_unavailable' | 'exchange_uncertain' | 'provider_rejected' | 'native_owner';
19
33
  statusObservedAt: string;
34
+ capabilities: { canReauthenticate: boolean; canBind: boolean; canPreuse: boolean };
20
35
  }
21
36
 
22
37
  export interface IAuthSwitchBinding {
@@ -29,16 +44,150 @@ export interface IAuthSwitchBinding {
29
44
  }
30
45
 
31
46
  export interface IAuthSwitchSnapshot {
32
- schemaVersion: 1;
47
+ schemaVersion: 2;
33
48
  epoch: string;
34
49
  revision: number;
35
50
  generatedAt: string;
36
51
  accounts: IAuthSwitchAccount[];
52
+ logins: IAuthSwitchLogin[];
37
53
  bindings: IAuthSwitchBinding[];
38
54
  nextAccountCursor: string | null;
55
+ nextLoginCursor: string | null;
39
56
  nextBindingCursor: string | null;
40
57
  }
41
58
 
59
+ /** Persisted account evidence. No provider request is made while collecting diagnostics. */
60
+ export interface IAuthSwitchDoctorAccountEvidence {
61
+ id: string;
62
+ providerId: string;
63
+ removed: boolean;
64
+ statusObservedAt: string;
65
+ evidence: 'stored';
66
+ liveCheck: 'not_checked';
67
+ }
68
+
69
+ /** Persisted login evidence. Stored readiness is not a live provider-health claim. */
70
+ export interface IAuthSwitchDoctorLoginEvidence {
71
+ id: string;
72
+ accountId: string;
73
+ providerId: string;
74
+ purpose: TAuthSwitchLoginPurpose;
75
+ owner: 'daemon' | 'claude_native' | 'legacy_native' | 'none';
76
+ state: 'ready' | 'exchange_may_have_been_sent' | 'retry_wait' | 'needs_reauth' | 'native'
77
+ | 'legacy_native_pending' | 'handoff_pending' | 'handoff_quarantined' | 'removed';
78
+ problem: 'none' | 'provider_unavailable' | 'exchange_uncertain' | 'provider_rejected' | 'native_owner';
79
+ grantGeneration: number;
80
+ authorizationGeneration: number;
81
+ accessExpiresAt: string | null;
82
+ retryAt: string | null;
83
+ statusObservedAt: string;
84
+ evidence: 'stored';
85
+ liveCheck: 'not_checked';
86
+ }
87
+
88
+ export interface IAuthSwitchDoctorManagedCodex {
89
+ homeId: string;
90
+ accountId: string;
91
+ loginId: string;
92
+ scopeId: string;
93
+ health: 'healthy' | 'degraded' | 'unknown';
94
+ durable: {
95
+ state: 'idle' | 'launch_prepared' | 'registered';
96
+ runId: string | null;
97
+ pid: number | null;
98
+ startedAt: string | null;
99
+ };
100
+ daemon: {
101
+ state: 'untracked' | 'starting' | 'ready' | 'stopping' | 'degraded';
102
+ assignment: 'untracked' | 'matches' | 'mismatch';
103
+ };
104
+ process: {
105
+ state: 'not_expected' | 'absent' | 'observed' | 'unknown';
106
+ identity: 'not_applicable' | 'unregistered' | 'matches' | 'mismatch' | 'unknown';
107
+ pid: number | null;
108
+ startedAt: string | null;
109
+ observedAt: string;
110
+ problem: 'none' | 'inspection_failed' | 'multiple_claimants';
111
+ };
112
+ }
113
+
114
+ export interface IAuthSwitchDoctorClaudeHome {
115
+ id: string;
116
+ activeAccountId: string;
117
+ activeLoginId: string;
118
+ pendingOperationId: string | null;
119
+ status: 'ready' | 'quarantined';
120
+ revision: number;
121
+ createdAt: string;
122
+ }
123
+
124
+ export interface IAuthSwitchDoctorClaudeHandoff {
125
+ id: string;
126
+ homeId: string;
127
+ outgoingAccountId: string;
128
+ outgoingLoginId: string;
129
+ incomingAccountId: string;
130
+ incomingLoginId: string;
131
+ phase: 'reserved' | 'prepared' | 'committed' | 'aborted' | 'quarantined';
132
+ problem: 'none' | 'native_uncertain' | 'foreign_or_torn' | 'unsupported_effective_auth' | 'database_uncertain';
133
+ runningEffectiveAuth: 'no_scoped_sessions' | 'unsupported_effective_auth' | null;
134
+ startedAt: string;
135
+ updatedAt: string;
136
+ revision: number;
137
+ }
138
+
139
+ export interface IAuthSwitchDoctorNativeHandoff {
140
+ id: string;
141
+ accountId: string;
142
+ loginId: string;
143
+ nativeHomeId: string;
144
+ direction: 'to_native' | 'to_daemon';
145
+ phase: 'prepared' | 'source_stopped' | 'target_written' | 'verified' | 'committed' | 'needs_reauth' | 'aborted';
146
+ operationId: string;
147
+ revision: number;
148
+ statusObservedAt: string;
149
+ }
150
+
151
+ export interface IAuthSwitchDoctorMigration {
152
+ id: string;
153
+ sourceKind: 'authswitch_stash' | 'authswitch_backup' | 'agl_flex' | 'codex_native' | 'opencode_native' | 'claude_native';
154
+ status: 'pending_native_owner' | 'prepared' | 'verified' | 'complete' | 'quarantined';
155
+ accountId: string | null;
156
+ loginId: string | null;
157
+ revision: number;
158
+ statusObservedAt: string;
159
+ }
160
+
161
+ /** One independently observed diagnostic page. Separate pages are not one atomic global snapshot. */
162
+ export interface IAuthSwitchDoctorPage {
163
+ observedAt: string;
164
+ daemon: {
165
+ state: 'ready' | 'closing';
166
+ claudeNativeAuthority: 'configured' | 'not_configured';
167
+ observedAt: string;
168
+ };
169
+ database: {
170
+ state: 'available';
171
+ observedAt: string;
172
+ epoch: string;
173
+ revision: number;
174
+ };
175
+ accounts: IAuthSwitchDoctorAccountEvidence[];
176
+ logins: IAuthSwitchDoctorLoginEvidence[];
177
+ managedCodex: IAuthSwitchDoctorManagedCodex[];
178
+ claudeHomes: IAuthSwitchDoctorClaudeHome[];
179
+ claudeHandoffs: IAuthSwitchDoctorClaudeHandoff[];
180
+ nativeHandoffs: IAuthSwitchDoctorNativeHandoff[];
181
+ migrations: IAuthSwitchDoctorMigration[];
182
+ nextAccountCursor: string | null;
183
+ nextLoginCursor: string | null;
184
+ nextCodexHomeCursor: string | null;
185
+ nextClaudeHomeCursor: string | null;
186
+ nextClaudeHandoffCursor: string | null;
187
+ nextNativeHandoffCursor: string | null;
188
+ nextMigrationCursor: string | null;
189
+ }
190
+
42
191
  export interface IAuthSwitchAccountEvent {
43
192
  epoch: string;
44
193
  revision: number;
@@ -52,18 +201,97 @@ export type TAuthSwitchLoginPrompt =
52
201
 
53
202
  export interface IAuthSwitchOperation {
54
203
  id: string;
55
- state: 'pending' | 'complete' | 'failed' | 'cancelled';
204
+ kind: 'add_openai' | 'reauth_openai';
205
+ purpose: 'openai_managed';
206
+ targetAccountId: string | null;
207
+ targetLoginId: string | null;
208
+ state: 'starting' | 'pending' | 'committing' | 'complete' | 'failed' | 'cancelled' | 'interrupted' | 'outcome_unknown';
56
209
  prompt: TAuthSwitchLoginPrompt | null;
57
- account: IAuthSwitchAccount | null;
210
+ result: { accountId: string; loginId: string; accountRevision: number; loginGeneration: number } | null;
58
211
  error: string | null;
212
+ createdAt: string;
213
+ updatedAt: string;
214
+ finishedAt: string | null;
215
+ revision: number;
216
+ }
217
+
218
+ /** Credential-free receipt for one exact, never-replayed quota-consuming request. */
219
+ export interface IAuthSwitchPreuseOperation {
220
+ id: string;
221
+ accountId: string;
222
+ loginId: string;
223
+ purpose: 'openai_managed';
224
+ requestedModel: string | null;
225
+ model: string | null;
226
+ state: 'preparing' | 'request_may_have_started' | 'complete' | 'not_sent' | 'outcome_unknown';
227
+ result: { inputTokens: number | null; outputTokens: number | null; totalTokens: number | null } | null;
228
+ problem: 'none' | 'target_unavailable' | 'access_unavailable' | 'catalog_unavailable'
229
+ | 'account_busy' | 'cancelled' | 'authority_closing' | 'interrupted'
230
+ | 'request_uncertain';
231
+ createdAt: string;
232
+ updatedAt: string;
233
+ finishedAt: string | null;
234
+ revision: number;
235
+ }
236
+
237
+ /** Credential-free native file ownership result. It makes no claim about a running session's active request. */
238
+ export interface IAuthSwitchClaudeNativeHandoff {
239
+ id: string;
240
+ homeId: string;
241
+ outgoingAccountId: string;
242
+ incomingAccountId: string;
243
+ phase: 'reserved' | 'prepared' | 'committed' | 'aborted' | 'quarantined';
244
+ problem: 'none' | 'native_uncertain' | 'foreign_or_torn' | 'unsupported_effective_auth' | 'database_uncertain';
245
+ runningEffectiveAuth: 'no_scoped_sessions' | 'unsupported_effective_auth' | null;
246
+ updatedAt: string;
247
+ }
248
+
249
+ export interface IReq_AuthSwitchSwitchClaudeNative extends ITypedRequest {
250
+ method: 'authswitch.authority.claude.switch';
251
+ request: { accountId: string; loginId: string; purpose: 'claude_host_native' };
252
+ response: { handoff: IAuthSwitchClaudeNativeHandoff };
253
+ }
254
+
255
+ export interface IReq_AuthSwitchClaudeNativeHandoff extends ITypedRequest {
256
+ method: 'authswitch.authority.claude.handoff';
257
+ request: { operationId: string };
258
+ response: { handoff: IAuthSwitchClaudeNativeHandoff };
259
+ }
260
+
261
+ export interface IReq_AuthSwitchClaudeNativeHandoffs extends ITypedRequest {
262
+ method: 'authswitch.authority.claude.handoffs';
263
+ request: { after?: string; limit?: number };
264
+ response: { handoffs: IAuthSwitchClaudeNativeHandoff[]; nextCursor: string | null };
59
265
  }
60
266
 
61
267
  export interface IReq_AuthSwitchSnapshot extends ITypedRequest {
62
268
  method: 'authswitch.authority.snapshot';
63
- request: { accountAfter?: string; bindingAfter?: string; limit?: number };
269
+ request: { accountAfter?: string; loginAfter?: string; bindingAfter?: string; limit?: number };
64
270
  response: { snapshot: IAuthSwitchSnapshot };
65
271
  }
66
272
 
273
+ export interface IReq_AuthSwitchDoctor extends ITypedRequest {
274
+ method: 'authswitch.authority.doctor';
275
+ request: {
276
+ accountAfter?: string;
277
+ loginAfter?: string;
278
+ codexHomeAfter?: string;
279
+ claudeHomeAfter?: string;
280
+ claudeHandoffAfter?: string;
281
+ nativeHandoffAfter?: string;
282
+ migrationAfter?: string;
283
+ limit?: number;
284
+ };
285
+ response: { page: IAuthSwitchDoctorPage };
286
+ }
287
+
288
+ /** Management-only usage read. The DTO contains no access or refresh credential. */
289
+ export interface IReq_AuthSwitchUsage extends ITypedRequest {
290
+ method: 'authswitch.authority.usage';
291
+ request: { accountId: string; loginId: string; force?: boolean };
292
+ response: { usage: IAuthSwitchUsageSnapshot };
293
+ }
294
+
67
295
  export interface IReq_AuthSwitchEvents extends ITypedRequest {
68
296
  method: 'authswitch.authority.events';
69
297
  request: { epoch: string; afterRevision: number; waitMs: number };
@@ -72,16 +300,22 @@ export interface IReq_AuthSwitchEvents extends ITypedRequest {
72
300
 
73
301
  export interface IReq_AuthSwitchBeginAdd extends ITypedRequest {
74
302
  method: 'authswitch.authority.add';
75
- request: { providerId: 'openai'; flow: 'device' };
303
+ request: { operationId: string; providerId: 'openai'; flow: 'device' };
76
304
  response: { operation: IAuthSwitchOperation };
77
305
  }
78
306
 
79
307
  export interface IReq_AuthSwitchBeginReauth extends ITypedRequest {
80
308
  method: 'authswitch.authority.reauth';
81
- request: { accountId: string; flow: 'device' };
309
+ request: { operationId: string; accountId: string; loginId: string; purpose: 'openai_managed'; flow: 'device' };
82
310
  response: { operation: IAuthSwitchOperation };
83
311
  }
84
312
 
313
+ export interface IReq_AuthSwitchListOperations extends ITypedRequest {
314
+ method: 'authswitch.authority.operations';
315
+ request: { after?: string; limit?: number };
316
+ response: { operations: IAuthSwitchOperation[]; nextCursor: string | null };
317
+ }
318
+
85
319
  export interface IReq_AuthSwitchGetOperation extends ITypedRequest {
86
320
  method: 'authswitch.authority.operation';
87
321
  request: { operationId: string };
@@ -94,6 +328,25 @@ export interface IReq_AuthSwitchCancelOperation extends ITypedRequest {
94
328
  response: { operation: IAuthSwitchOperation };
95
329
  }
96
330
 
331
+ export interface IReq_AuthSwitchStartPreuse extends ITypedRequest {
332
+ method: 'authswitch.authority.preuse.start';
333
+ request: { operationId: string; accountId: string; loginId: string; purpose: 'openai_managed';
334
+ prompt: string; model?: string };
335
+ response: { operation: IAuthSwitchPreuseOperation };
336
+ }
337
+
338
+ export interface IReq_AuthSwitchGetPreuse extends ITypedRequest {
339
+ method: 'authswitch.authority.preuse.operation';
340
+ request: { operationId: string };
341
+ response: { operation: IAuthSwitchPreuseOperation };
342
+ }
343
+
344
+ export interface IReq_AuthSwitchCancelPreuse extends ITypedRequest {
345
+ method: 'authswitch.authority.preuse.cancel';
346
+ request: { operationId: string };
347
+ response: { operation: IAuthSwitchPreuseOperation };
348
+ }
349
+
97
350
  export interface IReq_AuthSwitchRenameAccount extends ITypedRequest {
98
351
  method: 'authswitch.authority.rename';
99
352
  request: { accountId: string; expectedRevision: number; label: string };
@@ -6,6 +6,8 @@ export interface IReq_AuthSwitchBindAccount extends ITypedRequest {
6
6
  method: 'authswitch.authority.bind';
7
7
  request: {
8
8
  accountId: string;
9
+ loginId: string;
10
+ purpose: 'openai_managed';
9
11
  runtime: IAuthSwitchBinding['runtime'];
10
12
  scopeId: string;
11
13
  incarnationId: string;
@@ -31,3 +33,15 @@ export interface IReq_AuthSwitchResolveAccess extends ITypedRequest {
31
33
  grantGeneration: number;
32
34
  };
33
35
  }
36
+
37
+ /** Backend-only release for one exact external runtime capability. */
38
+ export interface IReq_AuthSwitchReleaseBinding extends ITypedRequest {
39
+ method: 'authswitch.authority.release';
40
+ request: {
41
+ bindingId: string;
42
+ capability: string;
43
+ };
44
+ response: {
45
+ state: 'released' | 'inactive';
46
+ };
47
+ }
@@ -29,7 +29,8 @@ export const readAccountList = async (harnessesArg: IAuthHarness[]): Promise<IAc
29
29
  result.saveUnavailableReason = state.saveUnavailableReason;
30
30
  result.credentialDrift = state.credentialDrift ?? [];
31
31
  result.accounts = rows.map(({ account, status }) => ({
32
- id: account.id, label: account.label, isActive: account.isActive, isStashed: account.isStashed,
32
+ id: account.id, providerAccountId: account.providerAccountId, label: account.label,
33
+ isActive: account.isActive, isStashed: account.isStashed,
33
34
  savedAt: account.savedAt, details: [...account.details], ...(account.slotId === undefined ? {} : { slotId: account.slotId }),
34
35
  status: publicStatus(status!),
35
36
  }));
@@ -39,7 +40,7 @@ export const readAccountList = async (harnessesArg: IAuthHarness[]): Promise<IAc
39
40
  harnesses.push(result);
40
41
  }
41
42
  return {
42
- schemaVersion: 2, generatedAt: new Date().toISOString(),
43
+ schemaVersion: 3, generatedAt: new Date().toISOString(),
43
44
  complete: harnesses.every(harness => !harness.problems.length && harness.accounts.every(account => !account.status.problems.length)),
44
45
  harnesses,
45
46
  };