@modelprofile.com/authswitch 9.0.0 → 9.2.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 (38) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +71 -1
  3. package/dist_ts/authority-contract.js +13 -2
  4. package/dist_ts/authority-import-contract.d.ts +6 -0
  5. package/dist_ts/authority-runtime-contract.d.ts +29 -0
  6. package/dist_ts/classes.authoritybroker.d.ts +37 -15
  7. package/dist_ts/classes.authoritybroker.js +121 -35
  8. package/dist_ts/classes.authorityclient.d.ts +28 -3
  9. package/dist_ts/classes.authorityclient.js +101 -18
  10. package/dist_ts/classes.authoritydaemon.d.ts +24 -0
  11. package/dist_ts/classes.authoritydaemon.js +87 -40
  12. package/dist_ts/classes.authoritydatabase.d.ts +29 -4
  13. package/dist_ts/classes.authoritydatabase.js +98 -13
  14. package/dist_ts/classes.authorityimport.js +2 -2
  15. package/dist_ts/classes.authoritymodels.js +5 -3
  16. package/dist_ts/classes.codexmanaged.d.ts +0 -9
  17. package/dist_ts/classes.codexmanaged.js +8 -28
  18. package/dist_ts/codexcontract.d.ts +30 -0
  19. package/dist_ts/codexcontract.js +174 -0
  20. package/dist_ts/ts_migration/0004_container_setup_owner.d.ts +12 -0
  21. package/dist_ts/ts_migration/0004_container_setup_owner.js +19 -0
  22. package/dist_ts/ts_migration/index.js +3 -1
  23. package/package.json +8 -8
  24. package/readme.md +102 -25
  25. package/ts/00_commitinfo_data.ts +1 -1
  26. package/ts/authority-contract.ts +75 -4
  27. package/ts/authority-import-contract.ts +6 -0
  28. package/ts/authority-runtime-contract.ts +30 -0
  29. package/ts/classes.authoritybroker.ts +122 -36
  30. package/ts/classes.authorityclient.ts +107 -21
  31. package/ts/classes.authoritydaemon.ts +91 -33
  32. package/ts/classes.authoritydatabase.ts +100 -14
  33. package/ts/classes.authorityimport.ts +1 -1
  34. package/ts/classes.authoritymodels.ts +4 -1
  35. package/ts/classes.codexmanaged.ts +6 -26
  36. package/ts/codexcontract.ts +200 -0
  37. package/ts/ts_migration/0004_container_setup_owner.ts +19 -0
  38. package/ts/ts_migration/index.ts +2 -0
package/readme.md CHANGED
@@ -37,6 +37,18 @@ that tool -- `ownerTool` is `claude_code`, `codex` or `opencode`, and `null` exa
37
37
  native tool holds it, because the authority refreshes it or nothing does. The authority
38
38
  names it rather than leaving a consumer to derive it from the purpose, which would be wrong
39
39
  as soon as two purposes share a tool or one tool gains a second purpose.
40
+ The snapshot also lists `nativeAssignments`: which account a vendor tool's own home on this host runs on,
41
+ as `{ id, tool, accountId, loginId, state, revision }` with the home named by its hash. Only homes the
42
+ authority itself switches are recorded -- Claude Code homes adopted by a verified import -- and `state` is
43
+ `switching` while a handoff to another account is in flight and `quarantined` until one is resolved. A
44
+ Codex or OpenCode store that its own tool still refreshes appears as its login instead, with
45
+ `owner: 'legacy_native'` and its `ownerTool`, because the authority does not decide what that store holds.
46
+ Every change of an assignment is an account event, so a subscriber sees it like any other change.
47
+ A removed account is left out of the snapshot unless the caller asks with `includeRemoved` (on `snapshot`,
48
+ `snapshotAll` and `subscribe`): it then appears with `removed: true` and its logins with `health: 'removed'`.
49
+ An account label follows one published rule, `isAuthSwitchAccountLabel` (at most
50
+ `authSwitchAccountLabelMaxLength` characters, no space at either end, no control character); a rename that
51
+ breaks it is refused with `invalid_input`.
40
52
 
