@theholocron/cli 5.0.0-alpha.159 → 5.0.0-alpha.160

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/cli.mjs CHANGED
@@ -24,8 +24,8 @@ import search from "@inquirer/search";
24
24
  import { execFileSync, spawn, spawnSync } from "node:child_process";
25
25
  import ora from "ora";
26
26
  import { findRootSync, walkFilesSync } from "@theholocron/fs-utils";
27
- import { stringifyJson } from "@theholocron/json-utils";
28
27
  import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
28
+ import { stringifyJson } from "@theholocron/json-utils";
29
29
  import { createHash, randomUUID } from "node:crypto";
30
30
  import { generateReadme } from "@theholocron/components-doc/markdown";
31
31
  import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
@@ -410,6 +410,8 @@ const COMMAND_CONTEXTS = {
410
410
  deploy: "workspace",
411
411
  "deploy cleanup-preview": "workspace",
412
412
  "monitoring check": "workspace",
413
+ "monitoring resolve-checks": "workspace",
414
+ "monitoring run-resolved": "global",
413
415
  "repo sync": "workspace",
414
416
  "sync github": "workspace"
415
417
  };
@@ -1142,6 +1144,7 @@ async function runCheckAgainstUrl(input, opts = {}) {
1142
1144
  response = await doFetch(url, {
1143
1145
  method,
1144
1146
  ...hasBody ? { body: input.body } : {},
1147
+ ...opts.headers ? { headers: opts.headers } : {},
1145
1148
  redirect: "manual",
1146
1149
  signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
1147
1150
  });
@@ -1232,6 +1235,42 @@ function evaluate(comparison, actual, target) {
1232
1235
  }
1233
1236
  }
1234
1237
  //#endregion
1238
+ //#region src/plugin/verify-vercel-project/verify-vercel-project.ts
1239
+ /**
1240
+ * `true` only when Vercel's API confirms `url` resolves to a deployment
1241
+ * whose `projectId` matches `expectedProjectId` exactly. `false` for any
1242
+ * other outcome — a non-matching project, a non-2xx response, a
1243
+ * malformed body, or a transport failure — never throws, since every one
1244
+ * of those outcomes means the same thing to a caller: don't attach the
1245
+ * credential.
1246
+ */
1247
+ async function verifyVercelDeploymentProject(url, expectedProjectId, token, opts = {}) {
1248
+ const doFetch = opts.fetch ?? fetch;
1249
+ const baseUrl = opts.baseUrl ?? "https://api.vercel.com";
1250
+ let host;
1251
+ try {
1252
+ host = new URL(url).host;
1253
+ } catch {
1254
+ return false;
1255
+ }
1256
+ const params = new URLSearchParams({ url: host });
1257
+ if (opts.teamId) params.set("teamId", opts.teamId);
1258
+ let response;
1259
+ try {
1260
+ response = await doFetch(`${baseUrl}/v13/deployments/get?${params}`, { headers: { Authorization: `Bearer ${token}` } });
1261
+ } catch {
1262
+ return false;
1263
+ }
1264
+ if (!response.ok) return false;
1265
+ let body;
1266
+ try {
1267
+ body = await response.json();
1268
+ } catch {
1269
+ return false;
1270
+ }
1271
+ return (typeof body === "object" && body !== null && "projectId" in body ? body.projectId : void 0) === expectedProjectId;
1272
+ }
1273
+ //#endregion
1235
1274
  //#region src/plugin/workspace/workspace.ts
