@modelprofile.com/authswitch 8.2.0 → 9.1.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 +84 -1
- package/dist_ts/authority-contract.js +14 -2
- package/dist_ts/authority-import-contract.d.ts +6 -0
- package/dist_ts/authority-paths.d.ts +37 -0
- package/dist_ts/authority-paths.js +46 -0
- package/dist_ts/authority-runtime-contract.d.ts +11 -1
- package/dist_ts/classes.authoritybroker.d.ts +23 -8
- package/dist_ts/classes.authoritybroker.js +155 -32
- package/dist_ts/classes.authorityclient.d.ts +22 -2
- package/dist_ts/classes.authorityclient.js +98 -13
- package/dist_ts/classes.authoritydaemon.d.ts +21 -3
- package/dist_ts/classes.authoritydaemon.js +77 -32
- package/dist_ts/classes.authoritydatabase.d.ts +21 -3
- package/dist_ts/classes.authoritydatabase.js +98 -12
- package/dist_ts/classes.authorityimport.d.ts +16 -4
- package/dist_ts/classes.authorityimport.js +75 -24
- package/dist_ts/classes.authoritymodels.js +5 -3
- package/dist_ts/classes.authoritypreuse.js +8 -3
- package/dist_ts/classes.authorityservice.d.ts +10 -11
- package/dist_ts/classes.authorityservice.js +14 -23
- package/dist_ts/classes.cli.d.ts +10 -2
- package/dist_ts/classes.cli.js +12 -4
- package/dist_ts/classes.codexmanaged.d.ts +0 -1
- package/dist_ts/classes.codexmanaged.js +13 -26
- package/dist_ts/classes.legacyfence.d.ts +53 -0
- package/dist_ts/classes.legacyfence.js +189 -0
- package/dist_ts/classes.operations.d.ts +15 -3
- package/dist_ts/classes.operations.js +22 -4
- package/dist_ts/classes.service.d.ts +21 -2
- package/dist_ts/classes.service.js +35 -8
- package/dist_ts/classes.tui.d.ts +2 -1
- package/dist_ts/classes.tui.js +3 -2
- 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/dist_ts/ts_migration/legacysources/authswitchstores.js +5 -2
- package/package.json +11 -11
- package/readme.md +194 -24
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +90 -4
- package/ts/authority-import-contract.ts +6 -0
- package/ts/authority-paths.ts +69 -0
- package/ts/authority-runtime-contract.ts +11 -1
- package/ts/classes.authoritybroker.ts +153 -33
- package/ts/classes.authorityclient.ts +102 -15
- package/ts/classes.authoritydaemon.ts +89 -27
- package/ts/classes.authoritydatabase.ts +98 -12
- package/ts/classes.authorityimport.ts +102 -25
- package/ts/classes.authoritymodels.ts +4 -1
- package/ts/classes.authoritypreuse.ts +7 -1
- package/ts/classes.authorityservice.ts +15 -30
- package/ts/classes.cli.ts +14 -3
- package/ts/classes.codexmanaged.ts +10 -19
- package/ts/classes.legacyfence.ts +219 -0
- package/ts/classes.operations.ts +22 -3
- package/ts/classes.service.ts +45 -8
- package/ts/classes.tui.ts +3 -1
- 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/ts/ts_migration/legacysources/authswitchstores.ts +4 -1
package/readme.md
CHANGED
|
@@ -32,7 +32,23 @@ bindings, durable device-operation receipts, Codex enrollment records, and nativ
|
|
|
32
32
|
owns OpenAI device login, targeted reauthentication, and proactive refresh for grants
|
|
33
33
|
added through this API. Management snapshots expose separate account and login rows,
|
|
34
34
|
including each login's purpose, owner, health, problem and available actions; neither
|
|
35
|
-
snapshots nor events contain credentials.
|
|
35
|
+
snapshots nor events contain credentials. A login that a native tool refreshes also names
|
|
36
|
+
that tool -- `ownerTool` is `claude_code`, `codex` or `opencode`, and `null` exactly when no
|
|
37
|
+
native tool holds it, because the authority refreshes it or nothing does. The authority
|
|
38
|
+
names it rather than leaving a consumer to derive it from the purpose, which would be wrong
|
|
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`.
|
|
36
52
|
|
|
37
53
|
```ts
|
|
38
54
|
import { randomUUID } from 'node:crypto';
|
|
@@ -46,18 +62,30 @@ const operationId = randomUUID();
|
|
|
46
62
|
const operation = await client.beginAddOpenAi(operationId);
|
|
47
63
|
// Keep operation.id to discover its final result after navigation or reconnect.
|
|
48
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
|
+
}
|
|
49
70
|
```
|
|
50
71
|
|
|
51
72
|
Browser code can import the credential-free DTOs from
|
|
52
73
|
`@modelprofile.com/authswitch/authority-contract`. The backend-only binding, access and release
|
|
53
74
|
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`;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
import's own DTOs by `@modelprofile.com/authswitch/authority-import-contract`; those capabilities must
|
|
76
|
+
never reach a browser. Two of the three runtime requests are served on the runtime socket -- resolving
|
|
77
|
+
access and releasing a binding -- while `authswitch.authority.bind` is served on the **management** socket
|
|
78
|
+
by design: minting a capability is the act that authorizes a runtime, so only a trusted management caller
|
|
79
|
+
may perform it, and a container that is given the runtime directory can use a binding it was handed but can
|
|
80
|
+
never create one. A backend binds an account to a runtime incarnation by exact account ID, login ID and
|
|
81
|
+
purpose, and keeps the returned capability private. Targeted reauthentication likewise requires the
|
|
82
|
+
exact login; an account's presentation default never selects a grant for either action. Completed and
|
|
59
83
|
interrupted operations remain discoverable through `getOperation()` and paged
|
|
60
|
-
`listOperations()`.
|
|
84
|
+
`listOperations()`. `watchOperation(id, afterRevision, waitMs = 30000)` is the same read as a long poll,
|
|
85
|
+
the way `events` waits: it answers as soon as the operation's revision passes `afterRevision` -- a sign-in
|
|
86
|
+
answers `starting` with no prompt, then `pending` with its device prompt, then its outcome -- at once for a
|
|
87
|
+
finished operation, and with the unchanged operation once `waitMs` (at most 30000) runs out. Only a change
|
|
88
|
+
of that operation answers it. A completed receipt and its account/grant change commit together, so
|
|
61
89
|
the receipt resolves a lost response or an event that arrives first. Add and reauthentication
|
|
62
90
|
callers supply one UUID for the logical start and reuse it after a lost response. Replaying that
|
|
63
91
|
UUID returns the same matching receipt without starting another provider login; using it for a
|
|
@@ -68,16 +96,29 @@ resolves that binding to a current access token; its directory can be mounted in
|
|
|
68
96
|
container without exposing the management socket. A provider that rejects a specific
|
|
69
97
|
access-token generation can request a newer one through `rejectedGrantGeneration`; the
|
|
70
98
|
authority coalesces concurrent rejection callbacks and never replays an uncertain refresh.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
runtime
|
|
99
|
+
A ChatGPT login backs a Claude Code runtime too (`runtime: 'claude'`); a Claude account never binds,
|
|
100
|
+
because it reaches Claude Code through the native switch. After an external Flex, OpenCode or Claude
|
|
101
|
+
runtime has fenced new work and drained its pending access resolutions and provider requests, it can
|
|
102
|
+
release its exact binding capability through the runtime socket. Every bind receives a fresh random capability, so a stale release cannot fence
|
|
74
103
|
or delete a successor even if the caller reused an incarnation label. Release waits for admitted
|
|
75
104
|
server-side resolution handlers, but handler completion does not prove response delivery and
|
|
76
105
|
cannot retract an access token already received or in use; the runtime owner therefore owns that
|
|
77
106
|
drain. Generic release cannot revoke a managed Codex binding, whose daemon-owned stop still closes
|
|
78
107
|
the runtime and clears its durable run before internal revocation. Binding release never transfers
|
|
79
|
-
or changes ownership of the account grant.
|
|
80
|
-
a
|
|
108
|
+
or changes ownership of the account grant. A holder that crashed and lost its capability recovers
|
|
109
|
+
its binding without a new route: the snapshot (and `getBinding`) publishes each binding's runtime and
|
|
110
|
+
scope, binding that runtime and scope again replaces the capability, and releasing the returned
|
|
111
|
+
capability releases the binding. If binding again is refused because the account needs a new sign-in,
|
|
112
|
+
reauthenticate it first.
|
|
113
|
+
|
|
114
|
+
`getBinding(bindingId)` (`authswitch.authority.binding`) reads one binding by the id `bind` returned,
|
|
115
|
+
credential-free, or `null` once the authority holds none by that id, so a backend checks its binding without
|
|
116
|
+
reading the whole snapshot.
|
|
117
|
+
|
|
118
|
+
Removing an account that still backs a Flex, OpenCode or Claude binding is refused with `account_busy`, naming
|
|
119
|
+
those runtimes: their holder stops them and releases the bindings first, so a removal never strands a
|
|
120
|
+
runtime whose next access would fail. Managed Codex bindings are the daemon's own; removal drains managed
|
|
121
|
+
Codex as before and then takes them with the account.
|
|
81
122
|
|
|
82
123
|
Every route on either socket answers in one of three ways: a result, a **refusal**, or a fault. A refusal
|
|
83
124
|
is an answer -- the daemon completed its check, nothing was left half-done, and its message says what the
|
|
@@ -91,9 +132,29 @@ instruction for an owner.
|
|
|
91
132
|
that marker: it answers `{ code, instruction }` for a refusal and `null` for a fault. The code is the
|
|
92
133
|
closed `TAuthSwitchRefusalCode` set, which is what a client branches on without reading the text:
|
|
93
134
|
`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
|
|
96
|
-
|
|
135
|
+
`binding_unauthorized`, `native_owner_holds_login`, `claude_home_unregistered`, `claude_handoff_pending`,
|
|
136
|
+
`claude_receipt_missing`, `import_refusal`, `codex_unsupported`, `invalid_input`, `login_needs_reauth` and
|
|
137
|
+
`access_not_fresh`, plus the two the legacy commands answer with on this side of
|
|
138
|
+
the socket, `authority_holds_login` and `authority_unavailable` (see "The legacy fence" below). The
|
|
139
|
+
instruction is authored for the owner and contains no path, credential or digest; only an importer refusal
|
|
140
|
+
may name the process the owner has to stop. The set is producer-owned: a consumer branches on the codes it
|
|
141
|
+
knows and keeps a default branch, which is what lets the authority name a new one without breaking it.
|
|
142
|
+
|
|
143
|
+
The runtime's own answers are named the same way. `authswitch.authority.bind` refuses `login_unavailable`
|
|
144
|
+
when the authority does not hold the login itself, and `authswitch.authority.resolveAccess` refuses
|
|
145
|
+
`binding_unauthorized` for a capability that authorizes nothing -- missing, wrong, or superseded while the
|
|
146
|
+
credential was being resolved. Those are one answer on purpose: an absent binding and a wrong capability are
|
|
147
|
+
compared against the same zero hash, so nothing distinguishes them, and the repair is the same either way --
|
|
148
|
+
bind again. A binding whose account has since been reauthorized reads `login_unavailable` instead, because
|
|
149
|
+
binding again cannot help until that account holds a login again. A device sign-in or preuse operation this
|
|
150
|
+
authority does not hold reads `not_found`, from whichever route met it. What the access loop decides about
|
|
151
|
+
the login itself is marked as well: `login_needs_reauth` when only a new device sign-in brings the login back
|
|
152
|
+
(after which a bound runtime binds again), and `access_not_fresh` when the login is intact but the provider
|
|
153
|
+
could not renew its access yet, so the same request can succeed later. A changed identity or a view that
|
|
154
|
+
keeps moving during resolution stays an unmarked fault, because nobody decided it. Binding an *account* this
|
|
155
|
+
authority does not hold at all stays one too: the storage layer throws `Account is not available for this runtime.`,
|
|
156
|
+
which a socket caller reads as `Internal server error`, because naming it is a decision about which layer
|
|
157
|
+
authors owner instructions rather than a wording fix.
|
|
97
158
|
|
|
98
159
|
The import routes keep the marker they published in 8.1.0, `authswitch_import_refusal`, and
|
|
99
160
|
`isAuthSwitchImportRefusal` from `@modelprofile.com/authswitch/authority-import-contract` is the same
|
|
@@ -104,6 +165,15 @@ and the ledger row it wrote says where that source stands -- while an unmarked f
|
|
|
104
165
|
unknown, and that source must then be read with `authswitch.authority.import.status` rather than
|
|
105
166
|
submitted again.
|
|
106
167
|
|
|
168
|
+
A daemon that is shutting down admits no new request -- every route answers `authority_closing` -- and
|
|
169
|
+
settles the requests it already admitted before it closes its store: a waiting `events` or
|
|
170
|
+
`watchOperation` read answers normally from the open store instead of failing.
|
|
171
|
+
|
|
172
|
+
A client reading a daemon that runs an older release -- one not restarted after an upgrade -- fails with
|
|
173
|
+
`AuthSwitchDaemonOutdatedError`, which names the restart (`authswitch authority service stop`, then
|
|
174
|
+
`start`), and a subscription reports `unavailable` with the reason `daemon_outdated` once and retries every
|
|
175
|
+
five seconds until the daemon is restarted.
|
|
176
|
+
|
|
107
177
|
`AuthSwitchClient.subscribe(onSnapshot, onEvent, signal, { onStatus })` reports `current`
|
|
108
178
|
after a fresh snapshot or verified heartbeat, `unavailable` on disconnect or resync, and
|
|
109
179
|
`closed` on abort. Keep cached account actions disabled while status is not `current`.
|
|
@@ -259,11 +329,19 @@ native harness process or Codex's app-server is running, and an app-server it ca
|
|
|
259
329
|
An adopt whose refresh fails, or whose answer is lost, is never retried: the source is quarantined, its grant
|
|
260
330
|
is left needing re-authentication, and the command says so. A projection fails differently, because nothing
|
|
261
331
|
rotating was ever sent for it: a native store that could not be proven live stays a pending native owner and
|
|
262
|
-
is imported by submitting the same source again once its own tool has refreshed it.
|
|
332
|
+
is imported by submitting the same source again once its own tool has refreshed it. That refresh rewrites the
|
|
333
|
+
file, so the retry carries the digest the inventory prints afterwards: a row waiting for its native owner
|
|
334
|
+
follows its own tool's bytes, and nothing else does. What it never follows is another login. A row names the
|
|
335
|
+
account and the grant it was approved for, and a source that now holds a different account's login is refused
|
|
336
|
+
with that stated -- before the account is registered, because the provider read that proves a projection asks
|
|
337
|
+
about the identity the new bytes themselves carry and would prove the wrong login perfectly well. Neither
|
|
338
|
+
that refusal nor "already imported" ever clears: no route forgets a row or re-points it at another account.
|
|
263
339
|
|
|
264
340
|
`import status` reads the migration ledger, the grants and the handoffs -- never the host -- and reports, per
|
|
265
341
|
source, where it stands, which owner refreshes its login now, and what to do next: nothing, submit the same
|
|
266
|
-
source again, or sign in to that account again.
|
|
342
|
+
source again, or sign in to that account again. Each entry also carries the source's `sourcePathHash`, so a
|
|
343
|
+
backend that submitted an external source finds its own record by `sourceKind` and that hash -- also after a
|
|
344
|
+
submit whose outcome it never learned -- without deriving the ledger row id. Submitting a source again is how an interrupted run resumes,
|
|
267
345
|
and the answer comes from the same durable record the daemon decides on: a source whose sign-in was in
|
|
268
346
|
flight when the run died is reported as needing a device sign-in, not as resumable, because a rotating
|
|
269
347
|
refresh token is never sent a second time. Sources that were never submitted have no ledger row and appear
|
|
@@ -273,6 +351,17 @@ An import deletes nothing: retiring the legacy files is a separate, later step.
|
|
|
273
351
|
deliberately not backed up, so losing it costs one device sign-in per account -- and the remote-control
|
|
274
352
|
enrollments and runtime bindings with it.
|
|
275
353
|
|
|
354
|
+
**Where a daemon reads those stores is stated by whoever builds it, never defaulted.** The legacy stores are
|
|
355
|
+
this user's real credentials, and every location they are found at comes from the environment, so
|
|
356
|
+
`AuthSwitchAuthorityDaemon` takes a required `legacyImport`: either the locations to read (an `env` and an
|
|
357
|
+
absolute `homeDirectory`, plus the provider operations that import uses) or `'none'`, which builds a daemon
|
|
358
|
+
with no account import at all and answers `import inventory`, `import status` and `import submit` with the
|
|
359
|
+
`import_refusal` code -- a decided refusal of the closed set, never an empty inventory and never a fault, so
|
|
360
|
+
a caller reads it exactly as it reads every other import refusal. A construction that names neither -- no
|
|
361
|
+
locations and no `'none'` -- is refused before the daemon exists, rather than reading whichever host happened
|
|
362
|
+
to start it. The per-user daemon this package installs -- `authswitch authority daemon`, the service unit's
|
|
363
|
+
own entry point -- is the one place that takes those locations from this host.
|
|
364
|
+
|
|
276
365
|
`AuthSwitchClient.getUsage(accountId, loginId)` reads OpenAI managed limits or a Claude
|
|
277
366
|
host login's profile and usage through the management socket. The tagged, credential-free
|
|
278
367
|
reading preserves Claude's labelled five-hour, weekly and model windows, plan provenance,
|
|
@@ -298,7 +387,7 @@ generated prose and credentials are absent from persistence and management respo
|
|
|
298
387
|
removal and targeted reauthentication refuse while that account has an active preuse request.
|
|
299
388
|
|
|
300
389
|
A trusted in-process daemon host can call `AuthSwitchAuthorityDaemon.startManagedCodex()`
|
|
301
|
-
for a bound account and workspace. It owns a private Codex
|
|
390
|
+
for a bound account and workspace. It owns a private Codex app-server, passes only
|
|
302
391
|
the authority's current access token through Codex's external-token login, answers its
|
|
303
392
|
unauthorized callback through the authority, and revokes the binding on stop. The returned
|
|
304
393
|
runtime exposes an interactive `codex --remote unix://…` launch descriptor and native
|
|
@@ -314,15 +403,37 @@ gone; an uncertain launch remains blocked. Callers must explicitly stop owned pr
|
|
|
314
403
|
after their work finishes. Once reauthentication starts, it holds new admissions until the
|
|
315
404
|
device operation settles. Only the private socket directory is removed on close. Codex's
|
|
316
405
|
credential store remains ephemeral and does not read or write `auth.json` in that home.
|
|
406
|
+
Codex binds the app-server socket in its own per-user directory,
|
|
407
|
+
`<realpath /tmp>/codex-daemon-<euid>`, and publishes the private socket path as a link to it.
|
|
408
|
+
The daemon checks that publication with `@modelprofile.com/mcp-crossharness`'s
|
|
409
|
+
`observeCodexUnixSocket` and connects only when the link is exactly Codex's own, in a
|
|
410
|
+
directory other users cannot rewrite, and the socket behind it is the user's own in a 0700
|
|
411
|
+
directory of the user's; anything else fails the start with that package's error. Codex keeps a
|
|
412
|
+
zero-byte startup lock in that directory for each socket path it has served; the file is
|
|
413
|
+
Codex's own and authswitch does not remove it.
|
|
414
|
+
Managed Codex runs Codex 0.156.0 or newer -- the floor `@modelprofile.com/mcp-crossharness`'s
|
|
415
|
+
`requireCodexVersion` checks, the first release that publishes the socket alias -- and checks the
|
|
416
|
+
contract at every start instead of pinning one release. Before anything is recorded or started, the
|
|
417
|
+
`codex` it will run must report a readable version at or above the floor, and its own protocol
|
|
418
|
+
description (`codex app-server generate-json-schema`) must still offer every surface managed Codex uses:
|
|
419
|
+
the requests `account/login/start`, `thread/start`, `thread/resume` and `turn/start`; the external
|
|
420
|
+
ChatGPT token login with the fields it sends; the `account/chatgptAuthTokens/refresh` callback with the
|
|
421
|
+
`unauthorized` reason and the reply it gives; and the thread and turn parameters it sends. After
|
|
422
|
+
connecting, the app-server must report exactly the version that was checked, because Codex can replace
|
|
423
|
+
itself between the check and the start. Each failure is the `codex_unsupported` refusal naming the
|
|
424
|
+
version or the missing surface, so a Codex update that drops something managed Codex relies on stops new
|
|
425
|
+
managed sessions by name instead of failing one later. A prerelease above the floor runs; a prerelease of
|
|
426
|
+
0.156.0 itself is below it.
|
|
317
427
|
Managed-Codex process ownership observation is qualified on Linux and reads exact argument
|
|
318
428
|
boundaries from `/proc`; on other hosts doctor reports the process observation as unknown,
|
|
319
429
|
and authority actions that require survivor proof refuse instead of guessing.
|
|
320
|
-
Codex 0.
|
|
430
|
+
Codex (verified through 0.157) does not persist an unused thread created by `thread/start`; resume applies
|
|
321
431
|
after Codex has committed a rollout. The existing CLI has not yet been moved to this path.
|
|
322
432
|
|
|
323
433
|
This SDK is additive at this stage. The commands documented below still use the existing
|
|
324
|
-
credential stores
|
|
325
|
-
|
|
434
|
+
credential stores, except that the legacy fence below refuses them a login the authority
|
|
435
|
+
holds. The authority daemon does not import those stores automatically, replace their
|
|
436
|
+
active native sessions, or take over their refresh grants. An active
|
|
326
437
|
Codex/OpenCode native grant remains pending until its owner is idle and an explicit
|
|
327
438
|
handoff verifies the latest native source. Claude Code's own native credential files
|
|
328
439
|
remain vendor-owned; a container setup-token grant is a separate grant under the same
|
|
@@ -353,6 +464,62 @@ restore system could have written.
|
|
|
353
464
|
The store is deliberately not backed up: its refresh grants are sealed to this host's TPM,
|
|
354
465
|
so a lost store is recovered by one device re-login per account.
|
|
355
466
|
|
|
467
|
+
### The legacy fence
|
|
468
|
+
|
|
469
|
+
Once the authority has taken a saved copy over, the legacy `save` and `switch` must not write it again: an
|
|
470
|
+
adopted copy's refresh token was rotated away by the import, and activating it would hand a tool a dead
|
|
471
|
+
login. `runAuthSwitchMutation` -- the one path the command line, the guide, `watch` and the hosted
|
|
472
|
+
`AuthSwitchService` all take -- asks first, and decides in three branches.
|
|
473
|
+
|
|
474
|
+
- **The authority answers**: it is asked for this record's ledger row through `import status` on the
|
|
475
|
+
management socket, keyed by the same source id the importer uses (a hash of the saved copy's path). A
|
|
476
|
+
`verified` or `complete` row refuses with `authority_holds_login`; any other row, or none, lets the command
|
|
477
|
+
run. An authority that answers decides, whether or not its service unit is installed.
|
|
478
|
+
- **Nothing answers within four seconds -- no socket, a silent one, or no `XDG_RUNTIME_DIR` -- and the
|
|
479
|
+
authority's service unit is installed**: the command refuses with `authority_unavailable` and the
|
|
480
|
+
instruction to run `authswitch authority service start`. Unknown is never treated as idle.
|
|
481
|
+
- **Nothing answers and no unit is installed**: the legacy commands behave exactly as before. This is every
|
|
482
|
+
host before the cutover.
|
|
483
|
+
|
|
484
|
+
What tells the second branch from the third is durable, never the socket: the runtime directory is tmpfs,
|
|
485
|
+
so after a reboot the socket is absent until the user service starts -- exactly when a fence keyed on it
|
|
486
|
+
would open. The evidence is the unit `authswitch authority service install` writes,
|
|
487
|
+
`$XDG_DATA_HOME/systemd/user/authswitch-authority.service` (default
|
|
488
|
+
`~/.local/share/systemd/user/authswitch-authority.service`), read through smartdaemon's
|
|
489
|
+
`SystemdUnitFile.inspect()`. The installer and the fence derive that directory from one rule,
|
|
490
|
+
`$XDG_DATA_HOME` or else the home directory; the installer takes both from its own process and its account's
|
|
491
|
+
home, the fence from the locations its caller states. The two agree unless `$HOME` is overridden while
|
|
492
|
+
`XDG_DATA_HOME` is unset, and then every store the fence guards lies under that overridden home too. The fence
|
|
493
|
+
writes nothing and keeps no record of its own.
|
|
494
|
+
|
|
495
|
+
The check works without a login session. Reading the unit needs only its directory, so a cron job or a plain
|
|
496
|
+
ssh command with no `XDG_RUNTIME_DIR` is decided by the unit exactly like a desktop session: no unit, the
|
|
497
|
+
legacy commands run; the unit installed, they refuse with `authority_unavailable`. The stated
|
|
498
|
+
`XDG_RUNTIME_DIR` names only the socket to ask; an absent one is never replaced by this process's own. The
|
|
499
|
+
answer depends on the unit directory derived from the stated `XDG_DATA_HOME` or home, and on nothing else:
|
|
500
|
+
a malformed `XDG_RUNTIME_DIR` or `XDG_CONFIG_HOME` never refuses the command, and neither does a malformed
|
|
501
|
+
process `XDG_DATA_HOME` when the caller states other locations. A unit that cannot be inspected -- a file the
|
|
502
|
+
installer would not have written, a directory another user can write -- refuses rather than being read as
|
|
503
|
+
absent, and so does a stated `XDG_DATA_HOME` that names no usable unit directory, because the reader can
|
|
504
|
+
then tell an installed authority from none no better. On a platform without systemd no unit can be
|
|
505
|
+
installed.
|
|
506
|
+
|
|
507
|
+
The unit says an authority exists, not which stores it imports from, so while an installed authority is
|
|
508
|
+
stopped the legacy save and switch refuse for every saved copy of every store, until it runs again. A
|
|
509
|
+
record the importer could never have read -- an account id that names no saved-copy file -- is never asked
|
|
510
|
+
about.
|
|
511
|
+
|
|
512
|
+
`drop` / `rm` and the `remove` mutation are deliberately not fenced: they delete the user's own saved copy
|
|
513
|
+
and write no credential. A projected source is never fenced either -- its native store stays with its own
|
|
514
|
+
tool. The command line takes this host's locations only when it composes its own harnesses, and with
|
|
515
|
+
injected harnesses is fenced only by locations its caller states; `AuthSwitchOperations`,
|
|
516
|
+
`runAuthSwitchMutation` and an `AuthSwitchService` given its harnesses take the locations as a required
|
|
517
|
+
argument (`authSwitchHostLegacyFence()` names this host's, `'none'` states that a
|
|
518
|
+
caller owns no authority), so no library path reads a host it was not given. `AuthSwitchOperations` and the
|
|
519
|
+
hosted service ask the fence before their AGL coordinator as well, so a refused mutation never has its host
|
|
520
|
+
stop the runtimes it would restart. The hosted service reports a fence refusal as the operation's problem, in
|
|
521
|
+
the instruction's own words.
|
|
522
|
+
|
|
356
523
|
## Usage
|
|
357
524
|
|
|
358
525
|
Run `authswitch`, `authswitch -i`, or `authswitch --interactive` for an interactive guide. Use the arrow keys and
|
|
@@ -515,9 +682,12 @@ login. `activateCredential(providerId, credential)` preserves and verifies the
|
|
|
515
682
|
outgoing native login before activation; the caller retains ownership of the
|
|
516
683
|
incoming credential's durable storage. Both are backend APIs, never wire payloads.
|
|
517
684
|
|
|
518
|
-
`AuthSwitchOperations` is shared by the command, guide and TUI
|
|
519
|
-
coordinator
|
|
520
|
-
|
|
685
|
+
`AuthSwitchOperations` is shared by the command, guide and TUI, and is built
|
|
686
|
+
with a coordinator (or `undefined`) and the legacy fence locations described in
|
|
687
|
+
"The legacy fence". A host that injects its own harnesses into `AuthSwitchService`
|
|
688
|
+
states that fence as the third argument. The coordinator receives only the
|
|
689
|
+
harness, operation, account ID, consent to wait, and a credential-location
|
|
690
|
+
fingerprint. AGL owns private-controller discovery;
|
|
521
691
|
authswitch uses `agl authswitch --request <json>` without an AGL package dependency.
|
|
522
692
|
A failed or ambiguous transport never falls through to a second local mutation.
|
|
523
693
|
AGL and the CLI must use matching credential locations and compatible versions.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/authority-contract.ts
CHANGED
|
@@ -15,11 +15,25 @@ export type TAuthSwitchRefusalCode =
|
|
|
15
15
|
| 'account_changed'
|
|
16
16
|
| 'account_busy'
|
|
17
17
|
| 'login_unavailable'
|
|
18
|
+
/** A runtime capability that is missing, wrong or superseded: deliberately one answer, and one repair. */
|
|
19
|
+
| 'binding_unauthorized'
|
|
20
|
+
/** The authority holds this login, so the legacy path that used to move it no longer may. */
|
|
21
|
+
| 'authority_holds_login'
|
|
22
|
+
/** An authority is installed on this host and is not answering, so nothing may assume it holds nothing. */
|
|
23
|
+
| 'authority_unavailable'
|
|
18
24
|
| 'native_owner_holds_login'
|
|
19
25
|
| 'claude_home_unregistered'
|
|
20
26
|
| 'claude_handoff_pending'
|
|
21
27
|
| 'claude_receipt_missing'
|
|
22
|
-
| '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';
|
|
23
37
|
|
|
24
38
|
/** The marker the importer has published since 8.1.0; it keeps its own value on the wire. */
|
|
25
39
|
export const authSwitchImportRefusalMarker = 'authswitch_import_refusal';
|
|
@@ -42,8 +56,10 @@ export interface IAuthSwitchRefusal {
|
|
|
42
56
|
|
|
43
57
|
const refusalCodes: ReadonlySet<string> = new Set<TAuthSwitchRefusalCode>([
|
|
44
58
|
'authority_closing', 'not_found', 'account_changed', 'account_busy',
|
|
45
|
-
'login_unavailable', 'native_owner_holds_login', 'claude_home_unregistered',
|
|
59
|
+
'login_unavailable', 'binding_unauthorized', 'native_owner_holds_login', 'claude_home_unregistered',
|
|
46
60
|
'claude_handoff_pending', 'claude_receipt_missing', 'import_refusal',
|
|
61
|
+
'authority_holds_login', 'authority_unavailable', 'codex_unsupported', 'invalid_input',
|
|
62
|
+
'login_needs_reauth', 'access_not_fresh',
|
|
47
63
|
]);
|
|
48
64
|
|
|
49
65
|
const isRefusalCode = (value: unknown): value is TAuthSwitchRefusalCode =>
|
|
@@ -95,6 +111,18 @@ export const asAuthSwitchRefusal = (error: unknown): IAuthSwitchRefusal | null =
|
|
|
95
111
|
return { code, instruction };
|
|
96
112
|
};
|
|
97
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
|
+
|
|
98
126
|
/** Credential-free account management contract. Safe to import in browser code. */
|
|
99
127
|
export type TAuthSwitchLoginPurpose = 'openai_managed' | 'claude_host_native' | 'claude_container_setup' | 'opencode_native';
|
|
100
128
|
export type TAuthSwitchLoginHealth = 'ready' | 'refreshing' | 'retry_wait' | 'needs_reauth' | 'unverified' | 'pending_handoff' | 'handoff_quarantined' | 'removed';
|
|
@@ -110,6 +138,16 @@ export interface IAuthSwitchAccount {
|
|
|
110
138
|
statusObservedAt: string;
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Which tool refreshes a natively owned login, named rather than inferred.
|
|
143
|
+
*
|
|
144
|
+
* `owner` says that a native tool holds the login; this says which one, so a consumer presents "Claude Code"
|
|
145
|
+
* or "Codex" without deriving it from `purpose` -- a derivation that would be wrong the moment two purposes
|
|
146
|
+
* share a tool, and that belongs to the side that decides ownership in the first place. It is `null` exactly
|
|
147
|
+
* when no native tool refreshes the login: the authority does, or nothing does.
|
|
148
|
+
*/
|
|
149
|
+
export type TAuthSwitchLoginOwnerTool = 'claude_code' | 'codex' | 'opencode';
|
|
150
|
+
|
|
113
151
|
/** One independently owned login. Native ownership does not imply observed health. */
|
|
114
152
|
export interface IAuthSwitchLogin {
|
|
115
153
|
id: string;
|
|
@@ -117,6 +155,8 @@ export interface IAuthSwitchLogin {
|
|
|
117
155
|
providerId: string;
|
|
118
156
|
purpose: TAuthSwitchLoginPurpose;
|
|
119
157
|
owner: 'daemon' | 'claude_native' | 'legacy_native' | 'none';
|
|
158
|
+
/** The native tool that refreshes this login, or null when the authority or nobody does. */
|
|
159
|
+
ownerTool: TAuthSwitchLoginOwnerTool | null;
|
|
120
160
|
health: TAuthSwitchLoginHealth;
|
|
121
161
|
problem: 'none' | 'provider_unavailable' | 'exchange_uncertain' | 'provider_rejected' | 'native_owner';
|
|
122
162
|
grantGeneration: number;
|
|
@@ -130,12 +170,35 @@ export interface IAuthSwitchLogin {
|
|
|
130
170
|
export interface IAuthSwitchBinding {
|
|
131
171
|
id: string;
|
|
132
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
|
+
*/
|
|
133
178
|
runtime: 'flex' | 'codex' | 'opencode' | 'claude';
|
|
134
179
|
scopeId: string;
|
|
135
180
|
incarnationId: string;
|
|
136
181
|
revision: number;
|
|
137
182
|
}
|
|
138
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Which account a vendor tool's own home on this host runs on. Credential-free: the home is named by the
|
|
186
|
+
* hash the authority registered it under, never by its path.
|
|
187
|
+
*
|
|
188
|
+
* Only homes the authority itself switches are recorded: Claude Code homes adopted by a verified import.
|
|
189
|
+
* A Codex or OpenCode store that a native tool still refreshes appears as its login instead, with
|
|
190
|
+
* `owner: 'legacy_native'` and its `ownerTool`, because the authority does not decide what that store holds.
|
|
191
|
+
*/
|
|
192
|
+
export interface IAuthSwitchNativeAssignment {
|
|
193
|
+
id: string;
|
|
194
|
+
tool: TAuthSwitchLoginOwnerTool;
|
|
195
|
+
accountId: string;
|
|
196
|
+
loginId: string;
|
|
197
|
+
/** `switching` while a handoff to another account is in flight; `quarantined` until it is resolved. */
|
|
198
|
+
state: 'ready' | 'switching' | 'quarantined';
|
|
199
|
+
revision: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
139
202
|
export interface IAuthSwitchSnapshot {
|
|
140
203
|
schemaVersion: 2;
|
|
141
204
|
epoch: string;
|
|
@@ -144,9 +207,11 @@ export interface IAuthSwitchSnapshot {
|
|
|
144
207
|
accounts: IAuthSwitchAccount[];
|
|
145
208
|
logins: IAuthSwitchLogin[];
|
|
146
209
|
bindings: IAuthSwitchBinding[];
|
|
210
|
+
nativeAssignments: IAuthSwitchNativeAssignment[];
|
|
147
211
|
nextAccountCursor: string | null;
|
|
148
212
|
nextLoginCursor: string | null;
|
|
149
213
|
nextBindingCursor: string | null;
|
|
214
|
+
nextNativeAssignmentCursor: string | null;
|
|
150
215
|
}
|
|
151
216
|
|
|
152
217
|
/** Persisted account evidence. No provider request is made while collecting diagnostics. */
|
|
@@ -372,7 +437,13 @@ export interface IReq_AuthSwitchClaudeNativeHandoffs extends ITypedRequest {
|
|
|
372
437
|
|
|
373
438
|
export interface IReq_AuthSwitchSnapshot extends ITypedRequest {
|
|
374
439
|
method: 'authswitch.authority.snapshot';
|
|
375
|
-
request: { accountAfter?: string; loginAfter?: string; bindingAfter?: string;
|
|
440
|
+
request: { accountAfter?: string; loginAfter?: string; bindingAfter?: string; nativeAssignmentAfter?: string;
|
|
441
|
+
limit?: number;
|
|
442
|
+
/**
|
|
443
|
+
* Also publish removed accounts (`removed: true`) and their removed logins (`health: 'removed'`), so a
|
|
444
|
+
* consumer can show them apart. Absent or false keeps the snapshot to what is live.
|
|
445
|
+
*/
|
|
446
|
+
includeRemoved?: boolean };
|
|
376
447
|
response: { snapshot: IAuthSwitchSnapshot };
|
|
377
448
|
}
|
|
378
449
|
|
|
@@ -422,9 +493,14 @@ export interface IReq_AuthSwitchListOperations extends ITypedRequest {
|
|
|
422
493
|
response: { operations: IAuthSwitchOperation[]; nextCursor: string | null };
|
|
423
494
|
}
|
|
424
495
|
|
|
496
|
+
/**
|
|
497
|
+
* One device sign-in. `afterRevision` and `waitMs` come together or not at all: with them the read is a long
|
|
498
|
+
* poll that answers once the operation's revision passes `afterRevision`, at once for a finished sign-in, or
|
|
499
|
+
* with the unchanged operation after `waitMs` (at most 30000).
|
|
500
|
+
*/
|
|
425
501
|
export interface IReq_AuthSwitchGetOperation extends ITypedRequest {
|
|
426
502
|
method: 'authswitch.authority.operation';
|
|
427
|
-
request: { operationId: string };
|
|
503
|
+
request: { operationId: string; afterRevision?: number; waitMs?: number };
|
|
428
504
|
response: { operation: IAuthSwitchOperation };
|
|
429
505
|
}
|
|
430
506
|
|
|
@@ -459,6 +535,16 @@ export interface IReq_AuthSwitchRenameAccount extends ITypedRequest {
|
|
|
459
535
|
response: { account: IAuthSwitchAccount };
|
|
460
536
|
}
|
|
461
537
|
|
|
538
|
+
/**
|
|
539
|
+
* One binding by the id `bind` returned, credential-free, or `null` when this authority holds no binding by
|
|
540
|
+
* that id. A backend checks that its binding still stands without reading the whole snapshot.
|
|
541
|
+
*/
|
|
542
|
+
export interface IReq_AuthSwitchGetBinding extends ITypedRequest {
|
|
543
|
+
method: 'authswitch.authority.binding';
|
|
544
|
+
request: { bindingId: string };
|
|
545
|
+
response: { binding: IAuthSwitchBinding | null };
|
|
546
|
+
}
|
|
547
|
+
|
|
462
548
|
export interface IReq_AuthSwitchRemoveAccount extends ITypedRequest {
|
|
463
549
|
method: 'authswitch.authority.remove';
|
|
464
550
|
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;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where this user's authority lives: its sockets, its store and the service unit that runs it.
|
|
5
|
+
*
|
|
6
|
+
* These rules sit in a leaf module because two sides read them. `AuthSwitchAuthorityService` installs and
|
|
7
|
+
* runs the daemon there, and the legacy fence (`./classes.legacyfence`) asks the same socket and looks for
|
|
8
|
+
* the same unit -- without importing the daemon, whose module graph reaches back to the legacy operations
|
|
9
|
+
* the fence guards.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface IAuthSwitchAuthorityPaths {
|
|
13
|
+
runtimeDirectory: string;
|
|
14
|
+
dataDirectory: string;
|
|
15
|
+
databaseSocketPath: string;
|
|
16
|
+
authoritySocketPath: string;
|
|
17
|
+
runtimeSocketPath: string;
|
|
18
|
+
runtimeSocketDirectory: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The authority's sockets. */
|
|
22
|
+
export type TAuthSwitchAuthoritySocketPaths = Omit<IAuthSwitchAuthorityPaths, 'dataDirectory'>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The sockets under one runtime directory, derived from it alone.
|
|
26
|
+
*
|
|
27
|
+
* The legacy fence asks the socket a caller states and must not depend on anything this process holds --
|
|
28
|
+
* its home or its `XDG_DATA_HOME`, which only the store's directory needs.
|
|
29
|
+
*/
|
|
30
|
+
export const authSwitchAuthoritySocketPaths = (
|
|
31
|
+
runtimeDirectory: string | undefined): TAuthSwitchAuthoritySocketPaths => {
|
|
32
|
+
if (!runtimeDirectory || !plugins.path.isAbsolute(runtimeDirectory)) {
|
|
33
|
+
throw new Error('A private XDG_RUNTIME_DIR is required for authswitch authority.');
|
|
34
|
+
}
|
|
35
|
+
const socketDirectory = plugins.path.join(runtimeDirectory, 'authswitch');
|
|
36
|
+
const runtimeSocketDirectory = plugins.path.join(socketDirectory, 'runtime');
|
|
37
|
+
return {
|
|
38
|
+
runtimeDirectory,
|
|
39
|
+
databaseSocketPath: plugins.path.join(socketDirectory, 'internal', 'db.sock'),
|
|
40
|
+
authoritySocketPath: plugins.path.join(socketDirectory, 'management', 'authority.sock'),
|
|
41
|
+
runtimeSocketPath: plugins.path.join(runtimeSocketDirectory, 'runtime.sock'),
|
|
42
|
+
runtimeSocketDirectory,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Canonical per-user paths. An explicit runtime directory is also usable by a trusted container host. */
|
|
47
|
+
export const resolveAuthSwitchAuthorityPaths = (runtimeDirectory = process.env.XDG_RUNTIME_DIR): IAuthSwitchAuthorityPaths => {
|
|
48
|
+
const sockets = authSwitchAuthoritySocketPaths(runtimeDirectory);
|
|
49
|
+
const home = plugins.os.userInfo().homedir;
|
|
50
|
+
const dataHome = process.env.XDG_DATA_HOME || plugins.path.join(home, '.local/share');
|
|
51
|
+
if (!plugins.path.isAbsolute(dataHome)) throw new Error('XDG_DATA_HOME must be an absolute path.');
|
|
52
|
+
return { ...sockets, dataDirectory: plugins.path.join(dataHome, 'authswitch', 'authority') };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The user service `authswitch authority service install` writes. */
|
|
56
|
+
export const authSwitchAuthorityUnitName = 'authswitch-authority.service';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The directory that unit is installed into, from a stated environment and home directory.
|
|
60
|
+
*
|
|
61
|
+
* It is the rule smartdaemon applies to a user unit when it is given no directory --
|
|
62
|
+
* `$XDG_DATA_HOME/systemd/user`, default `~/.local/share/systemd/user` -- stated here so the installer passes
|
|
63
|
+
* it explicitly and the fence derives it from the locations its caller states rather than from this process.
|
|
64
|
+
*/
|
|
65
|
+
export const authSwitchAuthorityUnitDirectory = (env: NodeJS.ProcessEnv, homeDirectory: string): string => {
|
|
66
|
+
const dataHome = env.XDG_DATA_HOME || plugins.path.join(homeDirectory, '.local/share');
|
|
67
|
+
if (!plugins.path.isAbsolute(dataHome)) throw new Error('XDG_DATA_HOME must be an absolute path.');
|
|
68
|
+
return plugins.path.join(dataHome, 'systemd/user');
|
|
69
|
+
};
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
|
|
2
2
|
import type { IAuthSwitchBinding } from './authority-contract.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Backend-only binding operation, served on the MANAGEMENT socket. Not a misplacement -- the rule.
|
|
6
|
+
*
|
|
7
|
+
* The other two requests in this file are answered on the runtime socket, whose directory a container may
|
|
8
|
+
* be given. This one is not, because it mints the capability those two spend: minting is the act that
|
|
9
|
+
* authorizes a runtime, so it belongs to a caller already trusted with account management. A container
|
|
10
|
+
* holding the runtime directory can therefore use the binding it was handed, and can never create one --
|
|
11
|
+
* not for itself, and not for another account.
|
|
12
|
+
*
|
|
13
|
+
* The capability must never enter a browser-facing response.
|
|
14
|
+
*/
|
|
5
15
|
export interface IReq_AuthSwitchBindAccount extends ITypedRequest {
|
|
6
16
|
method: 'authswitch.authority.bind';
|
|
7
17
|
request: {
|