41
53
  ```ts
42
54
  import { randomUUID } from 'node:crypto';
@@ -50,41 +62,82 @@ const operationId = randomUUID();
50
62
  const operation = await client.beginAddOpenAi(operationId);
51
63
  // Keep operation.id to discover its final result after navigation or reconnect.
52
64
  const receipt = await client.getOperation(operation.id);
65
+ // Or follow it without polling: each answer comes when the operation changes.
66
+ let current = operation;
67
+ while (['starting', 'pending', 'committing'].includes(current.state)) {
68
+ current = await client.watchOperation(current.id, current.revision);
69
+ }
53
70
  ```
54
71
 
55
72
  Browser code can import the credential-free DTOs from
56
73
  `@modelprofile.com/authswitch/authority-contract`. The backend-only binding, access and release
57
74
  requests are named by `@modelprofile.com/authswitch/authority-runtime-contract`, and the one-time
58
75
  import's own DTOs by `@modelprofile.com/authswitch/authority-import-contract`; those capabilities must
59
- never reach a browser. Two of the three runtime requests are served on the runtime socket -- resolving
60
- access and releasing a binding -- while `authswitch.authority.bind` is served on the **management** socket
61
- by design: minting a capability is the act that authorizes a runtime, so only a trusted management caller
62
- may perform it, and a container that is given the runtime directory can use a binding it was handed but can
63
- never create one. A backend binds an account to a runtime incarnation by exact account ID, login ID and
64
- purpose, and keeps the returned capability private. Targeted reauthentication likewise requires the
76
+ never reach a browser. Two of the four runtime requests are served on the runtime socket -- resolving
77
+ access and releasing a binding -- while `authswitch.authority.bind` and its inverse
78
+ `authswitch.authority.unbind` are served on the **management** socket by design: minting a capability is the
79
+ act that authorizes a runtime, so only a trusted management caller may perform it, and a container that is
80
+ given the runtime directory can use and release a binding it was handed but can never create one or
81
+ release one it was not handed. A backend binds an account to a runtime incarnation by exact account ID,
82
+ login ID and purpose, and keeps the returned capability private. Targeted reauthentication likewise requires the
65
83
  exact login; an account's presentation default never selects a grant for either action. Completed and
66
84
  interrupted operations remain discoverable through `getOperation()` and paged
67
- `listOperations()`. A completed receipt and its account/grant change commit together, so
85
+ `listOperations()`. `watchOperation(id, afterRevision, waitMs = 30000)` is the same read as a long poll,
86
+ the way `events` waits: it answers as soon as the operation's revision passes `afterRevision` -- a sign-in
87
+ answers `starting` with no prompt, then `pending` with its device prompt, then its outcome -- at once for a
88
+ finished operation, and with the unchanged operation once `waitMs` (at most 30000) runs out. Only a change
89
+ of that operation answers it. A completed receipt and its account/grant change commit together, so
68
90
  the receipt resolves a lost response or an event that arrives first. Add and reauthentication
69
91
  callers supply one UUID for the logical start and reuse it after a lost response. Replaying that
70
92
  UUID returns the same matching receipt without starting another provider login; using it for a
71
93
  different target is rejected. Cancelling a durable `starting` receipt prevents a late provider
72
94
  handle from promoting or overwriting it. Aborting a client request only detaches that request and
