@chordra.ai/ctl 0.2.0 → 0.2.1
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/bin/chord-ctl +32 -130
- package/package.json +1 -5
package/bin/chord-ctl
CHANGED
|
@@ -12,13 +12,10 @@ const path = require("node:path");
|
|
|
12
12
|
const { TextDecoder } = require("node:util");
|
|
13
13
|
const zlib = require("node:zlib");
|
|
14
14
|
|
|
15
|
-
const PACKAGE_NAME = "@sys9/chord-cli";
|
|
16
|
-
const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
17
15
|
const BOOTSTRAP_SCHEMA = 2;
|
|
18
16
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
19
17
|
const MAX_URL_BYTES = 4096;
|
|
20
18
|
const MAX_BOOTSTRAP_BYTES = 64 * 1024;
|
|
21
|
-
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
22
19
|
const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
|
|
23
20
|
const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
24
21
|
const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
@@ -27,13 +24,6 @@ const MAX_ARCHIVE_ENTRIES = 1024;
|
|
|
27
24
|
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
28
25
|
const ENVIRONMENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
29
26
|
|
|
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
27
|
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
38
28
|
|
|
39
29
|
function isPlainObject(value) {
|
|
@@ -103,14 +93,6 @@ function parseHTTPURL(value, label, { allowPath = false, allowQuery = false } =
|
|
|
103
93
|
return parsed;
|
|
104
94
|
}
|
|
105
95
|
|
|
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
96
|
function isVersion(value) {
|
|
115
97
|
return typeof value === "string" && value.length <= 128 && VERSION_PATTERN.test(value);
|
|
116
98
|
}
|
|
@@ -133,14 +115,10 @@ function validateBootstrapResponse(payload) {
|
|
|
133
115
|
if (!isPlainObject(payload)) {
|
|
134
116
|
throw new Error("bootstrap response must be an object");
|
|
135
117
|
}
|
|
136
|
-
if (payload.schema_version !==
|
|
118
|
+
if (payload.schema_version !== BOOTSTRAP_SCHEMA) {
|
|
137
119
|
throw new Error("unsupported bootstrap schema");
|
|
138
120
|
}
|
|
139
|
-
|
|
140
|
-
if (payload.schema_version === BOOTSTRAP_SCHEMA) {
|
|
141
|
-
expectedKeys.push("release");
|
|
142
|
-
}
|
|
143
|
-
if (Object.keys(payload).sort().join(",") !== expectedKeys.sort().join(",")) {
|
|
121
|
+
if (Object.keys(payload).sort().join(",") !== "approved_cli_version,canonical_environment_id,release,schema_version") {
|
|
144
122
|
throw new Error("bootstrap response has an invalid schema");
|
|
145
123
|
}
|
|
146
124
|
if (
|
|
@@ -152,9 +130,7 @@ function validateBootstrapResponse(payload) {
|
|
|
152
130
|
if (!isVersion(payload.approved_cli_version)) {
|
|
153
131
|
throw new Error("bootstrap response has an invalid version");
|
|
154
132
|
}
|
|
155
|
-
const release = payload.
|
|
156
|
-
? { sources: ["npm"], version: payload.approved_cli_version }
|
|
157
|
-
: validateRelease(payload.release, payload.approved_cli_version);
|
|
133
|
+
const release = validateRelease(payload.release, payload.approved_cli_version);
|
|
158
134
|
return {
|
|
159
135
|
environmentID: payload.canonical_environment_id,
|
|
160
136
|
version: payload.approved_cli_version,
|
|
@@ -166,17 +142,9 @@ function validateRelease(release, approvedVersion) {
|
|
|
166
142
|
if (!isPlainObject(release) || release.version !== approvedVersion) {
|
|
167
143
|
throw new Error("bootstrap release does not match the approved version");
|
|
168
144
|
}
|
|
169
|
-
if (!Array.isArray(release.sources) || release.sources.length
|
|
170
|
-
release.sources.some((source) => source !== "npm" && source !== "native") ||
|
|
171
|
-
new Set(release.sources).size !== release.sources.length) {
|
|
145
|
+
if (!Array.isArray(release.sources) || release.sources.length !== 1 || release.sources[0] !== "native") {
|
|
172
146
|
throw new Error("bootstrap release has invalid sources");
|
|
173
147
|
}
|
|
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
148
|
if (Object.keys(release).sort().join(",") !== "archive,sources,version" || !isPlainObject(release.archive)) {
|
|
181
149
|
throw new Error("native release has an invalid schema");
|
|
182
150
|
}
|
|
@@ -282,7 +250,7 @@ function requestBytes(target, limit, label, accept) {
|
|
|
282
250
|
|
|
283
251
|
async function fetchBootstrap(serverURL) {
|
|
284
252
|
const server = parseHTTPURL(serverURL, "--server");
|
|
285
|
-
|
|
253
|
+
targetDirectory();
|
|
286
254
|
const endpoint = new URL("/api/bootstrap", server);
|
|
287
255
|
endpoint.searchParams.set("schema", String(BOOTSTRAP_SCHEMA));
|
|
288
256
|
endpoint.searchParams.set("os", process.platform);
|
|
@@ -291,23 +259,9 @@ async function fetchBootstrap(serverURL) {
|
|
|
291
259
|
return validateBootstrapResponse(parseJSON(body, "bootstrap response"));
|
|
292
260
|
}
|
|
293
261
|
|
|
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
262
|
function parseIntegrity(value) {
|
|
309
263
|
if (typeof value !== "string" || value.trim() === "") {
|
|
310
|
-
throw new Error("
|
|
264
|
+
throw new Error("archive has an invalid sha512 integrity");
|
|
311
265
|
}
|
|
312
266
|
const digests = [];
|
|
313
267
|
for (const token of value.trim().split(/\s+/)) {
|
|
@@ -316,58 +270,20 @@ function parseIntegrity(value) {
|
|
|
316
270
|
}
|
|
317
271
|
const encoded = token.slice("sha512-".length);
|
|
318
272
|
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
|
|
319
|
-
throw new Error("
|
|
273
|
+
throw new Error("archive has an invalid sha512 integrity");
|
|
320
274
|
}
|
|
321
275
|
const digest = Buffer.from(encoded, "base64");
|
|
322
276
|
if (digest.length !== 64 || digest.toString("base64") !== encoded) {
|
|
323
|
-
throw new Error("
|
|
277
|
+
throw new Error("archive has an invalid sha512 integrity");
|
|
324
278
|
}
|
|
325
279
|
digests.push(digest);
|
|
326
280
|
}
|
|
327
281
|
if (digests.length === 0) {
|
|
328
|
-
throw new Error("
|
|
282
|
+
throw new Error("archive has an invalid sha512 integrity");
|
|
329
283
|
}
|
|
330
284
|
return digests;
|
|
331
285
|
}
|
|
332
286
|
|
|
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
287
|
function tarText(bytes, label) {
|
|
372
288
|
const end = bytes.indexOf(0);
|
|
373
289
|
const field = end === -1 ? bytes : bytes.subarray(0, end);
|
|
@@ -553,15 +469,15 @@ function parseTar(tar, target = targetDirectory()) {
|
|
|
553
469
|
return entries;
|
|
554
470
|
}
|
|
555
471
|
|
|
556
|
-
function
|
|
472
|
+
function unpackArchive(tarball, target = targetDirectory()) {
|
|
557
473
|
let tar;
|
|
558
474
|
try {
|
|
559
475
|
tar = zlib.gunzipSync(tarball, { maxOutputLength: MAX_ARCHIVE_BYTES });
|
|
560
476
|
} catch (error) {
|
|
561
477
|
if (error && error.code === "ERR_BUFFER_TOO_LARGE") {
|
|
562
|
-
throw new Error("
|
|
478
|
+
throw new Error("archive exceeds the uncompressed size limit");
|
|
563
479
|
}
|
|
564
|
-
throw new Error("
|
|
480
|
+
throw new Error("archive is not a valid gzip archive: " + error.message);
|
|
565
481
|
}
|
|
566
482
|
return parseTar(tar, target);
|
|
567
483
|
}
|
|
@@ -580,7 +496,7 @@ function materializeBinaries(entries) {
|
|
|
580
496
|
return root;
|
|
581
497
|
} catch (error) {
|
|
582
498
|
fs.rmSync(root, { recursive: true, force: true });
|
|
583
|
-
throw new Error("could not materialize
|
|
499
|
+
throw new Error("could not materialize release binaries: " + error.message);
|
|
584
500
|
}
|
|
585
501
|
}
|
|
586
502
|
|
|
@@ -609,36 +525,27 @@ function runRunner(root, serverURL, environmentID) {
|
|
|
609
525
|
});
|
|
610
526
|
}
|
|
611
527
|
|
|
612
|
-
async function acquireRelease(release
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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);
|
|
528
|
+
async function acquireRelease(release) {
|
|
529
|
+
const archive = release.archive;
|
|
530
|
+
try {
|
|
531
|
+
const tarball = await requestBytes(new URL(archive.url), archive.size, "native archive request");
|
|
532
|
+
if (tarball.length !== archive.size) {
|
|
533
|
+
throw new Error("archive size mismatch");
|
|
634
534
|
}
|
|
535
|
+
const actual = crypto.createHash("sha512").update(tarball).digest();
|
|
536
|
+
const expected = parseIntegrity(archive.integrity)[0];
|
|
537
|
+
if (!crypto.timingSafeEqual(actual, expected)) {
|
|
538
|
+
throw new Error("archive integrity mismatch");
|
|
539
|
+
}
|
|
540
|
+
return unpackArchive(tarball);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
throw new Error("could not acquire approved CLI release: " + error.message);
|
|
635
543
|
}
|
|
636
|
-
throw new Error("could not acquire approved CLI release: " + failures.join("; "));
|
|
637
544
|
}
|
|
638
545
|
|
|
639
|
-
async function setup(serverURL
|
|
546
|
+
async function setup(serverURL) {
|
|
640
547
|
const bootstrap = await fetchBootstrap(serverURL);
|
|
641
|
-
const binaries = await acquireRelease(bootstrap.release
|
|
548
|
+
const binaries = await acquireRelease(bootstrap.release);
|
|
642
549
|
const root = materializeBinaries(binaries);
|
|
643
550
|
try {
|
|
644
551
|
return await runRunner(root, serverURL, bootstrap.environmentID);
|
|
@@ -647,7 +554,7 @@ async function setup(serverURL, environment = process.env) {
|
|
|
647
554
|
}
|
|
648
555
|
}
|
|
649
556
|
|
|
650
|
-
async function main(argv = process.argv.slice(2)
|
|
557
|
+
async function main(argv = process.argv.slice(2)) {
|
|
651
558
|
// npm treats engines as advisory by default. Enforce the same package-owned
|
|
652
559
|
// floor before argument processing, network access, or native setup.
|
|
653
560
|
const requirement = require("../package.json").engines.node;
|
|
@@ -669,7 +576,7 @@ async function main(argv = process.argv.slice(2), environment = process.env) {
|
|
|
669
576
|
throw new Error("Node.js " + requirement + " is required; found " + process.versions.node + ". Upgrade Node.js and run setup again.");
|
|
670
577
|
}
|
|
671
578
|
const args = parseArguments(argv);
|
|
672
|
-
return setup(args.server
|
|
579
|
+
return setup(args.server);
|
|
673
580
|
}
|
|
674
581
|
|
|
675
582
|
if (require.main === module) {
|
|
@@ -688,18 +595,13 @@ if (require.main === module) {
|
|
|
688
595
|
|
|
689
596
|
module.exports = {
|
|
690
597
|
BOOTSTRAP_SCHEMA,
|
|
691
|
-
DEFAULT_NPM_REGISTRY,
|
|
692
598
|
MAX_ARCHIVE_BYTES,
|
|
693
599
|
MAX_BOOTSTRAP_BYTES,
|
|
694
600
|
MAX_ENTRY_BYTES,
|
|
695
|
-
MAX_METADATA_BYTES,
|
|
696
601
|
MAX_TARBALL_BYTES,
|
|
697
602
|
parseArguments,
|
|
698
603
|
parseTar,
|
|
699
|
-
platformTag,
|
|
700
|
-
registryURL,
|
|
701
604
|
setup,
|
|
702
|
-
|
|
605
|
+
unpackArchive,
|
|
703
606
|
validateBootstrapResponse,
|
|
704
|
-
validatePackageMetadata,
|
|
705
607
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chordra.ai/ctl",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Transient Chordra Host setup bootstrap",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/sys9-ai/chord.git"
|
|
9
|
-
},
|
|
10
6
|
"publishConfig": {
|
|
11
7
|
"access": "public"
|
|
12
8
|
},
|