create-op-node 0.9.1 → 0.10.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
@@ -237,7 +237,10 @@ var FRIENDLY = {
237
237
  "jwt-secret": "JWT signing secret",
238
238
  "supabase-anon-key": "Supabase anon key",
239
239
  "supabase-service-role-key": "Supabase service role key",
240
- "dashboard-password": "Supabase Studio dashboard password"
240
+ "dashboard-password": "Supabase Studio dashboard password",
241
+ "prompts-db-password": "prompt-service Postgres password",
242
+ "prompt-service-api-key": "prompt-service HMAC API key",
243
+ "prompt-service-admin-api-key": "prompt-service admin API key"
241
244
  };
242
245
  function labelFor(coords) {
243
246
  return `Opus Populi (${coords.region}) \u2014 ${FRIENDLY[coords.account]}`;
@@ -448,6 +451,9 @@ function generatePostgresPassword() {
448
451
  function generateDashboardPassword() {
449
452
  return base64url(randomBytes(24));
450
453
  }
454
+ function generateHmacApiKey() {
455
+ return base64url(randomBytes(32));
456
+ }
451
457
  function generateJwtSecret() {
452
458
  return randomBytes(48).toString("base64");
453
459
  }
@@ -1405,12 +1411,26 @@ async function teardownLaunchAgent(paths, opts = {}) {
1405
1411
 
1406
1412
  // src/lib/op-compose-script.ts
1407
1413
  var REGION_RE = /^[a-z0-9-]{2,32}$/;
1414
+ var PROMPT_SERVICE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
1408
1415
  function renderOpComposeScript(input) {
1409
1416
  if (!REGION_RE.test(input.region)) {
1410
1417
  throw new Error(
1411
1418
  `region ${JSON.stringify(input.region)} contains characters not allowed in a launchd / Keychain service identifier`
1412
1419
  );
1413
1420
  }
1421
+ if (input.promptServiceUrl !== void 0 && !PROMPT_SERVICE_URL_RE.test(input.promptServiceUrl)) {
1422
+ throw new Error(
1423
+ `promptServiceUrl ${JSON.stringify(input.promptServiceUrl)} contains characters not allowed in a wrapper export`
1424
+ );
1425
+ }
1426
+ const promptServiceUrlExport = input.promptServiceUrl !== void 0 ? `# Backend services call prompt-service via this URL. Service-level env
1427
+ # from docker-compose-prompt-service.yml overlay (when layered) wins
1428
+ # over this default \u2014 so colocated deployments still hit
1429
+ # http://opuspopuli-prompts:3210 even though we may have a public URL
1430
+ # baked in here.
1431
+ export PROMPT_SERVICE_URL="\${PROMPT_SERVICE_URL:-${input.promptServiceUrl}}"
1432
+
1433
+ ` : "";
1414
1434
  return `#!/bin/bash
1415
1435
  # op-compose \u2014 wrapper that reads region secrets from macOS Keychain just
1416
1436
  # before invoking \`docker compose\`. Auto-generated by
@@ -1470,6 +1490,29 @@ fi
1470
1490
  # default to localhost for --local-only.
1471
1491
  export SUPABASE_URL="\${SUPABASE_URL:-http://localhost:8000}"
1472
1492
 
1493
+ ${promptServiceUrlExport}# prompt-service credentials. Always exported \u2014 harmless when the operator
1494
+ # is NOT colocating prompt-service (the main compose doesn't read these
1495
+ # vars; the overlay does). The PROMPT_SERVICE_API_KEYS list is built from
1496
+ # the operator's region + the single HMAC key in Keychain, matching the
1497
+ # prompt-service env format \`<region>:<key>,<region>:<key>,\u2026\`.
1498
+ PROMPTS_DB_PASSWORD_VAL="$(optional_secret prompts-db-password)"
1499
+ PROMPT_SERVICE_API_KEY_VAL="$(optional_secret prompt-service-api-key)"
1500
+ PROMPT_SERVICE_ADMIN_API_KEY_VAL="$(optional_secret prompt-service-admin-api-key)"
1501
+ if [ -n "$PROMPTS_DB_PASSWORD_VAL" ]; then
1502
+ export PROMPTS_DB_PASSWORD="$PROMPTS_DB_PASSWORD_VAL"
1503
+ fi
1504
+ if [ -n "$PROMPT_SERVICE_API_KEY_VAL" ]; then
1505
+ export PROMPT_SERVICE_API_KEY="$PROMPT_SERVICE_API_KEY_VAL"
1506
+ # API_KEYS env on prompt-service is \`<region>:<key>,\u2026\` \u2014 render the
1507
+ # single-region pair here so prompt-service accepts the same key the
1508
+ # backend sends. The prompts team adds additional regions on the
1509
+ # central deployment for Phase 2.
1510
+ export PROMPT_SERVICE_API_KEYS="${input.region}:$PROMPT_SERVICE_API_KEY_VAL"
1511
+ fi
1512
+ if [ -n "$PROMPT_SERVICE_ADMIN_API_KEY_VAL" ]; then
1513
+ export PROMPT_SERVICE_ADMIN_API_KEYS="$PROMPT_SERVICE_ADMIN_API_KEY_VAL"
1514
+ fi
1515
+
1473
1516
  exec docker compose "$@"
1474
1517
  `;
1475
1518
  }
@@ -1478,7 +1521,10 @@ exec docker compose "$@"
1478
1521
  async function installOpComposeWrapper(input) {
1479
1522
  let content;
1480
1523
  try {
1481
- content = renderOpComposeScript({ region: input.region });
1524
+ content = renderOpComposeScript({
1525
+ region: input.region,
1526
+ ...input.promptServiceUrl !== void 0 ? { promptServiceUrl: input.promptServiceUrl } : {}
1527
+ });
1482
1528
  } catch (err) {
1483
1529
  return { ok: false, reason: err.message };
1484
1530
  }
@@ -1872,6 +1918,16 @@ var bootstrapCommand = new Command("bootstrap").description(
1872
1918
  "--supabase-url <url>",
1873
1919
  "Public-facing Supabase URL (what browsers + microservices use). Default: `http://localhost:8000` in --local-only mode, otherwise `https://supabase.<domain>` (which the operator can override)."
1874
1920
  )
1921
+ ).addOption(
1922
+ new Option(
1923
+ "--node-type <type>",
1924
+ "Deployment topology. `region` (default) = region node connecting to remote prompt-service. `region-with-prompts` = region node + colocated prompt-service overlay (team/dev use). `prompts-only` = future stub for the central prompts deployment."
1925
+ ).choices(["region", "region-with-prompts", "prompts-only"])
1926
+ ).addOption(
1927
+ new Option(
1928
+ "--prompt-service-url <url>",
1929
+ "Remote prompt-service URL. Only meaningful for --node-type=region (default: https://prompts.opuspopuli.org). Ignored for --node-type=region-with-prompts (the overlay pins it to http://opuspopuli-prompts:3210)."
1930
+ )
1875
1931
  ).addOption(new Option("--skip-stack", "Stop before `docker compose pull && up`").default(false)).addOption(
1876
1932
  new Option(
1877
1933
  "--local-only",
@@ -1879,6 +1935,32 @@ var bootstrapCommand = new Command("bootstrap").description(
1879
1935
  ).default(false)
1880
1936
  ).addOption(new Option("-y, --yes", "Skip confirmation prompts").default(false)).action(async (opts) => {
1881
1937
  p3.intro(pc2.bgCyan(pc2.black(" create-op-node bootstrap ")));
1938
+ const nodeType = opts.nodeType ? opts.nodeType : unwrap(
1939
+ await p3.select({
1940
+ message: "What kind of node are you provisioning?",
1941
+ options: [
1942
+ {
1943
+ value: "region",
1944
+ label: "Region node (civic data for one region, remote prompt-service)"
1945
+ },
1946
+ {
1947
+ value: "region-with-prompts",
1948
+ label: "Region + colocated prompt-service (team / dev)"
1949
+ },
1950
+ {
1951
+ value: "prompts-only",
1952
+ label: "Prompt-service node (central prompts deployment \u2014 future)"
1953
+ }
1954
+ ],
1955
+ initialValue: "region"
1956
+ })
1957
+ );
1958
+ if (nodeType === "prompts-only") {
1959
+ p3.cancel(
1960
+ "prompts-only node deployment is not yet implemented. This flag exists so the CLI surface is stable for when the prompts team deploys the central prompts.opuspopuli.org instance \u2014 but the deployment stack (compose file, healthchecks, observability) is filled in later, when there is an actual central prompt-service to deploy."
1961
+ );
1962
+ process.exit(1);
1963
+ }
1882
1964
  const region = opts.region ? opts.region : unwrap(
1883
1965
  await p3.text({
1884
1966
  message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
@@ -2064,6 +2146,46 @@ var bootstrapCommand = new Command("bootstrap").description(
2064
2146
  validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2065
2147
  generate: generateDashboardPassword
2066
2148
  });
2149
+ if (nodeType === "region-with-prompts") {
2150
+ await loadSecret({
2151
+ region,
2152
+ account: "prompts-db-password",
2153
+ label: "prompt-service Postgres password",
2154
+ validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2155
+ generate: generatePostgresPassword
2156
+ });
2157
+ await loadSecret({
2158
+ region,
2159
+ account: "prompt-service-api-key",
2160
+ label: "prompt-service HMAC API key",
2161
+ // Same URL-safe alphabet as other HMAC keys; embedded in the
2162
+ // `<region>:<key>` API_KEYS list and in Authorization headers.
2163
+ validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2164
+ generate: generateHmacApiKey
2165
+ });
2166
+ await loadSecret({
2167
+ region,
2168
+ account: "prompt-service-admin-api-key",
2169
+ label: "prompt-service admin API key",
2170
+ validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
2171
+ generate: generateHmacApiKey
2172
+ });
2173
+ } else {
2174
+ await loadSecret({
2175
+ region,
2176
+ account: "prompt-service-api-key",
2177
+ label: "prompt-service HMAC API key (issued by the prompts team)",
2178
+ placeholder: "paste the region-specific HMAC key",
2179
+ validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)"
2180
+ });
2181
+ }
2182
+ const promptServiceUrl = nodeType === "region-with-prompts" ? "http://opuspopuli-prompts:3210" : opts.promptServiceUrl ?? "https://prompts.opuspopuli.org";
2183
+ if (!SAFE_URL_RE.test(promptServiceUrl)) {
2184
+ p3.cancel(
2185
+ `--prompt-service-url ${JSON.stringify(promptServiceUrl)} contains characters outside the allowed URL set`
2186
+ );
2187
+ process.exit(1);
2188
+ }
2067
2189
  let supabaseUrl;
2068
2190
  if (opts.supabaseUrl) {
2069
2191
  supabaseUrl = opts.supabaseUrl;
@@ -2112,7 +2234,7 @@ var bootstrapCommand = new Command("bootstrap").description(
2112
2234
  }
2113
2235
  const wrapperSpin = p3.spinner();
2114
2236
  wrapperSpin.start("Installing bin/op-compose wrapper\u2026");
2115
- const wrapper = await installOpComposeWrapper({ repoDir: repoPath, region });
2237
+ const wrapper = await installOpComposeWrapper({ repoDir: repoPath, region, promptServiceUrl });
2116
2238
  if (!wrapper.ok) {
2117
2239
  wrapperSpin.stop(pc2.red("\u2717 op-compose wrapper install failed."));
2118
2240
  p3.cancel(wrapper.reason ?? "op-compose wrapper install failed.");
@@ -4212,7 +4334,7 @@ async function looksLikeRegionsRepo(dir) {
4212
4334
  }
4213
4335
 
4214
4336
  // src/cli.ts
4215
- var VERSION = "0.9.1";
4337
+ var VERSION = "0.10.0";
4216
4338
  var program = new Command();
4217
4339
  program.name("create-op-node").description(
4218
4340
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."