@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/authority-contract.d.ts +71 -1
- package/dist_ts/authority-contract.js +13 -2
- package/dist_ts/authority-import-contract.d.ts +6 -0
- package/dist_ts/authority-runtime-contract.d.ts +29 -0
- package/dist_ts/classes.authoritybroker.d.ts +37 -15
- package/dist_ts/classes.authoritybroker.js +121 -35
- package/dist_ts/classes.authorityclient.d.ts +28 -3
- package/dist_ts/classes.authorityclient.js +101 -18
- package/dist_ts/classes.authoritydaemon.d.ts +24 -0
- package/dist_ts/classes.authoritydaemon.js +87 -40
- package/dist_ts/classes.authoritydatabase.d.ts +29 -4
- package/dist_ts/classes.authoritydatabase.js +98 -13
- package/dist_ts/classes.authorityimport.js +2 -2
- package/dist_ts/classes.authoritymodels.js +5 -3
- package/dist_ts/classes.codexmanaged.d.ts +0 -9
- package/dist_ts/classes.codexmanaged.js +8 -28
- package/dist_ts/codexcontract.d.ts +30 -0
- package/dist_ts/codexcontract.js +174 -0
- package/dist_ts/ts_migration/0004_container_setup_owner.d.ts +12 -0
- package/dist_ts/ts_migration/0004_container_setup_owner.js +19 -0
- package/dist_ts/ts_migration/index.js +3 -1
- package/package.json +8 -8
- package/readme.md +102 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +75 -4
- package/ts/authority-import-contract.ts +6 -0
- package/ts/authority-runtime-contract.ts +30 -0
- package/ts/classes.authoritybroker.ts +122 -36
- package/ts/classes.authorityclient.ts +107 -21
- package/ts/classes.authoritydaemon.ts +91 -33
- package/ts/classes.authoritydatabase.ts +100 -14
- package/ts/classes.authorityimport.ts +1 -1
- package/ts/classes.authoritymodels.ts +4 -1
- package/ts/classes.codexmanaged.ts +6 -26
- package/ts/codexcontract.ts +200 -0
- package/ts/ts_migration/0004_container_setup_owner.ts +19 -0
- 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
|
|
60
|
-
access and releasing a binding -- while `authswitch.authority.bind`
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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()`.
|
|
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.
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
runtime
|
|
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.
|
|
87
|
-
|
|
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
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
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
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/authority-contract.ts
CHANGED
|
@@ -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;
|
|
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';
|