create-op-node 0.11.2 → 0.12.0

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
@@ -20,6 +20,10 @@ var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
20
20
  var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
21
21
  var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
22
22
  var SAFE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
23
+ var WELL_KNOWN_GATEWAY_HMAC_SECRET = (
24
+ // eslint-disable-next-line sonarjs/hardcoded-secret-signatures, sonarjs/no-hardcoded-passwords -- this is the PUBLIC well-known template default we are guarding AGAINST, not a live credential
25
+ "local-only-hmac-secret-rotate-before-tunnel-exposure"
26
+ );
23
27
  var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
24
28
  var API_REQUEST_TIMEOUT_MS = 15e3;
25
29
  var BODY_PREVIEW_MAX = 200;
@@ -259,12 +263,12 @@ async function fetchOutput(auth, workspaceId, outputName) {
259
263
  auth.token,
260
264
  `/workspaces/${encodeURIComponent(workspaceId)}/current-state-version-outputs`
261
265
  );
262
- if (res.status !== 200) return null;
266
+ if (res.status !== 200) return { kind: "error" };
263
267
  const body = res.body;
264
268
  const match = body.data?.find((o) => o.attributes?.name === outputName);
265
- if (!match) return null;
269
+ if (!match) return { kind: "absent" };
266
270
  const value = match.attributes?.value;
267
- return typeof value === "string" ? value : null;
271
+ return typeof value === "string" ? { kind: "value", value } : { kind: "absent" };
268
272
  }
