@modelprofile.com/authswitch 6.5.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +302 -6
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-runtime-contract.d.ts +13 -0
  5. package/dist_ts/authorityrehearsal.child.d.ts +1 -0
  6. package/dist_ts/authorityrehearsal.child.js +395 -0
  7. package/dist_ts/classes.accountlist.js +4 -3
  8. package/dist_ts/classes.authoritybackup.d.ts +134 -0
  9. package/dist_ts/classes.authoritybackup.js +769 -0
  10. package/dist_ts/classes.authoritybroker.d.ts +25 -5
  11. package/dist_ts/classes.authoritybroker.js +392 -123
  12. package/dist_ts/classes.authoritycli.d.ts +24 -0
  13. package/dist_ts/classes.authoritycli.js +675 -0
  14. package/dist_ts/classes.authorityclient.d.ts +19 -7
  15. package/dist_ts/classes.authorityclient.js +51 -14
  16. package/dist_ts/classes.authoritydaemon.d.ts +28 -1
  17. package/dist_ts/classes.authoritydaemon.js +482 -36
  18. package/dist_ts/classes.authoritydatabase.d.ts +175 -10
  19. package/dist_ts/classes.authoritydatabase.js +1100 -52
  20. package/dist_ts/classes.authoritymodels.d.ts +223 -1
  21. package/dist_ts/classes.authoritymodels.js +798 -9
  22. package/dist_ts/classes.authoritypreuse.d.ts +47 -0
  23. package/dist_ts/classes.authoritypreuse.js +278 -0
  24. package/dist_ts/classes.authorityregistry.d.ts +24 -0
  25. package/dist_ts/classes.authorityregistry.js +88 -0
  26. package/dist_ts/classes.authorityrehearsal.d.ts +127 -0
  27. package/dist_ts/classes.authorityrehearsal.js +632 -0
  28. package/dist_ts/classes.authorityusage.d.ts +160 -0
  29. package/dist_ts/classes.authorityusage.js +429 -0
  30. package/dist_ts/classes.claudeauthority.d.ts +66 -0
  31. package/dist_ts/classes.claudeauthority.js +532 -0
  32. package/dist_ts/classes.claudecodeharness.js +3 -2
  33. package/dist_ts/classes.claudecodelocks.d.ts +13 -0
  34. package/dist_ts/classes.claudecodelocks.js +75 -3
  35. package/dist_ts/classes.claudenative.d.ts +137 -0
  36. package/dist_ts/classes.claudenative.js +731 -0
  37. package/dist_ts/classes.claudestatus.d.ts +9 -0
  38. package/dist_ts/classes.claudestatus.js +26 -9
  39. package/dist_ts/classes.claudetokenrefresh.d.ts +15 -1
  40. package/dist_ts/classes.claudetokenrefresh.js +74 -21
  41. package/dist_ts/classes.codexharness.js +4 -2
  42. package/dist_ts/classes.codexmanaged.d.ts +11 -0
  43. package/dist_ts/classes.codexmanaged.js +75 -14
  44. package/dist_ts/classes.codexpreuse.d.ts +8 -0
  45. package/dist_ts/classes.codexpreuse.js +24 -6
  46. package/dist_ts/classes.fileharness.d.ts +2 -0
  47. package/dist_ts/classes.fileharness.js +6 -4
  48. package/dist_ts/classes.limits.js +12 -7
  49. package/dist_ts/classes.opencodeharness.js +2 -2
  50. package/dist_ts/claudehttp.d.ts +10 -0
  51. package/dist_ts/claudehttp.js +29 -3
  52. package/dist_ts/index.d.ts +1 -0
  53. package/dist_ts/index.js +7 -1
  54. package/dist_ts/interfaces.harness.d.ts +6 -0
  55. package/dist_ts/interfaces.list.d.ts +9 -3
  56. package/dist_ts/plugins.d.ts +5 -2
  57. package/dist_ts/plugins.js +7 -3
  58. package/dist_ts/ts_migration/0001_authority_meta.d.ts +3 -0
  59. package/dist_ts/ts_migration/0001_authority_meta.js +32 -0
  60. package/dist_ts/ts_migration/index.d.ts +3 -0
  61. package/dist_ts/ts_migration/index.js +6 -0
  62. package/package.json +4 -1
  63. package/readme.md +181 -14
  64. package/ts/00_commitinfo_data.ts +1 -1
  65. package/ts/authority-contract.ts +264 -9
  66. package/ts/authority-runtime-contract.ts +14 -0
  67. package/ts/authorityrehearsal.child.ts +330 -0
  68. package/ts/classes.accountlist.ts +3 -2
  69. package/ts/classes.authoritybackup.ts +780 -0
  70. package/ts/classes.authoritybroker.ts +389 -121
  71. package/ts/classes.authoritycli.ts +719 -0
  72. package/ts/classes.authorityclient.ts +75 -18
  73. package/ts/classes.authoritydaemon.ts +449 -36
  74. package/ts/classes.authoritydatabase.ts +1147 -55
  75. package/ts/classes.authoritymodels.ts +541 -6
  76. package/ts/classes.authoritypreuse.ts +297 -0
  77. package/ts/classes.authorityregistry.ts +118 -0
  78. package/ts/classes.authorityrehearsal.ts +625 -0
  79. package/ts/classes.authorityusage.ts +527 -0
  80. package/ts/classes.claudeauthority.ts +541 -0
  81. package/ts/classes.claudecodeharness.ts +2 -1
  82. package/ts/classes.claudecodelocks.ts +70 -1
  83. package/ts/classes.claudenative.ts +816 -0
  84. package/ts/classes.claudestatus.ts +38 -9
  85. package/ts/classes.claudetokenrefresh.ts +82 -19
  86. package/ts/classes.codexharness.ts +3 -1
  87. package/ts/classes.codexmanaged.ts +75 -12
  88. package/ts/classes.codexpreuse.ts +33 -5
  89. package/ts/classes.fileharness.ts +7 -3
  90. package/ts/classes.limits.ts +11 -6
  91. package/ts/classes.opencodeharness.ts +1 -1
  92. package/ts/claudehttp.ts +31 -2
  93. package/ts/index.ts +6 -0
  94. package/ts/interfaces.harness.ts +6 -0
  95. package/ts/interfaces.list.ts +9 -3
  96. package/ts/plugins.ts +7 -2
  97. package/ts/ts_migration/0001_authority_meta.ts +33 -0
  98. package/ts/ts_migration/index.ts +7 -0
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,113 @@ 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 database and admission state
96
+ unknown 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 and backup 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
+ Historical restore marks every unresolved preuse receipt `outcome_unknown`, including snapshots
191
+ captured before the marker, because the original authority may have sent after snapshot capture.
192
+
60
193
  A trusted in-process daemon host can call `AuthSwitchAuthorityDaemon.startManagedCodex()`