73
- does not cancel the daemon-owned login. The runtime-only socket
74
- resolves that binding to a current access token; its directory can be mounted into a
95
+ does not cancel the daemon-owned login. A client that disconnects before its answer -- a long poll it
96
+ stopped reading, or one the daemon's shutdown settles after the client went away -- ends only its own
97
+ connection: the daemon drops that request's pending work and keeps serving every other client. Each
98
+ request is bounded by its own deadline -- 35 s unless its route states another, 120 s for an import submit,
99
+ the wait plus 5 s for a long poll -- and nothing in the transport ends it sooner: the daemon gives a
100
+ connection 35 s from acceptance to deliver its complete request, however it trickles in, and sets no limit
101
+ once it is admitted.
102
+ The runtime-only socket resolves that binding to a current access token; its directory can be mounted into a
75
103
  container without exposing the management socket. A provider that rejects a specific
76
104
  access-token generation can request a newer one through `rejectedGrantGeneration`; the
77
105
  authority coalesces concurrent rejection callbacks and never replays an uncertain refresh.
78
- After an external Flex or OpenCode runtime has fenced new work and drained its pending access
79
- resolutions and provider requests, it can release its exact binding capability through the
80
- runtime socket. Every bind receives a fresh random capability, so a stale release cannot fence
106
+ A ChatGPT login backs a Claude Code runtime too (`runtime: 'claude'`); a Claude account never binds,
107
+ because it reaches Claude Code through the native switch. After an external Flex, OpenCode or Claude
108
+ runtime has fenced new work and drained its pending access resolutions and provider requests, it can
109
+ release its exact binding capability through the runtime socket. Every bind receives a fresh random capability, so a stale release cannot fence
81
110
  or delete a successor even if the caller reused an incarnation label. Release waits for admitted
82
111
  server-side resolution handlers, but handler completion does not prove response delivery and
83
112
  cannot retract an access token already received or in use; the runtime owner therefore owns that
84
113
  drain. Generic release cannot revoke a managed Codex binding, whose daemon-owned stop still closes
85
114
  the runtime and clears its durable run before internal revocation. Binding release never transfers
86
- or changes ownership of the account grant. External-binding recovery after an owner crash remains
87
- a prerequisite for the full account-mutation cutover.
115
+ or changes ownership of the account grant.
116
+
117
+ A holder that lost its capability -- a binding an earlier run of it left behind -- releases it with
118
+ `unbind({ runtime, scopeId, incarnationId, expectedRevision })` (`authswitch.authority.unbind`), naming the
119
+ binding by the identity it bound it under, as the snapshot and `getBinding` publish it. Unbinding needs no
120
+ ready login, so the leftover bindings of an account whose sign-in has ended are released, and the account
121
+ removed, without signing in again. It is authorized exactly as `bind` is: the management socket is the trust
122
+ boundary, and a runtime and scope are the namespace a holder binds under and owns -- AGL, for example, binds
123
+ under its own installation's scopes -- so a holder names only its own. The daemon releases only the binding
124
+ at exactly that runtime and scope, and only while it is still the incarnation and revision the holder read:
125
+ another scope, another runtime, or a successor bound in the meantime is left alone, and the answer is
126
+ `inactive`, as it is for a binding already released. A binding's revision is the authority revision of the
127
+ bind that wrote it, so it never repeats for a runtime and scope, even after a release: a repeated or stale
128
+ unbind never names a successor bound under the same incarnation. Its capability is fenced and released exactly as the
129
+ runtime release would, so admitted access resolutions finish first. Managed Codex bindings are never named
130
+ here. While the login is ready, a holder can instead bind the same runtime and scope again, which replaces
131
+ the capability, and release the returned one.
132
+
133
+ `getBinding(bindingId)` (`authswitch.authority.binding`) reads one binding by the id `bind` returned,
134
+ credential-free, or `null` once the authority holds none by that id, so a backend checks its binding without
135
+ reading the whole snapshot.
136
+
137
+ Removing an account that still backs a Flex, OpenCode or Claude binding is refused with `account_busy`, naming
138
+ those runtimes: their holder stops them and releases the bindings first -- by capability, or by `unbind` when
139
+ it no longer holds one -- so a removal never strands a runtime whose next access would fail. Managed Codex
140
+ bindings are the daemon's own; removal drains managed Codex as before and then takes them with the account.
88
141
 
