@skrr-ai/cli 0.1.123 → 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 (85) hide show
  1. package/dist/commands/agents/delete.d.ts +8 -0
  2. package/dist/commands/agents/delete.js +16 -0
  3. package/dist/commands/context.js +7 -0
  4. package/dist/commands/search/workspace.js +1 -1
  5. package/dist/commands/store/gallery-admissions/approve.js +1 -1
  6. package/dist/commands/store/gallery-admissions/preview.js +1 -1
  7. package/dist/commands/store/gallery-admissions/refuse.js +1 -1
  8. package/dist/commands/store/gallery-admissions/show.js +1 -1
  9. package/dist/lib/auth-core-init.d.ts +15 -2
  10. package/dist/lib/auth-core-init.js +47 -6
  11. package/dist/lib/command-hygiene.js +30 -1
  12. package/dist/lib/daemonBrokerRefusal.js +5 -1
  13. package/dist/lib/keychain.js +41 -4
  14. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
  15. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
  16. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +13 -28
  17. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +133 -4
  18. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +387 -90
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +102 -0
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -1
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +6 -0
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +8 -0
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +11 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +43 -3
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +33 -3
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -10
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +15 -37
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +133 -4
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +536 -101
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +13 -3
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -1
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -1
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +15 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +20 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +43 -3
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +104 -8
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +14 -10
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
  82. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  83. package/dist/node_modules/@skrr-ai/data-provider/index.js +25 -7
  84. package/oclif.manifest.json +1 -1
  85. package/package.json +4 -1