61
194
  for a bound account and workspace. It owns a private Codex 0.155.1 app-server, passes only
62
195
  the authority's current access token through Codex's external-token login, answers its
@@ -67,10 +200,16 @@ Callers supply a private, durable `codexHomeDirectory` for Codex's own configura
67
200
  conversation state. SmartData binds that home immutably to one account and scope, and the
68
201
  daemon records its active process/socket identity before launching it. Admission refuses
69
202
  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
203
+ created on first use. Account removal and reauthentication refuse while an in-memory or
204
+ durably recorded managed Codex process for that account may still be running, including
205
+ after daemon restart. A recorded run is cleared only when its exact process is proven
206
+ gone; an uncertain launch remains blocked. Callers must explicitly stop owned processes
207
+ after their work finishes. Once reauthentication starts, it holds new admissions until the
208
+ device operation settles. Only the private socket directory is removed on close. Codex's
73
209
  credential store remains ephemeral and does not read or write `auth.json` in that home.
210
+ Managed-Codex process ownership observation is qualified on Linux and reads exact argument
211
+ boundaries from `/proc`; on other hosts doctor reports the process observation as unknown,
212
+ and authority actions that require survivor proof refuse instead of guessing.
74
213
  Codex 0.155.1 does not persist an unused thread created by `thread/start`; resume applies
