create-op-node 0.11.3 → 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;
@@ -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
  }
@@ -1520,7 +1535,9 @@ function validatePlistInput(input) {
1520
1535
  ["jwtSecret", input.jwtSecret],
1521
1536
  ["supabaseAnonKey", input.supabaseAnonKey],
1522
1537
  ["supabaseServiceRoleKey", input.supabaseServiceRoleKey],
1523
- ["dashboardPassword", input.dashboardPassword]
1538
+ ["dashboardPassword", input.dashboardPassword],
1539
+ ["gatewayHmacSecret", input.gatewayHmacSecret],
1540
+ ["grafanaAdminPassword", input.grafanaAdminPassword]
1524
1541
  ];
1525
1542
  for (const [name, value] of supabaseFields) {
1526
1543
  if (value !== void 0 && !SAFE_LAUNCHCTL_VALUE_RE.test(value)) {
@@ -1548,6 +1565,8 @@ function buildSetenvCommand(input) {
1548
1565
  ...input.supabaseAnonKey !== void 0 ? [`launchctl setenv SUPABASE_ANON_KEY "${input.supabaseAnonKey}"`] : [],
1549
1566
  ...input.supabaseServiceRoleKey !== void 0 ? [`launchctl setenv SUPABASE_SERVICE_ROLE_KEY "${input.supabaseServiceRoleKey}"`] : [],
1550
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}"`] : [],
1551
1570
  ...input.supabaseUrl !== void 0 ? [`launchctl setenv SUPABASE_URL "${input.supabaseUrl}"`] : []
1552
1571
  ].join("; ");
1553
1572
  }
@@ -1586,6 +1605,8 @@ function plistInputFromSetup(input, keyFilePath) {
1586
1605
  ...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
1587
1606
  ...input.supabaseServiceRoleKey !== void 0 ? { supabaseServiceRoleKey: input.supabaseServiceRoleKey } : {},
1588
1607
  ...input.dashboardPassword !== void 0 ? { dashboardPassword: input.dashboardPassword } : {},
1608
+ ...input.gatewayHmacSecret !== void 0 ? { gatewayHmacSecret: input.gatewayHmacSecret } : {},
1609
+ ...input.grafanaAdminPassword !== void 0 ? { grafanaAdminPassword: input.grafanaAdminPassword } : {},
1589
1610
  ...input.supabaseUrl !== void 0 ? { supabaseUrl: input.supabaseUrl } : {}
1590
1611
  };
1591
1612
  }
@@ -1755,6 +1776,19 @@ export SUPABASE_ANON_KEY="$(require_secret supabase-anon-key)"
1755
1776
  export SUPABASE_SERVICE_ROLE_KEY="$(require_secret supabase-service-role-key)"
1756
1777
  export DASHBOARD_PASSWORD="$(require_secret dashboard-password)"
1757
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
+
1758
1792
  # AUTH_JWT_SECRET historically equals JWT_SECRET in dev/UAT \u2014 supply that
1759
1793
  # default if the operator hasn't set one independently.
1760
1794
  export AUTH_JWT_SECRET="\${AUTH_JWT_SECRET:-$JWT_SECRET}"
@@ -2551,6 +2585,22 @@ async function collectCoreSecrets(args) {
2551
2585
  validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2552
2586
  generate: generateDashboardPassword
2553
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
+ });
2554
2604
  return {
2555
2605
  pgsodiumKey,
2556
2606
  tunnelToken,
@@ -2558,7 +2608,9 @@ async function collectCoreSecrets(args) {
2558
2608
  jwtSecret,
2559
2609
  supabaseAnonKey,
2560
2610
  supabaseServiceRoleKey,
2561
- dashboardPassword
2611
+ dashboardPassword,
2612
+ gatewayHmacSecret,
2613
+ grafanaAdminPassword
2562
2614
  };
2563
2615
  }
2564
2616
  async function collectPromptServiceSecrets(args) {
@@ -2655,6 +2707,8 @@ async function runLaunchAgentPhase(args) {
2655
2707
  supabaseAnonKey: secrets.supabaseAnonKey,
2656
2708
  supabaseServiceRoleKey: secrets.supabaseServiceRoleKey,
2657
2709
  dashboardPassword: secrets.dashboardPassword,
2710
+ gatewayHmacSecret: secrets.gatewayHmacSecret,
2711
+ grafanaAdminPassword: secrets.grafanaAdminPassword,
2658
2712
  supabaseUrl: secrets.supabaseUrl
2659
2713
  });
2660
2714
  if (!la.ok) {
@@ -2741,6 +2795,14 @@ function buildComposeEnv(args) {
2741
2795
  SUPABASE_ANON_KEY: secrets.supabaseAnonKey,
2742
2796
  SUPABASE_SERVICE_ROLE_KEY: secrets.supabaseServiceRoleKey,
2743
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,
2744
2806
  SUPABASE_URL: secrets.supabaseUrl,
2745
2807
  AUTH_JWT_SECRET: process.env["AUTH_JWT_SECRET"] ?? secrets.jwtSecret,
2746
2808
  ...secrets.tunnelToken !== void 0 ? { TUNNEL_TOKEN: secrets.tunnelToken } : {},
@@ -2748,6 +2810,26 @@ function buildComposeEnv(args) {
2748
2810
  ...embeddingModel ? { EMBEDDINGS_MODEL: embeddingModel } : {}
2749
2811
  };
2750
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
+ }
2751
2833
  function planSignatureGate(allImages, opts) {
2752
2834
  if (opts.skipSignatureCheck) return { kind: "skip" };
2753
2835
  if (allImages === null) return { kind: "enumerate-failed" };
@@ -2824,6 +2906,7 @@ async function runStackPhase(args) {
2824
2906
  profiles: opts.localOnly ? LOCAL_PROFILES : PUBLIC_PROFILES,
2825
2907
  env: composeEnv
2826
2908
  };
2909
+ enforcePublicProfileSecrets({ localOnly: Boolean(opts.localOnly), env: composeEnv });
2827
2910
  if (opts.localOnly) {
2828
2911
  const evict = p3.spinner();
2829
2912
  evict.start("Evicting any cloudflared from a prior public bootstrap\u2026");
@@ -5004,7 +5087,7 @@ function withDefaultSubcommand(argv, known) {
5004
5087
  }
5005
5088
 
5006
5089
  // src/cli.ts
5007
- var VERSION = "0.11.3";
5090
+ var VERSION = "0.12.0";
5008
5091
  var program = new Command();
5009
5092
  program.name("create-op-node").description(
5010
5093
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."