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 +107 -17
- 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;
|
|
@@ -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
|
|
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
|
|
269
|
+
if (!match) return { kind: "absent" };
|
|
266
270
|
const value = match.attributes?.value;
|
|
267
|
-
return typeof value === "string" ? value :
|
|
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
|
|
713
|
-
|
|
714
|
-
|
|
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.
|
|
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."
|