@theholocron/cli 5.0.0-alpha.185 → 5.0.0-alpha.186

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
@@ -26,7 +26,7 @@ import ora from "ora";
26
26
  import { findRootSync, isWorkspaceRoot, walkFilesSync } from "@theholocron/fs-utils";
27
27
  import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
28
28
  import { stringifyJson } from "@theholocron/json-utils";
29
- import { createHash, randomUUID } from "node:crypto";
29
+ import { createHash, randomBytes, 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";
32
32
  import { createGitHubClient } from "@theholocron/github-client";
@@ -3249,8 +3249,16 @@ async function runNew(input) {
3249
3249
  function defaultSecretName(channel) {
3250
3250
  return `HOLOCRON_NOTIFY_WEBHOOK_${channel.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
3251
3251
  }
3252
- /** A throwaway secret, written and removed to prove write access. */
3253
- const PREFLIGHT_SECRET = "HOLOCRON_NOTIFY_PREFLIGHT";
3252
+ /**
3253
+ * Prefix of the throwaway secret the preflight writes and removes. Every run adds a
3254
+ * random suffix, so it can never overwrite (and then delete) a secret someone already
3255
+ * has; layouts may not use any name under this prefix for their own secrets.
3256
+ */
3257
+ const PREFLIGHT_PREFIX = "HOLOCRON_NOTIFY_PREFLIGHT";
3258
+ /** Whether a secret name is under the preflight's reserved prefix. GitHub secret names are case-insensitive. */
3259
+ function isReservedSecret(name) {
3260
+ return name.toUpperCase().startsWith(PREFLIGHT_PREFIX);
3261
+ }
3254
3262
  /**
3255
3263
  * Prove the secrets store can be WRITTEN before creating anything that needs it.
3256
3264
  * A webhook's URL is shown once; creating the webhook and then failing to store
@@ -3259,12 +3267,13 @@ const PREFLIGHT_SECRET = "HOLOCRON_NOTIFY_PREFLIGHT";
3259
3267
  * remove it.
3260
3268
  */
3261
3269
  async function assertSecretsReachable(secrets, scope) {
3270
+ const probe = `${PREFLIGHT_PREFIX}_${randomBytes(4).toString("hex").toUpperCase()}`;
3262
3271
  try {
3263
- await secrets.setSecret(scope, PREFLIGHT_SECRET, "ok");
3264
- await secrets.deleteSecret(scope, PREFLIGHT_SECRET);
3272
+ await secrets.setSecret(scope, probe, "ok");
3273
+ await secrets.deleteSecret(scope, probe);
3265
3274
  } catch (err) {
3266
3275
  const where = scope.kind === "organization" ? `organization ${scope.name}` : scope.kind;
3267
- throw new Error(`cannot write to the secrets store for ${where} (${describeProviderError(err)}) — fix that token before creating webhooks`, { cause: err });
3276
+ throw new Error(`cannot write to the secrets store for ${where} (${describeProviderError(err)}) — fix that token before creating webhooks. If a secret named ${probe} was left behind, delete it`, { cause: err });
3268
3277
  }
3269
3278
  }
3270
3279
  /** Why a capability is not loaded: the plugin's own load error when there is one (e.g. a missing token). */
@@ -3294,6 +3303,7 @@ async function provisionChannel(deps, spec) {
3294
3303
  const where = spec.category ? ` in ${spec.category}` : "";
3295
3304
  if (!notifications.ensureChannel) throw new Error(`\`${notifications.providerName}\` cannot create channels`);
3296
3305
  if (webhookCfg && !notifications.ensureWebhook) throw new Error(`\`${notifications.providerName}\` cannot create webhooks`);
3306
+ if (webhookCfg && isReservedSecret(secretName)) throw new Error(`secret name ${secretName} is reserved for the setup preflight -- choose another`);
3297
3307
  if (webhookCfg && !dryRun && !secrets) throw new Error("storing a webhook URL needs a `secrets` provider — add one to holocron.config.ts");
3298
3308
  if (dryRun) {
3299
3309
  print(` … would ensure channel ${spec.name}${where}`);
@@ -3463,10 +3473,10 @@ async function runNotificationsSetup(input) {
3463
3473
  }
3464
3474
  const layout = notifications.layout;
3465
3475
  if (!layout) return refuse(`the \`${notifications.providerName}\` provider declares no layout — add \`layout\` to its options in holocron.config.ts`);
3466
- const wantsWebhook = (layout.categories ?? []).some((c) => (c.channels ?? []).some((ch) => ch.webhook));
3476
+ const wantsSecrets = (layout.categories ?? []).some((c) => (c.channels ?? []).some((ch) => ch.webhook)) || (layout.roles ?? []).some((r) => r.secret);
3467
3477
  let secrets;
3468
- if (wantsWebhook && !dryRun) {
3469
- if (!loader.has("secrets")) return refuse(whyMissing(loader, "secrets", "the layout asks for webhooks, which need a `secrets` capability — add a `secrets` provider"));
3478
+ if (wantsSecrets && !dryRun) {
3479
+ if (!loader.has("secrets")) return refuse(whyMissing(loader, "secrets", "the layout stores webhook URLs or role ids, which need a `secrets` capability — add a `secrets` provider"));
3470
3480
  secrets = loader.get("secrets");
3471
3481
  }
3472
3482
  const org = input.context.org;
@@ -3474,19 +3484,29 @@ async function runNotificationsSetup(input) {
3474
3484
  kind: "organization",
3475
3485
  name: org
3476
3486
  } : void 0);
3477
- if (wantsWebhook && !scope && !dryRun) return refuse("no org to store webhook secrets under — pass --scope or set `org` in holocron.config.ts");
3487
+ if (wantsSecrets && !scope && !dryRun) return refuse("no org to store secrets under — pass --scope or set `org` in holocron.config.ts");
3478
3488
  if (secrets && scope && !dryRun) try {
3479
3489
  await assertSecretsReachable(secrets, scope);
3480
3490
  } catch (err) {
3481
3491
  return refuse(errorMessage(err), logReason(err));
3482
3492
  }
3483
3493
  const taken = /* @__PURE__ */ new Map();
3494
+ const claim = (secret, owner, hint) => {
3495
+ if (isReservedSecret(secret)) return `${owner} would store into ${secret}, a name reserved for the setup preflight — choose another`;
3496
+ const key = secret.toUpperCase();
3497
+ const other = taken.get(key);
3498
+ if (other) return `${other} and ${owner} would both store into ${secret} — ${hint}`;
3499
+ taken.set(key, owner);
3500
+ };
3484
3501
  for (const category of layout.categories ?? []) for (const channel of category.channels ?? []) {
3485
3502
  if (!channel.webhook) continue;
3486
- const secret = channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name);
3487
- const other = taken.get(secret);
3488
- if (other) return refuse(`channels \`${other}\` and \`${channel.name}\` would both store their webhook in ${secret} — give one a distinct \`webhook.secret\``);
3489
- taken.set(secret, channel.name);
3503
+ const clash = claim(channel.webhook !== true && channel.webhook.secret || defaultSecretName(channel.name), `channel \`${channel.name}\``, "give one a distinct `webhook.secret`");
3504
+ if (clash) return refuse(clash);
3505
+ }
3506
+ for (const role of layout.roles ?? []) {
3507
+ if (!role.secret) continue;
3508
+ const clash = claim(role.secret, `role \`${role.name}\``, "give one a distinct secret");
3509
+ if (clash) return refuse(clash);
3490
3510
  }
3491
3511
  print(`Holocron notifications setup — ${notifications.providerName}${dryRun ? " (dry-run)" : ""}`);
3492
3512
  const attempt = async (step, fn) => {
@@ -3537,10 +3557,11 @@ async function runNotificationsSetup(input) {
3537
3557
  for (const role of layout.roles ?? []) await attempt(`role ${role.name}`, async () => {
3538
3558
  if (!notifications.ensureRole) throw new Error(`\`${notifications.providerName}\` cannot manage roles`);
3539
3559
  if (dryRun) {
3540
- print(` … would ensure role ${role.name}`);
3560
+ print(` … would ensure role ${role.name}${role.secret ? ` and store its id in ${role.secret}` : ""}`);
3541
3561
  report.roles.push({
3542
3562
  name: role.name,
3543
- created: false
3563
+ created: false,
3564
+ ...role.secret ? { secret: role.secret } : {}
3544
3565
  });
3545
3566
  return;
3546
3567
  }
@@ -3555,6 +3576,12 @@ async function runNotificationsSetup(input) {
3555
3576
  };
3556
3577
  report.roles.push(outcome);
3557
3578
  print(` ${made.created ? "✓ created" : "· exists "} role ${role.name} (id ${made.id})`);
3579
+ if (role.secret) {
3580
+ await secrets.setSecret(scope, role.secret, made.id);
3581
+ outcome.secret = role.secret;
3582
+ outcome.stored = true;
3583
+ print(` ✓ stored the id of ${role.name} in ${role.secret}`);
3584
+ }
3558
3585
  });
3559
3586
  if (report.failures.length > 0) {
3560
3587
  report.status = "fail";