75
214
  after Codex has committed a rollout. The existing CLI has not yet been moved to this path.
76
215
 
@@ -83,6 +222,32 @@ remain vendor-owned; a container setup-token grant is a separate grant under the
83
222
  account. The one-time migration and deletion of authswitch's old stores are separate
84
223
  cutover steps.
85
224
 
225
+ The backend-only Claude native handoff journals an exact two-file write and transfers
226
+ ownership between two account grants in one SmartData transaction. An inactive Claude
227
+ grant is refreshed by the daemon only after a durable pre-send marker; pending, native,
228
+ and quarantined grants cannot enter that exchange. A native home and incoming grant
229
+ require source-mapped migration receipts with backup metadata. The ledger field does
230
+ not itself verify a SmartBucket backup, so the default daemon does not configure a
231
+ Claude native home or run this path against real credentials. One-time migration and
232
+ backup verification must precede production activation. Handoff receipts are queryable
233
+ after a lost response. `runningEffectiveAuth: unsupported_effective_auth` means the
234
+ native store changed, while an already-running Claude process's effective login cannot
235
+ be proven externally; qualified live native-store switching still works.
236
+
237
+ The authority database uses one versioned collection registry for startup preparation,
238
+ ordered backup capture and exact restore readback. Startup rejects unknown collections,
239
+ unknown metadata schemas and an incomplete restored database. Published 6.4/6.5 metadata
240
+ is upgraded in place while account IDs, sealed grants, enrollments and uncertain states
241
+ remain intact. A restored database keeps a durable closed-admission marker across restart;
242
+ the daemon refuses provider and runtime work until a separate ownership-reconciliation
243
+ step can prove it safe to open. Backup capture pins its paged records in one SmartData
244
+ transaction and stages each retry anew before any SmartBucket publication. The closed
245
+ restore rehearsal runs in a separate package-owned child, keeps its temporary source and
246
+ record stream encrypted, restores metadata closed before any other record, then stops and
247
+ reopens the same disposable database for exact stored readback. A receipt is issued only
248
+ after the child has closed and its temporary root has been removed. This rehearsal does
249
+ not perform a production restore or authorize a new authority instance to take ownership.
250
+
86
251
  ## Usage
87
252
 
88
253
  Run `authswitch`, `authswitch -i`, or `authswitch --interactive` for an interactive guide. Use the arrow keys and
@@ -975,7 +1140,7 @@ arguments to these commands exit with 2.
975
1140
 
976
1141
  `limits --json` emits `IAccountLimits` and `active --json` emits `IActiveAccounts`:
977
1142
  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
1143
+ `accountId`, `providerAccountId`, `slotId`, `accountType` (`{ plan, source }` with the adapter's plan
979
1144
  name, such as `max` or `pro`, sanitised but not display-formatted, and `source` `live`
980
1145
  or `stored`; null when no type is known or the row names no account; both documents
981
1146
  carry it), the limits rows' `billing` (the account's reported `hasActiveSubscription`,
@@ -986,18 +1151,20 @@ it gave none) and `headline` (true for the window the provider picks for a
986
1151
  single-value summary), the ISO `savedAt` beside the human `savedAgo`, the per-row
987
1152
  `unavailableReason` that the footnotes summarise, and `drift`, which carries the
988
1153
  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
