@skrr-ai/cli 0.1.78 → 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-lease-command.js +1 -0
- package/dist/lib/dedicated-service.js +10 -5
- package/dist/lib/dedicated-ssh.js +6 -3
- package/dist/lib/dedicated-terminal.d.ts +3 -0
- package/dist/lib/dedicated-terminal.js +77 -18
- 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 +41505 -41505
- package/package.json +2 -2
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -71,7 +71,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
71
71
|
};
|
|
72
72
|
})();
|
|
73
73
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74
|
-
exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = void 0;
|
|
74
|
+
exports.HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR = exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = void 0;
|
|
75
75
|
exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
76
76
|
exports.resolveBootstrapPath = resolveBootstrapPath;
|
|
77
77
|
exports.resolveProfileBootstrapPaths = resolveProfileBootstrapPaths;
|
|
@@ -80,6 +80,7 @@ exports.resolveCliHandoffCandidatePaths = resolveCliHandoffCandidatePaths;
|
|
|
80
80
|
exports.resolveExactProfileBootstrapPaths = resolveExactProfileBootstrapPaths;
|
|
81
81
|
exports.findLiveDaemonBootstrap = findLiveDaemonBootstrap;
|
|
82
82
|
exports.readBootstrap = readBootstrap;
|
|
83
|
+
exports.inspectBootstrap = inspectBootstrap;
|
|
83
84
|
exports.attemptDaemonBrokerLogin = attemptDaemonBrokerLogin;
|
|
84
85
|
exports.isBrokeredHandoffToken = isBrokeredHandoffToken;
|
|
85
86
|
exports.redeemBrokeredHandoffToken = redeemBrokeredHandoffToken;
|
|
@@ -95,9 +96,11 @@ const os = __importStar(require("node:os"));
|
|
|
95
96
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
96
97
|
const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
97
98
|
const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
|
|
99
|
+
const brokered_dpop_1 = require("./brokered-dpop");
|
|
98
100
|
const auth_storage_1 = require("./auth-storage");
|
|
99
101
|
const config_1 = require("./config");
|
|
100
102
|
const cli_identity_1 = require("./cli-identity");
|
|
103
|
+
const daemonRestartWait_1 = require("./daemonRestartWait");
|
|
101
104
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
102
105
|
/**
|
|
103
106
|
* Normalize a base URL for equality comparison. Mirrors the daemon-side
|
|
@@ -230,6 +233,17 @@ function resolveBootstrapCandidatePaths(profile = 'default') {
|
|
|
230
233
|
* user configures can point the broker somewhere else.
|
|
231
234
|
*/
|
|
232
235
|
exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR = '/run/skrr-dedicated-runtime/cli-handoff/descriptor.json';
|
|
236
|
+
/**
|
|
237
|
+
* A Hosted Machine's workload hand-off descriptor, published only on a box
|
|
238
|
+
* booted under the identity split (OSK-11981, OSK-12019), where the daemon runs
|
|
239
|
+
* as `oversky-daemon` and its island file is unreadable to the workload. Same
|
|
240
|
+
* format and same broker-only contract as the Dedicated descriptor
|
|
241
|
+
* (`daemon/src/dedicated-cli-handoff.ts`, `workloadCliHandoffTarget`), in a
|
|
242
|
+
* DIFFERENT directory so a Hosted Machine is never mistaken for a Dedicated
|
|
243
|
+
* Runtime guest (`runningOnDedicatedRuntimeGuest` keys on that directory). The
|
|
244
|
+
* parent is created by the box's launcher as root and owned by the daemon.
|
|
245
|
+
*/
|
|
246
|
+
exports.HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR = '/run/skrr-hosted-machine/cli-handoff/descriptor.json';
|
|
233
247
|
/** Where the cli-handoff broker looks, in order. */
|
|
234
248
|
function resolveCliHandoffCandidatePaths(profile = 'default') {
|
|
235
249
|
const candidates = resolveBootstrapCandidatePaths(profile);
|
|
@@ -237,7 +251,7 @@ function resolveCliHandoffCandidatePaths(profile = 'default') {
|
|
|
237
251
|
// default profile regardless of `--profile`, so a narrower condition here would
|
|
238
252
|
// only describe behaviour production never reaches.
|
|
239
253
|
if (process.platform === 'linux') {
|
|
240
|
-
candidates.push(exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR);
|
|
254
|
+
candidates.push(exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR, exports.HOSTED_MACHINE_CLI_HANDOFF_DESCRIPTOR);
|
|
241
255
|
}
|
|
242
256
|
return candidates;
|
|
243
257
|
}
|
|
@@ -286,21 +300,28 @@ function findLiveDaemonBootstrap({ profile = 'default', bootstrapPathOverride, }
|
|
|
286
300
|
* file is malformed, the CLI shouldn't try to repair it).
|
|
287
301
|
*/
|
|
288
302
|
function readBootstrap(filePath) {
|
|
303
|
+
const inspected = inspectBootstrap(filePath);
|
|
304
|
+
return inspected.state === 'ok' ? inspected.bootstrap : null;
|
|
305
|
+
}
|
|
306
|
+
function inspectBootstrap(filePath) {
|
|
289
307
|
let raw;
|
|
290
308
|
try {
|
|
291
309
|
if (!fs.existsSync(filePath))
|
|
292
|
-
return
|
|
310
|
+
return { state: 'absent' };
|
|
293
311
|
raw = fs.readFileSync(filePath, 'utf8');
|
|
294
312
|
}
|
|
295
|
-
catch {
|
|
296
|
-
|
|
313
|
+
catch (err) {
|
|
314
|
+
// Removed between the exists check and the read: a daemon shutting down.
|
|
315
|
+
if (err.code === 'ENOENT')
|
|
316
|
+
return { state: 'absent' };
|
|
317
|
+
return { state: 'unreadable', detail: err.message };
|
|
297
318
|
}
|
|
298
319
|
let parsedJson;
|
|
299
320
|
try {
|
|
300
321
|
parsedJson = JSON.parse(raw);
|
|
301
322
|
}
|
|
302
|
-
catch {
|
|
303
|
-
return
|
|
323
|
+
catch (err) {
|
|
324
|
+
return { state: 'invalid', detail: `not JSON: ${err.message}` };
|
|
304
325
|
}
|
|
305
326
|
// Shared wire parser (`@skrr-ai/auth-core/cli-handoff-wire`) validates the
|
|
306
327
|
// fields a caller cannot proceed without — host, port, secret — and
|
|
@@ -309,13 +330,13 @@ function readBootstrap(filePath) {
|
|
|
309
330
|
// port must be a real TCP port, and the secret is a fixed-width key.
|
|
310
331
|
const parsed = (0, cli_handoff_wire_1.parseCliHandoffDescriptor)(parsedJson);
|
|
311
332
|
if (!parsed)
|
|
312
|
-
return
|
|
333
|
+
return { state: 'invalid', detail: 'missing host, port or secret' };
|
|
313
334
|
if (parsed.host !== '127.0.0.1' ||
|
|
314
335
|
typeof parsed.port !== 'number' ||
|
|
315
336
|
parsed.port > 65_535 ||
|
|
316
337
|
typeof parsed.secret !== 'string' ||
|
|
317
338
|
Buffer.from(parsed.secret, 'base64').length !== 32) {
|
|
318
|
-
return
|
|
339
|
+
return { state: 'invalid', detail: 'not a loopback descriptor with a 32-byte secret' };
|
|
319
340
|
}
|
|
320
341
|
// PID liveness — ESRCH means the daemon was force-killed and the file
|
|
321
342
|
// outlived the process. EPERM means alive but not signalable by us,
|
|
@@ -327,11 +348,11 @@ function readBootstrap(filePath) {
|
|
|
327
348
|
catch (err) {
|
|
328
349
|
const code = err.code;
|
|
329
350
|
if (code === 'ESRCH')
|
|
330
|
-
return
|
|
351
|
+
return { state: 'dead_pid', pid: parsed.pid, bootstrap: parsed };
|
|
331
352
|
// EPERM / other: keep going.
|
|
332
353
|
}
|
|
333
354
|
}
|
|
334
|
-
return parsed;
|
|
355
|
+
return { state: 'ok', bootstrap: parsed };
|
|
335
356
|
}
|
|
336
357
|
/**
|
|
337
358
|
* Try the daemon-as-broker handoff. Returns a tagged outcome so callers
|
|
@@ -343,12 +364,83 @@ function readBootstrap(filePath) {
|
|
|
343
364
|
* PKCE", and `ok: true` means "we're done, persist + return".
|
|
344
365
|
*/
|
|
345
366
|
async function attemptDaemonBrokerLogin(opts) {
|
|
367
|
+
const first = await attemptDaemonBrokerLoginOnce(opts);
|
|
368
|
+
if (first.ok || !isRestartShapedFailure(first))
|
|
369
|
+
return first;
|
|
370
|
+
// The local service may be restarting: a graceful restart removes the
|
|
371
|
+
// descriptor and republishes it only once the new daemon is listening
|
|
372
|
+
// (measured ~14s), and a crash leaves it naming a dead pid until launchd
|
|
373
|
+
// brings a replacement up. Every `skrr` process in that window used to be
|
|
374
|
+
// told "Not signed in" and succeed on an immediate retry (OSK-13222). Wait —
|
|
375
|
+
// bounded, and only when something says a daemon is coming back — and ask
|
|
376
|
+
// again.
|
|
346
377
|
const profile = opts.profile ?? 'default';
|
|
347
|
-
const
|
|
378
|
+
const failedFingerprints = new Set();
|
|
379
|
+
if (first.descriptorPort !== undefined || first.descriptorPid !== undefined) {
|
|
380
|
+
failedFingerprints.add(`${first.descriptorPid ?? ''}:${first.descriptorPort ?? ''}`);
|
|
381
|
+
}
|
|
382
|
+
const waited = await (0, daemonRestartWait_1.awaitRestartedDaemon)({
|
|
383
|
+
profile,
|
|
384
|
+
...(opts.restartWait ?? {}),
|
|
385
|
+
ready: () => {
|
|
386
|
+
const fresh = listUsableBootstraps(opts);
|
|
387
|
+
return fresh.some(({ bootstrap }) => !failedFingerprints.has(`${bootstrap.pid ?? ''}:${bootstrap.port}`));
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
if (!waited.waited)
|
|
391
|
+
return first;
|
|
392
|
+
const second = await attemptDaemonBrokerLoginOnce(opts);
|
|
393
|
+
if (second.ok)
|
|
394
|
+
return second;
|
|
395
|
+
return { ...second, restartWaitedMs: waited.waitedMs };
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* A failure that looks like the local daemon is absent or mid-restart rather
|
|
399
|
+
* than a daemon that answered: no usable descriptor, a refused port, or an
|
|
400
|
+
* uncoded 401 on a secret it rotates when it restarts.
|
|
401
|
+
*/
|
|
402
|
+
function isRestartShapedFailure(failure) {
|
|
403
|
+
if (failure.dedicatedGuest)
|
|
404
|
+
return false;
|
|
405
|
+
switch (failure.reason) {
|
|
406
|
+
case 'no_bootstrap':
|
|
407
|
+
case 'bootstrap_parse':
|
|
408
|
+
case 'network':
|
|
409
|
+
return true;
|
|
410
|
+
case 'stale_bootstrap':
|
|
411
|
+
return !failure.code;
|
|
412
|
+
default:
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function candidatePathsFor(opts) {
|
|
417
|
+
const profile = opts.profile ?? 'default';
|
|
418
|
+
return opts.candidatePathsOverride
|
|
348
419
|
? [...opts.candidatePathsOverride]
|
|
349
420
|
: opts.bootstrapPathOverride
|
|
350
421
|
? [opts.bootstrapPathOverride]
|
|
351
422
|
: resolveCliHandoffCandidatePaths(profile);
|
|
423
|
+
}
|
|
424
|
+
/** Live descriptors whose serverUrl does not contradict this CLI's baseURL. */
|
|
425
|
+
function listUsableBootstraps(opts) {
|
|
426
|
+
const out = [];
|
|
427
|
+
for (const candidate of candidatePathsFor(opts)) {
|
|
428
|
+
const bootstrap = readBootstrap(candidate);
|
|
429
|
+
if (!bootstrap)
|
|
430
|
+
continue;
|
|
431
|
+
if (typeof bootstrap.serverUrl === 'string' &&
|
|
432
|
+
bootstrap.serverUrl.length > 0 &&
|
|
433
|
+
typeof opts.baseURL === 'string' &&
|
|
434
|
+
opts.baseURL.length > 0 &&
|
|
435
|
+
normalizeBaseUrl(opts.baseURL) !== normalizeBaseUrl(bootstrap.serverUrl)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
out.push({ bootstrap, sourcePath: candidate });
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
async function attemptDaemonBrokerLoginOnce(opts) {
|
|
443
|
+
const candidates = candidatePathsFor(opts);
|
|
352
444
|
// Bound candidates (their serverUrl matches this CLI) in order, then unbound
|
|
353
445
|
// ones. Every usable candidate is kept, not just the first: see below.
|
|
354
446
|
// `sourcePath` rides along — the brokered-migration cleanup keys on whether
|
|
@@ -357,10 +449,18 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
357
449
|
const unbound = [];
|
|
358
450
|
let mismatchedBootstrap = null;
|
|
359
451
|
let mismatchedPath = null;
|
|
452
|
+
// The first descriptor that EXISTED but could not be used, so a failure can
|
|
453
|
+
// say what was on disk instead of "no daemon" (OSK-13222).
|
|
454
|
+
let unusableDescriptor = null;
|
|
360
455
|
for (const candidate of candidates) {
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
456
|
+
const inspection = inspectBootstrap(candidate);
|
|
457
|
+
if (inspection.state !== 'ok') {
|
|
458
|
+
if (inspection.state !== 'absent' && !unusableDescriptor) {
|
|
459
|
+
unusableDescriptor = { path: candidate, inspection };
|
|
460
|
+
}
|
|
363
461
|
continue;
|
|
462
|
+
}
|
|
463
|
+
const candidateBootstrap = inspection.bootstrap;
|
|
364
464
|
if (typeof candidateBootstrap.serverUrl === 'string' &&
|
|
365
465
|
candidateBootstrap.serverUrl.length > 0 &&
|
|
366
466
|
typeof opts.baseURL === 'string' &&
|
|
@@ -392,6 +492,25 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
392
492
|
...(mismatchedPath ? { descriptorPath: mismatchedPath } : {}),
|
|
393
493
|
};
|
|
394
494
|
}
|
|
495
|
+
if (unusableDescriptor) {
|
|
496
|
+
const { path: descriptorPath, inspection } = unusableDescriptor;
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
reason: 'no_bootstrap',
|
|
500
|
+
detail: inspection.state === 'dead_pid'
|
|
501
|
+
? `The daemon hand-off ${descriptorPath} names pid ${inspection.pid}, which has exited`
|
|
502
|
+
: `The daemon hand-off ${descriptorPath} could not be used: ${inspection.detail}`,
|
|
503
|
+
descriptorPath,
|
|
504
|
+
descriptorState: inspection.state,
|
|
505
|
+
...(inspection.state === 'dead_pid'
|
|
506
|
+
? {
|
|
507
|
+
descriptorPid: inspection.pid,
|
|
508
|
+
descriptorPort: inspection.bootstrap.port,
|
|
509
|
+
...(inspection.bootstrap.daemonId ? { daemonId: inspection.bootstrap.daemonId } : {}),
|
|
510
|
+
}
|
|
511
|
+
: {}),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
395
514
|
return {
|
|
396
515
|
ok: false,
|
|
397
516
|
reason: 'no_bootstrap',
|
|
@@ -413,6 +532,8 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
413
532
|
outcome = {
|
|
414
533
|
...outcome,
|
|
415
534
|
descriptorPath: outcome.descriptorPath ?? sourcePath,
|
|
535
|
+
descriptorPort: bootstrap.port,
|
|
536
|
+
...(typeof bootstrap.pid === 'number' ? { descriptorPid: bootstrap.pid } : {}),
|
|
416
537
|
...(bootstrap.daemonId ? { daemonId: bootstrap.daemonId } : {}),
|
|
417
538
|
...(sourcePath === exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR
|
|
418
539
|
? { dedicatedGuest: true }
|
|
@@ -432,18 +553,41 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
|
|
|
432
553
|
// either the caller prefers broker mode (auto-broker) or the durable mint is
|
|
433
554
|
// not offered at all (the Dedicated guest shape — its hand-off-only secret
|
|
434
555
|
// cannot mint a family, so the token route is the only door).
|
|
556
|
+
//
|
|
557
|
+
// The no-durable-mint policy for the AUTOMATIC path lives here, in the CLI,
|
|
558
|
+
// and not in the daemon (OSK-12018). On a laptop the daemon cannot tell an
|
|
559
|
+
// explicit `skrr login` from an agent-originated implicit sign-in — same
|
|
560
|
+
// uid, same island secret — so a daemon-side refusal of the legacy mint
|
|
561
|
+
// would either break explicit login or be bypassed by anyone who calls it
|
|
562
|
+
// the way login does. It is not a boundary, so it is not attempted. What the
|
|
563
|
+
// CLI CAN guarantee is that its own automatic path never mints and persists
|
|
564
|
+
// a refresh family: when `access_token` is advertised, a failed redeem is
|
|
565
|
+
// the answer, and the daemon's outage is reported with its repair.
|
|
566
|
+
//
|
|
567
|
+
// COMPATIBILITY WINDOW: a v1 descriptor (no `handoffModes`, so no
|
|
568
|
+
// `access_token`) still takes the durable mint on the automatic path below,
|
|
569
|
+
// because that is the only door an older daemon has and refusing it would
|
|
570
|
+
// sign every laptop user out on a CLI upgrade. Remove that fallback for the
|
|
571
|
+
// automatic path once no supported laptop daemon still writes a v1 island
|
|
572
|
+
// descriptor — the v2 island descriptor (`handoffModes` in
|
|
573
|
+
// `daemon/src/local-server.ts`) and its token route shipped together in
|
|
574
|
+
// 1a636216b0, so the condition is "every daemon in use is at or past that
|
|
575
|
+
// release", which is a fleet measurement, not a date.
|
|
435
576
|
const modes = (0, cli_handoff_wire_1.advertisedHandoffModes)(bootstrap);
|
|
436
|
-
const offersAccessToken =
|
|
577
|
+
const offersAccessToken = (0, cli_handoff_wire_1.descriptorOffersAccessToken)(bootstrap);
|
|
437
578
|
const offersRefreshFamily = modes.includes('refresh_family');
|
|
438
579
|
const preferAccessToken = opts.preferHandoffMode === 'access_token';
|
|
439
580
|
if (offersAccessToken && (preferAccessToken || !offersRefreshFamily)) {
|
|
440
581
|
const redeem = await redeemCliHandoffToken(bootstrap, {
|
|
441
582
|
forceRefresh: false,
|
|
442
583
|
timeoutMs: opts.timeoutMs,
|
|
584
|
+
// OSK-12020 — ask for a key-bound token whenever the daemon can serve
|
|
585
|
+
// its proofs. Whether it IS bound is read from the answer's tokenType.
|
|
586
|
+
dpop: (0, cli_handoff_wire_1.descriptorOffersDpop)(bootstrap),
|
|
443
587
|
});
|
|
444
588
|
if (redeem.ok) {
|
|
445
589
|
const response = redeem.body;
|
|
446
|
-
registerBrokeredHandoff(bootstrap, response.accessToken, sourcePath);
|
|
590
|
+
registerBrokeredHandoff(bootstrap, response.accessToken, sourcePath, response.tokenType === 'DPoP');
|
|
447
591
|
if (sourcePath === exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR) {
|
|
448
592
|
// A guest descriptor's secret can ONLY redeem access tokens, so a
|
|
449
593
|
// stored cli-auth.json on this machine predates broker mode. Retire
|
|
@@ -467,20 +611,17 @@ async function brokerThrough(bootstrap, opts, sourcePath) {
|
|
|
467
611
|
...(sourcePath ? { descriptorPath: sourcePath } : {}),
|
|
468
612
|
};
|
|
469
613
|
}
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
// through
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
if (!minted.ok && !minted.code && redeem.code && redeem.reason !== 'network')
|
|
482
|
-
return redeem;
|
|
483
|
-
return minted;
|
|
614
|
+
// A redeem failure is final for the automatic path even when the
|
|
615
|
+
// descriptor ALSO offers the durable mint (the laptop island): falling
|
|
616
|
+
// through would silently mint and persist a refresh family nobody asked
|
|
617
|
+
// for, which is exactly what broker mode exists to stop (OSK-12018). The
|
|
618
|
+
// caller reports the outage (`describeBrokerRefusal`) and names
|
|
619
|
+
// `skrr login` as the way to a durable personal sign-in. When
|
|
620
|
+
// refresh_family is NOT offered (a guest) the legacy route would only
|
|
621
|
+
// answer 403 HANDOFF_BROKER_ONLY anyway. (An explicit `skrr login`
|
|
622
|
+
// against a laptop island never enters this branch: it prefers
|
|
623
|
+
// refresh_family, which is offered, and takes the mint below directly.)
|
|
624
|
+
return redeem;
|
|
484
625
|
}
|
|
485
626
|
return mintRefreshFamilyThrough(bootstrap, opts);
|
|
486
627
|
}
|
|
@@ -508,7 +649,10 @@ async function redeemCliHandoffToken(bootstrap, opts = {}) {
|
|
|
508
649
|
// No cliId — the family is daemon-owned; the caller is not enrolling a
|
|
509
650
|
// device. forceRefresh bypasses the daemon's cached access token (the
|
|
510
651
|
// 401-recovery path).
|
|
511
|
-
body: JSON.stringify({
|
|
652
|
+
body: JSON.stringify({
|
|
653
|
+
forceRefresh: opts.forceRefresh === true,
|
|
654
|
+
...(opts.dpop ? { dpop: true } : {}),
|
|
655
|
+
}),
|
|
512
656
|
signal: controller.signal,
|
|
513
657
|
});
|
|
514
658
|
}
|
|
@@ -766,8 +910,24 @@ async function mintRefreshFamilyThrough(bootstrap, opts) {
|
|
|
766
910
|
* and re-redeems on entry via `maybeAutoBroker`.
|
|
767
911
|
*/
|
|
768
912
|
let _brokeredHandoff = null;
|
|
769
|
-
function registerBrokeredHandoff(descriptor, accessToken, sourcePath) {
|
|
913
|
+
function registerBrokeredHandoff(descriptor, accessToken, sourcePath, dpopBound = false) {
|
|
770
914
|
_brokeredHandoff = { descriptor, accessToken, ...(sourcePath ? { sourcePath } : {}) };
|
|
915
|
+
// OSK-12020 — a token the daemon reported as `tokenType: 'DPoP'` carries
|
|
916
|
+
// `cnf.jkt`; every request it makes needs a fresh proof from the daemon's
|
|
917
|
+
// key (`brokered-dpop.ts`). Never inferred from having asked: an older
|
|
918
|
+
// server ignores the binding request and the daemon then says `Bearer`.
|
|
919
|
+
if (dpopBound) {
|
|
920
|
+
(0, brokered_dpop_1.registerDpopBinding)(accessToken, () => {
|
|
921
|
+
const current = (sourcePath ? readBootstrap(sourcePath) : null) ?? descriptor;
|
|
922
|
+
if (!current.host || !current.port || !current.secret) {
|
|
923
|
+
throw new Error('hand-off descriptor is missing its loopback address');
|
|
924
|
+
}
|
|
925
|
+
return { host: current.host, port: current.port, secret: current.secret };
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
(0, brokered_dpop_1.clearDpopBinding)();
|
|
930
|
+
}
|
|
771
931
|
}
|
|
772
932
|
/**
|
|
773
933
|
* True when `token` is the access token this process redeemed through the
|
|
@@ -790,15 +950,19 @@ async function redeemBrokeredHandoffToken(forceRefresh = true) {
|
|
|
790
950
|
if (!state)
|
|
791
951
|
return null;
|
|
792
952
|
const descriptor = (state.sourcePath ? readBootstrap(state.sourcePath) : null) ?? state.descriptor;
|
|
793
|
-
const result = await redeemCliHandoffToken(descriptor, {
|
|
953
|
+
const result = await redeemCliHandoffToken(descriptor, {
|
|
954
|
+
forceRefresh,
|
|
955
|
+
dpop: (0, cli_handoff_wire_1.descriptorOffersDpop)(descriptor),
|
|
956
|
+
});
|
|
794
957
|
if (!result.ok)
|
|
795
958
|
return null;
|
|
796
|
-
|
|
959
|
+
registerBrokeredHandoff(descriptor, result.body.accessToken, state.sourcePath, result.body.tokenType === 'DPoP');
|
|
797
960
|
return result.body;
|
|
798
961
|
}
|
|
799
962
|
/** @internal test seam — clears the module-scoped brokered credential. */
|
|
800
963
|
function __resetBrokeredHandoffForTest() {
|
|
801
964
|
_brokeredHandoff = null;
|
|
965
|
+
(0, brokered_dpop_1.clearDpopBinding)();
|
|
802
966
|
}
|
|
803
967
|
/**
|
|
804
968
|
* True when a live local descriptor advertises broker-mode access — i.e.
|
|
@@ -908,7 +1072,7 @@ function shouldBrokerBeforeStoredCredential(descriptorPath = exports.DEDICATED_R
|
|
|
908
1072
|
if (!bootstrap)
|
|
909
1073
|
return false;
|
|
910
1074
|
const modes = (0, cli_handoff_wire_1.advertisedHandoffModes)(bootstrap);
|
|
911
|
-
if (!
|
|
1075
|
+
if (!(0, cli_handoff_wire_1.descriptorOffersAccessToken)(bootstrap) || modes.includes('refresh_family'))
|
|
912
1076
|
return false;
|
|
913
1077
|
return retirableCliAuthFamily(bootstrap.serverUrl) !== null;
|
|
914
1078
|
}
|
|
@@ -1052,7 +1216,10 @@ const AUTH_SELF_MANAGED_COMMANDS = new Set([
|
|
|
1052
1216
|
* 'access_token'`. On a descriptor that advertises broker mode the
|
|
1053
1217
|
* outcome is `brokered` — an in-memory access token, nothing written;
|
|
1054
1218
|
* the caller holds it for this process and the next `skrr` re-redeems.
|
|
1055
|
-
*
|
|
1219
|
+
* A failed redeem on such a descriptor is returned as the outcome and
|
|
1220
|
+
* never retried as a durable mint (OSK-12018). Only on a legacy/v1
|
|
1221
|
+
* descriptor (no `access_token` advertised — the compatibility window
|
|
1222
|
+
* documented in `brokerThrough`) is the durable mint persisted to the
|
|
1056
1223
|
* keychain / cli-auth.json exactly as before.
|
|
1057
1224
|
* - May generate a new `cliId` if the config doesn't have one. The
|
|
1058
1225
|
* caller MUST persist `updatedConfig` so the next invocation
|
|
@@ -32,6 +32,12 @@ export interface BrokerRefusalDescription {
|
|
|
32
32
|
transient: boolean;
|
|
33
33
|
/** True when the refusing daemon is a Dedicated Runtime guest's. */
|
|
34
34
|
dedicatedGuest: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* True when no daemon answered because the local service is down or
|
|
37
|
+
* restarting (OSK-13222). The person is not signed out — their sign-in
|
|
38
|
+
* lives in that service — so the message must not open with "Not signed in".
|
|
39
|
+
*/
|
|
40
|
+
daemonUnavailable?: boolean;
|
|
35
41
|
/** Machine-readable twin for `--json` error details. */
|
|
36
42
|
broker: {
|
|
37
43
|
reason: DaemonBrokerFailure['reason'];
|
|
@@ -41,6 +47,10 @@ export interface BrokerRefusalDescription {
|
|
|
41
47
|
detail?: string;
|
|
42
48
|
descriptorPath?: string;
|
|
43
49
|
daemonId?: string;
|
|
50
|
+
descriptorState?: DaemonBrokerFailure['descriptorState'];
|
|
51
|
+
descriptorPid?: number;
|
|
52
|
+
descriptorPort?: number;
|
|
53
|
+
restartWaitedMs?: number;
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
@@ -70,3 +80,19 @@ export declare function describeGuestDaemonUnavailable(failure: DaemonBrokerFail
|
|
|
70
80
|
onGuest: boolean;
|
|
71
81
|
descriptorPresent?: boolean;
|
|
72
82
|
}): BrokerRefusalDescription | null;
|
|
83
|
+
/**
|
|
84
|
+
* What to tell a person on their own computer when NO daemon answered the
|
|
85
|
+
* hand-off, but something on disk says one was there (OSK-13222).
|
|
86
|
+
*
|
|
87
|
+
* The laptop twin of `describeGuestDaemonUnavailable`. A daemon that crashed
|
|
88
|
+
* leaves its descriptor naming a dead pid; one mid-restart has a descriptor
|
|
89
|
+
* whose port refuses, or a secret it already rotated; or the CLI waited for a
|
|
90
|
+
* restart that did not finish. Each of these used to read "Not signed in. Run
|
|
91
|
+
* `skrr login` first." — a claim about the person's account made on evidence
|
|
92
|
+
* about a local process. Their sign-in is held by that service; say the
|
|
93
|
+
* service is down and name what was read.
|
|
94
|
+
*
|
|
95
|
+
* Returns null when there is no evidence of a daemon at all (no descriptor,
|
|
96
|
+
* nothing waited for): "Not signed in" is then the true sentence.
|
|
97
|
+
*/
|
|
98
|
+
export declare function describeLocalDaemonUnavailable(failure: DaemonBrokerFailure, bin?: string): BrokerRefusalDescription | null;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isAnsweredBrokerRefusal = isAnsweredBrokerRefusal;
|
|
4
4
|
exports.describeBrokerRefusal = describeBrokerRefusal;
|
|
5
5
|
exports.describeGuestDaemonUnavailable = describeGuestDaemonUnavailable;
|
|
6
|
+
exports.describeLocalDaemonUnavailable = describeLocalDaemonUnavailable;
|
|
6
7
|
/**
|
|
7
8
|
* True when a daemon ANSWERED the hand-off and refused it — as opposed to no
|
|
8
9
|
* daemon being there to ask (`no_bootstrap`, a stale descriptor nobody listens
|
|
@@ -97,18 +98,18 @@ function describeBrokerRefusal(failure, bin = 'skrr') {
|
|
|
97
98
|
}
|
|
98
99
|
else if (failure.reason === 'ci_token_refused') {
|
|
99
100
|
remedy =
|
|
100
|
-
`The background service is signed in with a CI token, which cannot sign a person in. Use \`${bin} login
|
|
101
|
-
`
|
|
101
|
+
`The background service is signed in with a CI token, which cannot sign a person in. Use \`${bin} login\` ` +
|
|
102
|
+
`(\`--device-code\` without a terminal). A \`${bin} create-token\` token would not help: ${bin} commands reject it.`;
|
|
102
103
|
}
|
|
103
104
|
else if (transient) {
|
|
104
105
|
remedy =
|
|
105
106
|
`This is usually temporary; retry in a minute. \`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side, ` +
|
|
106
|
-
`and \`${bin} login\`
|
|
107
|
+
`and \`${bin} login\` gives this CLI a durable sign-in of its own, without the service.`;
|
|
107
108
|
}
|
|
108
109
|
else {
|
|
109
110
|
remedy =
|
|
110
111
|
`\`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side. ` +
|
|
111
|
-
`\`${bin} login\`
|
|
112
|
+
`\`${bin} login\` gives this CLI a durable sign-in of its own, without the service.`;
|
|
112
113
|
}
|
|
113
114
|
return {
|
|
114
115
|
headline: refusal,
|
|
@@ -210,3 +211,81 @@ function describeGuestDaemonUnavailable(failure, opts) {
|
|
|
210
211
|
},
|
|
211
212
|
};
|
|
212
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* What to tell a person on their own computer when NO daemon answered the
|
|
216
|
+
* hand-off, but something on disk says one was there (OSK-13222).
|
|
217
|
+
*
|
|
218
|
+
* The laptop twin of `describeGuestDaemonUnavailable`. A daemon that crashed
|
|
219
|
+
* leaves its descriptor naming a dead pid; one mid-restart has a descriptor
|
|
220
|
+
* whose port refuses, or a secret it already rotated; or the CLI waited for a
|
|
221
|
+
* restart that did not finish. Each of these used to read "Not signed in. Run
|
|
222
|
+
* `skrr login` first." — a claim about the person's account made on evidence
|
|
223
|
+
* about a local process. Their sign-in is held by that service; say the
|
|
224
|
+
* service is down and name what was read.
|
|
225
|
+
*
|
|
226
|
+
* Returns null when there is no evidence of a daemon at all (no descriptor,
|
|
227
|
+
* nothing waited for): "Not signed in" is then the true sentence.
|
|
228
|
+
*/
|
|
229
|
+
function describeLocalDaemonUnavailable(failure, bin = 'skrr') {
|
|
230
|
+
if (failure.dedicatedGuest)
|
|
231
|
+
return null;
|
|
232
|
+
if (isAnsweredBrokerRefusal(failure))
|
|
233
|
+
return null;
|
|
234
|
+
const who = `The skrr background service on this computer${failure.daemonId ? ` (daemon ${failure.daemonId})` : ''}`;
|
|
235
|
+
const where = failure.descriptorPath ? ` (${failure.descriptorPath})` : '';
|
|
236
|
+
const waited = typeof failure.restartWaitedMs === 'number' && failure.restartWaitedMs > 0
|
|
237
|
+
? ` Waited ${Math.max(1, Math.round(failure.restartWaitedMs / 1000))}s for it to restart.`
|
|
238
|
+
: '';
|
|
239
|
+
let headline = null;
|
|
240
|
+
if (failure.reason === 'network' && failure.descriptorPath) {
|
|
241
|
+
headline =
|
|
242
|
+
`${who} is not answering: its sign-in hand-off${where} names port ` +
|
|
243
|
+
`${failure.descriptorPort ?? '?'} on 127.0.0.1, where nothing is listening.`;
|
|
244
|
+
}
|
|
245
|
+
else if (failure.reason === 'stale_bootstrap') {
|
|
246
|
+
headline = `${who} refused this CLI's hand-off secret${where}, which it rotates when it restarts.`;
|
|
247
|
+
}
|
|
248
|
+
else if (failure.reason === 'bootstrap_parse' ||
|
|
249
|
+
failure.descriptorState === 'unreadable' ||
|
|
250
|
+
failure.descriptorState === 'invalid') {
|
|
251
|
+
headline = `${who} is not available: its sign-in hand-off${where} could not be read${failure.detail ? ` (${failure.detail.replace(/[.]$/, '')})` : ''}.`;
|
|
252
|
+
}
|
|
253
|
+
else if (failure.descriptorState === 'dead_pid') {
|
|
254
|
+
headline =
|
|
255
|
+
`${who} is not running: its sign-in hand-off${where} names pid ` +
|
|
256
|
+
`${failure.descriptorPid ?? '?'}, which has exited (a crash, or a restart in progress).`;
|
|
257
|
+
}
|
|
258
|
+
else if (failure.reason === 'no_bootstrap' && waited) {
|
|
259
|
+
headline = `${who} was restarting and did not publish its sign-in hand-off in time.`;
|
|
260
|
+
}
|
|
261
|
+
if (!headline)
|
|
262
|
+
return null;
|
|
263
|
+
const remedy = 'You are not signed out: your sign-in is held by that service, which normally restarts on its own. ' +
|
|
264
|
+
`Retry in a moment. \`${bin} daemon status\` and \`${bin} daemon logs\` show the service's side, ` +
|
|
265
|
+
`and \`${bin} login\` signs this CLI in without it.`;
|
|
266
|
+
return {
|
|
267
|
+
headline: `${headline}${waited}`,
|
|
268
|
+
remedy,
|
|
269
|
+
transient: true,
|
|
270
|
+
dedicatedGuest: false,
|
|
271
|
+
daemonUnavailable: true,
|
|
272
|
+
broker: {
|
|
273
|
+
reason: failure.reason,
|
|
274
|
+
...(typeof failure.status === 'number' ? { status: failure.status } : {}),
|
|
275
|
+
...(failure.code ? { code: failure.code } : {}),
|
|
276
|
+
...(failure.detail ? { detail: failure.detail } : {}),
|
|
277
|
+
...(failure.descriptorPath ? { descriptorPath: failure.descriptorPath } : {}),
|
|
278
|
+
...(failure.daemonId ? { daemonId: failure.daemonId } : {}),
|
|
279
|
+
...(failure.descriptorState ? { descriptorState: failure.descriptorState } : {}),
|
|
280
|
+
...(typeof failure.descriptorPid === 'number'
|
|
281
|
+
? { descriptorPid: failure.descriptorPid }
|
|
282
|
+
: {}),
|
|
283
|
+
...(typeof failure.descriptorPort === 'number'
|
|
284
|
+
? { descriptorPort: failure.descriptorPort }
|
|
285
|
+
: {}),
|
|
286
|
+
...(typeof failure.restartWaitedMs === 'number'
|
|
287
|
+
? { restartWaitedMs: failure.restartWaitedMs }
|
|
288
|
+
: {}),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type DaemonRestartSignal = 'starting' | 'supervised' | 'none';
|
|
2
|
+
/** A lock acquired within this window belongs to a daemon still starting up. */
|
|
3
|
+
export declare const STARTING_WINDOW_MS = 60000;
|
|
4
|
+
/** Total wait once a starting daemon is seen. Startup was measured at ~14s. */
|
|
5
|
+
export declare const DEFAULT_RESTART_WAIT_MS = 20000;
|
|
6
|
+
/** Wait for a supervisor to start a daemon when none holds the lock yet. */
|
|
7
|
+
export declare const SUPERVISED_GRACE_MS = 4000;
|
|
8
|
+
export declare const DEFAULT_POLL_MS = 250;
|
|
9
|
+
/**
|
|
10
|
+
* Read the restart signal for `profile` from the daemon's own on-disk state.
|
|
11
|
+
* Never throws; anything unreadable reads as absent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function probeDaemonRestart(profile: string, opts?: {
|
|
14
|
+
homeDir?: string;
|
|
15
|
+
now?: number;
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
17
|
+
}): DaemonRestartSignal;
|
|
18
|
+
export interface AwaitRestartedDaemonOptions {
|
|
19
|
+
profile: string;
|
|
20
|
+
/** True once a fresh descriptor the caller has not already failed on exists. */
|
|
21
|
+
ready: () => boolean;
|
|
22
|
+
/** Total budget once a starting daemon is seen. `SKRR_DAEMON_RESTART_WAIT_MS` overrides; 0 disables. */
|
|
23
|
+
maxWaitMs?: number;
|
|
24
|
+
supervisedGraceMs?: number;
|
|
25
|
+
pollMs?: number;
|
|
26
|
+
probe?: (profile: string) => DaemonRestartSignal;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
sleep?: (ms: number) => Promise<void>;
|
|
29
|
+
/** Told once when the wait begins. Defaults to one stderr line. */
|
|
30
|
+
notify?: (message: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface AwaitRestartedDaemonResult {
|
|
33
|
+
/** True when a restart was plausible and the CLI waited for it. */
|
|
34
|
+
waited: boolean;
|
|
35
|
+
waitedMs: number;
|
|
36
|
+
/** True when a fresh descriptor appeared inside the budget. */
|
|
37
|
+
ready: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function awaitRestartedDaemon(opts: AwaitRestartedDaemonOptions): Promise<AwaitRestartedDaemonResult>;
|