@skrr-ai/cli 0.1.79 → 0.1.81
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/base-command.d.ts +4 -0
- package/dist/base-command.js +40 -4
- package/dist/commands/agents/visibility.d.ts +14 -0
- package/dist/commands/agents/visibility.js +52 -0
- package/dist/commands/create-token.d.ts +0 -1
- package/dist/commands/create-token.js +3 -1
- package/dist/commands/inbox/index.js +4 -2
- package/dist/commands/list-tokens.d.ts +0 -1
- package/dist/commands/list-tokens.js +1 -1
- package/dist/commands/login.d.ts +8 -3
- package/dist/commands/login.js +14 -4
- package/dist/commands/machines/dedicated/ssh-proxy.js +1 -0
- package/dist/commands/revoke-token.d.ts +0 -1
- package/dist/commands/revoke-token.js +1 -1
- package/dist/commands/spaces/workflows/events.js +9 -2
- package/dist/commands/store/follow.d.ts +1 -0
- package/dist/commands/store/follow.js +7 -1
- package/dist/commands/store/follows/list.d.ts +11 -0
- package/dist/commands/store/follows/list.js +29 -0
- package/dist/commands/store/follows/remove.d.ts +16 -0
- package/dist/commands/store/follows/remove.js +37 -0
- package/dist/commands/store/publishers/profile/draft.d.ts +17 -0
- package/dist/commands/store/publishers/profile/draft.js +54 -0
- package/dist/commands/store/publishers/profile/handle.d.ts +14 -0
- package/dist/commands/store/publishers/profile/handle.js +39 -0
- package/dist/commands/store/publishers/profile/hide.d.ts +12 -0
- package/dist/commands/store/publishers/profile/hide.js +39 -0
- package/dist/commands/store/publishers/profile/preview.d.ts +11 -0
- package/dist/commands/store/publishers/profile/preview.js +39 -0
- package/dist/commands/store/publishers/profile/publish.d.ts +12 -0
- package/dist/commands/store/publishers/profile/publish.js +41 -0
- package/dist/commands/store/publishers/profile/restore.d.ts +15 -0
- package/dist/commands/store/publishers/profile/restore.js +49 -0
- package/dist/commands/store/publishers/profile/revisions.d.ts +11 -0
- package/dist/commands/store/publishers/profile/revisions.js +30 -0
- package/dist/commands/store/publishers/profile/show.d.ts +11 -0
- package/dist/commands/store/publishers/profile/show.js +39 -0
- package/dist/commands/store/review.d.ts +26 -0
- package/dist/commands/store/review.js +93 -0
- package/dist/commands/store/reviews.d.ts +29 -0
- package/dist/commands/store/reviews.js +170 -0
- package/dist/commands/store/tip.d.ts +20 -0
- package/dist/commands/store/tip.js +57 -0
- package/dist/commands/tokens/create.d.ts +28 -0
- package/dist/commands/tokens/create.js +136 -0
- package/dist/commands/tokens/list.d.ts +11 -0
- package/dist/commands/tokens/list.js +53 -0
- package/dist/commands/tokens/revoke.d.ts +14 -0
- package/dist/commands/tokens/revoke.js +42 -0
- package/dist/lib/access-tokens.d.ts +59 -0
- package/dist/lib/access-tokens.js +50 -0
- package/dist/lib/agentic-stream.js +13 -4
- package/dist/lib/api-fetch.js +6 -2
- package/dist/lib/auth-core-init.d.ts +6 -0
- package/dist/lib/auth-core-init.js +28 -6
- package/dist/lib/brokered-dpop.d.ts +34 -0
- package/dist/lib/brokered-dpop.js +89 -0
- package/dist/lib/credential-resolver.d.ts +1 -1
- package/dist/lib/credential-resolver.js +8 -2
- package/dist/lib/daemonBroker.d.ts +84 -4
- package/dist/lib/daemonBroker.js +209 -40
- package/dist/lib/daemonBrokerRefusal.d.ts +26 -0
- package/dist/lib/daemonBrokerRefusal.js +83 -4
- package/dist/lib/daemonRestartWait.d.ts +39 -0
- package/dist/lib/daemonRestartWait.js +181 -0
- package/dist/lib/dedicated-service.js +10 -5
- package/dist/lib/dedicated-ssh.d.ts +9 -0
- package/dist/lib/dedicated-ssh.js +235 -63
- package/dist/lib/dedicated-terminal.js +6 -3
- package/dist/lib/dpop-auth.d.ts +35 -0
- package/dist/lib/dpop-auth.js +57 -0
- package/dist/lib/login.d.ts +6 -0
- package/dist/lib/login.js +13 -4
- package/dist/lib/node-adapter.js +6 -2
- package/dist/lib/publisher-profile-flags.d.ts +19 -0
- package/dist/lib/publisher-profile-flags.js +48 -0
- package/dist/lib/refresh.d.ts +1 -1
- package/dist/lib/refresh.js +6 -1
- package/dist/lib/ssh-ordered-bytes.d.ts +8 -0
- package/dist/lib/ssh-ordered-bytes.js +31 -0
- package/dist/lib/usd-micros.d.ts +9 -0
- package/dist/lib/usd-micros.js +21 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +42 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +35 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +22 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +29 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +49 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +47 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +56 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +42 -20
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.d.ts +101 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +179 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +23 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +42 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +32 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +22 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +27 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +49 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +47 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +56 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +39 -20
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.d.ts +101 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +138 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3895 -3882
- package/oclif.manifest.json +34985 -33643
- package/package.json +28 -4
|
@@ -26,6 +26,7 @@ const socket_io_client_1 = require("socket.io-client");
|
|
|
26
26
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
27
27
|
const config_1 = require("./config");
|
|
28
28
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
29
|
+
const dpop_auth_1 = require("./dpop-auth");
|
|
29
30
|
/**
|
|
30
31
|
* Why a question was dismissed when no terminal exists to ask it on. It is the
|
|
31
32
|
* REASON for a dismissal, never an answer: it travels as the value of the
|
|
@@ -709,12 +710,13 @@ class AgenticStreamClient {
|
|
|
709
710
|
: undefined;
|
|
710
711
|
this.socket = (0, socket_io_client_1.io)(wsURL, {
|
|
711
712
|
path: '/ws/agentic',
|
|
712
|
-
|
|
713
|
+
// OSK-12020 — a DPoP-bound token proves possession on every handshake.
|
|
714
|
+
auth: (0, dpop_auth_1.socketAuthWithDpop)({
|
|
713
715
|
token: this.options.token,
|
|
714
716
|
...(0, data_provider_1.getClientIdentityAuth)(),
|
|
715
717
|
clientPlatform: 'cli',
|
|
716
718
|
clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
717
|
-
},
|
|
719
|
+
}, wsURL),
|
|
718
720
|
extraHeaders,
|
|
719
721
|
transports: ['websocket'],
|
|
720
722
|
reconnection: true,
|
|
@@ -948,12 +950,19 @@ class AgenticStreamClient {
|
|
|
948
950
|
if (this.closed || this.settled || generation !== this.replayGeneration)
|
|
949
951
|
return;
|
|
950
952
|
this.replayFailures = 0;
|
|
951
|
-
this.receiveSessionOutput({
|
|
953
|
+
this.receiveSessionOutput({
|
|
954
|
+
type: 'session.output',
|
|
955
|
+
sessionId: this.options.sessionId,
|
|
956
|
+
source: 'replay',
|
|
957
|
+
envelopes: result.envelopes,
|
|
958
|
+
});
|
|
952
959
|
// The canonical endpoint explicitly identifies retention gaps. Only
|
|
953
960
|
// then may later frames pass with the existing honest gap notice.
|
|
954
961
|
if (result.gap) {
|
|
955
962
|
// A retention gap proves only this page, never a later live frame.
|
|
956
|
-
const pageSeqs = result.envelopes
|
|
963
|
+
const pageSeqs = result.envelopes
|
|
964
|
+
.map((e) => Number(e?.__seq))
|
|
965
|
+
.filter(Number.isSafeInteger);
|
|
957
966
|
if (pageSeqs.length)
|
|
958
967
|
this.flushPendingEnvelopes(true, Math.max(...pageSeqs));
|
|
959
968
|
this.flushPendingEnvelopes(false);
|
package/dist/lib/api-fetch.js
CHANGED
|
@@ -44,6 +44,7 @@ const node_adapter_1 = require("./node-adapter");
|
|
|
44
44
|
const refresh_1 = require("./refresh");
|
|
45
45
|
const delegated_cli_1 = require("./delegated-cli");
|
|
46
46
|
const daemonBroker_1 = require("./daemonBroker");
|
|
47
|
+
const dpop_auth_1 = require("./dpop-auth");
|
|
47
48
|
const workspace_header_1 = require("./workspace-header");
|
|
48
49
|
const non_json_response_1 = require("./non-json-response");
|
|
49
50
|
class ApiFetchError extends Error {
|
|
@@ -162,7 +163,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
|
|
|
162
163
|
(0, node_adapter_1.getAdapterCredentialOverride)() ??
|
|
163
164
|
(await (0, credential_resolver_1.resolveCredential)());
|
|
164
165
|
}
|
|
165
|
-
const fetchWithCredential = (credential) => {
|
|
166
|
+
const fetchWithCredential = async (credential) => {
|
|
166
167
|
const headers = {
|
|
167
168
|
Accept: 'application/json',
|
|
168
169
|
'X-Oversky-Origin': (0, refresh_1.getCliOrigin)(),
|
|
@@ -178,10 +179,13 @@ async function apiFetch(pathOrUrl, opts = {}) {
|
|
|
178
179
|
// key is additive metadata, not a way to re-auth the request.
|
|
179
180
|
for (const [k, v] of Object.entries(callerHeaders)) {
|
|
180
181
|
const lower = k.toLowerCase();
|
|
181
|
-
if (lower === 'authorization' || lower === 'content-type')
|
|
182
|
+
if (lower === 'authorization' || lower === 'content-type' || lower === 'dpop')
|
|
182
183
|
continue;
|
|
183
184
|
headers[k] = v;
|
|
184
185
|
}
|
|
186
|
+
// OSK-12020 — a fresh proof per attempt for a DPoP-bound hand-off token
|
|
187
|
+
// (the 401 retry below re-enters here and gets its own). No-op otherwise.
|
|
188
|
+
Object.assign(headers, await (0, dpop_auth_1.dpopHeadersFor)(credential.token, method, url));
|
|
185
189
|
return fetch(url, {
|
|
186
190
|
method,
|
|
187
191
|
headers: (0, workspace_header_1.withWorkspaceHeader)(headers),
|
|
@@ -36,6 +36,12 @@ export declare function plainCredentialEnvelopeWarning(args: readonly unknown[])
|
|
|
36
36
|
* own when a local daemon can hand a credential over.
|
|
37
37
|
*/
|
|
38
38
|
export declare const CREDENTIAL_KEY_CHANGED_NOTICE: string;
|
|
39
|
+
/**
|
|
40
|
+
* The one line an unreadable device key prints when no credential-key change
|
|
41
|
+
* explains it. The CLI never enrolls or requires a device key (OSK-12063), so
|
|
42
|
+
* this degrades a proof, not access — say exactly that.
|
|
43
|
+
*/
|
|
44
|
+
export declare const DEVICE_KEY_RECOVERY_REQUIRED_NOTICE: string;
|
|
39
45
|
export declare function createCredentialEnvelopeWarningPresenter(): (args: readonly unknown[]) => string | null;
|
|
40
46
|
type DedicatedGenerationPreparationOptions = {
|
|
41
47
|
machineUuid?: string;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.CliCredentialInitializationError = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
|
|
6
|
+
exports.CliCredentialInitializationError = exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE = exports.CREDENTIAL_KEY_CHANGED_NOTICE = void 0;
|
|
7
7
|
exports.plainCredentialEnvelopeWarning = plainCredentialEnvelopeWarning;
|
|
8
8
|
exports.createCredentialEnvelopeWarningPresenter = createCredentialEnvelopeWarningPresenter;
|
|
9
9
|
exports.prepareDedicatedRuntimeAuthGeneration = prepareDedicatedRuntimeAuthGeneration;
|
|
@@ -108,6 +108,20 @@ exports.CREDENTIAL_KEY_CHANGED_NOTICE = "note: this machine's credential key cha
|
|
|
108
108
|
*/
|
|
109
109
|
/** auth-core's device-identity warning for a private key it could not open. */
|
|
110
110
|
const DEVICE_KEY_DECRYPT_FAILED = '[deviceIdentity] private_key_decrypt_failed';
|
|
111
|
+
/**
|
|
112
|
+
* auth-core's device-identity warning for a stored key it will NOT replace on
|
|
113
|
+
* ordinary boot (`b5e683280b`): minting a new key here would make the next
|
|
114
|
+
* refresh sign with a key the server never pinned. Only an explicit login with
|
|
115
|
+
* `recoverUnreadable` replaces it.
|
|
116
|
+
*/
|
|
117
|
+
const DEVICE_KEY_RECOVERY_REQUIRED = '[deviceIdentity] recovery_required';
|
|
118
|
+
/**
|
|
119
|
+
* The one line an unreadable device key prints when no credential-key change
|
|
120
|
+
* explains it. The CLI never enrolls or requires a device key (OSK-12063), so
|
|
121
|
+
* this degrades a proof, not access — say exactly that.
|
|
122
|
+
*/
|
|
123
|
+
exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE = "warning: this machine's stored device key could not be read, so skrr sends requests " +
|
|
124
|
+
'without a device proof until the local daemon signs in again. (DEBUG=1 shows the underlying error.)';
|
|
111
125
|
function createCredentialEnvelopeWarningPresenter() {
|
|
112
126
|
let keyChanged = false;
|
|
113
127
|
return (args) => {
|
|
@@ -133,12 +147,20 @@ function createCredentialEnvelopeWarningPresenter() {
|
|
|
133
147
|
if (keyChanged && event === 'cred_envelope.read.decrypt_failed')
|
|
134
148
|
return '';
|
|
135
149
|
// The device private key is one of those values: auth-core's device
|
|
136
|
-
// identity loads it next
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
150
|
+
// identity loads it next and cannot open it. Since `b5e683280b` it does
|
|
151
|
+
// NOT mint a replacement on ordinary boot — it parks in
|
|
152
|
+
// `recovery_required` until an explicit login recovers it. Both warnings
|
|
153
|
+
// come through the `[deviceIdentity]` prefix and reached the terminal as
|
|
154
|
+
// raw `[auth-core warn]` lines right under the notice, which already says
|
|
155
|
+
// the old key is gone and what replaces it.
|
|
156
|
+
if (keyChanged &&
|
|
157
|
+
(message === DEVICE_KEY_DECRYPT_FAILED || message === DEVICE_KEY_RECOVERY_REQUIRED)) {
|
|
141
158
|
return '';
|
|
159
|
+
}
|
|
160
|
+
// Without a key change an unreadable device key is a real fault: one
|
|
161
|
+
// plain line instead of an object dump.
|
|
162
|
+
if (message === DEVICE_KEY_RECOVERY_REQUIRED)
|
|
163
|
+
return exports.DEVICE_KEY_RECOVERY_REQUIRED_NOTICE;
|
|
142
164
|
return plainCredentialEnvelopeWarning(args);
|
|
143
165
|
};
|
|
144
166
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brokered-dpop.ts — this process's DPoP-bound hand-off token, and the proof
|
|
3
|
+
* fetch for it (OSK-12020).
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from `daemonBroker.ts` on purpose: every transport consults it on
|
|
6
|
+
* every request (`dpop-auth.ts`), so it must stay a leaf with no I/O at import
|
|
7
|
+
* and no dependency on the broker's own module graph. `daemonBroker` registers
|
|
8
|
+
* the binding after a redeem whose answer said `tokenType: 'DPoP'`, and clears
|
|
9
|
+
* it after one that did not.
|
|
10
|
+
*/
|
|
11
|
+
/** Where to ask for proofs. Re-resolved per call so a rotated secret heals. */
|
|
12
|
+
export interface DpopOracleEndpoint {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
secret: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function registerDpopBinding(accessToken: string, resolveEndpoint: () => DpopOracleEndpoint): void;
|
|
18
|
+
export declare function clearDpopBinding(): void;
|
|
19
|
+
/** True when `token` is this process's DPoP-bound hand-off token. */
|
|
20
|
+
export declare function isDpopBoundHandoffToken(token: string | null | undefined): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* A fresh RFC 9449 proof for one request made with `token`, from the local
|
|
23
|
+
* daemon that holds the key the token is bound to. Null when `token` is not the
|
|
24
|
+
* bound one (nothing to prove) or the daemon would not sign — the request then
|
|
25
|
+
* goes out without a proof and the server's `DPOP_PROOF_REQUIRED` names the
|
|
26
|
+
* problem, rather than this client inventing a second error for it.
|
|
27
|
+
*
|
|
28
|
+
* The token itself is never sent: the daemon gets its SHA-256 (`ath`) and signs
|
|
29
|
+
* only for hashes of tokens it issued. Proofs are single-use, so this runs per
|
|
30
|
+
* request and per retry.
|
|
31
|
+
*/
|
|
32
|
+
export declare function brokeredDpopProof(token: string | null | undefined, htm: string, htu: string): Promise<string | null>;
|
|
33
|
+
/** @internal test seam. */
|
|
34
|
+
export declare function __resetDpopBindingForTest(): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* brokered-dpop.ts — this process's DPoP-bound hand-off token, and the proof
|
|
4
|
+
* fetch for it (OSK-12020).
|
|
5
|
+
*
|
|
6
|
+
* Kept apart from `daemonBroker.ts` on purpose: every transport consults it on
|
|
7
|
+
* every request (`dpop-auth.ts`), so it must stay a leaf with no I/O at import
|
|
8
|
+
* and no dependency on the broker's own module graph. `daemonBroker` registers
|
|
9
|
+
* the binding after a redeem whose answer said `tokenType: 'DPoP'`, and clears
|
|
10
|
+
* it after one that did not.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.registerDpopBinding = registerDpopBinding;
|
|
14
|
+
exports.clearDpopBinding = clearDpopBinding;
|
|
15
|
+
exports.isDpopBoundHandoffToken = isDpopBoundHandoffToken;
|
|
16
|
+
exports.brokeredDpopProof = brokeredDpopProof;
|
|
17
|
+
exports.__resetDpopBindingForTest = __resetDpopBindingForTest;
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
20
|
+
const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
|
|
21
|
+
let binding = null;
|
|
22
|
+
/** Bound the proof fetch: it sits in front of every request a bound token makes. */
|
|
23
|
+
const DPOP_PROOF_TIMEOUT_MS = 5_000;
|
|
24
|
+
function registerDpopBinding(accessToken, resolveEndpoint) {
|
|
25
|
+
binding = { accessToken, resolveEndpoint };
|
|
26
|
+
}
|
|
27
|
+
function clearDpopBinding() {
|
|
28
|
+
binding = null;
|
|
29
|
+
}
|
|
30
|
+
/** True when `token` is this process's DPoP-bound hand-off token. */
|
|
31
|
+
function isDpopBoundHandoffToken(token) {
|
|
32
|
+
return Boolean(token && binding && token === binding.accessToken);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A fresh RFC 9449 proof for one request made with `token`, from the local
|
|
36
|
+
* daemon that holds the key the token is bound to. Null when `token` is not the
|
|
37
|
+
* bound one (nothing to prove) or the daemon would not sign — the request then
|
|
38
|
+
* goes out without a proof and the server's `DPOP_PROOF_REQUIRED` names the
|
|
39
|
+
* problem, rather than this client inventing a second error for it.
|
|
40
|
+
*
|
|
41
|
+
* The token itself is never sent: the daemon gets its SHA-256 (`ath`) and signs
|
|
42
|
+
* only for hashes of tokens it issued. Proofs are single-use, so this runs per
|
|
43
|
+
* request and per retry.
|
|
44
|
+
*/
|
|
45
|
+
async function brokeredDpopProof(token, htm, htu) {
|
|
46
|
+
const state = binding;
|
|
47
|
+
if (!token || !state || token !== state.accessToken)
|
|
48
|
+
return null;
|
|
49
|
+
let endpoint;
|
|
50
|
+
try {
|
|
51
|
+
endpoint = state.resolveEndpoint();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timer = setTimeout(() => controller.abort(), DPOP_PROOF_TIMEOUT_MS);
|
|
58
|
+
try {
|
|
59
|
+
// The descriptor secret is loopback-only: never global `fetch`, which can
|
|
60
|
+
// route 127.0.0.1 through HTTP_PROXY (OSK-12134).
|
|
61
|
+
const response = await (0, loopback_http_1.loopbackFetch)(`http://${endpoint.host}:${endpoint.port}${cli_handoff_wire_1.CLI_HANDOFF_DPOP_PROOF_PATH}`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${endpoint.secret}`,
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
Accept: 'application/json',
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
htm: htm.toUpperCase(),
|
|
70
|
+
htu,
|
|
71
|
+
ath: (0, node_crypto_1.createHash)('sha256').update(token, 'utf8').digest('base64url'),
|
|
72
|
+
}),
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok)
|
|
76
|
+
return null;
|
|
77
|
+
return (0, cli_handoff_wire_1.parseCliHandoffDpopProofResponse)(await response.json())?.proof ?? null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** @internal test seam. */
|
|
87
|
+
function __resetDpopBindingForTest() {
|
|
88
|
+
binding = null;
|
|
89
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* CLI-appropriate backends. The resolver walks the published chain:
|
|
6
6
|
*
|
|
7
7
|
* 1. flag — --token <jwt> (debugging; BaseCommand parses)
|
|
8
|
-
* 2. env-token — OVERSKY_TOKEN (CI / static; kind
|
|
8
|
+
* 2. env-token — OVERSKY_TOKEN, then SKRR_TOKEN (CI / static; kind 'ci' or 'pat' detected)
|
|
9
9
|
* 3. auth-helper — OVERSKY_AUTH_HELPER script output
|
|
10
10
|
* 4. fd-handoff — N/A for CLI (skipped; no parent fd)
|
|
11
11
|
* 5. keychain — ai.skrr.cli.<profile>
|
|
@@ -12,7 +12,7 @@ exports.formatCredentialSource = formatCredentialSource;
|
|
|
12
12
|
* CLI-appropriate backends. The resolver walks the published chain:
|
|
13
13
|
*
|
|
14
14
|
* 1. flag — --token <jwt> (debugging; BaseCommand parses)
|
|
15
|
-
* 2. env-token — OVERSKY_TOKEN (CI / static; kind
|
|
15
|
+
* 2. env-token — OVERSKY_TOKEN, then SKRR_TOKEN (CI / static; kind 'ci' or 'pat' detected)
|
|
16
16
|
* 3. auth-helper — OVERSKY_AUTH_HELPER script output
|
|
17
17
|
* 4. fd-handoff — N/A for CLI (skipped; no parent fd)
|
|
18
18
|
* 5. keychain — ai.skrr.cli.<profile>
|
|
@@ -78,6 +78,9 @@ async function resolveCredential(opts = {}) {
|
|
|
78
78
|
flagToken: opts.flagToken,
|
|
79
79
|
bareMode,
|
|
80
80
|
storeMode,
|
|
81
|
+
// SKRR_TOKEN is the name the CI recipe uses (OSK-13208); OVERSKY_TOKEN still
|
|
82
|
+
// wins when both are set, as in the config overlay (config.ts).
|
|
83
|
+
envTokenNames: ['OVERSKY_TOKEN', 'SKRR_TOKEN'],
|
|
81
84
|
// CLI never has an interactive slot — users run `skrr login` explicitly.
|
|
82
85
|
isHeadless: () => true,
|
|
83
86
|
// Stored credentials are only usable against the origin that issued
|
|
@@ -160,7 +163,10 @@ function formatCredentialSource(source) {
|
|
|
160
163
|
case 'flag':
|
|
161
164
|
return 'CLI --token flag';
|
|
162
165
|
case 'env-token':
|
|
163
|
-
|
|
166
|
+
// Name the variable that actually supplied it (OVERSKY_TOKEN wins).
|
|
167
|
+
return !process.env.OVERSKY_TOKEN?.trim() && process.env.SKRR_TOKEN?.trim()
|
|
168
|
+
? 'SKRR_TOKEN'
|
|
169
|
+
: 'OVERSKY_TOKEN';
|
|
164
170
|
case 'auth-helper':
|
|
165
171
|
return 'OVERSKY_AUTH_HELPER';
|
|
166
172
|
case 'fd-handoff':
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* daemonBroker.ts —
|
|
2
|
+
* daemonBroker.ts — skrr CLI client for the local `skrrd` daemon's
|
|
3
3
|
* cli-handoff endpoint.
|
|
4
4
|
*
|
|
5
5
|
* When the daemon is already authenticated for the user on this machine,
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { type CliHandoffDescriptor, type CliHandoffTokenResponse } from '@skrr-ai/auth-core/cli-handoff-wire';
|
|
40
40
|
import { type AuthBackend } from './auth-storage';
|
|
41
41
|
import { type CliConfig } from './config';
|
|
42
|
+
import { type AwaitRestartedDaemonOptions } from './daemonRestartWait';
|
|
42
43
|
/**
|
|
43
44
|
* Wire-shape of the bootstrap file — the shared declaration in
|
|
44
45
|
* `@skrr-ai/auth-core/cli-handoff-wire` (`CliHandoffDescriptor`), which the
|
|
@@ -173,6 +174,23 @@ export interface DaemonBrokerFailure {
|
|
|
173
174
|
* the repair for a refusal is the machine, never a login.
|
|
174
175
|
*/
|
|
175
176
|
dedicatedGuest?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* For `no_bootstrap`: what the descriptor file on disk actually was, when
|
|
179
|
+
* one existed. `dead_pid` is a daemon that crashed or is mid-restart and left
|
|
180
|
+
* its hand-off behind; `unreadable` / `invalid` a file that could not be
|
|
181
|
+
* used. Absent when no candidate file existed at all (OSK-13222).
|
|
182
|
+
*/
|
|
183
|
+
descriptorState?: 'dead_pid' | 'unreadable' | 'invalid';
|
|
184
|
+
/** The pid the descriptor named (a dead one, for `dead_pid`). */
|
|
185
|
+
descriptorPid?: number;
|
|
186
|
+
/** The loopback port the descriptor named — for `network`, the refused one. */
|
|
187
|
+
descriptorPort?: number;
|
|
188
|
+
/**
|
|
189
|
+
* How long the CLI waited for a restarting daemon before giving up. Set only
|
|
190
|
+
* when it waited: the local service was starting or is supervised, so a
|
|
191
|
+
* restart was the likely state (OSK-13222).
|
|
192
|
+
*/
|
|
193
|
+
restartWaitedMs?: number;
|
|
176
194
|
}
|
|
177
195
|
export type DaemonBrokerOutcome = (DaemonBrokerResult & {
|
|
178
196
|
ok: true;
|
|
@@ -181,7 +199,7 @@ export interface DaemonBrokerOptions {
|
|
|
181
199
|
/** Stable per-machine cliId the CLI wants the new refresh row bound to. */
|
|
182
200
|
cliId: string;
|
|
183
201
|
/**
|
|
184
|
-
* The
|
|
202
|
+
* The skrr CLI's currently-configured baseURL (e.g. `https://skrr.ai`).
|
|
185
203
|
* Two roles:
|
|
186
204
|
* 1. Pre-flight check vs `bootstrap.serverUrl` — when they don't
|
|
187
205
|
* match, we refuse to call the daemon so we never mint a token
|
|
@@ -215,7 +233,12 @@ export interface DaemonBrokerOptions {
|
|
|
215
233
|
* - `'access_token'` — broker mode: redeem a short-lived access token
|
|
216
234
|
* and hold it in memory only. Used by `maybeAutoBroker`: nothing
|
|
217
235
|
* credential-shaped lands on disk and each `skrr` process re-redeems
|
|
218
|
-
* over loopback.
|
|
236
|
+
* over loopback. When the descriptor advertises `access_token`, the
|
|
237
|
+
* token route is the ONLY door for this caller: a failed redeem is
|
|
238
|
+
* reported, never retried as a durable mint (OSK-12018). The automatic
|
|
239
|
+
* path must not mint and persist a refresh family the human never asked
|
|
240
|
+
* for — a durable login is the human's to hold, and only an explicit
|
|
241
|
+
* `skrr login` takes it.
|
|
219
242
|
* - `'refresh_family'` (default) — the legacy durable mint. Used by
|
|
220
243
|
* explicit `skrr login`: a human login still writes a real,
|
|
221
244
|
* user-owned refresh family. A descriptor that advertises ONLY
|
|
@@ -223,6 +246,12 @@ export interface DaemonBrokerOptions {
|
|
|
223
246
|
* regardless — its secret cannot mint a family at all.
|
|
224
247
|
*/
|
|
225
248
|
preferHandoffMode?: 'access_token' | 'refresh_family';
|
|
249
|
+
/**
|
|
250
|
+
* Overrides for the bounded wait on a restarting local daemon (OSK-13222):
|
|
251
|
+
* budgets, the clock and the supervisor probe. Tests use it; production
|
|
252
|
+
* leaves it unset.
|
|
253
|
+
*/
|
|
254
|
+
restartWait?: Omit<AwaitRestartedDaemonOptions, 'profile' | 'ready'>;
|
|
226
255
|
}
|
|
227
256
|
/**
|
|
228
257
|
* Compute the bootstrap file path. Exported for tests so they can assert
|
|
@@ -257,6 +286,17 @@ export declare function resolveBootstrapCandidatePaths(profile?: string): string
|
|
|
257
286
|
* user configures can point the broker somewhere else.
|
|
258
287
|
*/
|
|
259
288
|
export declare const DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = "/run/skrr-dedicated-runtime/cli-handoff/descriptor.json";
|
|
289
|
+
/**
|
|
290
|
+
* A Hosted Machine's workload hand-off descriptor, published only on a box
|
|
291
|
+
* booted under the identity split (OSK-11981, OSK-12019), where the daemon runs
|
|
292
|
+
* as `oversky-daemon` and its island file is unreadable to the workload. Same
|
|
293
|
+
* format and same broker-only contract as the Dedicated descriptor
|
|
294
|
+
* (`daemon/src/dedicated-cli-handoff.ts`, `workloadCliHandoffTarget`), in a
|
|
295
|
+
* DIFFERENT directory so a Hosted Machine is never mistaken for a Dedicated
|
|
296
|
+
* Runtime guest (`runningOnDedicatedRuntimeGuest` keys on that directory). The
|
|
297
|
+
* parent is created by the box's launcher as root and owned by the daemon.
|
|
298
|
+
*/
|
|
299
|
+
export declare const HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR = "/run/skrr-hosted-machine/cli-handoff/descriptor.json";
|
|
260
300
|
/** Where the cli-handoff broker looks, in order. */
|
|
261
301
|
export declare function resolveCliHandoffCandidatePaths(profile?: string): string[];
|
|
262
302
|
/**
|
|
@@ -283,6 +323,32 @@ export declare function findLiveDaemonBootstrap({ profile, bootstrapPathOverride
|
|
|
283
323
|
* file is malformed, the CLI shouldn't try to repair it).
|
|
284
324
|
*/
|
|
285
325
|
export declare function readBootstrap(filePath: string): LocalBootstrap | null;
|
|
326
|
+
/**
|
|
327
|
+
* Why a descriptor file did or did not yield a usable bootstrap (OSK-13222).
|
|
328
|
+
*
|
|
329
|
+
* `readBootstrap` answers null for all of these alike, which is right for a
|
|
330
|
+
* caller that only wants "is there a daemon to ask", and wrong for the one
|
|
331
|
+
* that has to explain a failure: a descriptor naming a pid that has exited is
|
|
332
|
+
* a daemon that crashed or is restarting, not a person who is signed out, and
|
|
333
|
+
* the two have opposite repairs.
|
|
334
|
+
*/
|
|
335
|
+
export type BootstrapInspection = {
|
|
336
|
+
state: 'ok';
|
|
337
|
+
bootstrap: LocalBootstrap;
|
|
338
|
+
} | {
|
|
339
|
+
state: 'absent';
|
|
340
|
+
} | {
|
|
341
|
+
state: 'unreadable';
|
|
342
|
+
detail: string;
|
|
343
|
+
} | {
|
|
344
|
+
state: 'invalid';
|
|
345
|
+
detail: string;
|
|
346
|
+
} | {
|
|
347
|
+
state: 'dead_pid';
|
|
348
|
+
pid: number;
|
|
349
|
+
bootstrap: LocalBootstrap;
|
|
350
|
+
};
|
|
351
|
+
export declare function inspectBootstrap(filePath: string): BootstrapInspection;
|
|
286
352
|
/**
|
|
287
353
|
* Try the daemon-as-broker handoff. Returns a tagged outcome so callers
|
|
288
354
|
* can decide between fall-through and surface-to-user without parsing
|
|
@@ -395,6 +461,17 @@ export interface MaybeAutoBrokerOptions {
|
|
|
395
461
|
* everywhere else. Production callers omit it and behave exactly as before.
|
|
396
462
|
*/
|
|
397
463
|
bootstrapPathOverride?: string;
|
|
464
|
+
/**
|
|
465
|
+
* The profile whose daemon may be borrowed. Defaults to the active profile.
|
|
466
|
+
*
|
|
467
|
+
* A silent hand-off must never cross profiles (OSK-13257). The `default`
|
|
468
|
+
* candidate list falls through to any running daemon, and every daemon is
|
|
469
|
+
* signed in as whoever started it — so `--profile buyer`, with its own
|
|
470
|
+
* session refused, used to run as the machine owner after one stderr line.
|
|
471
|
+
* A named profile now borrows only a daemon started under that same
|
|
472
|
+
* profile, and otherwise reports NOT_SIGNED_IN for the profile it was given.
|
|
473
|
+
*/
|
|
474
|
+
profile?: string;
|
|
398
475
|
}
|
|
399
476
|
export interface MaybeAutoBrokerResult {
|
|
400
477
|
/**
|
|
@@ -431,7 +508,10 @@ export interface MaybeAutoBrokerResult {
|
|
|
431
508
|
* 'access_token'`. On a descriptor that advertises broker mode the
|
|
432
509
|
* outcome is `brokered` — an in-memory access token, nothing written;
|
|
433
510
|
* the caller holds it for this process and the next `skrr` re-redeems.
|
|
434
|
-
*
|
|
511
|
+
* A failed redeem on such a descriptor is returned as the outcome and
|
|
512
|
+
* never retried as a durable mint (OSK-12018). Only on a legacy/v1
|
|
513
|
+
* descriptor (no `access_token` advertised — the compatibility window
|
|
514
|
+
* documented in `brokerThrough`) is the durable mint persisted to the
|
|
435
515
|
* keychain / cli-auth.json exactly as before.
|
|
436
516
|
* - May generate a new `cliId` if the config doesn't have one. The
|
|
437
517
|
* caller MUST persist `updatedConfig` so the next invocation
|