create-op-node 0.11.3 → 0.12.1
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 +92 -6
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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}"
|
|
@@ -2237,7 +2271,7 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2237
2271
|
new Option(
|
|
2238
2272
|
"--compose-file <path>",
|
|
2239
2273
|
"Repeatable. Compose file relative to repo root. Default: prod + backup (production), prod only (--local-only)."
|
|
2240
|
-
)
|
|
2274
|
+
).argParser(collectComposeFile)
|
|
2241
2275
|
).addOption(new Option("--env-file <path>", "Compose --env-file. Default: .env.production")).addOption(new Option("--skip-brew", "Skip the Homebrew package install pass").default(false)).addOption(
|
|
2242
2276
|
new Option(
|
|
2243
2277
|
"--skip-launch-agent",
|
|
@@ -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");
|
|
@@ -2928,6 +3011,9 @@ function resolveComposeFiles(repoPath, composeFile) {
|
|
|
2928
3011
|
const inputs = composeFile ?? ["docker-compose-prod.yml"];
|
|
2929
3012
|
return inputs.map((f) => f.startsWith("/") ? f : join(repoPath, f));
|
|
2930
3013
|
}
|
|
3014
|
+
function collectComposeFile(value, previous) {
|
|
3015
|
+
return [...previous ?? [], value];
|
|
3016
|
+
}
|
|
2931
3017
|
function resolveModels(opts) {
|
|
2932
3018
|
return [
|
|
2933
3019
|
opts.embeddingModel ?? DEFAULT_EMBEDDING_MODEL,
|
|
@@ -3261,7 +3347,7 @@ var resetCommand = new Command("reset").description(
|
|
|
3261
3347
|
new Option(
|
|
3262
3348
|
"--compose-file <path>",
|
|
3263
3349
|
"Repeatable. Compose file relative to repo root. Default: docker-compose-prod.yml"
|
|
3264
|
-
).
|
|
3350
|
+
).argParser(collectComposeFile)
|
|
3265
3351
|
).addOption(new Option("--env-file <path>", "Compose --env-file. Default: .env.production")).addOption(
|
|
3266
3352
|
new Option(
|
|
3267
3353
|
"--wipe-data",
|
|
@@ -5004,7 +5090,7 @@ function withDefaultSubcommand(argv, known) {
|
|
|
5004
5090
|
}
|
|
5005
5091
|
|
|
5006
5092
|
// src/cli.ts
|
|
5007
|
-
var VERSION = "0.
|
|
5093
|
+
var VERSION = "0.12.1";
|
|
5008
5094
|
var program = new Command();
|
|
5009
5095
|
program.name("create-op-node").description(
|
|
5010
5096
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|