@skrr-ai/cli 0.1.122 → 0.1.123
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.d.ts +7 -0
- package/dist/commands/agents/chat.js +32 -6
- package/dist/commands/followups/act.js +3 -0
- package/dist/commands/followups/checkin.js +3 -0
- package/dist/commands/followups/remind.js +3 -0
- package/dist/commands/followups/show.js +1 -1
- package/dist/commands/followups/watch.js +3 -0
- package/dist/lib/agentic-stream.d.ts +46 -0
- package/dist/lib/agentic-stream.js +135 -14
- package/dist/lib/auth-failure.js +12 -0
- package/dist/lib/exec-runtime-binary.d.ts +9 -0
- package/dist/lib/exec-runtime-binary.js +15 -1
- package/dist/lib/followups.d.ts +13 -0
- package/dist/lib/followups.js +8 -0
- package/dist/lib/profile-machine-identity-guard.js +2 -1
- package/dist/lib/runtime-start-verification.js +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +60 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +3 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +3 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +37 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -6
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +109 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +78 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +3 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +3 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +31 -6
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +40 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +10 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7330 -7324
- package/oclif.manifest.json +11 -2
- package/package.json +1 -1
|
@@ -242,6 +242,8 @@ async function loadRetiredDeks(profile, kek, aad, active) {
|
|
|
242
242
|
dek.fill(0);
|
|
243
243
|
}
|
|
244
244
|
} catch {
|
|
245
|
+
if (import_node_path.default.basename(target).startsWith(UNREADABLE_DEK_COPY_PREFIX))
|
|
246
|
+
continue;
|
|
245
247
|
emit("cred_envelope.retired_dek.unreadable", false, {
|
|
246
248
|
profile,
|
|
247
249
|
file: import_node_path.default.basename(target)
|
|
@@ -249,6 +251,40 @@ async function loadRetiredDeks(profile, kek, aad, active) {
|
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
253
|
}
|
|
254
|
+
const UNREADABLE_DEK_COPY_PREFIX = `${ACCOUNT_CRED_DEK_WRAPPED}${import_profileStateDir.RETIRED_COPY_SUFFIX}.unreadable-`;
|
|
255
|
+
const MAX_UNREADABLE_DEK_COPIES = 8;
|
|
256
|
+
function isKekUnavailableError(err) {
|
|
257
|
+
return err instanceof import_kek.KekUnavailableError || err?.name === "KekUnavailableError";
|
|
258
|
+
}
|
|
259
|
+
function keepUnreadableWrappedDek(profile, wrapped) {
|
|
260
|
+
try {
|
|
261
|
+
const dir = (0, import_profileStateDir.profileStateDir)(profile);
|
|
262
|
+
const existing = import_node_fs.default.existsSync(dir) ? import_node_fs.default.readdirSync(dir).filter((entry) => entry.startsWith(UNREADABLE_DEK_COPY_PREFIX)).sort() : [];
|
|
263
|
+
for (const entry of existing) {
|
|
264
|
+
try {
|
|
265
|
+
if (import_node_fs.default.readFileSync(import_node_path.default.join(dir, entry)).equals(wrapped))
|
|
266
|
+
return entry;
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const name = `${UNREADABLE_DEK_COPY_PREFIX}${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
271
|
+
atomicWrite0600(import_node_path.default.join(dir, name), wrapped);
|
|
272
|
+
const kept = [...existing, name].sort();
|
|
273
|
+
for (const stale of kept.slice(0, Math.max(0, kept.length - MAX_UNREADABLE_DEK_COPIES))) {
|
|
274
|
+
try {
|
|
275
|
+
import_node_fs.default.unlinkSync(import_node_path.default.join(dir, stale));
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return name;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
emit("cred_envelope.unreadable_dek_copy_failed", false, {
|
|
282
|
+
profile,
|
|
283
|
+
message: err?.message ?? "unknown"
|
|
284
|
+
});
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
252
288
|
function retiredDekFiles(profile) {
|
|
253
289
|
const dir = (0, import_profileStateDir.profileStateDir)(profile);
|
|
254
290
|
const prefix = `${ACCOUNT_CRED_DEK_WRAPPED}${import_profileStateDir.RETIRED_COPY_SUFFIX}.`;
|
|
@@ -466,6 +502,8 @@ async function initCredEnvelope(profile) {
|
|
|
466
502
|
}
|
|
467
503
|
let dek = null;
|
|
468
504
|
let unwrapAttemptedAndFailed = false;
|
|
505
|
+
let kekUnreadable = null;
|
|
506
|
+
let unreadableWrapped = null;
|
|
469
507
|
await withCredEnvelopeLock(profile, async () => {
|
|
470
508
|
let wrappedBytes = null;
|
|
471
509
|
try {
|
|
@@ -489,6 +527,7 @@ async function initCredEnvelope(profile) {
|
|
|
489
527
|
kekKind: captureKind()
|
|
490
528
|
});
|
|
491
529
|
unwrapAttemptedAndFailed = true;
|
|
530
|
+
unreadableWrapped = wrappedBytes;
|
|
492
531
|
} else {
|
|
493
532
|
dek = candidate;
|
|
494
533
|
}
|
|
@@ -499,15 +538,33 @@ async function initCredEnvelope(profile) {
|
|
|
499
538
|
kekKind: captureKind(),
|
|
500
539
|
message: err?.message ?? "unknown"
|
|
501
540
|
});
|
|
502
|
-
|
|
541
|
+
if (isKekUnavailableError(err)) {
|
|
542
|
+
kekUnreadable = err;
|
|
543
|
+
} else {
|
|
544
|
+
unwrapAttemptedAndFailed = true;
|
|
545
|
+
unreadableWrapped = wrappedBytes;
|
|
546
|
+
}
|
|
503
547
|
}
|
|
504
548
|
}
|
|
549
|
+
if (kekUnreadable !== null) {
|
|
550
|
+
_state = { kind: "disabled", reason: "kek_unavailable" };
|
|
551
|
+
emit("cred_envelope.kek_unavailable", false, {
|
|
552
|
+
kekId: kek.id,
|
|
553
|
+
profile,
|
|
554
|
+
phase: "unwrap",
|
|
555
|
+
describe: kek.describe(),
|
|
556
|
+
message: kekUnreadable?.message ?? "unknown"
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
505
560
|
if (unwrapAttemptedAndFailed && dek === null) {
|
|
506
561
|
_kekRotationDetected = true;
|
|
562
|
+
const keptAs = unreadableWrapped ? keepUnreadableWrappedDek(profile, unreadableWrapped) : null;
|
|
507
563
|
emit("cred_envelope.kek_rotated", false, {
|
|
508
564
|
kekId: kek.id,
|
|
509
565
|
kekKind: captureKind(),
|
|
510
566
|
profile,
|
|
567
|
+
...keptAs ? { previousWrappedDekKeptAs: keptAs } : {},
|
|
511
568
|
message: "wrapped DEK present but unwrap failed under available KEK; treating as rotation and re-enrolling"
|
|
512
569
|
});
|
|
513
570
|
}
|
|
@@ -11,6 +11,8 @@ type DeviceIdentityState = {
|
|
|
11
11
|
envelopeProtected: boolean;
|
|
12
12
|
};
|
|
13
13
|
type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key' | 'no_stored_key';
|
|
14
|
+
/** @internal Test seam — how often an unreadable key is looked at again. `null` restores 5 s. */
|
|
15
|
+
export declare function __setUnreadableReloadIntervalForTest(ms: number | null): void;
|
|
14
16
|
/** @internal Test seam — force-set state. */
|
|
15
17
|
export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
|
|
16
18
|
/** @internal Test seam — read state. */
|
|
@@ -34,6 +34,7 @@ __export(deviceIdentityBridge_exports, {
|
|
|
34
34
|
__privateKeyPathForTest: () => __privateKeyPathForTest,
|
|
35
35
|
__publicKeyPathForTest: () => __publicKeyPathForTest,
|
|
36
36
|
__setStateForTest: () => __setStateForTest,
|
|
37
|
+
__setUnreadableReloadIntervalForTest: () => __setUnreadableReloadIntervalForTest,
|
|
37
38
|
clearEnrollmentMarker: () => clearEnrollmentMarker,
|
|
38
39
|
describeDeviceIdentityState: () => describeDeviceIdentityState,
|
|
39
40
|
describeDeviceKeySplit: () => describeDeviceKeySplit,
|
|
@@ -65,6 +66,58 @@ const DEVICE_IDENTITY_FILES = [
|
|
|
65
66
|
const DAEMON_NOT_FOUND_BACKOFF_MS = 15 * 60 * 1e3;
|
|
66
67
|
let _state = { kind: "uninit" };
|
|
67
68
|
let _initializing = null;
|
|
69
|
+
let _unreadableSeen = null;
|
|
70
|
+
let _unreadableReloadIntervalMs = 5e3;
|
|
71
|
+
function __setUnreadableReloadIntervalForTest(ms) {
|
|
72
|
+
_unreadableReloadIntervalMs = ms ?? 5e3;
|
|
73
|
+
}
|
|
74
|
+
function keyFilesStamp(profile) {
|
|
75
|
+
const part = (file) => {
|
|
76
|
+
try {
|
|
77
|
+
const s = import_node_fs.default.statSync(file);
|
|
78
|
+
return `${s.mtimeMs}:${s.size}`;
|
|
79
|
+
} catch {
|
|
80
|
+
return "absent";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return `${part(publicKeyPath(profile))}|${part(privateKeyPath(profile))}`;
|
|
84
|
+
}
|
|
85
|
+
function reloadIfRecovered() {
|
|
86
|
+
if (_state.kind !== "disabled" || _state.reason !== "unreadable_key")
|
|
87
|
+
return;
|
|
88
|
+
const seen = _unreadableSeen;
|
|
89
|
+
if (!seen || _initializing)
|
|
90
|
+
return;
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
if (now - seen.checkedAt < _unreadableReloadIntervalMs)
|
|
93
|
+
return;
|
|
94
|
+
seen.checkedAt = now;
|
|
95
|
+
let stamp;
|
|
96
|
+
try {
|
|
97
|
+
stamp = keyFilesStamp(seen.profile);
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (stamp === seen.stamp)
|
|
102
|
+
return;
|
|
103
|
+
seen.stamp = stamp;
|
|
104
|
+
let keypair = null;
|
|
105
|
+
try {
|
|
106
|
+
keypair = loadKeypair(seen.profile);
|
|
107
|
+
} catch {
|
|
108
|
+
keypair = null;
|
|
109
|
+
}
|
|
110
|
+
if (!keypair)
|
|
111
|
+
return;
|
|
112
|
+
_state = {
|
|
113
|
+
kind: "active",
|
|
114
|
+
keypair,
|
|
115
|
+
profile: seen.profile,
|
|
116
|
+
envelopeProtected: (0, import_credentialEnvelopeBridge.isCredEnvelopeActive)()
|
|
117
|
+
};
|
|
118
|
+
_unreadableSeen = null;
|
|
119
|
+
emit("reloaded_after_recovery", true, { profile: seen.profile, thumbprint: keypair.thumbprint });
|
|
120
|
+
}
|
|
68
121
|
async function withDeviceIdentityLock(profile, body) {
|
|
69
122
|
const dir = (0, import_profileStateDir.profileStateDir)(profile);
|
|
70
123
|
import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
@@ -380,6 +433,7 @@ async function initDeviceIdentityLocked(profile, options = {}) {
|
|
|
380
433
|
const hasStoredKey = storedKeyMaterialExists(profile);
|
|
381
434
|
if (hasStoredKey && (!options.recoverUnreadable || independent)) {
|
|
382
435
|
_state = { kind: "disabled", reason: "unreadable_key" };
|
|
436
|
+
_unreadableSeen = independent ? null : { profile, stamp: keyFilesStamp(profile), checkedAt: Date.now() };
|
|
383
437
|
emit("recovery_required", false, { profile, reason: "stored_key_unreadable" });
|
|
384
438
|
return;
|
|
385
439
|
}
|
|
@@ -446,16 +500,20 @@ async function resetDeviceIdentity(opts) {
|
|
|
446
500
|
});
|
|
447
501
|
}
|
|
448
502
|
_state = { kind: "uninit" };
|
|
503
|
+
_unreadableSeen = null;
|
|
449
504
|
}
|
|
450
505
|
function isDeviceIdentityActive() {
|
|
506
|
+
reloadIfRecovered();
|
|
451
507
|
return _state.kind === "active";
|
|
452
508
|
}
|
|
453
509
|
function getDevicePublicKey() {
|
|
510
|
+
reloadIfRecovered();
|
|
454
511
|
if (_state.kind !== "active")
|
|
455
512
|
return null;
|
|
456
513
|
return { jwk: _state.keypair.publicJwk, thumbprint: _state.keypair.thumbprint };
|
|
457
514
|
}
|
|
458
515
|
function describeDeviceIdentityState() {
|
|
516
|
+
reloadIfRecovered();
|
|
459
517
|
if (_state.kind === "disabled") {
|
|
460
518
|
return { kind: "disabled", reason: _state.reason };
|
|
461
519
|
}
|
|
@@ -470,6 +528,7 @@ function describeDeviceIdentityState() {
|
|
|
470
528
|
return { kind: "uninit" };
|
|
471
529
|
}
|
|
472
530
|
function signServerRequest(opts) {
|
|
531
|
+
reloadIfRecovered();
|
|
473
532
|
if (_state.kind !== "active")
|
|
474
533
|
return null;
|
|
475
534
|
try {
|
|
@@ -676,6 +735,7 @@ async function enrollWithServer(opts) {
|
|
|
676
735
|
__privateKeyPathForTest,
|
|
677
736
|
__publicKeyPathForTest,
|
|
678
737
|
__setStateForTest,
|
|
738
|
+
__setUnreadableReloadIntervalForTest,
|
|
679
739
|
clearEnrollmentMarker,
|
|
680
740
|
describeDeviceIdentityState,
|
|
681
741
|
describeDeviceKeySplit,
|
|
@@ -25,11 +25,11 @@ export { loginWithLocalhost, LoginInitError, LoginExchangeError, type BrowserOpe
|
|
|
25
25
|
export { parseTtlDays, MIN_TTL_DAYS, MAX_TTL_DAYS, type ParseTtlResult, type ParseTtlSuccess, type ParseTtlError, } from './ttlParser.js';
|
|
26
26
|
export { formatRecoveryCode, parseRecoveryCode, normalizeRecoveryCode, isValidRecoveryCode, } from './recoveryCode.js';
|
|
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
|
-
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, LinuxKek, MacosKek, registerZeroizeHook, WindowsKek, zeroizeKekCaches, type KekStrategy, } from './kek/index.js';
|
|
28
|
+
export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, KekUnavailableError, isMasterKekKeychainService, LinuxKek, MacosKek, MASTER_KEK_KEYCHAIN_ACCOUNT, MASTER_KEK_KEYCHAIN_SERVICE, 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
30
|
export { DPOP_HEADER, DPOP_MAX_AGE_SECONDS, DPOP_PROOF_INVALID, DPOP_PROOF_REQUIRED, DPOP_PROOF_TYP, DPOP_PROOF_VERIFY_UNAVAILABLE, accessTokenHash, createDpopKey, isAth, isJwkThumbprint, normalizeDpopHtu, verifyDpopProof, type DpopKey, type DpopProofClaims, type DpopProofRequest, type DpopVerifyFailureReason, type DpopVerifyOptions, type DpopVerifyResult, } from './dpop.js';
|
|
31
31
|
export { initCredEnvelope, initExistingCredEnvelope, initNewIndependentCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
32
|
-
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
32
|
+
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setUnreadableReloadIntervalForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
33
33
|
export { getMachineUuid, getMachineUuidForProfile, describeMachineIdentity, type MachineIdentityDescription, type MachineIdentityReason, getHardwareMachineUuid, setMachineUuidProfile, getMachineUuidProfile, qualifyMachineUuid, MACHINE_IDENTITY_FILENAME, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
34
34
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
35
35
|
export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
|
|
@@ -66,6 +66,8 @@ __export(index_exports, {
|
|
|
66
66
|
LoginInitError: () => import_loginWithLocalhost.LoginInitError,
|
|
67
67
|
LoopbackTargetRefusedError: () => import_loopbackHttp.LoopbackTargetRefusedError,
|
|
68
68
|
MACHINE_IDENTITY_FILENAME: () => import_machineUuid.MACHINE_IDENTITY_FILENAME,
|
|
69
|
+
MASTER_KEK_KEYCHAIN_ACCOUNT: () => import_kek.MASTER_KEK_KEYCHAIN_ACCOUNT,
|
|
70
|
+
MASTER_KEK_KEYCHAIN_SERVICE: () => import_kek.MASTER_KEK_KEYCHAIN_SERVICE,
|
|
69
71
|
MAX_SIGS: () => import_releaseManifest.MAX_SIGS,
|
|
70
72
|
MAX_TTL_DAYS: () => import_ttlParser.MAX_TTL_DAYS,
|
|
71
73
|
MIN_TTL_DAYS: () => import_ttlParser.MIN_TTL_DAYS,
|
|
@@ -115,6 +117,7 @@ __export(index_exports, {
|
|
|
115
117
|
__setDeviceIdentityStateForTest: () => import_deviceIdentityBridge.__setStateForTest,
|
|
116
118
|
__setFdReaderForTest: () => import_fdToken.__setFdReaderForTest,
|
|
117
119
|
__setKekStrategyForTest: () => import_kek.__setKekStrategyForTest,
|
|
120
|
+
__setUnreadableReloadIntervalForTest: () => import_deviceIdentityBridge.__setUnreadableReloadIntervalForTest,
|
|
118
121
|
__wrappedDekFilePathForTest: () => import_credentialEnvelopeBridge.__wrappedDekFilePathForTest,
|
|
119
122
|
accessTokenHash: () => import_dpop.accessTokenHash,
|
|
120
123
|
assemblePairBundleBase64: () => import_daemonPair.assemblePairBundleBase64,
|
|
@@ -209,6 +212,7 @@ __export(index_exports, {
|
|
|
209
212
|
isJwkThumbprint: () => import_dpop.isJwkThumbprint,
|
|
210
213
|
isLoopbackAddress: () => import_loopbackHttp.isLoopbackAddress,
|
|
211
214
|
isLoopbackHostname: () => import_loopbackHttp.isLoopbackHostname,
|
|
215
|
+
isMasterKekKeychainService: () => import_kek.isMasterKekKeychainService,
|
|
212
216
|
isStaticTokenKind: () => import_credentialResolver.isStaticTokenKind,
|
|
213
217
|
isTrustedArtifactOrigin: () => import_releaseManifest.isTrustedArtifactOrigin,
|
|
214
218
|
isValidRecoveryCode: () => import_recoveryCode.isValidRecoveryCode,
|
|
@@ -370,6 +374,8 @@ var import_independentMachineReservation = require("./independentMachineReservat
|
|
|
370
374
|
LoginInitError,
|
|
371
375
|
LoopbackTargetRefusedError,
|
|
372
376
|
MACHINE_IDENTITY_FILENAME,
|
|
377
|
+
MASTER_KEK_KEYCHAIN_ACCOUNT,
|
|
378
|
+
MASTER_KEK_KEYCHAIN_SERVICE,
|
|
373
379
|
MAX_SIGS,
|
|
374
380
|
MAX_TTL_DAYS,
|
|
375
381
|
MIN_TTL_DAYS,
|
|
@@ -419,6 +425,7 @@ var import_independentMachineReservation = require("./independentMachineReservat
|
|
|
419
425
|
__setDeviceIdentityStateForTest,
|
|
420
426
|
__setFdReaderForTest,
|
|
421
427
|
__setKekStrategyForTest,
|
|
428
|
+
__setUnreadableReloadIntervalForTest,
|
|
422
429
|
__wrappedDekFilePathForTest,
|
|
423
430
|
accessTokenHash,
|
|
424
431
|
assemblePairBundleBase64,
|
|
@@ -513,6 +520,7 @@ var import_independentMachineReservation = require("./independentMachineReservat
|
|
|
513
520
|
isJwkThumbprint,
|
|
514
521
|
isLoopbackAddress,
|
|
515
522
|
isLoopbackHostname,
|
|
523
|
+
isMasterKekKeychainService,
|
|
516
524
|
isStaticTokenKind,
|
|
517
525
|
isTrustedArtifactOrigin,
|
|
518
526
|
isValidRecoveryCode,
|
|
@@ -19,6 +19,7 @@ export { MacosKek } from './macos.js';
|
|
|
19
19
|
export { LinuxKek } from './linux.js';
|
|
20
20
|
export { WindowsKek } from './windows.js';
|
|
21
21
|
export { zeroizeKekCaches, registerZeroizeHook } from './zeroize-registry.js';
|
|
22
|
+
export { isMasterKekKeychainService, MASTER_KEK_KEYCHAIN_ACCOUNT, MASTER_KEK_KEYCHAIN_SERVICE, } from './masterKekService.js';
|
|
22
23
|
/**
|
|
23
24
|
* @internal Test-only seam — inject a synthetic KEK strategy that wraps
|
|
24
25
|
* with a fixed in-memory key. Resets to platform default when called
|
|
@@ -31,11 +31,14 @@ __export(index_exports, {
|
|
|
31
31
|
InMemoryKek: () => InMemoryKek,
|
|
32
32
|
KekUnavailableError: () => import_types2.KekUnavailableError,
|
|
33
33
|
LinuxKek: () => import_linux2.LinuxKek,
|
|
34
|
+
MASTER_KEK_KEYCHAIN_ACCOUNT: () => import_masterKekService.MASTER_KEK_KEYCHAIN_ACCOUNT,
|
|
35
|
+
MASTER_KEK_KEYCHAIN_SERVICE: () => import_masterKekService.MASTER_KEK_KEYCHAIN_SERVICE,
|
|
34
36
|
MacosKek: () => import_macos2.MacosKek,
|
|
35
37
|
UnavailableKek: () => UnavailableKek,
|
|
36
38
|
WindowsKek: () => import_windows2.WindowsKek,
|
|
37
39
|
__setKekStrategyForTest: () => __setKekStrategyForTest,
|
|
38
40
|
getKekStrategy: () => getKekStrategy,
|
|
41
|
+
isMasterKekKeychainService: () => import_masterKekService.isMasterKekKeychainService,
|
|
39
42
|
registerZeroizeHook: () => import_zeroize_registry.registerZeroizeHook,
|
|
40
43
|
zeroizeKekCaches: () => import_zeroize_registry.zeroizeKekCaches
|
|
41
44
|
});
|
|
@@ -49,6 +52,7 @@ var import_macos2 = require("./macos.js");
|
|
|
49
52
|
var import_linux2 = require("./linux.js");
|
|
50
53
|
var import_windows2 = require("./windows.js");
|
|
51
54
|
var import_zeroize_registry = require("./zeroize-registry.js");
|
|
55
|
+
var import_masterKekService = require("./masterKekService.js");
|
|
52
56
|
class UnsupportedPlatformKek {
|
|
53
57
|
id;
|
|
54
58
|
platform;
|
|
@@ -148,11 +152,14 @@ class UnavailableKek {
|
|
|
148
152
|
InMemoryKek,
|
|
149
153
|
KekUnavailableError,
|
|
150
154
|
LinuxKek,
|
|
155
|
+
MASTER_KEK_KEYCHAIN_ACCOUNT,
|
|
156
|
+
MASTER_KEK_KEYCHAIN_SERVICE,
|
|
151
157
|
MacosKek,
|
|
152
158
|
UnavailableKek,
|
|
153
159
|
WindowsKek,
|
|
154
160
|
__setKekStrategyForTest,
|
|
155
161
|
getKekStrategy,
|
|
162
|
+
isMasterKekKeychainService,
|
|
156
163
|
registerZeroizeHook,
|
|
157
164
|
zeroizeKekCaches
|
|
158
165
|
});
|
|
@@ -42,10 +42,11 @@ var import_node_fs = __toESM(require("node:fs"), 1);
|
|
|
42
42
|
var import_credentialEnvelope = require("../credentialEnvelope.js");
|
|
43
43
|
var import_types = require("./types.js");
|
|
44
44
|
var import_zeroize_registry = require("./zeroize-registry.js");
|
|
45
|
+
var import_masterKekService = require("./masterKekService.js");
|
|
45
46
|
const KIND_LIBSECRET = "linux-libsecret-shellout";
|
|
46
47
|
const KIND_HKDF = "linux-machineid-hkdf";
|
|
47
|
-
const SS_SERVICE =
|
|
48
|
-
const SS_ACCOUNT =
|
|
48
|
+
const SS_SERVICE = import_masterKekService.MASTER_KEK_KEYCHAIN_SERVICE;
|
|
49
|
+
const SS_ACCOUNT = import_masterKekService.MASTER_KEK_KEYCHAIN_ACCOUNT;
|
|
49
50
|
const DBUS_TRANSPORT_ERRORS = [
|
|
50
51
|
"D-Bus",
|
|
51
52
|
"org.freedesktop.secrets",
|
|
@@ -46,9 +46,10 @@ var import_node_fs = __toESM(require("node:fs"), 1);
|
|
|
46
46
|
var import_credentialEnvelope = require("../credentialEnvelope.js");
|
|
47
47
|
var import_types = require("./types.js");
|
|
48
48
|
var import_zeroize_registry = require("./zeroize-registry.js");
|
|
49
|
+
var import_masterKekService = require("./masterKekService.js");
|
|
49
50
|
const KIND_SHELLOUT = "macos-keychain-shellout";
|
|
50
|
-
const KEYCHAIN_SERVICE =
|
|
51
|
-
const KEYCHAIN_ACCOUNT =
|
|
51
|
+
const KEYCHAIN_SERVICE = import_masterKekService.MASTER_KEK_KEYCHAIN_SERVICE;
|
|
52
|
+
const KEYCHAIN_ACCOUNT = import_masterKekService.MASTER_KEK_KEYCHAIN_ACCOUNT;
|
|
52
53
|
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
53
54
|
const execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
54
55
|
let _execFileForTest = null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kek/masterKekService.ts — the one place that names the machine-wide master KEK.
|
|
3
|
+
*
|
|
4
|
+
* The master KEK is ONE keychain item shared by every profile on the machine
|
|
5
|
+
* (`service=ai.skrr.daemon.kek`, `account=master-kek-v1`). Each profile's
|
|
6
|
+
* `cred-dek-wrapped` is sealed under it, and everything a profile stores — its
|
|
7
|
+
* tokens, its BYOK keys, its device private key — is sealed under that DEK.
|
|
8
|
+
* Deleting this one item therefore orphans every credential envelope on the
|
|
9
|
+
* machine at once, and nothing can bring it back: the key is random.
|
|
10
|
+
*
|
|
11
|
+
* It was deleted by accident on 2026-10-29. The desktop app's sign-out purge
|
|
12
|
+
* enumerates every service matching `ai.(skrr|oversky).daemon*` and drains each
|
|
13
|
+
* one, because that prefix used to hold only per-profile credential slots. The
|
|
14
|
+
* KEK moved under the same prefix without the purge being told, so a routine
|
|
15
|
+
* "your saved session was rejected" at app launch deleted it. The next daemon
|
|
16
|
+
* start could not open the DEK, treated that as a rotation, enrolled a new one,
|
|
17
|
+
* and the device private key sealed under the old DEK was gone for good — which
|
|
18
|
+
* the server then saw as `DEVICE_PROOF_MISSING` on every CLI hand-off.
|
|
19
|
+
*
|
|
20
|
+
* So anything that deletes keychain items by PREFIX or PATTERN asks
|
|
21
|
+
* `isMasterKekKeychainService` first. Deleting the KEK is a decision a caller
|
|
22
|
+
* makes by name, never a side effect of a sweep.
|
|
23
|
+
*/
|
|
24
|
+
/** Keychain `service` of the master KEK. */
|
|
25
|
+
export declare const MASTER_KEK_KEYCHAIN_SERVICE = "ai.skrr.daemon.kek";
|
|
26
|
+
/** Keychain `account` of the master KEK. */
|
|
27
|
+
export declare const MASTER_KEK_KEYCHAIN_ACCOUNT = "master-kek-v1";
|
|
28
|
+
/** Whether a keychain service name holds master-KEK material. */
|
|
29
|
+
export declare function isMasterKekKeychainService(service: string): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var masterKekService_exports = {};
|
|
20
|
+
__export(masterKekService_exports, {
|
|
21
|
+
MASTER_KEK_KEYCHAIN_ACCOUNT: () => MASTER_KEK_KEYCHAIN_ACCOUNT,
|
|
22
|
+
MASTER_KEK_KEYCHAIN_SERVICE: () => MASTER_KEK_KEYCHAIN_SERVICE,
|
|
23
|
+
isMasterKekKeychainService: () => isMasterKekKeychainService
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(masterKekService_exports);
|
|
26
|
+
const MASTER_KEK_KEYCHAIN_SERVICE = "ai.skrr.daemon.kek";
|
|
27
|
+
const MASTER_KEK_KEYCHAIN_ACCOUNT = "master-kek-v1";
|
|
28
|
+
const MASTER_KEK_SERVICE_PATTERN = /^ai\.(?:skrr|oversky)\.daemon\.kek(?:\..*)?$/;
|
|
29
|
+
function isMasterKekKeychainService(service) {
|
|
30
|
+
return typeof service === "string" && MASTER_KEK_SERVICE_PATTERN.test(service);
|
|
31
|
+
}
|
|
32
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
33
|
+
0 && (module.exports = {
|
|
34
|
+
MASTER_KEK_KEYCHAIN_ACCOUNT,
|
|
35
|
+
MASTER_KEK_KEYCHAIN_SERVICE,
|
|
36
|
+
isMasterKekKeychainService
|
|
37
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'UNKNOWN_401' | 'UNKNOWN';
|
|
1
|
+
export type AuthFailureReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'DEVICE_KEY_UNAVAILABLE' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'UNKNOWN_401' | 'UNKNOWN';
|
|
2
2
|
/**
|
|
3
3
|
* The command that recovers this process from a re-auth latch.
|
|
4
4
|
*
|
|
@@ -30,6 +30,7 @@ const REASON_HUMAN = {
|
|
|
30
30
|
REFRESH_EXPIRED: "Your session has expired.",
|
|
31
31
|
SESSION_REVOKED: "Your session was ended (you signed out elsewhere or your password changed).",
|
|
32
32
|
DEVICE_KEY_MISMATCH: "This computer's device key no longer matches the one skrr registered for it, so its session cannot be renewed.",
|
|
33
|
+
DEVICE_KEY_UNAVAILABLE: "This computer's device key could not be read, so skrr cannot prove which device is asking and its session cannot be renewed. Signing in again creates a new one.",
|
|
33
34
|
TOKEN_EXPIRED: "Your access token has expired.",
|
|
34
35
|
TOKEN_INVALID: "Your saved credentials are no longer valid.",
|
|
35
36
|
UNKNOWN_401: "skrr needs you to sign in again.",
|
|
@@ -282,7 +282,9 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
282
282
|
if (options.machineUuid) {
|
|
283
283
|
body.machineUuid = options.machineUuid;
|
|
284
284
|
}
|
|
285
|
+
let proofAttached = false;
|
|
285
286
|
function buildRequestInit() {
|
|
287
|
+
proofAttached = false;
|
|
286
288
|
const headers = { "Content-Type": "application/json" };
|
|
287
289
|
if (options.origin) {
|
|
288
290
|
headers["X-Oversky-Origin"] = options.origin;
|
|
@@ -298,6 +300,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
298
300
|
__refreshDiagnostics.proofAttemptCount += 1;
|
|
299
301
|
if (typeof proof === "string" && proof.length > 0) {
|
|
300
302
|
headers["OverSky-DPoP"] = proof;
|
|
303
|
+
proofAttached = true;
|
|
301
304
|
}
|
|
302
305
|
} catch (err) {
|
|
303
306
|
__refreshDiagnostics.proofAttemptCount += 1;
|
|
@@ -426,10 +429,13 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
426
429
|
});
|
|
427
430
|
throw new import_types.PermanentAuthFailure(code, parsedBody.message || code, revocationReason);
|
|
428
431
|
}
|
|
432
|
+
const noLocalProof = parsedBody.code === "DEVICE_PROOF_MISSING" && !proofAttached;
|
|
433
|
+
const latchReason = noLocalProof ? "DEVICE_KEY_UNAVAILABLE" : "UNKNOWN_401";
|
|
429
434
|
const message = parsedBody.message || "Unknown auth failure";
|
|
435
|
+
const latchMessage = noLocalProof ? `This computer has no usable device key, so the refresh carried no device proof (server: ${message})` : message;
|
|
430
436
|
const { exhausted, count } = recordUnknown401(daemonId, Date.now());
|
|
431
437
|
if (!exhausted) {
|
|
432
|
-
log.warn(`[auth/refresh] unknown-shape 401 (${count}/${UNKNOWN_401_BUDGET}) \u2014 treating as transient, will retry: ${message}`);
|
|
438
|
+
log.warn(`[auth/refresh] unknown-shape 401 (${count}/${UNKNOWN_401_BUDGET}) \u2014 treating as transient, will retry: ${message}` + (noLocalProof ? " (no device proof was sent: this machine has no usable device key)" : ""));
|
|
433
439
|
(0, import_runtime.emitAuthTelemetry)({
|
|
434
440
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
435
441
|
eventType: "auth",
|
|
@@ -448,7 +454,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
448
454
|
});
|
|
449
455
|
throw new import_types.TransientAuthFailure(`Unknown auth failure (transient, retrying): ${message}`);
|
|
450
456
|
}
|
|
451
|
-
log.warn(`[auth/refresh] unknown-shape 401 budget exhausted (${count}/${UNKNOWN_401_BUDGET}) \u2014 latching permanent: ${message}`);
|
|
457
|
+
log.warn(`[auth/refresh] unknown-shape 401 budget exhausted (${count}/${UNKNOWN_401_BUDGET}) \u2014 latching permanent ${latchReason}: ${message}`);
|
|
452
458
|
(0, import_runtime.emitAuthTelemetry)({
|
|
453
459
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
454
460
|
eventType: "auth",
|
|
@@ -457,18 +463,18 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
457
463
|
event: "refresh_failure",
|
|
458
464
|
daemonId,
|
|
459
465
|
durationMs: Date.now() - startMs,
|
|
460
|
-
errorCode:
|
|
466
|
+
errorCode: latchReason,
|
|
461
467
|
statusCode: 401,
|
|
462
468
|
transient: false,
|
|
463
469
|
unknown401Streak: count
|
|
464
470
|
}
|
|
465
471
|
});
|
|
466
472
|
persistReauthState({
|
|
467
|
-
code:
|
|
468
|
-
message,
|
|
473
|
+
code: latchReason,
|
|
474
|
+
message: latchMessage,
|
|
469
475
|
at: Date.now()
|
|
470
476
|
});
|
|
471
|
-
throw new import_types.PermanentAuthFailure(
|
|
477
|
+
throw new import_types.PermanentAuthFailure(latchReason, latchMessage);
|
|
472
478
|
}
|
|
473
479
|
let bodyText = "";
|
|
474
480
|
try {
|
|
@@ -61,6 +61,9 @@ export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
|
|
|
61
61
|
export declare const QR_SIGN_IN_REDACTION: "[redacted]";
|
|
62
62
|
/** Fields are protocol-specific; preserve ordinary request ids, error codes and metrics. */
|
|
63
63
|
export declare function isQrSignInSecretField(key: string): boolean;
|
|
64
|
+
export declare function isOAuthQuerySecretField(key: string): boolean;
|
|
65
|
+
export declare function isOAuthQueryContainerField(key: string): boolean;
|
|
66
|
+
export declare function isGraphHandoffCookieField(key: string): boolean;
|
|
64
67
|
/**
|
|
65
68
|
* Replace every credential that travels in a URL with a marker: a Hosted Machine preview token with
|
|
66
69
|
* {@link PREVIEW_TOKEN_REDACTION}, and a workspace invitation, platform invitation, member link or operator code with
|
|
@@ -20,6 +20,9 @@ var telemetryRedaction_exports = {};
|
|
|
20
20
|
__export(telemetryRedaction_exports, {
|
|
21
21
|
PREVIEW_TOKEN_REDACTION: () => PREVIEW_TOKEN_REDACTION,
|
|
22
22
|
QR_SIGN_IN_REDACTION: () => QR_SIGN_IN_REDACTION,
|
|
23
|
+
isGraphHandoffCookieField: () => isGraphHandoffCookieField,
|
|
24
|
+
isOAuthQueryContainerField: () => isOAuthQueryContainerField,
|
|
25
|
+
isOAuthQuerySecretField: () => isOAuthQuerySecretField,
|
|
23
26
|
isQrSignInSecretField: () => isQrSignInSecretField,
|
|
24
27
|
redactPreviewTokens: () => redactPreviewTokens,
|
|
25
28
|
redactPreviewTokensDeep: () => redactPreviewTokensDeep,
|
|
@@ -53,6 +56,18 @@ function isQrSignInSecretField(key) {
|
|
|
53
56
|
return QR_SECRET_FIELDS.has(leaf.replace(/[_-]/g, "").toLowerCase());
|
|
54
57
|
}
|
|
55
58
|
const QR_SECRET_ASSIGNMENT = /((?:["']?)(?:statusToken|claimToken|codeVerifier|codeChallenge|comparisonCode|status_token|claim_token|code_verifier|code_challenge|comparison_code)(?:["']?)\s*[:=]\s*["']?)([A-Za-z0-9._~%+-]+)/gi;
|
|
59
|
+
const OAUTH_QUERY_SECRET = /(^|[?&]|%(?:25)*26)(code|state|error_description|handoff)(=|%(?:25)*3[dD])([^&#\s"'<>]*)/gi;
|
|
60
|
+
function isOAuthQuerySecretField(key) {
|
|
61
|
+
return ["code", "state", "error_description", "handoff"].includes(key.toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
function isOAuthQueryContainerField(key) {
|
|
64
|
+
const leaf = (key.split(".").pop() || "").replace(/[_-]/g, "").toLowerCase();
|
|
65
|
+
return leaf === "query" || leaf === "querystring";
|
|
66
|
+
}
|
|
67
|
+
function isGraphHandoffCookieField(key) {
|
|
68
|
+
return key.toLowerCase() === "__secure-pc-graph-handoff";
|
|
69
|
+
}
|
|
70
|
+
const GRAPH_HANDOFF_COOKIE = /((?:_|%(?:25)*5[fF]){2}secure(?:-|%(?:25)*2[dD])pc(?:-|%(?:25)*2[dD])graph(?:-|%(?:25)*2[dD])handoff(?:=|%(?:25)*3[dD]))([^;,&\s"'<>]+)/gi;
|
|
56
71
|
function redactQrSignInText(value) {
|
|
57
72
|
const standaloneQuery = /(?:^|[?&])direction=(?:mobile_approves_web|web_approves_mobile)(?:&|$)/.test(value) ? value.replace(QR_REQUEST_QUERY, `$1$2${QR_SIGN_IN_REDACTION}`) : value;
|
|
58
73
|
return standaloneQuery.replace(QR_REQUEST_PATH, `$1${QR_SIGN_IN_REDACTION}`).replace(QR_LINK_QUERY, (_match, prefix, query) => prefix + query.replace(QR_REQUEST_QUERY, `$1$2${QR_SIGN_IN_REDACTION}`)).replace(QR_SECRET_ASSIGNMENT, `$1${QR_SIGN_IN_REDACTION}`);
|
|
@@ -62,7 +77,7 @@ const credentialMarker = (_match, prefix) => `${prefix}_[redacted]`;
|
|
|
62
77
|
function redactPreviewTokens(value) {
|
|
63
78
|
if (typeof value !== "string")
|
|
64
79
|
return value;
|
|
65
|
-
const qrSafe = redactQrSignInText(value);
|
|
80
|
+
const qrSafe = redactQrSignInText(value).replace(OAUTH_QUERY_SECRET, "$1$2$3[redacted]").replace(GRAPH_HANDOFF_COOKIE, "$1[redacted]");
|
|
66
81
|
if (!CREDENTIAL_HINTS.some((hint) => qrSafe.indexOf(hint) !== -1))
|
|
67
82
|
return qrSafe;
|
|
68
83
|
return qrSafe.replace(PREVIEW_TOKEN, PREVIEW_TOKEN_REDACTION).replace(SIGNED_CREDENTIAL, credentialMarker).replace(LEGACY_OPERATOR_CODE, credentialMarker);
|
|
@@ -81,7 +96,7 @@ function redactPreviewTokensDeep(root, limits = {}) {
|
|
|
81
96
|
let visited = 0;
|
|
82
97
|
let complete = true;
|
|
83
98
|
const rewrite = (holder, key, current) => {
|
|
84
|
-
const next = isQrSignInSecretField(key) ? QR_SIGN_IN_REDACTION : redactPreviewTokens(current);
|
|
99
|
+
const next = isQrSignInSecretField(key) || isGraphHandoffCookieField(key) ? QR_SIGN_IN_REDACTION : redactPreviewTokens(current);
|
|
85
100
|
if (next === current) {
|
|
86
101
|
return;
|
|
87
102
|
}
|
|
@@ -119,7 +134,27 @@ function redactPreviewTokensDeep(root, limits = {}) {
|
|
|
119
134
|
complete = false;
|
|
120
135
|
continue;
|
|
121
136
|
}
|
|
122
|
-
if (
|
|
137
|
+
if (isOAuthQueryContainerField(key) && Array.isArray(child)) {
|
|
138
|
+
try {
|
|
139
|
+
holder[key] = "[redacted]";
|
|
140
|
+
} catch {
|
|
141
|
+
complete = false;
|
|
142
|
+
}
|
|
143
|
+
if (holder[key] !== "[redacted]")
|
|
144
|
+
complete = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (isOAuthQueryContainerField(key) && child && typeof child === "object" && !Array.isArray(child)) {
|
|
148
|
+
const query = child;
|
|
149
|
+
for (const queryKey of Object.keys(query)) {
|
|
150
|
+
if (isOAuthQuerySecretField(queryKey)) {
|
|
151
|
+
query[queryKey] = "[redacted]";
|
|
152
|
+
if (query[queryKey] !== "[redacted]")
|
|
153
|
+
complete = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (isQrSignInSecretField(key) || isGraphHandoffCookieField(key) || typeof child === "string") {
|
|
123
158
|
rewrite(holder, key, child);
|
|
124
159
|
} else if (child !== null && typeof child === "object") {
|
|
125
160
|
if (depth + 1 > maxDepth) {
|
|
@@ -166,6 +201,9 @@ function withPreviewTokenRedaction(options) {
|
|
|
166
201
|
0 && (module.exports = {
|
|
167
202
|
PREVIEW_TOKEN_REDACTION,
|
|
168
203
|
QR_SIGN_IN_REDACTION,
|
|
204
|
+
isGraphHandoffCookieField,
|
|
205
|
+
isOAuthQueryContainerField,
|
|
206
|
+
isOAuthQuerySecretField,
|
|
169
207
|
isQrSignInSecretField,
|
|
170
208
|
redactPreviewTokens,
|
|
171
209
|
redactPreviewTokensDeep,
|
|
@@ -67,6 +67,12 @@ export interface TokenPair {
|
|
|
67
67
|
* (server code `DEVICE_PROOF_THUMBPRINT_MISMATCH`).
|
|
68
68
|
* The daemon signs every retry with the same key, so
|
|
69
69
|
* retrying can never succeed.
|
|
70
|
+
* - DEVICE_KEY_UNAVAILABLE — the server demanded a device proof
|
|
71
|
+
* (`DEVICE_PROOF_MISSING`) and this process had none
|
|
72
|
+
* to send: the key is unreadable, never created, or no
|
|
73
|
+
* signer is wired. Unlike `DEVICE_KEY_MISMATCH` there is
|
|
74
|
+
* no second key to reconcile; the fix is an interactive
|
|
75
|
+
* daemon sign-in, which mints and re-pins a new one.
|
|
70
76
|
* - UNKNOWN_401 — server returned 401 with a code we don't
|
|
71
77
|
* recognize. Treated as permanent (better to prompt
|
|
72
78
|
* re-auth than to loop forever) but tagged
|
|
@@ -79,7 +85,7 @@ export interface TokenPair {
|
|
|
79
85
|
* Without this, the client `describeAuthFailure` exhaustiveness check
|
|
80
86
|
* was silently broken for the non-REFRESH branches.
|
|
81
87
|
*/
|
|
82
|
-
export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
|
|
88
|
+
export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'DEVICE_KEY_UNAVAILABLE' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
|
|
83
89
|
export declare class PermanentAuthFailure extends Error {
|
|
84
90
|
readonly code: PermanentAuthReason;
|
|
85
91
|
/**
|