@skrr-ai/cli 0.1.122 → 0.1.124

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 (111) 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/agents/delete.d.ts +8 -0
  4. package/dist/commands/agents/delete.js +16 -0
  5. package/dist/commands/context.js +7 -0
  6. package/dist/commands/followups/act.js +3 -0
  7. package/dist/commands/followups/checkin.js +3 -0
  8. package/dist/commands/followups/remind.js +3 -0
  9. package/dist/commands/followups/show.js +1 -1
  10. package/dist/commands/followups/watch.js +3 -0
  11. package/dist/commands/search/workspace.js +1 -1
  12. package/dist/commands/store/gallery-admissions/approve.js +1 -1
  13. package/dist/commands/store/gallery-admissions/preview.js +1 -1
  14. package/dist/commands/store/gallery-admissions/refuse.js +1 -1
  15. package/dist/commands/store/gallery-admissions/show.js +1 -1
  16. package/dist/lib/agentic-stream.d.ts +46 -0
  17. package/dist/lib/agentic-stream.js +135 -14
  18. package/dist/lib/auth-core-init.d.ts +15 -2
  19. package/dist/lib/auth-core-init.js +47 -6
  20. package/dist/lib/auth-failure.js +12 -0
  21. package/dist/lib/command-hygiene.js +30 -1
  22. package/dist/lib/daemonBrokerRefusal.js +5 -1
  23. package/dist/lib/exec-runtime-binary.d.ts +9 -0
  24. package/dist/lib/exec-runtime-binary.js +15 -1
  25. package/dist/lib/followups.d.ts +13 -0
  26. package/dist/lib/followups.js +8 -0
  27. package/dist/lib/keychain.js +41 -4
  28. package/dist/lib/profile-machine-identity-guard.js +2 -1
  29. package/dist/lib/runtime-start-verification.js +6 -1
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +69 -27
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +135 -4
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +440 -83
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +110 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +13 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +11 -2
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +14 -2
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +69 -0
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +67 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +1 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +1 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +24 -16
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +3 -0
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +41 -3
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +7 -1
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +122 -38
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +135 -4
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +603 -90
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +14 -4
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -0
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +18 -2
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +23 -2
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +69 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +134 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +1 -1
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +1 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +45 -16
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +3 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +40 -3
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +7 -1
  108. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  109. package/dist/node_modules/@skrr-ai/data-provider/index.js +7349 -7325
  110. package/oclif.manifest.json +11 -2
  111. package/package.json +4 -1
@@ -25,7 +25,9 @@ import fs from 'node:fs';
25
25
  import path from 'node:path';
26
26
  import lockfile from 'proper-lockfile';
27
27
  import { getAuthLogger, getAuthConfigDir, emitAuthTelemetry, notifyResponseObserver, } from './runtime.js';
28
+ import { rethrowIfLiveStateRefusal } from './liveState.js';
28
29
  import { permanentReasonForRefreshCode } from './refreshClassification.js';
30
+ import { ensureStateDir, removeStateFile, writeStateFile } from './stateFiles.js';
29
31
  import { PermanentAuthFailure, TransientAuthFailure, } from './types.js';