89
142
  Every route on either socket answers in one of three ways: a result, a **refusal**, or a fault. A refusal
90
143
  is an answer -- the daemon completed its check, nothing was left half-done, and its message says what the
@@ -99,7 +152,8 @@ that marker: it answers `{ code, instruction }` for a refusal and `null` for a f
99
152
  closed `TAuthSwitchRefusalCode` set, which is what a client branches on without reading the text:
100
153
  `authority_closing`, `not_found`, `account_changed`, `account_busy`, `login_unavailable`,
101
154
  `binding_unauthorized`, `native_owner_holds_login`, `claude_home_unregistered`, `claude_handoff_pending`,
102
- `claude_receipt_missing` and `import_refusal`, plus the two the legacy commands answer with on this side of
155
+ `claude_receipt_missing`, `import_refusal`, `codex_unsupported`, `invalid_input`, `login_needs_reauth` and
156
+ `access_not_fresh`, plus the two the legacy commands answer with on this side of
103
157
  the socket, `authority_holds_login` and `authority_unavailable` (see "The legacy fence" below). The
104
158
  instruction is authored for the owner and contains no path, credential or digest; only an importer refusal
105
159
  may name the process the owner has to stop. The set is producer-owned: a consumer branches on the codes it
@@ -112,10 +166,12 @@ credential was being resolved. Those are one answer on purpose: an absent bindin
112
166
  compared against the same zero hash, so nothing distinguishes them, and the repair is the same either way --
113
167
  bind again. A binding whose account has since been reauthorized reads `login_unavailable` instead, because
114
168
  binding again cannot help until that account holds a login again. A device sign-in or preuse operation this
115
- authority does not hold reads `not_found`, from whichever route met it. Two things stay unmarked faults on
116
- purpose. What the shared managed-access loop refuses is one: it serves a bound runtime and the
117
- credential-free usage reader alike, and those two repair differently. Binding an *account* this authority
118
- does not hold at all is the other: the storage layer throws `Account is not available for this runtime.`,
169
+ authority does not hold reads `not_found`, from whichever route met it. What the access loop decides about
170
+ the login itself is marked as well: `login_needs_reauth` when only a new device sign-in brings the login back
171
+ (after which a bound runtime binds again), and `access_not_fresh` when the login is intact but the provider
172
+ could not renew its access yet, so the same request can succeed later. A changed identity or a view that
173
+ keeps moving during resolution stays an unmarked fault, because nobody decided it. Binding an *account* this
174
+ authority does not hold at all stays one too: the storage layer throws `Account is not available for this runtime.`,
119
175
  which a socket caller reads as `Internal server error`, because naming it is a decision about which layer
120
176
  authors owner instructions rather than a wording fix.
121
177
 
@@ -128,6 +184,15 @@ and the ledger row it wrote says where that source stands -- while an unmarked f
128
184
  unknown, and that source must then be read with `authswitch.authority.import.status` rather than
129
185
  submitted again.
130
186
 
187
+ A daemon that is shutting down admits no new request -- every route answers `authority_closing` -- and
188
+ settles the requests it already admitted before it closes its store: a waiting `events` or
189
+ `watchOperation` read answers normally from the open store instead of failing.
190
+
191
+ A client reading a daemon that runs an older release -- one not restarted after an upgrade -- fails with
192
+ `AuthSwitchDaemonOutdatedError`, which names the restart (`authswitch authority service stop`, then
193
+ `start`), and a subscription reports `unavailable` with the reason `daemon_outdated` once and retries every
194
+ five seconds until the daemon is restarted.
195
+
131
196
  `AuthSwitchClient.subscribe(onSnapshot, onEvent, signal, { onStatus })` reports `current`
