@skrr-ai/cli 0.1.79 → 0.1.80
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.js +9 -2
- package/dist/commands/inbox/index.js +4 -2
- package/dist/commands/login.d.ts +8 -3
- package/dist/commands/login.js +14 -4
- package/dist/commands/spaces/workflows/events.js +9 -2
- package/dist/lib/agentic-stream.js +13 -4
- package/dist/lib/api-fetch.js +6 -2
- package/dist/lib/brokered-dpop.d.ts +34 -0
- package/dist/lib/brokered-dpop.js +89 -0
- package/dist/lib/daemonBroker.d.ts +71 -2
- package/dist/lib/daemonBroker.js +203 -36
- 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.js +6 -3
- 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 +4 -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/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/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 +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +19 -3
- 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/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/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 +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3422 -3407
- package/oclif.manifest.json +31930 -31930
- package/package.json +2 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_POLL_MS = exports.SUPERVISED_GRACE_MS = exports.DEFAULT_RESTART_WAIT_MS = exports.STARTING_WINDOW_MS = void 0;
|
|
37
|
+
exports.probeDaemonRestart = probeDaemonRestart;
|
|
38
|
+
exports.awaitRestartedDaemon = awaitRestartedDaemon;
|
|
39
|
+
/**
|
|
40
|
+
* Bounded wait for a local daemon that is restarting (OSK-13222).
|
|
41
|
+
*
|
|
42
|
+
* The CLI's credential on a daemon-brokered machine is redeemed per process
|
|
43
|
+
* over the daemon's loopback hand-off. A daemon restart opens a window in which
|
|
44
|
+
* there is nothing to redeem from:
|
|
45
|
+
*
|
|
46
|
+
* - a GRACEFUL restart removes the hand-off descriptor on shutdown and the
|
|
47
|
+
* replacement publishes a new one only once it is listening — measured at
|
|
48
|
+
* ~14s on a laptop (shutdown 03:11:34 → `listening` 03:11:48), most of it
|
|
49
|
+
* harness detection;
|
|
50
|
+
* - a CRASH leaves the descriptor naming a pid that has exited until the
|
|
51
|
+
* supervisor starts a replacement.
|
|
52
|
+
*
|
|
53
|
+
* Every `skrr` process landing in that window used to report "Not signed in.
|
|
54
|
+
* Run `skrr login` first." and succeed on an immediate retry. This module
|
|
55
|
+
* decides whether a restart is plausibly in progress and, if so, waits for a
|
|
56
|
+
* fresh descriptor — bounded, and never when nothing says a daemon is coming
|
|
57
|
+
* back, so a machine with no daemon pays nothing.
|
|
58
|
+
*
|
|
59
|
+
* The signals, cheapest to strongest:
|
|
60
|
+
*
|
|
61
|
+
* - `starting` — the profile's singleton-lock sidecar names a LIVE pid that
|
|
62
|
+
* acquired the lock recently. That is a daemon between lock and listen;
|
|
63
|
+
* its descriptor is coming. Wait up to the full budget.
|
|
64
|
+
* - `supervised` — no live lock holder, but a launchd plist / systemd user
|
|
65
|
+
* unit for the profile is installed, so a supervisor is expected to bring
|
|
66
|
+
* one up (the ~1.4s between a graceful release and the next acquire, or a
|
|
67
|
+
* crash awaiting respawn). Wait a short grace, extended to the full budget
|
|
68
|
+
* if a holder appears.
|
|
69
|
+
* - `none` — no daemon is coming, or one is running and deliberately not
|
|
70
|
+
* serving hand-offs (its lock is old: auth latched, OSK-12807). Do not wait.
|
|
71
|
+
*/
|
|
72
|
+
const fs = __importStar(require("node:fs"));
|
|
73
|
+
const os = __importStar(require("node:os"));
|
|
74
|
+
const path = __importStar(require("node:path"));
|
|
75
|
+
/** A lock acquired within this window belongs to a daemon still starting up. */
|
|
76
|
+
exports.STARTING_WINDOW_MS = 60_000;
|
|
77
|
+
/** Total wait once a starting daemon is seen. Startup was measured at ~14s. */
|
|
78
|
+
exports.DEFAULT_RESTART_WAIT_MS = 20_000;
|
|
79
|
+
/** Wait for a supervisor to start a daemon when none holds the lock yet. */
|
|
80
|
+
exports.SUPERVISED_GRACE_MS = 4_000;
|
|
81
|
+
exports.DEFAULT_POLL_MS = 250;
|
|
82
|
+
const PROFILE_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
83
|
+
function pidAlive(pid) {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return err.code === 'EPERM';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read the restart signal for `profile` from the daemon's own on-disk state.
|
|
94
|
+
* Never throws; anything unreadable reads as absent.
|
|
95
|
+
*/
|
|
96
|
+
function probeDaemonRestart(profile, opts = {}) {
|
|
97
|
+
if (!PROFILE_RE.test(profile))
|
|
98
|
+
return 'none';
|
|
99
|
+
const home = opts.homeDir ?? os.homedir();
|
|
100
|
+
const now = opts.now ?? Date.now();
|
|
101
|
+
const platform = opts.platform ?? process.platform;
|
|
102
|
+
try {
|
|
103
|
+
const infoPath = path.join(home, '.skrr', 'profiles', profile, 'daemon.lock.info.json');
|
|
104
|
+
const info = JSON.parse(fs.readFileSync(infoPath, 'utf8'));
|
|
105
|
+
if (typeof info.pid === 'number' && info.pid > 0 && pidAlive(info.pid)) {
|
|
106
|
+
const startedAt = typeof info.startedAt === 'string' ? Date.parse(info.startedAt) : NaN;
|
|
107
|
+
// A live holder that started long ago is a running daemon that is not
|
|
108
|
+
// serving hand-offs on purpose — waiting would only add latency.
|
|
109
|
+
if (Number.isFinite(startedAt) && now - startedAt <= exports.STARTING_WINDOW_MS)
|
|
110
|
+
return 'starting';
|
|
111
|
+
return 'none';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* no sidecar, or unreadable: fall through to the supervisor check */
|
|
116
|
+
}
|
|
117
|
+
// Supervisor names mirror daemon/src/profile.ts (`launchdLabelFor`,
|
|
118
|
+
// `systemdUnitFor`); the CLI does not import daemon code.
|
|
119
|
+
const units = [];
|
|
120
|
+
if (platform === 'darwin') {
|
|
121
|
+
units.push(path.join(home, 'Library', 'LaunchAgents', `ai.skrr.daemon.${profile}.plist`));
|
|
122
|
+
}
|
|
123
|
+
else if (platform === 'linux') {
|
|
124
|
+
units.push(path.join(home, '.config', 'systemd', 'user', `skrrd-${profile}.service`));
|
|
125
|
+
}
|
|
126
|
+
for (const unit of units) {
|
|
127
|
+
try {
|
|
128
|
+
if (fs.existsSync(unit))
|
|
129
|
+
return 'supervised';
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* unreadable reads as absent */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return 'none';
|
|
136
|
+
}
|
|
137
|
+
function envBudget() {
|
|
138
|
+
const raw = process.env.SKRR_DAEMON_RESTART_WAIT_MS;
|
|
139
|
+
if (raw === undefined || raw.trim() === '')
|
|
140
|
+
return undefined;
|
|
141
|
+
const n = Number(raw);
|
|
142
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
143
|
+
}
|
|
144
|
+
async function awaitRestartedDaemon(opts) {
|
|
145
|
+
const maxWaitMs = envBudget() ?? opts.maxWaitMs ?? exports.DEFAULT_RESTART_WAIT_MS;
|
|
146
|
+
if (maxWaitMs <= 0)
|
|
147
|
+
return { waited: false, waitedMs: 0, ready: false };
|
|
148
|
+
const probe = opts.probe ?? ((p) => probeDaemonRestart(p));
|
|
149
|
+
const now = opts.now ?? Date.now;
|
|
150
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
151
|
+
const pollMs = opts.pollMs ?? exports.DEFAULT_POLL_MS;
|
|
152
|
+
const grace = Math.min(opts.supervisedGraceMs ?? exports.SUPERVISED_GRACE_MS, maxWaitMs);
|
|
153
|
+
let signal = probe(opts.profile);
|
|
154
|
+
if (signal === 'none')
|
|
155
|
+
return { waited: false, waitedMs: 0, ready: false };
|
|
156
|
+
const start = now();
|
|
157
|
+
let deadline = start + (signal === 'starting' ? maxWaitMs : grace);
|
|
158
|
+
const notify = opts.notify ??
|
|
159
|
+
((message) => {
|
|
160
|
+
process.stderr.write(`[skrr] ${message}\n`);
|
|
161
|
+
});
|
|
162
|
+
notify(`The skrr background service on this computer is restarting; waiting up to ${Math.round(maxWaitMs / 1000)}s for it to come back.`);
|
|
163
|
+
for (;;) {
|
|
164
|
+
if (opts.ready())
|
|
165
|
+
return { waited: true, waitedMs: now() - start, ready: true };
|
|
166
|
+
if (now() >= deadline)
|
|
167
|
+
break;
|
|
168
|
+
await sleep(pollMs);
|
|
169
|
+
signal = probe(opts.profile);
|
|
170
|
+
if (signal === 'starting')
|
|
171
|
+
deadline = Math.max(deadline, start + maxWaitMs);
|
|
172
|
+
else if (signal === 'none') {
|
|
173
|
+
// Nothing is coming back after all — but a descriptor may have landed
|
|
174
|
+
// between the last check and this probe.
|
|
175
|
+
if (opts.ready())
|
|
176
|
+
return { waited: true, waitedMs: now() - start, ready: true };
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { waited: true, waitedMs: now() - start, ready: false };
|
|
181
|
+
}
|
|
@@ -26,6 +26,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
26
26
|
const socket_io_client_1 = require("socket.io-client");
|
|
27
27
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
28
28
|
const agentic_stream_1 = require("./agentic-stream");
|
|
29
|
+
const dpop_auth_1 = require("./dpop-auth");
|
|
29
30
|
const format_1 = require("./format");
|
|
30
31
|
/**
|
|
31
32
|
* Declared services on a Dedicated Runtime — the CLI half of
|
|
@@ -411,17 +412,20 @@ function specFromFlags(input) {
|
|
|
411
412
|
}
|
|
412
413
|
return spec;
|
|
413
414
|
}
|
|
414
|
-
function socketOptions(token) {
|
|
415
|
+
function socketOptions(token, wsOrigin) {
|
|
415
416
|
return {
|
|
416
417
|
path: '/ws/agentic',
|
|
417
|
-
|
|
418
|
+
// OSK-12020 — a DPoP-bound token proves possession on every handshake.
|
|
419
|
+
auth: (0, dpop_auth_1.socketAuthWithDpop)({
|
|
418
420
|
token,
|
|
419
421
|
// This proves origin only; the normal CLI token retains full execution authority.
|
|
420
|
-
...(process.env.OVERSKY_SESSION_TOKEN
|
|
422
|
+
...(process.env.OVERSKY_SESSION_TOKEN
|
|
423
|
+
? { executionOriginToken: process.env.OVERSKY_SESSION_TOKEN }
|
|
424
|
+
: {}),
|
|
421
425
|
...(0, data_provider_1.getClientIdentityAuth)(),
|
|
422
426
|
clientPlatform: 'cli',
|
|
423
427
|
clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
424
|
-
},
|
|
428
|
+
}, wsOrigin),
|
|
425
429
|
transports: ['websocket'],
|
|
426
430
|
reconnection: false,
|
|
427
431
|
};
|
|
@@ -442,7 +446,8 @@ class DedicatedServiceChannel {
|
|
|
442
446
|
constructor(options) {
|
|
443
447
|
this.options = options;
|
|
444
448
|
const connect = options.connect ?? socket_io_client_1.io;
|
|
445
|
-
|
|
449
|
+
const wsOrigin = (0, agentic_stream_1.toWebSocketOrigin)(options.baseURL);
|
|
450
|
+
this.socket = connect(wsOrigin, socketOptions(options.token, wsOrigin));
|
|
446
451
|
// An ack resolves a request only when it arrives on THAT request's own
|
|
447
452
|
// ack event — a requestId echoed back on the wrong family is not an
|
|
448
453
|
// answer, however well-formed it looks.
|
|
@@ -22,6 +22,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
22
22
|
const socket_io_client_1 = require("socket.io-client");
|
|
23
23
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
24
24
|
const agentic_stream_1 = require("./agentic-stream");
|
|
25
|
+
const dpop_auth_1 = require("./dpop-auth");
|
|
25
26
|
/**
|
|
26
27
|
* Owner SSH to a Dedicated Runtime — SSH protocol over the admitted relay.
|
|
27
28
|
*
|
|
@@ -284,14 +285,16 @@ function runSshProxy(options) {
|
|
|
284
285
|
return new Promise((resolve) => {
|
|
285
286
|
let started = false;
|
|
286
287
|
let finished = false;
|
|
287
|
-
const
|
|
288
|
+
const wsOrigin = (0, agentic_stream_1.toWebSocketOrigin)(options.baseURL);
|
|
289
|
+
const socket = connect(wsOrigin, {
|
|
288
290
|
path: '/ws/agentic',
|
|
289
|
-
|
|
291
|
+
// OSK-12020 — a DPoP-bound token proves possession on every handshake.
|
|
292
|
+
auth: (0, dpop_auth_1.socketAuthWithDpop)({
|
|
290
293
|
token: options.token,
|
|
291
294
|
...(0, data_provider_1.getClientIdentityAuth)(),
|
|
292
295
|
clientPlatform: 'cli',
|
|
293
296
|
clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
294
|
-
},
|
|
297
|
+
}, wsOrigin),
|
|
295
298
|
transports: ['websocket'],
|
|
296
299
|
// A reconnect cannot resume a channel whose admission line the daemon
|
|
297
300
|
// revoked on socket death — say it ended and let ssh retry fresh.
|
|
@@ -9,6 +9,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
9
9
|
const socket_io_client_1 = require("socket.io-client");
|
|
10
10
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
11
11
|
const agentic_stream_1 = require("./agentic-stream");
|
|
12
|
+
const dpop_auth_1 = require("./dpop-auth");
|
|
12
13
|
/**
|
|
13
14
|
* A real pseudo-terminal on a Dedicated Runtime, from a local terminal.
|
|
14
15
|
*
|
|
@@ -100,14 +101,16 @@ function runDedicatedTerminal(options) {
|
|
|
100
101
|
let retryTimer;
|
|
101
102
|
let recoveryTimer;
|
|
102
103
|
let connected = false;
|
|
103
|
-
const
|
|
104
|
+
const wsOrigin = (0, agentic_stream_1.toWebSocketOrigin)(options.baseURL);
|
|
105
|
+
const socket = connect(wsOrigin, {
|
|
104
106
|
path: '/ws/agentic',
|
|
105
|
-
|
|
107
|
+
// OSK-12020 — a DPoP-bound token proves possession on every handshake.
|
|
108
|
+
auth: (0, dpop_auth_1.socketAuthWithDpop)({
|
|
106
109
|
token: options.token,
|
|
107
110
|
...(0, data_provider_1.getClientIdentityAuth)(),
|
|
108
111
|
clientPlatform: 'cli',
|
|
109
112
|
clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
110
|
-
},
|
|
113
|
+
}, wsOrigin),
|
|
111
114
|
transports: ['websocket'],
|
|
112
115
|
// A bare PTY cannot be resumed: the relay forgets the session with the
|
|
113
116
|
// socket, and silently opening a NEW shell would drop the user into a
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dpop-auth.ts — attach RFC 9449 proofs for a DPoP-bound hand-off token
|
|
3
|
+
* (OSK-12020).
|
|
4
|
+
*
|
|
5
|
+
* The ONE place a request path asks for a proof, so every transport the CLI
|
|
6
|
+
* has — `apiFetch`, the data-service node adapter, SSE, and Socket.IO — binds
|
|
7
|
+
* the same way. For any other credential both helpers are no-ops and the wire
|
|
8
|
+
* is byte-identical to before.
|
|
9
|
+
*
|
|
10
|
+
* The CLI never holds the key. Each proof comes from the local daemon
|
|
11
|
+
* (`brokeredDpopProof`), which keeps the private half in its own memory; that
|
|
12
|
+
* is what makes a copied token useless anywhere but on this machine.
|
|
13
|
+
*/
|
|
14
|
+
/** RFC 9449 §4.1. */
|
|
15
|
+
export declare const DPOP_HEADER = "DPoP";
|
|
16
|
+
/** Headers to add to one HTTP request made with `token`; `{}` when unbound. */
|
|
17
|
+
export declare function dpopHeadersFor(token: string | null | undefined, method: string, url: string): Promise<Record<string, string>>;
|
|
18
|
+
/**
|
|
19
|
+
* The URI a Socket.IO handshake proof commits to: the realtime origin's
|
|
20
|
+
* http(s) form plus the engine path, no trailing slash — what the server
|
|
21
|
+
* rebuilds from the handshake request (`reconstructHandshakeHtu`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function socketHandshakeHtu(wsOrigin: string, ioPath?: string): string;
|
|
24
|
+
type SocketAuth = Record<string, unknown>;
|
|
25
|
+
type SocketAuthFn = (cb: (data: SocketAuth) => void) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Socket.IO `auth` for a handshake made with `auth.token`. Unbound: the object
|
|
28
|
+
* itself, unchanged. Bound: a FUNCTION, which socket.io-client calls on every
|
|
29
|
+
* (re)connect — so each handshake presents a fresh, single-use proof instead
|
|
30
|
+
* of replaying the first one into the server's jti cache.
|
|
31
|
+
*/
|
|
32
|
+
export declare function socketAuthWithDpop(auth: SocketAuth & {
|
|
33
|
+
token?: string | null;
|
|
34
|
+
}, wsOrigin: string, ioPath?: string): SocketAuth | SocketAuthFn;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dpop-auth.ts — attach RFC 9449 proofs for a DPoP-bound hand-off token
|
|
4
|
+
* (OSK-12020).
|
|
5
|
+
*
|
|
6
|
+
* The ONE place a request path asks for a proof, so every transport the CLI
|
|
7
|
+
* has — `apiFetch`, the data-service node adapter, SSE, and Socket.IO — binds
|
|
8
|
+
* the same way. For any other credential both helpers are no-ops and the wire
|
|
9
|
+
* is byte-identical to before.
|
|
10
|
+
*
|
|
11
|
+
* The CLI never holds the key. Each proof comes from the local daemon
|
|
12
|
+
* (`brokeredDpopProof`), which keeps the private half in its own memory; that
|
|
13
|
+
* is what makes a copied token useless anywhere but on this machine.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DPOP_HEADER = void 0;
|
|
17
|
+
exports.dpopHeadersFor = dpopHeadersFor;
|
|
18
|
+
exports.socketHandshakeHtu = socketHandshakeHtu;
|
|
19
|
+
exports.socketAuthWithDpop = socketAuthWithDpop;
|
|
20
|
+
const brokered_dpop_1 = require("./brokered-dpop");
|
|
21
|
+
/** RFC 9449 §4.1. */
|
|
22
|
+
exports.DPOP_HEADER = 'DPoP';
|
|
23
|
+
/** Headers to add to one HTTP request made with `token`; `{}` when unbound. */
|
|
24
|
+
async function dpopHeadersFor(token, method, url) {
|
|
25
|
+
if (!(0, brokered_dpop_1.isDpopBoundHandoffToken)(token))
|
|
26
|
+
return {};
|
|
27
|
+
const proof = await (0, brokered_dpop_1.brokeredDpopProof)(token, method, url);
|
|
28
|
+
return proof ? { [exports.DPOP_HEADER]: proof } : {};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The URI a Socket.IO handshake proof commits to: the realtime origin's
|
|
32
|
+
* http(s) form plus the engine path, no trailing slash — what the server
|
|
33
|
+
* rebuilds from the handshake request (`reconstructHandshakeHtu`).
|
|
34
|
+
*/
|
|
35
|
+
function socketHandshakeHtu(wsOrigin, ioPath = '/ws/agentic') {
|
|
36
|
+
const u = new URL(wsOrigin);
|
|
37
|
+
u.protocol = u.protocol === 'wss:' ? 'https:' : u.protocol === 'ws:' ? 'http:' : u.protocol;
|
|
38
|
+
u.pathname = ioPath.replace(/\/+$/, '');
|
|
39
|
+
u.search = '';
|
|
40
|
+
u.hash = '';
|
|
41
|
+
return u.toString();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Socket.IO `auth` for a handshake made with `auth.token`. Unbound: the object
|
|
45
|
+
* itself, unchanged. Bound: a FUNCTION, which socket.io-client calls on every
|
|
46
|
+
* (re)connect — so each handshake presents a fresh, single-use proof instead
|
|
47
|
+
* of replaying the first one into the server's jti cache.
|
|
48
|
+
*/
|
|
49
|
+
function socketAuthWithDpop(auth, wsOrigin, ioPath = '/ws/agentic') {
|
|
50
|
+
const token = typeof auth.token === 'string' ? auth.token : null;
|
|
51
|
+
if (!(0, brokered_dpop_1.isDpopBoundHandoffToken)(token))
|
|
52
|
+
return auth;
|
|
53
|
+
const htu = socketHandshakeHtu(wsOrigin, ioPath);
|
|
54
|
+
return (cb) => {
|
|
55
|
+
void (0, brokered_dpop_1.brokeredDpopProof)(token, 'GET', htu).then((proof) => cb(proof ? { ...auth, dpop: proof } : auth), () => cb(auth));
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/lib/login.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface CliLoginWithRefreshTokenOptions {
|
|
|
42
42
|
* What a shell with no terminal can do instead (OSK-12117). The refusal used to
|
|
43
43
|
* name only CI tokens, which is not a sign-in: an agent session asking for one
|
|
44
44
|
* could only hand the job to a person typing `! skrr login`.
|
|
45
|
+
*
|
|
46
|
+
* It also used to send CI to `skrr create-token` + `OVERSKY_TOKEN=osk_ci_...`.
|
|
47
|
+
* That token only bootstraps a daemon; every `skrr` command rejects it with 401
|
|
48
|
+
* (create-token.ts, base-command.ts), so the advice was a dead end (OSK-13209).
|
|
49
|
+
* The route that works today is a minted machine session — see
|
|
50
|
+
* docs/cli/SKRR_AUTH.md "CI today" for its caveats.
|
|
45
51
|
*/
|
|
46
52
|
export declare const HEADLESS_LOGIN_HINT: string;
|
|
47
53
|
/**
|
package/dist/lib/login.js
CHANGED
|
@@ -43,10 +43,18 @@ const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
|
|
|
43
43
|
* What a shell with no terminal can do instead (OSK-12117). The refusal used to
|
|
44
44
|
* name only CI tokens, which is not a sign-in: an agent session asking for one
|
|
45
45
|
* could only hand the job to a person typing `! skrr login`.
|
|
46
|
+
*
|
|
47
|
+
* It also used to send CI to `skrr create-token` + `OVERSKY_TOKEN=osk_ci_...`.
|
|
48
|
+
* That token only bootstraps a daemon; every `skrr` command rejects it with 401
|
|
49
|
+
* (create-token.ts, base-command.ts), so the advice was a dead end (OSK-13209).
|
|
50
|
+
* The route that works today is a minted machine session — see
|
|
51
|
+
* docs/cli/SKRR_AUTH.md "CI today" for its caveats.
|
|
46
52
|
*/
|
|
47
53
|
exports.HEADLESS_LOGIN_HINT = '`skrr login --device-code` works without a terminal: it prints a URL and a code for a ' +
|
|
48
|
-
'person to approve in any browser, and finishes when they do.
|
|
49
|
-
'`skrr
|
|
54
|
+
'person to approve in any browser, and finishes when they do. With no person to approve ' +
|
|
55
|
+
'(CI, a script), mint a session for this machine elsewhere with `skrr token mint --for <id>` ' +
|
|
56
|
+
'and run `skrr login` here with OVERSKY_REFRESH_TOKEN and OVERSKY_CLI_ID set. ' +
|
|
57
|
+
'`skrr create-token` tokens are not accepted by skrr commands.';
|
|
50
58
|
/**
|
|
51
59
|
* The line printed when the daemon descriptor this CLI found is for another
|
|
52
60
|
* server (OSK-12117).
|
|
@@ -164,8 +172,9 @@ async function cliLogin(opts = {}) {
|
|
|
164
172
|
// silently fall through to PKCE / device-code below.
|
|
165
173
|
if (brokerResult.reason === 'ci_token_refused') {
|
|
166
174
|
throw new Error('The skrr background service on this computer is bootstrapped from a CI token, which cannot broker an interactive CLI login. ' +
|
|
167
|
-
'
|
|
168
|
-
'
|
|
175
|
+
'Sign this CLI in on its own with `skrr login --device-code`, or with a session minted elsewhere by ' +
|
|
176
|
+
'`skrr token mint --for <id>` (OVERSKY_REFRESH_TOKEN + OVERSKY_CLI_ID). ' +
|
|
177
|
+
'A `skrr create-token` token is not accepted by skrr commands.');
|
|
169
178
|
}
|
|
170
179
|
// `base_url_mismatch` is a configuration drift the user can resolve
|
|
171
180
|
// explicitly; print a one-line hint so they're not surprised when
|
package/dist/lib/node-adapter.js
CHANGED
|
@@ -12,6 +12,7 @@ const config_1 = require("./config");
|
|
|
12
12
|
const auth_storage_1 = require("./auth-storage");
|
|
13
13
|
const credential_resolver_1 = require("./credential-resolver");
|
|
14
14
|
const daemonBroker_1 = require("./daemonBroker");
|
|
15
|
+
const dpop_auth_1 = require("./dpop-auth");
|
|
15
16
|
const refresh_1 = require("./refresh");
|
|
16
17
|
const delegated_cli_1 = require("./delegated-cli");
|
|
17
18
|
const workspace_header_1 = require("./workspace-header");
|
|
@@ -159,6 +160,9 @@ function createNodeAdapter(opts = {}) {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
const finalUrl = appendQuery(resolveUrl(url), options?.params);
|
|
163
|
+
// OSK-12020 — a DPoP-bound hand-off token needs a fresh proof per request
|
|
164
|
+
// (and per retry: proofs are single-use). No-op for every other token.
|
|
165
|
+
Object.assign(headers, await (0, dpop_auth_1.dpopHeadersFor)(currentCredential().token, method, finalUrl));
|
|
162
166
|
return fetch(finalUrl, { method, headers, body: finalBody });
|
|
163
167
|
}
|
|
164
168
|
/**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* - `access_token`: broker mode — a short-lived access token from a
|
|
23
23
|
* daemon-owned family; nothing durable leaves the daemon.
|
|
24
24
|
*/
|
|
25
|
-
export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token"];
|
|
25
|
+
export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token", "access_token_dpop"];
|
|
26
26
|
export type CliHandoffMode = (typeof CLI_HANDOFF_MODES)[number];
|
|
27
27
|
/** Broker-mode redeem route on the daemon's loopback server. */
|
|
28
28
|
export declare const CLI_HANDOFF_TOKEN_PATH = "/v1/auth/cli-handoff-token";
|
|
@@ -74,10 +74,26 @@ export declare function normalizeHandoffModes(value: unknown): CliHandoffMode[];
|
|
|
74
74
|
*/
|
|
75
75
|
export declare function advertisedHandoffModes(d: CliHandoffDescriptor): CliHandoffMode[];
|
|
76
76
|
export declare function descriptorOffersAccessToken(d: CliHandoffDescriptor): boolean;
|
|
77
|
+
/** True when the token route can serve DPoP-bound tokens (OSK-12020). */
|
|
78
|
+
export declare function descriptorOffersDpop(d: CliHandoffDescriptor): boolean;
|
|
77
79
|
export interface CliHandoffTokenRequest {
|
|
78
80
|
/** Bypass the daemon broker's cached token (the 401-recovery path). */
|
|
79
81
|
forceRefresh?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* The caller can fetch per-request proofs from `CLI_HANDOFF_DPOP_PROOF_PATH`
|
|
84
|
+
* and wants a DPoP-bound token. Sent only to a descriptor that advertises
|
|
85
|
+
* `access_token_dpop`; a daemon that does not know the field ignores it and
|
|
86
|
+
* answers an unbound token, which `tokenType` then says.
|
|
87
|
+
*/
|
|
88
|
+
dpop?: boolean;
|
|
80
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* How the token must be presented. `DPoP` means the token carries `cnf.jkt`
|
|
92
|
+
* and every request needs a fresh proof; absent or `Bearer` means an ordinary
|
|
93
|
+
* bearer token. Read from the RESPONSE, never assumed from the request: an
|
|
94
|
+
* older server ignores the binding request and mints an unbound token.
|
|
95
|
+
*/
|
|
96
|
+
export type CliHandoffTokenType = 'Bearer' | 'DPoP';
|
|
81
97
|
export interface CliHandoffTokenResponse {
|
|
82
98
|
accessToken: string;
|
|
83
99
|
/** Absolute expiry in ms since epoch, when the daemon knows it. */
|
|
@@ -85,6 +101,31 @@ export interface CliHandoffTokenResponse {
|
|
|
85
101
|
/** Server the minted credential belongs to — lets the caller verify it is
|
|
86
102
|
* about to use a token for the API it thinks it is talking to. */
|
|
87
103
|
serverUrl?: string;
|
|
104
|
+
tokenType?: CliHandoffTokenType;
|
|
88
105
|
}
|
|
89
106
|
/** Parse the redeem response; null when the token itself is absent. */
|
|
90
107
|
export declare function parseCliHandoffTokenResponse(value: unknown): CliHandoffTokenResponse | null;
|
|
108
|
+
/** The daemon refuses an UNBOUND token because its posture requires binding. */
|
|
109
|
+
export declare const CLI_HANDOFF_ERROR_DPOP_REQUIRED = "HANDOFF_DPOP_REQUIRED";
|
|
110
|
+
/**
|
|
111
|
+
* The daemon signs RFC 9449 proofs for a bound token with a key that never
|
|
112
|
+
* leaves it. Same descriptor secret as the token route: using a stolen bound
|
|
113
|
+
* token therefore needs live loopback access to THIS daemon, which is the
|
|
114
|
+
* boundary the capability already has.
|
|
115
|
+
*/
|
|
116
|
+
export declare const CLI_HANDOFF_DPOP_PROOF_PATH = "/v1/auth/cli-handoff-dpop-proof";
|
|
117
|
+
/** The oracle refused to sign (bad htm/htu, or `ath` names no token it issued). */
|
|
118
|
+
export declare const CLI_HANDOFF_ERROR_DPOP_PROOF_REFUSED = "HANDOFF_DPOP_PROOF_REFUSED";
|
|
119
|
+
/** The daemon holds no DPoP key (binding is off on this daemon). */
|
|
120
|
+
export declare const CLI_HANDOFF_ERROR_DPOP_UNAVAILABLE = "HANDOFF_DPOP_UNAVAILABLE";
|
|
121
|
+
export interface CliHandoffDpopProofRequest {
|
|
122
|
+
htm: string;
|
|
123
|
+
htu: string;
|
|
124
|
+
/** base64url SHA-256 of the bound access token. The token itself is never
|
|
125
|
+
* sent: the oracle commits to a hash, and only to one it issued. */
|
|
126
|
+
ath: string;
|
|
127
|
+
}
|
|
128
|
+
export interface CliHandoffDpopProofResponse {
|
|
129
|
+
proof: string;
|
|
130
|
+
}
|
|
131
|
+
export declare function parseCliHandoffDpopProofResponse(value: unknown): CliHandoffDpopProofResponse | null;
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
* must produce the full shape.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.CLI_HANDOFF_ERROR_BROKER_ONLY = exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = exports.CLI_HANDOFF_TOKEN_PATH = exports.CLI_HANDOFF_MODES = void 0;
|
|
21
|
+
exports.CLI_HANDOFF_ERROR_DPOP_UNAVAILABLE = exports.CLI_HANDOFF_ERROR_DPOP_PROOF_REFUSED = exports.CLI_HANDOFF_DPOP_PROOF_PATH = exports.CLI_HANDOFF_ERROR_DPOP_REQUIRED = exports.CLI_HANDOFF_ERROR_BROKER_ONLY = exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = exports.CLI_HANDOFF_TOKEN_PATH = exports.CLI_HANDOFF_MODES = void 0;
|
|
22
22
|
exports.parseCliHandoffDescriptor = parseCliHandoffDescriptor;
|
|
23
23
|
exports.normalizeHandoffModes = normalizeHandoffModes;
|
|
24
24
|
exports.advertisedHandoffModes = advertisedHandoffModes;
|
|
25
25
|
exports.descriptorOffersAccessToken = descriptorOffersAccessToken;
|
|
26
|
+
exports.descriptorOffersDpop = descriptorOffersDpop;
|
|
26
27
|
exports.parseCliHandoffTokenResponse = parseCliHandoffTokenResponse;
|
|
28
|
+
exports.parseCliHandoffDpopProofResponse = parseCliHandoffDpopProofResponse;
|
|
27
29
|
/**
|
|
28
30
|
* What a caller may redeem the descriptor secret for.
|
|
29
31
|
* - `refresh_family`: the legacy durable mint (90-day cli-scope family).
|
|
30
32
|
* - `access_token`: broker mode — a short-lived access token from a
|
|
31
33
|
* daemon-owned family; nothing durable leaves the daemon.
|
|
32
34
|
*/
|
|
33
|
-
exports.CLI_HANDOFF_MODES = ['refresh_family', 'access_token'];
|
|
35
|
+
exports.CLI_HANDOFF_MODES = ['refresh_family', 'access_token', 'access_token_dpop'];
|
|
34
36
|
/** Broker-mode redeem route on the daemon's loopback server. */
|
|
35
37
|
exports.CLI_HANDOFF_TOKEN_PATH = '/v1/auth/cli-handoff-token';
|
|
36
38
|
/** Error codes this contract emits. String literals so both ends compare
|
|
@@ -96,7 +98,12 @@ function advertisedHandoffModes(d) {
|
|
|
96
98
|
return declared.length > 0 ? declared : ['refresh_family'];
|
|
97
99
|
}
|
|
98
100
|
function descriptorOffersAccessToken(d) {
|
|
99
|
-
|
|
101
|
+
const modes = advertisedHandoffModes(d);
|
|
102
|
+
return modes.includes('access_token') || modes.includes('access_token_dpop');
|
|
103
|
+
}
|
|
104
|
+
/** True when the token route can serve DPoP-bound tokens (OSK-12020). */
|
|
105
|
+
function descriptorOffersDpop(d) {
|
|
106
|
+
return advertisedHandoffModes(d).includes('access_token_dpop');
|
|
100
107
|
}
|
|
101
108
|
/** Parse the redeem response; null when the token itself is absent. */
|
|
102
109
|
function parseCliHandoffTokenResponse(value) {
|
|
@@ -109,5 +116,30 @@ function parseCliHandoffTokenResponse(value) {
|
|
|
109
116
|
accessToken: v.accessToken,
|
|
110
117
|
...(typeof v.accessExpiresAt === 'number' ? { accessExpiresAt: v.accessExpiresAt } : {}),
|
|
111
118
|
...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
|
|
119
|
+
...(v.tokenType === 'DPoP' || v.tokenType === 'Bearer' ? { tokenType: v.tokenType } : {}),
|
|
112
120
|
};
|
|
113
121
|
}
|
|
122
|
+
/** The daemon refuses an UNBOUND token because its posture requires binding. */
|
|
123
|
+
exports.CLI_HANDOFF_ERROR_DPOP_REQUIRED = 'HANDOFF_DPOP_REQUIRED';
|
|
124
|
+
// ---------------------------------------------------------------------
|
|
125
|
+
// Proof oracle — POST CLI_HANDOFF_DPOP_PROOF_PATH (OSK-12020)
|
|
126
|
+
// ---------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* The daemon signs RFC 9449 proofs for a bound token with a key that never
|
|
129
|
+
* leaves it. Same descriptor secret as the token route: using a stolen bound
|
|
130
|
+
* token therefore needs live loopback access to THIS daemon, which is the
|
|
131
|
+
* boundary the capability already has.
|
|
132
|
+
*/
|
|
133
|
+
exports.CLI_HANDOFF_DPOP_PROOF_PATH = '/v1/auth/cli-handoff-dpop-proof';
|
|
134
|
+
/** The oracle refused to sign (bad htm/htu, or `ath` names no token it issued). */
|
|
135
|
+
exports.CLI_HANDOFF_ERROR_DPOP_PROOF_REFUSED = 'HANDOFF_DPOP_PROOF_REFUSED';
|
|
136
|
+
/** The daemon holds no DPoP key (binding is off on this daemon). */
|
|
137
|
+
exports.CLI_HANDOFF_ERROR_DPOP_UNAVAILABLE = 'HANDOFF_DPOP_UNAVAILABLE';
|
|
138
|
+
function parseCliHandoffDpopProofResponse(value) {
|
|
139
|
+
if (value == null || typeof value !== 'object')
|
|
140
|
+
return null;
|
|
141
|
+
const v = value;
|
|
142
|
+
if (typeof v.proof !== 'string' || v.proof.split('.').length !== 3)
|
|
143
|
+
return null;
|
|
144
|
+
return { proof: v.proof };
|
|
145
|
+
}
|
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
* Deliberately thin: no persistence, no retry policy, no logging. Callers
|
|
14
14
|
* own their store and their error surfaces; this owns the cache/single-
|
|
15
15
|
* flight/invalidate invariants so they cannot drift between surfaces.
|
|
16
|
+
*
|
|
17
|
+
* Two policies are options because mechanisms genuinely need different ones,
|
|
18
|
+
* and both default to the original behaviour:
|
|
19
|
+
*
|
|
20
|
+
* - `forcedAcquire: 'join'` — a forced acquire joins whatever is in flight,
|
|
21
|
+
* for a mechanism where a second concurrent renewal is itself the hazard
|
|
22
|
+
* (a refresh-family rotation or a family mint);
|
|
23
|
+
* - `classifyAcquireError` — stale-on-degrade: a TRANSIENT failure serves the
|
|
24
|
+
* still-unexpired cached credential instead of failing, a PERMANENT one
|
|
25
|
+
* drops it.
|
|
26
|
+
*
|
|
27
|
+
* Mechanism-specific limits — how often a new family may be minted, which
|
|
28
|
+
* failures retire it — stay in the mechanism. The daemon's task token broker
|
|
29
|
+
* (`daemon/src/task/token-broker.ts`) is the consumer that uses both options.
|
|
16
30
|
*/
|
|
17
31
|
/** What an acquire returns. `expiresAtMs` absent means "no known expiry" —
|
|
18
32
|
* the token is served until `invalidate()` or an acquire replaces it. */
|
|
@@ -26,13 +40,45 @@ export interface CredentialSessionOptions {
|
|
|
26
40
|
* The mechanism. Called with `forceRefresh: true` when the caller asked to
|
|
27
41
|
* bypass the cache (the 401-recovery path) — implementations should renew
|
|
28
42
|
* rather than serve their own cache. A rejected promise propagates to every
|
|
29
|
-
* waiter and is never cached
|
|
43
|
+
* waiter and is never cached (`classifyAcquireError` is the one way a waiter
|
|
44
|
+
* can be served the previous credential instead).
|
|
30
45
|
*/
|
|
31
46
|
acquire: (forceRefresh: boolean) => Promise<AcquiredCredential>;
|
|
32
47
|
/** Serve the cached token until this many ms before expiry. Default 30s. */
|
|
33
48
|
skewMs?: number;
|
|
34
49
|
/** Injectable clock for tests. */
|
|
35
50
|
now?: () => number;
|
|
51
|
+
/**
|
|
52
|
+
* What a forced acquire does when an acquire is already in flight.
|
|
53
|
+
*
|
|
54
|
+
* - `'supersede'` (default): a forced acquire never joins a NON-forced
|
|
55
|
+
* in-flight one — that could serve the very credential a 401 just
|
|
56
|
+
* invalidated — so it starts a second operation, and the superseded one
|
|
57
|
+
* cannot write its result back. Right for a mechanism whose plain
|
|
58
|
+
* acquire may serve its own cache.
|
|
59
|
+
* - `'join'`: every acquire, forced or not, joins the one in flight. Right
|
|
60
|
+
* — and required — for a mechanism whose acquire ALWAYS renews (it never
|
|
61
|
+
* serves a cache of its own), and for which a second concurrent renewal
|
|
62
|
+
* is itself the hazard: a rotation that burns a refresh family, a mint
|
|
63
|
+
* that creates a new one. Whatever is in flight was started after the
|
|
64
|
+
* credential being replaced was issued, so its result is not that
|
|
65
|
+
* credential.
|
|
66
|
+
*/
|
|
67
|
+
forcedAcquire?: 'supersede' | 'join';
|
|
68
|
+
/**
|
|
69
|
+
* Classify an acquire failure. Without it every failure propagates and the
|
|
70
|
+
* cache is left as it was (the default).
|
|
71
|
+
*
|
|
72
|
+
* - `'transient'` — the renewal failed but the cached credential is still
|
|
73
|
+
* good: a waiter is served the cached token while it is inside its HARD
|
|
74
|
+
* expiry (the skew window only says when to start renewing), instead of
|
|
75
|
+
* the error. Nothing stale is served after `invalidate()`, after a forced
|
|
76
|
+
* acquire discarded it, or once it has actually expired.
|
|
77
|
+
* - `'permanent'` — the credential is dead upstream: the cache is dropped
|
|
78
|
+
* and the error propagates.
|
|
79
|
+
* - `undefined` — not classified: the default behaviour.
|
|
80
|
+
*/
|
|
81
|
+
classifyAcquireError?: (err: unknown) => 'transient' | 'permanent' | undefined;
|
|
36
82
|
}
|
|
37
83
|
export interface CredentialSession {
|
|
38
84
|
/**
|
|
@@ -41,6 +87,8 @@ export interface CredentialSession {
|
|
|
41
87
|
* discards the cache first (use after a resource 401).
|
|
42
88
|
*/
|
|
43
89
|
getToken(forceRefresh?: boolean): Promise<string>;
|
|
90
|
+
/** Same as getToken, but resolves the whole credential (with its expiry). */
|
|
91
|
+
getCredential(forceRefresh?: boolean): Promise<AcquiredCredential>;
|
|
44
92
|
/** Drop the cached token; the next getToken() acquires. */
|
|
45
93
|
invalidate(): void;
|
|
46
94
|
}
|