@@ -58,6 +58,7 @@ import { openBuffer, sealBuffer, deserialize as envelopeDeserialize, serialize a
58
58
  import { KekUnavailableError } from './types.js';
59
59
  import { registerZeroizeHook } from './zeroize-registry.js';
60
60
  import { MASTER_KEK_KEYCHAIN_ACCOUNT, MASTER_KEK_KEYCHAIN_SERVICE } from './masterKekService.js';
61
+ import { assertMayMutateLiveKeychain, rethrowIfLiveStateRefusal } from '../liveState.js';
61
62
  /**
62
63
  * Stable telemetry kind string — fleet dashboards size the macOS cohort
63
64
  * by this value. Only one backend ships in this module today.
@@ -295,6 +296,15 @@ async function writeKekToKeychain(kek, service, account) {
295
296
  if (keychainPath === null) {
296
297
  throw new KekUnavailableError(KIND_SHELLOUT, 'no user login keychain resolvable; cannot write master KEK');
297
298
  }
299
+ // A test that injected a fake `security` writes nothing real. One that did not is about to add
300
+ // an item to the keychain it just resolved, and the master KEK is the one item that cannot be
301
+ // recreated. A keychain that is not the account's (a throwaway file, or one a faked `security`
302
+ // names) cannot hold the installed product's KEK, so only the account's is protected.
303
+ if (_execFileForTest === null) {
304
+ assertMayMutateLiveKeychain(service, `add the Keychain item ${account} (a master KEK)`, {
305
+ keychainFile: keychainPath,
306
+ });
307
+ }
298
308
  const b64 = kek.toString('base64');
299
309
  try {
300
310
  await exec('security', [
@@ -324,6 +334,12 @@ async function writeKekToKeychain(kek, service, account) {
324
334
  });
325
335
  }
326
336
  }
337
+ /**
338
+ * @internal Test seam — the Keychain write on its own, so a spec can reach the live-state
339
+ * guard without `wrap()`'s `security` probe getting in the way. A spec using it empties `PATH`,
340
+ * so that a guard which stopped firing reaches no `security` binary.
341
+ */
342
+ export const __writeKekToKeychainForTest = writeKekToKeychain;
327
343
  /**
328
344
  * In-process cache of master KEKs, keyed by `${service}::${account}` so
329
345
  * multiple `MacosKek` instances scoped to different keychain entries
@@ -410,6 +426,7 @@ async function resolveOrCreateKek(service, account) {
410
426
  // A concurrent creator may have won. Only a successful read of its
411
427
  // existing item makes the failed create recoverable; all other errors
412
428
  // remain failures and must not cause an update or a second write.
429
+ rethrowIfLiveStateRefusal(error);
413
430
  const winner = await readKekFromKeychain(service, account).catch(() => null);
414
431
  if (!winner)
415
432
  throw error;
@@ -547,6 +564,9 @@ export class MacosKek {
547
564
  catch (err) {
548
565
  if (err instanceof KekUnavailableError)
549
566
  throw err;
567
+ // A refusal is not "the KEK is unavailable": wrapping it would let the credential
568
+ // envelope read a test aimed at the real Keychain as a locked one and carry on.
569
+ rethrowIfLiveStateRefusal(err);
550
570
  throw new KekUnavailableError(this.id, err?.message ?? 'shellout wrap failed', {
551
571
  cause: err,
552
572
  });
@@ -17,9 +17,18 @@
17
17
  * and the device private key sealed under the old DEK was gone for good — which
18
18
  * the server then saw as `DEVICE_PROOF_MISSING` on every CLI hand-off.
19
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.
20
+ * The fix first written for it was a denylist on a wildcard: the sweep kept
21
+ * matching everything under `ai.(skrr|oversky).daemon*` and skipped the services
22
+ * it knew to be precious. That fails open. The default for a service added under
23
+ * that prefix tomorrow was "deleted", which is how the KEK went, and how the
24
+ * person's own BYOK keys were being swept as well.
25
+ *
26
+ * So this module names what a sweep may delete, not what it may not. Every
27
+ * service in the daemon's Keychain namespace is in exactly one class
28
+ * (`classifyDaemonKeychainService`), and a sign-out selects with
29
+ * `isSessionKeychainService`: a service nobody has classified is LEFT IN PLACE.
30
+ * Deleting the KEK is a decision a caller makes by name, never a side effect of
31
+ * a sweep.
23
32
  */
24
33
  /** Keychain `service` of the master KEK. */
25
34
  export declare const MASTER_KEK_KEYCHAIN_SERVICE = "ai.skrr.daemon.kek";
@@ -27,3 +36,34 @@ export declare const MASTER_KEK_KEYCHAIN_SERVICE = "ai.skrr.daemon.kek";
27
36
  export declare const MASTER_KEK_KEYCHAIN_ACCOUNT = "master-kek-v1";
28
37
  /** Whether a keychain service name holds master-KEK material. */
29
38
  export declare function isMasterKekKeychainService(service: string): boolean;
39
+ /** Whether a keychain service name holds a secret the person supplied, not a session. */
40
+ export declare function isUserOwnedSecretKeychainService(service: string): boolean;
41
+ /**
42
+ * Which kind of thing a service in the daemon's Keychain namespace holds. Exactly
43
+ * one, and the order of the checks in `classifyDaemonKeychainService` is what makes
44
+ * that true.
45
+ *
46
+ * `master_kek` key material every profile's envelope is sealed under. No sweep
47
+ * deletes it; a caller that must does so by name.
48
+ * `user_secret` what the person typed in (BYOK provider keys). A sign-out keeps
49
+ * it; a full uninstall removes it.
50
+ * `session` a profile's sign-in. A sign-out removes it.
51
+ * `unclassified` none of the above. Left in place, and reported, so that the next
52
+ * engineer who adds a service has to decide what it is.
53
+ */
54
+ export type DaemonKeychainServiceClass = 'master_kek' | 'user_secret' | 'session' | 'unclassified';
55
+ /**
56
+ * Classify one Keychain `service` name. The named non-members are asked FIRST: a
57
+ * profile may legally be called `byok.work` or `kek.x`, and its service then has
58
+ * the session shape AND the shape of the namespace it collides with. Reading it as
59
+ * the protected class is the safe direction (a sign-out leaves that profile's slots
60
+ * in place), so the protected classes win.
61
+ */
62
+ export declare function classifyDaemonKeychainService(service: string): DaemonKeychainServiceClass;
63
+ /**
64
+ * Whether a sign-out owns this service. This is the ONE predicate a sign-out sweep
65
+ * selects with (`services.filter(isSessionKeychainService)`): it names what is
66
+ * deleted, not what is spared. An uninstall adds `isUserOwnedSecretKeychainService`
67
+ * to it; no sweep adds the KEK.
68
+ */
69
+ export declare function isSessionKeychainService(service: string): boolean;
@@ -17,22 +17,118 @@
17
17
  * and the device private key sealed under the old DEK was gone for good — which
18
18
  * the server then saw as `DEVICE_PROOF_MISSING` on every CLI hand-off.
19
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.
20
+ * The fix first written for it was a denylist on a wildcard: the sweep kept
21
+ * matching everything under `ai.(skrr|oversky).daemon*` and skipped the services
22
+ * it knew to be precious. That fails open. The default for a service added under
23
+ * that prefix tomorrow was "deleted", which is how the KEK went, and how the
24
+ * person's own BYOK keys were being swept as well.
25
+ *
26
+ * So this module names what a sweep may delete, not what it may not. Every
27
+ * service in the daemon's Keychain namespace is in exactly one class
28
+ * (`classifyDaemonKeychainService`), and a sign-out selects with
29
+ * `isSessionKeychainService`: a service nobody has classified is LEFT IN PLACE.
30
+ * Deleting the KEK is a decision a caller makes by name, never a side effect of
31
+ * a sweep.
23
32
  */
24
33
  /** Keychain `service` of the master KEK. */
25
34
  export const MASTER_KEK_KEYCHAIN_SERVICE = 'ai.skrr.daemon.kek';
26
35
  /** Keychain `account` of the master KEK. */
27
36
  export const MASTER_KEK_KEYCHAIN_ACCOUNT = 'master-kek-v1';
28
37
  /**
29
- * The master KEK under the current brand and the one before the rename, plus
30
- * anything namespaced beneath either (`…kek.probe`, the Linux Secret Service
31
- * probe). The pre-rename item is protected too: leaving a dead item behind costs
32
- * bytes, and deleting key material is the one step that cannot be taken back.
38
+ * The master KEK under the current brand and the two names before the rename
39
+ * (`ai.oversky.daemon.kek`, and `com.oversky.daemon.kek`, which was the very first
40
+ * KEK identifier: see `LEGACY_NATIVE_ID_PREFIXES`), plus anything namespaced
41
+ * beneath any of them (`…kek.probe`, the Linux Secret Service probe). The
42
+ * pre-rename items are protected too: leaving a dead item behind costs bytes, and
43
+ * deleting key material is the one step that cannot be taken back.
33
44
  */
34
- const MASTER_KEK_SERVICE_PATTERN = /^ai\.(?:skrr|oversky)\.daemon\.kek(?:\..*)?$/;
45
+ const MASTER_KEK_SERVICE_PATTERN = /^(?:ai\.(?:skrr|oversky)|com\.oversky)\.daemon\.kek(?:\..*)?$/;
35
46
  /** Whether a keychain service name holds master-KEK material. */
36
47
  export function isMasterKekKeychainService(service) {
37
48
  return typeof service === 'string' && MASTER_KEK_SERVICE_PATTERN.test(service);
38
49
  }
50
+ /**
51
+ * The person's own secrets, kept under the same `ai.*.daemon.` prefix: local BYOK provider keys
52
+ * (`ai.skrr.daemon.byok.<profile>-<suffix>`, written by `skrr daemon byok set`).
53
+ *
54
+ * They are not part of a session, and they are not key material either. They have their own
55
+ * lifecycle (`byok set`, `byok remove`), so a sign-out, above all one triggered by a server that
56
+ * rejected the saved session, must not delete them: that is the person's data, typed in once, and
57
+ * the sign-out that took it never asked. Only a full uninstall removes them: it adds this
58
+ * predicate to `isSessionKeychainService`, and no other sweep selects these services.
59
+ */
60
+ const USER_OWNED_SECRET_SERVICE_PATTERN = /^ai\.(?:skrr|oversky)\.daemon\.byok(?:\..*)?$/;
61
+ /** Whether a keychain service name holds a secret the person supplied, not a session. */
62
+ export function isUserOwnedSecretKeychainService(service) {
63
+ return typeof service === 'string' && USER_OWNED_SECRET_SERVICE_PATTERN.test(service);
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // What a sign-out OWNS
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * A profile name as the daemon accepts it: `VALID_PROFILE_RE` in
70
+ * `daemon/src/profile.ts` (letters, digits, dot, dash, underscore; starts with a
71
+ * letter or digit; 64 characters at most). Dots are allowed, which is why the
72
+ * session grammar below cannot be told apart from the KEK or BYOK namespaces by
73
+ * its shape alone, and why the classifier asks those two first.
74
+ */
75
+ const PROFILE_NAME = '[A-Za-z0-9][A-Za-z0-9._-]{0,63}';
76
+ /**
77
+ * The config-dir suffix every current service carries: the first eight hex
78
+ * characters of sha256 of the profile's config dir (`configDirSuffix` in
79
+ * `daemon/src/keychain.ts`, mirrored by the desktop supervisor).
80
+ */
81
+ const CONFIG_DIR_HASH = '-[a-f0-9]{8}';
82
+ /**
83
+ * The session grammar: the services one profile's sign-in lives in, which are the
84
+ * ones a sign-out is for. It is POSITIVE and deliberately narrow.
85
+ *
86
+ * ai.skrr.daemon.<profile>-<hash8>
87
+ * The current layout, written by the daemon and the desktop supervisor
88
+ * (`computeKeychainServiceForProfile`, and `machine-dek.ts`). The hash is
89
+ * REQUIRED. `ai.skrr.daemon.<profile>` with no hash is the same shape as the
90
+ * KEK (`ai.skrr.daemon.kek`, a "profile" called kek), and no current code
91
+ * writes one, so a name of that shape is not a session: it is unclassified.
92
+ *
93
+ * ai.oversky.daemon[.<profile>[-<hash8>]]
94
+ * The three shapes the retired brand left on machines carried through the
95
+ * rename: the pre-Phase-K bare service, the per-profile service, and the
96
+ * per-profile service with the config-dir hash. The brand is retired, so this
97
+ * set is closed: nothing new is ever written under it. A sign-out still has to
98
+ * clear them, or a person who signed out keeps a live refresh token in the
99
+ * Keychain.
100
+ *
101
+ * Anything else, including a service added under `ai.skrr.daemon` tomorrow, is not
102
+ * matched here, and a sweep that selects with this leaves it alone.
103
+ */
104
+ const SESSION_SERVICE_PATTERNS = [
105
+ new RegExp(`^ai\\.skrr\\.daemon\\.${PROFILE_NAME}${CONFIG_DIR_HASH}$`),
106
+ new RegExp(`^ai\\.oversky\\.daemon(?:\\.${PROFILE_NAME}(?:${CONFIG_DIR_HASH})?)?$`),
107
+ ];
108
+ /**
109
+ * Classify one Keychain `service` name. The named non-members are asked FIRST: a
110
+ * profile may legally be called `byok.work` or `kek.x`, and its service then has
111
+ * the session shape AND the shape of the namespace it collides with. Reading it as
112
+ * the protected class is the safe direction (a sign-out leaves that profile's slots
113
+ * in place), so the protected classes win.
114
+ */
115
+ export function classifyDaemonKeychainService(service) {
116
+ if (typeof service !== 'string')
117
+ return 'unclassified';
118
+ if (isMasterKekKeychainService(service))
119
+ return 'master_kek';
120
+ if (isUserOwnedSecretKeychainService(service))
121
+ return 'user_secret';
122
+ if (SESSION_SERVICE_PATTERNS.some((pattern) => pattern.test(service)))
123
+ return 'session';
124
+ return 'unclassified';
125
+ }
126
+ /**
127
+ * Whether a sign-out owns this service. This is the ONE predicate a sign-out sweep
128
+ * selects with (`services.filter(isSessionKeychainService)`): it names what is
129
+ * deleted, not what is spared. An uninstall adds `isUserOwnedSecretKeychainService`
130
+ * to it; no sweep adds the KEK.
131
+ */
132
+ export function isSessionKeychainService(service) {
133
+ return classifyDaemonKeychainService(service) === 'session';
134
+ }
@@ -0,0 +1,154 @@
1
+ import { CONFIG_DIR_NAME } from './localIdentity.js';
2
+ /** Directory name of the state root inside a home directory. */
3
+ export { CONFIG_DIR_NAME };
4
+ /** Opt-in for a deliberate check against the machine's real state. */
5
+ export declare const ALLOW_LIVE_STATE_ENV = "SKRR_TEST_ALLOW_LIVE_STATE";
6
+ /** Set by every hermetic runner to the throwaway home it gave the process. */
7
+ export declare const HERMETIC_HOME_ENV = "SKRR_TEST_HERMETIC_HOME";
8
+ /** `LiveStateRefusedError.code`. Kept at its pre-consolidation value: scripts match on it. */
9
+ export declare const LIVE_STATE_REFUSED_CODE = "SKRR_TEST_LIVE_STATE";
10
+ /**
11
+ * Which signal says this is a test process, or `null`. The text names the signal so a false
12
+ * positive (a shell that exports `NODE_ENV=test`) diagnoses itself.
13
+ *
14
+ * `NODE_TEST_CONTEXT` is set by `node --test` in every file's child process and inherited by
15
+ * anything a test spawns. `--test` in `execArgv` covers `--test-isolation=none`, where tests run
16
+ * in the runner itself. A test file run DIRECTLY (`node file.test.ts`) has neither: node:test
17
+ * sets no marker for it, so the entry script's name is the only evidence. `JEST_WORKER_ID` is
18
+ * set in the real environment of every jest worker (and by `--runInBand`), so a child a spec
19
+ * spawns without `env` still carries it. `NODE_ENV=test` is what jest, vitest and `bun test`
20
+ * default to, and what custom scripts set by hand.
21
+ *
22
+ * `argv` is the process's own only when `env` is: a caller describing a hypothetical process
23
+ * (`{}`) describes all of it.
24
+ */
25
+ export declare function testProcessSignal(env?: NodeJS.ProcessEnv, execArgv?: readonly string[], argv?: readonly string[]): string | null;
26
+ /** Is this a test process? See `testProcessSignal` for what counts. */
27
+ export declare function isTestProcess(env?: NodeJS.ProcessEnv, execArgv?: readonly string[], argv?: readonly string[]): boolean;
28
+ /** Should a test process be refused access to the machine's real state? */
29
+ export declare function refusesLiveStateInTest(env?: NodeJS.ProcessEnv, execArgv?: readonly string[], argv?: readonly string[]): boolean;
30
+ /**
31
+ * The account's home directory from the user database. A test that points `HOME` somewhere else
32
+ * cannot move it, which is the point: it names where the INSTALLED product keeps its state,
33
+ * whatever this process believes.
34
+ *
35
+ * Node's `os.userInfo().homedir` reads the user database. Bun's returns `$HOME`, which is
36
+ * exactly what a test moves, so the compiled daemon asks the database itself — without that, a
37
+ * compiled binary spawned by a test with its own `HOME` saw that home as the live one and
38
+ * refused to start. `null` when it cannot be determined.
39
+ */
40
+ export declare function accountHomeDir(): string | null;
41
+ /**
42
+ * @internal Test seam — make `accountHomeDir()` answer `home` (`null`: "cannot be determined"),
43
+ * or restore the user-database lookup with `undefined`. It lets a spec stand a THROWAWAY
44
+ * directory in for the account's real home, so the guard can be exercised end to end — and its
45
+ * refusal removed, to prove the spec notices — without any path ever touching the real one.
46
+ * Never called from production code; `stateFileBoundary.spec.ts` fails if it is.
47
+ */
48
+ export declare function __setAccountHomeForTest(home: string | null | undefined): void;
49
+ /** The state root of the product installed for this account, when it can be known. */
50
+ export declare function liveStateRoot(home?: string | null): string | null;
51
+ /**
52
+ * What a guard decision is made against. Everything defaults to the running process; a spec
53
+ * injects a throwaway directory as `realHome` to prove the guard fires WITHOUT going near the
54
+ * machine's actual home.
55
+ */
56
+ export interface LiveStateProbe {
57
+ env?: NodeJS.ProcessEnv;
58
+ execArgv?: readonly string[];
59
+ argv?: readonly string[];
60
+ /** The account's real home. `null` means "cannot be determined". Default: the user database. */
61
+ realHome?: string | null;
62
+ }
63
+ export declare class LiveStateRefusedError extends Error {
64
+ readonly code = "SKRR_TEST_LIVE_STATE";
65
+ /** What the mutation was aimed at: a path, or a Keychain service. */
66
+ readonly target: string;
67
+ /** Why this process was taken for a test. */
68
+ readonly signal: string;
69
+ constructor(what: string, target: string, signal: string, why: string, hint?: string);
70
+ }
71
+ /**
72
+ * Is `err` a live-state refusal? By name and code as well as by class: auth-core ships as both
73
+ * ESM and CJS, and two copies of it in one process (a farmed worktree) have two classes.
74
+ */
75
+ export declare function isLiveStateRefusal(err: unknown): err is LiveStateRefusedError;
76
+ /**
77
+ * First line of any `catch` that wraps a guarded mutation. A refusal swallowed into a telemetry
78
+ * event is a write that silently did not happen under a test that reports green, which is the
79
+ * "everything reads healthy" shape this repo has paid for before. A no-op outside a test
80
+ * process, where no refusal is ever constructed.
81
+ */
82
+ export declare function rethrowIfLiveStateRefusal(err: unknown): void;
83
+ /**
84
+ * Throw `LiveStateRefusedError` when a test process is about to mutate `target`, a path under
85
+ * the account's real home. Returns without reading anything else outside a test process.
86
+ *
87
+ * `what` completes "Refusing to …" (`replace the wrapped DEK`). `hint` replaces the default
88
+ * advice about how to run the test safely.
89
+ */
90
+ export declare function assertMayMutateLiveState(target: string, what: string, probe?: LiveStateProbe, hint?: string): void;
91
+ /** Service name prefix of the daemon's credential slots; always followed by `.<profile>`. */
92
+ export declare const DAEMON_KEYCHAIN_SERVICE_BASE = "ai.skrr.daemon";
93
+ /**
94
+ * Separate Keychain service for local BYOK secrets.
95
+ *
96
+ * Keeping provider keys out of the daemon-auth service prevents a broad "export daemon
97
+ * credentials" operation from accidentally including them and lets the user remove a provider
98
+ * key without disturbing their login.
99
+ */
100
+ export declare const DAEMON_BYOK_KEYCHAIN_SERVICE_BASE = "ai.skrr.daemon.byok";
101
+ /**
102
+ * The PRE-RENAME, pre-Phase-K service name. Read ONLY by the daemon's migration helper.
103
+ *
104
+ * It must NOT equal `DAEMON_KEYCHAIN_SERVICE_BASE`, and it held that value until a fix: the
105
+ * skrr rename swept the string like any other, and the two became identical. Nothing failed,
106
+ * because the failure is an absence — `migrateLegacyServiceToProfile` read a service nothing had
107
+ * ever written, found no slots, and reported success. Pre-rename credentials stayed where they
108
+ * were and the user silently re-authenticated. Written as a literal on purpose.
109
+ */
110
+ export declare const LEGACY_DAEMON_KEYCHAIN_SERVICE = "ai.oversky.daemon";
111
+ /** The profile the installed daemon runs when nothing selects another. */
112
+ export declare const LIVE_DEFAULT_PROFILE = "default";
113
+ /**
114
+ * `-<sha256(dir)[0:8]>`, the suffix of every per-profile Keychain service name. The input is the
115
+ * absolute, trailing-slash-stripped config directory, so two OS users running the daemon under
116
+ * their own `~/.skrr/` get distinct names and cannot read each other's tokens.
117
+ */
118
+ export declare function keychainSuffixForDir(dir: string): string;
119
+ /**
120
+ * Keychain services that belong to the product installed on this machine: the master KEK
121
+ * (current brand and the ones before it), the default profile's credential and BYOK slots under
122
+ * the account's real home, and the legacy names the daemon migrates from on start — a write to
123
+ * one of those is adopted by the next restart instead of the next read.
124
+ *
125
+ * The per-profile names are derived from the ACCOUNT's home, which no `HOME` override can reach.
126
+ * When the home is unknown only the names that need no home are returned; see
127
+ * `isLiveKeychainService` for what is done about the rest.
128
+ */
129
+ export declare function liveKeychainServices(probe?: Pick<LiveStateProbe, 'realHome'>): ReadonlySet<string>;
130
+ /**
131
+ * Does `service` name an item of the installed product? When the real home is unknown, a
132
+ * default-profile slot with a hash suffix is assumed live: its hash cannot be checked, and the
133
+ * guard fails closed.
134
+ */
135
+ export declare function isLiveKeychainService(service: string, probe?: Pick<LiveStateProbe, 'realHome'>): boolean;
136
+ /** Would a mutation of Keychain `service` be refused in a process with this shape? */
137
+ export declare function refusesLiveKeychainMutation(service: string, probe?: LiveStateProbe): boolean;
138
+ /** What a Keychain guard decision is made against. */
139
+ export interface LiveKeychainProbe extends LiveStateProbe {
140
+ /**
141
+ * The keychain FILE the write is aimed at, when the caller knows it (macOS passes the login
142
+ * keychain it resolved). A keychain outside the real home is not the account's, so nothing
143
+ * the installed product reads can be in it, whatever the service is called: a throwaway
144
+ * keychain file, or a fake `security` that answers with a path of its own. Without it, the
145
+ * service name alone decides.
146
+ */
147
+ keychainFile?: string;
148
+ }
149
+ /**
150
+ * Throw `LiveStateRefusedError` when a test process is about to write or delete an item in a
151
+ * Keychain service the installed product reads. Callers that can be handed a fake `security`
152
+ * binary call this only when the real one would run: a mocked write touches nothing.
153
+ */
154
+ export declare function assertMayMutateLiveKeychain(service: string, what: string, probe?: LiveKeychainProbe, hint?: string): void;