132
197
  after a fresh snapshot or verified heartbeat, `unavailable` on disconnect or resync, and
133
198
  `closed` on abort. Keep cached account actions disabled while status is not `current`.
@@ -293,7 +358,9 @@ that refusal nor "already imported" ever clears: no route forgets a row or re-po
293
358
 
294
359
  `import status` reads the migration ledger, the grants and the handoffs -- never the host -- and reports, per
295
360
  source, where it stands, which owner refreshes its login now, and what to do next: nothing, submit the same
296
- source again, or sign in to that account again. Submitting a source again is how an interrupted run resumes,
361
+ source again, or sign in to that account again. Each entry also carries the source's `sourcePathHash`, so a
362
+ backend that submitted an external source finds its own record by `sourceKind` and that hash -- also after a
363
+ submit whose outcome it never learned -- without deriving the ledger row id. Submitting a source again is how an interrupted run resumes,
297
364
  and the answer comes from the same durable record the daemon decides on: a source whose sign-in was in
298
365
  flight when the run died is reported as needing a device sign-in, not as resumable, because a rotating
299
366
  refresh token is never sent a second time. Sources that were never submitted have no ledger row and appear
@@ -339,7 +406,7 @@ generated prose and credentials are absent from persistence and management respo
339
406
  removal and targeted reauthentication refuse while that account has an active preuse request.
340
407
 
341
408
  A trusted in-process daemon host can call `AuthSwitchAuthorityDaemon.startManagedCodex()`
342
- for a bound account and workspace. It owns a private Codex 0.157.0 app-server, passes only
409
+ for a bound account and workspace. It owns a private Codex app-server, passes only
343
410
  the authority's current access token through Codex's external-token login, answers its
344
411
  unauthorized callback through the authority, and revokes the binding on stop. The returned
345
412
  runtime exposes an interactive `codex --remote unix://…` launch descriptor and native
@@ -363,13 +430,23 @@ directory other users cannot rewrite, and the socket behind it is the user's own
363
430
  directory of the user's; anything else fails the start with that package's error. Codex keeps a
364
431
  zero-byte startup lock in that directory for each socket path it has served; the file is
365
432
  Codex's own and authswitch does not remove it.
366
- Managed Codex runs only the verified Codex release: a `codex` that reports any other
367
- version is refused before it starts, so an automatic Codex update stops new managed
368
- sessions until authswitch has verified that release.
433
+ Managed Codex runs Codex 0.156.0 or newer -- the floor `@modelprofile.com/mcp-crossharness`'s
434
+ `requireCodexVersion` checks, the first release that publishes the socket alias -- and checks the
435
+ contract at every start instead of pinning one release. Before anything is recorded or started, the
436
+ `codex` it will run must report a readable version at or above the floor, and its own protocol
437
+ description (`codex app-server generate-json-schema`) must still offer every surface managed Codex uses:
438
+ the requests `account/login/start`, `thread/start`, `thread/resume` and `turn/start`; the external
439
+ ChatGPT token login with the fields it sends; the `account/chatgptAuthTokens/refresh` callback with the
440
+ `unauthorized` reason and the reply it gives; and the thread and turn parameters it sends. After
441
+ connecting, the app-server must report exactly the version that was checked, because Codex can replace
442
+ itself between the check and the start. Each failure is the `codex_unsupported` refusal naming the
443
+ version or the missing surface, so a Codex update that drops something managed Codex relies on stops new
444
+ managed sessions by name instead of failing one later. A prerelease above the floor runs; a prerelease of
445
+ 0.156.0 itself is below it.
369
446
  Managed-Codex process ownership observation is qualified on Linux and reads exact argument
370
447
  boundaries from `/proc`; on other hosts doctor reports the process observation as unknown,
371
448
  and authority actions that require survivor proof refuse instead of guessing.
