@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.
Files changed (54) hide show
  1. package/dist/commands/agents/chat.d.ts +7 -0
  2. package/dist/commands/agents/chat.js +32 -6
  3. package/dist/commands/followups/act.js +3 -0
  4. package/dist/commands/followups/checkin.js +3 -0
  5. package/dist/commands/followups/remind.js +3 -0
  6. package/dist/commands/followups/show.js +1 -1
  7. package/dist/commands/followups/watch.js +3 -0
  8. package/dist/lib/agentic-stream.d.ts +46 -0
  9. package/dist/lib/agentic-stream.js +135 -14
  10. package/dist/lib/auth-failure.js +12 -0
  11. package/dist/lib/exec-runtime-binary.d.ts +9 -0
  12. package/dist/lib/exec-runtime-binary.js +15 -1
  13. package/dist/lib/followups.d.ts +13 -0
  14. package/dist/lib/followups.js +8 -0
  15. package/dist/lib/profile-machine-identity-guard.js +2 -1
  16. package/dist/lib/runtime-start-verification.js +6 -1
  17. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +58 -1
  18. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +2 -0
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +60 -0
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -0
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -0
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +7 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +3 -2
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +3 -2
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +29 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +37 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +1 -1
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +1 -0
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -6
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +3 -0
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +41 -3
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +7 -1
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +109 -3
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +2 -0
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +78 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +3 -2
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +3 -2
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +29 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +38 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +1 -1
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +1 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +31 -6
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +3 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +40 -3
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +7 -1
  51. package/dist/node_modules/@skrr-ai/auth-core/package.json +10 -0
  52. package/dist/node_modules/@skrr-ai/data-provider/index.js +7330 -7324
  53. package/oclif.manifest.json +11 -2
  54. 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
- unwrapAttemptedAndFailed = true;
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 = "ai.skrr.daemon.kek";
48
- const SS_ACCOUNT = "master-kek-v1";
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 = "ai.skrr.daemon.kek";
51
- const KEYCHAIN_ACCOUNT = "master-kek-v1";
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: "UNKNOWN_401",
466
+ errorCode: latchReason,
461
467
  statusCode: 401,
462
468
  transient: false,
463
469
  unknown401Streak: count
464
470
  }
465
471
  });
466
472
  persistReauthState({
467
- code: "UNKNOWN_401",
468
- message,
473
+ code: latchReason,
474
+ message: latchMessage,
469
475
  at: Date.now()
470
476
  });
471
- throw new import_types.PermanentAuthFailure("UNKNOWN_401", message);
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 (isQrSignInSecretField(key) || typeof child === "string") {
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
  /**