@modelprofile.com/authswitch 8.1.0 → 8.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 +56 -0
- package/dist_ts/authority-contract.js +54 -2
- package/dist_ts/authority-import-contract.d.ts +8 -4
- package/dist_ts/authority-import-contract.js +12 -13
- package/dist_ts/classes.authoritybroker.js +3 -2
- package/dist_ts/classes.authorityclient.d.ts +17 -11
- package/dist_ts/classes.authorityclient.js +28 -22
- package/dist_ts/classes.authoritydaemon.d.ts +8 -0
- package/dist_ts/classes.authoritydaemon.js +74 -58
- package/dist_ts/classes.authoritydatabase.d.ts +9 -1
- package/dist_ts/classes.authoritydatabase.js +29 -12
- package/dist_ts/classes.authorityimport.d.ts +17 -2
- package/dist_ts/classes.authorityimport.js +29 -3
- package/dist_ts/classes.authoritymodels.d.ts +4 -1
- package/dist_ts/classes.authoritymodels.js +15 -3
- package/dist_ts/classes.authorityservice.d.ts +10 -0
- package/dist_ts/classes.authorityservice.js +24 -1
- package/dist_ts/classes.claudeauthority.js +19 -11
- package/dist_ts/classes.claudenative.d.ts +63 -3
- package/dist_ts/classes.claudenative.js +68 -8
- package/dist_ts/ts_migration/0003_claude_handoff_proof.d.ts +13 -0
- package/dist_ts/ts_migration/0003_claude_handoff_proof.js +20 -0
- package/dist_ts/ts_migration/index.js +3 -1
- package/dist_ts/ts_migration/legacysources/nativestores.js +15 -8
- package/dist_ts/ts_migration/legacysources/shared.d.ts +3 -2
- package/dist_ts/ts_migration/legacysources/shared.js +3 -5
- package/package.json +5 -1
- package/readme.md +65 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +106 -0
- package/ts/authority-import-contract.ts +12 -11
- package/ts/classes.authoritybroker.ts +2 -1
- package/ts/classes.authorityclient.ts +43 -21
- package/ts/classes.authoritydaemon.ts +71 -56
- package/ts/classes.authoritydatabase.ts +29 -11
- package/ts/classes.authorityimport.ts +30 -2
- package/ts/classes.authoritymodels.ts +11 -2
- package/ts/classes.authorityservice.ts +27 -0
- package/ts/classes.claudeauthority.ts +22 -10
- package/ts/classes.claudenative.ts +104 -10
- package/ts/ts_migration/0003_claude_handoff_proof.ts +19 -0
- package/ts/ts_migration/index.ts +2 -0
- package/ts/ts_migration/legacysources/nativestores.ts +15 -7
- package/ts/ts_migration/legacysources/shared.ts +4 -6
package/readme.md
CHANGED
|
@@ -49,8 +49,11 @@ const receipt = await client.getOperation(operation.id);
|
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Browser code can import the credential-free DTOs from
|
|
52
|
-
`@modelprofile.com/authswitch/authority-contract`.
|
|
53
|
-
|
|
52
|
+
`@modelprofile.com/authswitch/authority-contract`. The backend-only binding, access and release
|
|
53
|
+
requests are named by `@modelprofile.com/authswitch/authority-runtime-contract`, and the one-time
|
|
54
|
+
import's own DTOs by `@modelprofile.com/authswitch/authority-import-contract`; the runtime contract
|
|
55
|
+
belongs to the runtime socket and its capability must never reach a browser. A backend binds an
|
|
56
|
+
account to a runtime incarnation by exact account ID, login ID and purpose, and keeps the returned
|
|
54
57
|
capability private. Targeted reauthentication likewise requires the exact login; an
|
|
55
58
|
account's presentation default never selects a grant for either action. Completed and
|
|
56
59
|
interrupted operations remain discoverable through `getOperation()` and paged
|
|
@@ -76,6 +79,31 @@ the runtime and clears its durable run before internal revocation. Binding relea
|
|
|
76
79
|
or changes ownership of the account grant. External-binding recovery after an owner crash remains
|
|
77
80
|
a prerequisite for the full account-mutation cutover.
|
|
78
81
|
|
|
82
|
+
Every route on either socket answers in one of three ways: a result, a **refusal**, or a fault. A refusal
|
|
83
|
+
is an answer -- the daemon completed its check, nothing was left half-done, and its message says what the
|
|
84
|
+
owner does instead -- so it crosses the wire marked, with the message verbatim beside
|
|
85
|
+
`{ reason, code }`. A fault is anything the daemon did not decide: the transport replaces its text with
|
|
86
|
+
`Internal server error`, and a caller must never present that as if it said what to do next. A malformed
|
|
87
|
+
request is neither; it keeps its own text and carries no marker, because a caller's own bug is not an
|
|
88
|
+
instruction for an owner.
|
|
89
|
+
|
|
90
|
+
`asAuthSwitchRefusal(error)` from `@modelprofile.com/authswitch/authority-contract` is the one reader of
|
|
91
|
+
that marker: it answers `{ code, instruction }` for a refusal and `null` for a fault. The code is the
|
|
92
|
+
closed `TAuthSwitchRefusalCode` set, which is what a client branches on without reading the text:
|
|
93
|
+
`authority_closing`, `not_found`, `account_changed`, `account_busy`, `login_unavailable`,
|
|
94
|
+
`native_owner_holds_login`, `claude_home_unregistered`, `claude_handoff_pending`,
|
|
95
|
+
`claude_receipt_missing` and `import_refusal`. The instruction is authored for the owner and contains no
|
|
96
|
+
path, credential or digest; only an importer refusal may name the process the owner has to stop.
|
|
97
|
+
|
|
98
|
+
The import routes keep the marker they published in 8.1.0, `authswitch_import_refusal`, and
|
|
99
|
+
`isAuthSwitchImportRefusal` from `@modelprofile.com/authswitch/authority-import-contract` is the same
|
|
100
|
+
reader asking for the `import_refusal` code, so a consumer written against 8.1.0 keeps working and a
|
|
101
|
+
client of this release still reads the payload a daemon of that release answers with. The distinction
|
|
102
|
+
matters most for `import.submit`: a refusal there is a submit that decided -- it left nothing half-done,
|
|
103
|
+
and the ledger row it wrote says where that source stands -- while an unmarked failure leaves the outcome
|
|
104
|
+
unknown, and that source must then be read with `authswitch.authority.import.status` rather than
|
|
105
|
+
submitted again.
|
|
106
|
+
|
|
79
107
|
`AuthSwitchClient.subscribe(onSnapshot, onEvent, signal, { onStatus })` reports `current`
|
|
80
108
|
after a fresh snapshot or verified heartbeat, `unavailable` on disconnect or resync, and
|
|
81
109
|
`closed` on abort. Keep cached account actions disabled while status is not `current`.
|
|
@@ -167,6 +195,35 @@ If the daemon is unavailable before the first response, it fails with a fixed di
|
|
|
167
195
|
stderr and leaves stdout empty. The older native-store commands documented below still coexist until the coordinated
|
|
168
196
|
major-version migration and removal; these authority routes never use them.
|
|
169
197
|
|
|
198
|
+
### The Claude native home
|
|
199
|
+
|
|
200
|
+
The daemon serves Claude Code's own credential store, which stays the native store that Claude Code
|
|
201
|
+
reads. `authswitch.authority.claude.switch`, `.handoff` and `.handoffs` on the management socket move
|
|
202
|
+
that one home between two authority logins and report the journal of those moves, and
|
|
203
|
+
`authswitch.authority.usage` answers for an Anthropic account from it. The home is
|
|
204
|
+
`CLAUDE_CONFIG_DIR` or `~/.claude`, and the authority identifies it by the hash of the credential file
|
|
205
|
+
Claude Code owns -- the same value the import publishes as that source's path hash, which is what lets a
|
|
206
|
+
verified receipt adopt it.
|
|
207
|
+
|
|
208
|
+
The daemon is wired for that home whether or not Claude Code is installed, so `doctor` reports
|
|
209
|
+
`claudeNativeAuthority: configured` on any host; the `claudeHomes` page is what says whether a home has
|
|
210
|
+
been adopted. Until a verified `claude_native` import has adopted one, every Claude route refuses. A
|
|
211
|
+
`CLAUDE_CONFIG_DIR` that is not an absolute path is refused when the daemon starts, because the same
|
|
212
|
+
variable also decides which file the import would read; it is never resolved against a working directory.
|
|
213
|
+
|
|
214
|
+
A switch is only attempted when the host can prove Claude Code uses that native login: exactly the
|
|
215
|
+
verified Claude Code release, a subscriber login in the store, no credential override in the
|
|
216
|
+
environment, no managed or local settings selecting another auth source, and no scoped Claude session
|
|
217
|
+
running. Anything else refuses and leaves both logins where they were. A move that reaches the home and
|
|
218
|
+
then cannot complete that proof is quarantined with `problem: unsupported_effective_auth`, and the
|
|
219
|
+
handoff's optional `proofFailure` names which condition failed -- `unsupported_release`, `subscription`,
|
|
220
|
+
`override`, `settings`, `profile`, `profile_unreadable` or `running_session` -- so an owner is told what
|
|
221
|
+
to change. It is absent whenever `problem` is anything else. The settings that proof reads are the ones
|
|
222
|
+
the daemon may speak for: the user settings file of the resolved home and the managed settings directory.
|
|
223
|
+
A project's `.claude/settings.json` is read from the working directory of each running Claude session,
|
|
224
|
+
never from the daemon's own, which is its service unit's and selects nothing. While a login is in the
|
|
225
|
+
native home, Claude Code is its only refresher; the authority takes it back by the same journaled handoff.
|
|
226
|
+
|
|
170
227
|
## The one-time account import
|
|
171
228
|
|
|
172
229
|
The three `authority import` commands move the accounts that existed before the authority into it.
|
|
@@ -189,6 +246,12 @@ Each source is classified by who refreshes its login **after** the import.
|
|
|
189
246
|
credential. It is verified by a provider read with the access token already in the store, which rotates
|
|
190
247
|
nothing. Two refreshers on one rotating refresh token is the failure this avoids.
|
|
191
248
|
|
|
249
|
+
A verified Claude projection also adopts the native home it came from, so the daemon may serve Claude
|
|
250
|
+
switches for it. The receipt is what adopts it, and a home belongs to one login at a time: a second home
|
|
251
|
+
for a login another one already holds -- a `.credentials.json` copied to another `CLAUDE_CONFIG_DIR` --
|
|
252
|
+
and a home whose active login has since moved are both refused with the importer's own code, so the
|
|
253
|
+
submit is known to have decided, the ledger row stays `verified` and no home changed.
|
|
254
|
+
|
|
192
255
|
`import submit` takes the source id and the digest the owner read in the inventory. The daemon re-reads the
|
|
193
256
|
source itself and refuses if the digest changed, so approving one source can never import a different one;
|
|
194
257
|
no credential crosses the socket for a source this package can read. It refuses while an authswitch watch, a
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/authority-contract.ts
CHANGED
|
@@ -2,6 +2,99 @@ import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
|
|
|
2
2
|
import type { IAuthSwitchUsageSnapshot } from './classes.authorityusage.js';
|
|
3
3
|
export type { IAuthSwitchUsageSnapshot } from './classes.authorityusage.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Why the authority declined, as a closed set a client branches on without reading the text.
|
|
7
|
+
*
|
|
8
|
+
* A refusal is an answer: the daemon did the check, nothing was left half-done, and the message says what
|
|
9
|
+
* the owner does instead. Everything else stays a fault whose text the transport replaces, and a caller
|
|
10
|
+
* must never present a fault as if it said what to do next.
|
|
11
|
+
*/
|
|
12
|
+
export type TAuthSwitchRefusalCode =
|
|
13
|
+
| 'authority_closing'
|
|
14
|
+
| 'not_found'
|
|
15
|
+
| 'account_changed'
|
|
16
|
+
| 'account_busy'
|
|
17
|
+
| 'login_unavailable'
|
|
18
|
+
| 'native_owner_holds_login'
|
|
19
|
+
| 'claude_home_unregistered'
|
|
20
|
+
| 'claude_handoff_pending'
|
|
21
|
+
| 'claude_receipt_missing'
|
|
22
|
+
| 'import_refusal';
|
|
23
|
+
|
|
24
|
+
/** The marker the importer has published since 8.1.0; it keeps its own value on the wire. */
|
|
25
|
+
export const authSwitchImportRefusalMarker = 'authswitch_import_refusal';
|
|
26
|
+
export const authSwitchRefusalReason = 'authswitch_refusal';
|
|
27
|
+
|
|
28
|
+
export type TAuthSwitchRefusalReason = typeof authSwitchRefusalReason | typeof authSwitchImportRefusalMarker;
|
|
29
|
+
|
|
30
|
+
/** Everything a refusal puts on the wire beside its message: two literals, never a value of its own. */
|
|
31
|
+
export interface IAuthSwitchRefusalData {
|
|
32
|
+
reason: TAuthSwitchRefusalReason;
|
|
33
|
+
code: TAuthSwitchRefusalCode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the owner is told when the answer is an instruction rather than a fault. */
|
|
37
|
+
export interface IAuthSwitchRefusal {
|
|
38
|
+
code: TAuthSwitchRefusalCode;
|
|
39
|
+
/** The instruction, authored by the handler that declined. */
|
|
40
|
+
instruction: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const refusalCodes: ReadonlySet<string> = new Set<TAuthSwitchRefusalCode>([
|
|
44
|
+
'authority_closing', 'not_found', 'account_changed', 'account_busy',
|
|
45
|
+
'login_unavailable', 'native_owner_holds_login', 'claude_home_unregistered',
|
|
46
|
+
'claude_handoff_pending', 'claude_receipt_missing', 'import_refusal',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const isRefusalCode = (value: unknown): value is TAuthSwitchRefusalCode =>
|
|
50
|
+
typeof value === 'string' && refusalCodes.has(value);
|
|
51
|
+
|
|
52
|
+
/** The importer keeps the marker it published; every other code shares the authority's own. */
|
|
53
|
+
const reasonOf = (code: TAuthSwitchRefusalCode): TAuthSwitchRefusalReason =>
|
|
54
|
+
code === 'import_refusal' ? authSwitchImportRefusalMarker : authSwitchRefusalReason;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One refusal, thrown by a handler and turned into a marked answer by the router that owns the wire.
|
|
58
|
+
*
|
|
59
|
+
* It accepts nothing but a code of the closed set and the text the owner is to read, and builds the marker
|
|
60
|
+
* from the code alone, so no handler can attach a value of its own to what a client branches on.
|
|
61
|
+
*/
|
|
62
|
+
export class AuthSwitchRefusal extends Error {
|
|
63
|
+
public readonly code: TAuthSwitchRefusalCode;
|
|
64
|
+
|
|
65
|
+
constructor(code: TAuthSwitchRefusalCode, instruction: string) {
|
|
66
|
+
super(instruction);
|
|
67
|
+
this.code = code;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Everything this refusal puts on the wire beside its message. */
|
|
71
|
+
public get data(): IAuthSwitchRefusalData { return { reason: reasonOf(this.code), code: this.code }; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The refusal an error answer carries, or null when it is a fault whose text says nothing.
|
|
76
|
+
*
|
|
77
|
+
* This is the only reader of `errorData` in the package: the importer's published predicate is expressed
|
|
78
|
+
* through it, so a caller never has to know which of the two markers a route uses.
|
|
79
|
+
*
|
|
80
|
+
* 8.1.0 marked the importer's refusal with the reason alone. A per-user daemon keeps running across an
|
|
81
|
+
* upgrade until its owner restarts it, so a newer client still meets that payload, and it names the one
|
|
82
|
+
* refusal that release could answer with -- which is why it is read rather than treated as a fault.
|
|
83
|
+
*/
|
|
84
|
+
export const asAuthSwitchRefusal = (error: unknown): IAuthSwitchRefusal | null => {
|
|
85
|
+
if (error instanceof AuthSwitchRefusal) return { code: error.code, instruction: error.message };
|
|
86
|
+
if (typeof error !== 'object' || error === null || !('errorData' in error)) return null;
|
|
87
|
+
const data: unknown = error.errorData;
|
|
88
|
+
if (typeof data !== 'object' || data === null || !('reason' in data)) return null;
|
|
89
|
+
const code: unknown = 'code' in data ? data.code : undefined;
|
|
90
|
+
const instruction = 'message' in error && typeof error.message === 'string' ? error.message : '';
|
|
91
|
+
if (data.reason === authSwitchImportRefusalMarker) {
|
|
92
|
+
return code === undefined || code === 'import_refusal' ? { code: 'import_refusal', instruction } : null;
|
|
93
|
+
}
|
|
94
|
+
if (data.reason !== authSwitchRefusalReason || !isRefusalCode(code)) return null;
|
|
95
|
+
return { code, instruction };
|
|
96
|
+
};
|
|
97
|
+
|
|
5
98
|
/** Credential-free account management contract. Safe to import in browser code. */
|
|
6
99
|
export type TAuthSwitchLoginPurpose = 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native';
|
|
7
100
|
export type TAuthSwitchLoginHealth = 'ready' | 'refreshing' | 'retry_wait' | 'needs_reauth' | 'unverified' | 'pending_handoff' | 'handoff_quarantined' | 'removed';
|
|
@@ -234,6 +327,17 @@ export interface IAuthSwitchPreuseOperation {
|
|
|
234
327
|
revision: number;
|
|
235
328
|
}
|
|
236
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Why the host could not prove that Claude Code uses its own native login.
|
|
332
|
+
*
|
|
333
|
+
* Each value names one condition of that proof, so an owner is told what to change: install the supported
|
|
334
|
+
* release, sign in to a subscriber plan, remove a credential override or a settings source, let the
|
|
335
|
+
* profile read succeed, or close the running session.
|
|
336
|
+
*/
|
|
337
|
+
export type TAuthSwitchClaudeProofFailure =
|
|
338
|
+
| 'unsupported_release' | 'override' | 'profile' | 'profile_unreadable'
|
|
339
|
+
| 'settings' | 'subscription' | 'running_session';
|
|
340
|
+
|
|
237
341
|
/** Credential-free native file ownership result. It makes no claim about a running session's active request. */
|
|
238
342
|
export interface IAuthSwitchClaudeNativeHandoff {
|
|
239
343
|
id: string;
|
|
@@ -243,6 +347,8 @@ export interface IAuthSwitchClaudeNativeHandoff {
|
|
|
243
347
|
phase: 'reserved' | 'prepared' | 'committed' | 'aborted' | 'quarantined';
|
|
244
348
|
problem: 'none' | 'native_uncertain' | 'foreign_or_torn' | 'unsupported_effective_auth' | 'database_uncertain';
|
|
245
349
|
runningEffectiveAuth: 'no_scoped_sessions' | 'unsupported_effective_auth' | null;
|
|
350
|
+
/** Which condition of the native-login proof failed, when `problem` is `unsupported_effective_auth`. */
|
|
351
|
+
proofFailure?: TAuthSwitchClaudeProofFailure;
|
|
246
352
|
updatedAt: string;
|
|
247
353
|
}
|
|
248
354
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
|
|
2
|
+
import { asAuthSwitchRefusal, authSwitchImportRefusalMarker } from './authority-contract.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The one-time account import: reading the legacy stores that existed before the authority, and the
|
|
@@ -148,20 +149,20 @@ export type TAuthSwitchImportAction = 'none' | 'resume' | 'device_login';
|
|
|
148
149
|
/**
|
|
149
150
|
* Marks an error answer whose text is an instruction for the owner, not a report of a fault.
|
|
150
151
|
*
|
|
151
|
-
* The daemon sets it on the refusals the importer authors and on nothing else
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
* The daemon sets it on the refusals the importer authors and on nothing else: a refusal any other route
|
|
153
|
+
* authors carries the authority's own marker, and a failure neither of them decided answers with the
|
|
154
|
+
* transport's sanitised text, which a caller must never present as if it said what to do next. For a
|
|
155
|
+
* submit in particular, an unmarked failure means the outcome is unknown and the source must be read with
|
|
156
|
+
* `authswitch.authority.import.status` rather than submitted again.
|
|
157
|
+
*
|
|
158
|
+
* It is one code of the authority's refusal contract (`./authority-contract`), which every route now uses;
|
|
159
|
+
* this marker keeps its own value on the wire so a consumer written against 8.1.0 keeps working.
|
|
155
160
|
*/
|
|
156
|
-
export const authSwitchImportRefusalReason =
|
|
161
|
+
export const authSwitchImportRefusalReason = authSwitchImportRefusalMarker;
|
|
157
162
|
|
|
158
163
|
/** True when the daemon answered with an importer refusal, so `error.message` is the instruction to show. */
|
|
159
|
-
export const isAuthSwitchImportRefusal = (error: unknown): boolean =>
|
|
160
|
-
|
|
161
|
-
const data = error.errorData;
|
|
162
|
-
return typeof data === 'object' && data !== null && 'reason' in data
|
|
163
|
-
&& data.reason === authSwitchImportRefusalReason;
|
|
164
|
-
};
|
|
164
|
+
export const isAuthSwitchImportRefusal = (error: unknown): boolean =>
|
|
165
|
+
asAuthSwitchRefusal(error)?.code === 'import_refusal';
|
|
165
166
|
|
|
166
167
|
/**
|
|
167
168
|
* Where one source stands, read from the migration ledger, the grant it produced and its handoff.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type { IAuthSwitchAccount, IAuthSwitchAccountEvent, IAuthSwitchBinding, IAuthSwitchLogin,
|
|
3
3
|
IAuthSwitchOperation, IAuthSwitchSnapshot } from './authority-contract.js';
|
|
4
|
+
import { AuthSwitchRefusal } from './authority-contract.js';
|
|
4
5
|
import { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
|
|
5
6
|
import type { IStoredAuthorityAccount, IStoredAuthorityBinding, IStoredAuthorityGrant,
|
|
6
7
|
IStoredAuthorityDeviceOperation } from './classes.authoritymodels.js';
|
|
@@ -554,7 +555,7 @@ export class AuthSwitchAuthorityBroker {
|
|
|
554
555
|
if (!isId(accountId) || !validRevision(expectedRevision) || !safeLabel(label)) throw new Error('Invalid account change.');
|
|
555
556
|
const updateId = plugins.crypto.randomUUID();
|
|
556
557
|
const updated = await this.database.changeAccount(updateId, accountId, account => {
|
|
557
|
-
if (!account || account.removed || account.revision !== expectedRevision) throw new
|
|
558
|
+
if (!account || account.removed || account.revision !== expectedRevision) throw new AuthSwitchRefusal('account_changed', 'Account changed; refresh before editing.');
|
|
558
559
|
return { ...account, label, revision: account.revision + 1, updateId,
|
|
559
560
|
statusObservedAt: new Date(this.now()).toISOString() };
|
|
560
561
|
});
|
|
@@ -68,7 +68,13 @@ export interface IAuthSwitchSubscriptionOptions {
|
|
|
68
68
|
heartbeatMs?: number;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Node-only client for the per-user daemon. Each request has its own bounded Unix connection.
|
|
73
|
+
*
|
|
74
|
+
* Every method takes an optional `AbortSignal`. A referenced socket with a 35 second timeout outlives a
|
|
75
|
+
* consumer's own stop unless that consumer can cancel the read, so the signal is what releases it; it
|
|
76
|
+
* detaches this request only and never cancels work the daemon already owns.
|
|
77
|
+
*/
|
|
72
78
|
export class AuthSwitchClient {
|
|
73
79
|
private readonly target: plugins.typedrequest.TypedTarget;
|
|
74
80
|
private readonly runtimeTarget: plugins.typedrequest.TypedTarget;
|
|
@@ -119,9 +125,10 @@ export class AuthSwitchClient {
|
|
|
119
125
|
{ submission, callerQuiescent: true, acknowledgeRunOrder: true }, 120_000, true, signal)).result;
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
public async getUsage(accountId: string, loginId: string, force = false
|
|
128
|
+
public async getUsage(accountId: string, loginId: string, force = false,
|
|
129
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchUsage['response']['usage']> {
|
|
123
130
|
return (await this.request<IReq_AuthSwitchUsage>('authswitch.authority.usage',
|
|
124
|
-
{ accountId, loginId, force })).usage;
|
|
131
|
+
{ accountId, loginId, force }, 35_000, false, signal)).usage;
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
/** Assemble a consistent view from bounded database pages; restart if a writer changes the revision. */
|
|
@@ -258,8 +265,10 @@ export class AuthSwitchClient {
|
|
|
258
265
|
return (await this.request<IReq_AuthSwitchBeginReauth>('authswitch.authority.reauth',
|
|
259
266
|
{ operationId, accountId, loginId, purpose, flow: 'device' }, 35_000, false, signal)).operation;
|
|
260
267
|
}
|
|
261
|
-
public listOperations(after?: string, limit?: number
|
|
262
|
-
|
|
268
|
+
public listOperations(after?: string, limit?: number,
|
|
269
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchListOperations['response']> {
|
|
270
|
+
return this.request<IReq_AuthSwitchListOperations>('authswitch.authority.operations',
|
|
271
|
+
{ after, limit }, 35_000, false, signal);
|
|
263
272
|
}
|
|
264
273
|
public async getOperation(operationId: string, signal?: AbortSignal): Promise<IReq_AuthSwitchGetOperation['response']['operation']> {
|
|
265
274
|
return (await this.request<IReq_AuthSwitchGetOperation>('authswitch.authority.operation',
|
|
@@ -284,27 +293,39 @@ export class AuthSwitchClient {
|
|
|
284
293
|
return (await this.request<IReq_AuthSwitchCancelPreuse>(
|
|
285
294
|
'authswitch.authority.preuse.cancel', { operationId }, 35_000, false, signal)).operation;
|
|
286
295
|
}
|
|
287
|
-
public async renameAccount(accountId: string, expectedRevision: number, label: string
|
|
288
|
-
|
|
296
|
+
public async renameAccount(accountId: string, expectedRevision: number, label: string,
|
|
297
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchRenameAccount['response']['account']> {
|
|
298
|
+
return (await this.request<IReq_AuthSwitchRenameAccount>('authswitch.authority.rename',
|
|
299
|
+
{ accountId, expectedRevision, label }, 35_000, false, signal)).account;
|
|
289
300
|
}
|
|
290
|
-
public async removeAccount(accountId: string, expectedRevision: number
|
|
291
|
-
|
|
301
|
+
public async removeAccount(accountId: string, expectedRevision: number,
|
|
302
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchRemoveAccount['response']['account']> {
|
|
303
|
+
return (await this.request<IReq_AuthSwitchRemoveAccount>('authswitch.authority.remove',
|
|
304
|
+
{ accountId, expectedRevision }, 35_000, false, signal)).account;
|
|
292
305
|
}
|
|
293
|
-
public async switchClaudeNative(accountId: string, loginId: string
|
|
306
|
+
public async switchClaudeNative(accountId: string, loginId: string,
|
|
307
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchSwitchClaudeNative['response']['handoff']> {
|
|
294
308
|
return (await this.request<IReq_AuthSwitchSwitchClaudeNative>('authswitch.authority.claude.switch',
|
|
295
|
-
{ accountId, loginId, purpose: 'claude_host_native' })).handoff;
|
|
309
|
+
{ accountId, loginId, purpose: 'claude_host_native' }, 35_000, false, signal)).handoff;
|
|
296
310
|
}
|
|
297
|
-
public async getClaudeNativeHandoff(operationId: string
|
|
298
|
-
|
|
311
|
+
public async getClaudeNativeHandoff(operationId: string,
|
|
312
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchClaudeNativeHandoff['response']['handoff']> {
|
|
313
|
+
return (await this.request<IReq_AuthSwitchClaudeNativeHandoff>('authswitch.authority.claude.handoff',
|
|
314
|
+
{ operationId }, 35_000, false, signal)).handoff;
|
|
299
315
|
}
|
|
300
|
-
public listClaudeNativeHandoffs(after?: string, limit?: number
|
|
301
|
-
|
|
316
|
+
public listClaudeNativeHandoffs(after?: string, limit?: number,
|
|
317
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchClaudeNativeHandoffs['response']> {
|
|
318
|
+
return this.request<IReq_AuthSwitchClaudeNativeHandoffs>('authswitch.authority.claude.handoffs',
|
|
319
|
+
{ after, limit }, 35_000, false, signal);
|
|
302
320
|
}
|
|
303
|
-
public bindAccount(input: IReq_AuthSwitchBindAccount['request']
|
|
304
|
-
|
|
321
|
+
public bindAccount(input: IReq_AuthSwitchBindAccount['request'],
|
|
322
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchBindAccount['response']> {
|
|
323
|
+
return this.request<IReq_AuthSwitchBindAccount>('authswitch.authority.bind', input, 35_000, false, signal);
|
|
305
324
|
}
|
|
306
|
-
public resolveAccess(input: IReq_AuthSwitchResolveAccess['request']
|
|
307
|
-
|
|
325
|
+
public resolveAccess(input: IReq_AuthSwitchResolveAccess['request'],
|
|
326
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchResolveAccess['response']> {
|
|
327
|
+
return this.request<IReq_AuthSwitchResolveAccess>('authswitch.authority.resolveAccess',
|
|
328
|
+
input, 35_000, true, signal);
|
|
308
329
|
}
|
|
309
330
|
public releaseBinding(input: IReq_AuthSwitchReleaseBinding['request'], signal?: AbortSignal): Promise<IReq_AuthSwitchReleaseBinding['response']> {
|
|
310
331
|
return this.request<IReq_AuthSwitchReleaseBinding>('authswitch.authority.release', input, 35_000, true, signal);
|
|
@@ -320,9 +341,10 @@ export class AuthSwitchRuntimeClient {
|
|
|
320
341
|
postMethod: (payload, options) => post(runtimeSocketPath, payload, options?.signal),
|
|
321
342
|
});
|
|
322
343
|
}
|
|
323
|
-
public resolveAccess(input: IReq_AuthSwitchResolveAccess['request']
|
|
344
|
+
public resolveAccess(input: IReq_AuthSwitchResolveAccess['request'],
|
|
345
|
+
signal?: AbortSignal): Promise<IReq_AuthSwitchResolveAccess['response']> {
|
|
324
346
|
return new plugins.typedrequest.TypedRequest<IReq_AuthSwitchResolveAccess>(this.target,
|
|
325
|
-
'authswitch.authority.resolveAccess').fire(input, { timeoutMs: 35_000, maxRetries: 0 });
|
|
347
|
+
'authswitch.authority.resolveAccess').fire(input, { timeoutMs: 35_000, maxRetries: 0, abortSignal: signal });
|
|
326
348
|
}
|
|
327
349
|
public releaseBinding(input: IReq_AuthSwitchReleaseBinding['request'], signal?: AbortSignal): Promise<IReq_AuthSwitchReleaseBinding['response']> {
|
|
328
350
|
return new plugins.typedrequest.TypedRequest<IReq_AuthSwitchReleaseBinding>(this.target,
|