372
- Codex 0.157.0 does not persist an unused thread created by `thread/start`; resume applies
449
+ Codex (verified through 0.157) does not persist an unused thread created by `thread/start`; resume applies
373
450
  after Codex has committed a rollout. The existing CLI has not yet been moved to this path.
374
451
 
375
452
  This SDK is additive at this stage. The commands documented below still use the existing
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/authswitch',
6
- version: '9.0.0',
6
+ version: '9.2.0',
7
7
  description: 'Manage Codex, OpenCode and Claude Code accounts with guided switching and live usage status'
8
8
  }
@@ -25,7 +25,15 @@ export type TAuthSwitchRefusalCode =
25
25
  | 'claude_home_unregistered'
26
26
  | 'claude_handoff_pending'
27
27
  | 'claude_receipt_missing'
28
- | 'import_refusal';
28
+ | 'import_refusal'
29
+ /** The Codex on this host is older than managed Codex runs, or no longer offers a surface it uses. */
30
+ | 'codex_unsupported'
31
+ /** A value the owner typed breaks a rule the instruction states, such as an account label. */
32
+ | 'invalid_input'
33
+ /** The account's login has ended and only a new device sign-in brings it back. */
34
+ | 'login_needs_reauth'
35
+ /** The login is intact, but the provider could not renew its access yet; the same request can succeed later. */
36
+ | 'access_not_fresh';
29
37
 
30
38
  /** The marker the importer has published since 8.1.0; it keeps its own value on the wire. */
31
39
  export const authSwitchImportRefusalMarker = 'authswitch_import_refusal';
@@ -50,7 +58,8 @@ const refusalCodes: ReadonlySet<string> = new Set<TAuthSwitchRefusalCode>([
50
58
  'authority_closing', 'not_found', 'account_changed', 'account_busy',
51
59
  'login_unavailable', 'binding_unauthorized', 'native_owner_holds_login', 'claude_home_unregistered',
52
60
  'claude_handoff_pending', 'claude_receipt_missing', 'import_refusal',
53
- 'authority_holds_login', 'authority_unavailable',
61
+ 'authority_holds_login', 'authority_unavailable', 'codex_unsupported', 'invalid_input',
62
+ 'login_needs_reauth', 'access_not_fresh',
54
63
  ]);
55
64
 
56
65
  const isRefusalCode = (value: unknown): value is TAuthSwitchRefusalCode =>
@@ -102,6 +111,18 @@ export const asAuthSwitchRefusal = (error: unknown): IAuthSwitchRefusal | null =
102
111
  return { code, instruction };
103
112
  };
104
113
 
114
+ /** The longest account label, in UTF-16 code units. */
115
+ export const authSwitchAccountLabelMaxLength = 128;
116
+
117
+ /**
118
+ * The account label rule, the one the authority applies to a rename: one to `authSwitchAccountLabelMaxLength`
119
+ * characters, no space at either end, no control character. A consumer validates with this rather than
120
+ * restating it; the authority answers a label that breaks it with the `invalid_input` refusal.
121
+ */
122
+ export const isAuthSwitchAccountLabel = (value: unknown): value is string => typeof value === 'string'
123
+ && value.trim() === value && value.length > 0 && value.length <= authSwitchAccountLabelMaxLength
124
+ && !/[\u0000-\u001f\u007f]/.test(value);
125
+
105
126
  /** Credential-free account management contract. Safe to import in browser code. */
106
127
  export type TAuthSwitchLoginPurpose = 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native';
107
128
  export type TAuthSwitchLoginHealth = 'ready' | 'refreshing' | 'retry_wait' | 'needs_reauth' | 'unverified' | 'pending_handoff' | 'handoff_quarantined' | 'removed';