30
32
  /**
31
33
  * P1-6: Reduced from 30 s to 5 s.
@@ -254,9 +256,9 @@ export async function withAuthLock(body, daemonId) {
254
256
  const lockPath = authLockPath();
255
257
  const dir = path.dirname(lockPath);
256
258
  if (!fs.existsSync(dir))
257
- fs.mkdirSync(dir, { recursive: true });
259
+ ensureStateDir(dir, 'create the auth lock directory', null);
258
260
  if (!fs.existsSync(lockPath)) {
259
- fs.writeFileSync(lockPath, '', { mode: 0o600 });
261
+ writeStateFile(lockPath, '', { mode: 0o600 }, 'create the auth lock');
260
262
  }
261
263
  const acquireStart = Date.now();
262
264
  emitAuthTelemetry({
@@ -611,7 +613,12 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
611
613
  // "proof-per-attempt invariant: a retry loop must call buildRequestInit()
612
614
  // per attempt" in `refresh.deviceProof.test.ts` asserts the function
613
615
  // is defined inside `doRefresh` and will fail if it is moved.
614
- function buildRequestInit() {
616
+ // Whether the request the server just answered carried a device proof. A
617
+ // `DEVICE_PROOF_MISSING` is only a LOCAL fact (no key to sign with) when we
618
+ // know none went out; if one did, something between us stripped it.
619
+ let proofAttached = false;
620
+ async function buildRequestInit() {
621
+ proofAttached = false;
615
622
  // Phase 3-R5: the stable `X-Oversky-Origin` header is the server's
616
623
  // preferred identity signal on the benign-grace rotation match. Newer
617
624
  // servers prefer it over the raw User-Agent; older servers ignore
@@ -633,7 +640,9 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
633
640
  __refreshDiagnostics.requestAttemptCount += 1;
634
641
  if (options.signRequestProof) {
635
642
  try {
636
- const proof = options.signRequestProof({
643
+ // Awaited: a hardware key signs in a helper process. A synchronous
644
+ // signer (a test double, an older caller) still works unchanged.
645
+ const proof = await options.signRequestProof({
637
646
  method: 'POST',
638
647
  url,
639
648
  refreshToken: currentRefreshToken,
@@ -646,6 +655,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
646
655
  __refreshDiagnostics.proofAttemptCount += 1;
647
656
  if (typeof proof === 'string' && proof.length > 0) {
648
657
  headers['OverSky-DPoP'] = proof;
658
+ proofAttached = true;
649
659
  }
650
660
  }
651
661
  catch (err) {
@@ -669,7 +679,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
669
679
  // Single attempt today. The builder structure is the load-bearing
670
680
  // bit: any future retry must call `buildRequestInit()` again so
671
681
  // the per-attempt invariant — fresh jti per request — holds.
672
- response = await fetch(url, buildRequestInit());
682
+ response = await fetch(url, await buildRequestInit());
673
683
  notifyResponseObserver(response);
674
684
  }
675
685
  catch (err) {
@@ -867,10 +877,29 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
867
877
  // the FINAL fallback. The distinct `UNKNOWN_401` telemetry tag is kept
868
878
  // on BOTH branches so monitoring can still see a server-side bug
869
879
  // separately from a real expiration (and watch the retry-vs-latch split).
880
+ //
881
+ // One unknown-shape 401 IS diagnosable: the server demanded a device proof
882
+ // (`DEVICE_PROOF_MISSING`) and this request carried none. That is a fact
883
+ // about this machine — an unreadable, never-created or unwired device key —
884
+ // and it never self-heals, but it stays on the same budget (a proxy that
885
+ // strips the header, or an identity still initialising, reads the same on
886
+ // one attempt). Only what the exhausted latch SAYS differs, so a person is
887
+ // sent to the sign-in that mints a key rather than told "sign in again".
888
+ // If a proof DID go out, the header was lost on the way: that stays generic.
889
+ const noLocalProof = parsedBody.code === 'DEVICE_PROOF_MISSING' && !proofAttached;
890
+ const latchReason = noLocalProof
891
+ ? 'DEVICE_KEY_UNAVAILABLE'
892
+ : 'UNKNOWN_401';
870
893
  const message = parsedBody.message || 'Unknown auth failure';
894
+ const latchMessage = noLocalProof
895
+ ? `This computer has no usable device key, so the refresh carried no device proof (server: ${message})`
896
+ : message;
871
897
  const { exhausted, count } = recordUnknown401(daemonId, Date.now());
872
898
  if (!exhausted) {
873
- log.warn(`[auth/refresh] unknown-shape 401 (${count}/${UNKNOWN_401_BUDGET}) — treating as transient, will retry: ${message}`);
899
+ log.warn(`[auth/refresh] unknown-shape 401 (${count}/${UNKNOWN_401_BUDGET}) — treating as transient, will retry: ${message}` +
900
+ (noLocalProof
901
+ ? ' (no device proof was sent: this machine has no usable device key)'
902
+ : ''));
874
903
  emitAuthTelemetry({
875
904
  timestamp: new Date().toISOString(),
876
905
  eventType: 'auth',
@@ -893,7 +922,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
893
922
  // permanent behavior: latch + persist the reauth flag so the daemon
894
923
  // surfaces the banner and the interactive modal becomes the final
895
924
  // recovery path.
896
- log.warn(`[auth/refresh] unknown-shape 401 budget exhausted (${count}/${UNKNOWN_401_BUDGET}) — latching permanent: ${message}`);
925
+ log.warn(`[auth/refresh] unknown-shape 401 budget exhausted (${count}/${UNKNOWN_401_BUDGET}) — latching permanent ${latchReason}: ${message}`);
897
926
  emitAuthTelemetry({
898
927
  timestamp: new Date().toISOString(),
899
928
  eventType: 'auth',
@@ -902,18 +931,18 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
902
931
  event: 'refresh_failure',
903
932
  daemonId,
904
933
  durationMs: Date.now() - startMs,
905
- errorCode: 'UNKNOWN_401',
934
+ errorCode: latchReason,
906
935
  statusCode: 401,
907
936
  transient: false,
908
937
  unknown401Streak: count,
909
938
  },
910
939
  });
911
940
  persistReauthState({
912
- code: 'UNKNOWN_401',
913
- message,
941
+ code: latchReason,
942
+ message: latchMessage,
914
943
  at: Date.now(),
915
944
  });
916
- throw new PermanentAuthFailure('UNKNOWN_401', message);
945
+ throw new PermanentAuthFailure(latchReason, latchMessage);
917
946
  }
918
947
  // 5xx / unexpected — transient. Caller will back off.
919
948
  // RESET SEMANTICS (Finding 4 follow-up): a 5xx is transient and does NOT
@@ -947,12 +976,11 @@ export function persistReauthState(state) {
947
976
  try {
948
977
  const dir = path.dirname(flagPath);
949
978
  if (!fs.existsSync(dir))
950
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
951
- fs.writeFileSync(flagPath, JSON.stringify(state, null, 2) + '\n', {
952
- mode: 0o600,
953
- });
979
+ ensureStateDir(dir, 'create the re-auth latch directory');
980
+ writeStateFile(flagPath, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 }, 'write the re-auth latch');
954
981
  }
955
982
  catch (err) {
983
+ rethrowIfLiveStateRefusal(err);
956
984
  log.debug(`[auth/refresh] failed to persist reauth flag: ${err?.message}`);
957
985
  }
958
986
  }
@@ -984,9 +1012,10 @@ export function clearReauthState() {
984
1012
  try {
985
1013
  const flagPath = reauthFlagPath();
986
1014
  if (fs.existsSync(flagPath))
987
- fs.unlinkSync(flagPath);
1015
+ removeStateFile(flagPath, 'clear the re-auth latch');
988
1016
  }
989
1017
  catch (err) {
1018
+ rethrowIfLiveStateRefusal(err);
990
1019
  log.debug(`[auth/refresh] clearReauthState: ${err?.message}`);
991
1020
  }
992
1021
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * stateFiles.ts — the ONE place auth-core mutates credential state on disk.
3
+ *
4
+ * Every write, delete, rename and mkdir of a wrapped DEK, a device key, an enrollment marker, a
5
+ * lock sentinel, a re-auth latch or a machine identity goes through a function here, and each
6
+ * asks `assertMayMutateLiveState` first: a test process aimed at the account's real home is
7
+ * refused with a named error before a byte changes (`liveState.ts` says why that cannot be left
8
+ * to each runner).
9
+ *
10
+ * This is the "own a control surface, or do not touch it" rule applied to the real home. The
11
+ * atomic writer used to exist three times (device identity, credential envelope, profile
12
+ * consolidation), so a guard added to one left two doors open. `stateFileBoundary.spec.ts`
13
+ * fails when a file other than this one starts calling a raw `fs` mutator, so a fourth copy
14
+ * cannot appear unguarded.
15
+ *
16
+ * Outside a test process every function here is exactly the `fs` call it wraps.
17
+ */
18
+ import fs from 'node:fs';
19
+ /** `mkdir -p` for a state directory, 0700 unless `mode` says otherwise. */
20
+ export declare function ensureStateDir(dir: string, what: string, mode?: number | null): void;
21
+ /**
22
+ * Atomic 0600 write — tmp+fsync+rename+dir-fsync. The same shape the daemon and CLI always used,
23
+ * so a partial-write race between `skrr` and `skrrd` produces the same on-disk transitions.
24
+ */
25
+ export declare function atomicWrite0600(target: string, value: Buffer | string, what: string): void;
26
+ /** `fs.writeFileSync` for a state file: lock sentinels, a re-auth latch, a machine identity. */
27
+ export declare function writeStateFile(target: string, data: string | NodeJS.ArrayBufferView, options: fs.WriteFileOptions, what: string): void;
28
+ /** `fs.unlinkSync` for a state file. Throws what `unlinkSync` throws, ENOENT included. */
29
+ export declare function removeStateFile(target: string, what: string): void;
30
+ /** `fs.renameSync` between state paths; both ends are checked. */
31
+ export declare function renameStateFile(from: string, to: string, what: string): void;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * stateFiles.ts — the ONE place auth-core mutates credential state on disk.
3
+ *
4
+ * Every write, delete, rename and mkdir of a wrapped DEK, a device key, an enrollment marker, a
5
+ * lock sentinel, a re-auth latch or a machine identity goes through a function here, and each
6
+ * asks `assertMayMutateLiveState` first: a test process aimed at the account's real home is
7
+ * refused with a named error before a byte changes (`liveState.ts` says why that cannot be left
8
+ * to each runner).
9
+ *
10
+ * This is the "own a control surface, or do not touch it" rule applied to the real home. The
11
+ * atomic writer used to exist three times (device identity, credential envelope, profile
12
+ * consolidation), so a guard added to one left two doors open. `stateFileBoundary.spec.ts`
13
+ * fails when a file other than this one starts calling a raw `fs` mutator, so a fourth copy
14
+ * cannot appear unguarded.
15
+ *
16
+ * Outside a test process every function here is exactly the `fs` call it wraps.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { assertMayMutateLiveState } from './liveState.js';
21
+ /** `mkdir -p` for a state directory, 0700 unless `mode` says otherwise. */
22
+ export function ensureStateDir(dir, what, mode = 0o700) {
23
+ assertMayMutateLiveState(dir, what);
24
+ fs.mkdirSync(dir, mode === null ? { recursive: true } : { recursive: true, mode });
25
+ }
26
+ /**
27
+ * Atomic 0600 write — tmp+fsync+rename+dir-fsync. The same shape the daemon and CLI always used,
28
+ * so a partial-write race between `skrr` and `skrrd` produces the same on-disk transitions.
29
+ */
30
+ export function atomicWrite0600(target, value, what) {
31
+ assertMayMutateLiveState(target, what);
32
+ const dir = path.dirname(target);
33
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
34
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
35
+ const buf = typeof value === 'string' ? Buffer.from(value, 'utf-8') : value;
36
+ const fd = fs.openSync(tmp, 'w', 0o600);
37
+ try {
38
+ fs.writeSync(fd, buf);
39
+ fs.fsyncSync(fd);
40
+ }
41
+ finally {
42
+ fs.closeSync(fd);
43
+ }
44
+ fs.renameSync(tmp, target);
45
+ try {
46
+ const dirFd = fs.openSync(dir, 'r');
47
+ try {
48
+ fs.fsyncSync(dirFd);
49
+ }
50
+ finally {
51
+ fs.closeSync(dirFd);
52
+ }
53
+ }
54
+ catch {
55
+ /* best-effort — Windows / some BSDs reject dir fsync */
56
+ }
57
+ }
58
+ /** `fs.writeFileSync` for a state file: lock sentinels, a re-auth latch, a machine identity. */
59
+ export function writeStateFile(target, data, options, what) {
60
+ assertMayMutateLiveState(target, what);
61
+ fs.writeFileSync(target, data, options);
62
+ }
63
+ /** `fs.unlinkSync` for a state file. Throws what `unlinkSync` throws, ENOENT included. */
64
+ export function removeStateFile(target, what) {
65
+ assertMayMutateLiveState(target, what);
66
+ fs.unlinkSync(target);
67
+ }
68
+ /** `fs.renameSync` between state paths; both ends are checked. */
69
+ export function renameStateFile(from, to, what) {
70
+ assertMayMutateLiveState(from, what);
71
+ assertMayMutateLiveState(to, what);
72
+ fs.renameSync(from, to);
73
+ }
@@ -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
@@ -107,6 +107,21 @@ export function isQrSignInSecretField(key) {
107
107
  return QR_SECRET_FIELDS.has(leaf.replace(/[_-]/g, '').toLowerCase());
108
108
  }
