@skrr-ai/cli 0.1.48 → 0.1.49
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/agent-worker.d.ts +12 -0
- package/dist/commands/agent-worker.js +140 -0
- package/dist/lib/daemon-installer.d.ts +6 -5
- package/dist/lib/daemon-installer.js +4 -17
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +1 -1
- 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 +4 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +66 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +1 -1
- 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/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +64 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/oclif.manifest.json +13832 -13795
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class AgentWorker extends Command {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static flags: {
|
|
6
|
+
socket: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
|
+
'heartbeat-ms': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
/** `revoked`/`invalid_handle` — the binding is gone, not the transport. */
|
|
11
|
+
private bindingRefused;
|
|
12
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const guest_service_1 = require("../lib/guest-service");
|
|
5
|
+
/**
|
|
6
|
+
* `skrr agent-worker` — the long-lived guest agent worker (§10.2).
|
|
7
|
+
*
|
|
8
|
+
* The blessed `agent` service template runs this command inside a Dedicated
|
|
9
|
+
* Runtime guest. The control plane mints the binding at service-create admit
|
|
10
|
+
* and injects it into the spec's env:
|
|
11
|
+
*
|
|
12
|
+
* SKRR_AGENT_ID the agent this worker holds presence for
|
|
13
|
+
* SKRR_AGENT_HANDLE the presence capability — `awh_<id>.aws_<secret>`
|
|
14
|
+
*
|
|
15
|
+
* On start the worker registers with the daemon's broker socket, then
|
|
16
|
+
* heartbeats until it is stopped (SIGTERM → bye) or the binding is revoked —
|
|
17
|
+
* `service remove` deletes the spec, the broker's next answer is `revoked`,
|
|
18
|
+
* and the worker exits. The service lifecycle IS the handle lifecycle.
|
|
19
|
+
*
|
|
20
|
+
* The handle is never logged and never echoed — the daemon's reply names the
|
|
21
|
+
* agent and service it validated, which is everything worth printing.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_SOCKET = '/run/skrr-dedicated-runtime/agent-worker/agent-worker.sock';
|
|
24
|
+
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
25
|
+
/** Register retries cover a service started while the daemon is restarting. */
|
|
26
|
+
const REGISTER_RETRY_MS = 3_000;
|
|
27
|
+
const REGISTER_BUDGET_MS = 120_000;
|
|
28
|
+
/** Consecutive transport failures before the worker gives up and lets the reconciler see the crash. */
|
|
29
|
+
const HEARTBEAT_FAILURE_LIMIT = 10;
|
|
30
|
+
const BYE_TIMEOUT_MS = 5_000;
|
|
31
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
32
|
+
class AgentWorker extends core_1.Command {
|
|
33
|
+
static summary = 'Run the agent-worker presence loop for this machine (launched by the agent service template)';
|
|
34
|
+
static description = 'Reads the server-issued binding from SKRR_AGENT_ID / SKRR_AGENT_HANDLE, registers with ' +
|
|
35
|
+
'the machine’s daemon over the agent-worker socket, and holds presence with heartbeats ' +
|
|
36
|
+
'until the service is stopped or the binding is revoked. Not meant to be run by hand — ' +
|
|
37
|
+
'declare it with `skrr machines dedicated service create --template agent --param agent=<agent-id>` ' +
|
|
38
|
+
'or the service API.';
|
|
39
|
+
static flags = {
|
|
40
|
+
socket: core_1.Flags.string({
|
|
41
|
+
description: 'Agent-worker socket path (defaults to the machine’s broker socket)',
|
|
42
|
+
env: 'SKRR_AGENT_WORKER_SOCKET',
|
|
43
|
+
}),
|
|
44
|
+
'heartbeat-ms': core_1.Flags.integer({
|
|
45
|
+
description: 'Heartbeat interval override in milliseconds (the broker’s answer wins)',
|
|
46
|
+
min: 1000,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
async run() {
|
|
50
|
+
const { flags } = await this.parse(AgentWorker);
|
|
51
|
+
const agentId = (process.env.SKRR_AGENT_ID ?? '').trim();
|
|
52
|
+
const handle = (process.env.SKRR_AGENT_HANDLE ?? '').trim();
|
|
53
|
+
if (!agentId || !handle) {
|
|
54
|
+
this.error('SKRR_AGENT_ID and SKRR_AGENT_HANDLE must both be set — the control plane injects them ' +
|
|
55
|
+
'when a service is created with an agent binding. This process is not meant to be run ' +
|
|
56
|
+
'by hand; create the service with `--template agent --param agent=<agent-id>` instead.', { exit: 2 });
|
|
57
|
+
}
|
|
58
|
+
const socketPath = flags.socket ?? DEFAULT_SOCKET;
|
|
59
|
+
const heartbeatOverride = flags['heartbeat-ms'];
|
|
60
|
+
const call = (verb, timeoutMs) => (0, guest_service_1.callGuestServiceSocket)({ verb, handle }, { socketPath, ...(timeoutMs ? { timeoutMs } : {}) });
|
|
61
|
+
// ---- register --------------------------------------------------------
|
|
62
|
+
// The service can start while the daemon is restarting; transport errors
|
|
63
|
+
// retry inside a budget, a refused binding never does.
|
|
64
|
+
const deadline = Date.now() + REGISTER_BUDGET_MS;
|
|
65
|
+
let heartbeatMs = heartbeatOverride ?? DEFAULT_HEARTBEAT_MS;
|
|
66
|
+
for (;;) {
|
|
67
|
+
try {
|
|
68
|
+
const reply = await call('register');
|
|
69
|
+
if (typeof reply.agentId === 'string' && reply.agentId !== agentId) {
|
|
70
|
+
this.error(`The daemon validated this handle for ${reply.agentId}, not ${agentId} — ` +
|
|
71
|
+
'the service spec is inconsistent. Remove and recreate the service.', { exit: 2 });
|
|
72
|
+
}
|
|
73
|
+
if (typeof reply.heartbeatMs === 'number' && reply.heartbeatMs >= 1000) {
|
|
74
|
+
heartbeatMs = reply.heartbeatMs;
|
|
75
|
+
}
|
|
76
|
+
this.log(`agent-worker registered: agent=${reply.agentId ?? agentId} ` +
|
|
77
|
+
`service=${reply.serviceName ?? '?'} heartbeat=${heartbeatMs}ms`);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (this.bindingRefused(error)) {
|
|
82
|
+
this.error(`The daemon refused this worker's binding: ${error.message}. ` +
|
|
83
|
+
'The service spec predates the handle — remove and recreate the service.', { exit: 1 });
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() >= deadline) {
|
|
86
|
+
this.error(`The agent-worker socket never answered within ${REGISTER_BUDGET_MS / 1000}s ` +
|
|
87
|
+
`(${error.message}) — the machine's daemon is not offering the ` +
|
|
88
|
+
'agent-worker lane.', { exit: 1 });
|
|
89
|
+
}
|
|
90
|
+
await sleep(REGISTER_RETRY_MS);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ---- heartbeat -------------------------------------------------------
|
|
94
|
+
let failures = 0;
|
|
95
|
+
let stopping = false;
|
|
96
|
+
const stop = () => {
|
|
97
|
+
stopping = true;
|
|
98
|
+
};
|
|
99
|
+
process.on('SIGTERM', stop);
|
|
100
|
+
process.on('SIGINT', stop);
|
|
101
|
+
while (!stopping) {
|
|
102
|
+
await sleep(heartbeatMs);
|
|
103
|
+
if (stopping)
|
|
104
|
+
break;
|
|
105
|
+
try {
|
|
106
|
+
await call('heartbeat');
|
|
107
|
+
failures = 0;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (this.bindingRefused(error)) {
|
|
111
|
+
// The binding is dead — almost always `service remove` having
|
|
112
|
+
// deleted the spec. Exit cleanly: there is nothing to retry, and
|
|
113
|
+
// under an on-failure restart policy a clean exit keeps the
|
|
114
|
+
// reconciler from flapping a corpse.
|
|
115
|
+
this.log('The agent binding was revoked — exiting.');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
failures += 1;
|
|
119
|
+
if (failures >= HEARTBEAT_FAILURE_LIMIT) {
|
|
120
|
+
this.error(`Lost the agent-worker socket ${failures} times in a row ` +
|
|
121
|
+
`(${error.message}) — exiting so the service state is honest.`, { exit: 1 });
|
|
122
|
+
}
|
|
123
|
+
this.warn(`agent-worker heartbeat missed (${failures}/${HEARTBEAT_FAILURE_LIMIT})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ---- bye ---------------------------------------------------------------
|
|
127
|
+
try {
|
|
128
|
+
await call('bye', BYE_TIMEOUT_MS);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* a dead socket cannot hear goodbye — the missed heartbeats say it */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** `revoked`/`invalid_handle` — the binding is gone, not the transport. */
|
|
135
|
+
bindingRefused(error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
return /revoked|invalid_handle|forbidden/i.test(message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.default = AgentWorker;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type DaemonVersionManifest, type ReleaseKey } from '@skrr-ai/auth-core';
|
|
1
|
+
import { isTrustedArtifactOrigin, type DaemonVersionManifest, type ReleaseKey } from '@skrr-ai/auth-core';
|
|
2
2
|
/** Public CloudFront feed. Mirrors the daemon's own `CLOUDFRONT_FEED_BASE`. */
|
|
3
|
-
export declare const DEFAULT_DAEMON_FEED_BASE = "https://updates.
|
|
3
|
+
export declare const DEFAULT_DAEMON_FEED_BASE = "https://updates.skrr.ai/daemon";
|
|
4
4
|
/** Same override the daemon honours, so one env var steers both. */
|
|
5
5
|
export declare const DAEMON_FEED_BASE_ENV = "OVERSKY_DAEMON_UPDATE_FEED_BASE";
|
|
6
6
|
export type DaemonInstallFailureCode = 'unsupported_platform' | 'manifest_unreachable' | 'manifest_malformed' | 'manifest_unsigned' | 'platform_missing' | 'untrusted_artifact_origin' | 'download_failed' | 'checksum_mismatch' | 'artifact_unsigned' | 'write_failed';
|
|
@@ -44,10 +44,11 @@ export declare function daemonBinaryPath(env?: NodeJS.ProcessEnv): string;
|
|
|
44
44
|
export declare function daemonPlatformKey(platform?: string, arch?: string): string | null;
|
|
45
45
|
/**
|
|
46
46
|
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* The predicate is shared with the daemon's self-updater via
|
|
48
|
+
* `@skrr-ai/auth-core` — during the host transition a signed manifest may
|
|
49
|
+
* legitimately point at either update origin (see TRUSTED_FEED_ORIGINS there).
|
|
49
50
|
*/
|
|
50
|
-
export
|
|
51
|
+
export { isTrustedArtifactOrigin };
|
|
51
52
|
/** Fetch `<feedBase>/latest/daemon-version.json` and verify its signature. */
|
|
52
53
|
export declare function fetchVerifiedDaemonManifest(opts?: DaemonInstallOptions): Promise<{
|
|
53
54
|
ok: true;
|
|
@@ -3,11 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DAEMON_FEED_BASE_ENV = exports.DEFAULT_DAEMON_FEED_BASE = void 0;
|
|
6
|
+
exports.isTrustedArtifactOrigin = exports.DAEMON_FEED_BASE_ENV = exports.DEFAULT_DAEMON_FEED_BASE = void 0;
|
|
7
7
|
exports.daemonFeedBase = daemonFeedBase;
|
|
8
8
|
exports.daemonBinaryPath = daemonBinaryPath;
|
|
9
9
|
exports.daemonPlatformKey = daemonPlatformKey;
|
|
10
|
-
exports.isTrustedArtifactOrigin = isTrustedArtifactOrigin;
|
|
11
10
|
exports.fetchVerifiedDaemonManifest = fetchVerifiedDaemonManifest;
|
|
12
11
|
exports.installDaemon = installDaemon;
|
|
13
12
|
/**
|
|
@@ -63,8 +62,9 @@ const node_fs_1 = require("node:fs");
|
|
|
63
62
|
const node_os_1 = __importDefault(require("node:os"));
|
|
64
63
|
const node_path_1 = __importDefault(require("node:path"));
|
|
65
64
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
65
|
+
Object.defineProperty(exports, "isTrustedArtifactOrigin", { enumerable: true, get: function () { return auth_core_1.isTrustedArtifactOrigin; } });
|
|
66
66
|
/** Public CloudFront feed. Mirrors the daemon's own `CLOUDFRONT_FEED_BASE`. */
|
|
67
|
-
exports.DEFAULT_DAEMON_FEED_BASE = 'https://updates.
|
|
67
|
+
exports.DEFAULT_DAEMON_FEED_BASE = 'https://updates.skrr.ai/daemon';
|
|
68
68
|
/** Same override the daemon honours, so one env var steers both. */
|
|
69
69
|
exports.DAEMON_FEED_BASE_ENV = 'OVERSKY_DAEMON_UPDATE_FEED_BASE';
|
|
70
70
|
/** Refuse a manifest larger than this. It is a small JSON document. */
|
|
@@ -102,19 +102,6 @@ function daemonPlatformKey(platform = process.platform, arch = process.arch) {
|
|
|
102
102
|
const key = `${platform}-${arch}`;
|
|
103
103
|
return supported.has(key) ? key : null;
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
106
|
-
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
107
|
-
* Same rule as the daemon's `isTrustedArtifactOrigin`: a signed manifest still
|
|
108
|
-
* does not get to point the download at another origin.
|
|
109
|
-
*/
|
|
110
|
-
function isTrustedArtifactOrigin(candidate, feedBase) {
|
|
111
|
-
try {
|
|
112
|
-
return new URL(candidate).origin === new URL(feedBase).origin;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
105
|
function fail(code, error) {
|
|
119
106
|
return { ok: false, code, error };
|
|
120
107
|
}
|
|
@@ -193,7 +180,7 @@ async function installDaemon(opts = {}) {
|
|
|
193
180
|
if (!artifact?.url || !artifact.sha256) {
|
|
194
181
|
return fail('platform_missing', `release ${manifest.version} publishes no artifact for ${platformKey}`);
|
|
195
182
|
}
|
|
196
|
-
if (!isTrustedArtifactOrigin(artifact.url, feedBase)) {
|
|
183
|
+
if (!(0, auth_core_1.isTrustedArtifactOrigin)(artifact.url, feedBase)) {
|
|
197
184
|
return fail('untrusted_artifact_origin', `manifest points ${platformKey} at ${artifact.url}, which is not on the feed's origin`);
|
|
198
185
|
}
|
|
199
186
|
const artifactVerdict = (0, auth_core_1.verifyArtifact)(platformKey, manifest.version, artifact, keys);
|
|
@@ -223,7 +223,7 @@ export declare function firstPartyHarnessManifestFile(provider?: string): string
|
|
|
223
223
|
* The update feed ORIGIN. Not a product name, and not moved by this identity:
|
|
224
224
|
* a feed host sits inside signed manifests (naming map §2 "Update feed").
|
|
225
225
|
*/
|
|
226
|
-
export declare const FIRST_PARTY_HARNESS_FEED_ORIGIN = "https://updates.
|
|
226
|
+
export declare const FIRST_PARTY_HARNESS_FEED_ORIGIN = "https://updates.skrr.ai";
|
|
227
227
|
/** Object-key prefix for one spelling's feed, e.g. `skrr-code/`. */
|
|
228
228
|
export declare function firstPartyHarnessFeedPrefix(provider?: string): string;
|
|
229
229
|
/**
|
|
@@ -339,7 +339,7 @@ function firstPartyHarnessManifestFile(provider = exports.FIRST_PARTY_HARNESS.pr
|
|
|
339
339
|
* The update feed ORIGIN. Not a product name, and not moved by this identity:
|
|
340
340
|
* a feed host sits inside signed manifests (naming map §2 "Update feed").
|
|
341
341
|
*/
|
|
342
|
-
exports.FIRST_PARTY_HARNESS_FEED_ORIGIN = 'https://updates.
|
|
342
|
+
exports.FIRST_PARTY_HARNESS_FEED_ORIGIN = 'https://updates.skrr.ai';
|
|
343
343
|
/** Object-key prefix for one spelling's feed, e.g. `skrr-code/`. */
|
|
344
344
|
function firstPartyHarnessFeedPrefix(provider = exports.FIRST_PARTY_HARNESS.provider) {
|
|
345
345
|
return `${provider}/`;
|
|
@@ -40,6 +40,6 @@ export * from './firstPartyHarness.js';
|
|
|
40
40
|
export { FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX, FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, firstPartyHarnessBinDir, firstPartyHarnessHomeDir, firstPartyHarnessInstalledBinaryPath, linkFirstPartyHarnessAliases, migrateFirstPartyHarnessHome, type FirstPartyHarnessHomeFs, type FirstPartyHarnessHomeMigration, } from './firstPartyHarnessHome.js';
|
|
41
41
|
export { DEFAULT_FIRST_PARTY_HARNESS_CHANNEL, FIRST_PARTY_HARNESS_CHANNELS, firstPartyHarnessChannelPrefix, firstPartyHarnessFeedBase, firstPartyHarnessFeedCandidates, isFirstPartyHarnessChannel, resolveChannelManifestUrl, resolveFirstPartyHarnessChannel, type FirstPartyHarnessChannel, type FirstPartyHarnessFeedCandidate, } from './firstPartyHarnessChannels.js';
|
|
42
42
|
export { OVERSKY_RELEASE_PUBLIC_KEYS, type ReleaseKey } from './releaseKeys.js';
|
|
43
|
-
export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
43
|
+
export { MAX_SIGS, TRUSTED_FEED_ORIGINS, buildArtifactMessage, buildManifestMessage, getDeltaFor, isTrustedArtifactOrigin, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
44
44
|
export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
|
|
45
45
|
export { PROFILES_SEGMENT, SUPERSEDED_MARKER_SUFFIX, SUPERSEDED_COPY_SUFFIX, profileStateDir, legacyProfileStateDir, resolveProfileStateDir, resolveProfileStateFile, profileStateFileLocations, profileStateFilePathsToClear, consolidateProfileState, type ResolvedProfileStateDir, type ConsolidationSpec, type ConsolidationStatus, type ConsolidationOutcome, } from './profileStateDir.js';
|
|
@@ -25,8 +25,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
25
25
|
exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.reauthCommand = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
|
|
26
26
|
exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = exports.challenge = void 0;
|
|
27
27
|
exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.hasCredEnvelopePathConflict = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = exports.tokenHash = void 0;
|
|
28
|
-
exports.
|
|
29
|
-
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = void 0;
|
|
28
|
+
exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.isTrustedArtifactOrigin = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.resolveFirstPartyHarnessChannel = exports.resolveChannelManifestUrl = exports.isFirstPartyHarnessChannel = exports.firstPartyHarnessFeedCandidates = exports.firstPartyHarnessFeedBase = exports.firstPartyHarnessChannelPrefix = exports.FIRST_PARTY_HARNESS_CHANNELS = exports.DEFAULT_FIRST_PARTY_HARNESS_CHANNEL = exports.migrateFirstPartyHarnessHome = exports.linkFirstPartyHarnessAliases = exports.firstPartyHarnessInstalledBinaryPath = exports.firstPartyHarnessHomeDir = exports.firstPartyHarnessBinDir = exports.FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = exports.FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_NAME_TOKENS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.harnessLoadsClaudeSkillsLayout = exports.claudeSkillsLayoutEvidence = exports.HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = exports.getHarnessTier = exports.HARNESS_TIERS = void 0;
|
|
29
|
+
exports.consolidateProfileState = exports.profileStateFilePathsToClear = exports.profileStateFileLocations = exports.resolveProfileStateFile = exports.resolveProfileStateDir = exports.legacyProfileStateDir = exports.profileStateDir = void 0;
|
|
30
30
|
// Daemon scope vocabulary — single source of truth for non-Mongoose tiers
|
|
31
31
|
// (daemon, desktop, cli). Schema-bound canonical lives in
|
|
32
32
|
// `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
|
|
@@ -361,9 +361,11 @@ var releaseKeys_js_1 = require("./releaseKeys.js");
|
|
|
361
361
|
Object.defineProperty(exports, "OVERSKY_RELEASE_PUBLIC_KEYS", { enumerable: true, get: function () { return releaseKeys_js_1.OVERSKY_RELEASE_PUBLIC_KEYS; } });
|
|
362
362
|
var releaseManifest_js_1 = require("./releaseManifest.js");
|
|
363
363
|
Object.defineProperty(exports, "MAX_SIGS", { enumerable: true, get: function () { return releaseManifest_js_1.MAX_SIGS; } });
|
|
364
|
+
Object.defineProperty(exports, "TRUSTED_FEED_ORIGINS", { enumerable: true, get: function () { return releaseManifest_js_1.TRUSTED_FEED_ORIGINS; } });
|
|
364
365
|
Object.defineProperty(exports, "buildArtifactMessage", { enumerable: true, get: function () { return releaseManifest_js_1.buildArtifactMessage; } });
|
|
365
366
|
Object.defineProperty(exports, "buildManifestMessage", { enumerable: true, get: function () { return releaseManifest_js_1.buildManifestMessage; } });
|
|
366
367
|
Object.defineProperty(exports, "getDeltaFor", { enumerable: true, get: function () { return releaseManifest_js_1.getDeltaFor; } });
|
|
368
|
+
Object.defineProperty(exports, "isTrustedArtifactOrigin", { enumerable: true, get: function () { return releaseManifest_js_1.isTrustedArtifactOrigin; } });
|
|
367
369
|
Object.defineProperty(exports, "verifyArtifact", { enumerable: true, get: function () { return releaseManifest_js_1.verifyArtifact; } });
|
|
368
370
|
Object.defineProperty(exports, "verifyManifest", { enumerable: true, get: function () { return releaseManifest_js_1.verifyManifest; } });
|
|
369
371
|
// The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
|
|
@@ -159,3 +159,41 @@ export declare function verifyManifest(manifest: DaemonVersionManifest, keys?: r
|
|
|
159
159
|
* against the signed `artifact.sha256`, not from this metadata).
|
|
160
160
|
*/
|
|
161
161
|
export declare function getDeltaFor(artifact: PlatformArtifact, fromVersion: string): DeltaDescriptor | null;
|
|
162
|
+
/**
|
|
163
|
+
* Origins an artifact may be served from, beyond the feed base this run used.
|
|
164
|
+
*
|
|
165
|
+
* WHY A SET AND NOT THE ONE ORIGIN
|
|
166
|
+
* --------------------------------
|
|
167
|
+
* The pin used to require the artifact origin to EQUAL the feed base's
|
|
168
|
+
* origin. That is correct with one host and unsatisfiable with two, which the
|
|
169
|
+
* skrr rename creates:
|
|
170
|
+
*
|
|
171
|
+
* - a shipped daemon/CLI has `updates.oversky.ai` compiled in, so a manifest
|
|
172
|
+
* advertising skrr.ai artifacts is refused and it can never self-update
|
|
173
|
+
* again — including to the build that would have fixed it;
|
|
174
|
+
* - a new build shipping `updates.skrr.ai` refuses the SAME manifest for the
|
|
175
|
+
* mirror-image reason.
|
|
176
|
+
*
|
|
177
|
+
* One manifest of absolute URLs therefore cannot serve both, and the update
|
|
178
|
+
* path is the one thing that must not break: a client that cannot update
|
|
179
|
+
* cannot be repaired remotely.
|
|
180
|
+
*
|
|
181
|
+
* Widening to a small, closed set of origins we publish keeps the property the
|
|
182
|
+
* pin exists for: with keys pinned the ed25519 signature already binds the
|
|
183
|
+
* URL, and this is defence-in-depth for when verification is disabled —
|
|
184
|
+
* stopping a manifest-integrity failure from redirecting a download to an
|
|
185
|
+
* ARBITRARY origin serving a hash-matching payload. An attacker gains nothing
|
|
186
|
+
* from a second origin that is also ours.
|
|
187
|
+
*
|
|
188
|
+
* Both entries are removed down to one when no shipped client uses the old
|
|
189
|
+
* host, per the add -> verify -> remove ordering.
|
|
190
|
+
*/
|
|
191
|
+
export declare const TRUSTED_FEED_ORIGINS: readonly string[];
|
|
192
|
+
/**
|
|
193
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
194
|
+
*
|
|
195
|
+
* The feed base's own origin always passes, so an override
|
|
196
|
+
* (OVERSKY_DAEMON_UPDATE_FEED_BASE, a canary, a test server) keeps working
|
|
197
|
+
* without being listed here.
|
|
198
|
+
*/
|
|
199
|
+
export declare function isTrustedArtifactOrigin(candidate: string, feedBase: string): boolean;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MAX_SIGS = void 0;
|
|
3
|
+
exports.TRUSTED_FEED_ORIGINS = exports.MAX_SIGS = void 0;
|
|
4
4
|
exports.buildArtifactMessage = buildArtifactMessage;
|
|
5
5
|
exports.buildManifestMessage = buildManifestMessage;
|
|
6
6
|
exports.verifyArtifact = verifyArtifact;
|
|
7
7
|
exports.verifyManifest = verifyManifest;
|
|
8
8
|
exports.getDeltaFor = getDeltaFor;
|
|
9
|
+
exports.isTrustedArtifactOrigin = isTrustedArtifactOrigin;
|
|
9
10
|
/**
|
|
10
11
|
* release-manifest.ts — signed daemon release manifest: types, canonical
|
|
11
12
|
* signature messages, and offline ed25519 verification.
|
|
@@ -233,3 +234,67 @@ function getDeltaFor(artifact, fromVersion) {
|
|
|
233
234
|
}
|
|
234
235
|
return d;
|
|
235
236
|
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Trusted artifact origins
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* Origins an artifact may be served from, beyond the feed base this run used.
|
|
242
|
+
*
|
|
243
|
+
* WHY A SET AND NOT THE ONE ORIGIN
|
|
244
|
+
* --------------------------------
|
|
245
|
+
* The pin used to require the artifact origin to EQUAL the feed base's
|
|
246
|
+
* origin. That is correct with one host and unsatisfiable with two, which the
|
|
247
|
+
* skrr rename creates:
|
|
248
|
+
*
|
|
249
|
+
* - a shipped daemon/CLI has `updates.oversky.ai` compiled in, so a manifest
|
|
250
|
+
* advertising skrr.ai artifacts is refused and it can never self-update
|
|
251
|
+
* again — including to the build that would have fixed it;
|
|
252
|
+
* - a new build shipping `updates.skrr.ai` refuses the SAME manifest for the
|
|
253
|
+
* mirror-image reason.
|
|
254
|
+
*
|
|
255
|
+
* One manifest of absolute URLs therefore cannot serve both, and the update
|
|
256
|
+
* path is the one thing that must not break: a client that cannot update
|
|
257
|
+
* cannot be repaired remotely.
|
|
258
|
+
*
|
|
259
|
+
* Widening to a small, closed set of origins we publish keeps the property the
|
|
260
|
+
* pin exists for: with keys pinned the ed25519 signature already binds the
|
|
261
|
+
* URL, and this is defence-in-depth for when verification is disabled —
|
|
262
|
+
* stopping a manifest-integrity failure from redirecting a download to an
|
|
263
|
+
* ARBITRARY origin serving a hash-matching payload. An attacker gains nothing
|
|
264
|
+
* from a second origin that is also ours.
|
|
265
|
+
*
|
|
266
|
+
* Both entries are removed down to one when no shipped client uses the old
|
|
267
|
+
* host, per the add -> verify -> remove ordering.
|
|
268
|
+
*/
|
|
269
|
+
exports.TRUSTED_FEED_ORIGINS = [
|
|
270
|
+
'https://updates.oversky.ai',
|
|
271
|
+
'https://updates.skrr.ai',
|
|
272
|
+
];
|
|
273
|
+
/**
|
|
274
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
275
|
+
*
|
|
276
|
+
* The feed base's own origin always passes, so an override
|
|
277
|
+
* (OVERSKY_DAEMON_UPDATE_FEED_BASE, a canary, a test server) keeps working
|
|
278
|
+
* without being listed here.
|
|
279
|
+
*/
|
|
280
|
+
function isTrustedArtifactOrigin(candidate, feedBase) {
|
|
281
|
+
let feedOrigin;
|
|
282
|
+
try {
|
|
283
|
+
feedOrigin = new URL(feedBase).origin;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
// Callers hand over either a bare origin (the daemon's self-updater) or a
|
|
289
|
+
// full artifact URL (the CLI installer) — both spell the same check.
|
|
290
|
+
let candidateOrigin = candidate;
|
|
291
|
+
try {
|
|
292
|
+
candidateOrigin = new URL(candidate).origin;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
/* not a URL — compare raw; it will not match anything */
|
|
296
|
+
}
|
|
297
|
+
if (candidateOrigin === feedOrigin)
|
|
298
|
+
return true;
|
|
299
|
+
return exports.TRUSTED_FEED_ORIGINS.includes(candidateOrigin);
|
|
300
|
+
}
|
|
@@ -223,7 +223,7 @@ export declare function firstPartyHarnessManifestFile(provider?: string): string
|
|
|
223
223
|
* The update feed ORIGIN. Not a product name, and not moved by this identity:
|
|
224
224
|
* a feed host sits inside signed manifests (naming map §2 "Update feed").
|
|
225
225
|
*/
|
|
226
|
-
export declare const FIRST_PARTY_HARNESS_FEED_ORIGIN = "https://updates.
|
|
226
|
+
export declare const FIRST_PARTY_HARNESS_FEED_ORIGIN = "https://updates.skrr.ai";
|
|
227
227
|
/** Object-key prefix for one spelling's feed, e.g. `skrr-code/`. */
|
|
228
228
|
export declare function firstPartyHarnessFeedPrefix(provider?: string): string;
|
|
229
229
|
/**
|
|
@@ -304,7 +304,7 @@ export function firstPartyHarnessManifestFile(provider = FIRST_PARTY_HARNESS.pro
|
|
|
304
304
|
* The update feed ORIGIN. Not a product name, and not moved by this identity:
|
|
305
305
|
* a feed host sits inside signed manifests (naming map §2 "Update feed").
|
|
306
306
|
*/
|
|
307
|
-
export const FIRST_PARTY_HARNESS_FEED_ORIGIN = 'https://updates.
|
|
307
|
+
export const FIRST_PARTY_HARNESS_FEED_ORIGIN = 'https://updates.skrr.ai';
|
|
308
308
|
/** Object-key prefix for one spelling's feed, e.g. `skrr-code/`. */
|
|
309
309
|
export function firstPartyHarnessFeedPrefix(provider = FIRST_PARTY_HARNESS.provider) {
|
|
310
310
|
return `${provider}/`;
|
|
@@ -40,6 +40,6 @@ export * from './firstPartyHarness.js';
|
|
|
40
40
|
export { FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX, FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, firstPartyHarnessBinDir, firstPartyHarnessHomeDir, firstPartyHarnessInstalledBinaryPath, linkFirstPartyHarnessAliases, migrateFirstPartyHarnessHome, type FirstPartyHarnessHomeFs, type FirstPartyHarnessHomeMigration, } from './firstPartyHarnessHome.js';
|
|
41
41
|
export { DEFAULT_FIRST_PARTY_HARNESS_CHANNEL, FIRST_PARTY_HARNESS_CHANNELS, firstPartyHarnessChannelPrefix, firstPartyHarnessFeedBase, firstPartyHarnessFeedCandidates, isFirstPartyHarnessChannel, resolveChannelManifestUrl, resolveFirstPartyHarnessChannel, type FirstPartyHarnessChannel, type FirstPartyHarnessFeedCandidate, } from './firstPartyHarnessChannels.js';
|
|
42
42
|
export { OVERSKY_RELEASE_PUBLIC_KEYS, type ReleaseKey } from './releaseKeys.js';
|
|
43
|
-
export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
43
|
+
export { MAX_SIGS, TRUSTED_FEED_ORIGINS, buildArtifactMessage, buildManifestMessage, getDeltaFor, isTrustedArtifactOrigin, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
44
44
|
export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
|
|
45
45
|
export { PROFILES_SEGMENT, SUPERSEDED_MARKER_SUFFIX, SUPERSEDED_COPY_SUFFIX, profileStateDir, legacyProfileStateDir, resolveProfileStateDir, resolveProfileStateFile, profileStateFileLocations, profileStateFilePathsToClear, consolidateProfileState, type ResolvedProfileStateDir, type ConsolidationSpec, type ConsolidationStatus, type ConsolidationOutcome, } from './profileStateDir.js';
|
|
@@ -161,7 +161,7 @@ export { DEFAULT_FIRST_PARTY_HARNESS_CHANNEL, FIRST_PARTY_HARNESS_CHANNELS, firs
|
|
|
161
161
|
// same manifest against the same root. See releaseManifest.ts for why a mirror
|
|
162
162
|
// would be the wrong shape here.
|
|
163
163
|
export { OVERSKY_RELEASE_PUBLIC_KEYS } from './releaseKeys.js';
|
|
164
|
-
export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, } from './releaseManifest.js';
|
|
164
|
+
export { MAX_SIGS, TRUSTED_FEED_ORIGINS, buildArtifactMessage, buildManifestMessage, getDeltaFor, isTrustedArtifactOrigin, verifyArtifact, verifyManifest, } from './releaseManifest.js';
|
|
165
165
|
// The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
|
|
166
166
|
// module for why re-deriving it kept producing divergences.
|
|
167
167
|
export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
|
|
@@ -159,3 +159,41 @@ export declare function verifyManifest(manifest: DaemonVersionManifest, keys?: r
|
|
|
159
159
|
* against the signed `artifact.sha256`, not from this metadata).
|
|
160
160
|
*/
|
|
161
161
|
export declare function getDeltaFor(artifact: PlatformArtifact, fromVersion: string): DeltaDescriptor | null;
|
|
162
|
+
/**
|
|
163
|
+
* Origins an artifact may be served from, beyond the feed base this run used.
|
|
164
|
+
*
|
|
165
|
+
* WHY A SET AND NOT THE ONE ORIGIN
|
|
166
|
+
* --------------------------------
|
|
167
|
+
* The pin used to require the artifact origin to EQUAL the feed base's
|
|
168
|
+
* origin. That is correct with one host and unsatisfiable with two, which the
|
|
169
|
+
* skrr rename creates:
|
|
170
|
+
*
|
|
171
|
+
* - a shipped daemon/CLI has `updates.oversky.ai` compiled in, so a manifest
|
|
172
|
+
* advertising skrr.ai artifacts is refused and it can never self-update
|
|
173
|
+
* again — including to the build that would have fixed it;
|
|
174
|
+
* - a new build shipping `updates.skrr.ai` refuses the SAME manifest for the
|
|
175
|
+
* mirror-image reason.
|
|
176
|
+
*
|
|
177
|
+
* One manifest of absolute URLs therefore cannot serve both, and the update
|
|
178
|
+
* path is the one thing that must not break: a client that cannot update
|
|
179
|
+
* cannot be repaired remotely.
|
|
180
|
+
*
|
|
181
|
+
* Widening to a small, closed set of origins we publish keeps the property the
|
|
182
|
+
* pin exists for: with keys pinned the ed25519 signature already binds the
|
|
183
|
+
* URL, and this is defence-in-depth for when verification is disabled —
|
|
184
|
+
* stopping a manifest-integrity failure from redirecting a download to an
|
|
185
|
+
* ARBITRARY origin serving a hash-matching payload. An attacker gains nothing
|
|
186
|
+
* from a second origin that is also ours.
|
|
187
|
+
*
|
|
188
|
+
* Both entries are removed down to one when no shipped client uses the old
|
|
189
|
+
* host, per the add -> verify -> remove ordering.
|
|
190
|
+
*/
|
|
191
|
+
export declare const TRUSTED_FEED_ORIGINS: readonly string[];
|
|
192
|
+
/**
|
|
193
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
194
|
+
*
|
|
195
|
+
* The feed base's own origin always passes, so an override
|
|
196
|
+
* (OVERSKY_DAEMON_UPDATE_FEED_BASE, a canary, a test server) keeps working
|
|
197
|
+
* without being listed here.
|
|
198
|
+
*/
|
|
199
|
+
export declare function isTrustedArtifactOrigin(candidate: string, feedBase: string): boolean;
|
|
@@ -225,3 +225,67 @@ export function getDeltaFor(artifact, fromVersion) {
|
|
|
225
225
|
}
|
|
226
226
|
return d;
|
|
227
227
|
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Trusted artifact origins
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
/**
|
|
232
|
+
* Origins an artifact may be served from, beyond the feed base this run used.
|
|
233
|
+
*
|
|
234
|
+
* WHY A SET AND NOT THE ONE ORIGIN
|
|
235
|
+
* --------------------------------
|
|
236
|
+
* The pin used to require the artifact origin to EQUAL the feed base's
|
|
237
|
+
* origin. That is correct with one host and unsatisfiable with two, which the
|
|
238
|
+
* skrr rename creates:
|
|
239
|
+
*
|
|
240
|
+
* - a shipped daemon/CLI has `updates.oversky.ai` compiled in, so a manifest
|
|
241
|
+
* advertising skrr.ai artifacts is refused and it can never self-update
|
|
242
|
+
* again — including to the build that would have fixed it;
|
|
243
|
+
* - a new build shipping `updates.skrr.ai` refuses the SAME manifest for the
|
|
244
|
+
* mirror-image reason.
|
|
245
|
+
*
|
|
246
|
+
* One manifest of absolute URLs therefore cannot serve both, and the update
|
|
247
|
+
* path is the one thing that must not break: a client that cannot update
|
|
248
|
+
* cannot be repaired remotely.
|
|
249
|
+
*
|
|
250
|
+
* Widening to a small, closed set of origins we publish keeps the property the
|
|
251
|
+
* pin exists for: with keys pinned the ed25519 signature already binds the
|
|
252
|
+
* URL, and this is defence-in-depth for when verification is disabled —
|
|
253
|
+
* stopping a manifest-integrity failure from redirecting a download to an
|
|
254
|
+
* ARBITRARY origin serving a hash-matching payload. An attacker gains nothing
|
|
255
|
+
* from a second origin that is also ours.
|
|
256
|
+
*
|
|
257
|
+
* Both entries are removed down to one when no shipped client uses the old
|
|
258
|
+
* host, per the add -> verify -> remove ordering.
|
|
259
|
+
*/
|
|
260
|
+
export const TRUSTED_FEED_ORIGINS = [
|
|
261
|
+
'https://updates.oversky.ai',
|
|
262
|
+
'https://updates.skrr.ai',
|
|
263
|
+
];
|
|
264
|
+
/**
|
|
265
|
+
* True when `candidate` may serve an artifact for a feed rooted at `feedBase`.
|
|
266
|
+
*
|
|
267
|
+
* The feed base's own origin always passes, so an override
|
|
268
|
+
* (OVERSKY_DAEMON_UPDATE_FEED_BASE, a canary, a test server) keeps working
|
|
269
|
+
* without being listed here.
|
|
270
|
+
*/
|
|
271
|
+
export function isTrustedArtifactOrigin(candidate, feedBase) {
|
|
272
|
+
let feedOrigin;
|
|
273
|
+
try {
|
|
274
|
+
feedOrigin = new URL(feedBase).origin;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
// Callers hand over either a bare origin (the daemon's self-updater) or a
|
|
280
|
+
// full artifact URL (the CLI installer) — both spell the same check.
|
|
281
|
+
let candidateOrigin = candidate;
|
|
282
|
+
try {
|
|
283
|
+
candidateOrigin = new URL(candidate).origin;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
/* not a URL — compare raw; it will not match anything */
|
|
287
|
+
}
|
|
288
|
+
if (candidateOrigin === feedOrigin)
|
|
289
|
+
return true;
|
|
290
|
+
return TRUSTED_FEED_ORIGINS.includes(candidateOrigin);
|
|
291
|
+
}
|