@skrr-ai/cli 0.1.52 → 0.1.54
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/commands/agents/chat.js +2 -1
- package/dist/commands/agents/create.js +13 -4
- package/dist/commands/agents/update.d.ts +32 -0
- package/dist/commands/agents/update.js +65 -14
- package/dist/commands/tasks/approvals/list.d.ts +58 -0
- package/dist/commands/tasks/approvals/list.js +173 -4
- package/dist/commands/tasks/create.js +2 -1
- package/dist/commands/tasks/start.js +1 -1
- package/dist/commands/tasks/update.js +23 -3
- package/dist/lib/agent-config.d.ts +13 -0
- package/dist/lib/agent-config.js +32 -0
- package/dist/lib/agentic-stream.d.ts +1 -1
- package/dist/lib/agentic-stream.js +1 -1
- package/dist/lib/auth-storage.js +7 -3
- package/dist/lib/cred-envelope.d.ts +12 -1
- package/dist/lib/cred-envelope.js +20 -3
- package/dist/lib/dedicated-machines.d.ts +20 -2
- package/dist/lib/dedicated-machines.js +46 -5
- package/dist/lib/keychain.js +26 -1
- package/dist/lib/label-ref.d.ts +1 -1
- package/dist/lib/label-ref.js +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +18 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +18 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +182 -175
- package/oclif.manifest.json +28759 -28747
- package/package.json +1 -1
package/dist/lib/agent-config.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RELEASE_INSTALL_CHANNELS = exports.RETIRED_COMMITMENT_PERMISSION_FLAGS = exports.COMMITMENT_PERMISSION_FLAG_RETIRED = exports.REASONING_TOOL_ESCAPES = exports.COMMITMENT_RESPONSIBILITY_LEVELS = void 0;
|
|
4
|
+
exports.retiredCommitmentPermissionFlagMessage = retiredCommitmentPermissionFlagMessage;
|
|
5
|
+
exports.deprecatedFieldsWarnings = deprecatedFieldsWarnings;
|
|
6
|
+
exports.deprecatedFieldNames = deprecatedFieldNames;
|
|
4
7
|
exports.parseWorkingHoursFlag = parseWorkingHoursFlag;
|
|
5
8
|
exports.mergeAgentConfigFlags = mergeAgentConfigFlags;
|
|
6
9
|
const prompt_1 = require("./prompt");
|
|
@@ -15,6 +18,35 @@ exports.RETIRED_COMMITMENT_PERMISSION_FLAGS = [
|
|
|
15
18
|
'commitment-responsibility',
|
|
16
19
|
'commitment-responsibility-ceiling',
|
|
17
20
|
];
|
|
21
|
+
/**
|
|
22
|
+
* The deprecation warning ONE retired flag prints. oclif prints a
|
|
23
|
+
* `deprecated.message` verbatim, so a shared message printed the same line
|
|
24
|
+
* once per flag with nothing saying which flag each one was about (OSK-12068).
|
|
25
|
+
*/
|
|
26
|
+
function retiredCommitmentPermissionFlagMessage(flag) {
|
|
27
|
+
return `--${flag} is retired and was not sent. ${exports.COMMITMENT_PERMISSION_FLAG_RETIRED}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The fields the server ignored and why, from a write's `deprecatedFields`
|
|
31
|
+
* notice, one line each. Empty when the response carries none.
|
|
32
|
+
*/
|
|
33
|
+
function deprecatedFieldsWarnings(response) {
|
|
34
|
+
const entries = response?.deprecatedFields;
|
|
35
|
+
if (!Array.isArray(entries))
|
|
36
|
+
return [];
|
|
37
|
+
return entries
|
|
38
|
+
.filter((entry) => Boolean(entry) && typeof entry.field === 'string')
|
|
39
|
+
.map((entry) => `${entry.field}: ${entry.reason || 'ignored by the server'}`);
|
|
40
|
+
}
|
|
41
|
+
/** The payload keys the server reported as ignored, so they are not also called "not kept". */
|
|
42
|
+
function deprecatedFieldNames(response) {
|
|
43
|
+
const entries = response?.deprecatedFields;
|
|
44
|
+
return Array.isArray(entries)
|
|
45
|
+
? entries
|
|
46
|
+
.map((entry) => entry?.field)
|
|
47
|
+
.filter((field) => typeof field === 'string')
|
|
48
|
+
: [];
|
|
49
|
+
}
|
|
18
50
|
exports.RELEASE_INSTALL_CHANNELS = ['latest', 'manual'];
|
|
19
51
|
/**
|
|
20
52
|
* `09:00-18:00` or `09:00-18:00@America/Los_Angeles`, or a JSON object.
|
|
@@ -299,7 +299,7 @@ export type SeqGap = {
|
|
|
299
299
|
* `service` envelopes that carry a `notice` — words for the person, not
|
|
300
300
|
* diagnostics — projected as `session.notice` events. Today the only notice is
|
|
301
301
|
* `harness-session-reset`: the local harness could not resume its previous
|
|
302
|
-
* session and began a new one,
|
|
302
|
+
* session and began a new one, which does not see earlier turns (OSK-12155).
|
|
303
303
|
*/
|
|
304
304
|
export declare function projectSessionNotices(envelopes: unknown[]): AgenticStreamEvent[];
|
|
305
305
|
export declare function detectSeqGap(highestApplied: number, incoming: number): SeqGap | null;
|
|
@@ -426,7 +426,7 @@ function jitteredRetryDelay(retryAfterMs, random = Math.random) {
|
|
|
426
426
|
* `service` envelopes that carry a `notice` — words for the person, not
|
|
427
427
|
* diagnostics — projected as `session.notice` events. Today the only notice is
|
|
428
428
|
* `harness-session-reset`: the local harness could not resume its previous
|
|
429
|
-
* session and began a new one,
|
|
429
|
+
* session and began a new one, which does not see earlier turns (OSK-12155).
|
|
430
430
|
*/
|
|
431
431
|
function projectSessionNotices(envelopes) {
|
|
432
432
|
const out = [];
|
package/dist/lib/auth-storage.js
CHANGED
|
@@ -288,7 +288,12 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
288
288
|
// L12 — route the secret string fields through the envelope. Mixed
|
|
289
289
|
// bundles (e.g., legacy plaintext token + freshly-rotated wrapped
|
|
290
290
|
// refresh) decrypt independently so partial migrations stay safe.
|
|
291
|
-
|
|
291
|
+
// Read the (plaintext) owner first, so a value another config root sealed
|
|
292
|
+
// under its own key fails with a sentence that says so (OSK-12135).
|
|
293
|
+
const owner = parseOwner(parsed.owner);
|
|
294
|
+
const ownerRoot = owner?.configRoot ? path.resolve(owner.configRoot) : undefined;
|
|
295
|
+
const decryptOptions = ownerRoot && ownerRoot !== path.resolve((0, config_1.configRoot)()) ? { foreignOwnerRoot: ownerRoot } : {};
|
|
296
|
+
const tokenResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.token, decryptOptions);
|
|
292
297
|
if (tokenResult.plaintext === null) {
|
|
293
298
|
// Envelope on disk but no DEK to unwrap → treat as missing so the
|
|
294
299
|
// caller falls through to a clean `skrr login`.
|
|
@@ -300,7 +305,7 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
300
305
|
bundle.expiresAt = parsed.expiresAt;
|
|
301
306
|
}
|
|
302
307
|
if (typeof parsed.refreshToken === 'string' && parsed.refreshToken.length > 0) {
|
|
303
|
-
const refreshResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.refreshToken);
|
|
308
|
+
const refreshResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.refreshToken, decryptOptions);
|
|
304
309
|
if (refreshResult.plaintext !== null) {
|
|
305
310
|
bundle.refreshToken = refreshResult.plaintext;
|
|
306
311
|
if (refreshResult.needsMigration)
|
|
@@ -316,7 +321,6 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
316
321
|
if (typeof parsed.serverOrigin === 'string' && parsed.serverOrigin.length > 0) {
|
|
317
322
|
bundle.serverOrigin = parsed.serverOrigin;
|
|
318
323
|
}
|
|
319
|
-
const owner = parseOwner(parsed.owner);
|
|
320
324
|
if (owner)
|
|
321
325
|
bundle.owner = owner;
|
|
322
326
|
// Best-effort upgrade: re-write the bundle so legacy plaintext fields
|
|
@@ -8,7 +8,18 @@
|
|
|
8
8
|
* and file backends and avoids a detect-then-read TOCTOU race.
|
|
9
9
|
*/
|
|
10
10
|
import { type ReadTransformResult } from '@skrr-ai/auth-core';
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The `decrypt_failed` hint for a value whose owner record names ANOTHER
|
|
13
|
+
* config root. Each root seals under its own key, so that value is unreadable
|
|
14
|
+
* here by construction: "signing in again rebuilds it" would send someone to
|
|
15
|
+
* re-login over a credential that is not broken and not theirs (OSK-12135).
|
|
16
|
+
*/
|
|
17
|
+
export declare function foreignCredentialHint(ownerConfigRoot: string): string;
|
|
18
|
+
export interface CliDecryptOnReadOptions {
|
|
19
|
+
/** The config root the stored value's owner record names, when not ours. */
|
|
20
|
+
foreignOwnerRoot?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function maybeDecryptOnRead(stored: string, options?: CliDecryptOnReadOptions): ReadTransformResult;
|
|
12
23
|
/** Consume and clear the per-process blocked-read observation. */
|
|
13
24
|
export declare function consumeEncryptedCredentialReadWhileInactive(): boolean;
|
|
14
25
|
/** Consume any unreadable envelope observation, including tamper/decrypt failure. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.__wrappedDekFilePathForTest = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = void 0;
|
|
4
|
+
exports.foreignCredentialHint = foreignCredentialHint;
|
|
4
5
|
exports.maybeDecryptOnRead = maybeDecryptOnRead;
|
|
5
6
|
exports.consumeEncryptedCredentialReadWhileInactive = consumeEncryptedCredentialReadWhileInactive;
|
|
6
7
|
exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCredentialRead;
|
|
@@ -16,10 +17,26 @@ exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCre
|
|
|
16
17
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
17
18
|
let encryptedReadWhileInactive = false;
|
|
18
19
|
let unreadableEncryptedRead = false;
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The `decrypt_failed` hint for a value whose owner record names ANOTHER
|
|
22
|
+
* config root. Each root seals under its own key, so that value is unreadable
|
|
23
|
+
* here by construction: "signing in again rebuilds it" would send someone to
|
|
24
|
+
* re-login over a credential that is not broken and not theirs (OSK-12135).
|
|
25
|
+
*/
|
|
26
|
+
function foreignCredentialHint(ownerConfigRoot) {
|
|
27
|
+
return (`stored credential belongs to another skrr config (config root ${ownerConfigRoot}) and is ` +
|
|
28
|
+
"sealed under that config's key, so this config cannot read it. Nothing is wrong with " +
|
|
29
|
+
'either credential and it was left untouched; signing in here does not change it');
|
|
30
|
+
}
|
|
31
|
+
function maybeDecryptOnRead(stored, options = {}) {
|
|
32
|
+
const result = (0, auth_core_1.maybeDecryptOnRead)(stored, options.foreignOwnerRoot
|
|
33
|
+
? { unreadableHint: foreignCredentialHint(options.foreignOwnerRoot) }
|
|
34
|
+
: {});
|
|
21
35
|
if (result.plaintext === null && (0, auth_core_1.isCredEnvelopeString)(stored)) {
|
|
22
|
-
|
|
36
|
+
// Another config's value is not an unreadable credential of OURS, so it
|
|
37
|
+
// must not tell `logout` there is local encrypted state to erase.
|
|
38
|
+
if (!options.foreignOwnerRoot)
|
|
39
|
+
unreadableEncryptedRead = true;
|
|
23
40
|
if ((0, auth_core_1.describeCredEnvelopeState)().kind !== 'active') {
|
|
24
41
|
encryptedReadWhileInactive = true;
|
|
25
42
|
}
|
|
@@ -122,6 +122,11 @@ export interface DedicatedImageStanding {
|
|
|
122
122
|
* machine runs that image and its daemon reported a version. Null otherwise.
|
|
123
123
|
*/
|
|
124
124
|
approvedImageDaemonVersion?: string | null;
|
|
125
|
+
/**
|
|
126
|
+
* Why `approvedImageDaemonVersion` is null, from a closed server vocabulary.
|
|
127
|
+
* Absent from a server older than this field.
|
|
128
|
+
*/
|
|
129
|
+
approvedImageDaemonUnknownReason?: 'image_superseded' | 'approval_unreadable' | 'daemon_unreported' | null;
|
|
125
130
|
/** When the older image stops being supported; null when there is no deadline. */
|
|
126
131
|
updateBy?: string | null;
|
|
127
132
|
lastUpdate?: {
|
|
@@ -150,8 +155,21 @@ export declare function compareDaemonVersions(a: string, b: string): number | nu
|
|
|
150
155
|
* half is said to be unknown rather than left out.
|
|
151
156
|
*/
|
|
152
157
|
export declare function describeDedicatedDaemonRelease(image: DedicatedImageStanding, latest: LatestDaemonRelease | null | undefined): string | null;
|
|
153
|
-
/**
|
|
154
|
-
|
|
158
|
+
/**
|
|
159
|
+
* One line saying where a machine's image stands and what the owner can do.
|
|
160
|
+
*
|
|
161
|
+
* "current" is claimed only when it is KNOWN: no newer approved image AND this
|
|
162
|
+
* machine's own daemon is at the latest release (OSK-12153). A guest never
|
|
163
|
+
* updates its own daemon, so "no newer approved image" beside a daemon that is
|
|
164
|
+
* behind the latest release is a different — and worse — state than "current",
|
|
165
|
+
* and the line says so instead of collapsing toward the reassuring reading.
|
|
166
|
+
* With the release feed unread, or no daemon version, it says only what is
|
|
167
|
+
* known: there is no newer approved image.
|
|
168
|
+
*/
|
|
169
|
+
export declare function describeDedicatedImageStanding(image: DedicatedImageStanding, leaseId?: string, opts?: {
|
|
170
|
+
daemonVersion?: string | null;
|
|
171
|
+
latestDaemonRelease?: LatestDaemonRelease | null;
|
|
172
|
+
}): string;
|
|
155
173
|
/**
|
|
156
174
|
* A deleted workspace: restorable into a new machine until `recoverableUntil`,
|
|
157
175
|
* then every copy is deleted and `completedAt` records it. `withheld`: an unpaid
|
|
@@ -107,7 +107,7 @@ function describeDedicatedDaemonRelease(image, latest) {
|
|
|
107
107
|
? `latest release skrrd ${latest.version}`
|
|
108
108
|
: `latest release unknown (${latest.error})`;
|
|
109
109
|
if (!carried) {
|
|
110
|
-
return image
|
|
110
|
+
return `${approvedImageDaemonUnknownText(image)} · ${latestText}`;
|
|
111
111
|
}
|
|
112
112
|
if (!('version' in latest)) {
|
|
113
113
|
return `approved image carries skrrd ${carried} · ${latestText}`;
|
|
@@ -121,8 +121,35 @@ function describeDedicatedDaemonRelease(image, latest) {
|
|
|
121
121
|
}
|
|
122
122
|
return `approved image carries skrrd ${carried} · ${latestText}`;
|
|
123
123
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Why the server could not say which daemon the approved image carries, in
|
|
126
|
+
* words an owner can act on (OSK-12153). A bare "unknown" named no cause and no
|
|
127
|
+
* next step.
|
|
128
|
+
*/
|
|
129
|
+
function approvedImageDaemonUnknownText(image) {
|
|
130
|
+
switch (image.approvedImageDaemonUnknownReason) {
|
|
131
|
+
case 'image_superseded':
|
|
132
|
+
return 'this machine runs an older image than the approved one, so which skrrd the approved image carries is seen once it moves onto it';
|
|
133
|
+
case 'approval_unreadable':
|
|
134
|
+
return "the server cannot compare this machine's image with the approved one, so it cannot say which skrrd the approved image carries";
|
|
135
|
+
case 'daemon_unreported':
|
|
136
|
+
return 'this machine runs the approved image, but its daemon has not reported a version yet';
|
|
137
|
+
default:
|
|
138
|
+
return 'the server did not say which skrrd the approved image carries';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* One line saying where a machine's image stands and what the owner can do.
|
|
143
|
+
*
|
|
144
|
+
* "current" is claimed only when it is KNOWN: no newer approved image AND this
|
|
145
|
+
* machine's own daemon is at the latest release (OSK-12153). A guest never
|
|
146
|
+
* updates its own daemon, so "no newer approved image" beside a daemon that is
|
|
147
|
+
* behind the latest release is a different — and worse — state than "current",
|
|
148
|
+
* and the line says so instead of collapsing toward the reassuring reading.
|
|
149
|
+
* With the release feed unread, or no daemon version, it says only what is
|
|
150
|
+
* known: there is no newer approved image.
|
|
151
|
+
*/
|
|
152
|
+
function describeDedicatedImageStanding(image, leaseId, opts = {}) {
|
|
126
153
|
const last = image.lastUpdate;
|
|
127
154
|
if (last?.status === 'applying') {
|
|
128
155
|
return `moving onto the current image${last.startedAt ? ` since ${last.startedAt}` : ''}; files are kept`;
|
|
@@ -141,7 +168,18 @@ function describeDedicatedImageStanding(image, leaseId) {
|
|
|
141
168
|
parts.push('the last image update did not complete on either image; files are kept');
|
|
142
169
|
}
|
|
143
170
|
else {
|
|
144
|
-
|
|
171
|
+
const here = String(opts.daemonVersion || '').trim();
|
|
172
|
+
const latest = opts.latestDaemonRelease;
|
|
173
|
+
const compared = here && latest && 'version' in latest ? compareDaemonVersions(here, latest.version) : null;
|
|
174
|
+
if (compared === -1 && latest && 'version' in latest) {
|
|
175
|
+
parts.push(`no newer approved image, but this machine's skrrd ${here} is behind the latest release ${latest.version}`);
|
|
176
|
+
}
|
|
177
|
+
else if (compared !== null) {
|
|
178
|
+
parts.push('current');
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
parts.push('no newer approved image');
|
|
182
|
+
}
|
|
145
183
|
}
|
|
146
184
|
if (last?.status === 'succeeded' && last.settledAt)
|
|
147
185
|
parts.push(`updated ${last.settledAt}`);
|
|
@@ -631,7 +669,10 @@ function describeDedicatedLease(lease, harnesses = [], opts = {}) {
|
|
|
631
669
|
const image = [resources.imageVersion || lease.daemon?.imageVersion].filter(Boolean).join('');
|
|
632
670
|
row('image', image);
|
|
633
671
|
if (lease.health?.image) {
|
|
634
|
-
row('image update', describeDedicatedImageStanding(lease.health.image, dedicatedLeaseId(lease)
|
|
672
|
+
row('image update', describeDedicatedImageStanding(lease.health.image, dedicatedLeaseId(lease), {
|
|
673
|
+
daemonVersion: lease.daemon?.version,
|
|
674
|
+
latestDaemonRelease: opts.latestDaemonRelease,
|
|
675
|
+
}));
|
|
635
676
|
row('image daemon', describeDedicatedDaemonRelease(lease.health.image, opts.latestDaemonRelease));
|
|
636
677
|
}
|
|
637
678
|
if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
|
package/dist/lib/keychain.js
CHANGED
|
@@ -290,7 +290,7 @@ function readSlot(account, service = currentService(), raw_ = false) {
|
|
|
290
290
|
// re-login; the bundle is unrecoverable without the DEK).
|
|
291
291
|
// - Legacy plaintext + path active → returns plaintext + needsMigration;
|
|
292
292
|
// we re-write in place to upgrade the slot.
|
|
293
|
-
const result = (0, cred_envelope_1.maybeDecryptOnRead)(raw);
|
|
293
|
+
const result = (0, cred_envelope_1.maybeDecryptOnRead)(raw, { foreignOwnerRoot: foreignOwnerRootAt(service) });
|
|
294
294
|
if (result.plaintext === null)
|
|
295
295
|
return null;
|
|
296
296
|
// Only this root's OWN slot is upgraded. A slot read as a migration source may
|
|
@@ -308,6 +308,31 @@ function readSlot(account, service = currentService(), raw_ = false) {
|
|
|
308
308
|
}
|
|
309
309
|
return result.plaintext;
|
|
310
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* The config root named by `service`'s owner record, when it is NOT this
|
|
313
|
+
* process's root; otherwise undefined. The record is non-secret JSON
|
|
314
|
+
* (`{configRoot, cliId}`), so it can be read before the secret it describes.
|
|
315
|
+
*
|
|
316
|
+
* Only a diagnostic input. Whether the bundle is ours is still decided by
|
|
317
|
+
* `auth-storage`'s `judgeOwnership`, which also weighs the token's own `did`
|
|
318
|
+
* — so the secret is decrypted as before, and only what a failure SAYS changes.
|
|
319
|
+
*/
|
|
320
|
+
function foreignOwnerRootAt(service) {
|
|
321
|
+
const raw = readSlot(ACCOUNT_OWNER, service);
|
|
322
|
+
if (!raw)
|
|
323
|
+
return undefined;
|
|
324
|
+
let root;
|
|
325
|
+
try {
|
|
326
|
+
root = JSON.parse(raw)?.configRoot;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
if (typeof root !== 'string' || root.length === 0)
|
|
332
|
+
return undefined;
|
|
333
|
+
const resolved = path.resolve(root);
|
|
334
|
+
return resolved === resolvedConfigRoot() ? undefined : resolved;
|
|
335
|
+
}
|
|
311
336
|
/**
|
|
312
337
|
* Escape a value for `security -i` (stdin interactive mode).
|
|
313
338
|
* `security` accepts double-quoted args with `\"` and `\\` backslash escapes.
|
package/dist/lib/label-ref.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export declare class LabelRefError extends Error {
|
|
|
103
103
|
*/
|
|
104
104
|
export declare function resolveLabelRefs(refs: string[] | undefined, context: LabelRefContext): Promise<string[]>;
|
|
105
105
|
/**
|
|
106
|
-
* Help for a `--tag` flag that sits beside `--label` (OSK-12053).
|
|
106
|
+
* Help for a `--tag` flag that sits beside `--label-id` (OSK-12053, OSK-12166).
|
|
107
107
|
*
|
|
108
108
|
* Tags and labels are two DIFFERENT vocabularies: a tag is any free string,
|
|
109
109
|
* stored as typed on `task.tags`; a label is declared (`skrr labels`) and is
|
package/dist/lib/label-ref.js
CHANGED
|
@@ -175,7 +175,7 @@ async function resolveLabelRefs(refs, context) {
|
|
|
175
175
|
return resolved;
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
|
-
* Help for a `--tag` flag that sits beside `--label` (OSK-12053).
|
|
178
|
+
* Help for a `--tag` flag that sits beside `--label-id` (OSK-12053, OSK-12166).
|
|
179
179
|
*
|
|
180
180
|
* Tags and labels are two DIFFERENT vocabularies: a tag is any free string,
|
|
181
181
|
* stored as typed on `task.tags`; a label is declared (`skrr labels`) and is
|
|
@@ -185,7 +185,8 @@ async function resolveLabelRefs(refs, context) {
|
|
|
185
185
|
* and none of them was labelled.
|
|
186
186
|
*/
|
|
187
187
|
exports.TAG_FLAG_HELP = 'Free-form tag (repeatable), stored exactly as typed and never checked against anything. ' +
|
|
188
|
-
'NOT a label: to classify the task under a declared label (`labels list`), use --label
|
|
188
|
+
'NOT a label: to classify the task under a declared label (`labels list`), use --label-id ' +
|
|
189
|
+
'(it takes a label name or id).';
|
|
189
190
|
/**
|
|
190
191
|
* The `--tag` values that equal a declared label's name, each paired with that
|
|
191
192
|
* label — the one case where the mistake is certain enough to say so.
|
|
@@ -221,7 +222,10 @@ function describeTagsNamingLabels(hits) {
|
|
|
221
222
|
if (hits.length === 0)
|
|
222
223
|
return '';
|
|
223
224
|
const names = hits.map((hit) => `"${hit.tag}"`).join(', ');
|
|
224
|
-
|
|
225
|
+
// `--label-id` is the flag the help LISTS; `--label` is a hidden alias of it,
|
|
226
|
+
// so a warning that named the alias sent people to a flag they could not
|
|
227
|
+
// find in `--help` (OSK-12166).
|
|
228
|
+
const flags = hits.map((hit) => `--label-id ${hit.label.name}`).join(' ');
|
|
225
229
|
return (`--tag ${names} ${hits.length === 1 ? 'was' : 'were'} stored as free-form ` +
|
|
226
230
|
`${hits.length === 1 ? 'tag' : 'tags'}, NOT as the declared label of the same name, ` +
|
|
227
231
|
`so a label filter will not find this task. To attach the label, pass ${flags}.`);
|
|
@@ -141,7 +141,19 @@ export interface ReadTransformResult {
|
|
|
141
141
|
* path is active — caller should re-write to upgrade. */
|
|
142
142
|
needsMigration: boolean;
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
/** What the caller knows about the stored value that the envelope does not. */
|
|
145
|
+
export interface DecryptOnReadOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Replaces the default `decrypt_failed` hint. The default says a re-login
|
|
148
|
+
* rebuilds the credential, which is true only of THIS config's own value. A
|
|
149
|
+
* caller reading a value another config sealed under its own key knows the
|
|
150
|
+
* failure is expected and a re-login here changes nothing (OSK-12135).
|
|
151
|
+
*/
|
|
152
|
+
unreadableHint?: string;
|
|
153
|
+
}
|
|
154
|
+
/** The `decrypt_failed` hint when the caller has no better explanation. */
|
|
155
|
+
export declare const DEFAULT_UNREADABLE_CREDENTIAL_HINT = "stored credential is unreadable on this machine \u2014 signing in again rebuilds it";
|
|
156
|
+
export declare function maybeDecryptOnRead(stored: string, options?: DecryptOnReadOptions): ReadTransformResult;
|
|
145
157
|
/** @internal Test seam — wrapped-DEK path resolver. */
|
|
146
158
|
export declare function __wrappedDekFilePathForTest(profile: string): string;
|
|
147
159
|
export {};
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT = void 0;
|
|
6
7
|
exports.__setStateForTest = __setStateForTest;
|
|
7
8
|
exports.__getStateForTest = __getStateForTest;
|
|
8
9
|
exports.__clearWarnDedupeForTest = __clearWarnDedupeForTest;
|
|
@@ -165,7 +166,10 @@ function emit(eventName, success, metadata) {
|
|
|
165
166
|
metadata: { event: eventName, ...metadata },
|
|
166
167
|
});
|
|
167
168
|
if (!success) {
|
|
168
|
-
|
|
169
|
+
// The hint is part of the signature: the same crypto failure on this
|
|
170
|
+
// config's own credential and on another config's is two different facts,
|
|
171
|
+
// and the second must not swallow the first (OSK-12135).
|
|
172
|
+
const signature = `${eventName}:${metadata.kekId ?? ''}:${metadata.kekKind ?? ''}:${metadata.message ?? ''}:${metadata.hint ?? ''}`;
|
|
169
173
|
if (!warnedSignatures.has(signature)) {
|
|
170
174
|
if (warnedSignatures.size >= MAX_WARNED_SIGNATURES)
|
|
171
175
|
warnedSignatures.clear();
|
|
@@ -325,14 +329,18 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
325
329
|
};
|
|
326
330
|
// The paths go in the MESSAGE, not only the metadata: a log pipeline that
|
|
327
331
|
// drops structured fields would otherwise render this as an unactionable
|
|
328
|
-
// noun. Naming the remedy matters too
|
|
329
|
-
//
|
|
332
|
+
// noun. Naming the remedy matters too, and it has to be one that converges
|
|
333
|
+
// (OSK-12172). Only the CLI's logout deletes a key, and only the canonical
|
|
334
|
+
// one; a daemon logout deletes neither. So the CLI logs out, the daemon
|
|
335
|
+
// BINARY restarts and adopts its legacy key into the empty canonical slot,
|
|
336
|
+
// and the CLI logs back in on it. Not `skrr daemon restart`: any `skrr`
|
|
337
|
+
// command mints a fresh key into an empty slot before doing anything else.
|
|
330
338
|
(0, runtime_js_1.getAuthLogger)().warn(`[credEnvelope] profile "${profile}" has two different usable wrapped DEKs; ` +
|
|
331
339
|
`refusing to choose. Keeping ${_pathConflict.legacyPath} and leaving ` +
|
|
332
340
|
`${_pathConflict.canonicalPath} untouched. Each side can still read its own ` +
|
|
333
|
-
"credentials, and neither can read the other's.
|
|
334
|
-
|
|
335
|
-
'
|
|
341
|
+
"credentials, and neither can read the other's. Do not delete " +
|
|
342
|
+
`${_pathConflict.legacyPath}: it is the key the daemon reads. To converge on it, ` +
|
|
343
|
+
'run `skrr logout && skrrd restart && skrr login`.');
|
|
336
344
|
}
|
|
337
345
|
/**
|
|
338
346
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
@@ -755,7 +763,9 @@ function wrapIfActiveOrPassthrough(plaintext) {
|
|
|
755
763
|
return plaintext;
|
|
756
764
|
}
|
|
757
765
|
}
|
|
758
|
-
|
|
766
|
+
/** The `decrypt_failed` hint when the caller has no better explanation. */
|
|
767
|
+
exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT = 'stored credential is unreadable on this machine — signing in again rebuilds it';
|
|
768
|
+
function maybeDecryptOnRead(stored, options = {}) {
|
|
759
769
|
if (typeof stored !== 'string' || stored.length === 0) {
|
|
760
770
|
return { plaintext: stored, needsMigration: false };
|
|
761
771
|
}
|
|
@@ -787,7 +797,7 @@ function maybeDecryptOnRead(stored) {
|
|
|
787
797
|
// The crypto error alone reads as a crash; the actionable truth is
|
|
788
798
|
// that this stored credential is unreadable by THIS binary (usually
|
|
789
799
|
// a signing-identity or profile drift) and re-login rebuilds it.
|
|
790
|
-
hint:
|
|
800
|
+
hint: options.unreadableHint ?? exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT,
|
|
791
801
|
});
|
|
792
802
|
return { plaintext: null, needsMigration: false };
|
|
793
803
|
}
|
|
@@ -27,7 +27,7 @@ export { formatRecoveryCode, parseRecoveryCode, normalizeRecoveryCode, isValidRe
|
|
|
27
27
|
export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, ENVELOPE_MAGIC as CRED_ENVELOPE_MAGIC, generateDek, isEnvelopeString as isCredEnvelopeString, KEY_LEN as CRED_KEY_LEN, openBuffer as credOpenBuffer, sealBuffer as credSealBuffer, serialize as credEnvelopeSerialize, unwrap as credEnvelopeUnwrap, wrap as credEnvelopeWrap, type SealedBlob as CredSealedBlob, } from './credentialEnvelope.js';
|
|
28
28
|
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
|
|
29
29
|
export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
|
|
30
|
-
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
30
|
+
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
31
31
|
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
32
32
|
export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
33
33
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
@@ -24,9 +24,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.reauthCommand = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
|
|
26
26
|
exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = exports.challenge = void 0;
|
|
27
|
-
exports.
|
|
28
|
-
exports.
|
|
29
|
-
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = void 0;
|
|
27
|
+
exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__resetMachineUuidCacheForTest = exports.HOSTED_RUNTIME_MACHINE_SOURCES = exports.hostedRuntimeIdOverride = exports.getMachineUuid = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.DEFAULT_UNREADABLE_CREDENTIAL_HINT = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.hasCredEnvelopePathConflict = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = exports.tokenHash = void 0;
|
|
28
|
+
exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.resolveFirstPartyHarnessChannel = exports.resolveChannelManifestUrl = exports.isFirstPartyHarnessChannel = exports.firstPartyHarnessFeedCandidates = exports.firstPartyHarnessFeedBase = exports.firstPartyHarnessChannelPrefix = exports.FIRST_PARTY_HARNESS_CHANNELS = exports.DEFAULT_FIRST_PARTY_HARNESS_CHANNEL = exports.migrateFirstPartyHarnessHome = exports.linkFirstPartyHarnessAliases = exports.firstPartyHarnessInstalledBinaryPath = exports.firstPartyHarnessHomeDir = exports.firstPartyHarnessBinDir = exports.FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = exports.FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.LOOPBACK_DEFAULT_MAX_BODY_BYTES = exports.LoopbackTargetRefusedError = exports.isLoopbackAddress = exports.isLoopbackHostname = exports.assertLoopbackHttpUrl = exports.loopbackFetch = exports.withLoopbackNoProxy = exports.envHasProxy = exports.LOOPBACK_NO_PROXY_HOSTS = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.harnessLoadsClaudeSkillsLayout = exports.claudeSkillsLayoutEvidence = exports.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = void 0;
|
|
29
|
+
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = void 0;
|
|
30
30
|
// Daemon scope vocabulary — single source of truth for non-Mongoose tiers
|
|
31
31
|
// (daemon, desktop, cli). Schema-bound canonical lives in
|
|
32
32
|
// `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
|
|
@@ -234,6 +234,7 @@ Object.defineProperty(exports, "hasCredEnvelopePathConflict", { enumerable: true
|
|
|
234
234
|
Object.defineProperty(exports, "describeCredEnvelopeState", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.describeCredEnvelopeState; } });
|
|
235
235
|
Object.defineProperty(exports, "maybeEncryptForWrite", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.maybeEncryptForWrite; } });
|
|
236
236
|
Object.defineProperty(exports, "maybeDecryptOnRead", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.maybeDecryptOnRead; } });
|
|
237
|
+
Object.defineProperty(exports, "DEFAULT_UNREADABLE_CREDENTIAL_HINT", { enumerable: true, get: function () { return credentialEnvelopeBridge_js_1.DEFAULT_UNREADABLE_CREDENTIAL_HINT; } });
|
|
237
238
|
// Test seams — `__`-prefixed so consumers don't import them in
|
|
238
239
|
// production code paths. Re-exported here so CLI/daemon spec files
|
|
239
240
|
// can resolve the wrapped-DEK path against the same module instance.
|
|
@@ -141,7 +141,19 @@ export interface ReadTransformResult {
|
|
|
141
141
|
* path is active — caller should re-write to upgrade. */
|
|
142
142
|
needsMigration: boolean;
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
/** What the caller knows about the stored value that the envelope does not. */
|
|
145
|
+
export interface DecryptOnReadOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Replaces the default `decrypt_failed` hint. The default says a re-login
|
|
148
|
+
* rebuilds the credential, which is true only of THIS config's own value. A
|
|
149
|
+
* caller reading a value another config sealed under its own key knows the
|
|
150
|
+
* failure is expected and a re-login here changes nothing (OSK-12135).
|
|
151
|
+
*/
|
|
152
|
+
unreadableHint?: string;
|
|
153
|
+
}
|
|
154
|
+
/** The `decrypt_failed` hint when the caller has no better explanation. */
|
|
155
|
+
export declare const DEFAULT_UNREADABLE_CREDENTIAL_HINT = "stored credential is unreadable on this machine \u2014 signing in again rebuilds it";
|
|
156
|
+
export declare function maybeDecryptOnRead(stored: string, options?: DecryptOnReadOptions): ReadTransformResult;
|
|
145
157
|
/** @internal Test seam — wrapped-DEK path resolver. */
|
|
146
158
|
export declare function __wrappedDekFilePathForTest(profile: string): string;
|
|
147
159
|
export {};
|
|
@@ -47,7 +47,7 @@ import lockfile from 'proper-lockfile';
|
|
|
47
47
|
import { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, isEnvelopeString as isCredEnvelopeString, generateDek, serialize as credEnvelopeSerialize, } from './credentialEnvelope.js';
|
|
48
48
|
import { getKekStrategy, zeroizeKekCaches } from './kek/index.js';
|
|
49
49
|
import { consolidateProfileState, profileStateDir, profileStateFilePathsToClear, resolveProfileStateFile, } from './profileStateDir.js';
|
|
50
|
-
import { emitAuthTelemetry,
|
|
50
|
+
import { emitAuthTelemetry, getAuthLogger, getCredEnvelopeFailClosed } from './runtime.js';
|
|
51
51
|
/**
|
|
52
52
|
* Account name the wrapped DEK lives under (file fallback). Keeping the
|
|
53
53
|
* literal here so a future rename surfaces as a single grep target.
|
|
@@ -146,7 +146,10 @@ function emit(eventName, success, metadata) {
|
|
|
146
146
|
metadata: { event: eventName, ...metadata },
|
|
147
147
|
});
|
|
148
148
|
if (!success) {
|
|
149
|
-
|
|
149
|
+
// The hint is part of the signature: the same crypto failure on this
|
|
150
|
+
// config's own credential and on another config's is two different facts,
|
|
151
|
+
// and the second must not swallow the first (OSK-12135).
|
|
152
|
+
const signature = `${eventName}:${metadata.kekId ?? ''}:${metadata.kekKind ?? ''}:${metadata.message ?? ''}:${metadata.hint ?? ''}`;
|
|
150
153
|
if (!warnedSignatures.has(signature)) {
|
|
151
154
|
if (warnedSignatures.size >= MAX_WARNED_SIGNATURES)
|
|
152
155
|
warnedSignatures.clear();
|
|
@@ -306,14 +309,18 @@ async function consolidateWrappedDek(profile, kek, aad) {
|
|
|
306
309
|
};
|
|
307
310
|
// The paths go in the MESSAGE, not only the metadata: a log pipeline that
|
|
308
311
|
// drops structured fields would otherwise render this as an unactionable
|
|
309
|
-
// noun. Naming the remedy matters too
|
|
310
|
-
//
|
|
312
|
+
// noun. Naming the remedy matters too, and it has to be one that converges
|
|
313
|
+
// (OSK-12172). Only the CLI's logout deletes a key, and only the canonical
|
|
314
|
+
// one; a daemon logout deletes neither. So the CLI logs out, the daemon
|
|
315
|
+
// BINARY restarts and adopts its legacy key into the empty canonical slot,
|
|
316
|
+
// and the CLI logs back in on it. Not `skrr daemon restart`: any `skrr`
|
|
317
|
+
// command mints a fresh key into an empty slot before doing anything else.
|
|
311
318
|
getAuthLogger().warn(`[credEnvelope] profile "${profile}" has two different usable wrapped DEKs; ` +
|
|
312
319
|
`refusing to choose. Keeping ${_pathConflict.legacyPath} and leaving ` +
|
|
313
320
|
`${_pathConflict.canonicalPath} untouched. Each side can still read its own ` +
|
|
314
|
-
"credentials, and neither can read the other's.
|
|
315
|
-
|
|
316
|
-
'
|
|
321
|
+
"credentials, and neither can read the other's. Do not delete " +
|
|
322
|
+
`${_pathConflict.legacyPath}: it is the key the daemon reads. To converge on it, ` +
|
|
323
|
+
'run `skrr logout && skrrd restart && skrr login`.');
|
|
317
324
|
}
|
|
318
325
|
/**
|
|
319
326
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
@@ -736,7 +743,9 @@ export function wrapIfActiveOrPassthrough(plaintext) {
|
|
|
736
743
|
return plaintext;
|
|
737
744
|
}
|
|
738
745
|
}
|
|
739
|
-
|
|
746
|
+
/** The `decrypt_failed` hint when the caller has no better explanation. */
|
|
747
|
+
export const DEFAULT_UNREADABLE_CREDENTIAL_HINT = 'stored credential is unreadable on this machine — signing in again rebuilds it';
|
|
748
|
+
export function maybeDecryptOnRead(stored, options = {}) {
|
|
740
749
|
if (typeof stored !== 'string' || stored.length === 0) {
|
|
741
750
|
return { plaintext: stored, needsMigration: false };
|
|
742
751
|
}
|
|
@@ -768,7 +777,7 @@ export function maybeDecryptOnRead(stored) {
|
|
|
768
777
|
// The crypto error alone reads as a crash; the actionable truth is
|
|
769
778
|
// that this stored credential is unreadable by THIS binary (usually
|
|
770
779
|
// a signing-identity or profile drift) and re-login rebuilds it.
|
|
771
|
-
hint:
|
|
780
|
+
hint: options.unreadableHint ?? DEFAULT_UNREADABLE_CREDENTIAL_HINT,
|
|
772
781
|
});
|
|
773
782
|
return { plaintext: null, needsMigration: false };
|
|
774
783
|
}
|
|
@@ -27,7 +27,7 @@ export { formatRecoveryCode, parseRecoveryCode, normalizeRecoveryCode, isValidRe
|
|
|
27
27
|
export { buildCredentialAad, decrypt as credEnvelopeDecrypt, deserialize as credEnvelopeDeserialize, encrypt as credEnvelopeEncrypt, ENVELOPE_MAGIC as CRED_ENVELOPE_MAGIC, generateDek, isEnvelopeString as isCredEnvelopeString, KEY_LEN as CRED_KEY_LEN, openBuffer as credOpenBuffer, sealBuffer as credSealBuffer, serialize as credEnvelopeSerialize, unwrap as credEnvelopeUnwrap, wrap as credEnvelopeWrap, type SealedBlob as CredSealedBlob, } from './credentialEnvelope.js';
|
|
28
28
|
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
|
|
29
29
|
export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
|
|
30
|
-
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
30
|
+
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
31
31
|
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
32
32
|
export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
33
33
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
@@ -108,7 +108,7 @@ export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, n
|
|
|
108
108
|
// Daemon and CLI consume these; the underlying primitives live above.
|
|
109
109
|
export {
|
|
110
110
|
// credEnvelope bridge
|
|
111
|
-
initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead,
|
|
111
|
+
initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT,
|
|
112
112
|
// Test seams — `__`-prefixed so consumers don't import them in
|
|
113
113
|
// production code paths. Re-exported here so CLI/daemon spec files
|
|
114
114
|
// can resolve the wrapped-DEK path against the same module instance.
|