1154
+ the ISO `switchedAt` of the switch that was undone, and the same sentence as `reason`. `accountId` remains the opaque
1155
+ authswitch identifier used for harness operations; `providerAccountId` is the exact provider-native identifier, or null
1156
+ when the credential reports none. Both documents carry `schemaVersion: 2`, the
990
1157
  shared `generatedAt` snapshot every countdown is relative to, and `complete`, which
991
1158
  is false when any account or harness could not be read.
992
1159
 
993
1160
  The exported `IAccountList` contract contains:
994
1161
 
995
- - `schemaVersion: 2`, `generatedAt` (ISO UTC), and `complete`.
1162
+ - `schemaVersion: 3`, `generatedAt` (ISO UTC), and `complete`.
996
1163
  - `harnesses[]`: `id`, `label`, `loginHint`, `saveUnavailableReason`, `accounts`,
997
1164
  `credentialDrift` (slots whose file no longer holds the account the last switch
998
1165
  wrote), and harness-level `problems` (distinguishing failed discovery from an
999
1166
  empty list).
1000
- - Each account's opaque `id`, `label`, `isActive`, verified `isStashed`, `savedAt`,
1167
+ - Each account's opaque `id`, exact provider-native `providerAccountId` (or null), `label`, `isActive`, verified `isStashed`, `savedAt`,
1001
1168
  `details`, and `status` containing all labelled `facts`, `problems`, and optional
1002
1169
  typed `summary` fields. Missing fields remain omitted, not replaced by zero;
1003
1170
  `summary.usageWindows[]` carries `severity` and `headline` only when the provider
@@ -1009,9 +1176,9 @@ harness internals. A partial result still produces valid JSON and exits with 1;
1009
1176
  successful and empty results exit with 0. `complete` describes lookup success,
1010
1177
  not whether the provider exposes every possible metric.
1011
1178
 
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
1179
+ Version 3 adds `providerAccountId` without changing the opaque `id` used by mutation commands. Version 2 allowed
1180
+ `usageWindows[].resetAt` to be null and added optional account `slotId` fields. Consumers must
1181
+ accept schema version 3, preserve the two identifiers' distinct meanings and handle null provider IDs and reset timestamps. Scripts should qualify
1015
1182
  mutation commands with `codex`, `opencode` or `claude`, since all three now register
1016
1183
  by default. Saved Codex credentials retain their existing format.