269
273
  async function safeExeca(cmd, args, options) {
270
274
  try {
@@ -296,7 +300,9 @@ var FRIENDLY = {
296
300
  "dashboard-password": "Supabase Studio dashboard password",
297
301
  "prompts-db-password": "prompt-service Postgres password",
298
302
  "prompt-service-api-key": "prompt-service HMAC API key",
299
- "prompt-service-admin-api-key": "prompt-service admin API key"
303
+ "prompt-service-admin-api-key": "prompt-service admin API key",
304
+ "gateway-hmac-secret": "API Gateway HMAC secret",
305
+ "grafana-admin-password": "Grafana admin password"
300
306
  };
301
307
  function labelFor(coords) {
302
308
  return `Opus Populi (${coords.region}) \u2014 ${FRIENDLY[coords.account]}`;
@@ -590,6 +596,15 @@ function generateDashboardPassword() {
590
596
  function generateHmacApiKey() {
591
597
  return base64url(randomBytes(32));
592
598
  }
599
+ function generateGatewayHmacSecret() {
600
+ return base64url(randomBytes(32));
601
+ }
602
+ function renderApiKeys(gatewayHmacSecret) {
603
+ return JSON.stringify({ "api-gateway": gatewayHmacSecret });
604
+ }
605
+ function generateGrafanaAdminPassword() {
606
+ return base64url(randomBytes(24));
607
+ }
593
608
  function generateJwtSecret() {
594
609
  return randomBytes(48).toString("base64");
595
610
  }
@@ -709,21 +724,28 @@ async function waitForRunOutput(input, budgets, deps, runId) {
709
724
  );
710
725
  if (r?.finished) {
711
726
  if (!r.succeeded) return { kind: "run-failed", status: r.status };
712
- deps.onProgress?.("fetching");
713
- const value = await safePoll(
714
- () => deps.fetchOutput(
715
- { token: input.token, organization: input.organization },
716
- input.workspaceId,
717
- input.outputName
718
- ),
719
- () => deps.onProgress?.("retry")
720
- );
721
- return value !== null ? { kind: "success", value } : { kind: "output-missing" };
727
+ const out = await tryFetchOutput(input, deps);
728
+ if (out.kind === "value") return { kind: "success", value: out.value };
729
+ if (out.kind === "absent") return { kind: "output-missing" };
722
730
  }
723
731
  await deps.sleep(budgets.pollMs);
724
732
  }
725
733
  return { kind: "timeout" };
726
734
  }
735
+ async function tryFetchOutput(input, deps) {
736
+ deps.onProgress?.("fetching");
737
+ const out = await safePoll(
738
+ () => deps.fetchOutput(
739
+ { token: input.token, organization: input.organization },
740
+ input.workspaceId,
741
+ input.outputName
742
+ ),
743
+ () => deps.onProgress?.("retry")
744
+ );
745
+ if (out === null) return { kind: "error" };
746
+ if (out.kind === "error") deps.onProgress?.("retry");
747
+ return out;
748
+ }
727
749
 
728
750
  // src/commands/init.ts
729
751
  async function ghTokenFromCli() {
@@ -1513,7 +1535,9 @@ function validatePlistInput(input) {
1513
1535
  ["jwtSecret", input.jwtSecret],
1514
1536
  ["supabaseAnonKey", input.supabaseAnonKey],
1515
1537
  ["supabaseServiceRoleKey", input.supabaseServiceRoleKey],
1516
- ["dashboardPassword", input.dashboardPassword]
1538
+ ["dashboardPassword", input.dashboardPassword],
1539
+ ["gatewayHmacSecret", input.gatewayHmacSecret],
1540
+ ["grafanaAdminPassword", input.grafanaAdminPassword]
1517
1541
  ];
1518
1542
  for (const [name, value] of supabaseFields) {
1519
1543
  if (value !== void 0 && !SAFE_LAUNCHCTL_VALUE_RE.test(value)) {
@@ -1541,6 +1565,8 @@ function buildSetenvCommand(input) {
1541
1565
  ...input.supabaseAnonKey !== void 0 ? [`launchctl setenv SUPABASE_ANON_KEY "${input.supabaseAnonKey}"`] : [],
1542
1566
  ...input.supabaseServiceRoleKey !== void 0 ? [`launchctl setenv SUPABASE_SERVICE_ROLE_KEY "${input.supabaseServiceRoleKey}"`] : [],
1543
1567
  ...input.dashboardPassword !== void 0 ? [`launchctl setenv DASHBOARD_PASSWORD "${input.dashboardPassword}"`] : [],
1568
+ ...input.gatewayHmacSecret !== void 0 ? [`launchctl setenv GATEWAY_HMAC_SECRET "${input.gatewayHmacSecret}"`] : [],
1569
+ ...input.grafanaAdminPassword !== void 0 ? [`launchctl setenv GRAFANA_ADMIN_PASSWORD "${input.grafanaAdminPassword}"`] : [],
1544
1570
  ...input.supabaseUrl !== void 0 ? [`launchctl setenv SUPABASE_URL "${input.supabaseUrl}"`] : []
1545
1571
  ].join("; ");
1546
1572
  }
@@ -1579,6 +1605,8 @@ function plistInputFromSetup(input, keyFilePath) {
1579
1605
  ...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
1580
1606
  ...input.supabaseServiceRoleKey !== void 0 ? { supabaseServiceRoleKey: input.supabaseServiceRoleKey } : {},
1581
1607
  ...input.dashboardPassword !== void 0 ? { dashboardPassword: input.dashboardPassword } : {},
1608
+ ...input.gatewayHmacSecret !== void 0 ? { gatewayHmacSecret: input.gatewayHmacSecret } : {},
1609
+ ...input.grafanaAdminPassword !== void 0 ? { grafanaAdminPassword: input.grafanaAdminPassword } : {},
1582
1610
  ...input.supabaseUrl !== void 0 ? { supabaseUrl: input.supabaseUrl } : {}
1583
1611
  };
1584
1612
  }
@@ -1748,6 +1776,19 @@ export SUPABASE_ANON_KEY="$(require_secret supabase-anon-key)"
1748
1776
  export SUPABASE_SERVICE_ROLE_KEY="$(require_secret supabase-service-role-key)"
1749
1777
  export DASHBOARD_PASSWORD="$(require_secret dashboard-password)"
1750
1778
 
1779
+ # API Gateway HMAC secret. The gateway signs gateway\u2192microservice requests
1780
+ # with this (X-HMAC-Auth) in EVERY mode, so it is required. API_KEYS is the
1781
+ # JSON map each microservice reads to verify the caller; the api-gateway's
1782
+ # key MUST equal GATEWAY_HMAC_SECRET. We render the JSON from the single
1783
+ # Keychain value here so the compose \`\${GATEWAY_HMAC_SECRET:-\u2026}\` /
1784
+ # \`\${API_KEYS:-\u2026}\` well-known defaults are always overridden with a real
1785
+ # per-node value.
1786
+ export GATEWAY_HMAC_SECRET="$(require_secret gateway-hmac-secret)"
1787
+ export API_KEYS="{\\"api-gateway\\":\\"\${GATEWAY_HMAC_SECRET}\\"}"
1788
+
1789
+ # Grafana admin password \u2014 overrides the compose \`admin\`/\`admin\` default.
1790
+ export GRAFANA_ADMIN_PASSWORD="$(require_secret grafana-admin-password)"
1791
+
1751
1792
  # AUTH_JWT_SECRET historically equals JWT_SECRET in dev/UAT \u2014 supply that
1752
1793
  # default if the operator hasn't set one independently.
1753
1794
  export AUTH_JWT_SECRET="\${AUTH_JWT_SECRET:-$JWT_SECRET}"
@@ -2544,6 +2585,22 @@ async function collectCoreSecrets(args) {
2544
2585
  validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2545
2586
  generate: generateDashboardPassword
2546
2587
  });
2588
+ const gatewayHmacSecret = await loadSecret({
2589
+ region,
2590
+ account: "gateway-hmac-secret",
2591
+ label: "API Gateway HMAC secret",
2592
+ validate: (v) => URL_SAFE_PASSWORD_RE.test(v) && v !== WELL_KNOWN_GATEWAY_HMAC_SECRET ? void 0 : "must be base64url chars only (no + / =) and not the well-known template default",
2593
+ generate: generateGatewayHmacSecret
2594
+ });
2595
+ const grafanaAdminPassword = await loadSecret({
2596
+ region,
2597
+ account: "grafana-admin-password",
2598
+ label: "Grafana admin password",
2599
+ // URL-safe alphabet — exported into the compose env for Grafana's
2600
+ // GF_SECURITY_ADMIN_PASSWORD; keep it free of shell/URL metacharacters.
2601
+ validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2602
+ generate: generateGrafanaAdminPassword
2603
+ });
2547
2604
  return {
2548
2605
  pgsodiumKey,
2549
2606
  tunnelToken,
@@ -2551,7 +2608,9 @@ async function collectCoreSecrets(args) {
2551
2608
  jwtSecret,
2552
2609
  supabaseAnonKey,
2553
2610
  supabaseServiceRoleKey,
2554
- dashboardPassword
2611
+ dashboardPassword,
2612
+ gatewayHmacSecret,
2613
+ grafanaAdminPassword
2555
2614
  };
2556
2615
  }
2557
2616
  async function collectPromptServiceSecrets(args) {
@@ -2648,6 +2707,8 @@ async function runLaunchAgentPhase(args) {
2648
2707
  supabaseAnonKey: secrets.supabaseAnonKey,
2649
2708
  supabaseServiceRoleKey: secrets.supabaseServiceRoleKey,
2650
2709
  dashboardPassword: secrets.dashboardPassword,
2710
+ gatewayHmacSecret: secrets.gatewayHmacSecret,
2711
+ grafanaAdminPassword: secrets.grafanaAdminPassword,
2651
2712
  supabaseUrl: secrets.supabaseUrl
2652
2713
  });
2653
2714
  if (!la.ok) {
@@ -2734,6 +2795,14 @@ function buildComposeEnv(args) {
2734
2795
  SUPABASE_ANON_KEY: secrets.supabaseAnonKey,
2735
2796
  SUPABASE_SERVICE_ROLE_KEY: secrets.supabaseServiceRoleKey,
2736
2797
  DASHBOARD_PASSWORD: secrets.dashboardPassword,
2798
+ // Real per-node gateway HMAC secret + the derived API_KEYS map. These
2799
+ // override the template's well-known `${GATEWAY_HMAC_SECRET:-…}` /
2800
+ // `${API_KEYS:-…}` compose defaults so a Tunnel-exposed node never signs
2801
+ // with the shared placeholder. The api-gateway's key MUST equal
2802
+ // GATEWAY_HMAC_SECRET, which renderApiKeys guarantees.
2803
+ GATEWAY_HMAC_SECRET: secrets.gatewayHmacSecret,
2804
+ API_KEYS: renderApiKeys(secrets.gatewayHmacSecret),
2805
+ GRAFANA_ADMIN_PASSWORD: secrets.grafanaAdminPassword,
2737
2806
  SUPABASE_URL: secrets.supabaseUrl,
2738
2807
  AUTH_JWT_SECRET: process.env["AUTH_JWT_SECRET"] ?? secrets.jwtSecret,
2739
2808
  ...secrets.tunnelToken !== void 0 ? { TUNNEL_TOKEN: secrets.tunnelToken } : {},
@@ -2741,6 +2810,26 @@ function buildComposeEnv(args) {
2741
2810
  ...embeddingModel ? { EMBEDDINGS_MODEL: embeddingModel } : {}
2742
2811
  };
2743
2812
  }
2813
+ function checkPublicProfileSecrets(env) {
2814
+ const gateway = env["GATEWAY_HMAC_SECRET"];
2815
+ const apiKeys = env["API_KEYS"];
2816
+ const bad = (v) => v === void 0 || v.length === 0 || v.includes(WELL_KNOWN_GATEWAY_HMAC_SECRET);
2817
+ if (bad(gateway) || bad(apiKeys)) {
2818
+ return {
2819
+ ok: false,
2820
+ reason: "Refusing to expose this node via the Cloudflare Tunnel (public profile) with the well-known GATEWAY_HMAC_SECRET / API_KEYS default. Every copy of the template ships the same placeholder value \u2014 a Tunnel-exposed gateway signed with it is trivially forgeable. Re-run `create-op-node bootstrap` so it generates a real per-node secret (or use `--local-only` to run without the public profile)."
2821
+ };
2822
+ }
2823
+ return { ok: true };
2824
+ }
2825
+ function enforcePublicProfileSecrets(args) {
2826
+ if (args.localOnly) return;
2827
+ const verdict = checkPublicProfileSecrets(args.env);
2828
+ if (!verdict.ok) {
2829
+ p3.cancel(verdict.reason);
2830
+ process.exit(1);
2831
+ }
2832
+ }
2744
2833
  function planSignatureGate(allImages, opts) {
2745
2834
  if (opts.skipSignatureCheck) return { kind: "skip" };
2746
2835
  if (allImages === null) return { kind: "enumerate-failed" };
@@ -2817,6 +2906,7 @@ async function runStackPhase(args) {
2817
2906
  profiles: opts.localOnly ? LOCAL_PROFILES : PUBLIC_PROFILES,
2818
2907
  env: composeEnv
2819
2908
  };
2909
+ enforcePublicProfileSecrets({ localOnly: Boolean(opts.localOnly), env: composeEnv });
2820
2910
  if (opts.localOnly) {
2821
2911
  const evict = p3.spinner();
2822
2912
  evict.start("Evicting any cloudflared from a prior public bootstrap\u2026");
@@ -4997,7 +5087,7 @@ function withDefaultSubcommand(argv, known) {
4997
5087
  }
4998
5088
 
4999
5089
  // src/cli.ts
5000
- var VERSION = "0.11.2";
5090
+ var VERSION = "0.12.0";
5001
5091
  var program = new Command();
5002
5092
  program.name("create-op-node").description(
5003
5093
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."