@chordra.ai/ctl 0.0.0-stage → 0.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.
Files changed (3) hide show
  1. package/README.md +20 -2
  2. package/bin/chord-ctl +705 -0
  3. package/package.json +24 -4
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
- # Temporary Holding Version
1
+ # @chordra.ai/ctl
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@chordra.ai/ctl` is the transient bootstrap used to install or repair a Chordra Host. It provides the
4
+ `chordra-ai-ctl` command.
5
+ It obtains the server-selected native release, verifies it, and delegates setup to the
6
+ native runner.
7
+
8
+ Requires Node.js **22.22.0 or newer**. The setup command enforces the package's
9
+ `engines.node` requirement before making network requests or installing files,
10
+ even when npm's `engine-strict` is disabled.
11
+
12
+ ```sh
13
+ npx -y @chordra.ai/ctl@latest setup --server https://api.chordra.ai
14
+ ```
15
+
16
+ The bootstrap exits after handing control to the native runner. It does not install or
17
+ invoke a package manager while acquiring the selected release.
18
+
19
+ Its version belongs to this package's `package.json`, independently of the native release
20
+ selected by the server. Native daemon releases do not rebuild or republish this package.
21
+ Bootstrap changes use the [independent publication procedure](../../deploy/runbook.md#bootstrap-publication).
package/bin/chord-ctl ADDED
@@ -0,0 +1,705 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const crypto = require("node:crypto");
6
+ const { spawn } = require("node:child_process");
7
+ const fs = require("node:fs");
8
+ const http = require("node:http");
9
+ const https = require("node:https");
10
+ const os = require("node:os");
11
+ const path = require("node:path");
12
+ const { TextDecoder } = require("node:util");
13
+ const zlib = require("node:zlib");
14
+
15
+ const PACKAGE_NAME = "@sys9/chord-cli";
16
+ const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
17
+ const BOOTSTRAP_SCHEMA = 2;
18
+ const REQUEST_TIMEOUT_MS = 30_000;
19
+ const MAX_URL_BYTES = 4096;
20
+ const MAX_BOOTSTRAP_BYTES = 64 * 1024;
21
+ const MAX_METADATA_BYTES = 1024 * 1024;
22
+ const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
23
+ const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
24
+ const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
25
+ const MAX_ARCHIVE_ENTRIES = 1024;
26
+
27
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
28
+ const ENVIRONMENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
29
+
30
+ const PLATFORM_TAGS = {
31
+ "linux:x64": "linux-x64",
32
+ "linux:arm64": "linux-arm64",
33
+ "darwin:x64": "darwin-x64",
34
+ "darwin:arm64": "darwin-arm64",
35
+ };
36
+
37
+ const textDecoder = new TextDecoder("utf-8", { fatal: true });
38
+
39
+ function isPlainObject(value) {
40
+ return value !== null && typeof value === "object" && !Array.isArray(value);
41
+ }
42
+
43
+ function parseArguments(argv) {
44
+ if (!Array.isArray(argv) || argv.length === 0 || argv[0] !== "setup") {
45
+ throw new Error("expected: setup --server URL");
46
+ }
47
+
48
+ let server = null;
49
+ for (let index = 1; index < argv.length; index += 1) {
50
+ const argument = argv[index];
51
+ if (argument === "--server") {
52
+ if (server !== null || index + 1 >= argv.length) {
53
+ throw new Error("setup requires exactly one --server URL");
54
+ }
55
+ server = argv[index + 1];
56
+ index += 1;
57
+ continue;
58
+ }
59
+ if (argument.startsWith("--server=")) {
60
+ if (server !== null || argument.length === "--server=".length) {
61
+ throw new Error("setup requires exactly one --server URL");
62
+ }
63
+ server = argument.slice("--server=".length);
64
+ continue;
65
+ }
66
+ throw new Error("unsupported argument: " + argument);
67
+ }
68
+
69
+ if (server === null) {
70
+ throw new Error("missing required --server URL");
71
+ }
72
+ return { server };
73
+ }
74
+
75
+ function parseHTTPURL(value, label, { allowPath = false, allowQuery = false } = {}) {
76
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_URL_BYTES) {
77
+ throw new Error(label + " must be a bounded URL");
78
+ }
79
+ if (value !== value.trim()) {
80
+ throw new Error(label + " must not contain surrounding whitespace");
81
+ }
82
+
83
+ let parsed;
84
+ try {
85
+ parsed = new URL(value);
86
+ } catch {
87
+ throw new Error(label + " must be a valid URL");
88
+ }
89
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
90
+ throw new Error(label + " must use http or https");
91
+ }
92
+ if (parsed.username || parsed.password || parsed.hash || (!allowQuery && parsed.search)) {
93
+ throw new Error(
94
+ label +
95
+ (allowQuery
96
+ ? " must not contain credentials or fragment"
97
+ : " must not contain credentials, query, or fragment"),
98
+ );
99
+ }
100
+ if (!allowPath && parsed.pathname !== "/") {
101
+ throw new Error(label + " must be an HTTP origin");
102
+ }
103
+ return parsed;
104
+ }
105
+
106
+ function platformTag(platform = process.platform, arch = process.arch) {
107
+ const tag = PLATFORM_TAGS[platform + ":" + arch];
108
+ if (!tag) {
109
+ throw new Error("unsupported platform: " + platform + " (" + arch + ")");
110
+ }
111
+ return tag;
112
+ }
113
+
114
+ function isVersion(value) {
115
+ return typeof value === "string" && value.length <= 128 && VERSION_PATTERN.test(value);
116
+ }
117
+
118
+ function parseJSON(body, label) {
119
+ let text;
120
+ try {
121
+ text = textDecoder.decode(body);
122
+ } catch {
123
+ throw new Error(label + " is not valid UTF-8");
124
+ }
125
+ try {
126
+ return JSON.parse(text);
127
+ } catch {
128
+ throw new Error(label + " is not valid JSON");
129
+ }
130
+ }
131
+
132
+ function validateBootstrapResponse(payload) {
133
+ if (!isPlainObject(payload)) {
134
+ throw new Error("bootstrap response must be an object");
135
+ }
136
+ if (payload.schema_version !== 1 && payload.schema_version !== BOOTSTRAP_SCHEMA) {
137
+ throw new Error("unsupported bootstrap schema");
138
+ }
139
+ const expectedKeys = ["approved_cli_version", "canonical_environment_id", "schema_version"];
140
+ if (payload.schema_version === BOOTSTRAP_SCHEMA) {
141
+ expectedKeys.push("release");
142
+ }
143
+ if (Object.keys(payload).sort().join(",") !== expectedKeys.sort().join(",")) {
144
+ throw new Error("bootstrap response has an invalid schema");
145
+ }
146
+ if (
147
+ typeof payload.canonical_environment_id !== "string" ||
148
+ !ENVIRONMENT_ID_PATTERN.test(payload.canonical_environment_id)
149
+ ) {
150
+ throw new Error("bootstrap response has an invalid canonical_environment_id");
151
+ }
152
+ if (!isVersion(payload.approved_cli_version)) {
153
+ throw new Error("bootstrap response has an invalid version");
154
+ }
155
+ const release = payload.schema_version === 1
156
+ ? { sources: ["npm"], version: payload.approved_cli_version }
157
+ : validateRelease(payload.release, payload.approved_cli_version);
158
+ return {
159
+ environmentID: payload.canonical_environment_id,
160
+ version: payload.approved_cli_version,
161
+ release,
162
+ };
163
+ }
164
+
165
+ function validateRelease(release, approvedVersion) {
166
+ if (!isPlainObject(release) || release.version !== approvedVersion) {
167
+ throw new Error("bootstrap release does not match the approved version");
168
+ }
169
+ if (!Array.isArray(release.sources) || release.sources.length === 0 ||
170
+ release.sources.some((source) => source !== "npm" && source !== "native") ||
171
+ new Set(release.sources).size !== release.sources.length) {
172
+ throw new Error("bootstrap release has invalid sources");
173
+ }
174
+ if (!release.sources.includes("native")) {
175
+ if (Object.keys(release).sort().join(",") !== "sources,version") {
176
+ throw new Error("npm release has an invalid schema");
177
+ }
178
+ return release;
179
+ }
180
+ if (Object.keys(release).sort().join(",") !== "archive,sources,version" || !isPlainObject(release.archive)) {
181
+ throw new Error("native release has an invalid schema");
182
+ }
183
+ const archive = release.archive;
184
+ if (Object.keys(archive).sort().join(",") !== "goarch,goos,integrity,size,url") {
185
+ throw new Error("native archive has an invalid schema");
186
+ }
187
+ const arch = process.arch === "x64" ? "amd64" : process.arch;
188
+ if (archive.goos !== process.platform || archive.goarch !== arch) {
189
+ throw new Error("native archive does not match this platform");
190
+ }
191
+ if (!Number.isSafeInteger(archive.size) || archive.size <= 0 || archive.size > MAX_TARBALL_BYTES) {
192
+ throw new Error("native archive has an invalid size");
193
+ }
194
+ if (typeof archive.url !== "string" || Buffer.byteLength(archive.url) > MAX_URL_BYTES || /[\x00-\x20\x7f?#]/.test(archive.url)) {
195
+ throw new Error("native archive has an invalid URL");
196
+ }
197
+ const url = parseHTTPURL(archive.url, "native archive URL", { allowPath: true });
198
+ if (url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
199
+ throw new Error("native archive URL must use HTTPS");
200
+ }
201
+ const digests = parseIntegrity(archive.integrity);
202
+ if (digests.length !== 1 || archive.integrity !== "sha512-" + digests[0].toString("base64")) {
203
+ throw new Error("native archive has an invalid sha512 integrity");
204
+ }
205
+ return release;
206
+ }
207
+
208
+ function requestBytes(target, limit, label, accept) {
209
+ return new Promise((resolve, reject) => {
210
+ const client = target.protocol === "https:" ? https : http;
211
+ let settled = false;
212
+ let request;
213
+ let deadline;
214
+
215
+ const finish = (error, value) => {
216
+ if (settled) {
217
+ return;
218
+ }
219
+ settled = true;
220
+ clearTimeout(deadline);
221
+ if (error) {
222
+ reject(error);
223
+ } else {
224
+ resolve(value);
225
+ }
226
+ };
227
+
228
+ try {
229
+ request = client.get(
230
+ target,
231
+ {
232
+ headers: {
233
+ accept: accept || "*/*",
234
+ "accept-encoding": "identity",
235
+ },
236
+ },
237
+ (response) => {
238
+ const status = response.statusCode || 0;
239
+ if (status !== 200) {
240
+ finish(new Error(label + " returned HTTP " + status));
241
+ response.destroy();
242
+ return;
243
+ }
244
+
245
+ const lengthHeader = response.headers["content-length"];
246
+ if (lengthHeader !== undefined) {
247
+ if (!/^\d+$/.test(lengthHeader) || Number(lengthHeader) > limit) {
248
+ finish(new Error(label + " exceeds the size limit"));
249
+ response.destroy();
250
+ return;
251
+ }
252
+ }
253
+
254
+ const chunks = [];
255
+ let size = 0;
256
+ response.on("data", (chunk) => {
257
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
258
+ size += bytes.length;
259
+ if (size > limit) {
260
+ finish(new Error(label + " exceeds the size limit"));
261
+ response.destroy();
262
+ return;
263
+ }
264
+ chunks.push(bytes);
265
+ });
266
+ response.on("end", () => finish(null, Buffer.concat(chunks, size)));
267
+ response.on("aborted", () => finish(new Error(label + " response was interrupted")));
268
+ response.on("error", (error) => finish(new Error(label + " failed: " + error.message)));
269
+ },
270
+ );
271
+ } catch (error) {
272
+ finish(new Error(label + " failed: " + error.message));
273
+ return;
274
+ }
275
+
276
+ deadline = setTimeout(() => {
277
+ request.destroy(new Error(label + " request timed out"));
278
+ }, REQUEST_TIMEOUT_MS);
279
+ request.on("error", (error) => finish(new Error(label + " failed: " + error.message)));
280
+ });
281
+ }
282
+
283
+ async function fetchBootstrap(serverURL) {
284
+ const server = parseHTTPURL(serverURL, "--server");
285
+ platformTag();
286
+ const endpoint = new URL("/api/bootstrap", server);
287
+ endpoint.searchParams.set("schema", String(BOOTSTRAP_SCHEMA));
288
+ endpoint.searchParams.set("os", process.platform);
289
+ endpoint.searchParams.set("arch", process.arch === "x64" ? "amd64" : process.arch);
290
+ const body = await requestBytes(endpoint, MAX_BOOTSTRAP_BYTES, "bootstrap request", "application/json");
291
+ return validateBootstrapResponse(parseJSON(body, "bootstrap response"));
292
+ }
293
+
294
+ function registryURL(environment = process.env) {
295
+ const hasOverride = Object.prototype.hasOwnProperty.call(environment, "CHORDRA_NPM_REGISTRY");
296
+ const value = hasOverride ? environment.CHORDRA_NPM_REGISTRY : DEFAULT_NPM_REGISTRY;
297
+ const registry = parseHTTPURL(value, "CHORDRA_NPM_REGISTRY", { allowPath: true });
298
+ if (!registry.pathname.endsWith("/")) {
299
+ registry.pathname += "/";
300
+ }
301
+ return registry;
302
+ }
303
+
304
+ function metadataURL(registry, version) {
305
+ return new URL("@sys9/chord-cli/" + encodeURIComponent(version), registry);
306
+ }
307
+
308
+ function parseIntegrity(value) {
309
+ if (typeof value !== "string" || value.trim() === "") {
310
+ throw new Error("package metadata has an invalid sha512 integrity");
311
+ }
312
+ const digests = [];
313
+ for (const token of value.trim().split(/\s+/)) {
314
+ if (!token.startsWith("sha512-")) {
315
+ continue;
316
+ }
317
+ const encoded = token.slice("sha512-".length);
318
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
319
+ throw new Error("package metadata has an invalid sha512 integrity");
320
+ }
321
+ const digest = Buffer.from(encoded, "base64");
322
+ if (digest.length !== 64 || digest.toString("base64") !== encoded) {
323
+ throw new Error("package metadata has an invalid sha512 integrity");
324
+ }
325
+ digests.push(digest);
326
+ }
327
+ if (digests.length === 0) {
328
+ throw new Error("package metadata has an invalid sha512 integrity");
329
+ }
330
+ return digests;
331
+ }
332
+
333
+ function validatePackageMetadata(payload, expectedVersion, registry) {
334
+ if (!isPlainObject(payload) || payload.name !== PACKAGE_NAME || payload.version !== expectedVersion) {
335
+ throw new Error("package metadata does not describe the requested package version");
336
+ }
337
+ if (!isPlainObject(payload.dist)) {
338
+ throw new Error("package metadata has no dist object");
339
+ }
340
+ const integrity = parseIntegrity(payload.dist.integrity);
341
+ if (typeof payload.dist.tarball !== "string") {
342
+ throw new Error("package metadata has no tarball URL");
343
+ }
344
+ const tarball = parseHTTPURL(payload.dist.tarball, "package tarball", {
345
+ allowPath: true,
346
+ allowQuery: true,
347
+ });
348
+ if (tarball.origin !== registry.origin) {
349
+ throw new Error("package tarball origin does not match the registry");
350
+ }
351
+ return { integrity, tarball };
352
+ }
353
+
354
+ async function fetchPackage(registry, version) {
355
+ const metadata = metadataURL(registry, version);
356
+ const metadataBody = await requestBytes(
357
+ metadata,
358
+ MAX_METADATA_BYTES,
359
+ "package metadata request",
360
+ "application/json",
361
+ );
362
+ const packageInfo = validatePackageMetadata(parseJSON(metadataBody, "package metadata"), version, registry);
363
+ const tarballBody = await requestBytes(packageInfo.tarball, MAX_TARBALL_BYTES, "package tarball request");
364
+ const actual = crypto.createHash("sha512").update(tarballBody).digest();
365
+ if (!packageInfo.integrity.some((digest) => crypto.timingSafeEqual(actual, digest))) {
366
+ throw new Error("package tarball integrity mismatch");
367
+ }
368
+ return tarballBody;
369
+ }
370
+
371
+ function tarText(bytes, label) {
372
+ const end = bytes.indexOf(0);
373
+ const field = end === -1 ? bytes : bytes.subarray(0, end);
374
+ try {
375
+ return textDecoder.decode(field);
376
+ } catch {
377
+ throw new Error("tar archive has an invalid " + label);
378
+ }
379
+ }
380
+
381
+ function tarNumber(bytes, label) {
382
+ const raw = tarText(bytes, label).trim();
383
+ if (!/^[0-7]+$/.test(raw)) {
384
+ throw new Error("tar archive has an invalid " + label);
385
+ }
386
+ const value = Number.parseInt(raw, 8);
387
+ if (!Number.isSafeInteger(value)) {
388
+ throw new Error("tar archive has an invalid " + label);
389
+ }
390
+ return value;
391
+ }
392
+
393
+ function isZeroBlock(block) {
394
+ for (const byte of block) {
395
+ if (byte !== 0) {
396
+ return false;
397
+ }
398
+ }
399
+ return true;
400
+ }
401
+
402
+ function validateTarPath(value, directory) {
403
+ if (
404
+ typeof value !== "string" ||
405
+ value.length === 0 ||
406
+ value.length > 512 ||
407
+ value.includes("\\") ||
408
+ value.includes("\0") ||
409
+ value.startsWith("/") ||
410
+ /^[A-Za-z]:/.test(value)
411
+ ) {
412
+ throw new Error("tar archive contains an unsafe path");
413
+ }
414
+ const pathValue = directory && value.endsWith("/") ? value.slice(0, -1) : value;
415
+ const components = pathValue.split("/");
416
+ if (
417
+ components.some(
418
+ (component) =>
419
+ component.length === 0 ||
420
+ component === "." ||
421
+ component === ".." ||
422
+ component.includes(":")
423
+ )
424
+ ) {
425
+ throw new Error("tar archive contains an unsafe path");
426
+ }
427
+ return pathValue;
428
+ }
429
+
430
+ function targetDirectory(platform = process.platform, arch = process.arch) {
431
+ const goarch = arch === "x64" ? "amd64" : arch;
432
+ if (platform !== "linux" && platform !== "darwin") {
433
+ throw new Error("unsupported platform: " + platform + " (" + arch + ")");
434
+ }
435
+ if (goarch !== "amd64" && goarch !== "arm64") {
436
+ throw new Error("unsupported platform: " + platform + " (" + arch + ")");
437
+ }
438
+ return platform + "_" + goarch;
439
+ }
440
+
441
+ function parseTar(tar, target = targetDirectory()) {
442
+ if (!Buffer.isBuffer(tar) || tar.length === 0 || tar.length > MAX_ARCHIVE_BYTES) {
443
+ throw new Error("tar archive exceeds the size limit");
444
+ }
445
+ if (!/^(?:linux|darwin)_(?:amd64|arm64)$/.test(target)) {
446
+ throw new Error("invalid native release target");
447
+ }
448
+ const targetRoot = "package/vendor/" + target;
449
+ const expectedPaths = new Map([
450
+ [targetRoot + "/runner", "runner"],
451
+ [targetRoot + "/chord-daemon", "chord-daemon"],
452
+ [targetRoot + "/chord-bridge", "chord-bridge"],
453
+ ]);
454
+ const entries = new Map();
455
+ const extractedNames = new Set();
456
+ let offset = 0;
457
+ let entryCount = 0;
458
+ let endBlocks = 0;
459
+
460
+ while (offset + 512 <= tar.length) {
461
+ const header = tar.subarray(offset, offset + 512);
462
+ offset += 512;
463
+ if (isZeroBlock(header)) {
464
+ endBlocks += 1;
465
+ if (endBlocks === 2) {
466
+ break;
467
+ }
468
+ continue;
469
+ }
470
+ if (endBlocks !== 0) {
471
+ throw new Error("tar archive has data after its end marker");
472
+ }
473
+ entryCount += 1;
474
+ if (entryCount > MAX_ARCHIVE_ENTRIES) {
475
+ throw new Error("tar archive has too many entries");
476
+ }
477
+
478
+ const checksum = tarNumber(header.subarray(148, 156), "checksum");
479
+ let computedChecksum = 0;
480
+ for (let index = 0; index < header.length; index += 1) {
481
+ computedChecksum += index >= 148 && index < 156 ? 32 : header[index];
482
+ }
483
+ if (checksum !== computedChecksum) {
484
+ throw new Error("tar archive has an invalid checksum");
485
+ }
486
+
487
+ const name = tarText(header.subarray(0, 100), "path");
488
+ const prefix = tarText(header.subarray(345, 500), "path");
489
+ const fullName = prefix ? prefix + "/" + name : name;
490
+ const type = header[156];
491
+ const directory = type === 53;
492
+ const safeName = validateTarPath(fullName, directory);
493
+ if (safeName !== "package" && !safeName.startsWith("package/")) {
494
+ throw new Error("tar archive entry is outside package/");
495
+ }
496
+ if (extractedNames.has(safeName)) {
497
+ throw new Error("tar archive contains a duplicate path");
498
+ }
499
+ extractedNames.add(safeName);
500
+
501
+ if (type !== 0 && type !== 48 && !directory) {
502
+ throw new Error("tar archive contains a link or special file");
503
+ }
504
+ const size = tarNumber(header.subarray(124, 136), "file size");
505
+ if (size > MAX_ENTRY_BYTES) {
506
+ throw new Error("tar archive entry exceeds the size limit");
507
+ }
508
+ const end = offset + size;
509
+ if (end < offset || end > tar.length) {
510
+ throw new Error("tar archive entry is truncated");
511
+ }
512
+ if (directory && size !== 0) {
513
+ throw new Error("tar archive directory has data");
514
+ }
515
+ const padding = (512 - (size % 512)) % 512;
516
+ if (end + padding > tar.length) {
517
+ throw new Error("tar archive entry padding is truncated");
518
+ }
519
+ const outputName = expectedPaths.get(safeName);
520
+ const inTarget = safeName === targetRoot || safeName.startsWith(targetRoot + "/");
521
+ if (inTarget && safeName !== targetRoot && !outputName) {
522
+ throw new Error("tar archive contains an unexpected target entry " + safeName);
523
+ }
524
+ if (type === 0 || type === 48) {
525
+ if (outputName) {
526
+ const mode = tarNumber(header.subarray(100, 108), "file mode");
527
+ if ((mode & 0o111) === 0 || size === 0) {
528
+ throw new Error("native release file " + safeName + " is not executable");
529
+ }
530
+ if (entries.has(outputName)) {
531
+ throw new Error("tar archive contains a duplicate binary");
532
+ }
533
+ entries.set(outputName, Buffer.from(tar.subarray(offset, end)));
534
+ }
535
+ }
536
+ offset = end + padding;
537
+ }
538
+
539
+ if (endBlocks < 2 || offset > tar.length || (tar.length - offset) % 512 !== 0) {
540
+ throw new Error("tar archive has no complete end marker");
541
+ }
542
+ for (const byte of tar.subarray(offset)) {
543
+ if (byte !== 0) {
544
+ throw new Error("tar archive has trailing data");
545
+ }
546
+ }
547
+ for (const name of ["runner", "chord-daemon", "chord-bridge"]) {
548
+ const content = entries.get(name);
549
+ if (!content || content.length === 0) {
550
+ throw new Error("tar archive is missing " + name);
551
+ }
552
+ }
553
+ return entries;
554
+ }
555
+
556
+ function unpackPackage(tarball, target = targetDirectory()) {
557
+ let tar;
558
+ try {
559
+ tar = zlib.gunzipSync(tarball, { maxOutputLength: MAX_ARCHIVE_BYTES });
560
+ } catch (error) {
561
+ if (error && error.code === "ERR_BUFFER_TOO_LARGE") {
562
+ throw new Error("package tarball exceeds the uncompressed size limit");
563
+ }
564
+ throw new Error("package tarball is not a valid gzip archive: " + error.message);
565
+ }
566
+ return parseTar(tar, target);
567
+ }
568
+
569
+ function materializeBinaries(entries) {
570
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "chord-ctl-"));
571
+ try {
572
+ for (const name of ["runner", "chord-daemon", "chord-bridge"]) {
573
+ const target = path.join(root, name);
574
+ fs.writeFileSync(target, entries.get(name), { encoding: null, mode: 0o700, flag: "wx" });
575
+ fs.chmodSync(target, 0o700);
576
+ if (!fs.lstatSync(target).isFile()) {
577
+ throw new Error("extracted " + name + " is not a regular file");
578
+ }
579
+ }
580
+ return root;
581
+ } catch (error) {
582
+ fs.rmSync(root, { recursive: true, force: true });
583
+ throw new Error("could not materialize package binaries: " + error.message);
584
+ }
585
+ }
586
+
587
+ function runRunner(root, serverURL, environmentID) {
588
+ const runnerPath = path.join(root, "runner");
589
+ return new Promise((resolve, reject) => {
590
+ let child;
591
+ try {
592
+ child = spawn(
593
+ runnerPath,
594
+ ["setup", "--server", serverURL, "--environment-id", environmentID],
595
+ { stdio: "inherit" },
596
+ );
597
+ } catch (error) {
598
+ reject(error);
599
+ return;
600
+ }
601
+ child.once("error", reject);
602
+ child.once("exit", (code, signal) => {
603
+ if (signal) {
604
+ resolve(1);
605
+ } else {
606
+ resolve(code === null ? 1 : code);
607
+ }
608
+ });
609
+ });
610
+ }
611
+
612
+ async function acquireRelease(release, environment) {
613
+ const failures = [];
614
+ for (const source of release.sources) {
615
+ try {
616
+ let tarball;
617
+ if (source === "native") {
618
+ const archive = release.archive;
619
+ tarball = await requestBytes(new URL(archive.url), archive.size, "native archive request");
620
+ if (tarball.length !== archive.size) {
621
+ throw new Error("archive size mismatch");
622
+ }
623
+ const actual = crypto.createHash("sha512").update(tarball).digest();
624
+ const expected = parseIntegrity(archive.integrity)[0];
625
+ if (!crypto.timingSafeEqual(actual, expected)) {
626
+ throw new Error("archive integrity mismatch");
627
+ }
628
+ } else {
629
+ tarball = await fetchPackage(registryURL(environment), release.version + "-" + platformTag());
630
+ }
631
+ return unpackPackage(tarball);
632
+ } catch (error) {
633
+ failures.push(source + ": " + error.message);
634
+ }
635
+ }
636
+ throw new Error("could not acquire approved CLI release: " + failures.join("; "));
637
+ }
638
+
639
+ async function setup(serverURL, environment = process.env) {
640
+ const bootstrap = await fetchBootstrap(serverURL);
641
+ const binaries = await acquireRelease(bootstrap.release, environment);
642
+ const root = materializeBinaries(binaries);
643
+ try {
644
+ return await runRunner(root, serverURL, bootstrap.environmentID);
645
+ } finally {
646
+ fs.rmSync(root, { recursive: true, force: true });
647
+ }
648
+ }
649
+
650
+ async function main(argv = process.argv.slice(2), environment = process.env) {
651
+ // npm treats engines as advisory by default. Enforce the same package-owned
652
+ // floor before argument processing, network access, or native setup.
653
+ const requirement = require("../package.json").engines.node;
654
+ const minimum = /^>=(\d+)\.(\d+)\.(\d+)$/.exec(requirement);
655
+ if (!minimum) {
656
+ throw new Error("unsupported package Node.js requirement: " + requirement);
657
+ }
658
+ const current = /^(\d+)\.(\d+)\.(\d+)$/.exec(process.versions.node);
659
+ let supported = current !== null;
660
+ if (current) {
661
+ for (let index = 1; index <= 3; index += 1) {
662
+ if (Number(current[index]) !== Number(minimum[index])) {
663
+ supported = Number(current[index]) > Number(minimum[index]);
664
+ break;
665
+ }
666
+ }
667
+ }
668
+ if (!supported) {
669
+ throw new Error("Node.js " + requirement + " is required; found " + process.versions.node + ". Upgrade Node.js and run setup again.");
670
+ }
671
+ const args = parseArguments(argv);
672
+ return setup(args.server, environment);
673
+ }
674
+
675
+ if (require.main === module) {
676
+ main().then(
677
+ (exitCode) => {
678
+ if (exitCode) {
679
+ process.exitCode = exitCode;
680
+ }
681
+ },
682
+ (error) => {
683
+ console.error("chordra-ai-ctl: " + error.message);
684
+ process.exitCode = 1;
685
+ },
686
+ );
687
+ }
688
+
689
+ module.exports = {
690
+ BOOTSTRAP_SCHEMA,
691
+ DEFAULT_NPM_REGISTRY,
692
+ MAX_ARCHIVE_BYTES,
693
+ MAX_BOOTSTRAP_BYTES,
694
+ MAX_ENTRY_BYTES,
695
+ MAX_METADATA_BYTES,
696
+ MAX_TARBALL_BYTES,
697
+ parseArguments,
698
+ parseTar,
699
+ platformTag,
700
+ registryURL,
701
+ setup,
702
+ unpackPackage,
703
+ validateBootstrapResponse,
704
+ validatePackageMetadata,
705
+ };
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "@chordra.ai/ctl",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Transient Chordra Host setup bootstrap",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/sys9-ai/chord.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "bin": {
14
+ "chordra-ai-ctl": "bin/chord-ctl"
15
+ },
16
+ "files": [
17
+ "README.md",
18
+ "bin"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test test/*.test.js"
22
+ },
23
+ "engines": {
24
+ "node": ">=22.22.0"
25
+ }
26
+ }