1236
1275
  /**
1237
1276
  * The `workspace`-context guard.
@@ -2313,16 +2352,57 @@ function mk(capability, provider, status, message) {
2313
2352
  }
2314
2353
  //#endregion
2315
2354
  //#region src/commands/monitoring-check/monitoring-check.ts
2316
- async function runMonitoringCheck(input) {
2317
- const print = input.print ?? ((line) => console.log(line));
2318
- const logger = input.logger ?? getLogger();
2355
+ /**
2356
+ * `holocron monitoring check <url>` — runs every configured `monitoring`
2357
+ * check against an arbitrary URL instead of each check's own configured
2358
+ * one (holocron#1010).
2359
+ *
2360
+ * Built for CI: the `delivery.previewCheck` task runs this against a
2361
+ * PR's Vercel preview deployment, reusing the exact same check
2362
+ * definitions already in `holocron.config.ts` for production — no
2363
+ * vendor API call, no separate checks-as-code file to keep in sync.
2364
+ *
2365
+ * Split into two pieces (holocron#1030/#1032 review) so a caller that
2366
+ * needs to attach a credential to the request (e.g. Vercel's Deployment
2367
+ * Protection bypass header) can do so in a process that never loads a
2368
+ * PR's own `holocron.config.ts` at all:
2369
+ * - {@link resolveMonitoringChecks} — loads config/plugins, returns the
2370
+ * configured `MonitoringCheckInput[]`. Untrusted: this executes
2371
+ * whatever code a PR's own config file contains. Never give this
2372
+ * process a credential worth protecting.
2373
+ * - {@link reportChecks} — takes an already-resolved check list (no
2374
+ * config loading at all) and runs/report them against a URL. Safe
2375
+ * to run with a credential present, since nothing here executes
2376
+ * PR-authored code.
2377
+ * `runMonitoringCheck` composes both, unchanged, for every caller that
2378
+ * doesn't need the split (the plain CLI invocation, tests, anywhere a
2379
+ * credential isn't in play).
2380
+ */
2381
+ /**
2382
+ * Loads config/plugins and returns the configured `monitoring` checks —
2383
+ * the untrusted half of `monitoring check`. Executes a PR's own
2384
+ * `holocron.config.ts` (and whatever it imports); never run this with a
2385
+ * credential in the process environment that a malicious config could
2386
+ * read and exfiltrate.
2387
+ */
2388
+ async function resolveMonitoringChecks(input) {
2319
2389
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
2320
2390
  await loader.load();
2321
2391
  assertPluginsResolvable(loader, "monitoring check");
2322
- const monitoring = loader.get("monitoring");
2392
+ return [...loader.get("monitoring").checks];
2393
+ }
2394
+ /**
2395
+ * Runs an already-resolved check list against `url` and reports
2396
+ * pass/fail — the trusted half of `monitoring check`. Takes plain data,
2397
+ * never touches `holocron.config.ts` or any plugin: safe to run with a
2398
+ * credential (in `headers` or elsewhere in the process env) present.
2399
+ */
2400
+ async function reportChecks(input) {
2401
+ const print = input.print ?? ((line) => console.log(line));
2402
+ const logger = input.logger ?? getLogger();
2323
2403
  const run = input.runCheck ?? runCheckAgainstUrl;
2324
2404
  print(`Holocron monitoring check — against ${input.url}`);
2325
- if (monitoring.checks.length === 0) {
2405
+ if (input.checks.length === 0) {
2326
2406
  print(`✗ no monitoring checks configured — nothing to validate`);
2327
2407
  logger.warn({ url: input.url }, "monitoring check: no checks configured");
2328
2408
  return {
@@ -2331,8 +2411,11 @@ async function runMonitoringCheck(input) {
2331
2411
  };
2332
2412
  }
2333
2413
  const checks = [];
2334
- for (const checkInput of monitoring.checks) {
2335
- const result = await run(checkInput, { url: input.url });
2414
+ for (const checkInput of input.checks) {
2415
+ const result = await run(checkInput, {
2416
+ url: input.url,
2417
+ ...input.headers ? { headers: input.headers } : {}
2418
+ });
2336
2419
  if (result.requestError) {
2337
2420
  print(` ✗ ${checkInput.name} (request failed: ${result.requestError})`);
2338
2421
  checks.push({
@@ -2372,6 +2455,110 @@ async function runMonitoringCheck(input) {
2372
2455
  checks
2373
2456
  };
2374
2457
  }
2458
+ /**
2459
+ * `resolveMonitoringChecks` + `reportChecks` in one call — the plain CLI
2460
+ * contract, unchanged. Only safe to run with a credential present when
2461
+ * the config being loaded is trusted (no PR in the loop); CI's
2462
+ * `delivery.previewCheck` uses the two halves separately instead.
2463
+ */
2464
+ async function runMonitoringCheck(input) {
2465
+ return reportChecks({
2466
+ checks: await resolveMonitoringChecks({
2467
+ loaded: input.loaded,
2468
+ context: input.context,
2469
+ loader: input.loader
2470
+ }),
2471
+ url: input.url,
2472
+ ...input.headers ? { headers: input.headers } : {},
2473
+ ...input.print ? { print: input.print } : {},
2474
+ ...input.logger ? { logger: input.logger } : {},
2475
+ ...input.runCheck ? { runCheck: input.runCheck } : {}
2476
+ });
2477
+ }
2478
+ //#endregion
2479
+ //#region src/commands/monitoring-resolve-checks/monitoring-resolve-checks.ts
2480
+ /**
2481
+ * `holocron monitoring resolve-checks` — loads config/plugins and writes
2482
+ * the configured `monitoring` checks to a file as JSON (holocron#1030/
2483
+ * #1032 review).
2484
+ *
2485
+ * The untrusted half of the `delivery.previewCheck` split: this process
2486
+ * executes a PR's own `holocron.config.ts`, so it must never run with a
2487
+ * credential worth protecting (e.g. a Deployment Protection bypass
2488
+ * secret) present in its environment. `monitoring run-resolved` is the
2489
+ * trusted half — it reads this command's output file and performs the
2490
+ * actual authenticated request, with no config loading at all.
2491
+ */
2492
+ async function runMonitoringResolveChecks(input) {
2493
+ const print = input.print ?? ((line) => console.log(line));
2494
+ const checks = await resolveMonitoringChecks({
2495
+ loaded: input.loaded,
2496
+ context: input.context,
2497
+ loader: input.loader
2498
+ });
2499
+ await writeFile(input.outFile, JSON.stringify(checks), "utf8");
2500
+ print(`Resolved ${checks.length} monitoring check(s) → ${input.outFile}`);
2501
+ }
2502
+ //#endregion
2503
+ //#region src/commands/monitoring-run-resolved/monitoring-run-resolved.ts
2504
+ /**
2505
+ * `holocron monitoring run-resolved <url>` — runs an already-resolved
2506
+ * check list (no `holocron.config.ts` loading at all) against `url`
2507
+ * (holocron#1030/#1032 review).
2508
+ *
2509
+ * The trusted half of the `delivery.previewCheck` split, paired with
2510
+ * `monitoring resolve-checks` (the untrusted half, which loads config).
2511
+ * Safe to run with a credential present, since nothing here executes
2512
+ * PR-authored code — but a credential is only ever attached after
2513
+ * `verifyVercelDeploymentProject` confirms `url` actually belongs to
2514
+ * `vercelProject`: a URL merely ending in `.vercel.app` proves nothing,
2515
+ * since anyone can register their own Vercel project (sourcery review
2516
+ * on holocron#1032). No `vercelProject` given at all → the credential
2517
+ * is never attached, regardless of whether one is configured.
2518
+ */
2519
+ /**
2520
+ * The shared gate every caller that might attach the Deployment
2521
+ * Protection bypass header goes through — `monitoring check`'s direct
2522
+ * CLI invocation included (sourcery review on holocron#1032 called this
2523
+ * out explicitly: verifying only in `delivery.previewCheck`'s own split
2524
+ * commands and not here would leave the exact same unverified-host
2525
+ * problem reachable by hand). No `vercelProject` → never attach
2526
+ * anything, regardless of whether a `bypassSecret` happens to be
2527
+ * configured. `vercelProject` given but verification fails or no token
2528
+ * is available → `undefined` AND a printed reason, never a thrown
2529
+ * error — callers decide for themselves whether that's fatal.
2530
+ */
2531
+ async function resolveVercelBypassHeaders(input) {
2532
+ const print = input.print ?? ((line) => console.log(line));
2533
+ if (!input.vercelProject) return { verified: true };
2534
+ if (!input.vercelToken) {
2535
+ print(`✗ --vercel-project was given but no Vercel token is available to verify deployment ownership`);
2536
+ return { verified: false };
2537
+ }
2538
+ if (!await (input.verify ?? verifyVercelDeploymentProject)(input.url, input.vercelProject, input.vercelToken, { ...input.vercelTeamId ? { teamId: input.vercelTeamId } : {} })) {
2539
+ print(`✗ ${input.url} does not resolve to a deployment of Vercel project "${input.vercelProject}" — refusing to attach the Deployment Protection bypass credential or run checks against an unverified URL`);
2540
+ return { verified: false };
2541
+ }
2542
+ return input.bypassSecret ? {
2543
+ verified: true,
2544
+ headers: { "x-vercel-protection-bypass": input.bypassSecret }
2545
+ } : { verified: true };
2546
+ }
2547
+ async function runMonitoringRunResolved(input) {
2548
+ const { headers, verified } = await resolveVercelBypassHeaders(input);
2549
+ if (!verified) return {
2550
+ status: "fail",
2551
+ checks: []
2552
+ };
2553
+ return reportChecks({
2554
+ checks: input.checks,
2555
+ url: input.url,
2556
+ ...headers ? { headers } : {},
2557
+ ...input.print ? { print: input.print } : {},
2558
+ ...input.logger ? { logger: input.logger } : {},
2559
+ ...input.runCheck ? { runCheck: input.runCheck } : {}
2560
+ });
2561
+ }
2375
2562
  //#endregion
2376
2563
  //#region src/commands/new/new.ts
2377
2564
  /**
@@ -10111,12 +10298,22 @@ try {
10111
10298
  }).command("monitoring", "Run monitoring checks ad hoc, outside the configured provider", (g) => g.command("check <url>", "Run every configured `monitoring` check against a given URL instead of its own configured url", (y) => y.positional("url", {
10112
10299
  type: "string",
10113
10300
  describe: "URL to test every configured check against (e.g. a PR's preview deployment)"
10301
+ }).option("vercel-project", {
10302
+ type: "string",
10303
+ describe: "Expected Vercel project id/name `url` must resolve to before the Deployment Protection bypass header (VERCEL_PROTECTION_BYPASS_SECRET) is attached. Omitted → the header is never attached, regardless of that env var."
10114
10304
  }), async (argv) => {
10115
10305
  const tokens = tokenContext(argv.token);
10116
10306
  if (!tokens) return;
10117
10307
  const [url] = await promptForPositionals(getEntry("monitoring check"), argv);
10118
10308
  const loaded = await loadConfig(argv.cwd);
10119
10309
  applyResolvedConfig(argv, loaded.resolved);
10310
+ const { headers } = await resolveVercelBypassHeaders({
10311
+ url,
10312
+ ...argv.vercelProject ? { vercelProject: argv.vercelProject } : {},
10313
+ ...process.env["VERCEL_TOKEN"] ? { vercelToken: process.env["VERCEL_TOKEN"] } : {},
10314
+ ...process.env["VERCEL_TEAM_ID"] ? { vercelTeamId: process.env["VERCEL_TEAM_ID"] } : {},
10315
+ ...process.env["VERCEL_PROTECTION_BYPASS_SECRET"] ? { bypassSecret: process.env["VERCEL_PROTECTION_BYPASS_SECRET"] } : {}
10316
+ });
10120
10317
  if ((await runMonitoringCheck({
10121
10318
  loaded,
10122
10319
  context: {
@@ -10125,7 +10322,46 @@ try {
10125
10322
  ...tokens,
10126
10323
  org: resolveOrg(argv, loaded.resolved)
10127
10324
  },
10128
- url
10325
+ url,
10326
+ ...headers ? { headers } : {}
10327
+ })).status === "fail") process.exitCode = 1;
10328
+ }).command("resolve-checks", "Load config/plugins and write the configured `monitoring` checks to a file as JSON — the untrusted half of the delivery.previewCheck split (holocron#1030/#1032)", (y) => y.option("out", {
10329
+ type: "string",
10330
+ demandOption: true,
10331
+ describe: "File path to write the resolved checks to"
10332
+ }), async (argv) => {
10333
+ const tokens = tokenContext(argv.token);
10334
+ if (!tokens) return;
10335
+ const loaded = await loadConfig(argv.cwd);
10336
+ applyResolvedConfig(argv, loaded.resolved);
10337
+ await runMonitoringResolveChecks({
10338
+ loaded,
10339
+ context: {
10340
+ repoRoot: argv.cwd,
10341
+ dryRun: argv.dryRun,
10342
+ ...tokens,
10343
+ org: resolveOrg(argv, loaded.resolved)
10344
+ },
10345
+ outFile: argv.out
10346
+ });
10347
+ }).command("run-resolved <url>", "Run an already-resolved check list (from `monitoring resolve-checks`) against a URL — no config/plugin loading at all, the trusted half of the delivery.previewCheck split (holocron#1030/#1032)", (y) => y.positional("url", {
10348
+ type: "string",
10349
+ describe: "URL to run every resolved check against"
10350
+ }).option("checks-file", {
10351
+ type: "string",
10352
+ demandOption: true,
10353
+ describe: "File path to read the resolved checks from (written by `monitoring resolve-checks`)"
10354
+ }).option("vercel-project", {
10355
+ type: "string",
10356
+ describe: "Expected Vercel project id/name `url` must resolve to before the Deployment Protection bypass header (VERCEL_PROTECTION_BYPASS_SECRET) is attached. Omitted → the header is never attached."
10357
+ }), async (argv) => {
10358
+ if ((await runMonitoringRunResolved({
10359
+ checks: JSON.parse(readFileSync(argv.checksFile, "utf8")),
10360
+ url: argv.url,
10361
+ ...argv.vercelProject ? { vercelProject: argv.vercelProject } : {},
10362
+ ...process.env["VERCEL_TOKEN"] ? { vercelToken: process.env["VERCEL_TOKEN"] } : {},
10363
+ ...process.env["VERCEL_TEAM_ID"] ? { vercelTeamId: process.env["VERCEL_TEAM_ID"] } : {},
10364
+ ...process.env["VERCEL_PROTECTION_BYPASS_SECRET"] ? { bypassSecret: process.env["VERCEL_PROTECTION_BYPASS_SECRET"] } : {}
10129
10365
  })).status === "fail") process.exitCode = 1;
10130
10366
  }).command("$0", false, () => {}, async (argv) => {
10131
10367
  await launchGroupMenu("monitoring", argv);