create-op-node 0.10.15 → 0.10.17

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.js CHANGED
@@ -4,6 +4,7 @@ import pc2 from 'picocolors';
4
4
  import * as p3 from '@clack/prompts';
5
5
  import { execa } from 'execa';
6
6
  import { Octokit } from '@octokit/rest';
7
+ import _sodium from 'libsodium-wrappers';
7
8
  import { randomBytes, createHmac } from 'crypto';
8
9
  import { join, resolve, dirname } from 'path';
9
10
  import { mkdir, writeFile, access, chmod, rename, rm } from 'fs/promises';
@@ -443,25 +444,54 @@ async function createRepoFromTemplate(input) {
443
444
  defaultBranch: res.data.default_branch ?? "main"
444
445
  };
445
446
  }
446
- async function setRepoSecret(input) {
447
- const res = await safeExeca(
448
- "gh",
449
- ["secret", "set", input.name, "--repo", input.repo, "--body", "-"],
450
- { input: input.value }
451
- );
452
- if (res === null) {
447
+ function parseRepoSlug(repo) {
448
+ const slash = repo.indexOf("/");
449
+ if (slash <= 0 || slash === repo.length - 1 || repo.indexOf("/", slash + 1) !== -1) return null;
450
+ return { owner: repo.slice(0, slash), repo: repo.slice(slash + 1) };
451
+ }
452
+ async function setRepoSecrets(input) {
453
+ const firstName = input.secrets[0]?.name ?? "(none)";
454
+ const parsed = parseRepoSlug(input.repo);
455
+ if (!parsed) {
453
456
  return {
454
- written: false,
455
- reason: "`gh` CLI not installed \u2014 install from https://cli.github.com or seed secrets manually"
457
+ seeded: [],
458
+ failed: { name: firstName, reason: `invalid repo "${input.repo}" (expected <owner>/<repo>)` }
456
459
  };
457
460
  }
458
- if (res.exitCode !== 0) {
461
+ const { owner, repo } = parsed;
462
+ const octokit = client(input.token);
463
+ let key;
464
+ try {
465
+ key = await octokit.actions.getRepoPublicKey({ owner, repo });
466
+ } catch (err) {
459
467
  return {
460
- written: false,
461
- reason: `gh secret set failed (${res.exitCode ?? "signal"}): ${res.stderr || res.stdout}`
468
+ seeded: [],
469
+ failed: { name: firstName, reason: `fetching the repo public key failed: ${err.message}` }
462
470
  };
463
471
  }
464
- return { written: true };
472
+ await _sodium.ready;
473
+ const sodium = _sodium;
474
+ const publicKey = sodium.from_base64(key.data.key, sodium.base64_variants.ORIGINAL);
475
+ const seeded = [];
476
+ for (const s of input.secrets) {
477
+ const encrypted_value = sodium.to_base64(
478
+ sodium.crypto_box_seal(sodium.from_string(s.value), publicKey),
479
+ sodium.base64_variants.ORIGINAL
480
+ );
481
+ try {
482
+ await octokit.actions.createOrUpdateRepoSecret({
483
+ owner,
484
+ repo,
485
+ secret_name: s.name,
486
+ encrypted_value,
487
+ key_id: key.data.key_id
488
+ });
489
+ } catch (err) {
490
+ return { seeded, failed: { name: s.name, reason: `setting secret ${s.name} failed: ${err.message}` } };
491
+ }
492
+ seeded.push(s.name);
493
+ }
494
+ return { seeded };
465
495
  }
466
496
  async function commitFile(input) {
467
497
  const [owner, repo] = input.repo.split("/");
@@ -728,7 +758,7 @@ var initCommand = new Command("init").description(
728
758
  await confirmPlan({ opts, region, owner, newRepoFull, publicConfig });
729
759
  const created = await createRegionRepo({ opts, ghToken, owner, newRepoName, newRepoFull, region });
730
760
  if (publicConfig) {
731
- await seedRepoSecrets({ publicConfig, newRepoFull });
761
+ await seedRepoSecrets({ ghToken, publicConfig, newRepoFull });
732
762
  }
733
763
  const pr = publicConfig ? await openInfraPr({ opts, ghToken, region, newRepoFull, publicConfig, created }) : null;
734
764
  await persistPgsodiumKey({ keychain, opts, region });
@@ -889,7 +919,7 @@ async function createRegionRepo(args) {
889
919
  }
890
920
  }
891
921
  async function seedRepoSecrets(args) {
892
- const { publicConfig, newRepoFull } = args;
922
+ const { ghToken, publicConfig, newRepoFull } = args;
893
923
  const secrets = [
894
924
  { name: "CLOUDFLARE_API_TOKEN", value: publicConfig.cfToken },
895
925
  { name: "CLOUDFLARE_ACCOUNT_ID", value: publicConfig.cfAccount },
@@ -899,24 +929,20 @@ async function seedRepoSecrets(args) {
899
929
  ];
900
930
  const secSpin = p3.spinner();
901
931
  secSpin.start(`Seeding ${secrets.length} repo secrets\u2026`);
902
- const seeded = [];
903
- for (const s of secrets) {
904
- const r = await setRepoSecret({ repo: newRepoFull, name: s.name, value: s.value });
905
- if (!r.written) {
906
- secSpin.stop(pc2.red(`\u2717 Failed to set ${s.name}: ${r.reason ?? "unknown"}`));
907
- const remaining = secrets.slice(secrets.findIndex((x) => x.name === s.name)).map((x) => x.name);
908
- p3.cancel(
909
- [
910
- seeded.length > 0 ? `Already seeded on ${newRepoFull}: ${seeded.join(", ")}.` : `Nothing seeded yet on ${newRepoFull}.`,
911
- `Still pending: ${remaining.join(", ")}.`,
912
- "",
913
- `Make sure \`gh\` is installed and signed in (\`gh auth login\`).`,
914
- `Re-run with --use-existing-repo to retry (GitHub will overwrite the already-set secrets idempotently).`
915
- ].join("\n")
916
- );
917
- process.exit(1);
918
- }
919
- seeded.push(s.name);
932
+ const { seeded, failed } = await setRepoSecrets({ token: ghToken, repo: newRepoFull, secrets });
933
+ if (failed) {
934
+ secSpin.stop(pc2.red(`\u2717 Failed to set ${failed.name}: ${failed.reason}`));
935
+ const remaining = secrets.slice(seeded.length).map((x) => x.name);
936
+ p3.cancel(
937
+ [
938
+ seeded.length > 0 ? `Already seeded on ${newRepoFull}: ${seeded.join(", ")}.` : `Nothing seeded yet on ${newRepoFull}.`,
939
+ `Still pending: ${remaining.join(", ")}.`,
940
+ "",
941
+ `Make sure your --gh-token PAT has "Actions Secrets: write" (fine-grained) or \`repo\` scope (classic).`,
942
+ `Re-run with --use-existing-repo to retry (GitHub will overwrite the already-set secrets idempotently).`
943
+ ].join("\n")
944
+ );
945
+ process.exit(1);
920
946
  }
921
947
  secSpin.stop(pc2.green(`\u2713 Seeded ${secrets.length} secrets on ${newRepoFull}`));
922
948
  }
@@ -3331,7 +3357,7 @@ function reportResetOutcome(args) {
3331
3357
 
3332
3358
  // src/lib/cosign.ts
3333
3359
  var COSIGN_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
3334
- var DEFAULT_IDENTITY_REGEXP = "^https://github\\.com/OpusPopuli/opuspopuli/\\.github/workflows/.*$";
3360
+ var DEFAULT_IDENTITY_REGEXP = "^https://github\\.com/OpusPopuli/opuspopuli/\\.github/workflows/release\\.yml@refs/heads/.*$";
3335
3361
  async function cosignVerifyImage(input) {
3336
3362
  const idRegex = input.certificateIdentityRegexp ?? DEFAULT_IDENTITY_REGEXP;
3337
3363
  const issuer = input.certificateOidcIssuer ?? COSIGN_OIDC_ISSUER;
@@ -4860,7 +4886,7 @@ async function looksLikeRegionsRepo(dir) {
4860
4886
  }
4861
4887
 
4862
4888
  // src/cli.ts
4863
- var VERSION = "0.10.15";
4889
+ var VERSION = "0.10.17";
4864
4890
  var program = new Command();
4865
4891
  program.name("create-op-node").description(
4866
4892
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."