@@ -149,9 +170,36 @@ export interface IAuthSwitchLogin {
149
170
  export interface IAuthSwitchBinding {
150
171
  id: string;
151
172
  accountId: string;
173
+ /**
174
+ * The runtime an OpenAI (ChatGPT) login backs. `flex`, `opencode` and `claude` bindings are held and
175
+ * released by their caller over the runtime socket; `codex` is the daemon's own managed Codex. A Claude
176
+ * account never binds: it reaches Claude Code through the native switch.
177
+ */
152
178
  runtime: 'flex' | 'codex' | 'opencode' | 'claude';
153
179
  scopeId: string;
154
180
  incarnationId: string;
181
+ /**
182
+ * The authority revision at which this runtime and scope were last bound. It grows with every bind and never
183
+ * repeats for them, not even after a release and a new bind, so it names exactly one bind.
184
+ */
185
+ revision: number;
186
+ }
187
+
188
+ /**
189
+ * Which account a vendor tool's own home on this host runs on. Credential-free: the home is named by the
190
+ * hash the authority registered it under, never by its path.
191
+ *
192
+ * Only homes the authority itself switches are recorded: Claude Code homes adopted by a verified import.
193
+ * A Codex or OpenCode store that a native tool still refreshes appears as its login instead, with
194
+ * `owner: 'legacy_native'` and its `ownerTool`, because the authority does not decide what that store holds.
195
+ */
196
+ export interface IAuthSwitchNativeAssignment {
197
+ id: string;
198
+ tool: TAuthSwitchLoginOwnerTool;
199
+ accountId: string;
200
+ loginId: string;
201
+ /** `switching` while a handoff to another account is in flight; `quarantined` until it is resolved. */
202
+ state: 'ready' | 'switching' | 'quarantined';
155
203
  revision: number;
156
204
  }
157
205
 
@@ -163,9 +211,11 @@ export interface IAuthSwitchSnapshot {
163
211
  accounts: IAuthSwitchAccount[];
164
212
  logins: IAuthSwitchLogin[];
165
213
  bindings: IAuthSwitchBinding[];
214
+ nativeAssignments: IAuthSwitchNativeAssignment[];
166
215
  nextAccountCursor: string | null;
167
216
  nextLoginCursor: string | null;
168
217
  nextBindingCursor: string | null;
218
+ nextNativeAssignmentCursor: string | null;
169
219
  }
170
220
 
171
221
  /** Persisted account evidence. No provider request is made while collecting diagnostics. */
@@ -391,7 +441,13 @@ export interface IReq_AuthSwitchClaudeNativeHandoffs extends ITypedRequest {
391
441
 
392
442
  export interface IReq_AuthSwitchSnapshot extends ITypedRequest {
393
443
  method: 'authswitch.authority.snapshot';
394
- request: { accountAfter?: string; loginAfter?: string; bindingAfter?: string; limit?: number };
444
+ request: { accountAfter?: string; loginAfter?: string; bindingAfter?: string; nativeAssignmentAfter?: string;
445
+ limit?: number;
446
+ /**
447
+ * Also publish removed accounts (`removed: true`) and their removed logins (`health: 'removed'`), so a
448
+ * consumer can show them apart. Absent or false keeps the snapshot to what is live.
449
+ */
450
+ includeRemoved?: boolean };
395
451
  response: { snapshot: IAuthSwitchSnapshot };
396
452
  }
397
453
 
@@ -441,9 +497,14 @@ export interface IReq_AuthSwitchListOperations extends ITypedRequest {
441
497
  response: { operations: IAuthSwitchOperation[]; nextCursor: string | null };
442
498
  }
443
499
 
500
+ /**
501
+ * One device sign-in. `afterRevision` and `waitMs` come together or not at all: with them the read is a long
502
+ * poll that answers once the operation's revision passes `afterRevision`, at once for a finished sign-in, or
503
+ * with the unchanged operation after `waitMs` (at most 30000).
504
+ */
444
505
  export interface IReq_AuthSwitchGetOperation extends ITypedRequest {
445
506
  method: 'authswitch.authority.operation';
446
- request: { operationId: string };
507
+ request: { operationId: string; afterRevision?: number; waitMs?: number };
447
508
  response: { operation: IAuthSwitchOperation };
448
509
  }
449
510
 
@@ -478,6 +539,16 @@ export interface IReq_AuthSwitchRenameAccount extends ITypedRequest {
478
539
  response: { account: IAuthSwitchAccount };
479
540
  }
480
541
 
542
+ /**
543
+ * One binding by the id `bind` returned, credential-free, or `null` when this authority holds no binding by
544
+ * that id. A backend checks that its binding still stands without reading the whole snapshot.
545
+ */
546
+ export interface IReq_AuthSwitchGetBinding extends ITypedRequest {
547
+ method: 'authswitch.authority.binding';
548
+ request: { bindingId: string };
549
+ response: { binding: IAuthSwitchBinding | null };
550
+ }
551
+
481
552
  export interface IReq_AuthSwitchRemoveAccount extends ITypedRequest {
482
553
  method: 'authswitch.authority.remove';
483
554
  request: { accountId: string; expectedRevision: number };
@@ -173,6 +173,12 @@ export const isAuthSwitchImportRefusal = (error: unknown): boolean =>
173
173
  export interface IAuthSwitchImportStatusEntry {
174
174
  sourceId: string;
175
175
  sourceKind: TAuthSwitchImportSourceKind;
176
+ /**
177
+ * The source location as a hash, exactly as it was submitted or inventoried. A backend that submits an
178
+ * external source finds its own record here by `sourceKind` and this hash, including after a submit whose
179
+ * outcome it never learned.
180
+ */
181
+ sourcePathHash: string;
176
182
  status: TAuthSwitchImportLedgerStatus;
177
183
  accountId: string | null;
178
184
  loginId: string | null;
@@ -25,6 +25,36 @@ export interface IReq_AuthSwitchBindAccount extends ITypedRequest {
25
25
  response: { binding: IAuthSwitchBinding; capability: string };
26
26
  }
27
27
 
28
+ /**
29
+ * Backend-only release of a binding by the identity its holder bound it under, served on the MANAGEMENT
30
+ * socket beside `bind`, of which it is the inverse.
31
+ *
32
+ * It is how a holder releases a binding it no longer holds the capability for -- one an earlier run of it
33
+ * left behind -- and it needs no ready login: an account whose sign-in has ended can have its leftover
34
+ * bindings released, and then be removed, without signing in again first. It is authorized as `bind` is: a
35
+ * caller trusted with the management socket names a runtime and scope, the namespace a holder binds under
36
+ * and owns. It releases only the binding at exactly that runtime and scope, and only while it is still the
37
+ * incarnation and revision the holder read from the snapshot or `getBinding`; any other binding -- another
38
+ * scope, another runtime, or a successor bound meanwhile -- is left alone and the answer is `inactive`.
39
+ * A binding's revision names one bind and never repeats for its runtime and scope, so a successor is told
40
+ * apart even when it was bound under the same incarnation after the one read was released.
41
+ * The daemon's own managed Codex bindings are never named here. Like the runtime release, it waits for
42
+ * the access resolutions already admitted for that binding.
43
+ */
44
+ export interface IReq_AuthSwitchUnbind extends ITypedRequest {
45
+ method: 'authswitch.authority.unbind';
46
+ request: {
47
+ runtime: Exclude<IAuthSwitchBinding['runtime'], 'codex'>;
48
+ scopeId: string;
49
+ incarnationId: string;
50
+ /** The binding's `revision` as the holder read it. */
51
+ expectedRevision: number;
52
+ };
53
+ response: {
54
+ state: 'released' | 'inactive';
55
+ };
56
+ }
57
+
28
58
  /** Backend-only access operation. Refresh grants never cross this interface. */
29
59
  export interface IReq_AuthSwitchResolveAccess extends ITypedRequest {
30
60
  method: 'authswitch.authority.resolveAccess';