@skrr-ai/cli 0.1.123 → 0.1.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agents/chat.d.ts +26 -0
- package/dist/commands/agents/chat.js +48 -10
- package/dist/commands/agents/delete.d.ts +8 -0
- package/dist/commands/agents/delete.js +16 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/search/workspace.js +1 -1
- package/dist/commands/store/gallery-admissions/approve.js +1 -1
- package/dist/commands/store/gallery-admissions/preview.js +1 -1
- package/dist/commands/store/gallery-admissions/refuse.js +1 -1
- package/dist/commands/store/gallery-admissions/show.js +1 -1
- package/dist/generated/agent-guides.json +19 -19
- package/dist/lib/agentic-stream.js +15 -1
- package/dist/lib/auth-core-init.d.ts +15 -2
- package/dist/lib/auth-core-init.js +47 -6
- package/dist/lib/command-hygiene.js +30 -1
- package/dist/lib/daemonBrokerRefusal.js +5 -1
- package/dist/lib/keychain.js +41 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +13 -28
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +133 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +387 -90
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +102 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +11 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +43 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +33 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +15 -37
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +133 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +536 -101
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +13 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +20 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +43 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +104 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +14 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1094 -1067
- package/oclif.manifest.json +16 -3
- 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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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;
|