@skrr-ai/cli 0.1.89 → 0.1.90
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/machines/dedicated/exec.js +60 -14
- package/dist/commands/machines/dedicated/index.js +2 -0
- package/dist/commands/machines/dedicated/power/explain.d.ts +13 -0
- package/dist/commands/machines/dedicated/power/explain.js +36 -0
- package/dist/commands/machines/dedicated/power/index.d.ts +10 -0
- package/dist/commands/machines/dedicated/power/index.js +24 -0
- package/dist/commands/machines/dedicated/power/keep-awake.d.ts +20 -0
- package/dist/commands/machines/dedicated/power/keep-awake.js +69 -0
- package/dist/commands/machines/dedicated/power/set.d.ts +23 -0
- package/dist/commands/machines/dedicated/power/set.js +106 -0
- package/dist/commands/machines/dedicated/power/show.d.ts +13 -0
- package/dist/commands/machines/dedicated/power/show.js +36 -0
- package/dist/lib/commitments.d.ts +1 -1
- package/dist/lib/commitments.js +4 -2
- package/dist/lib/dedicated-keep-awake.d.ts +19 -0
- package/dist/lib/dedicated-keep-awake.js +64 -0
- package/dist/lib/dedicated-power.d.ts +61 -0
- package/dist/lib/dedicated-power.js +243 -0
- package/dist/lib/format.d.ts +13 -0
- package/dist/lib/format.js +51 -0
- package/dist/lib/keychain.js +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +27 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.d.ts +107 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.js +151 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.d.ts +115 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.js +117 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +31 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +149 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +24 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.d.ts +107 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.d.ts +115 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.js +113 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +31 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +144 -8
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +6393 -6278
- package/oclif.manifest.json +30493 -30127
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Dedicated Runtime wake-progress wire contract (power-control task W5,
|
|
3
|
+
* `dedicated-runtime-power-and-demand-wake-2026-10-07.md` §5.3, §7.2, §9).
|
|
4
|
+
*
|
|
5
|
+
* Declared ONCE, here, for the same reason `computerWire.ts` is: the browser,
|
|
6
|
+
* the React Native app and the CLI import it, and none of them may spell the
|
|
7
|
+
* event, the capability or a code a second time. `dedicatedRuntimeWakeWire.test.ts`
|
|
8
|
+
* fails on a second spelling in the API.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT IS
|
|
11
|
+
* ---------------------------------------------------------------------------
|
|
12
|
+
* A person opens a terminal, an SSH channel, a service call or the Computer on a
|
|
13
|
+
* STOPPED Dedicated Runtime. That is wake progress, not an error: the relay asks
|
|
14
|
+
* `requestWake` (source `person`) to start the machine, then tells THAT request,
|
|
15
|
+
* in stages, how far the start has got, until the machine is assignable and the
|
|
16
|
+
* original request is served, or a bound runs out and it says why and what to do.
|
|
17
|
+
*
|
|
18
|
+
* THE CLIENT ANNOUNCES IT
|
|
19
|
+
* ---------------------------------------------------------------------------
|
|
20
|
+
* A client that has not announced `dedicated_wake_progress_v1` in the open
|
|
21
|
+
* request's `capabilities` array is NEVER sent `dedicated:wake:progress` and is
|
|
22
|
+
* never made to wait: it gets an immediate typed refusal instead. (A client
|
|
23
|
+
* socket has no registration step to announce on, so the announcement rides each
|
|
24
|
+
* request — stateless, nothing held in the relay.)
|
|
25
|
+
*
|
|
26
|
+
* THE VOCABULARY
|
|
27
|
+
* ---------------------------------------------------------------------------
|
|
28
|
+
* `phase` uses the cold-start measurement's phase names
|
|
29
|
+
* (`scripts/dedicated-runtime/measure-cold-start.js` `COLD_START_PHASES`), so
|
|
30
|
+
* what the person sees and what F4 measures are the same words. A spec holds the
|
|
31
|
+
* two lists equal.
|
|
32
|
+
*
|
|
33
|
+
* NOT A SPINNER WITHOUT A BOUND
|
|
34
|
+
* ---------------------------------------------------------------------------
|
|
35
|
+
* `boundMs` is carried on every event so a surface can show elapsed-of-bound.
|
|
36
|
+
* The bound sits ABOVE a slow-but-working start (design §9): running out says
|
|
37
|
+
* "this is taking longer than expected" and the machine is left starting; it is
|
|
38
|
+
* never an accusation that the machine is dead.
|
|
39
|
+
*/
|
|
40
|
+
/** API -> client, on the same socket as the request it belongs to. */
|
|
41
|
+
export const DEDICATED_WAKE_EVENTS = {
|
|
42
|
+
progress: 'dedicated:wake:progress',
|
|
43
|
+
};
|
|
44
|
+
/** Announced in the open request's `capabilities` array. */
|
|
45
|
+
export const DEDICATED_WAKE_CAPABILITIES = {
|
|
46
|
+
progress: 'dedicated_wake_progress_v1',
|
|
47
|
+
};
|
|
48
|
+
/** Same names, same order as `WAKE_PHASES` in api/server/services/DedicatedRuntime/wakePhases.js (pinned by a test). */
|
|
49
|
+
export const DEDICATED_WAKE_PHASES = [
|
|
50
|
+
'start_requested',
|
|
51
|
+
'provider_start_accepted',
|
|
52
|
+
'instance_running',
|
|
53
|
+
'daemon_connected',
|
|
54
|
+
'first_heartbeat',
|
|
55
|
+
'assignable',
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* - `waking` a start is in flight; `phase` says how far.
|
|
59
|
+
* - `ready` assignable; the original request is about to be served.
|
|
60
|
+
* - `deferred` the machine is stopping; nothing was started. Retry after `retryAfterSeconds`.
|
|
61
|
+
* - `refused` the start was refused, with `code`, `reason`, `detail`, `fix`.
|
|
62
|
+
* - `timed_out` the bound ran out while it was still starting. Not a failure of the machine.
|
|
63
|
+
*/
|
|
64
|
+
export const DEDICATED_WAKE_STATES = [
|
|
65
|
+
'waking',
|
|
66
|
+
'ready',
|
|
67
|
+
'deferred',
|
|
68
|
+
'refused',
|
|
69
|
+
'timed_out',
|
|
70
|
+
];
|
|
71
|
+
/** The surfaces a wake can be asked from. */
|
|
72
|
+
export const DEDICATED_WAKE_SURFACES = [
|
|
73
|
+
'terminal',
|
|
74
|
+
'ssh',
|
|
75
|
+
'services',
|
|
76
|
+
'templates',
|
|
77
|
+
'computer',
|
|
78
|
+
'http',
|
|
79
|
+
];
|
|
80
|
+
/**
|
|
81
|
+
* Typed refusals. Each is a different fact with a different next step; none of
|
|
82
|
+
* them is the generic "Dedicated Runtime dispatch was refused".
|
|
83
|
+
*/
|
|
84
|
+
export const DEDICATED_WAKE_CODES = {
|
|
85
|
+
/** Stopped, and this request did not (or could not) wake it. */
|
|
86
|
+
stopped: 'DEDICATED_RUNTIME_STOPPED',
|
|
87
|
+
/** A stop is finishing; retry when it has. */
|
|
88
|
+
stopping: 'DEDICATED_RUNTIME_STOPPING',
|
|
89
|
+
/** A start is in flight (ours or someone's). */
|
|
90
|
+
starting: 'DEDICATED_RUNTIME_STARTING',
|
|
91
|
+
/** The bound ran out while it was still starting. */
|
|
92
|
+
timeout: 'DEDICATED_RUNTIME_WAKE_TIMEOUT',
|
|
93
|
+
/** The start was refused: the plan no longer includes the machine. */
|
|
94
|
+
entitlement: 'DEDICATED_RUNTIME_WAKE_REFUSED_ENTITLEMENT',
|
|
95
|
+
/** The start was refused: the spend cap was reached. */
|
|
96
|
+
spendCap: 'DEDICATED_RUNTIME_WAKE_REFUSED_SPEND_CAP',
|
|
97
|
+
/** Any other refusal; `reason` names it. */
|
|
98
|
+
refused: 'DEDICATED_RUNTIME_WAKE_REFUSED',
|
|
99
|
+
/** The start was requested and the machine fell back to stopped or failed. */
|
|
100
|
+
failed: 'DEDICATED_RUNTIME_WAKE_FAILED',
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* How long a surface waits for a start before saying it is taking longer than
|
|
104
|
+
* expected. Above the slow-but-working case: a cold start on a fresh guest
|
|
105
|
+
* takes 25-30 s for the browser alone, and the AMI boot is longer. It is a
|
|
106
|
+
* bound on what we show, never a verdict on the machine.
|
|
107
|
+
*/
|
|
108
|
+
export const DEDICATED_WAKE_BOUND_MS = 180_000;
|
|
109
|
+
/** How often the relay re-reads the lease while waiting. */
|
|
110
|
+
export const DEDICATED_WAKE_POLL_MS = 2_000;
|
|
111
|
+
export function clientAnnouncesWakeProgress(capabilities) {
|
|
112
|
+
return Array.isArray(capabilities) && capabilities.includes(DEDICATED_WAKE_CAPABILITIES.progress);
|
|
113
|
+
}
|
|
@@ -30,7 +30,7 @@ export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, n
|
|
|
30
30
|
export { DPOP_HEADER, DPOP_MAX_AGE_SECONDS, DPOP_PROOF_INVALID, DPOP_PROOF_REQUIRED, DPOP_PROOF_TYP, DPOP_PROOF_VERIFY_UNAVAILABLE, accessTokenHash, createDpopKey, isAth, isJwkThumbprint, normalizeDpopHtu, verifyDpopProof, type DpopKey, type DpopProofClaims, type DpopProofRequest, type DpopVerifyFailureReason, type DpopVerifyOptions, type DpopVerifyResult, } from './dpop.js';
|
|
31
31
|
export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, splitKeyConvergencePolicy, maybeEncryptForWrite, maybeDecryptOnRead, DEFAULT_UNREADABLE_CREDENTIAL_HINT, type DecryptOnReadOptions, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
|
|
32
32
|
export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, describeDeviceKeySplit, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
|
|
33
|
-
export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
33
|
+
export { getMachineUuid, getHardwareMachineUuid, setMachineUuidProfile, getMachineUuidProfile, qualifyMachineUuid, MACHINE_IDENTITY_FILENAME, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
34
34
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
35
35
|
export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
|
|
36
36
|
export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
|
|
@@ -125,7 +125,7 @@ __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest,
|
|
|
125
125
|
// Hardware machine identity sent as `machineUuid` (IOPlatformUUID /
|
|
126
126
|
// machine-id / MachineGuid). ONE implementation for daemon and CLI —
|
|
127
127
|
// OSK-12063. Also exposed dependency-free as `@skrr-ai/auth-core/machine-uuid`.
|
|
128
|
-
export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
128
|
+
export { getMachineUuid, getHardwareMachineUuid, setMachineUuidProfile, getMachineUuidProfile, qualifyMachineUuid, MACHINE_IDENTITY_FILENAME, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
|
|
129
129
|
// Stable per-machine UUIDv7 identity persisted at <configDir>/daemon.id.
|
|
130
130
|
// NOT a `machineUuid` — see machineUuid.ts.
|
|
131
131
|
export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
|
|
@@ -18,19 +18,43 @@ export declare const HOSTED_RUNTIME_MACHINE_SOURCES: ReadonlySet<string>;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function hostedRuntimeIdOverride(name: string, env?: NodeJS.ProcessEnv): string | null;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* The HARDWARE identity of this machine (IOPlatformUUID / machine-id /
|
|
22
|
+
* MachineGuid, or a control-plane-issued value on a hosted runtime). Cached for
|
|
23
|
+
* the process lifetime. Same machine -> same value across reboots.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* The guarantee is only: same machine -> same value across reboots, and the
|
|
26
|
-
* same value from every binary that calls this function.
|
|
27
|
-
*
|
|
28
|
-
* On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
|
|
25
|
+
* Almost nothing should call this: `getMachineUuid()` is what goes on the wire,
|
|
26
|
+
* and it equals this for the default profile.
|
|
29
27
|
*
|
|
30
28
|
* Falls back to a deterministic hostname+username hash if the platform
|
|
31
29
|
* probe fails. This preserves server-side record continuity even on
|
|
32
30
|
* locked-down machines that refuse ioreg / registry access.
|
|
33
31
|
*/
|
|
32
|
+
export declare function getHardwareMachineUuid(): string;
|
|
33
|
+
/** The profile's persisted machine-identity choice, beside its other state. */
|
|
34
|
+
export declare const MACHINE_IDENTITY_FILENAME = "machine-identity.json";
|
|
35
|
+
/** The server caps `machineUuid` at 64 characters; 36 + 15 fits, and so does a 41-character fallback. */
|
|
36
|
+
export declare function qualifyMachineUuid(hardware: string, profile: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Tell this module which profile the process runs as. Called wherever a binary
|
|
39
|
+
* resolves its profile (`setActiveProfile` in the daemon and in the CLI), so the
|
|
40
|
+
* identity cannot depend on call order. `null`, '' and the default profile all
|
|
41
|
+
* mean "the hardware identity".
|
|
42
|
+
*/
|
|
43
|
+
export declare function setMachineUuidProfile(profile: string | null | undefined): void;
|
|
44
|
+
export declare function getMachineUuidProfile(): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Return the `machineUuid` this process sends to the server.
|
|
47
|
+
*
|
|
48
|
+
* Callers should treat this as opaque: the format differs per platform
|
|
49
|
+
* (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid), and a
|
|
50
|
+
* non-default profile that started new carries a `:p-<hash>` suffix. The
|
|
51
|
+
* guarantee is only: same machine + same profile -> same value across reboots,
|
|
52
|
+
* and the same value from every binary of that profile that calls this function.
|
|
53
|
+
*
|
|
54
|
+
* Equals the hardware identity for the default profile, for a hosted runtime
|
|
55
|
+
* (the control-plane-issued `OVERSKY_MACHINE_UUID` wins) and for a non-default
|
|
56
|
+
* profile that already held credentials; see the block above.
|
|
57
|
+
*/
|
|
34
58
|
export declare function getMachineUuid(): string;
|
|
35
59
|
/** @internal test seam — forget the cached value so the next call re-probes. */
|
|
36
60
|
export declare function __resetMachineUuidCacheForTest(): void;
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
import crypto from 'node:crypto';
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import os from 'node:os';
|
|
28
|
+
import path from 'node:path';
|
|
28
29
|
import { execFileSync } from 'node:child_process';
|
|
30
|
+
import { resolveConfigRoot } from './configRoot.js';
|
|
29
31
|
/**
|
|
30
32
|
* Daemon sources whose identity is ISSUED by the control plane rather than
|
|
31
33
|
* derived from the host: the provisioner mints the daemon id and machine uuid
|
|
@@ -97,20 +99,18 @@ function readMachineUuidWindows() {
|
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
102
|
+
* The HARDWARE identity of this machine (IOPlatformUUID / machine-id /
|
|
103
|
+
* MachineGuid, or a control-plane-issued value on a hosted runtime). Cached for
|
|
104
|
+
* the process lifetime. Same machine -> same value across reboots.
|
|
101
105
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* The guarantee is only: same machine -> same value across reboots, and the
|
|
105
|
-
* same value from every binary that calls this function.
|
|
106
|
-
*
|
|
107
|
-
* On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
|
|
106
|
+
* Almost nothing should call this: `getMachineUuid()` is what goes on the wire,
|
|
107
|
+
* and it equals this for the default profile.
|
|
108
108
|
*
|
|
109
109
|
* Falls back to a deterministic hostname+username hash if the platform
|
|
110
110
|
* probe fails. This preserves server-side record continuity even on
|
|
111
111
|
* locked-down machines that refuse ioreg / registry access.
|
|
112
112
|
*/
|
|
113
|
-
export function
|
|
113
|
+
export function getHardwareMachineUuid() {
|
|
114
114
|
const hostedMachineUuid = hostedRuntimeIdOverride('OVERSKY_MACHINE_UUID');
|
|
115
115
|
if (hostedMachineUuid)
|
|
116
116
|
return hostedMachineUuid;
|
|
@@ -141,7 +141,143 @@ export function getMachineUuid() {
|
|
|
141
141
|
cachedMachineUuid = fallback;
|
|
142
142
|
return fallback;
|
|
143
143
|
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Profile-qualified machine identity (OSK-13845)
|
|
146
|
+
//
|
|
147
|
+
// The server keys a Daemon row on (user, machineUuid), pins ONE device key to
|
|
148
|
+
// that row, and groups Harness rows into one "machine" by it. A profile is the
|
|
149
|
+
// documented way to run a second, isolated daemon on the same computer, and
|
|
150
|
+
// while the machineUuid was hardware only a second profile landed on the FIRST
|
|
151
|
+
// profile's row: it replaced the machine page's Daemon and working directory,
|
|
152
|
+
// re-registered every harness under itself, and its interactive login re-pinned
|
|
153
|
+
// the row's device key (OSK-12063), so the default profile's next refresh was
|
|
154
|
+
// refused DEVICE_KEY_MISMATCH and every `skrr` call failed until a re-login.
|
|
155
|
+
//
|
|
156
|
+
// So a profile that STARTS NEW on a machine is qualified:
|
|
157
|
+
// `<hardware>:p-<12 hex of sha256(profile)>`. Two rules keep everything that
|
|
158
|
+
// exists where it is:
|
|
159
|
+
//
|
|
160
|
+
// - the default profile is NEVER qualified: its identity is the hardware value
|
|
161
|
+
// it has always had, so no existing machine row changes;
|
|
162
|
+
// - a non-default profile that already holds credentials (an
|
|
163
|
+
// `auth-binding.json` or a persisted daemon identity) keeps the hardware
|
|
164
|
+
// value. Its refresh tokens are bound to that row and the server refuses a
|
|
165
|
+
// refresh whose machineUuid differs from the stored one (H7), so qualifying
|
|
166
|
+
// it would sign it out. It stays a second daemon on the shared row.
|
|
167
|
+
//
|
|
168
|
+
// The choice is made ONCE, from that evidence, and written to the profile's
|
|
169
|
+
// `machine-identity.json`; afterwards it is read, never re-derived. Deriving it
|
|
170
|
+
// every time would flip a profile from qualified to shared the moment its own
|
|
171
|
+
// first login wrote `auth-binding.json`.
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/** The profile's persisted machine-identity choice, beside its other state. */
|
|
174
|
+
export const MACHINE_IDENTITY_FILENAME = 'machine-identity.json';
|
|
175
|
+
/** Files that prove a profile already holds credentials bound to the shared row. */
|
|
176
|
+
const PRE_EXISTING_PROFILE_EVIDENCE = ['auth-binding.json', 'daemon-identity.json'];
|
|
177
|
+
const DEFAULT_PROFILE_NAME = 'default';
|
|
178
|
+
/** The server caps `machineUuid` at 64 characters; 36 + 15 fits, and so does a 41-character fallback. */
|
|
179
|
+
export function qualifyMachineUuid(hardware, profile) {
|
|
180
|
+
const digest = crypto
|
|
181
|
+
.createHash('sha256')
|
|
182
|
+
.update(`profile:${profile}`)
|
|
183
|
+
.digest('hex')
|
|
184
|
+
.slice(0, 12);
|
|
185
|
+
return `${hardware}:p-${digest}`;
|
|
186
|
+
}
|
|
187
|
+
let activeMachineProfile = null;
|
|
188
|
+
const profileChoiceCache = new Map();
|
|
189
|
+
/**
|
|
190
|
+
* Tell this module which profile the process runs as. Called wherever a binary
|
|
191
|
+
* resolves its profile (`setActiveProfile` in the daemon and in the CLI), so the
|
|
192
|
+
* identity cannot depend on call order. `null`, '' and the default profile all
|
|
193
|
+
* mean "the hardware identity".
|
|
194
|
+
*/
|
|
195
|
+
export function setMachineUuidProfile(profile) {
|
|
196
|
+
activeMachineProfile = typeof profile === 'string' && profile.trim() ? profile.trim() : null;
|
|
197
|
+
}
|
|
198
|
+
export function getMachineUuidProfile() {
|
|
199
|
+
return activeMachineProfile;
|
|
200
|
+
}
|
|
201
|
+
function readPersistedChoice(file, hardware, profile) {
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
204
|
+
// A file from another machine (a restored backup, a synced home) is absent.
|
|
205
|
+
if (parsed?.version !== 1 || parsed.hardware !== hardware)
|
|
206
|
+
return null;
|
|
207
|
+
// Only the two values this module can produce are accepted: a hand-edited or
|
|
208
|
+
// foreign string must never become an identity.
|
|
209
|
+
if (parsed.machineUuid === hardware)
|
|
210
|
+
return hardware;
|
|
211
|
+
const qualified = qualifyMachineUuid(hardware, profile);
|
|
212
|
+
return parsed.machineUuid === qualified ? qualified : null;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function persistChoice(dir, file, hardware, machineUuid, reason) {
|
|
219
|
+
try {
|
|
220
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
221
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
222
|
+
const body = { version: 1, hardware, machineUuid, reason, decidedAt: new Date().toISOString() };
|
|
223
|
+
fs.writeFileSync(tmp, `${JSON.stringify(body)}\n`, { mode: 0o600 });
|
|
224
|
+
fs.renameSync(tmp, file);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Not persisted: the in-process cache keeps this process consistent, and the
|
|
228
|
+
// next process decides again from the same evidence.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** The identity a NON-default profile registers under. Never throws. */
|
|
232
|
+
function resolveProfileMachineUuid(profile, hardware) {
|
|
233
|
+
const cacheKey = `${hardware}\u0000${profile}`;
|
|
234
|
+
const cached = profileChoiceCache.get(cacheKey);
|
|
235
|
+
if (cached)
|
|
236
|
+
return cached;
|
|
237
|
+
const dir = path.join(resolveConfigRoot(), 'profiles', profile);
|
|
238
|
+
const file = path.join(dir, MACHINE_IDENTITY_FILENAME);
|
|
239
|
+
let chosen = readPersistedChoice(file, hardware, profile);
|
|
240
|
+
if (!chosen) {
|
|
241
|
+
const preExisting = PRE_EXISTING_PROFILE_EVIDENCE.some((name) => {
|
|
242
|
+
try {
|
|
243
|
+
return fs.existsSync(path.join(dir, name));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
chosen = preExisting ? hardware : qualifyMachineUuid(hardware, profile);
|
|
250
|
+
persistChoice(dir, file, hardware, chosen, preExisting ? 'pre_existing_profile' : 'new_profile');
|
|
251
|
+
}
|
|
252
|
+
profileChoiceCache.set(cacheKey, chosen);
|
|
253
|
+
return chosen;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Return the `machineUuid` this process sends to the server.
|
|
257
|
+
*
|
|
258
|
+
* Callers should treat this as opaque: the format differs per platform
|
|
259
|
+
* (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid), and a
|
|
260
|
+
* non-default profile that started new carries a `:p-<hash>` suffix. The
|
|
261
|
+
* guarantee is only: same machine + same profile -> same value across reboots,
|
|
262
|
+
* and the same value from every binary of that profile that calls this function.
|
|
263
|
+
*
|
|
264
|
+
* Equals the hardware identity for the default profile, for a hosted runtime
|
|
265
|
+
* (the control-plane-issued `OVERSKY_MACHINE_UUID` wins) and for a non-default
|
|
266
|
+
* profile that already held credentials; see the block above.
|
|
267
|
+
*/
|
|
268
|
+
export function getMachineUuid() {
|
|
269
|
+
const hardware = getHardwareMachineUuid();
|
|
270
|
+
// A hosted runtime's identity is ISSUED and fenced by the control plane.
|
|
271
|
+
if (hostedRuntimeIdOverride('OVERSKY_MACHINE_UUID'))
|
|
272
|
+
return hardware;
|
|
273
|
+
const profile = activeMachineProfile;
|
|
274
|
+
if (!profile || profile === DEFAULT_PROFILE_NAME)
|
|
275
|
+
return hardware;
|
|
276
|
+
return resolveProfileMachineUuid(profile, hardware);
|
|
277
|
+
}
|
|
144
278
|
/** @internal test seam — forget the cached value so the next call re-probes. */
|
|
145
279
|
export function __resetMachineUuidCacheForTest() {
|
|
146
280
|
cachedMachineUuid = null;
|
|
281
|
+
activeMachineProfile = null;
|
|
282
|
+
profileChoiceCache.clear();
|
|
147
283
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skrr-ai/auth-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"types": "dist/esm/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -84,6 +84,16 @@
|
|
|
84
84
|
"default": "./dist/cjs/cliHandoffWire.js"
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
"./dedicated-runtime-busy-wire": {
|
|
88
|
+
"import": {
|
|
89
|
+
"types": "./dist/esm/dedicatedRuntimeBusyWire.d.ts",
|
|
90
|
+
"default": "./dist/esm/dedicatedRuntimeBusyWire.js"
|
|
91
|
+
},
|
|
92
|
+
"require": {
|
|
93
|
+
"types": "./dist/cjs/dedicatedRuntimeBusyWire.d.ts",
|
|
94
|
+
"default": "./dist/cjs/dedicatedRuntimeBusyWire.js"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
87
97
|
"./computer-wire": {
|
|
88
98
|
"import": {
|
|
89
99
|
"types": "./dist/esm/computerWire.d.ts",
|
|
@@ -113,6 +123,16 @@
|
|
|
113
123
|
"types": "./dist/cjs/machineUuid.d.ts",
|
|
114
124
|
"default": "./dist/cjs/machineUuid.js"
|
|
115
125
|
}
|
|
126
|
+
},
|
|
127
|
+
"./dedicated-runtime-wake-wire": {
|
|
128
|
+
"import": {
|
|
129
|
+
"types": "./dist/esm/dedicatedRuntimeWakeWire.d.ts",
|
|
130
|
+
"default": "./dist/esm/dedicatedRuntimeWakeWire.js"
|
|
131
|
+
},
|
|
132
|
+
"require": {
|
|
133
|
+
"types": "./dist/cjs/dedicatedRuntimeWakeWire.d.ts",
|
|
134
|
+
"default": "./dist/cjs/dedicatedRuntimeWakeWire.js"
|
|
135
|
+
}
|
|
116
136
|
}
|
|
117
137
|
},
|
|
118
138
|
"dependencies": {
|