@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.
Files changed (2) hide show
  1. package/bin/chord-ctl +32 -130
  2. 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 !== 1 && payload.schema_version !== BOOTSTRAP_SCHEMA) {
118
+ if (payload.schema_version !== BOOTSTRAP_SCHEMA) {
137
119
  throw new Error("unsupported bootstrap schema");
138
120
  }
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(",")) {
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.schema_version === 1
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 === 0 ||
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
- platformTag();
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("package metadata has an invalid sha512 integrity");
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("package metadata has an invalid sha512 integrity");
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("package metadata has an invalid sha512 integrity");
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("package metadata has an invalid sha512 integrity");
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 unpackPackage(tarball, target = targetDirectory()) {
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("package tarball exceeds the uncompressed size limit");
478
+ throw new Error("archive exceeds the uncompressed size limit");
563
479
  }
564
- throw new Error("package tarball is not a valid gzip archive: " + error.message);
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 package binaries: " + error.message);
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, 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);
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, environment = process.env) {
546
+ async function setup(serverURL) {
640
547
  const bootstrap = await fetchBootstrap(serverURL);
641
- const binaries = await acquireRelease(bootstrap.release, environment);
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), environment = process.env) {
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, environment);
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
- unpackPackage,
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.0",
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
  },