1017
1184
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/authswitch',
6
- version: '6.5.0',
6
+ version: '7.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,152 @@ 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
+ schemaVersion: 2;
173
+ admission: 'open' | 'closed_restored';
174
+ epoch: string;
175
+ revision: number;
176
+ };
177
+ accounts: IAuthSwitchDoctorAccountEvidence[];
178
+ logins: IAuthSwitchDoctorLoginEvidence[];
179
+ managedCodex: IAuthSwitchDoctorManagedCodex[];
180
+ claudeHomes: IAuthSwitchDoctorClaudeHome[];
181
+ claudeHandoffs: IAuthSwitchDoctorClaudeHandoff[];
182
+ nativeHandoffs: IAuthSwitchDoctorNativeHandoff[];
183
+ migrations: IAuthSwitchDoctorMigration[];
184
+ nextAccountCursor: string | null;
185
+ nextLoginCursor: string | null;
186
+ nextCodexHomeCursor: string | null;
187
+ nextClaudeHomeCursor: string | null;
188
+ nextClaudeHandoffCursor: string | null;
189
+ nextNativeHandoffCursor: string | null;
190
+ nextMigrationCursor: string | null;
191
+ }
192
+
42
193
  export interface IAuthSwitchAccountEvent {
43
194
  epoch: string;
44
195
  revision: number;
@@ -52,18 +203,97 @@ export type TAuthSwitchLoginPrompt =
52
203
 
53
204
  export interface IAuthSwitchOperation {
54
205
  id: string;
55
- state: 'pending' | 'complete' | 'failed' | 'cancelled';
206
+ kind: 'add_openai' | 'reauth_openai';
207
+ purpose: 'openai_managed';
208
+ targetAccountId: string | null;
209
+ targetLoginId: string | null;
210
+ state: 'starting' | 'pending' | 'committing' | 'complete' | 'failed' | 'cancelled' | 'interrupted' | 'outcome_unknown';
56
211
  prompt: TAuthSwitchLoginPrompt | null;
57
- account: IAuthSwitchAccount | null;
212
+ result: { accountId: string; loginId: string; accountRevision: number; loginGeneration: number } | null;
58
213
  error: string | null;
214
+ createdAt: string;
215
+ updatedAt: string;
216
+ finishedAt: string | null;
217
+ revision: number;
218
+ }
219
+
220
+ /** Credential-free receipt for one exact, never-replayed quota-consuming request. */
221
+ export interface IAuthSwitchPreuseOperation {
222
+ id: string;
223
+ accountId: string;
224
+ loginId: string;
225
+ purpose: 'openai_managed';
226
+ requestedModel: string | null;
227
+ model: string | null;
228
+ state: 'preparing' | 'request_may_have_started' | 'complete' | 'not_sent' | 'outcome_unknown';
229
+ result: { inputTokens: number | null; outputTokens: number | null; totalTokens: number | null } | null;
230
+ problem: 'none' | 'target_unavailable' | 'access_unavailable' | 'catalog_unavailable'
231
+ | 'account_busy' | 'cancelled' | 'authority_closing' | 'interrupted'
232
+ | 'request_uncertain' | 'restored_history_uncertain';
233
+ createdAt: string;
234
+ updatedAt: string;
235
+ finishedAt: string | null;
236
+ revision: number;
237
+ }
238
+
239
+ /** Credential-free native file ownership result. It makes no claim about a running session's active request. */
240
+ export interface IAuthSwitchClaudeNativeHandoff {
241
+ id: string;
242
+ homeId: string;
243
+ outgoingAccountId: string;
244
+ incomingAccountId: string;
245
+ phase: 'reserved' | 'prepared' | 'committed' | 'aborted' | 'quarantined';
246
+ problem: 'none' | 'native_uncertain' | 'foreign_or_torn' | 'unsupported_effective_auth' | 'database_uncertain';
247
+ runningEffectiveAuth: 'no_scoped_sessions' | 'unsupported_effective_auth' | null;
248
+ updatedAt: string;
249
+ }
250
+
251
+ export interface IReq_AuthSwitchSwitchClaudeNative extends ITypedRequest {
252
+ method: 'authswitch.authority.claude.switch';
253
+ request: { accountId: string; loginId: string; purpose: 'claude_host_native' };
254
+ response: { handoff: IAuthSwitchClaudeNativeHandoff };
255
+ }
256
+
257
+ export interface IReq_AuthSwitchClaudeNativeHandoff extends ITypedRequest {
258
+ method: 'authswitch.authority.claude.handoff';
259
+ request: { operationId: string };
260
+ response: { handoff: IAuthSwitchClaudeNativeHandoff };
261
+ }
262
+
263
+ export interface IReq_AuthSwitchClaudeNativeHandoffs extends ITypedRequest {
264
+ method: 'authswitch.authority.claude.handoffs';
265
+ request: { after?: string; limit?: number };
266
+ response: { handoffs: IAuthSwitchClaudeNativeHandoff[]; nextCursor: string | null };
59
267
  }
60
268
 
61
269
  export interface IReq_AuthSwitchSnapshot extends ITypedRequest {
62
270
  method: 'authswitch.authority.snapshot';
63
- request: { accountAfter?: string; bindingAfter?: string; limit?: number };
271
+ request: { accountAfter?: string; loginAfter?: string; bindingAfter?: string; limit?: number };
64
272
  response: { snapshot: IAuthSwitchSnapshot };
65
273
  }
66
274
 
275
+ export interface IReq_AuthSwitchDoctor extends ITypedRequest {
276
+ method: 'authswitch.authority.doctor';
277
+ request: {
278
+ accountAfter?: string;
279
+ loginAfter?: string;
280
+ codexHomeAfter?: string;
281
+ claudeHomeAfter?: string;
282
+ claudeHandoffAfter?: string;
283
+ nativeHandoffAfter?: string;
284
+ migrationAfter?: string;
285
+ limit?: number;
286
+ };
287
+ response: { page: IAuthSwitchDoctorPage };
288
+ }
289
+
290
+ /** Management-only usage read. The DTO contains no access or refresh credential. */
291
+ export interface IReq_AuthSwitchUsage extends ITypedRequest {
292
+ method: 'authswitch.authority.usage';
293
+ request: { accountId: string; loginId: string; force?: boolean };
294
+ response: { usage: IAuthSwitchUsageSnapshot };
295
+ }
296
+
67
297
  export interface IReq_AuthSwitchEvents extends ITypedRequest {
68
298
  method: 'authswitch.authority.events';
69
299
  request: { epoch: string; afterRevision: number; waitMs: number };
@@ -72,16 +302,22 @@ export interface IReq_AuthSwitchEvents extends ITypedRequest {
72
302
 
73
303
  export interface IReq_AuthSwitchBeginAdd extends ITypedRequest {
74
304
  method: 'authswitch.authority.add';
75
- request: { providerId: 'openai'; flow: 'device' };
305
+ request: { operationId: string; providerId: 'openai'; flow: 'device' };
76
306
  response: { operation: IAuthSwitchOperation };
77
307
  }
78
308
 
79
309
  export interface IReq_AuthSwitchBeginReauth extends ITypedRequest {
80
310
  method: 'authswitch.authority.reauth';
81
- request: { accountId: string; flow: 'device' };
311
+ request: { operationId: string; accountId: string; loginId: string; purpose: 'openai_managed'; flow: 'device' };
82
312
  response: { operation: IAuthSwitchOperation };
83
313
  }
84
314
 
315
+ export interface IReq_AuthSwitchListOperations extends ITypedRequest {
316
+ method: 'authswitch.authority.operations';
317
+ request: { after?: string; limit?: number };
318
+ response: { operations: IAuthSwitchOperation[]; nextCursor: string | null };
319
+ }
320
+
85
321
  export interface IReq_AuthSwitchGetOperation extends ITypedRequest {
86
322
  method: 'authswitch.authority.operation';
87
323
  request: { operationId: string };
@@ -94,6 +330,25 @@ export interface IReq_AuthSwitchCancelOperation extends ITypedRequest {
94
330
  response: { operation: IAuthSwitchOperation };
95
331
  }
96
332
 
333
+ export interface IReq_AuthSwitchStartPreuse extends ITypedRequest {
334
+ method: 'authswitch.authority.preuse.start';
335
+ request: { operationId: string; accountId: string; loginId: string; purpose: 'openai_managed';
336
+ prompt: string; model?: string };
337
+ response: { operation: IAuthSwitchPreuseOperation };
338
+ }
339
+
340
+ export interface IReq_AuthSwitchGetPreuse extends ITypedRequest {
341
+ method: 'authswitch.authority.preuse.operation';
342
+ request: { operationId: string };
343
+ response: { operation: IAuthSwitchPreuseOperation };
344
+ }
345
+
346
+ export interface IReq_AuthSwitchCancelPreuse extends ITypedRequest {
347
+ method: 'authswitch.authority.preuse.cancel';
348
+ request: { operationId: string };
349
+ response: { operation: IAuthSwitchPreuseOperation };
350
+ }
351
+
97
352
  export interface IReq_AuthSwitchRenameAccount extends ITypedRequest {
98
353
  method: 'authswitch.authority.rename';
99
354
  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
+ }