@skrr-ai/cli 0.1.49 → 0.1.51
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/README.md +5 -4
- package/dist/base-command.js +75 -6
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +9 -0
- package/dist/commands/agents/update.js +38 -5
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +49 -5
- package/dist/commands/code/run.js +5 -1
- package/dist/commands/commitments/cycles.d.ts +19 -0
- package/dist/commands/commitments/cycles.js +46 -0
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +40 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/commitments/next-tick.js +3 -1
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +38 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- package/dist/commands/logout.js +12 -1
- package/dist/commands/machines/dedicated/index.js +1 -1
- package/dist/commands/machines/dedicated/sign-in.js +4 -1
- package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
- package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
- package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
- package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
- package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
- package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
- package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
- package/dist/commands/machines/dedicated/terminal.js +27 -2
- package/dist/commands/machines/hosted/connect.js +2 -2
- package/dist/commands/machines/hosted/destroy.js +1 -1
- package/dist/commands/machines/hosted/exec.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/pause.js +1 -1
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/resume.js +1 -1
- package/dist/commands/machines/hosted/start.js +2 -2
- package/dist/commands/machines/hosted/status.js +2 -1
- package/dist/commands/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +1 -0
- package/dist/commands/tasks/create.js +6 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +22 -0
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/agentic-stream.d.ts +68 -1
- package/dist/lib/agentic-stream.js +294 -7
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +65 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +37 -2
- package/dist/lib/commitments.js +148 -0
- package/dist/lib/daemonBroker.d.ts +134 -31
- package/dist/lib/daemonBroker.js +376 -31
- package/dist/lib/dedicated-lease-command.d.ts +14 -0
- package/dist/lib/dedicated-lease-command.js +30 -1
- package/dist/lib/dedicated-machines.js +8 -25
- package/dist/lib/dedicated-service-command.d.ts +11 -3
- package/dist/lib/dedicated-service-command.js +20 -3
- package/dist/lib/dedicated-service.d.ts +46 -0
- package/dist/lib/dedicated-service.js +85 -7
- package/dist/lib/dedicated-terminal.d.ts +21 -0
- package/dist/lib/dedicated-terminal.js +104 -9
- package/dist/lib/delegated-cli.js +4 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +13 -1
- package/dist/lib/first-party-harness-doctor.js +41 -1
- package/dist/lib/first-party-harness-managed.d.ts +4 -3
- package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
- package/dist/lib/first-party-harness-project-trust.js +364 -0
- package/dist/lib/first-party-harness.d.ts +9 -2
- package/dist/lib/first-party-harness.js +6 -5
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +60 -10
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/node-adapter.js +15 -3
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +54 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
- package/oclif.manifest.json +24813 -24242
- package/package.json +4 -1
package/dist/lib/node-adapter.js
CHANGED
|
@@ -271,10 +271,22 @@ function createNodeAdapter(opts = {}) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
// Refresh the in-process override so the retry on the SAME request
|
|
274
|
-
// sees the new token. writeToBackend (called inside
|
|
275
|
-
// already wrote to the keychain/file — but the adapter
|
|
276
|
-
// credential and only re-reads when explicitly told to.
|
|
274
|
+
// sees the new token. For a durable mint, writeToBackend (called inside
|
|
275
|
+
// the broker) already wrote to the keychain/file — but the adapter
|
|
276
|
+
// caches the credential and only re-reads when explicitly told to.
|
|
277
277
|
const accessToken = outcome.outcome.accessToken;
|
|
278
|
+
if (outcome.outcome.brokered) {
|
|
279
|
+
// Broker-mode hand-off: an in-memory access token, nothing persisted.
|
|
280
|
+
// 'env-token' keeps `ensureFreshCliCredential` from trying to rotate a
|
|
281
|
+
// stored family that does not exist; renewal re-redeems the descriptor.
|
|
282
|
+
setAdapterCredentialOverride({
|
|
283
|
+
token: accessToken,
|
|
284
|
+
source: 'env-token',
|
|
285
|
+
kind: (0, auth_core_1.classifyTokenKind)(accessToken),
|
|
286
|
+
});
|
|
287
|
+
onTokenRefresh?.(accessToken);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
278
290
|
setAdapterCredentialOverride({
|
|
279
291
|
token: accessToken,
|
|
280
292
|
source: outcome.outcome.storedIn === 'keychain' ? 'keychain' : 'file',
|
package/dist/lib/tasks.d.ts
CHANGED
|
@@ -8,6 +8,25 @@ import type { ParsedDeliverableCoordinate, ParsedExpectationSpec } from '@skrr-a
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function resolveTaskGoalFlag(value: string | undefined): string | null | undefined;
|
|
10
10
|
export declare const TASK_GOAL_NOT_FOUND = "TASK_GOAL_NOT_FOUND";
|
|
11
|
+
/**
|
|
12
|
+
* `--working-directory` on `tasks create` / `update` / `upsert`. `none`,
|
|
13
|
+
* `null` and `clear` mean "no per-task override": on update the PATCH sends
|
|
14
|
+
* `workingDirectory: null` ($unset → inherit the agent default), on create it
|
|
15
|
+
* stores as absent. The sentinel set mirrors `resolveTaskGoalFlag` — it is the
|
|
16
|
+
* spelling a user who knows `--goal none` reaches for.
|
|
17
|
+
* Undefined means the flag was not passed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function workingDirectoryFromFlag(value: string | undefined): string | null | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Validate a RESOLVED `workingDirectory` payload value (post-merge, so flag
|
|
22
|
+
* and `--from-json` inputs are checked identically). `null`/`undefined` are
|
|
23
|
+
* always fine — they mean "no override". A string must be absolute: the path
|
|
24
|
+
* is resolved on the daemon's filesystem, so a relative value can neither be
|
|
25
|
+
* verified here nor trusted there — it would fall back, silently, at run
|
|
26
|
+
* time. Failing here is the same contract as every other bad spec in this
|
|
27
|
+
* file: refuse before the write, never store a value that cannot take effect.
|
|
28
|
+
*/
|
|
29
|
+
export declare function workingDirectoryValidationError(value: unknown): string | null;
|
|
11
30
|
/**
|
|
12
31
|
* The refusal for a goal id that names no goal, in CLI terms.
|
|
13
32
|
*
|
|
@@ -91,6 +110,7 @@ export type TaskCreateFlagValues = {
|
|
|
91
110
|
estimate?: number;
|
|
92
111
|
tag?: string[];
|
|
93
112
|
'label-id'?: string[];
|
|
113
|
+
'working-directory'?: string;
|
|
94
114
|
worktree?: boolean;
|
|
95
115
|
'lifecycle-definition'?: string;
|
|
96
116
|
'lifecycle-version'?: string;
|
package/dist/lib/tasks.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RECOGNIZED_TASK_VERDICTS = exports.DELIVERABLE_LIFECYCLE_VALUES = exports.EXPECT_FLAG_HELP = exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = exports.TASK_GOAL_NOT_FOUND = void 0;
|
|
4
4
|
exports.resolveTaskGoalFlag = resolveTaskGoalFlag;
|
|
5
|
+
exports.workingDirectoryFromFlag = workingDirectoryFromFlag;
|
|
6
|
+
exports.workingDirectoryValidationError = workingDirectoryValidationError;
|
|
5
7
|
exports.describeTaskGoalNotFound = describeTaskGoalNotFound;
|
|
6
8
|
exports.releasePinRepairGuard = releasePinRepairGuard;
|
|
7
9
|
exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
|
|
@@ -38,6 +40,7 @@ exports.verdictIsRecognized = verdictIsRecognized;
|
|
|
38
40
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
39
41
|
const prompt_1 = require("./prompt");
|
|
40
42
|
const projects_1 = require("./projects");
|
|
43
|
+
const node_path_1 = require("node:path");
|
|
41
44
|
/**
|
|
42
45
|
* `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
|
|
43
46
|
* sends `goalId: null`. Anything else names a goal. `clear` is accepted because
|
|
@@ -54,6 +57,47 @@ function resolveTaskGoalFlag(value) {
|
|
|
54
57
|
return v;
|
|
55
58
|
}
|
|
56
59
|
exports.TASK_GOAL_NOT_FOUND = 'TASK_GOAL_NOT_FOUND';
|
|
60
|
+
/**
|
|
61
|
+
* `--working-directory` on `tasks create` / `update` / `upsert`. `none`,
|
|
62
|
+
* `null` and `clear` mean "no per-task override": on update the PATCH sends
|
|
63
|
+
* `workingDirectory: null` ($unset → inherit the agent default), on create it
|
|
64
|
+
* stores as absent. The sentinel set mirrors `resolveTaskGoalFlag` — it is the
|
|
65
|
+
* spelling a user who knows `--goal none` reaches for.
|
|
66
|
+
* Undefined means the flag was not passed.
|
|
67
|
+
*/
|
|
68
|
+
function workingDirectoryFromFlag(value) {
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const v = value.trim();
|
|
72
|
+
if (!v || ['none', 'null', 'clear'].includes(v.toLowerCase()))
|
|
73
|
+
return null;
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate a RESOLVED `workingDirectory` payload value (post-merge, so flag
|
|
78
|
+
* and `--from-json` inputs are checked identically). `null`/`undefined` are
|
|
79
|
+
* always fine — they mean "no override". A string must be absolute: the path
|
|
80
|
+
* is resolved on the daemon's filesystem, so a relative value can neither be
|
|
81
|
+
* verified here nor trusted there — it would fall back, silently, at run
|
|
82
|
+
* time. Failing here is the same contract as every other bad spec in this
|
|
83
|
+
* file: refuse before the write, never store a value that cannot take effect.
|
|
84
|
+
*/
|
|
85
|
+
function workingDirectoryValidationError(value) {
|
|
86
|
+
if (value === undefined || value === null)
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
89
|
+
return 'workingDirectory must be an absolute path string (or null for no override)';
|
|
90
|
+
}
|
|
91
|
+
if (value.length > 1024) {
|
|
92
|
+
return 'workingDirectory must be at most 1024 characters';
|
|
93
|
+
}
|
|
94
|
+
if (!(0, node_path_1.isAbsolute)(value.trim())) {
|
|
95
|
+
return (`workingDirectory must be an absolute path on the daemon's filesystem ` +
|
|
96
|
+
`(got '${value}'). ` +
|
|
97
|
+
'A relative path cannot be resolved there — pass the full path, or "none" to clear.');
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
57
101
|
/**
|
|
58
102
|
* The refusal for a goal id that names no goal, in CLI terms.
|
|
59
103
|
*
|
|
@@ -331,6 +375,8 @@ function buildTaskCreatePayload(body, flags = {}) {
|
|
|
331
375
|
// is a Task→Task edge rather than a pointer at a chat message.
|
|
332
376
|
spawnedByTaskId: flags['spawned-by'],
|
|
333
377
|
spawnReason: flags['spawn-reason'],
|
|
378
|
+
// Per-task working-directory override; 'none' ⇒ null (no override).
|
|
379
|
+
workingDirectory: workingDirectoryFromFlag(flags['working-directory']),
|
|
334
380
|
// Worktree isolation opt-in. --worktree ⇒ 'ephemeral', --no-worktree ⇒
|
|
335
381
|
// 'shared', omitted ⇒ undefined (inherit space policy / global default).
|
|
336
382
|
executionIsolation: executionIsolationFromWorktreeFlag(flags.worktree),
|
|
@@ -409,6 +455,9 @@ function validateTaskCreatePayload(payload) {
|
|
|
409
455
|
if (payload.templateId !== undefined && !isBoundedString(payload.templateId, 1, 100)) {
|
|
410
456
|
errors.push('templateId must be a non-empty string at most 100 characters');
|
|
411
457
|
}
|
|
458
|
+
const workingDirectoryError = workingDirectoryValidationError(payload.workingDirectory);
|
|
459
|
+
if (workingDirectoryError)
|
|
460
|
+
errors.push(workingDirectoryError);
|
|
412
461
|
if (payload.lifecycle !== undefined) {
|
|
413
462
|
const binding = payload.lifecycle;
|
|
414
463
|
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
@@ -756,6 +805,11 @@ function createPatchFromCreatePayload(payload) {
|
|
|
756
805
|
'executionMode',
|
|
757
806
|
'tags',
|
|
758
807
|
'labelIds',
|
|
808
|
+
// Per-task working-directory override — part of the same execution shape
|
|
809
|
+
// the create payload carries, and equally meaningful on the update leg of
|
|
810
|
+
// an upsert. Absent here, `upsert --working-directory` silently no-ops on
|
|
811
|
+
// an existing task.
|
|
812
|
+
'workingDirectory',
|
|
759
813
|
];
|
|
760
814
|
for (const field of copyFields) {
|
|
761
815
|
if (payload[field] !== undefined)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cliHandoffWire.ts — the ONE declaration of the CLI↔daemon hand-off wire
|
|
3
|
+
* contracts, shared by the daemon (writer/server) and the CLI (reader).
|
|
4
|
+
*
|
|
5
|
+
* Two contracts live here:
|
|
6
|
+
*
|
|
7
|
+
* 1. The hand-off descriptor file (`descriptor.json` on Dedicated guests,
|
|
8
|
+
* `daemon-island.<profile>.json` on laptops). Until now the shape was
|
|
9
|
+
* declared twice — `DedicatedCliHandoffDescriptor` daemon-side and
|
|
10
|
+
* `LocalBootstrap` CLI-side — two type declarations for one file format,
|
|
11
|
+
* the same drift disease `sessionPermissionAuthority` fixed.
|
|
12
|
+
* 2. The broker-mode redeem exchange: `POST /v1/auth/cli-handoff-token`
|
|
13
|
+
* request/response plus its error codes.
|
|
14
|
+
*
|
|
15
|
+
* Readers stay loose (unknown fields ignored, absent `handoffModes` reads as
|
|
16
|
+
* refresh-family-only) so a new field never breaks an older peer; writers
|
|
17
|
+
* must produce the full shape.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* What a caller may redeem the descriptor secret for.
|
|
21
|
+
* - `refresh_family`: the legacy durable mint (90-day cli-scope family).
|
|
22
|
+
* - `access_token`: broker mode — a short-lived access token from a
|
|
23
|
+
* daemon-owned family; nothing durable leaves the daemon.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token"];
|
|
26
|
+
export type CliHandoffMode = (typeof CLI_HANDOFF_MODES)[number];
|
|
27
|
+
/** Broker-mode redeem route on the daemon's loopback server. */
|
|
28
|
+
export declare const CLI_HANDOFF_TOKEN_PATH = "/v1/auth/cli-handoff-token";
|
|
29
|
+
/** Error codes this contract emits. String literals so both ends compare
|
|
30
|
+
* without importing a symbol they may not have. */
|
|
31
|
+
export declare const CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = "HANDOFF_TOKEN_UNAVAILABLE";
|
|
32
|
+
/** Emitted by the LEGACY /v1/auth/cli-handoff route when a hand-off-secret
|
|
33
|
+
* caller asks for a refresh family the daemon no longer mints for it. */
|
|
34
|
+
export declare const CLI_HANDOFF_ERROR_BROKER_ONLY = "HANDOFF_BROKER_ONLY";
|
|
35
|
+
/**
|
|
36
|
+
* The descriptor file format. `version` is absent on the oldest island files
|
|
37
|
+
* and `1` on the first dedicated-guest descriptors; `handoffModes` first
|
|
38
|
+
* appears at version 2. Every field the CLI needs to dial is validated; the
|
|
39
|
+
* rest ride along untyped.
|
|
40
|
+
*/
|
|
41
|
+
export interface CliHandoffDescriptor {
|
|
42
|
+
version?: number;
|
|
43
|
+
pid?: number;
|
|
44
|
+
daemonId?: string;
|
|
45
|
+
host?: string;
|
|
46
|
+
port?: number;
|
|
47
|
+
/** Base64 handshake secret — hand-off-only on guests, island on laptops. */
|
|
48
|
+
secret?: string;
|
|
49
|
+
/** Server URL the daemon is authenticated against. Older daemons omit it;
|
|
50
|
+
* readers refuse to broker without it (never mint against an unknown
|
|
51
|
+
* server). */
|
|
52
|
+
serverUrl?: string;
|
|
53
|
+
/** Daemon-authored epoch counter; bumped when the descriptor is republished. */
|
|
54
|
+
epoch?: number;
|
|
55
|
+
handoffModes?: CliHandoffMode[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse an untrusted descriptor JSON value. Returns null when the fields the
|
|
59
|
+
* caller cannot proceed without — host, port, secret — are absent or
|
|
60
|
+
* mistyped. Unknown fields and future `version` values are tolerated: the
|
|
61
|
+
* reader ignores them, which is what makes additive evolution safe.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseCliHandoffDescriptor(value: unknown): CliHandoffDescriptor | null;
|
|
64
|
+
/**
|
|
65
|
+
* Untrusted `handoffModes` input → known literals only, deduped. An
|
|
66
|
+
* unrecognized future mode is dropped rather than widening by coercion.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeHandoffModes(value: unknown): CliHandoffMode[];
|
|
69
|
+
/**
|
|
70
|
+
* The versioned-default rule: a descriptor that never declares modes offers
|
|
71
|
+
* only the durable mint it has always offered. Mode negotiation is purely
|
|
72
|
+
* additive — a daemon advertises the new mode, it never un-advertises the
|
|
73
|
+
* old one by accident.
|
|
74
|
+
*/
|
|
75
|
+
export declare function advertisedHandoffModes(d: CliHandoffDescriptor): CliHandoffMode[];
|
|
76
|
+
export declare function descriptorOffersAccessToken(d: CliHandoffDescriptor): boolean;
|
|
77
|
+
export interface CliHandoffTokenRequest {
|
|
78
|
+
/** Bypass the daemon broker's cached token (the 401-recovery path). */
|
|
79
|
+
forceRefresh?: boolean;
|
|
80
|
+
}
|
|
81
|
+
export interface CliHandoffTokenResponse {
|
|
82
|
+
accessToken: string;
|
|
83
|
+
/** Absolute expiry in ms since epoch, when the daemon knows it. */
|
|
84
|
+
accessExpiresAt?: number;
|
|
85
|
+
/** Server the minted credential belongs to — lets the caller verify it is
|
|
86
|
+
* about to use a token for the API it thinks it is talking to. */
|
|
87
|
+
serverUrl?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Parse the redeem response; null when the token itself is absent. */
|
|
90
|
+
export declare function parseCliHandoffTokenResponse(value: unknown): CliHandoffTokenResponse | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cliHandoffWire.ts — the ONE declaration of the CLI↔daemon hand-off wire
|
|
4
|
+
* contracts, shared by the daemon (writer/server) and the CLI (reader).
|
|
5
|
+
*
|
|
6
|
+
* Two contracts live here:
|
|
7
|
+
*
|
|
8
|
+
* 1. The hand-off descriptor file (`descriptor.json` on Dedicated guests,
|
|
9
|
+
* `daemon-island.<profile>.json` on laptops). Until now the shape was
|
|
10
|
+
* declared twice — `DedicatedCliHandoffDescriptor` daemon-side and
|
|
11
|
+
* `LocalBootstrap` CLI-side — two type declarations for one file format,
|
|
12
|
+
* the same drift disease `sessionPermissionAuthority` fixed.
|
|
13
|
+
* 2. The broker-mode redeem exchange: `POST /v1/auth/cli-handoff-token`
|
|
14
|
+
* request/response plus its error codes.
|
|
15
|
+
*
|
|
16
|
+
* Readers stay loose (unknown fields ignored, absent `handoffModes` reads as
|
|
17
|
+
* refresh-family-only) so a new field never breaks an older peer; writers
|
|
18
|
+
* must produce the full shape.
|
|
19
|
+
*/
|
|
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;
|
|
22
|
+
exports.parseCliHandoffDescriptor = parseCliHandoffDescriptor;
|
|
23
|
+
exports.normalizeHandoffModes = normalizeHandoffModes;
|
|
24
|
+
exports.advertisedHandoffModes = advertisedHandoffModes;
|
|
25
|
+
exports.descriptorOffersAccessToken = descriptorOffersAccessToken;
|
|
26
|
+
exports.parseCliHandoffTokenResponse = parseCliHandoffTokenResponse;
|
|
27
|
+
/**
|
|
28
|
+
* What a caller may redeem the descriptor secret for.
|
|
29
|
+
* - `refresh_family`: the legacy durable mint (90-day cli-scope family).
|
|
30
|
+
* - `access_token`: broker mode — a short-lived access token from a
|
|
31
|
+
* daemon-owned family; nothing durable leaves the daemon.
|
|
32
|
+
*/
|
|
33
|
+
exports.CLI_HANDOFF_MODES = ['refresh_family', 'access_token'];
|
|
34
|
+
/** Broker-mode redeem route on the daemon's loopback server. */
|
|
35
|
+
exports.CLI_HANDOFF_TOKEN_PATH = '/v1/auth/cli-handoff-token';
|
|
36
|
+
/** Error codes this contract emits. String literals so both ends compare
|
|
37
|
+
* without importing a symbol they may not have. */
|
|
38
|
+
exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = 'HANDOFF_TOKEN_UNAVAILABLE';
|
|
39
|
+
/** Emitted by the LEGACY /v1/auth/cli-handoff route when a hand-off-secret
|
|
40
|
+
* caller asks for a refresh family the daemon no longer mints for it. */
|
|
41
|
+
exports.CLI_HANDOFF_ERROR_BROKER_ONLY = 'HANDOFF_BROKER_ONLY';
|
|
42
|
+
/**
|
|
43
|
+
* Parse an untrusted descriptor JSON value. Returns null when the fields the
|
|
44
|
+
* caller cannot proceed without — host, port, secret — are absent or
|
|
45
|
+
* mistyped. Unknown fields and future `version` values are tolerated: the
|
|
46
|
+
* reader ignores them, which is what makes additive evolution safe.
|
|
47
|
+
*/
|
|
48
|
+
function parseCliHandoffDescriptor(value) {
|
|
49
|
+
if (value == null || typeof value !== 'object')
|
|
50
|
+
return null;
|
|
51
|
+
const v = value;
|
|
52
|
+
if (typeof v.host !== 'string' || v.host.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
if (typeof v.port !== 'number' || !Number.isInteger(v.port) || v.port <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
if (typeof v.secret !== 'string' || v.secret.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
return {
|
|
59
|
+
...(typeof v.version === 'number' ? { version: v.version } : {}),
|
|
60
|
+
...(typeof v.pid === 'number' ? { pid: v.pid } : {}),
|
|
61
|
+
...(typeof v.daemonId === 'string' ? { daemonId: v.daemonId } : {}),
|
|
62
|
+
host: v.host,
|
|
63
|
+
port: v.port,
|
|
64
|
+
secret: v.secret,
|
|
65
|
+
...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
|
|
66
|
+
...(typeof v.epoch === 'number' ? { epoch: v.epoch } : {}),
|
|
67
|
+
...(Array.isArray(v.handoffModes)
|
|
68
|
+
? { handoffModes: normalizeHandoffModes(v.handoffModes) }
|
|
69
|
+
: {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Untrusted `handoffModes` input → known literals only, deduped. An
|
|
74
|
+
* unrecognized future mode is dropped rather than widening by coercion.
|
|
75
|
+
*/
|
|
76
|
+
function normalizeHandoffModes(value) {
|
|
77
|
+
if (!Array.isArray(value))
|
|
78
|
+
return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const m of value) {
|
|
81
|
+
if (exports.CLI_HANDOFF_MODES.includes(m) &&
|
|
82
|
+
!out.includes(m)) {
|
|
83
|
+
out.push(m);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The versioned-default rule: a descriptor that never declares modes offers
|
|
90
|
+
* only the durable mint it has always offered. Mode negotiation is purely
|
|
91
|
+
* additive — a daemon advertises the new mode, it never un-advertises the
|
|
92
|
+
* old one by accident.
|
|
93
|
+
*/
|
|
94
|
+
function advertisedHandoffModes(d) {
|
|
95
|
+
const declared = normalizeHandoffModes(d.handoffModes);
|
|
96
|
+
return declared.length > 0 ? declared : ['refresh_family'];
|
|
97
|
+
}
|
|
98
|
+
function descriptorOffersAccessToken(d) {
|
|
99
|
+
return advertisedHandoffModes(d).includes('access_token');
|
|
100
|
+
}
|
|
101
|
+
/** Parse the redeem response; null when the token itself is absent. */
|
|
102
|
+
function parseCliHandoffTokenResponse(value) {
|
|
103
|
+
if (value == null || typeof value !== 'object')
|
|
104
|
+
return null;
|
|
105
|
+
const v = value;
|
|
106
|
+
if (typeof v.accessToken !== 'string' || v.accessToken.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
return {
|
|
109
|
+
accessToken: v.accessToken,
|
|
110
|
+
...(typeof v.accessExpiresAt === 'number' ? { accessExpiresAt: v.accessExpiresAt } : {}),
|
|
111
|
+
...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -21,6 +21,8 @@ type DisabledReason = 'kek_unavailable' | 'init_failed' | 'platform_unsupported'
|
|
|
21
21
|
export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelopeState;
|
|
22
22
|
/** @internal Test seam — read state for assertions. */
|
|
23
23
|
export declare function __getStateForTest(): CredEnvelopeState;
|
|
24
|
+
/** @internal Test seam — each spec owns its warn-dedupe horizon. */
|
|
25
|
+
export declare function __clearWarnDedupeForTest(): void;
|
|
24
26
|
/**
|
|
25
27
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
26
28
|
* non-uninit returns immediately.
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.__setStateForTest = __setStateForTest;
|
|
7
7
|
exports.__getStateForTest = __getStateForTest;
|
|
8
|
+
exports.__clearWarnDedupeForTest = __clearWarnDedupeForTest;
|
|
8
9
|
exports.initCredEnvelope = initCredEnvelope;
|
|
9
10
|
exports.resetCredEnvelope = resetCredEnvelope;
|
|
10
11
|
exports.shutdownCredEnvelope = shutdownCredEnvelope;
|
|
@@ -141,7 +142,21 @@ function atomicWrite0600(target, value) {
|
|
|
141
142
|
* The event name passed in is the FULLY-QUALIFIED `cred_envelope.*` form
|
|
142
143
|
* because daemon-side telemetry consumers (`event-log.ts`, dashboards,
|
|
143
144
|
* spec assertions) key off the prefixed name. Do not strip the prefix.
|
|
145
|
+
*
|
|
146
|
+
* Identical failure warnings hit the logger only once per process.
|
|
147
|
+
* Telemetry still records every occurrence — the dedupe exists because a
|
|
148
|
+
* single unreadable envelope produced the same warning once per stored
|
|
149
|
+
* field (token, refresh token, expiry…), which read to users as three
|
|
150
|
+
* crashes rather than one stale credential (OSK-12074). The signature is
|
|
151
|
+
* event + kek identity + message, so a genuinely different failure mode
|
|
152
|
+
* still gets its own line.
|
|
144
153
|
*/
|
|
154
|
+
const warnedSignatures = new Set();
|
|
155
|
+
const MAX_WARNED_SIGNATURES = 64;
|
|
156
|
+
/** @internal Test seam — each spec owns its warn-dedupe horizon. */
|
|
157
|
+
function __clearWarnDedupeForTest() {
|
|
158
|
+
warnedSignatures.clear();
|
|
159
|
+
}
|
|
145
160
|
function emit(eventName, success, metadata) {
|
|
146
161
|
(0, runtime_js_1.emitAuthTelemetry)({
|
|
147
162
|
timestamp: new Date().toISOString(),
|
|
@@ -150,7 +165,13 @@ function emit(eventName, success, metadata) {
|
|
|
150
165
|
metadata: { event: eventName, ...metadata },
|
|
151
166
|
});
|
|
152
167
|
if (!success) {
|
|
153
|
-
|
|
168
|
+
const signature = `${eventName}:${metadata.kekId ?? ''}:${metadata.kekKind ?? ''}:${metadata.message ?? ''}`;
|
|
169
|
+
if (!warnedSignatures.has(signature)) {
|
|
170
|
+
if (warnedSignatures.size >= MAX_WARNED_SIGNATURES)
|
|
171
|
+
warnedSignatures.clear();
|
|
172
|
+
warnedSignatures.add(signature);
|
|
173
|
+
(0, runtime_js_1.getAuthLogger)().warn(`[credEnvelope] ${eventName}`, metadata);
|
|
174
|
+
}
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
// ---------------------------------------------------------------------------
|
|
@@ -763,6 +784,10 @@ function maybeDecryptOnRead(stored) {
|
|
|
763
784
|
kekId: _state.kekId,
|
|
764
785
|
kekKind: _state.kekKind,
|
|
765
786
|
message: err?.message ?? 'unknown',
|
|
787
|
+
// The crypto error alone reads as a crash; the actionable truth is
|
|
788
|
+
// that this stored credential is unreadable by THIS binary (usually
|
|
789
|
+
// a signing-identity or profile drift) and re-login rebuilds it.
|
|
790
|
+
hint: 'stored credential is unreadable on this machine — signing in again rebuilds it',
|
|
766
791
|
});
|
|
767
792
|
return { plaintext: null, needsMigration: false };
|
|
768
793
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credentialSession.ts — the shared credential-lifecycle kernel (policy half).
|
|
3
|
+
*
|
|
4
|
+
* A credential lifecycle is policy × mechanism. The policy — serve a cached
|
|
5
|
+
* token until its expiry-skew window, collapse concurrent acquires onto one
|
|
6
|
+
* in-flight operation, invalidate on a resource-side 401 — is identical for
|
|
7
|
+
* every surface and was being re-implemented per consumer (task token-broker,
|
|
8
|
+
* delegated-cli redemption, the human CLI refresh path). The mechanism —
|
|
9
|
+
* WHERE the durable secret lives and HOW a token is acquired — legitimately
|
|
10
|
+
* differs (in-memory daemon family vs file+envelope+lock vs loopback redeem)
|
|
11
|
+
* and stays behind the `acquire` seam.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately thin: no persistence, no retry policy, no logging. Callers
|
|
14
|
+
* own their store and their error surfaces; this owns the cache/single-
|
|
15
|
+
* flight/invalidate invariants so they cannot drift between surfaces.
|
|
16
|
+
*/
|
|
17
|
+
/** What an acquire returns. `expiresAtMs` absent means "no known expiry" —
|
|
18
|
+
* the token is served until `invalidate()` or an acquire replaces it. */
|
|
19
|
+
export interface AcquiredCredential {
|
|
20
|
+
accessToken: string;
|
|
21
|
+
/** Absolute expiry in ms since epoch, when the issuer tells us. */
|
|
22
|
+
expiresAtMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface CredentialSessionOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The mechanism. Called with `forceRefresh: true` when the caller asked to
|
|
27
|
+
* bypass the cache (the 401-recovery path) — implementations should renew
|
|
28
|
+
* rather than serve their own cache. A rejected promise propagates to every
|
|
29
|
+
* waiter and is never cached.
|
|
30
|
+
*/
|
|
31
|
+
acquire: (forceRefresh: boolean) => Promise<AcquiredCredential>;
|
|
32
|
+
/** Serve the cached token until this many ms before expiry. Default 30s. */
|
|
33
|
+
skewMs?: number;
|
|
34
|
+
/** Injectable clock for tests. */
|
|
35
|
+
now?: () => number;
|
|
36
|
+
}
|
|
37
|
+
export interface CredentialSession {
|
|
38
|
+
/**
|
|
39
|
+
* Resolves a usable access token: the cached one while it is inside its
|
|
40
|
+
* freshness window, otherwise a single shared acquire. `forceRefresh`
|
|
41
|
+
* discards the cache first (use after a resource 401).
|
|
42
|
+
*/
|
|
43
|
+
getToken(forceRefresh?: boolean): Promise<string>;
|
|
44
|
+
/** Drop the cached token; the next getToken() acquires. */
|
|
45
|
+
invalidate(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function createCredentialSession(opts: CredentialSessionOptions): CredentialSession;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* credentialSession.ts — the shared credential-lifecycle kernel (policy half).
|
|
4
|
+
*
|
|
5
|
+
* A credential lifecycle is policy × mechanism. The policy — serve a cached
|
|
6
|
+
* token until its expiry-skew window, collapse concurrent acquires onto one
|
|
7
|
+
* in-flight operation, invalidate on a resource-side 401 — is identical for
|
|
8
|
+
* every surface and was being re-implemented per consumer (task token-broker,
|
|
9
|
+
* delegated-cli redemption, the human CLI refresh path). The mechanism —
|
|
10
|
+
* WHERE the durable secret lives and HOW a token is acquired — legitimately
|
|
11
|
+
* differs (in-memory daemon family vs file+envelope+lock vs loopback redeem)
|
|
12
|
+
* and stays behind the `acquire` seam.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately thin: no persistence, no retry policy, no logging. Callers
|
|
15
|
+
* own their store and their error surfaces; this owns the cache/single-
|
|
16
|
+
* flight/invalidate invariants so they cannot drift between surfaces.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.createCredentialSession = createCredentialSession;
|
|
20
|
+
const DEFAULT_SKEW_MS = 30_000;
|
|
21
|
+
function createCredentialSession(opts) {
|
|
22
|
+
const skewMs = opts.skewMs ?? DEFAULT_SKEW_MS;
|
|
23
|
+
const now = opts.now ?? Date.now;
|
|
24
|
+
let cached = null;
|
|
25
|
+
let inFlight = null;
|
|
26
|
+
let inFlightForced = false;
|
|
27
|
+
// Monotonic generation so a superseded in-flight acquire (a forceRefresh
|
|
28
|
+
// launched while a plain acquire was still running) cannot write its stale
|
|
29
|
+
// result back over the newer one.
|
|
30
|
+
let generation = 0;
|
|
31
|
+
const fresh = (t) => t.expiresAtMs == null || now() < t.expiresAtMs - skewMs;
|
|
32
|
+
const acquireShared = (force) => {
|
|
33
|
+
// A forced acquire must not join a non-forced in-flight one — that would
|
|
34
|
+
// serve the very credential a 401 just invalidated. A forced in-flight
|
|
35
|
+
// DOES satisfy non-forced waiters: it is already renewing.
|
|
36
|
+
if (inFlight && (inFlightForced || !force))
|
|
37
|
+
return inFlight;
|
|
38
|
+
const gen = ++generation;
|
|
39
|
+
const p = opts.acquire(force).then((t) => {
|
|
40
|
+
if (gen === generation)
|
|
41
|
+
cached = t;
|
|
42
|
+
return t;
|
|
43
|
+
});
|
|
44
|
+
inFlight = p;
|
|
45
|
+
inFlightForced = force;
|
|
46
|
+
// `then(cleanup, cleanup)` not `.finally(cleanup)`: finally propagates the
|
|
47
|
+
// rejection into a derived promise nobody awaits — an unhandledRejection.
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
if (inFlight === p) {
|
|
50
|
+
inFlight = null;
|
|
51
|
+
inFlightForced = false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
p.then(cleanup, cleanup);
|
|
55
|
+
return p;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
async getToken(forceRefresh = false) {
|
|
59
|
+
if (forceRefresh)
|
|
60
|
+
cached = null;
|
|
61
|
+
if (cached && fresh(cached))
|
|
62
|
+
return cached.accessToken;
|
|
63
|
+
return (await acquireShared(forceRefresh)).accessToken;
|
|
64
|
+
},
|
|
65
|
+
invalidate() {
|
|
66
|
+
cached = null;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|