@camstack/server 1.1.75 → 1.2.0
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/agent/agent-cap-dispatch-service.js +93 -0
- package/dist/agent/agent-config.js +127 -0
- package/dist/agent/agent-deploy-swap.js +137 -0
- package/dist/agent/agent-group-runner.js +175 -0
- package/dist/agent/agent-http-auth.js +76 -0
- package/dist/agent/agent-http.js +444 -0
- package/dist/agent/agent-service.js +595 -0
- package/dist/agent/agent-update-service.js +184 -0
- package/dist/agent/apply-model-distribution.js +14 -0
- package/dist/agent/derive-hub-url.js +139 -0
- package/dist/agent/fetch-bundle-from-hub.js +46 -0
- package/dist/agent/main.js +1137 -0
- package/dist/agent/register-agent-cap-dispatch.js +37 -0
- package/dist/api/trpc/generated-cap-routers.js +24 -15
- package/dist/core/agent/agent-registry.service.js +4 -3
- package/dist/core/server-update/server-update.service.js +14 -5
- package/dist/core/server-update/system-exec-npm.js +33 -0
- package/dist/launcher.js +28 -5
- package/dist/node-role.js +11 -0
- package/dist/server-root/index.js +0 -8
- package/package.json +27 -14
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AgentUpdateService = exports.AGENT_RESTART_GRACE_MS = exports.AGENT_RUNTIME_ADDON_ID = void 0;
|
|
37
|
+
exports.isRunningInContainer = isRunningInContainer;
|
|
38
|
+
exports.resolveAgentPackageJsonPath = resolveAgentPackageJsonPath;
|
|
39
|
+
exports.scheduleAgentRestart = scheduleAgentRestart;
|
|
40
|
+
exports.agentRuntimeManifestEntry = agentRuntimeManifestEntry;
|
|
41
|
+
/**
|
|
42
|
+
* AgentUpdateService — agent-side adapter over the SHARED `RootUpdateService`
|
|
43
|
+
* (`@camstack/node-root`, bundled into the `@camstack/server` dist by tsup).
|
|
44
|
+
* Post-consolidation the agent boots the SAME `@camstack/server` closure as
|
|
45
|
+
* the hub (via `CAMSTACK_ROLE=agent`), so this service stages/applies
|
|
46
|
+
* `@camstack/server` closures in `<agentDataDir>/server-root/`, applied on
|
|
47
|
+
* restart by the shared STARTER (single-copy in-place swap — no probation,
|
|
48
|
+
* no auto-rollback).
|
|
49
|
+
*
|
|
50
|
+
* The engine lives in the shared core; this module binds the agent's
|
|
51
|
+
* parameters, which differ from the hub only in the RESTART seam:
|
|
52
|
+
* - spec `@camstack/server` + `dist/launcher.js` (the single root spec)
|
|
53
|
+
* - env markers `CAMSTACK_SERVER_{BOOT_MODE,ACTIVE_VERSION}` +
|
|
54
|
+
* `CAMSTACK_SEED_SERVER_DIR` (written by the shared starter / image) —
|
|
55
|
+
* identical to the hub, because the on-disk root is `@camstack/server`
|
|
56
|
+
* - restart = graceful self-SIGTERM → the supervisor (docker restart
|
|
57
|
+
* policy / Electron main) relaunches → the shared starter swaps the copy
|
|
58
|
+
* - confirmBootHealthy = the agent's `$hub.registerNode` reaching its
|
|
59
|
+
* acked state (wired in main.ts) — a single-copy GC sweep, no promotion
|
|
60
|
+
*
|
|
61
|
+
* Spec: docs/superpowers/specs/2026-07-18-single-framework-copy-collapse-design.md
|
|
62
|
+
*/
|
|
63
|
+
const fs = __importStar(require("node:fs"));
|
|
64
|
+
const path = __importStar(require("node:path"));
|
|
65
|
+
const index_js_1 = require("../server-root/index.js");
|
|
66
|
+
const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
|
|
67
|
+
/**
|
|
68
|
+
* The synthetic addonId the agent's OWN runtime registers infra providers
|
|
69
|
+
* under (no addon owns them — the bootstrap does). Appears in the agent's
|
|
70
|
+
* `$hub.registerNode` manifest so the hub's cap routing can reach the
|
|
71
|
+
* agent-hosted `server-management` provider via `nodeId` pinning.
|
|
72
|
+
*/
|
|
73
|
+
exports.AGENT_RUNTIME_ADDON_ID = 'agent-runtime';
|
|
74
|
+
/** Grace before the self-SIGTERM so the tRPC reply can flush to the hub. */
|
|
75
|
+
exports.AGENT_RESTART_GRACE_MS = 2_000;
|
|
76
|
+
/**
|
|
77
|
+
* Best-effort container detection for the restart-policy startup warning
|
|
78
|
+
* (security review #3). `/.dockerenv` is present in Docker/Podman containers;
|
|
79
|
+
* this is diagnostic only — never gates any logic.
|
|
80
|
+
*/
|
|
81
|
+
function isRunningInContainer(existsSyncFn) {
|
|
82
|
+
return existsSyncFn('/.dockerenv');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the RUNNING root (`@camstack/server`) package.json. The agent boots
|
|
86
|
+
* the same `@camstack/server` closure as the hub, whose dist lives at
|
|
87
|
+
* `<pkg>/dist/*.js`, so `../package.json` is the normal hit; the second
|
|
88
|
+
* candidate covers a nested layout.
|
|
89
|
+
*/
|
|
90
|
+
function resolveAgentPackageJsonPath(fromDir) {
|
|
91
|
+
const candidates = [
|
|
92
|
+
path.resolve(fromDir, '..', 'package.json'),
|
|
93
|
+
path.resolve(fromDir, '..', '..', 'package.json'),
|
|
94
|
+
];
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
try {
|
|
97
|
+
const raw = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
98
|
+
if (raw.name === index_js_1.HUB_ROOT_SPEC.packageName)
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* keep searching */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return candidates[0] ?? path.resolve(fromDir, '..', 'package.json');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Default restart seam: log, then after a short grace self-deliver SIGTERM so
|
|
109
|
+
* the bootstrap's graceful shutdown runs (addons stopped, broker stopped,
|
|
110
|
+
* exit 0) and the container restart policy relaunches into the starter. A
|
|
111
|
+
* hard exit fallback guards against a wedged shutdown.
|
|
112
|
+
*
|
|
113
|
+
* When `dataDir` is given, drop a restart-intent MARKER in the server-root dir
|
|
114
|
+
* BEFORE the SIGTERM so an external process supervisor (phase 3: the Electron
|
|
115
|
+
* shell) can tell this INTENTIONAL update/rollback restart apart from a crash
|
|
116
|
+
* and not penalise its crash-backoff. The docker restart policy ignores the
|
|
117
|
+
* marker (it relaunches regardless), so this is a no-op there. Best-effort: a
|
|
118
|
+
* marker-write failure must never block the restart.
|
|
119
|
+
*/
|
|
120
|
+
function scheduleAgentRestart(logger, requestedBy, dataDir) {
|
|
121
|
+
if (dataDir !== undefined && dataDir.length > 0) {
|
|
122
|
+
try {
|
|
123
|
+
(0, index_js_1.writeRestartIntentMarker)((0, index_js_1.serverRootDir)(dataDir), {
|
|
124
|
+
requestedAtMs: Date.now(),
|
|
125
|
+
reason: requestedBy,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
logger.warn('failed to write restart-intent marker', {
|
|
130
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
logger.warn('agent restart requested — exiting for supervisor relaunch', {
|
|
135
|
+
meta: { requestedBy, graceMs: exports.AGENT_RESTART_GRACE_MS },
|
|
136
|
+
});
|
|
137
|
+
const grace = setTimeout(() => {
|
|
138
|
+
process.kill(process.pid, 'SIGTERM');
|
|
139
|
+
const hard = setTimeout(() => process.exit(0), 15_000);
|
|
140
|
+
hard.unref();
|
|
141
|
+
}, exports.AGENT_RESTART_GRACE_MS);
|
|
142
|
+
grace.unref();
|
|
143
|
+
}
|
|
144
|
+
class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
145
|
+
constructor(options) {
|
|
146
|
+
super({
|
|
147
|
+
spec: index_js_1.HUB_ROOT_SPEC,
|
|
148
|
+
// The agent's on-disk root IS `@camstack/server` (booted via
|
|
149
|
+
// CAMSTACK_ROLE=agent by the shared starter), so it reads the SAME
|
|
150
|
+
// `CAMSTACK_SERVER_*` boot markers the starter writes.
|
|
151
|
+
envNames: {
|
|
152
|
+
bootMode: 'CAMSTACK_SERVER_BOOT_MODE',
|
|
153
|
+
activeVersion: 'CAMSTACK_SERVER_ACTIVE_VERSION',
|
|
154
|
+
seedDir: 'CAMSTACK_SEED_SERVER_DIR',
|
|
155
|
+
},
|
|
156
|
+
logger: options.logger,
|
|
157
|
+
restartServer: options.restartAgent ??
|
|
158
|
+
((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir)),
|
|
159
|
+
dataDir: options.dataDir,
|
|
160
|
+
runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
|
|
161
|
+
workspaceProbeDir: __dirname,
|
|
162
|
+
// Default to the runNpm-backed adapter — a packaged desktop agent ships a
|
|
163
|
+
// bare Node with no system npm, so the raw `execFile('npm')` default would
|
|
164
|
+
// ENOENT. `cacheDir` mirrors AddonInstaller's `<dataDir>/addons/.npm-bootstrap`.
|
|
165
|
+
execNpm: options.execNpm ??
|
|
166
|
+
(0, system_exec_npm_js_1.buildSystemExecNpm)({
|
|
167
|
+
cacheDir: path.join(options.dataDir, 'addons', '.npm-bootstrap'),
|
|
168
|
+
logger: options.logger,
|
|
169
|
+
}),
|
|
170
|
+
env: options.env,
|
|
171
|
+
now: options.now,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.AgentUpdateService = AgentUpdateService;
|
|
176
|
+
/**
|
|
177
|
+
* The manifest entry for the agent runtime's own providers. Appended to
|
|
178
|
+
* `buildAgentOwnManifest`'s output so `server-management` reaches the hub's
|
|
179
|
+
* `HubNodeRegistry` (and therefore `createCapabilityProxy` node routing) even
|
|
180
|
+
* though no `loadedAddons` entry owns it.
|
|
181
|
+
*/
|
|
182
|
+
function agentRuntimeManifestEntry() {
|
|
183
|
+
return { addonId: exports.AGENT_RUNTIME_ADDON_ID, capabilities: ['server-management'] };
|
|
184
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent-side model distribution (P2): pull a staged model tarball from the hub
|
|
4
|
+
* and untar it into this node's modelsDir. Factored behind a seam so the
|
|
5
|
+
* fetch/extract/fs effects are injectable in tests.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.applyModelDistribution = applyModelDistribution;
|
|
9
|
+
async function applyModelDistribution(seam, params) {
|
|
10
|
+
seam.mkdirp(seam.modelsDir);
|
|
11
|
+
const buffer = await seam.fetchBundle(params.source);
|
|
12
|
+
await seam.extract(buffer, seam.modelsDir);
|
|
13
|
+
return { success: true, modelId: params.modelId, format: params.format, path: seam.modelsDir };
|
|
14
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Derive the agent's `CAMSTACK_HUB_URL` from its friendly `hubAddress`.
|
|
4
|
+
*
|
|
5
|
+
* `hubAddress` is the agent's single source of truth for "where is the hub"
|
|
6
|
+
* (the Moleculer TCP dial target). The cross-node consumers of
|
|
7
|
+
* `CAMSTACK_HUB_URL` — the pipeline-runner remote-source leg and the
|
|
8
|
+
* cross-node recorder pull — only ever extract the HOSTNAME from it
|
|
9
|
+
* (`resolveHubHostname` / `extractHost` in
|
|
10
|
+
* `packages/addon-pipeline/src/shared/hub-hostname.ts`). Deriving the value
|
|
11
|
+
* here removes the historical requirement to set a second, redundant env var
|
|
12
|
+
* by hand on every remote agent (the cross-node-source trap).
|
|
13
|
+
*
|
|
14
|
+
* Accepts every friendly form `normalizeHubUrl`
|
|
15
|
+
* (`packages/system/src/kernel/moleculer/broker-factory.ts`) accepts:
|
|
16
|
+
* `host`, `host:port`, `host:port/nodeID`, `nodeID@host`,
|
|
17
|
+
* `nodeID@host:port`, and bracketed IPv6 (`[::1]:6000`).
|
|
18
|
+
* The optional `nodeID@` prefix and `/nodeID` suffix are stripped and the
|
|
19
|
+
* Moleculer `:port` (6000-family) is dropped — only the host survives.
|
|
20
|
+
*
|
|
21
|
+
* Port note: the emitted `:4443` is the DEFAULT hub API port (matching the
|
|
22
|
+
* hub-side default in `server/backend/src/api/addon-upload.ts` and the
|
|
23
|
+
* manually-proven value used on live remote agents), NOT a probed value.
|
|
24
|
+
* Because every current consumer keeps only the host, the port is convention;
|
|
25
|
+
* a future full-URL consumer must treat it as the default, not authoritative.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.deriveHubUrlFromHubAddress = deriveHubUrlFromHubAddress;
|
|
29
|
+
exports.deriveHubUrlForExport = deriveHubUrlForExport;
|
|
30
|
+
exports.pickIpv4 = pickIpv4;
|
|
31
|
+
exports.normalizeObservedHost = normalizeObservedHost;
|
|
32
|
+
exports.deriveHubUrlFromRegistry = deriveHubUrlFromRegistry;
|
|
33
|
+
const HUB_API_PORT = 4443;
|
|
34
|
+
function deriveHubUrlFromHubAddress(hubAddress) {
|
|
35
|
+
const trimmed = hubAddress.trim();
|
|
36
|
+
if (trimmed.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
// Strip an optional `nodeID@` prefix.
|
|
39
|
+
const afterAt = trimmed.includes('@') ? (trimmed.split('@')[1] ?? '') : trimmed;
|
|
40
|
+
// Strip an optional `/nodeID` suffix — keep only the `host[:port]` authority.
|
|
41
|
+
const authority = afterAt.split('/')[0] ?? '';
|
|
42
|
+
const host = extractHostFromAuthority(authority);
|
|
43
|
+
if (host === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
return `https://${host}:${HUB_API_PORT}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute the `CAMSTACK_HUB_URL` value the agent should export, or `undefined`
|
|
49
|
+
* when it must be left untouched. An explicit operator-provided env value
|
|
50
|
+
* (`hubUrlWasExplicit`) always wins and is never clobbered — this preserves
|
|
51
|
+
* today's working deployments. Kept pure (env is passed in) so both the
|
|
52
|
+
* boot-time derivation and the reconnect path can be unit-tested.
|
|
53
|
+
*/
|
|
54
|
+
function deriveHubUrlForExport(hubUrlWasExplicit, hubAddress) {
|
|
55
|
+
if (hubUrlWasExplicit)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (hubAddress === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
return deriveHubUrlFromHubAddress(hubAddress);
|
|
60
|
+
}
|
|
61
|
+
/** Take the bare host from a `host[:port]` authority, keeping bracketed IPv6. */
|
|
62
|
+
function extractHostFromAuthority(authority) {
|
|
63
|
+
const value = authority.trim();
|
|
64
|
+
if (value.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (value.startsWith('[')) {
|
|
67
|
+
const end = value.indexOf(']');
|
|
68
|
+
if (end > 0)
|
|
69
|
+
return value.slice(0, end + 1);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const host = value.split(':')[0] ?? '';
|
|
73
|
+
return host.length > 0 ? host : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Node id the hub broker always registers under. */
|
|
76
|
+
const HUB_NODE_ID = 'hub';
|
|
77
|
+
const IPV4_MAPPED_PREFIX = '::ffff:';
|
|
78
|
+
const IPV4_DOTTED_QUAD = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
79
|
+
/**
|
|
80
|
+
* First non-internal IPv4 in a Moleculer node's advertised IP list.
|
|
81
|
+
* Canonical home of this helper — `resolveHubEndpoint` in `agent-http.ts`
|
|
82
|
+
* (UI-facing `resolvedHubAddress`) re-exports and reuses it so the UI display
|
|
83
|
+
* and the exported `CAMSTACK_HUB_URL` always agree.
|
|
84
|
+
*/
|
|
85
|
+
function pickIpv4(ipList) {
|
|
86
|
+
if (!ipList)
|
|
87
|
+
return null;
|
|
88
|
+
for (const ip of ipList) {
|
|
89
|
+
if (ip.includes(':'))
|
|
90
|
+
continue; // skip IPv6
|
|
91
|
+
if (ip.startsWith('127.'))
|
|
92
|
+
continue; // skip loopback
|
|
93
|
+
return ip;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a socket-observed address so it survives a WHATWG-`URL` hostname
|
|
99
|
+
* write (`substituteRtspHost`): strip the `::ffff:` IPv4-mapped prefix,
|
|
100
|
+
* bracket bare IPv6 (an unbracketed IPv6 assigned to `url.hostname` is
|
|
101
|
+
* silently IGNORED — the URL would keep 127.0.0.1), pass plain IPv4 /
|
|
102
|
+
* hostnames / already-bracketed IPv6 through untouched.
|
|
103
|
+
*/
|
|
104
|
+
function normalizeObservedHost(raw) {
|
|
105
|
+
if (raw === undefined || raw === null)
|
|
106
|
+
return undefined;
|
|
107
|
+
const trimmed = raw.trim();
|
|
108
|
+
if (trimmed.length === 0)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (trimmed.startsWith('['))
|
|
111
|
+
return trimmed; // already-bracketed IPv6
|
|
112
|
+
if (trimmed.toLowerCase().startsWith(IPV4_MAPPED_PREFIX)) {
|
|
113
|
+
const mapped = trimmed.slice(IPV4_MAPPED_PREFIX.length);
|
|
114
|
+
if (IPV4_DOTTED_QUAD.test(mapped))
|
|
115
|
+
return mapped;
|
|
116
|
+
}
|
|
117
|
+
// Any remaining colon means bare IPv6 — bracket it.
|
|
118
|
+
if (trimmed.includes(':'))
|
|
119
|
+
return `[${trimmed}]`;
|
|
120
|
+
return trimmed;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Best hub host from the agent-side Moleculer registry view:
|
|
124
|
+
* `udpAddress` (wire truth) → first non-loopback IPv4 in `ipList`
|
|
125
|
+
* (hub self-advertised; container-internal in bridge-mode Docker) →
|
|
126
|
+
* `hostname` (hub's `os.hostname()`; often unresolvable — last resort).
|
|
127
|
+
* Returns `undefined` when the hub node is not (yet) known. Emits the SAME
|
|
128
|
+
* `https://host:4443` shape as `deriveHubUrlFromHubAddress` — see the port
|
|
129
|
+
* note at the top of this file.
|
|
130
|
+
*/
|
|
131
|
+
function deriveHubUrlFromRegistry(nodes) {
|
|
132
|
+
const hub = nodes.find((node) => node.id === HUB_NODE_ID);
|
|
133
|
+
if (hub === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
const host = normalizeObservedHost(hub.udpAddress) ?? pickIpv4(hub.ipList) ?? hub.hostname;
|
|
136
|
+
if (host === undefined || host.length === 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
return `https://${host}:${HUB_API_PORT}`;
|
|
139
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchBundleFromHub = fetchBundleFromHub;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const undici_1 = require("undici");
|
|
6
|
+
/**
|
|
7
|
+
* Stream a staged addon tgz from the hub's one-time-token bundle route and
|
|
8
|
+
* verify integrity (byte count + sha256) before handing it to the installer.
|
|
9
|
+
*/
|
|
10
|
+
async function fetchBundleFromHub(opts) {
|
|
11
|
+
// When a fetchImpl is injected (tests), call it as-is — the stub ignores dispatcher.
|
|
12
|
+
// For production (global fetch against the real hub's self-signed TLS cert), use
|
|
13
|
+
// undici.fetch with an Agent that disables CA verification. The hub serves HTTPS with a
|
|
14
|
+
// self-signed cert; bare fetch rejects it with UNABLE_TO_VERIFY_LEAF_SIGNATURE.
|
|
15
|
+
// Integrity is independently guaranteed by the sha256+bytes checks below; trust is
|
|
16
|
+
// established by the cluster relationship — consistent with the CLI's approach.
|
|
17
|
+
//
|
|
18
|
+
// We call undici.fetch (not the global fetch) so that `Agent` and `RequestInit` share
|
|
19
|
+
// the same undici type declarations; the global fetch augmentation uses `undici-types`
|
|
20
|
+
// which ships a structurally incompatible `Dispatcher` type. Both return a structurally
|
|
21
|
+
// compatible Response; we narrow via the shared `ResponseLike` interface to avoid
|
|
22
|
+
// cross-package type unification.
|
|
23
|
+
// `accept-encoding: identity` — never let the hub gzip/brotli this binary
|
|
24
|
+
// pull. A Brotli-encoded response made undici's BrotliDecompress abort with
|
|
25
|
+
// "TypeError: terminated", silently breaking the pull. The hub also opts the
|
|
26
|
+
// route out of compression; this is the client-side belt-and-suspenders.
|
|
27
|
+
const authHeader = { Authorization: `Bearer ${opts.token}`, 'accept-encoding': 'identity' };
|
|
28
|
+
const res = opts.fetchImpl
|
|
29
|
+
? await opts.fetchImpl(opts.url, { headers: authHeader })
|
|
30
|
+
: await (0, undici_1.fetch)(opts.url, {
|
|
31
|
+
headers: authHeader,
|
|
32
|
+
dispatcher: new undici_1.Agent({ connect: { rejectUnauthorized: false } }),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`bundle fetch failed: HTTP ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
38
|
+
if (buffer.length !== opts.bytes) {
|
|
39
|
+
throw new Error(`bundle bytes mismatch: expected ${opts.bytes}, got ${buffer.length}`);
|
|
40
|
+
}
|
|
41
|
+
const sha = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
42
|
+
if (sha !== opts.sha256) {
|
|
43
|
+
throw new Error(`bundle sha256 mismatch: expected ${opts.sha256}, got ${sha}`);
|
|
44
|
+
}
|
|
45
|
+
return buffer;
|
|
46
|
+
}
|