109
109
  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;
110
+ // OAuth callback queries may be exported detached from their URL. Redact
111
+ // sensitive parameter values even without path context, including when the
112
+ // query separators were percent-encoded by an enclosing URL.
113
+ const OAUTH_QUERY_SECRET = /(^|[?&]|%(?:25)*26)(code|state|error_description|handoff)(=|%(?:25)*3[dD])([^&#\s"'<>]*)/gi;
114
+ export function isOAuthQuerySecretField(key) {
115
+ return ['code', 'state', 'error_description', 'handoff'].includes(key.toLowerCase());
116
+ }
117
+ export function isOAuthQueryContainerField(key) {
118
+ const leaf = (key.split('.').pop() || '').replace(/[_-]/g, '').toLowerCase();
119
+ return leaf === 'query' || leaf === 'querystring';
120
+ }
121
+ export function isGraphHandoffCookieField(key) {
122
+ return key.toLowerCase() === '__secure-pc-graph-handoff';
123
+ }
124
+ 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;
110
125
  function redactQrSignInText(value) {
111
126
  // Undici/Sentry may split url.query/query_string from its path. The QR's
112
127
  // direction vocabulary identifies that standalone query without treating an
@@ -124,7 +139,8 @@ const credentialMarker = (_match, prefix) => `${prefix}_[redacted]`;
124
139
  export function redactPreviewTokens(value) {
125
140
  if (typeof value !== 'string')
126
141
  return value;
127
- const qrSafe = redactQrSignInText(value);
142
+ const qrSafe = redactQrSignInText(value).replace(OAUTH_QUERY_SECRET, '$1$2$3[redacted]')
143
+ .replace(GRAPH_HANDOFF_COOKIE, '$1[redacted]');
128
144
  if (!CREDENTIAL_HINTS.some((hint) => qrSafe.indexOf(hint) !== -1))
129
145
  return qrSafe;
130
146
  return qrSafe
@@ -153,7 +169,7 @@ export function redactPreviewTokensDeep(root, limits = {}) {
153
169
  let visited = 0;
154
170
  let complete = true;
155
171
  const rewrite = (holder, key, current) => {
156
- const next = isQrSignInSecretField(key) ? QR_SIGN_IN_REDACTION : redactPreviewTokens(current);
172
+ const next = isQrSignInSecretField(key) || isGraphHandoffCookieField(key) ? QR_SIGN_IN_REDACTION : redactPreviewTokens(current);
157
173
  if (next === current) {
158
174
  return;
159
175
  }
@@ -195,7 +211,28 @@ export function redactPreviewTokensDeep(root, limits = {}) {
195
211
  complete = false;
196
212
  continue;
197
213
  }
198
- if (isQrSignInSecretField(key) || typeof child === 'string') {
214
+ if (isOAuthQueryContainerField(key) && Array.isArray(child)) {
215
+ try {
216
+ holder[key] = '[redacted]';
217
+ }
218
+ catch {
219
+ complete = false;
220
+ }
221
+ if (holder[key] !== '[redacted]')
222
+ complete = false;
223
+ continue;
224
+ }
225
+ if (isOAuthQueryContainerField(key) && child && typeof child === 'object' && !Array.isArray(child)) {
226
+ const query = child;
227
+ for (const queryKey of Object.keys(query)) {
228
+ if (isOAuthQuerySecretField(queryKey)) {
229
+ query[queryKey] = '[redacted]';
230
+ if (query[queryKey] !== '[redacted]')
231
+ complete = false;
232
+ }
233
+ }
234
+ }
235
+ if (isQrSignInSecretField(key) || isGraphHandoffCookieField(key) || typeof child === 'string') {
199
236
  rewrite(holder, key, child);
200
237
  }
201
238
  else if (child !== null && typeof child === 'object') {
@@ -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
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrr-ai/auth-core",
3
- "version": "0.4.32",
3
+ "version": "0.4.33",
4
4
  "main": "dist/cjs/index.js",
5
5
  "types": "dist/esm/index.d.ts",
6
6
  "exports": {
@@ -294,6 +294,26 @@
294
294
  "default": "./dist/cjs/restartArtifact.js"
295
295
  }
296
296
  },
297
+ "./master-kek-service": {
298
+ "import": {
299
+ "types": "./dist/esm/kek/masterKekService.d.ts",
300
+ "default": "./dist/esm/kek/masterKekService.js"
301
+ },
302
+ "require": {
303
+ "types": "./dist/cjs/kek/masterKekService.d.ts",
304
+ "default": "./dist/cjs/kek/masterKekService.js"
305
+ }
306
+ },
307
+ "./live-state": {
308
+ "import": {
309
+ "types": "./dist/esm/liveState.d.ts",
310
+ "default": "./dist/esm/liveState.js"
311
+ },
312
+ "require": {
313
+ "types": "./dist/cjs/liveState.d.ts",
314
+ "default": "./dist/cjs/liveState.js"
315
+ }
316
+ },
297
317
  "./daemon-consent-proof": {
298
318
  "import": {
299
319
  "types": "./dist/esm/daemonConsentProof.d.ts",