void 0.23.0 → 0.24.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/AGENTS_TEMPLATE.md +1 -1
- package/dist/{account-cmd-C84Ee8cO.mjs → account-cmd-vJhh5lrb.mjs} +5 -5
- package/dist/{auth-CSkdO2Bb.mjs → auth-CQkSDS50.mjs} +22 -49
- package/dist/auth-DX3JURcX.mjs +2 -0
- package/dist/{auth-link-CioEg6uY.mjs → auth-link-D75t9wO2.mjs} +5 -5
- package/dist/{auth-router-BsR981d4.mjs → auth-router-ClfhL1J7.mjs} +7 -7
- package/dist/{build-cmd-LzvNJORH.mjs → build-cmd-zMi1iFnS.mjs} +6 -6
- package/dist/{cache-C4MvnrMH.mjs → cache-ZtQmyxVV.mjs} +5 -5
- package/dist/{cancel-deploy-abUxpP2n.mjs → cancel-deploy-Box0kkEV.mjs} +5 -5
- package/dist/{cf-access-P0iZnu0q.mjs → cf-access-BGe5sWXF.mjs} +4 -48
- package/dist/{cf-access-DRsQRe6k.mjs → cf-access-TWNzXltp.mjs} +3 -11
- package/dist/cli/cli.mjs +47 -44
- package/dist/cli/cloudflare-operation-process.mjs +14 -5
- package/dist/cli/env-schema-probe.mjs +3 -3
- package/dist/client-B9UBI03V.mjs +2 -0
- package/dist/{client-RV8NVeB8.mjs → client-DKAsFA4X.mjs} +33 -15
- package/dist/{cloudflare-cmd-BcrVyTmJ.mjs → cloudflare-cmd-CmY8dGkd.mjs} +1 -1
- package/dist/{cloudflare-connect-B8uPZ4nx.mjs → cloudflare-connect-IiR3ZieC.mjs} +1 -1
- package/dist/{cloudflare-operations-AiWashgg.mjs → cloudflare-operations-BCdXSsmo.mjs} +1 -1
- package/dist/{cloudflare-operations-fxHb-byx.mjs → cloudflare-operations-DzH-A966.mjs} +1 -1
- package/dist/{connect-Bd4kJd9U.mjs → connect-B-usTnlF.mjs} +9 -9
- package/dist/{create-project-bMf6ffLZ.mjs → create-project-CYOOAYcn.mjs} +12 -12
- package/dist/{create-project-Boczwj5r.mjs → create-project-FWbf5Olo.mjs} +1 -1
- package/dist/{db-8uG64XLl.mjs → db-Ck2eZ2iB.mjs} +15 -15
- package/dist/{delete-NvbiyeJf.mjs → delete-sK56qtzZ.mjs} +5 -5
- package/dist/{deploy-DzAIwqNU.mjs → deploy-gCVaiJbS.mjs} +86 -44
- package/dist/{deploy-bjXdFCtn.mjs → deploy-irP5q0-x.mjs} +1 -1
- package/dist/{domain-y5Tvydvo.mjs → domain-DFfQE8jg.mjs} +6 -6
- package/dist/{email-B3umsW75.mjs → email-CdnhUHvJ.mjs} +7 -7
- package/dist/{env-BS6qYHDb.mjs → env-DmGdHi5i.mjs} +8 -8
- package/dist/{env-types-C2_bIk_b.mjs → env-types-D5kds_OZ.mjs} +1 -1
- package/dist/{env-validation-BPn7vk-V.mjs → env-validation-C_8tPdhs.mjs} +2 -2
- package/dist/{env-validation-DbTg7-ar.mjs → env-validation-zZOCcQVZ.mjs} +1 -1
- package/dist/{gen-B580--vC.mjs → gen-CshAkukR.mjs} +2 -2
- package/dist/gen-DvQTWzDU.mjs +2 -0
- package/dist/{github-cmd-v45BdfKX.mjs → github-cmd-DftuEvIt.mjs} +13 -13
- package/dist/{headers-DWi2IXWx.mjs → headers-2rKda9Y6.mjs} +1 -1
- package/dist/{help-daGKjXGk.mjs → help-Dotbf6iz.mjs} +8 -4
- package/dist/help-mabuIV98.mjs +2 -0
- package/dist/{inbound-aVHEUhKo.mjs → inbound-fRkqcXaC.mjs} +1 -1
- package/dist/index.mjs +16 -20
- package/dist/{init-Bvy7zrBo.mjs → init-BTQEZZGJ.mjs} +25 -22
- package/dist/{link-CDqqCjFl.mjs → link-B46sYH0K.mjs} +6 -6
- package/dist/{list-CZj0dzKY.mjs → list-BnPrIsU_.mjs} +6 -6
- package/dist/log-vqY3pqwI.mjs +29 -0
- package/dist/{login-UZKFM_u7.mjs → login-BuiMyUrx.mjs} +8 -8
- package/dist/login-DIy9Sb-U.mjs +2 -0
- package/dist/{logs-CUZ6t9t3.mjs → logs-CZG9d-ic.mjs} +10 -11
- package/dist/migrate-BCiJ8w0f.mjs +2 -0
- package/dist/{migrate-DHul7PRV.mjs → migrate-q4_njtaW.mjs} +2 -2
- package/dist/{operator-args-CLgKGlwU.mjs → operator-args-B3BTmSoi.mjs} +12 -5
- package/dist/{operator-auth-8KnOVXVd.mjs → operator-auth-7erYc6Lt.mjs} +2 -2
- package/dist/{operator-client-DUwumHpQ.mjs → operator-client-CpHjTd0t.mjs} +3 -3
- package/dist/{operator-cmd-DBA6dl0m.mjs → operator-cmd-DGqPJUD3.mjs} +6 -6
- package/dist/{output-Dm_A4Tbv.mjs → output-CbM85T4k.mjs} +18 -10
- package/dist/pages/index.mjs +1 -1
- package/dist/{platform-args-BJdRtlLq.mjs → platform-args-3k8X9-H_.mjs} +11 -3
- package/dist/platform-args-DdnM6WIf.mjs +2 -0
- package/dist/{platform-auth-config-B56E9YP1.mjs → platform-auth-config-DDrxUy4m.mjs} +6 -6
- package/dist/{platform-auth-protection-BDWO_aER.mjs → platform-auth-protection-BLadnnct.mjs} +8 -8
- package/dist/{platform-auth-recovery-1gg4CSRe.mjs → platform-auth-recovery-CmCzX2Fl.mjs} +6 -6
- package/dist/{platform-cmd-DMcQStSc.mjs → platform-cmd-DMDEc9uS.mjs} +10 -10
- package/dist/{platform-cmd-Bt-1w6pY.mjs → platform-cmd-pMTRhvYs.mjs} +1 -1
- package/dist/{platform-domain-BNkcz0OB.mjs → platform-domain-Di6EYrQl.mjs} +5 -5
- package/dist/{platform-lifecycle-9OyBALhH.mjs → platform-lifecycle-BBoNzojY.mjs} +1264 -407
- package/dist/{platform-lifecycle-BE6C_jxh.mjs → platform-lifecycle-BFdSi9Qt.mjs} +1 -1
- package/dist/{platform-management-CjwLVQwN.mjs → platform-management-CWOQOWXm.mjs} +5 -5
- package/dist/{platform-management-CnyTdcWX.mjs → platform-management-DG47jys8.mjs} +1 -1
- package/dist/{platform-plans-config-BNGKGr4P.mjs → platform-plans-config-Vl9BP3H4.mjs} +3 -3
- package/dist/{platform-recovery-D6KSpuFm.mjs → platform-recovery-BJnQJEtQ.mjs} +4 -4
- package/dist/{platform-registry-QBa4DK32.mjs → platform-registry-DkPPXfp_.mjs} +2 -2
- package/dist/{prepare-B5Mkic5u.mjs → prepare-CBLT3CHH.mjs} +3 -3
- package/dist/prepare-CnjcTV2s.mjs +2 -0
- package/dist/{prepare-DOsL0CC9.mjs → prepare-DnaKBi--.mjs} +1 -1
- package/dist/{project-C0Fqu05O.mjs → project-DKG5Ra8p.mjs} +15 -82
- package/dist/project-DsblYifa.mjs +2 -0
- package/dist/{project-cmd-CNzrqvBv.mjs → project-cmd-DxnXstAq.mjs} +19 -19
- package/dist/{project-team-DQOWPfQT.mjs → project-team-BmHcaALJ.mjs} +5 -5
- package/dist/{project-token-CTDYU0Cc.mjs → project-token-GionZlBI.mjs} +5 -5
- package/dist/{project-zero-trust-BNAkW-_0.mjs → project-zero-trust-JLCjVQA7.mjs} +5 -5
- package/dist/prompts-2TGE6oXx.mjs +2 -0
- package/dist/prompts-BNUoTiaf.mjs +2044 -0
- package/dist/{provision-C4ORE7G7.mjs → provision-CVku1JFU.mjs} +1 -1
- package/dist/{provision-CJFgTZY8.mjs → provision-CaoxO_U7.mjs} +1 -1
- package/dist/{requests-MhxYnau8.mjs → requests-BOjIVAeF.mjs} +5 -5
- package/dist/{rollback-CJ6iSDoU.mjs → rollback-CwR3pLj3.mjs} +6 -6
- package/dist/runtime/ai.mjs +3 -5
- package/dist/runtime/email/testing.mjs +1 -1
- package/dist/runtime/email.mjs +1 -1
- package/dist/runtime/remote/index.mjs +2 -2
- package/dist/{secret-Dli5fP0B.mjs → secret-ByWJWBUG.mjs} +7 -7
- package/dist/{skills-D1II1Juz.mjs → skills-C1PlclPt.mjs} +1 -1
- package/dist/{subcommand-prompt-CY1C4fvl.mjs → subcommand-prompt-klFCfcor.mjs} +40 -32
- package/package.json +8 -9
- package/skills/void/SKILL.md +6 -1
- package/skills/void/docs/guide/deployment.md +22 -0
- package/skills/void/docs/guide/edge/headers.md +1 -1
- package/skills/void/docs/guide/email/domains.md +5 -1
- package/skills/void/docs/guide/email/sending.md +1 -1
- package/skills/void/docs/guide/platform/administration/zero-trust.md +27 -9
- package/skills/void/docs/guide/platform/installation/ci.md +1 -1
- package/skills/void/docs/guide/platform/installation/credentials.md +10 -3
- package/skills/void/docs/guide/platform/installation/domains.md +8 -3
- package/skills/void/docs/guide/platform/installation/first-deployment.md +2 -2
- package/skills/void/docs/guide/platform/installation/maintenance.md +12 -0
- package/skills/void/docs/guide/platform/installation/prerequisites.md +8 -7
- package/skills/void/docs/guide/platform/installation/setup.md +80 -10
- package/skills/void/docs/guide/remote-dev.md +1 -1
- package/skills/void/docs/reference/cli/auth.md +4 -3
- package/skills/void/docs/reference/cli/database.md +1 -1
- package/skills/void/docs/reference/cli/deploy.md +20 -0
- package/skills/void/docs/reference/cli/platform-installation.md +42 -27
- package/skills/void/docs/reference/cli/platform-zero-trust.md +14 -0
- package/skills/void/docs/reference/config.md +1 -1
- package/dist/auth-CCjt0hcq.mjs +0 -2
- package/dist/client-Cu7jWiF1.mjs +0 -2
- package/dist/dist-BuDuKZJv.mjs +0 -2
- package/dist/dist-CTBk70IR.mjs +0 -1310
- package/dist/gen-BVaUUumi.mjs +0 -2
- package/dist/help-DofyZuY7.mjs +0 -2
- package/dist/log-ChfPKsVd.mjs +0 -26
- package/dist/login-DYt_An22.mjs +0 -2
- package/dist/migrate-8_2u55MD.mjs +0 -2
- package/dist/platform-args-D3RXyR6h.mjs +0 -2
- package/dist/prepare-cgvDMtSb.mjs +0 -2
- package/dist/project-BuUwfj6f.mjs +0 -2
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { n as __toESM } from "./rolldown-runtime-DXIUcv95.mjs";
|
|
2
1
|
import { a as join, n as dirname, u as resolve } from "./dist-BVqXeR_h.mjs";
|
|
3
|
-
import { t as
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { B as text$1, E as confirm, F as password, G as settings, I as select, K as u$1, M as multiselect, N as note, O as intro, P as outro, R as symbol, U as CANCEL_SYMBOL, W as isCancel, g as S_PASSWORD_MASK, i as S_BAR_END, j as log, r as S_BAR } from "./dist-CTBk70IR.mjs";
|
|
2
|
+
import { t as cfAccessHeaders } from "./cf-access-TWNzXltp.mjs";
|
|
3
|
+
import { _ as savePlatformConnection, a as listInstallationManifests, b as writePendingInstallationSecrets, d as promoteInstallationSecrets, f as readInstallationRecoverySecrets, g as resolveInstallationManifest, i as latestPlatformWorkerCutoverGeneration, l as platformRegistryDirectory, n as assertPlatformInstallationSlugAvailable, p as readPendingInstallationSecrets, s as normalizePlatformApiUrl, t as acquirePlatformLifecycleLock, u as platformWorkerSecretUpdateIntentState, y as writeInstallationManifest } from "./platform-registry-DkPPXfp_.mjs";
|
|
4
|
+
import { n as ensurePlatformAccessToken } from "./cf-access-BGe5sWXF.mjs";
|
|
5
|
+
import { u as openBrowser } from "./auth-CQkSDS50.mjs";
|
|
6
|
+
import { A as confirm, B as note, H as outro, K as select, L as log, N as intro, U as password, X as text$1, tt as isCancel, z as multiselect } from "./prompts-BNUoTiaf.mjs";
|
|
9
7
|
import { c as resolveCloudflareOperationArgv } from "./cloudflare-process-B-wekeR6.mjs";
|
|
10
8
|
import { o as runCloudflareAuthCommand } from "./cloudflare-auth-Qdc7tw8F.mjs";
|
|
11
|
-
import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-
|
|
12
|
-
import { c as import_picocolors
|
|
13
|
-
import {
|
|
9
|
+
import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-CaoxO_U7.mjs";
|
|
10
|
+
import { c as import_picocolors, i as createSpinner, n as cliCommandName, r as cliTitle } from "./output-CbM85T4k.mjs";
|
|
11
|
+
import { t as parsePlatformArgs } from "./platform-args-3k8X9-H_.mjs";
|
|
12
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
13
|
import { fileURLToPath } from "node:url";
|
|
15
14
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
17
16
|
import { execFile } from "node:child_process";
|
|
18
|
-
import { PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
17
|
+
import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAppAccessSetup, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
19
18
|
import { stripVTControlCharacters } from "node:util";
|
|
20
19
|
import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
21
20
|
import { readFile } from "node:fs/promises";
|
|
@@ -142,7 +141,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
142
141
|
} catch (error) {
|
|
143
142
|
const status = error instanceof PlatformCloudflareApiError ? error.status : null;
|
|
144
143
|
const reason = status === null ? "the request failed" : `HTTP ${status}`;
|
|
145
|
-
const guidance = status === 401 || status === 403 ? "Check your Cloudflare login or API token permissions for this account, then rerun the command." : status === 404 ? `
|
|
144
|
+
const guidance = status === 401 || status === 403 ? "Check your Cloudflare login or API token permissions for this account, then rerun the command." : status === 404 ? `Open ${setupUrl} once to provision your account for Workers automatically, then retry the install.` : "Rerun the command. If the dashboard already shows a subdomain, no Workers onboarding change is needed.";
|
|
146
145
|
throw new Error(`platform: Could not read the workers.dev subdomain for account ${accountId} (${reason}). ${guidance}`, { cause: error });
|
|
147
146
|
}
|
|
148
147
|
if (typeof result?.subdomain !== "string" || result.subdomain.trim() === "") throw new Error(`platform: This Cloudflare account does not have a workers.dev subdomain. Open ${setupUrl}, finish Workers onboarding, choose a subdomain, and rerun the command.`);
|
|
@@ -680,11 +679,12 @@ function hyperdriveOriginConfigHash(origin) {
|
|
|
680
679
|
host: origin.host.toLowerCase(),
|
|
681
680
|
port: origin.port,
|
|
682
681
|
scheme: "postgresql",
|
|
683
|
-
user: origin.user
|
|
682
|
+
user: origin.user,
|
|
683
|
+
...origin.accessClientId ? { accessClientId: origin.accessClientId } : {}
|
|
684
684
|
})).digest("hex");
|
|
685
685
|
}
|
|
686
686
|
function hyperdriveOriginMatches(live, expected) {
|
|
687
|
-
return live.database === expected.database && live.host.toLowerCase() === expected.host.toLowerCase() && live.port === expected.port && live.scheme === expected.scheme && (live.user === null || live.user === expected.user);
|
|
687
|
+
return live.database === expected.database && live.host.toLowerCase() === expected.host.toLowerCase() && live.port === expected.port && live.accessClientId === expected.accessClientId && live.scheme === expected.scheme && (live.user === null || live.user === expected.user);
|
|
688
688
|
}
|
|
689
689
|
function parseHyperdriveConfig(value) {
|
|
690
690
|
const origin = value.origin;
|
|
@@ -692,13 +692,15 @@ function parseHyperdriveConfig(value) {
|
|
|
692
692
|
const record = origin;
|
|
693
693
|
const scheme = requiredString(record.scheme, "Hyperdrive origin scheme").toLowerCase();
|
|
694
694
|
if (scheme !== "postgres" && scheme !== "postgresql") throw new Error(`platform: Unsupported Hyperdrive origin scheme ${JSON.stringify(scheme)}.`);
|
|
695
|
-
const
|
|
696
|
-
|
|
695
|
+
const accessClientId = typeof record.access_client_id === "string" && record.access_client_id ? record.access_client_id : void 0;
|
|
696
|
+
const port = accessClientId && record.port == null ? null : typeof record.port === "number" || typeof record.port === "string" ? Number(record.port) : NaN;
|
|
697
|
+
if (port !== null && (!Number.isInteger(port) || port <= 0 || port > 65535)) throw new Error("platform: Cloudflare Hyperdrive config has an invalid origin port.");
|
|
697
698
|
return {
|
|
698
699
|
id: requiredString(value.id, "Hyperdrive id"),
|
|
699
700
|
name: requiredString(value.name, "Hyperdrive name"),
|
|
700
701
|
cachingDisabled: !!value.caching && typeof value.caching === "object" && !Array.isArray(value.caching) && value.caching.disabled === true,
|
|
701
702
|
origin: {
|
|
703
|
+
...accessClientId ? { accessClientId } : {},
|
|
702
704
|
database: requiredString(record.database, "Hyperdrive origin database"),
|
|
703
705
|
host: requiredString(record.host, "Hyperdrive origin host").toLowerCase(),
|
|
704
706
|
port,
|
|
@@ -1124,7 +1126,343 @@ async function verifyPlatformAccessApplication(client, input, app, type, machine
|
|
|
1124
1126
|
if (machineId && !currentPolicies.some((policy) => policy.decision === "non_identity")) throw new Error("platform: The platform service token is not admitted by Access.");
|
|
1125
1127
|
}
|
|
1126
1128
|
//#endregion
|
|
1129
|
+
//#region src/cli/platform-setup.ts
|
|
1130
|
+
function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
|
|
1131
|
+
return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + import_picocolors.default.underline(page.title) + "\x1B]8;;\x1B\\" : page.title + ": " + import_picocolors.default.underline(page.url);
|
|
1132
|
+
}
|
|
1133
|
+
function platformSetupFieldHelp(key) {
|
|
1134
|
+
const docs = (page, section, title) => platformSetupLink({
|
|
1135
|
+
title,
|
|
1136
|
+
url: `https://void.cloud/guide/platform/installation/${page}#${section}`
|
|
1137
|
+
});
|
|
1138
|
+
switch (key) {
|
|
1139
|
+
case "databaseUrl": return {
|
|
1140
|
+
placeholder: "postgresql://user:pw@…/database",
|
|
1141
|
+
description: "Provide a PostgreSQL database URL for platform users, projects, and deployments. Use an empty hosted database, e.g. " + platformSetupLink({
|
|
1142
|
+
title: "Neon",
|
|
1143
|
+
url: "https://neon.com/"
|
|
1144
|
+
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
1145
|
+
};
|
|
1146
|
+
case "jwtSecret": return { description: `Create a JWT signing key for platform login tokens. Run ${import_picocolors.default.blue("openssl rand -base64 32")}. Save the key in your password manager. ` + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
1147
|
+
case "projectSecretKey": return { description: `Generate a project encryption key for app secrets. Run ${import_picocolors.default.blue("openssl rand -base64 32")} separately. Save this key for recovery. ` + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
1148
|
+
default: return;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function supportsSetupLinks() {
|
|
1152
|
+
return !!process.stdout.isTTY && ([
|
|
1153
|
+
"ghostty",
|
|
1154
|
+
"iTerm.app",
|
|
1155
|
+
"WezTerm",
|
|
1156
|
+
"vscode",
|
|
1157
|
+
"WarpTerminal"
|
|
1158
|
+
].includes(process.env.TERM_PROGRAM ?? "") || !!process.env.WT_SESSION || !!process.env.VTE_VERSION || !!process.env.KONSOLE_VERSION || process.env.TERM === "xterm-kitty");
|
|
1159
|
+
}
|
|
1160
|
+
/** Dashboard keys use permission-catalog labels, which can differ from product names. */
|
|
1161
|
+
function platformAccountTokenPermissions(includeAiGateway) {
|
|
1162
|
+
return [
|
|
1163
|
+
{
|
|
1164
|
+
key: "account_settings",
|
|
1165
|
+
type: "read"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
key: "workers_scripts",
|
|
1169
|
+
type: "edit"
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
key: "workers_kv_storage",
|
|
1173
|
+
type: "edit"
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
key: "workers_r2",
|
|
1177
|
+
type: "edit"
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
key: "d1",
|
|
1181
|
+
type: "edit"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
key: "queues",
|
|
1185
|
+
type: "edit"
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
key: "account_analytics",
|
|
1189
|
+
type: "read"
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
key: "workers_tail",
|
|
1193
|
+
type: "read"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
key: "query_cache",
|
|
1197
|
+
type: "edit"
|
|
1198
|
+
},
|
|
1199
|
+
...includeAiGateway ? [{
|
|
1200
|
+
key: "aig",
|
|
1201
|
+
type: "edit"
|
|
1202
|
+
}] : []
|
|
1203
|
+
];
|
|
1204
|
+
}
|
|
1205
|
+
function platformManagementSetupPage(plan, options = {}) {
|
|
1206
|
+
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1207
|
+
const zoneName = plan.deployment.application.zoneName;
|
|
1208
|
+
const apiHostname = plan.deployment.controlPlane.apiHostname;
|
|
1209
|
+
const access = options.access ?? (plan.deployment.authentication.accessProtection || plan.deployment.authentication.providers.includes("cloudflare-access") ? {
|
|
1210
|
+
protection: !!plan.deployment.authentication.accessProtection,
|
|
1211
|
+
readOnly: false
|
|
1212
|
+
} : void 0);
|
|
1213
|
+
const accessLevel = access && access.readOnly ? "read" : "edit";
|
|
1214
|
+
const appAccess = !!plan.deployment.appAccess;
|
|
1215
|
+
const appAccessLevel = appAccess ? "edit" : accessLevel;
|
|
1216
|
+
const permissions = [
|
|
1217
|
+
...platformAccountTokenPermissions(true),
|
|
1218
|
+
...zoneName || apiHostname || createsZone ? [
|
|
1219
|
+
{
|
|
1220
|
+
key: "zone",
|
|
1221
|
+
type: createsZone ? "edit" : "read"
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
key: "dns",
|
|
1225
|
+
type: "edit"
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
key: "workers_routes",
|
|
1229
|
+
type: "edit"
|
|
1230
|
+
}
|
|
1231
|
+
] : [],
|
|
1232
|
+
...access || appAccess ? [
|
|
1233
|
+
{
|
|
1234
|
+
key: "access",
|
|
1235
|
+
type: appAccessLevel
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
key: "access_acct",
|
|
1239
|
+
type: "read"
|
|
1240
|
+
},
|
|
1241
|
+
...access && access.protection ? [{
|
|
1242
|
+
key: "access_service_token",
|
|
1243
|
+
type: accessLevel
|
|
1244
|
+
}] : []
|
|
1245
|
+
] : [],
|
|
1246
|
+
...zoneName && plan.deployment.application.baseDomain !== zoneName ? [{
|
|
1247
|
+
key: "ssl_and_certificates",
|
|
1248
|
+
type: "read"
|
|
1249
|
+
}] : []
|
|
1250
|
+
];
|
|
1251
|
+
return {
|
|
1252
|
+
title: "Cloudflare management API token",
|
|
1253
|
+
linkInTitle: true,
|
|
1254
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1255
|
+
to: "/" + plan.account.id + "/api-tokens",
|
|
1256
|
+
name: plan.deployment.displayName + " Management",
|
|
1257
|
+
permissionGroupKeys: JSON.stringify(permissions)
|
|
1258
|
+
}),
|
|
1259
|
+
instructions: [
|
|
1260
|
+
"This token lets the Void CLI install your platform. Void will now open a pre-configured token creation site. Please review the permissions and then paste the token here.",
|
|
1261
|
+
`Scope it to ${plan.account.name}${createsZone ? "; allow zone creation." : zoneName ? ` and zone ${zoneName}.` : "."}`,
|
|
1262
|
+
...apiHostname && apiHostname !== zoneName && !apiHostname.endsWith(`.${zoneName}`) ? [`Also include the zone for ${apiHostname}.`] : []
|
|
1263
|
+
].join("\n")
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function platformSetupPages(plan) {
|
|
1267
|
+
const permissions = [
|
|
1268
|
+
...platformAccountTokenPermissions(plan.actions.some((action) => action.kind === "ai-gateway" && action.action === "inspect")),
|
|
1269
|
+
...plan.deployment.application.zoneName ? [{
|
|
1270
|
+
key: "cache",
|
|
1271
|
+
type: "purge"
|
|
1272
|
+
}] : [],
|
|
1273
|
+
...plan.deployment.capabilities.customDomains ? [{
|
|
1274
|
+
key: "ssl_and_certificates",
|
|
1275
|
+
type: "edit"
|
|
1276
|
+
}] : [],
|
|
1277
|
+
...plan.deployment.capabilities.email ? [
|
|
1278
|
+
{
|
|
1279
|
+
key: "zone",
|
|
1280
|
+
type: "read"
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
key: "zone_settings",
|
|
1284
|
+
type: "edit"
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
key: "email_routing_address",
|
|
1288
|
+
type: "edit"
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
key: "email_routing_rule",
|
|
1292
|
+
type: "edit"
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
key: "email_sending",
|
|
1296
|
+
type: "edit"
|
|
1297
|
+
}
|
|
1298
|
+
] : []
|
|
1299
|
+
];
|
|
1300
|
+
return {
|
|
1301
|
+
runtime: {
|
|
1302
|
+
title: "Cloudflare runtime token",
|
|
1303
|
+
linkInTitle: true,
|
|
1304
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1305
|
+
to: "/" + plan.account.id + "/api-tokens",
|
|
1306
|
+
name: plan.deployment.displayName + " Runtime",
|
|
1307
|
+
permissionGroupKeys: JSON.stringify(permissions)
|
|
1308
|
+
}),
|
|
1309
|
+
instructions: "Create a Cloudflare runtime token that Void can use to manage the platform. Please review the permissions and then paste the token here."
|
|
1310
|
+
},
|
|
1311
|
+
github: {
|
|
1312
|
+
title: "GitHub OAuth App",
|
|
1313
|
+
url: "https://github.com/settings/applications/new",
|
|
1314
|
+
instructions: "GitHub Auth will be used by the users of your Void Platform instance. Register a GitHub app and generate a client secret.\nHomepage: " + plan.apiUrl + "\nRedirect URI: " + plan.apiUrl + "/auth/callback"
|
|
1315
|
+
},
|
|
1316
|
+
r2: {
|
|
1317
|
+
title: "R2 upload token",
|
|
1318
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1319
|
+
to: "/" + plan.account.id + "/r2/api-tokens/create",
|
|
1320
|
+
type: "account"
|
|
1321
|
+
}),
|
|
1322
|
+
instructions: [
|
|
1323
|
+
"Select Object Read & Write.",
|
|
1324
|
+
"Keep Apply to all buckets in this account selected.",
|
|
1325
|
+
"Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
|
|
1326
|
+
].join("\n")
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function formatPlatformSetupPlan(plan, options = {}) {
|
|
1331
|
+
const writesDns = plan.actions.some((action) => action.kind === "zone" && action.action === "create" || action.kind === "dns-record" && action.action !== "reuse");
|
|
1332
|
+
return [
|
|
1333
|
+
"The installation will guide you through setting up the following credentials:",
|
|
1334
|
+
"",
|
|
1335
|
+
...[
|
|
1336
|
+
...options.managementToken ? [options.managementAccess ? `Cloudflare management API token for ${writesDns ? "DNS changes and " : ""}Access setup` : "Cloudflare management API token for DNS changes"] : [],
|
|
1337
|
+
"PostgreSQL database URL",
|
|
1338
|
+
"Cloudflare runtime token",
|
|
1339
|
+
"R2 upload token",
|
|
1340
|
+
...plan.deployment.authentication.providers.includes("github") ? ["GitHub OAuth App client ID and secret"] : [],
|
|
1341
|
+
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => (provider === "google" ? "Google OAuth" : provider === "oidc" ? "OIDC" : "Cloudflare Access OIDC") + " client ID and secret"),
|
|
1342
|
+
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access application and installer credentials"] : [],
|
|
1343
|
+
...plan.deployment.appAccess ? ["Cloudflare Access check and app-management token when needed (stored encrypted)"] : [],
|
|
1344
|
+
"JWT signing key and project encryption key"
|
|
1345
|
+
].map((credential) => `• ${credential}`)
|
|
1346
|
+
].join("\n");
|
|
1347
|
+
}
|
|
1348
|
+
function platformInstallCommandArguments(options) {
|
|
1349
|
+
const parts = [
|
|
1350
|
+
options.commandName,
|
|
1351
|
+
"platform",
|
|
1352
|
+
"install"
|
|
1353
|
+
];
|
|
1354
|
+
const flag = (name, value) => {
|
|
1355
|
+
if (value) parts.push(name, value);
|
|
1356
|
+
};
|
|
1357
|
+
if (options.resumeId) {
|
|
1358
|
+
parts.push("--resume");
|
|
1359
|
+
flag("--name", options.resumeId);
|
|
1360
|
+
} else {
|
|
1361
|
+
flag("--name", options.installationSlug);
|
|
1362
|
+
flag("--account", options.accountId);
|
|
1363
|
+
flag("--display-name", options.displayName);
|
|
1364
|
+
if (options.workersDev) parts.push("--workers-dev");
|
|
1365
|
+
else {
|
|
1366
|
+
flag("--application-domain", options.applicationDomain);
|
|
1367
|
+
flag("--zone", options.zoneName);
|
|
1368
|
+
if (options.dedicatedZone) parts.push("--dedicated-zone");
|
|
1369
|
+
else parts.push("--no-dedicated-zone");
|
|
1370
|
+
}
|
|
1371
|
+
flag("--control-plane-domain", options.controlPlaneDomain);
|
|
1372
|
+
if (!options.controlPlaneDomain) parts.push("--no-control-plane-domain");
|
|
1373
|
+
}
|
|
1374
|
+
flag("--hyperdrive", options.hyperdrive ?? "");
|
|
1375
|
+
flag("--auth-config", options.authConfig ?? "");
|
|
1376
|
+
if (!options.resumeId && options.appAccess) flag("--app-access", options.appAccess);
|
|
1377
|
+
if (!options.authConfig && !options.resumeId && options.authentication) {
|
|
1378
|
+
flag("--login-methods", options.authentication.providers.join(","));
|
|
1379
|
+
parts.push(options.authentication.accessProtection ? "--access-protection" : "--no-access-protection");
|
|
1380
|
+
}
|
|
1381
|
+
flag("--dashboard-url", options.dashboardUrl ?? "");
|
|
1382
|
+
flag("--runtime", options.runtime);
|
|
1383
|
+
if (options.yes) parts.push("--yes");
|
|
1384
|
+
return parts;
|
|
1385
|
+
}
|
|
1386
|
+
function formatPlatformInstallCommand(options) {
|
|
1387
|
+
return platformInstallCommandArguments(options).map((value) => {
|
|
1388
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
1389
|
+
return process.platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
1390
|
+
}).join(" ");
|
|
1391
|
+
}
|
|
1392
|
+
function openPlatformSetupPage(page, dependencies = {}) {
|
|
1393
|
+
log.info((page.linkInTitle ? platformSetupLink(page) : page.title) + "\n" + page.instructions);
|
|
1394
|
+
if (!page.linkInTitle) console.log(platformSetupLink({
|
|
1395
|
+
...page,
|
|
1396
|
+
title: "Open " + page.title
|
|
1397
|
+
}));
|
|
1398
|
+
if (dependencies.interactive ?? process.stdin.isTTY === true) try {
|
|
1399
|
+
(dependencies.open ?? ((url) => openBrowser(url, execFile)))(page.url);
|
|
1400
|
+
} catch {
|
|
1401
|
+
log.info("Could not open the browser. Use the link above.");
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
//#endregion
|
|
1127
1405
|
//#region src/cli/platform-access-setup.ts
|
|
1406
|
+
function accessSetupGuide() {
|
|
1407
|
+
return platformSetupLink({
|
|
1408
|
+
title: "Setup guide and required permissions",
|
|
1409
|
+
url: "https://void.cloud/guide/platform/installation/setup#cloudflare-access"
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
async function selectCompanyAccessPolicies(client, initialPolicies, accountId, mode) {
|
|
1413
|
+
const create = "__create__";
|
|
1414
|
+
const refresh = "__refresh__";
|
|
1415
|
+
let policies = initialPolicies;
|
|
1416
|
+
let selectedIds = [];
|
|
1417
|
+
while (true) {
|
|
1418
|
+
const options = policies.filter((policy) => typeof policy.id === "string" && ["allow", "deny"].includes(String(policy.decision))).map((policy) => ({
|
|
1419
|
+
value: String(policy.id),
|
|
1420
|
+
label: stripVTControlCharacters(String(policy.name ?? policy.id))
|
|
1421
|
+
}));
|
|
1422
|
+
const available = new Set(options.map((option) => option.value));
|
|
1423
|
+
selectedIds = selectedIds.filter((id) => available.has(id));
|
|
1424
|
+
const actions = [{
|
|
1425
|
+
value: create,
|
|
1426
|
+
label: "Create new",
|
|
1427
|
+
hint: "Set up a policy in Cloudflare, then return here"
|
|
1428
|
+
}, {
|
|
1429
|
+
value: refresh,
|
|
1430
|
+
label: "Refresh list"
|
|
1431
|
+
}];
|
|
1432
|
+
let choices;
|
|
1433
|
+
if (options.length === 0) {
|
|
1434
|
+
log.info("No reusable Allow or Deny policies were found in this account. Create an Allow policy for your company, or refresh after saving one.");
|
|
1435
|
+
choices = [answer$2(await select({
|
|
1436
|
+
message: "Company Access policies:",
|
|
1437
|
+
options: actions
|
|
1438
|
+
}))];
|
|
1439
|
+
} else choices = answer$2(await multiselect({
|
|
1440
|
+
message: "Company Access policies (select all that apply):",
|
|
1441
|
+
options: [...options, ...actions],
|
|
1442
|
+
initialValues: selectedIds,
|
|
1443
|
+
required: true
|
|
1444
|
+
}));
|
|
1445
|
+
selectedIds = choices.filter((id) => available.has(id));
|
|
1446
|
+
if (choices.includes(create)) {
|
|
1447
|
+
note([
|
|
1448
|
+
"1. Open Zero Trust → Access controls → Policies → Add a policy in this account.",
|
|
1449
|
+
"2. Name the policy and choose Allow.",
|
|
1450
|
+
"3. Include your company’s identity-provider group or Emails ending in your company domain, such as example.com.",
|
|
1451
|
+
"4. Require your selected company login method and your company’s MFA/device checks.",
|
|
1452
|
+
...mode === "existing" ? ["5. Save the policy and attach it to the existing Access applications you’re connecting, then return here."] : ["5. Save the policy, then return here."]
|
|
1453
|
+
].join("\n"), import_picocolors.default.bold("Create an Access policy"));
|
|
1454
|
+
console.log(platformSetupLink({
|
|
1455
|
+
title: "Open Cloudflare account",
|
|
1456
|
+
url: `https://dash.cloudflare.com/${encodeURIComponent(accountId)}`
|
|
1457
|
+
}));
|
|
1458
|
+
if (!answer$2(await confirm({
|
|
1459
|
+
message: "Have you saved the new policy?",
|
|
1460
|
+
initialValue: true
|
|
1461
|
+
}))) continue;
|
|
1462
|
+
} else if (!choices.includes(refresh)) return selectedIds;
|
|
1463
|
+
policies = await client.list("policies");
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1128
1466
|
function platformAccessResourceInventory(encoded, installationId) {
|
|
1129
1467
|
if (!encoded) return [];
|
|
1130
1468
|
const state = JSON.parse(encoded);
|
|
@@ -1149,6 +1487,10 @@ function checkpointPlatformAccessResources(manifest, encoded, updatedAt) {
|
|
|
1149
1487
|
}
|
|
1150
1488
|
function isPlatformAuthenticationPreparationKey(name) {
|
|
1151
1489
|
return name.startsWith("authenticationConnection:") || [
|
|
1490
|
+
"appAccessToken",
|
|
1491
|
+
"appAccessTokenRejected",
|
|
1492
|
+
"appAccessIdentityProviderIds",
|
|
1493
|
+
"appAccessPolicyIds",
|
|
1152
1494
|
"accessManagementToken",
|
|
1153
1495
|
"accessSetupSpecJson",
|
|
1154
1496
|
"accessExistingServiceTokenJson",
|
|
@@ -1194,7 +1536,7 @@ function parsePlatformAccessSetupSpec(input) {
|
|
|
1194
1536
|
}
|
|
1195
1537
|
return value;
|
|
1196
1538
|
}
|
|
1197
|
-
function answer$
|
|
1539
|
+
function answer$2(value) {
|
|
1198
1540
|
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
1199
1541
|
return value;
|
|
1200
1542
|
}
|
|
@@ -1222,22 +1564,32 @@ function activatePlatformAccessCredentials(encoded, origins) {
|
|
|
1222
1564
|
}
|
|
1223
1565
|
async function preparePlatformAccessSetup(input) {
|
|
1224
1566
|
if (!input.login && !input.protection) return;
|
|
1567
|
+
if (process.stdin.isTTY) {
|
|
1568
|
+
note([
|
|
1569
|
+
input.login && input.protection ? "Access lets employees sign in with your company login and controls who can reach the platform’s UI and APIs." : input.login ? "Access lets employees sign in to the platform with your company login." : "Access controls who can reach the platform’s UI and APIs.",
|
|
1570
|
+
"",
|
|
1571
|
+
"Void can create dedicated Access applications using your company’s existing login providers and policies, or use Access applications your company already manages."
|
|
1572
|
+
].join("\n"), import_picocolors.default.bold("Set up Cloudflare Access"));
|
|
1573
|
+
console.log(accessSetupGuide());
|
|
1574
|
+
}
|
|
1225
1575
|
let spec = input.spec ?? (input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : void 0);
|
|
1226
1576
|
if (!spec) {
|
|
1227
1577
|
if (!process.stdin.isTTY) {
|
|
1228
1578
|
if (input.protection) throw new Error("platform: Protection requires cloudflareAccess create or existing application verification in --auth-config.");
|
|
1229
1579
|
return;
|
|
1230
1580
|
}
|
|
1231
|
-
const mode = answer$
|
|
1581
|
+
const mode = answer$2(await select({
|
|
1232
1582
|
message: "Cloudflare Access setup:",
|
|
1233
1583
|
options: [
|
|
1234
1584
|
{
|
|
1235
1585
|
value: "create",
|
|
1236
|
-
label: "Create applications
|
|
1586
|
+
label: "Create Access applications for this platform",
|
|
1587
|
+
hint: "Use this account’s existing Zero Trust setup"
|
|
1237
1588
|
},
|
|
1238
1589
|
{
|
|
1239
1590
|
value: "existing",
|
|
1240
|
-
label: "
|
|
1591
|
+
label: "Use Access applications your company already manages",
|
|
1592
|
+
hint: "May use another account"
|
|
1241
1593
|
},
|
|
1242
1594
|
...!input.protection ? [{
|
|
1243
1595
|
value: "manual",
|
|
@@ -1248,12 +1600,19 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1248
1600
|
if (mode === "manual") return;
|
|
1249
1601
|
spec = { mode };
|
|
1250
1602
|
if (mode === "existing") {
|
|
1251
|
-
|
|
1603
|
+
log.info("Enter the account that owns your company’s Access configuration. This may differ from the account hosting Void.");
|
|
1604
|
+
spec.accountId = answer$2(await text$1({
|
|
1252
1605
|
message: "Cloudflare identity account ID:",
|
|
1253
1606
|
initialValue: input.plan.account.id
|
|
1254
1607
|
}));
|
|
1255
|
-
if (input.login)
|
|
1256
|
-
|
|
1608
|
+
if (input.login) {
|
|
1609
|
+
log.info("Enter the ID of the Access application that provides company sign-in to Void.");
|
|
1610
|
+
spec.loginApplicationId = answer$2(await text$1({ message: "Existing Access OIDC application ID:" }));
|
|
1611
|
+
}
|
|
1612
|
+
if (input.protection) {
|
|
1613
|
+
log.info("Enter the ID of the Access application that protects the platform’s UI and APIs.");
|
|
1614
|
+
spec.protectionApplicationId = answer$2(await text$1({ message: "Existing Access protection application ID:" }));
|
|
1615
|
+
}
|
|
1257
1616
|
}
|
|
1258
1617
|
}
|
|
1259
1618
|
spec = parsePlatformAccessSetupSpec(spec);
|
|
@@ -1270,8 +1629,9 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1270
1629
|
policies = await client.list("policies");
|
|
1271
1630
|
} catch (error) {
|
|
1272
1631
|
if (!(error instanceof PlatformCloudflareApiError) || ![401, 403].includes(error.status) || !process.stdin.isTTY || credential) throw error;
|
|
1273
|
-
|
|
1274
|
-
|
|
1632
|
+
log.info(`Your current token lacks the required Access permissions for account ${accountId}. Create a token for that account using the permissions in the setup guide.`);
|
|
1633
|
+
console.log(accessSetupGuide());
|
|
1634
|
+
const token = answer$2(await password({ message: "Access setup API token:" }));
|
|
1275
1635
|
input.save({ accessManagementToken: token });
|
|
1276
1636
|
management = new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl });
|
|
1277
1637
|
client = new PlatformAccessClient(management, accountId);
|
|
@@ -1279,7 +1639,8 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1279
1639
|
providers = await client.list("identity_providers");
|
|
1280
1640
|
policies = await client.list("policies");
|
|
1281
1641
|
}
|
|
1282
|
-
|
|
1642
|
+
if (!spec.identityProviderIds && process.stdin.isTTY) log.info("Choose how employees sign in—for example, your company’s Okta or Google Workspace connection.");
|
|
1643
|
+
const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ? answer$2(await multiselect({
|
|
1283
1644
|
message: "Company identity providers:",
|
|
1284
1645
|
required: true,
|
|
1285
1646
|
options: providers.filter((provider) => typeof provider.id === "string").map(platformAccessIdentityProvider).map((provider) => ({
|
|
@@ -1287,14 +1648,15 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1287
1648
|
label: stripVTControlCharacters(provider.name)
|
|
1288
1649
|
}))
|
|
1289
1650
|
})) : []);
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1651
|
+
if (identityProviderIds.length) {
|
|
1652
|
+
spec = {
|
|
1653
|
+
...spec,
|
|
1654
|
+
identityProviderIds
|
|
1655
|
+
};
|
|
1656
|
+
input.save({ accessSetupSpecJson: JSON.stringify(spec) });
|
|
1657
|
+
}
|
|
1658
|
+
if (!spec.policyIds && process.stdin.isTTY) log.info("Choose which existing access rules apply to this platform, including MFA and device requirements.");
|
|
1659
|
+
const policyIds = spec.policyIds ?? (process.stdin.isTTY ? await selectCompanyAccessPolicies(client, policies, accountId, spec.mode) : []);
|
|
1298
1660
|
if (!identityProviderIds.length || !policyIds.length) throw new Error("platform: Access setup requires explicit identityProviderIds and policyIds.");
|
|
1299
1661
|
spec = {
|
|
1300
1662
|
...spec,
|
|
@@ -1311,10 +1673,11 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1311
1673
|
if (!serviceToken && spec.mode === "existing" && input.protection) {
|
|
1312
1674
|
if (input.checkpoint.accessExistingServiceTokenJson) serviceToken = JSON.parse(input.checkpoint.accessExistingServiceTokenJson);
|
|
1313
1675
|
else if (process.stdin.isTTY) {
|
|
1676
|
+
log.info("This lets the installer reach the protected platform during setup. Use a service token allowed by the existing Access application.");
|
|
1314
1677
|
serviceToken = {
|
|
1315
|
-
id: answer$
|
|
1316
|
-
clientId: answer$
|
|
1317
|
-
clientSecret: answer$
|
|
1678
|
+
id: answer$2(await text$1({ message: "Existing admitted Access service token ID:" })),
|
|
1679
|
+
clientId: answer$2(await password({ message: "Access service token client ID:" })),
|
|
1680
|
+
clientSecret: answer$2(await password({ message: "Access service token client secret:" }))
|
|
1318
1681
|
};
|
|
1319
1682
|
input.save({ accessExistingServiceTokenJson: JSON.stringify(serviceToken) });
|
|
1320
1683
|
}
|
|
@@ -1335,7 +1698,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1335
1698
|
};
|
|
1336
1699
|
console.log(JSON.stringify({ accessPlan: reviewed }, null, 2));
|
|
1337
1700
|
if (!input.yes) {
|
|
1338
|
-
if (!process.stdin.isTTY || !answer$
|
|
1701
|
+
if (!process.stdin.isTTY || !answer$2(await confirm({
|
|
1339
1702
|
message: "Apply this Access configuration?",
|
|
1340
1703
|
initialValue: false
|
|
1341
1704
|
}))) throw new Error("platform: Access setup changes require confirmation or --yes.");
|
|
@@ -1349,7 +1712,8 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1349
1712
|
};
|
|
1350
1713
|
let loginClientSecret = spec.loginClientSecretEnv ? secretReference(spec.loginClientSecretEnv) : input.checkpoint.accessExistingLoginSecret;
|
|
1351
1714
|
if (input.login && spec.mode === "existing" && !loginClientSecret && process.stdin.isTTY) {
|
|
1352
|
-
|
|
1715
|
+
log.info("Enter the client secret from the existing Access login application’s OIDC settings.");
|
|
1716
|
+
loginClientSecret = answer$2(await password({ message: "Existing Access OIDC client secret:" }));
|
|
1353
1717
|
input.save({ accessExistingLoginSecret: loginClientSecret });
|
|
1354
1718
|
}
|
|
1355
1719
|
const result = await provisionPlatformAccess(client, {
|
|
@@ -1381,13 +1745,34 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1381
1745
|
}
|
|
1382
1746
|
//#endregion
|
|
1383
1747
|
//#region src/cli/platform-auth.ts
|
|
1384
|
-
|
|
1385
|
-
|
|
1748
|
+
/** Check Access inventory permissions before the installer starts provisioning. */
|
|
1749
|
+
async function verifyPlatformManagementAccess(client, accountId, access) {
|
|
1750
|
+
const base = `/accounts/${encodeURIComponent(accountId)}/access`;
|
|
1751
|
+
await Promise.all([client.request(`${base}/organizations`), ...[
|
|
1752
|
+
"identity_providers",
|
|
1753
|
+
"groups",
|
|
1754
|
+
"policies",
|
|
1755
|
+
"apps",
|
|
1756
|
+
...access.protection ? ["service_tokens"] : []
|
|
1757
|
+
].map((resource) => client.request(`${base}/${resource}?per_page=1`))]);
|
|
1758
|
+
}
|
|
1759
|
+
function assertPlatformOperatorCredentialCanApplyPlan(credential, plan, access) {
|
|
1760
|
+
if (!platformOperatorNeedsManagementToken(credential, plan, access)) return;
|
|
1386
1761
|
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1387
1762
|
const writesDns = plan.actions.some((action) => action.kind === "dns-record" && action.action !== "reuse");
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1763
|
+
const permissions = [
|
|
1764
|
+
...createsZone ? ["Account → Zone → Edit"] : [],
|
|
1765
|
+
...writesDns ? ["Zone → DNS → Edit"] : [],
|
|
1766
|
+
...access ? [
|
|
1767
|
+
`Account → Access: Apps and Policies → ${access.readOnly ? "Read" : "Edit"}`,
|
|
1768
|
+
"Account → Access: Organizations, Identity Providers, and Groups → Read",
|
|
1769
|
+
...access.protection ? [`Account → Access: Service Tokens → ${access.readOnly ? "Read" : "Edit"}`] : []
|
|
1770
|
+
] : []
|
|
1771
|
+
];
|
|
1772
|
+
throw new Error(`platform: Cloudflare browser login cannot authorize the planned ${access ? "Access setup" : "zone or DNS changes"}. Set CLOUDFLARE_API_TOKEN with ${permissions.join(" and ")}, scoped to the selected account${createsZone || writesDns ? " and application zone" : ""}, then rerun the command.`);
|
|
1773
|
+
}
|
|
1774
|
+
function platformOperatorNeedsManagementToken(credential, plan, access) {
|
|
1775
|
+
return credential.source === "oauth" && (!!access || plan.actions.some((action) => action.kind === "zone" && action.action === "create" || action.kind === "dns-record" && action.action !== "reuse"));
|
|
1391
1776
|
}
|
|
1392
1777
|
async function resolvePlatformCloudflareCredential(dependencies = {}) {
|
|
1393
1778
|
const { credential } = await resolvePlatformCloudflareCredentialWithInput(async () => void 0, dependencies);
|
|
@@ -1443,6 +1828,11 @@ async function readExistingCredential(dependencies, runWrangler) {
|
|
|
1443
1828
|
source: "environment",
|
|
1444
1829
|
token: environmentToken
|
|
1445
1830
|
};
|
|
1831
|
+
const savedManagementToken = dependencies.savedManagementToken?.trim();
|
|
1832
|
+
if (savedManagementToken) return {
|
|
1833
|
+
source: "saved",
|
|
1834
|
+
token: savedManagementToken
|
|
1835
|
+
};
|
|
1446
1836
|
if (!(dependencies.interactive ?? process.stdin.isTTY === true)) throw new Error("platform: Cloudflare authentication is required. Run `void cloudflare login` interactively, or set CLOUDFLARE_API_TOKEN in CI.");
|
|
1447
1837
|
return await readWranglerOAuthCredential(runWrangler);
|
|
1448
1838
|
}
|
|
@@ -1461,6 +1851,116 @@ async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflare
|
|
|
1461
1851
|
}
|
|
1462
1852
|
}
|
|
1463
1853
|
//#endregion
|
|
1854
|
+
//#region src/cli/platform-access-health.ts
|
|
1855
|
+
/** These reserved endpoints require Void's probe token and cannot serve application traffic. */
|
|
1856
|
+
function platformAccessHealthTargets(plan) {
|
|
1857
|
+
const destination = `${plan.deployment.application.zoneName ? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${plan.deployment.application.baseDomain}` : `${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev`}/health`;
|
|
1858
|
+
return [{
|
|
1859
|
+
key: `health-dispatch-${createHash("sha256").update(destination).digest("hex").slice(0, 12)}`,
|
|
1860
|
+
destination
|
|
1861
|
+
}];
|
|
1862
|
+
}
|
|
1863
|
+
async function verifyHealthException(access, app, destination) {
|
|
1864
|
+
const destinations = platformAccessPublicDestinations(app);
|
|
1865
|
+
if (typeof app.id !== "string" || !app.id || app.type !== "self_hosted" || app.domain !== destination || !destinations.length || destinations.some((value) => value !== destination) || app.destinations !== void 0 && (!Array.isArray(app.destinations) || app.destinations.some((value) => !value || value.type !== "public" || value.uri !== destination)) || app.target_criteria != null && (!Array.isArray(app.target_criteria) || app.target_criteria.length > 0)) throw new Error(`platform: The Access health exception must cover only ${destination}.`);
|
|
1866
|
+
const policies = await access.list(`apps/${encodeURIComponent(app.id)}/policies`);
|
|
1867
|
+
const policy = policies[0];
|
|
1868
|
+
if (policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== "[{\"everyone\":{}}]" || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) throw new Error(`platform: The Access policy for ${destination} does not match the reserved health-check exception.`);
|
|
1869
|
+
}
|
|
1870
|
+
/** Keep the same path-only exception used by project Zero Trust available during bootstrap. */
|
|
1871
|
+
async function ensurePlatformAccessHealthChecks(input) {
|
|
1872
|
+
const appAccess = !input.manifest.steps.some((step) => step.key === "app-access-initialized:v1") && input.checkpoint.appAccessSetupJson ? parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson)) : null;
|
|
1873
|
+
if (appAccess && appAccess.accountId !== input.plan.account.id) throw new Error("platform: App access recovery belongs to another Cloudflare account.");
|
|
1874
|
+
if (!input.plan.deployment.authentication.accessProtection && !appAccess?.managed) return;
|
|
1875
|
+
const spec = appAccess?.managed ? {
|
|
1876
|
+
mode: "create",
|
|
1877
|
+
accountId: input.plan.account.id
|
|
1878
|
+
} : parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson || "null"));
|
|
1879
|
+
const accountId = input.plan.account.id;
|
|
1880
|
+
const identityAccountId = spec.accountId ?? accountId;
|
|
1881
|
+
const token = appAccess?.managed ? appAccess.apiToken : identityAccountId === accountId ? process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN || input.checkpoint.accessManagementToken : void 0;
|
|
1882
|
+
if (!token && !input.client.request) throw new Error("platform: Access management is required to configure the reserved health endpoints.");
|
|
1883
|
+
const access = new PlatformAccessClient(token ? new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }) : { request: input.client.request.bind(input.client) }, accountId);
|
|
1884
|
+
const nonce = createHash("sha256").update(`void-access-health:${input.manifest.ownershipNonce}`).digest("hex").slice(0, 32);
|
|
1885
|
+
const state = input.checkpoint.accessHealthProvisioningJson ? JSON.parse(input.checkpoint.accessHealthProvisioningJson) : {
|
|
1886
|
+
version: 1,
|
|
1887
|
+
accountId,
|
|
1888
|
+
installationId: input.manifest.id,
|
|
1889
|
+
nonce,
|
|
1890
|
+
resources: {}
|
|
1891
|
+
};
|
|
1892
|
+
if (!state || state.version !== 1 || state.accountId !== accountId || input.manifest.account.id !== accountId || state.installationId !== input.manifest.id || state.installationId !== input.plan.installationId || state.nonce !== nonce || !state.resources || typeof state.resources !== "object" || Array.isArray(state.resources)) throw new Error("platform: Access health recovery data belongs to a different installation.");
|
|
1893
|
+
const save = () => input.save(JSON.stringify(state));
|
|
1894
|
+
const inventory = await access.list("apps");
|
|
1895
|
+
for (const { key, destination } of platformAccessHealthTargets(input.plan)) {
|
|
1896
|
+
const expected = {
|
|
1897
|
+
type: "self_hosted",
|
|
1898
|
+
domain: destination,
|
|
1899
|
+
destinations: [{
|
|
1900
|
+
type: "public",
|
|
1901
|
+
uri: destination
|
|
1902
|
+
}],
|
|
1903
|
+
policies: [{
|
|
1904
|
+
name: "Void platform health check",
|
|
1905
|
+
decision: "bypass",
|
|
1906
|
+
precedence: 1,
|
|
1907
|
+
include: [{ everyone: {} }]
|
|
1908
|
+
}]
|
|
1909
|
+
};
|
|
1910
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(expected)).digest("hex");
|
|
1911
|
+
let recorded = state.resources[key];
|
|
1912
|
+
const name = `void-${input.manifest.id}-${key}`.slice(0, 90) + "-" + nonce.slice(0, 16);
|
|
1913
|
+
if (recorded && (recorded.name !== name || recorded.fingerprint !== fingerprint || recorded.adopted)) throw new Error("platform: The recorded Access health exception changed identity or scope.");
|
|
1914
|
+
const matching = inventory.filter((app) => platformAccessPublicDestinations(app).includes(destination));
|
|
1915
|
+
if (matching.length > 1 || recorded?.id && matching.some((app) => app.id !== recorded.id)) throw new Error(`platform: Multiple Access applications cover ${destination}. Reconcile them before resuming.`);
|
|
1916
|
+
if (!recorded && matching.length === 1) {
|
|
1917
|
+
await verifyHealthException(access, matching[0], destination);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (!recorded?.id && recorded?.pending) {
|
|
1921
|
+
const candidates = inventory.filter((app) => app.name === name);
|
|
1922
|
+
if (candidates.length !== 1 || typeof candidates[0]?.id !== "string") throw new Error("platform: Access health application creation has an uncertain outcome. Inspect Cloudflare before retrying; no duplicate was created.");
|
|
1923
|
+
recorded.id = candidates[0].id;
|
|
1924
|
+
save();
|
|
1925
|
+
}
|
|
1926
|
+
if (!recorded?.id) {
|
|
1927
|
+
if (spec.mode !== "create") throw new Error(`platform: Configure an Access exception limited to ${destination}, then resume. Void's private probe token still protects this endpoint.`);
|
|
1928
|
+
if (inventory.some((app) => app.name === name)) throw new Error("platform: An Access health application with this installation identity already exists. Inspect it before resuming.");
|
|
1929
|
+
recorded = {
|
|
1930
|
+
name,
|
|
1931
|
+
fingerprint,
|
|
1932
|
+
pending: true
|
|
1933
|
+
};
|
|
1934
|
+
state.resources[key] = recorded;
|
|
1935
|
+
save();
|
|
1936
|
+
try {
|
|
1937
|
+
const created = await access.create("apps", {
|
|
1938
|
+
...expected,
|
|
1939
|
+
name
|
|
1940
|
+
});
|
|
1941
|
+
if (!created || typeof created.id !== "string" || !created.id) throw new Error("platform: Cloudflare did not return the Access health application ID.");
|
|
1942
|
+
recorded.id = created.id;
|
|
1943
|
+
save();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (error instanceof PlatformCloudflareApiError && error.status >= 400 && error.status < 500 && ![
|
|
1946
|
+
408,
|
|
1947
|
+
409,
|
|
1948
|
+
429
|
|
1949
|
+
].includes(error.status)) {
|
|
1950
|
+
recorded.pending = false;
|
|
1951
|
+
save();
|
|
1952
|
+
}
|
|
1953
|
+
throw error;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
const app = await access.get(`apps/${encodeURIComponent(recorded.id)}`);
|
|
1957
|
+
if (!app || app.id !== recorded.id || app.name !== recorded.name) throw new Error("platform: The Access health application changed ownership identity.");
|
|
1958
|
+
await verifyHealthException(access, app, destination);
|
|
1959
|
+
recorded.pending = false;
|
|
1960
|
+
save();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
//#endregion
|
|
1464
1964
|
//#region src/cli/platform-access-preflight.ts
|
|
1465
1965
|
/** Recheck live protection without invoking any provisioning or configuration write. */
|
|
1466
1966
|
async function verifyInstalledPlatformAccessProtection(input) {
|
|
@@ -1507,7 +2007,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1507
2007
|
}
|
|
1508
2008
|
const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
|
|
1509
2009
|
if (existingWorkers.length) {
|
|
1510
|
-
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-
|
|
2010
|
+
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-BLadnnct.mjs");
|
|
1511
2011
|
await verifyProtectionWorkerOrigins({
|
|
1512
2012
|
request: input.client.request.bind(input.client),
|
|
1513
2013
|
listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
|
|
@@ -1516,6 +2016,241 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1516
2016
|
}
|
|
1517
2017
|
}
|
|
1518
2018
|
//#endregion
|
|
2019
|
+
//#region src/cli/platform-app-access.ts
|
|
2020
|
+
function answer$1(value) {
|
|
2021
|
+
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
2022
|
+
return value;
|
|
2023
|
+
}
|
|
2024
|
+
async function selectInstallationAppAccess(authentication, recorded, choice) {
|
|
2025
|
+
const config = authentication.file?.appAccess;
|
|
2026
|
+
if (config && (typeof config !== "object" || Array.isArray(config) || "apiToken" in config || config.tokenEnv !== void 0 && (typeof config.tokenEnv !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(config.tokenEnv)))) throw new Error("platform: App access configuration must use tokenEnv; plaintext tokens are not accepted.");
|
|
2027
|
+
if (choice && config?.mode && choice !== config.mode) throw new Error("platform: --app-access conflicts with the authentication file’s app access choice.");
|
|
2028
|
+
const supplied = choice ?? config?.mode;
|
|
2029
|
+
if (supplied !== void 0 && ![
|
|
2030
|
+
"public",
|
|
2031
|
+
"platform",
|
|
2032
|
+
"custom"
|
|
2033
|
+
].includes(supplied)) throw new Error("platform: App access must be public, platform, or custom.");
|
|
2034
|
+
if (recorded) {
|
|
2035
|
+
if (supplied !== void 0 && supplied !== recorded.appAccess) throw new Error("platform: Resume with the saved app access choice. Manage an installed platform’s app access in Admin → Zero Trust.");
|
|
2036
|
+
return recorded.appAccess;
|
|
2037
|
+
}
|
|
2038
|
+
const protectedPlatform = !!authentication.authentication.accessProtection;
|
|
2039
|
+
const selected = supplied ?? (!process.stdin.isTTY ? protectedPlatform ? "platform" : "public" : answer$1(await select({
|
|
2040
|
+
message: "Who should be able to access deployed apps?",
|
|
2041
|
+
initialValue: protectedPlatform ? "platform" : "public",
|
|
2042
|
+
options: [
|
|
2043
|
+
...protectedPlatform ? [{
|
|
2044
|
+
value: "platform",
|
|
2045
|
+
label: "Same people allowed by the platform’s Access policy",
|
|
2046
|
+
hint: "recommended"
|
|
2047
|
+
}] : [],
|
|
2048
|
+
{
|
|
2049
|
+
value: "public",
|
|
2050
|
+
label: "Anyone — public apps"
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
value: "custom",
|
|
2054
|
+
label: protectedPlatform ? "Choose a different Access policy" : "Set up Cloudflare Access protection"
|
|
2055
|
+
}
|
|
2056
|
+
]
|
|
2057
|
+
})));
|
|
2058
|
+
if (selected === "public" && (config?.identityProviderIds?.length || config?.policyIds?.length)) throw new Error("platform: Public apps cannot select identity policies. Choose custom app access to require company login.");
|
|
2059
|
+
if (selected === "platform" && !protectedPlatform) throw new Error("platform: Reusing the platform Access policy requires --access-protection. Choose --app-access custom to protect apps separately.");
|
|
2060
|
+
if (![
|
|
2061
|
+
"public",
|
|
2062
|
+
"platform",
|
|
2063
|
+
"custom"
|
|
2064
|
+
].includes(String(selected))) throw new Error("platform: Choose who can access deployed apps.");
|
|
2065
|
+
return selected;
|
|
2066
|
+
}
|
|
2067
|
+
/** Collect the hosting-account policy once, with explicit consent for a durable management token. */
|
|
2068
|
+
async function prepareInstallationAppAccess(input) {
|
|
2069
|
+
const choice = input.plan.deployment.appAccess;
|
|
2070
|
+
if (!choice) throw new Error("platform: App access was not selected for this installation.");
|
|
2071
|
+
const explicitToken = (input.authentication.file?.appAccess?.tokenEnv ? process.env[input.authentication.file.appAccess.tokenEnv] : void 0) || process.env.VOID_PLATFORM_APP_ACCESS_TOKEN;
|
|
2072
|
+
if (explicitToken && /\s/.test(explicitToken)) throw new Error("platform: App access tokens must not contain whitespace.");
|
|
2073
|
+
if (input.checkpoint.appAccessSetupJson && !input.checkpoint.appAccessTokenRejected) {
|
|
2074
|
+
const saved = parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson));
|
|
2075
|
+
if (saved.accountId !== input.plan.account.id || saved.mode !== (choice === "public" ? "public" : "protected")) throw new Error("platform: Saved app access belongs to another account or choice.");
|
|
2076
|
+
if (saved.managed) {
|
|
2077
|
+
const refreshed = explicitToken ? JSON.stringify({
|
|
2078
|
+
...saved,
|
|
2079
|
+
apiToken: explicitToken
|
|
2080
|
+
}) : input.checkpoint.appAccessSetupJson;
|
|
2081
|
+
input.save({ appAccessSetupJson: refreshed });
|
|
2082
|
+
return refreshed;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const file = input.authentication.file?.appAccess;
|
|
2086
|
+
if (file?.tokenEnv && !/^[A-Z_][A-Z0-9_]*$/.test(file.tokenEnv)) throw new Error("platform: Invalid app access token environment reference.");
|
|
2087
|
+
let token = explicitToken || (input.checkpoint.appAccessTokenRejected ? void 0 : input.checkpoint.appAccessToken);
|
|
2088
|
+
if (input.checkpoint.appAccessTokenRejected) log.info("The previous app access token was rejected. Provide a replacement to continue.");
|
|
2089
|
+
const setupSpec = input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : input.authentication.file?.cloudflareAccess;
|
|
2090
|
+
const setupAccount = setupSpec?.accountId ?? input.plan.account.id;
|
|
2091
|
+
const reusable = setupAccount === input.plan.account.id ? process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN || input.checkpoint.accessManagementToken || input.managementToken : input.managementToken;
|
|
2092
|
+
const tokenPage = () => openPlatformSetupPage({
|
|
2093
|
+
title: "App access API token",
|
|
2094
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
2095
|
+
to: `/${input.plan.account.id}/api-tokens`,
|
|
2096
|
+
name: `${input.plan.deployment.displayName} App Access`,
|
|
2097
|
+
permissionGroupKeys: JSON.stringify([{
|
|
2098
|
+
key: "access",
|
|
2099
|
+
type: "edit"
|
|
2100
|
+
}, {
|
|
2101
|
+
key: "access_acct",
|
|
2102
|
+
type: "read"
|
|
2103
|
+
}])
|
|
2104
|
+
}),
|
|
2105
|
+
instructions: `Scope the token to ${input.plan.account.name}: Access Apps and Policies Edit; Organizations, Identity Providers, and Groups Read. Void stores it encrypted to manage app access.`
|
|
2106
|
+
});
|
|
2107
|
+
const collectToken = async () => {
|
|
2108
|
+
if (!process.stdin.isTTY) throw new Error("platform: App access requires VOID_PLATFORM_APP_ACCESS_TOKEN or appAccess.tokenEnv in --auth-config, scoped to the hosting account.");
|
|
2109
|
+
tokenPage();
|
|
2110
|
+
const value = String(answer$1(await password({ message: "App access API token (stored encrypted when app rules are needed):" }))).trim();
|
|
2111
|
+
if (!value || /\s/.test(value)) throw new Error("platform: An app access token without whitespace is required.");
|
|
2112
|
+
input.save({
|
|
2113
|
+
appAccessToken: value,
|
|
2114
|
+
appAccessTokenRejected: ""
|
|
2115
|
+
});
|
|
2116
|
+
return value;
|
|
2117
|
+
};
|
|
2118
|
+
if (!token && input.checkpoint.appAccessTokenRejected) token = await collectToken();
|
|
2119
|
+
let access = new PlatformAccessClient(token ? new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }) : input.client, input.plan.account.id);
|
|
2120
|
+
let organization;
|
|
2121
|
+
const readOrganization = async () => {
|
|
2122
|
+
try {
|
|
2123
|
+
return await access.get("organizations");
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
if (error instanceof PlatformCloudflareApiError && error.status === 404) return null;
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
try {
|
|
2130
|
+
organization = await readOrganization();
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
if (!(error instanceof PlatformCloudflareApiError) || ![401, 403].includes(error.status)) throw error;
|
|
2133
|
+
log.info("Check this account’s Access requirements before opening app traffic.");
|
|
2134
|
+
token = await collectToken();
|
|
2135
|
+
access = new PlatformAccessClient(new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }), input.plan.account.id);
|
|
2136
|
+
organization = await readOrganization();
|
|
2137
|
+
}
|
|
2138
|
+
const defaultDeny = organization?.deny_unmatched_requests === true;
|
|
2139
|
+
if (choice === "public" && !defaultDeny) {
|
|
2140
|
+
const result = JSON.stringify({
|
|
2141
|
+
version: 1,
|
|
2142
|
+
accountId: input.plan.account.id,
|
|
2143
|
+
mode: "public",
|
|
2144
|
+
managed: false,
|
|
2145
|
+
identityProviderIds: [],
|
|
2146
|
+
policyIds: []
|
|
2147
|
+
});
|
|
2148
|
+
input.save({ appAccessSetupJson: result });
|
|
2149
|
+
return result;
|
|
2150
|
+
}
|
|
2151
|
+
if (!organization) throw new Error(`platform: Enable Zero Trust in account ${input.plan.account.name}, then resume to configure app access.`);
|
|
2152
|
+
if (!token) {
|
|
2153
|
+
log.info("Void needs an Access-management token stored encrypted to manage access for future apps and custom domains.");
|
|
2154
|
+
if (reusable && process.stdin.isTTY && answer$1(await confirm({
|
|
2155
|
+
message: "Reuse the setup token for ongoing app access management?",
|
|
2156
|
+
initialValue: true
|
|
2157
|
+
}))) {
|
|
2158
|
+
token = reusable;
|
|
2159
|
+
input.save({ appAccessToken: token });
|
|
2160
|
+
} else token = await collectToken();
|
|
2161
|
+
access = new PlatformAccessClient(new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }), input.plan.account.id);
|
|
2162
|
+
}
|
|
2163
|
+
let identityProviderIds = [];
|
|
2164
|
+
let policyIds = [];
|
|
2165
|
+
if (choice !== "public") {
|
|
2166
|
+
const reuse = choice === "platform" && setupAccount === input.plan.account.id;
|
|
2167
|
+
if (choice === "platform" && !reuse) log.info("The platform uses Access in another account. Choose equivalent identities and policies in the hosting account for apps.");
|
|
2168
|
+
identityProviderIds = input.checkpoint.appAccessIdentityProviderIds ? JSON.parse(input.checkpoint.appAccessIdentityProviderIds) : (reuse ? setupSpec?.identityProviderIds : void 0) ?? file?.identityProviderIds ?? [];
|
|
2169
|
+
policyIds = input.checkpoint.appAccessPolicyIds ? JSON.parse(input.checkpoint.appAccessPolicyIds) : (reuse ? setupSpec?.policyIds : void 0) ?? file?.policyIds ?? [];
|
|
2170
|
+
if (!identityProviderIds.length) {
|
|
2171
|
+
if (!process.stdin.isTTY) throw new Error("platform: Set appAccess.identityProviderIds in --auth-config for the hosting account.");
|
|
2172
|
+
const providers = (await access.list("identity_providers")).map(platformAccessIdentityProvider);
|
|
2173
|
+
if (!providers.length) throw new Error(`platform: Add an identity provider in Zero Trust for ${input.plan.account.name}, then resume.`);
|
|
2174
|
+
identityProviderIds = answer$1(await multiselect({
|
|
2175
|
+
message: "App identity providers:",
|
|
2176
|
+
required: true,
|
|
2177
|
+
options: providers.map((p) => ({
|
|
2178
|
+
value: p.id,
|
|
2179
|
+
label: stripVTControlCharacters(p.name)
|
|
2180
|
+
}))
|
|
2181
|
+
}));
|
|
2182
|
+
input.save({ appAccessIdentityProviderIds: JSON.stringify(identityProviderIds) });
|
|
2183
|
+
}
|
|
2184
|
+
if (!policyIds.length) {
|
|
2185
|
+
if (!process.stdin.isTTY) throw new Error("platform: Set appAccess.policyIds in --auth-config for the hosting account.");
|
|
2186
|
+
policyIds = await selectCompanyAccessPolicies(access, await access.list("policies"), input.plan.account.id, "create");
|
|
2187
|
+
input.save({ appAccessPolicyIds: JSON.stringify(policyIds) });
|
|
2188
|
+
}
|
|
2189
|
+
} else log.info("This account requires Access. Void will create public rules scoped to app hostnames.");
|
|
2190
|
+
const setup = parsePlatformAppAccessSetup({
|
|
2191
|
+
version: 1,
|
|
2192
|
+
accountId: input.plan.account.id,
|
|
2193
|
+
mode: choice === "public" ? "public" : "protected",
|
|
2194
|
+
managed: true,
|
|
2195
|
+
apiToken: token,
|
|
2196
|
+
identityProviderIds,
|
|
2197
|
+
policyIds
|
|
2198
|
+
});
|
|
2199
|
+
if (setup.mode === "protected") try {
|
|
2200
|
+
await inspectPlatformAccess(access, setup);
|
|
2201
|
+
} catch (error) {
|
|
2202
|
+
if (error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) input.save({ appAccessTokenRejected: "true" });
|
|
2203
|
+
else input.save({
|
|
2204
|
+
appAccessIdentityProviderIds: "",
|
|
2205
|
+
appAccessPolicyIds: ""
|
|
2206
|
+
});
|
|
2207
|
+
throw error;
|
|
2208
|
+
}
|
|
2209
|
+
const result = JSON.stringify(setup);
|
|
2210
|
+
input.save({
|
|
2211
|
+
appAccessSetupJson: result,
|
|
2212
|
+
appAccessIdentityProviderIds: JSON.stringify(identityProviderIds),
|
|
2213
|
+
appAccessPolicyIds: JSON.stringify(policyIds),
|
|
2214
|
+
appAccessTokenRejected: ""
|
|
2215
|
+
});
|
|
2216
|
+
return result;
|
|
2217
|
+
}
|
|
2218
|
+
var PlatformAppAccessCredentialError = class extends Error {};
|
|
2219
|
+
/** Bounded, resumable handoff to the service that owns Zero Trust state. */
|
|
2220
|
+
async function initializeInstalledAppAccess(input, fetcher = fetch) {
|
|
2221
|
+
parsePlatformAppAccessSetup(JSON.parse(input.setupJson));
|
|
2222
|
+
if (!input.setupToken || input.setupToken.length < 32) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
|
|
2223
|
+
const signature = createHmac("sha256", input.setupToken).update(`${PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX}\n${input.installationId}\n${input.setupJson}`).digest("base64url");
|
|
2224
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2225
|
+
const response = await fetcher(new URL("/internal/platform/app-access", input.apiUrl), {
|
|
2226
|
+
method: "POST",
|
|
2227
|
+
headers: {
|
|
2228
|
+
"Content-Type": "application/json",
|
|
2229
|
+
"X-Void-Platform-Probe": input.probeToken,
|
|
2230
|
+
"X-Void-App-Access-Setup": signature,
|
|
2231
|
+
...cfAccessHeaders(process.env, input.apiUrl)
|
|
2232
|
+
},
|
|
2233
|
+
body: input.setupJson,
|
|
2234
|
+
redirect: "manual",
|
|
2235
|
+
signal: AbortSignal.timeout(6e4)
|
|
2236
|
+
});
|
|
2237
|
+
if (response.status === 403) {
|
|
2238
|
+
const detail = await response.clone().text();
|
|
2239
|
+
if (/1050|Default-Deny/i.test(detail)) throw new Error(`platform: Account Default-Deny blocks the platform API at ${input.apiUrl}. Configure Access for the platform API and its installer credentials, then resume.`);
|
|
2240
|
+
}
|
|
2241
|
+
let body;
|
|
2242
|
+
try {
|
|
2243
|
+
body = await response.json();
|
|
2244
|
+
} catch {
|
|
2245
|
+
throw new Error(`platform: App access setup did not respond (HTTP ${response.status}). Check platform Access permissions and resume.`);
|
|
2246
|
+
}
|
|
2247
|
+
if (body.code === "app_access_credentials") throw new PlatformAppAccessCredentialError("App access token rejected. Resume installation to provide a replacement.");
|
|
2248
|
+
if (!response.ok || body.installationId !== input.installationId) throw new Error(`platform: ${body.error || "App access setup failed."} Resume installation after correcting the Access configuration.`);
|
|
2249
|
+
if (body.ready) return;
|
|
2250
|
+
}
|
|
2251
|
+
throw new Error("platform: App access setup is safely checkpointed. Resume installation to finish before app traffic opens.");
|
|
2252
|
+
}
|
|
2253
|
+
//#endregion
|
|
1519
2254
|
//#region src/cli/platform-auth-bootstrap.ts
|
|
1520
2255
|
/** Checkpoint a replacement before submission so an interrupted response is safely replayable. */
|
|
1521
2256
|
async function resumeInstalledPlatformAuthentication(input, initialize = initializeInstalledPlatformAuthentication) {
|
|
@@ -1560,6 +2295,26 @@ async function initializeInstalledPlatformAuthentication(apiUrl, installationId,
|
|
|
1560
2295
|
}
|
|
1561
2296
|
//#endregion
|
|
1562
2297
|
//#region src/cli/platform-auth-database.ts
|
|
2298
|
+
/** Restore the selected external origin before an adoption receipt exists. */
|
|
2299
|
+
function readPlatformHyperdriveSelection(checkpoint) {
|
|
2300
|
+
if (!checkpoint?.externalHyperdriveJson) return;
|
|
2301
|
+
let value;
|
|
2302
|
+
try {
|
|
2303
|
+
value = JSON.parse(checkpoint.externalHyperdriveJson);
|
|
2304
|
+
} catch {
|
|
2305
|
+
throw new Error("platform: The saved Hyperdrive selection is invalid.");
|
|
2306
|
+
}
|
|
2307
|
+
const selected = value;
|
|
2308
|
+
const nonempty = (value) => typeof value === "string" && value.trim() !== "";
|
|
2309
|
+
if (!selected || typeof selected !== "object" || Array.isArray(selected) || !nonempty(selected.id) || !nonempty(selected.host) || !nonempty(selected.user) || selected.name !== void 0 && (typeof selected.name !== "string" || !selected.name) || selected.database !== void 0 && (typeof selected.database !== "string" || !selected.database)) throw new Error("platform: The saved Hyperdrive selection is invalid.");
|
|
2310
|
+
return {
|
|
2311
|
+
id: selected.id,
|
|
2312
|
+
host: selected.host,
|
|
2313
|
+
user: selected.user,
|
|
2314
|
+
...selected.name ? { name: selected.name } : {},
|
|
2315
|
+
...selected.database ? { database: selected.database } : {}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
1563
2318
|
/** Read the database name from an owner URL without applying Hyperdrive origin restrictions. */
|
|
1564
2319
|
function platformDatabaseNameFromOwnerUrl(databaseUrl) {
|
|
1565
2320
|
let url;
|
|
@@ -1637,8 +2392,10 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1637
2392
|
...enableEmail ? { email: true } : {}
|
|
1638
2393
|
};
|
|
1639
2394
|
if (capabilities.email && options?.includeEmailGateway !== false && !enableEmail) throw new Error("platform: Email-enabled installations require VOID_EMAIL_SENDER_DOMAIN and VOID_EMAIL_SHARED_ZONE_ID before deploying this runtime.");
|
|
2395
|
+
if (input.appAccess && input.appAccess !== "public" && input.controlPlaneDomain.toLowerCase().replace(/\.$/, "").endsWith(`.${input.applicationDomain.toLowerCase()}`)) throw new Error("platform: The API hostname must be outside the app Access wildcard. Use the application domain apex or workers.dev for the API, then plan again.");
|
|
1640
2396
|
const deployment = createPlatformDeploymentManifest({
|
|
1641
2397
|
authentication: input.authentication ?? existing?.deployment.authentication,
|
|
2398
|
+
appAccess: input.appAccess ?? existing?.deployment.appAccess,
|
|
1642
2399
|
...Object.keys(capabilities).length > 0 ? { capabilities } : {},
|
|
1643
2400
|
application: {
|
|
1644
2401
|
baseDomain: testing ? `${accountSubdomain}.workers.dev` : input.applicationDomain,
|
|
@@ -1844,6 +2601,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1844
2601
|
actions,
|
|
1845
2602
|
apiUrl,
|
|
1846
2603
|
deployment: resolvedDeployment,
|
|
2604
|
+
...resolvedDeployment.appAccess ? { appAccessSetupRequired: !existing?.steps.some((step) => step.key === "app-access-initialized:v1") } : {},
|
|
1847
2605
|
...input.externalHyperdrive ? { externalHyperdrive: input.externalHyperdrive } : {},
|
|
1848
2606
|
installationId,
|
|
1849
2607
|
proxyUrl,
|
|
@@ -2146,18 +2904,21 @@ async function readRemoteOwnership(client, accountId, routingNamespaces, existin
|
|
|
2146
2904
|
}
|
|
2147
2905
|
function formatPlatformInstallPlan(plan) {
|
|
2148
2906
|
const lines = [
|
|
2149
|
-
`${import_picocolors
|
|
2150
|
-
`${import_picocolors
|
|
2151
|
-
plan.deployment.application.zoneName ? `${import_picocolors
|
|
2152
|
-
`${import_picocolors
|
|
2153
|
-
`${import_picocolors
|
|
2154
|
-
...platformDashboardUrl(plan.deployment) ? [`${import_picocolors
|
|
2155
|
-
`${import_picocolors
|
|
2156
|
-
`${import_picocolors
|
|
2157
|
-
`${import_picocolors
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2907
|
+
`${import_picocolors.default.bold(`${plan.deployment.displayName} in ${plan.account.name}`)}`,
|
|
2908
|
+
`${import_picocolors.default.bold("Account:")} ${plan.account.id}`,
|
|
2909
|
+
plan.deployment.application.zoneName ? `${import_picocolors.default.bold("Applications:")} *.${plan.deployment.application.baseDomain}` : `${import_picocolors.default.bold("Applications:")} workers.dev for testing. Add a domain with void platform domain set <domain>`,
|
|
2910
|
+
`${import_picocolors.default.bold("Control plane:")} ${plan.apiUrl}`,
|
|
2911
|
+
`${import_picocolors.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
|
|
2912
|
+
...platformDashboardUrl(plan.deployment) ? [`${import_picocolors.default.bold("Dashboard:")} ${platformDashboardUrl(plan.deployment)} (deploy separately)`] : [],
|
|
2913
|
+
`${import_picocolors.default.bold("Login methods:")} ${plan.deployment.authentication.providers.join(", ")}`,
|
|
2914
|
+
`${import_picocolors.default.bold("Access protection:")} ${plan.deployment.authentication.accessProtection ? "Enabled" : "Not enabled"}`,
|
|
2915
|
+
...plan.deployment.appAccess ? [plan.appAccessSetupRequired === false ? `${import_picocolors.default.bold("App access:")} Preserve administrator settings` : `${import_picocolors.default.bold("Default app access:")} ${{
|
|
2916
|
+
public: "Public apps",
|
|
2917
|
+
platform: "Same as platform Access policy",
|
|
2918
|
+
custom: "Selected Access policy"
|
|
2919
|
+
}[plan.deployment.appAccess]}`] : [],
|
|
2920
|
+
`${import_picocolors.default.bold("Login callback:")} ${plan.apiUrl}/auth/callback`,
|
|
2921
|
+
`${import_picocolors.default.bold("Database:")} ${plan.externalHyperdrive?.database || "PostgreSQL"} via Hyperdrive ${plan.deployment.resources.hyperdrive}`,
|
|
2161
2922
|
""
|
|
2162
2923
|
];
|
|
2163
2924
|
for (const action of plan.actions) lines.push(`${action.action.padEnd(6)} ${action.kind.padEnd(19)} ${action.name}`);
|
|
@@ -2647,12 +3408,17 @@ function createPlatformInstallationSecrets(input) {
|
|
|
2647
3408
|
});
|
|
2648
3409
|
return {
|
|
2649
3410
|
authenticationPreparation: Object.fromEntries(Object.entries(checkpoint).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
3411
|
+
...checkpoint.appAccessSetupToken ? { appAccessSetupToken: checkpoint.appAccessSetupToken } : {},
|
|
3412
|
+
...checkpoint.appAccessSetupJson ? { appAccessSetupJson: checkpoint.appAccessSetupJson } : {},
|
|
3413
|
+
...checkpoint.managementApiToken ? { managementApiToken: checkpoint.managementApiToken } : {},
|
|
3414
|
+
...checkpoint.externalHyperdriveJson ? { externalHyperdriveJson: checkpoint.externalHyperdriveJson } : {},
|
|
2650
3415
|
...modernAuthentication ? {
|
|
2651
3416
|
authenticationSetupJson,
|
|
2652
3417
|
authSetupToken,
|
|
2653
3418
|
authBootstrapCode
|
|
2654
3419
|
} : {},
|
|
2655
3420
|
...checkpoint.accessProvisioningJson ? { accessProvisioningJson: checkpoint.accessProvisioningJson } : {},
|
|
3421
|
+
...checkpoint.accessHealthProvisioningJson ? { accessHealthProvisioningJson: checkpoint.accessHealthProvisioningJson } : {},
|
|
2656
3422
|
...checkpoint.accessCredentialsJson ? { accessCredentialsJson: checkpoint.accessCredentialsJson } : {},
|
|
2657
3423
|
workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
|
|
2658
3424
|
pendingWorkerSecretUpdateIntent: input.updateExistingWorkers ? pendingWorkerSecretUpdateIntent : void 0,
|
|
@@ -2722,6 +3488,10 @@ function assertCanonicalProjectSecretKey(value, name, uniqueToInstallation = fal
|
|
|
2722
3488
|
}
|
|
2723
3489
|
function secretCheckpoint(secrets, workerSecretUpdateIntent) {
|
|
2724
3490
|
return {
|
|
3491
|
+
...secrets.appAccessSetupToken ? { appAccessSetupToken: secrets.appAccessSetupToken } : {},
|
|
3492
|
+
...secrets.appAccessSetupJson ? { appAccessSetupJson: secrets.appAccessSetupJson } : {},
|
|
3493
|
+
...secrets.managementApiToken ? { managementApiToken: secrets.managementApiToken } : {},
|
|
3494
|
+
...secrets.externalHyperdriveJson ? { externalHyperdriveJson: secrets.externalHyperdriveJson } : {},
|
|
2725
3495
|
...Object.fromEntries(Object.entries(secrets.authenticationPreparation ?? {}).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
2726
3496
|
...secrets.authSetupToken ? {
|
|
2727
3497
|
authenticationSetupJson: secrets.authenticationSetupJson ?? "",
|
|
@@ -2729,6 +3499,7 @@ function secretCheckpoint(secrets, workerSecretUpdateIntent) {
|
|
|
2729
3499
|
authBootstrapCode: secrets.authBootstrapCode ?? ""
|
|
2730
3500
|
} : {},
|
|
2731
3501
|
...secrets.accessProvisioningJson ? { accessProvisioningJson: secrets.accessProvisioningJson } : {},
|
|
3502
|
+
...secrets.accessHealthProvisioningJson ? { accessHealthProvisioningJson: secrets.accessHealthProvisioningJson } : {},
|
|
2732
3503
|
...secrets.accessCredentialsJson ? { accessCredentialsJson: secrets.accessCredentialsJson } : {},
|
|
2733
3504
|
bootstrapGitHubId: secrets.bootstrapGitHubId,
|
|
2734
3505
|
bootstrapGitHubLogin: secrets.bootstrapGitHubLogin,
|
|
@@ -2985,6 +3756,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2985
3756
|
const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
|
|
2986
3757
|
const runtimeDirectory = resolvedRuntime.directory;
|
|
2987
3758
|
const runtime = resolvedRuntime.manifest;
|
|
3759
|
+
if (plan.deployment.appAccess && runtime.appAccessSetup !== 1) throw new Error("platform: This runtime does not support the installation’s app access setup.");
|
|
2988
3760
|
const hasEmailGatewayArtifact = runtime.artifacts.emailGateway !== void 0;
|
|
2989
3761
|
if (runtime.emailProtocolVersion !== 1 && plan.deployment.capabilities.email) throw new Error("platform: This runtime cannot serve enabled platform email without email protocol version 1.");
|
|
2990
3762
|
if (runtime.emailProtocolVersion !== 1 && existing?.resources.some((resource) => resource.key === "email-gateway-worker" && resource.state === "ready")) throw new Error("platform: This runtime cannot replace an installed email gateway Worker. Use a runtime with email protocol version 1.");
|
|
@@ -3077,6 +3849,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3077
3849
|
parsePostgresConnectionString(secrets.databaseUrl);
|
|
3078
3850
|
assertVoidCreatedHyperdriveTrustsDatabase(secrets.databaseUrl);
|
|
3079
3851
|
}
|
|
3852
|
+
if (plan.externalHyperdrive) secrets = {
|
|
3853
|
+
...secrets,
|
|
3854
|
+
externalHyperdriveJson: JSON.stringify(plan.externalHyperdrive)
|
|
3855
|
+
};
|
|
3080
3856
|
const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
|
|
3081
3857
|
writePendingInstallationSecrets(manifest, {
|
|
3082
3858
|
...pendingSecrets,
|
|
@@ -3117,10 +3893,6 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3117
3893
|
throw error;
|
|
3118
3894
|
}
|
|
3119
3895
|
if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
|
|
3120
|
-
let workerDeploymentSecrets = {
|
|
3121
|
-
...secrets,
|
|
3122
|
-
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
3123
|
-
};
|
|
3124
3896
|
try {
|
|
3125
3897
|
assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
|
|
3126
3898
|
if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
|
|
@@ -3239,15 +4011,47 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3239
4011
|
activateAccessCredentials();
|
|
3240
4012
|
assertRequiredPlatformSecrets(plan, manifest, secrets);
|
|
3241
4013
|
if (secrets.authenticationSetupJson && createHash("sha256").update(secrets.authBootstrapCode ?? "").digest("hex") !== parsePlatformAuthenticationSetup(JSON.parse(secrets.authenticationSetupJson)).bootstrapCredentialHash) throw new Error("platform: Prepared authentication does not match its administrator setup code.");
|
|
3242
|
-
workerDeploymentSecrets = {
|
|
3243
|
-
...secrets,
|
|
3244
|
-
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
3245
|
-
};
|
|
3246
4014
|
writePendingInstallationSecrets(manifest, {
|
|
3247
4015
|
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
3248
4016
|
databaseClaimState: "claimed"
|
|
3249
4017
|
});
|
|
3250
4018
|
}
|
|
4019
|
+
const appAccessSetupRequired = !!plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
4020
|
+
if (appAccessSetupRequired) {
|
|
4021
|
+
if (dependencies.prepareAppAccess) try {
|
|
4022
|
+
const appAccessSetupJson = await dependencies.prepareAppAccess(manifest, secrets);
|
|
4023
|
+
const latest = readPendingInstallationSecrets(manifest) ?? {};
|
|
4024
|
+
secrets = {
|
|
4025
|
+
...secrets,
|
|
4026
|
+
appAccessSetupJson,
|
|
4027
|
+
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
|
|
4028
|
+
};
|
|
4029
|
+
writePendingInstallationSecrets(manifest, {
|
|
4030
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4031
|
+
databaseClaimState: "claimed"
|
|
4032
|
+
});
|
|
4033
|
+
} catch (error) {
|
|
4034
|
+
const latest = readPendingInstallationSecrets(manifest) ?? {};
|
|
4035
|
+
secrets = {
|
|
4036
|
+
...secrets,
|
|
4037
|
+
appAccessSetupJson: latest.appAccessSetupJson || secrets.appAccessSetupJson,
|
|
4038
|
+
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
|
|
4039
|
+
};
|
|
4040
|
+
throw error;
|
|
4041
|
+
}
|
|
4042
|
+
if (!secrets.appAccessSetupJson) throw new Error("platform: Restore the encrypted app access setup checkpoint before resuming.");
|
|
4043
|
+
if (!secrets.appAccessSetupToken) {
|
|
4044
|
+
if (manifest.resources.some((resource) => resource.key === "api-worker" && resource.state === "ready")) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
|
|
4045
|
+
secrets = {
|
|
4046
|
+
...secrets,
|
|
4047
|
+
appAccessSetupToken: randomBytes(32).toString("hex")
|
|
4048
|
+
};
|
|
4049
|
+
writePendingInstallationSecrets(manifest, {
|
|
4050
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4051
|
+
databaseClaimState: "claimed"
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
3251
4055
|
if (plan.deployment.authentication.accessProtection) {
|
|
3252
4056
|
const protection = liveAuthentication ? liveAuthentication.protection : secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(secrets.authenticationSetupJson)).protection : null;
|
|
3253
4057
|
if (!protection) throw new Error("platform: The active Access protection configuration is unavailable. Restore its database and encrypted setup checkpoint before maintenance.");
|
|
@@ -3261,6 +4065,44 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3261
4065
|
checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent)
|
|
3262
4066
|
});
|
|
3263
4067
|
}
|
|
4068
|
+
if (plan.deployment.authentication.accessProtection || appAccessSetupRequired && secrets.appAccessSetupJson && JSON.parse(secrets.appAccessSetupJson).managed === true) {
|
|
4069
|
+
dependencies.onProgress?.("Preparing Access for authenticated platform health checks");
|
|
4070
|
+
try {
|
|
4071
|
+
await (dependencies.prepareHealthAccess ?? ensurePlatformAccessHealthChecks)({
|
|
4072
|
+
client: dependencies.client,
|
|
4073
|
+
manifest,
|
|
4074
|
+
plan,
|
|
4075
|
+
checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4076
|
+
save: (accessHealthProvisioningJson) => {
|
|
4077
|
+
secrets = {
|
|
4078
|
+
...secrets,
|
|
4079
|
+
accessHealthProvisioningJson
|
|
4080
|
+
};
|
|
4081
|
+
manifest = checkpointAuthenticationResources(manifest, secrets, now());
|
|
4082
|
+
writePendingInstallationSecrets(manifest, {
|
|
4083
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4084
|
+
databaseClaimState: "claimed"
|
|
4085
|
+
});
|
|
4086
|
+
writeInstallationManifest(manifest);
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
} catch (error) {
|
|
4090
|
+
if (appAccessSetupRequired && secrets.appAccessSetupJson && JSON.parse(secrets.appAccessSetupJson).managed && error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) {
|
|
4091
|
+
secrets = {
|
|
4092
|
+
...secrets,
|
|
4093
|
+
authenticationPreparation: {
|
|
4094
|
+
...secrets.authenticationPreparation,
|
|
4095
|
+
appAccessTokenRejected: "true"
|
|
4096
|
+
}
|
|
4097
|
+
};
|
|
4098
|
+
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
4099
|
+
throw new PlatformAppAccessCredentialError("platform: App access token rejected while preparing health checks. Resume installation to provide a replacement.");
|
|
4100
|
+
}
|
|
4101
|
+
throw error;
|
|
4102
|
+
} finally {
|
|
4103
|
+
await releaseDatabaseLock.checkpoint?.(manifest);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
3264
4106
|
const routingAction = plan.actions.find((action) => action.key === "routing-kv");
|
|
3265
4107
|
if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
|
|
3266
4108
|
if (routingAction.action === "reuse") {
|
|
@@ -3362,7 +4204,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3362
4204
|
const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
3363
4205
|
const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
|
|
3364
4206
|
const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
|
|
3365
|
-
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain;
|
|
4207
|
+
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain || !!plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
3366
4208
|
if (requiresTrafficGuard) {
|
|
3367
4209
|
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
3368
4210
|
workerTrafficGuarded = true;
|
|
@@ -3379,6 +4221,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3379
4221
|
}
|
|
3380
4222
|
const configInput = {
|
|
3381
4223
|
authenticationDatabaseUncached: true,
|
|
4224
|
+
proxyHealthBinding: true,
|
|
3382
4225
|
accountId: plan.account.id,
|
|
3383
4226
|
apiUrl: plan.apiUrl,
|
|
3384
4227
|
deployment: plan.deployment,
|
|
@@ -3399,6 +4242,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3399
4242
|
runtimeEntrypoints: entrypoints,
|
|
3400
4243
|
...runtime.assets ? { runtimeApiAssetsDirectory: join(runtimeDirectory, PLATFORM_ADMIN_ASSETS_DIRECTORY) } : {}
|
|
3401
4244
|
};
|
|
4245
|
+
const workerDeploymentSecrets = {
|
|
4246
|
+
...secrets,
|
|
4247
|
+
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
4248
|
+
};
|
|
3402
4249
|
const requiresStagedCommit = !preserveDisabled && requiresTrafficGuard;
|
|
3403
4250
|
const enabledConfigs = createPlatformWorkerConfigs({
|
|
3404
4251
|
...configInput,
|
|
@@ -3665,6 +4512,37 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3665
4512
|
writeInstallationManifest(manifest);
|
|
3666
4513
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3667
4514
|
}
|
|
4515
|
+
if (plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1")) {
|
|
4516
|
+
dependencies.onProgress?.("Configuring and verifying deployed-app access");
|
|
4517
|
+
try {
|
|
4518
|
+
await (dependencies.initializeAppAccess ?? initializeInstalledAppAccess)({
|
|
4519
|
+
apiUrl: plan.apiUrl,
|
|
4520
|
+
installationId: manifest.id,
|
|
4521
|
+
probeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
4522
|
+
setupJson: secrets.appAccessSetupJson,
|
|
4523
|
+
setupToken: secrets.appAccessSetupToken
|
|
4524
|
+
});
|
|
4525
|
+
} catch (error) {
|
|
4526
|
+
if (error instanceof PlatformAppAccessCredentialError) {
|
|
4527
|
+
secrets = {
|
|
4528
|
+
...secrets,
|
|
4529
|
+
authenticationPreparation: {
|
|
4530
|
+
...secrets.authenticationPreparation,
|
|
4531
|
+
appAccessTokenRejected: "true"
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
4535
|
+
}
|
|
4536
|
+
throw error;
|
|
4537
|
+
}
|
|
4538
|
+
manifest = recordInstallationStep(manifest, {
|
|
4539
|
+
key: "app-access-initialized:v1",
|
|
4540
|
+
completedAt: now(),
|
|
4541
|
+
resultHash: hash(secrets.appAccessSetupJson)
|
|
4542
|
+
}, now());
|
|
4543
|
+
writeInstallationManifest(manifest);
|
|
4544
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
4545
|
+
}
|
|
3668
4546
|
dependencies.onProgress?.("Verifying platform health");
|
|
3669
4547
|
const emailGatewayUrl = hasEmailGatewayArtifact && plan.deployment.capabilities.email ? `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev` : void 0;
|
|
3670
4548
|
if (plan.deployment.authentication.accessProtection) {
|
|
@@ -3673,6 +4551,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3673
4551
|
}
|
|
3674
4552
|
const healthCheckContext = {
|
|
3675
4553
|
accountName: plan.account.name,
|
|
4554
|
+
...plan.deployment.authentication.accessProtection ? { accessProtected: true } : {},
|
|
3676
4555
|
workers: [
|
|
3677
4556
|
plan.deployment.resources.apiWorker,
|
|
3678
4557
|
plan.deployment.resources.proxyWorker,
|
|
@@ -3844,7 +4723,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3844
4723
|
}
|
|
3845
4724
|
}
|
|
3846
4725
|
function checkpointAuthenticationResources(manifest, secrets, updatedAt) {
|
|
3847
|
-
return checkpointPlatformAccessResources(manifest, secrets.accessProvisioningJson, updatedAt);
|
|
4726
|
+
return checkpointPlatformAccessResources(checkpointPlatformAccessResources(manifest, secrets.accessProvisioningJson, updatedAt), secrets.accessHealthProvisioningJson, updatedAt);
|
|
3848
4727
|
}
|
|
3849
4728
|
async function writeR2OwnershipMarkerIfNeeded(client, plan, manifest, action) {
|
|
3850
4729
|
if (action.kind !== "r2-bucket") return;
|
|
@@ -4094,6 +4973,7 @@ function apiWorkerSecrets(secrets, accountId, zoneId) {
|
|
|
4094
4973
|
JWT_SECRET: secrets.jwtSecret,
|
|
4095
4974
|
PLATFORM_BOOTSTRAP_GITHUB_ID: secrets.bootstrapGitHubId,
|
|
4096
4975
|
PLATFORM_AUTH_SETUP_TOKEN: secrets.authSetupToken ?? "",
|
|
4976
|
+
PLATFORM_APP_ACCESS_SETUP_TOKEN: secrets.appAccessSetupToken ?? "",
|
|
4097
4977
|
...secrets.projectSecretKeysJson && secrets.projectSecretActiveKeyVersion ? {
|
|
4098
4978
|
PROJECT_SECRET_ACTIVE_KEY_VERSION: secrets.projectSecretActiveKeyVersion,
|
|
4099
4979
|
PROJECT_SECRET_KEYS_JSON: secrets.projectSecretKeysJson
|
|
@@ -5642,7 +6522,15 @@ function errorMessage(error) {
|
|
|
5642
6522
|
}
|
|
5643
6523
|
var PlatformAccessBlockedError = class extends Error {
|
|
5644
6524
|
constructor(url, context) {
|
|
5645
|
-
super([
|
|
6525
|
+
super(context?.accessProtected ? [
|
|
6526
|
+
"platform: Cloudflare Access blocked a protected platform health check (Default-Deny, error 1050).",
|
|
6527
|
+
`Blocked URL: ${url}`,
|
|
6528
|
+
"Keep employee Access protection and account-wide Default-Deny enabled.",
|
|
6529
|
+
"The reserved dispatch /health endpoint uses a path-only Access exception and still requires Void’s private probe token.",
|
|
6530
|
+
"Resume to verify the health exceptions and saved installer credentials.",
|
|
6531
|
+
...context.resumeCommand ? [`Resume: ${context.resumeCommand}`] : [],
|
|
6532
|
+
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
6533
|
+
].join("\n") : [
|
|
5646
6534
|
"platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
|
|
5647
6535
|
`Blocked URL: ${url}`,
|
|
5648
6536
|
"",
|
|
@@ -5688,16 +6576,16 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
|
|
|
5688
6576
|
request(`${dispatchUrl}/health`),
|
|
5689
6577
|
context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
|
|
5690
6578
|
]);
|
|
5691
|
-
if (!health.ok) throw await platformProbeFailure("Health check", apiUrl
|
|
5692
|
-
if (!discovery.ok) throw await platformProbeFailure("Discovery check", apiUrl
|
|
6579
|
+
if (!health.ok) throw await platformProbeFailure("Health check", `${apiUrl}/health`, health, context);
|
|
6580
|
+
if (!discovery.ok) throw await platformProbeFailure("Discovery check", `${apiUrl}/.well-known/void-platform`, discovery, context);
|
|
5693
6581
|
const body = await discovery.json();
|
|
5694
6582
|
if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
|
|
5695
6583
|
if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
|
|
5696
6584
|
if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
|
|
5697
|
-
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", proxyUrl
|
|
5698
|
-
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", dispatchUrl
|
|
6585
|
+
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", `${proxyUrl}/health`, proxy, context);
|
|
6586
|
+
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", `${dispatchUrl}/health`, dispatch, context);
|
|
5699
6587
|
if (emailGateway) {
|
|
5700
|
-
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", context.emailGatewayUrl
|
|
6588
|
+
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", `${context.emailGatewayUrl}/health`, emailGateway, context);
|
|
5701
6589
|
const identity = await emailGateway.json();
|
|
5702
6590
|
if (identity?.installationId !== installationId || identity.platformVersion !== platformVersion || identity.emailProtocolVersion !== 1) throw new Error("platform: Deployed email gateway identity, version, or protocol does not match this installation.");
|
|
5703
6591
|
}
|
|
@@ -5838,6 +6726,15 @@ function createPlatformHealthProbeToken(ownershipNonce) {
|
|
|
5838
6726
|
}
|
|
5839
6727
|
//#endregion
|
|
5840
6728
|
//#region src/cli/platform-install-auth.ts
|
|
6729
|
+
function platformManagementAccessForAuthentication(selection, accountId, checkpoint) {
|
|
6730
|
+
if (!selection.authentication.accessProtection && (!selection.authentication.providers.includes("cloudflare-access") || selection.file && !selection.file.cloudflareAccess)) return;
|
|
6731
|
+
const spec = selection.file?.cloudflareAccess ?? (checkpoint?.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(checkpoint.accessSetupSpecJson)) : void 0);
|
|
6732
|
+
if (spec?.accountId && spec.accountId !== accountId) return;
|
|
6733
|
+
return {
|
|
6734
|
+
protection: !!selection.authentication.accessProtection,
|
|
6735
|
+
readOnly: spec?.mode === "existing"
|
|
6736
|
+
};
|
|
6737
|
+
}
|
|
5841
6738
|
function answer(value) {
|
|
5842
6739
|
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
5843
6740
|
return value;
|
|
@@ -5867,8 +6764,8 @@ async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
|
5867
6764
|
}
|
|
5868
6765
|
if (!process.stdin.isTTY && !choices.loginMethods && choices.accessProtection === void 0) return { authentication: { providers: ["github"] } };
|
|
5869
6766
|
const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] : answer(await multiselect({
|
|
5870
|
-
message: "Platform login methods:",
|
|
5871
|
-
initialValues: [
|
|
6767
|
+
message: "Platform login methods (select all that apply):",
|
|
6768
|
+
initialValues: [],
|
|
5872
6769
|
required: true,
|
|
5873
6770
|
options: [
|
|
5874
6771
|
{
|
|
@@ -6076,6 +6973,72 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
6076
6973
|
return result;
|
|
6077
6974
|
}
|
|
6078
6975
|
//#endregion
|
|
6976
|
+
//#region src/cli/platform-saved-plans.ts
|
|
6977
|
+
function planId(args) {
|
|
6978
|
+
return createHash("sha256").update(JSON.stringify([args.account, args.name])).digest("hex");
|
|
6979
|
+
}
|
|
6980
|
+
function parseSavedPlan(value) {
|
|
6981
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected an installation plan.");
|
|
6982
|
+
const record = value;
|
|
6983
|
+
if (record.schemaVersion !== 1 || !Array.isArray(record.arguments) || !record.arguments.every((arg) => typeof arg === "string")) throw new Error("Invalid installation plan format.");
|
|
6984
|
+
const args = parsePlatformArgs(record.arguments);
|
|
6985
|
+
if (args.subcommand !== "install" || args.plan || args.resume || args.yes || !args.account || !args.name || !args.displayName) throw new Error("Saved plans must contain installation choices without mutation approval.");
|
|
6986
|
+
if (typeof record.accountName !== "string" || !record.accountName || typeof record.savedAt !== "string" || !Number.isFinite(Date.parse(record.savedAt))) throw new Error("Invalid installation plan metadata.");
|
|
6987
|
+
return {
|
|
6988
|
+
id: planId(args),
|
|
6989
|
+
args,
|
|
6990
|
+
accountName: record.accountName,
|
|
6991
|
+
savedAt: record.savedAt
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
function savePlatformInstallChoices(arguments_, accountName, directory = platformRegistryDirectory()) {
|
|
6995
|
+
const record = {
|
|
6996
|
+
schemaVersion: 1,
|
|
6997
|
+
arguments: arguments_,
|
|
6998
|
+
accountName,
|
|
6999
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7000
|
+
};
|
|
7001
|
+
const plan = parseSavedPlan(record);
|
|
7002
|
+
mkdirSync(directory, {
|
|
7003
|
+
recursive: true,
|
|
7004
|
+
mode: 448
|
|
7005
|
+
});
|
|
7006
|
+
const path = join(directory, `plan-${plan.id}.json`);
|
|
7007
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
7008
|
+
try {
|
|
7009
|
+
writeFileSync(temporary, JSON.stringify(record, null, 2) + "\n", {
|
|
7010
|
+
mode: 384,
|
|
7011
|
+
flag: "wx"
|
|
7012
|
+
});
|
|
7013
|
+
renameSync(temporary, path);
|
|
7014
|
+
} finally {
|
|
7015
|
+
rmSync(temporary, { force: true });
|
|
7016
|
+
}
|
|
7017
|
+
return plan;
|
|
7018
|
+
}
|
|
7019
|
+
function listSavedPlatformInstallPlans(directory = platformRegistryDirectory()) {
|
|
7020
|
+
let entries;
|
|
7021
|
+
try {
|
|
7022
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
7023
|
+
} catch (error) {
|
|
7024
|
+
if (error.code === "ENOENT") return [];
|
|
7025
|
+
throw error;
|
|
7026
|
+
}
|
|
7027
|
+
return entries.filter((entry) => entry.isFile() && /^plan-[a-f0-9]{64}\.json$/.test(entry.name)).map((entry) => {
|
|
7028
|
+
const path = join(directory, entry.name);
|
|
7029
|
+
try {
|
|
7030
|
+
const plan = parseSavedPlan(JSON.parse(readFileSync(path, "utf8")));
|
|
7031
|
+
if (entry.name !== `plan-${plan.id}.json`) throw new Error("Plan identity does not match its file.");
|
|
7032
|
+
return plan;
|
|
7033
|
+
} catch (error) {
|
|
7034
|
+
throw new Error(`platform: Invalid saved installation plan ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7035
|
+
}
|
|
7036
|
+
}).sort((left, right) => right.savedAt.localeCompare(left.savedAt) || left.id.localeCompare(right.id));
|
|
7037
|
+
}
|
|
7038
|
+
function removeSavedPlatformInstallPlan(args, directory = platformRegistryDirectory()) {
|
|
7039
|
+
rmSync(join(directory, `plan-${planId(args)}.json`), { force: true });
|
|
7040
|
+
}
|
|
7041
|
+
//#endregion
|
|
6079
7042
|
//#region src/cli/platform-setup-draft.ts
|
|
6080
7043
|
const STARTED = "credential-setup:v1";
|
|
6081
7044
|
const COMPLETED = "credential-setup-complete:v1";
|
|
@@ -6111,7 +7074,7 @@ function savePlatformSetupDraft(plan, version) {
|
|
|
6111
7074
|
resultHash: plan.installationId
|
|
6112
7075
|
}, now);
|
|
6113
7076
|
writeInstallationManifest(manifest);
|
|
6114
|
-
writePendingInstallationSecrets(manifest, {});
|
|
7077
|
+
writePendingInstallationSecrets(manifest, plan.externalHyperdrive ? { externalHyperdriveJson: JSON.stringify(plan.externalHyperdrive) } : {});
|
|
6115
7078
|
return manifest;
|
|
6116
7079
|
}
|
|
6117
7080
|
function completePlatformSetupDraft(manifest) {
|
|
@@ -6125,246 +7088,10 @@ function completePlatformSetupDraft(manifest) {
|
|
|
6125
7088
|
return completed;
|
|
6126
7089
|
}
|
|
6127
7090
|
//#endregion
|
|
6128
|
-
//#region src/cli/platform-setup.ts
|
|
6129
|
-
function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
|
|
6130
|
-
return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + page.title + "\x1B]8;;\x1B\\" : page.title + ": " + page.url;
|
|
6131
|
-
}
|
|
6132
|
-
function platformSetupFieldHelp(key) {
|
|
6133
|
-
const docs = (page, section, title) => platformSetupLink({
|
|
6134
|
-
title,
|
|
6135
|
-
url: `https://void.cloud/guide/platform/installation/${page}#${section}`
|
|
6136
|
-
});
|
|
6137
|
-
switch (key) {
|
|
6138
|
-
case "databaseUrl": return {
|
|
6139
|
-
placeholder: "postgresql://user:pw@…/database",
|
|
6140
|
-
description: "Stores platform users, projects, and deployments. Use an empty hosted database, e.g. " + platformSetupLink({
|
|
6141
|
-
title: "Neon",
|
|
6142
|
-
url: "https://neon.com/"
|
|
6143
|
-
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
6144
|
-
};
|
|
6145
|
-
case "jwtSecret": return { description: "Signs platform login tokens. Run openssl rand -base64 32. Save the key in your password manager. " + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
6146
|
-
case "projectSecretKey": return { description: "Encrypts app secrets. Run openssl rand -base64 32 separately. Save this key for recovery. " + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
6147
|
-
default: return;
|
|
6148
|
-
}
|
|
6149
|
-
}
|
|
6150
|
-
function supportsSetupLinks() {
|
|
6151
|
-
return !!process.stdout.isTTY && ([
|
|
6152
|
-
"ghostty",
|
|
6153
|
-
"iTerm.app",
|
|
6154
|
-
"WezTerm",
|
|
6155
|
-
"vscode",
|
|
6156
|
-
"WarpTerminal"
|
|
6157
|
-
].includes(process.env.TERM_PROGRAM ?? "") || !!process.env.WT_SESSION || !!process.env.VTE_VERSION || !!process.env.KONSOLE_VERSION || process.env.TERM === "xterm-kitty");
|
|
6158
|
-
}
|
|
6159
|
-
/** Dashboard keys use permission-catalog labels, which can differ from product names. */
|
|
6160
|
-
function platformSetupPages(plan) {
|
|
6161
|
-
const needsGateway = plan.actions.some((action) => action.kind === "ai-gateway" && action.action === "inspect");
|
|
6162
|
-
const permissions = [
|
|
6163
|
-
{
|
|
6164
|
-
key: "account_settings",
|
|
6165
|
-
type: "read"
|
|
6166
|
-
},
|
|
6167
|
-
{
|
|
6168
|
-
key: "workers_scripts",
|
|
6169
|
-
type: "edit"
|
|
6170
|
-
},
|
|
6171
|
-
{
|
|
6172
|
-
key: "workers_kv_storage",
|
|
6173
|
-
type: "edit"
|
|
6174
|
-
},
|
|
6175
|
-
{
|
|
6176
|
-
key: "workers_r2",
|
|
6177
|
-
type: "edit"
|
|
6178
|
-
},
|
|
6179
|
-
{
|
|
6180
|
-
key: "d1",
|
|
6181
|
-
type: "edit"
|
|
6182
|
-
},
|
|
6183
|
-
{
|
|
6184
|
-
key: "queues",
|
|
6185
|
-
type: "edit"
|
|
6186
|
-
},
|
|
6187
|
-
{
|
|
6188
|
-
key: "account_analytics",
|
|
6189
|
-
type: "read"
|
|
6190
|
-
},
|
|
6191
|
-
{
|
|
6192
|
-
key: "workers_tail",
|
|
6193
|
-
type: "read"
|
|
6194
|
-
},
|
|
6195
|
-
{
|
|
6196
|
-
key: "query_cache",
|
|
6197
|
-
type: "edit"
|
|
6198
|
-
},
|
|
6199
|
-
...needsGateway ? [{
|
|
6200
|
-
key: "aig",
|
|
6201
|
-
type: "edit"
|
|
6202
|
-
}] : [],
|
|
6203
|
-
...plan.deployment.application.zoneName ? [{
|
|
6204
|
-
key: "cache",
|
|
6205
|
-
type: "purge"
|
|
6206
|
-
}] : [],
|
|
6207
|
-
...plan.deployment.capabilities.customDomains ? [{
|
|
6208
|
-
key: "ssl_and_certificates",
|
|
6209
|
-
type: "edit"
|
|
6210
|
-
}] : [],
|
|
6211
|
-
...plan.deployment.capabilities.email ? [
|
|
6212
|
-
{
|
|
6213
|
-
key: "zone",
|
|
6214
|
-
type: "read"
|
|
6215
|
-
},
|
|
6216
|
-
{
|
|
6217
|
-
key: "zone_settings",
|
|
6218
|
-
type: "edit"
|
|
6219
|
-
},
|
|
6220
|
-
{
|
|
6221
|
-
key: "email_routing_address",
|
|
6222
|
-
type: "edit"
|
|
6223
|
-
},
|
|
6224
|
-
{
|
|
6225
|
-
key: "email_routing_rule",
|
|
6226
|
-
type: "edit"
|
|
6227
|
-
},
|
|
6228
|
-
{
|
|
6229
|
-
key: "email_sending",
|
|
6230
|
-
type: "edit"
|
|
6231
|
-
}
|
|
6232
|
-
] : []
|
|
6233
|
-
];
|
|
6234
|
-
return {
|
|
6235
|
-
runtime: {
|
|
6236
|
-
title: "Cloudflare runtime token",
|
|
6237
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
6238
|
-
to: "/" + plan.account.id + "/api-tokens",
|
|
6239
|
-
name: plan.deployment.displayName + " Runtime",
|
|
6240
|
-
permissionGroupKeys: JSON.stringify(permissions)
|
|
6241
|
-
}),
|
|
6242
|
-
instructions: [
|
|
6243
|
-
"Review the preselected permissions for account " + plan.account.name + ".",
|
|
6244
|
-
"Edit: Workers Scripts, KV, R2, D1, Queues, Hyperdrive" + (needsGateway ? ", AI Gateway." : "."),
|
|
6245
|
-
"Read: Account Settings, Account Analytics, Workers Tail.",
|
|
6246
|
-
...plan.deployment.application.zoneName ? ["Zone " + plan.deployment.application.zoneName + ": Cache Purge: Purge."] : [],
|
|
6247
|
-
...plan.deployment.capabilities.customDomains ? ["Custom project domains also need SSL and Certificates: Edit on that zone."] : [],
|
|
6248
|
-
...plan.deployment.capabilities.email ? ["Email-enabled installations need Email Routing Addresses: Edit and Email Sending: Edit on the account, plus Zone: Read, Zone Settings: Edit and Email Routing Rules: Edit on the mail zones."] : [],
|
|
6249
|
-
"If the form differs, match the permissions above. Create the token, then paste it here."
|
|
6250
|
-
].join("\n")
|
|
6251
|
-
},
|
|
6252
|
-
github: {
|
|
6253
|
-
title: "GitHub OAuth App",
|
|
6254
|
-
url: "https://github.com/settings/applications/new",
|
|
6255
|
-
instructions: "GitHub Auth will be used by the users of your Void Platform instance. Register a GitHub app and generate a client secret.\nHomepage: " + plan.apiUrl + "\nRedirect URI: " + plan.apiUrl + "/auth/callback"
|
|
6256
|
-
},
|
|
6257
|
-
r2: {
|
|
6258
|
-
title: "R2 upload token",
|
|
6259
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
6260
|
-
to: "/" + plan.account.id + "/r2/api-tokens/create",
|
|
6261
|
-
type: "account"
|
|
6262
|
-
}),
|
|
6263
|
-
instructions: [
|
|
6264
|
-
"Select Object Read & Write.",
|
|
6265
|
-
"Keep Apply to all buckets in this account selected.",
|
|
6266
|
-
"Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
|
|
6267
|
-
].join("\n")
|
|
6268
|
-
}
|
|
6269
|
-
};
|
|
6270
|
-
}
|
|
6271
|
-
function formatPlatformSetupPlan(plan) {
|
|
6272
|
-
const pages = platformSetupPages(plan);
|
|
6273
|
-
return [
|
|
6274
|
-
"Credentials needed during installation:",
|
|
6275
|
-
"PostgreSQL database URL",
|
|
6276
|
-
platformSetupLink(pages.runtime),
|
|
6277
|
-
platformSetupLink(pages.r2),
|
|
6278
|
-
...plan.deployment.authentication.providers.includes("github") ? [platformSetupLink(pages.github)] : [],
|
|
6279
|
-
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => (provider === "google" ? "Google OAuth" : provider === "oidc" ? "OIDC" : "Cloudflare Access OIDC") + " client ID and secret"),
|
|
6280
|
-
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access application and installer credentials"] : [],
|
|
6281
|
-
platformSetupLink({
|
|
6282
|
-
title: "JWT signing key and project encryption key",
|
|
6283
|
-
url: "https://void.cloud/guide/platform/installation/credentials#signing-and-encryption-keys"
|
|
6284
|
-
}),
|
|
6285
|
-
"",
|
|
6286
|
-
"Login callback: " + plan.apiUrl + "/auth/callback",
|
|
6287
|
-
"Installation collects and validates missing credentials after you confirm the plan."
|
|
6288
|
-
].join("\n");
|
|
6289
|
-
}
|
|
6290
|
-
function formatPlatformInstallCommand(options) {
|
|
6291
|
-
const quote = (value) => {
|
|
6292
|
-
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
6293
|
-
return process.platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
6294
|
-
};
|
|
6295
|
-
const parts = [
|
|
6296
|
-
options.commandName,
|
|
6297
|
-
"platform",
|
|
6298
|
-
"install"
|
|
6299
|
-
];
|
|
6300
|
-
const flag = (name, value) => {
|
|
6301
|
-
if (value) parts.push(name, quote(value));
|
|
6302
|
-
};
|
|
6303
|
-
if (options.resumeId) {
|
|
6304
|
-
parts.push("--resume");
|
|
6305
|
-
flag("--name", options.resumeId);
|
|
6306
|
-
} else {
|
|
6307
|
-
flag("--name", options.installationSlug);
|
|
6308
|
-
flag("--account", options.accountId);
|
|
6309
|
-
flag("--display-name", options.displayName);
|
|
6310
|
-
if (options.workersDev) parts.push("--workers-dev");
|
|
6311
|
-
else {
|
|
6312
|
-
flag("--application-domain", options.applicationDomain);
|
|
6313
|
-
flag("--zone", options.zoneName);
|
|
6314
|
-
if (options.dedicatedZone) parts.push("--dedicated-zone");
|
|
6315
|
-
else parts.push("--no-dedicated-zone");
|
|
6316
|
-
}
|
|
6317
|
-
flag("--control-plane-domain", options.controlPlaneDomain);
|
|
6318
|
-
if (!options.controlPlaneDomain) parts.push("--no-control-plane-domain");
|
|
6319
|
-
}
|
|
6320
|
-
flag("--hyperdrive", options.hyperdrive ?? "");
|
|
6321
|
-
flag("--auth-config", options.authConfig ?? "");
|
|
6322
|
-
if (!options.authConfig && !options.resumeId && options.authentication) {
|
|
6323
|
-
flag("--login-methods", options.authentication.providers.join(","));
|
|
6324
|
-
parts.push(options.authentication.accessProtection ? "--access-protection" : "--no-access-protection");
|
|
6325
|
-
}
|
|
6326
|
-
flag("--dashboard-url", options.dashboardUrl ?? "");
|
|
6327
|
-
flag("--runtime", options.runtime);
|
|
6328
|
-
if (options.yes) parts.push("--yes");
|
|
6329
|
-
return parts.join(" ");
|
|
6330
|
-
}
|
|
6331
|
-
function openPlatformSetupPage(page, dependencies = {}) {
|
|
6332
|
-
log.info(page.title + "\n" + page.instructions);
|
|
6333
|
-
console.log(platformSetupLink({
|
|
6334
|
-
...page,
|
|
6335
|
-
title: "Open " + page.title
|
|
6336
|
-
}));
|
|
6337
|
-
if (dependencies.interactive ?? process.stdin.isTTY === true) try {
|
|
6338
|
-
(dependencies.open ?? ((url) => openBrowser(url, execFile)))(page.url);
|
|
6339
|
-
} catch {
|
|
6340
|
-
log.info("Could not open the browser. Use the link above.");
|
|
6341
|
-
}
|
|
6342
|
-
}
|
|
6343
|
-
//#endregion
|
|
6344
7091
|
//#region src/cli/prompt-password.ts
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
return await new u$1({
|
|
6349
|
-
input: options.input,
|
|
6350
|
-
output: options.output,
|
|
6351
|
-
signal: options.signal,
|
|
6352
|
-
mask: options.mask ?? S_PASSWORD_MASK,
|
|
6353
|
-
validate: options.validate,
|
|
6354
|
-
render() {
|
|
6355
|
-
const guide = options.withGuide ?? settings.withGuide;
|
|
6356
|
-
const prefix = `${guide ? `${import_picocolors.default.gray(S_BAR)}\n` : ""}${symbol(this.state)} ${options.message}\n`;
|
|
6357
|
-
const bar = guide ? `${import_picocolors.default.gray(S_BAR)} ` : "";
|
|
6358
|
-
if (this.state === "submit" || this.state === "cancel") return `${prefix}${bar}${import_picocolors.default.dim(this.masked)}`;
|
|
6359
|
-
if (this.state === "error") {
|
|
6360
|
-
const masked = this.masked;
|
|
6361
|
-
if (options.clearOnError) this.clear();
|
|
6362
|
-
return `${prefix}${bar}${import_picocolors.default.dim(masked)}\n${guide ? `${import_picocolors.default.yellow(S_BAR_END)} ` : ""}${import_picocolors.default.yellow(this.error)}\n`;
|
|
6363
|
-
}
|
|
6364
|
-
const input = this.userInput.length === 0 ? import_picocolors.default.dim(options.placeholder) : this.userInputWithCursor;
|
|
6365
|
-
return `${prefix}${guide ? `${import_picocolors.default.cyan(S_BAR)} ` : ""}${input}\n${guide ? import_picocolors.default.cyan(S_BAR_END) : ""}\n`;
|
|
6366
|
-
}
|
|
6367
|
-
}).prompt() ?? CANCEL_SYMBOL;
|
|
7092
|
+
/** A visual example for an empty secret field; never a default value or unmasked input. */
|
|
7093
|
+
function passwordWithPlaceholder(options) {
|
|
7094
|
+
return password(options);
|
|
6368
7095
|
}
|
|
6369
7096
|
//#endregion
|
|
6370
7097
|
//#region src/cli/platform-lifecycle.ts
|
|
@@ -6381,17 +7108,17 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
|
6381
7108
|
}
|
|
6382
7109
|
return explicit || recorded;
|
|
6383
7110
|
}
|
|
6384
|
-
async function runPlatformLifecycleCommand(
|
|
6385
|
-
if (
|
|
6386
|
-
const { runPlatformDomainCommand } = await import("./platform-domain-
|
|
6387
|
-
await runPlatformDomainCommand(
|
|
7111
|
+
async function runPlatformLifecycleCommand(commandArgs) {
|
|
7112
|
+
if (commandArgs.subcommand === "domain") {
|
|
7113
|
+
const { runPlatformDomainCommand } = await import("./platform-domain-Di6EYrQl.mjs");
|
|
7114
|
+
await runPlatformDomainCommand(commandArgs);
|
|
6388
7115
|
return;
|
|
6389
7116
|
}
|
|
6390
|
-
if (
|
|
7117
|
+
if (commandArgs.subcommand === "discover") {
|
|
6391
7118
|
const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
|
|
6392
|
-
const accountId = await selectAccount(await readAccounts(client),
|
|
6393
|
-
const { recoverPlatformInstallations } = await import("./platform-recovery-
|
|
6394
|
-
const recovered = await recoverPlatformInstallations(client, accountId, void 0,
|
|
7119
|
+
const accountId = await selectAccount(await readAccounts(client), commandArgs.account);
|
|
7120
|
+
const { recoverPlatformInstallations } = await import("./platform-recovery-BJnQJEtQ.mjs");
|
|
7121
|
+
const recovered = await recoverPlatformInstallations(client, accountId, void 0, commandArgs.selector);
|
|
6395
7122
|
if (recovered.length === 0) {
|
|
6396
7123
|
log.info(`No recoverable Void platform installations were found in ${accountId}.`);
|
|
6397
7124
|
return;
|
|
@@ -6399,18 +7126,20 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6399
7126
|
for (const manifest of recovered) log.success(`Recovered ${manifest.deployment.displayName} (${manifest.id}) at ${manifest.endpoints.apiUrl}.`);
|
|
6400
7127
|
return;
|
|
6401
7128
|
}
|
|
6402
|
-
if (
|
|
6403
|
-
const { runInstalledPlatformCommand } = await import("./platform-management-
|
|
6404
|
-
await runInstalledPlatformCommand(
|
|
7129
|
+
if (commandArgs.subcommand !== "install") {
|
|
7130
|
+
const { runInstalledPlatformCommand } = await import("./platform-management-DG47jys8.mjs");
|
|
7131
|
+
await runInstalledPlatformCommand(commandArgs);
|
|
6405
7132
|
return;
|
|
6406
7133
|
}
|
|
6407
|
-
|
|
7134
|
+
let args = commandArgs;
|
|
7135
|
+
let runtime = loadPlatformRuntime(args.runtime);
|
|
6408
7136
|
assertNoninteractivePlatformMutationAcknowledged(args, process.stdin.isTTY === true);
|
|
6409
7137
|
console.log();
|
|
6410
7138
|
intro(cliTitle("platform install"));
|
|
6411
7139
|
let existing = args.resume ? resolveInstallationManifest(args.name) : null;
|
|
6412
7140
|
let selectedInstall = false;
|
|
6413
7141
|
let selectedDraft = false;
|
|
7142
|
+
let installationName = args.name;
|
|
6414
7143
|
const { input: installationSlug, credential: operatorCredential } = await resolvePlatformCloudflareCredentialWithInput(async () => {
|
|
6415
7144
|
let requestedName = args.name;
|
|
6416
7145
|
const resumableInstalls = () => listInstallationManifests().filter((manifest) => isPlatformInstallInProgress(manifest) && (!args.account || manifest.account.id === args.account || manifest.account.name.toLowerCase() === args.account.toLowerCase()));
|
|
@@ -6426,21 +7155,42 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6426
7155
|
};
|
|
6427
7156
|
if (!args.resume && process.stdin.isTTY) {
|
|
6428
7157
|
const installs = resumableInstalls().filter((manifest) => !args.name || manifest.deployment.installationSlug === normalizeInstallationSlug(args.name));
|
|
6429
|
-
|
|
7158
|
+
const manifests = listInstallationManifests();
|
|
7159
|
+
const savedPlans = !args.applicationDomain && !args.workersDev && !args.zone && !args.displayName && !args.hyperdrive && !args.controlPlaneDomain && !args.noControlPlaneDomain && !args.authConfig && !args.loginMethods && !args.runtime && args.accessProtection === void 0 && args.appAccess === void 0 && !args.dedicatedZoneSpecified && args.dashboardUrl === void 0 ? listSavedPlatformInstallPlans().filter((saved) => (!args.name || saved.args.name === normalizeInstallationSlug(args.name)) && (!args.account || saved.args.account === args.account || saved.accountName.toLowerCase() === args.account.toLowerCase()) && !manifests.some((manifest) => manifest.status !== "uninstalled" && manifest.account.id === saved.args.account && manifest.deployment.installationSlug === saved.args.name)) : [];
|
|
7160
|
+
if (installs.length > 0 || savedPlans.length > 0) {
|
|
6430
7161
|
const choice = await select({
|
|
6431
7162
|
message: "Continue a previous platform install?",
|
|
6432
|
-
options: [
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
7163
|
+
options: [
|
|
7164
|
+
...installs.map((install) => ({
|
|
7165
|
+
value: install.id,
|
|
7166
|
+
label: `Continue setup for ${install.deployment.displayName} (${install.deployment.installationSlug})`,
|
|
7167
|
+
hint: install.account.name
|
|
7168
|
+
})),
|
|
7169
|
+
...savedPlans.map((saved) => ({
|
|
7170
|
+
value: `plan:${saved.id}`,
|
|
7171
|
+
label: `Use saved plan for ${saved.args.displayName} (${saved.args.name})`,
|
|
7172
|
+
hint: `${saved.accountName} · ${saved.args.workersDev ? "workers.dev" : saved.args.applicationDomain}`
|
|
7173
|
+
})),
|
|
7174
|
+
{
|
|
7175
|
+
value: "new",
|
|
7176
|
+
label: "Start a new platform install"
|
|
7177
|
+
}
|
|
7178
|
+
]
|
|
6440
7179
|
});
|
|
6441
7180
|
if (isCancel(choice) || typeof choice === "symbol") throw new Error("platform: Cancelled.");
|
|
6442
7181
|
if (choice === "new") requestedName = "";
|
|
6443
|
-
else {
|
|
7182
|
+
else if (choice.startsWith("plan:")) {
|
|
7183
|
+
const saved = savedPlans.find((plan) => `plan:${plan.id}` === choice);
|
|
7184
|
+
if (!saved) throw new Error("platform: The selected plan is no longer available.");
|
|
7185
|
+
args = {
|
|
7186
|
+
...saved.args,
|
|
7187
|
+
plan: args.plan,
|
|
7188
|
+
yes: args.yes
|
|
7189
|
+
};
|
|
7190
|
+
runtime = loadPlatformRuntime(args.runtime);
|
|
7191
|
+
installationName = args.name;
|
|
7192
|
+
requestedName = args.name;
|
|
7193
|
+
} else {
|
|
6444
7194
|
const selected = installs.find((install) => install.id === choice);
|
|
6445
7195
|
if (!selected) throw new Error("platform: The selected setup is no longer available.");
|
|
6446
7196
|
selectInstall(selected);
|
|
@@ -6484,6 +7234,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6484
7234
|
continue;
|
|
6485
7235
|
}
|
|
6486
7236
|
assertNewNameAvailable(slug);
|
|
7237
|
+
installationName = name.trim();
|
|
6487
7238
|
return slug;
|
|
6488
7239
|
}
|
|
6489
7240
|
if (existing.endpoints.apiUrl && !readPendingInstallationSecrets(existing)) throw new Error(`platform: Installation ${existing.id} has already been deployed. Use \`void platform repair ${existing.id}\` or \`void platform upgrade ${existing.id}\` instead.`);
|
|
@@ -6512,10 +7263,10 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6512
7263
|
if (isCancel(mode)) throw new Error("platform: Cancelled.");
|
|
6513
7264
|
workersDev = mode === "workers-dev";
|
|
6514
7265
|
}
|
|
6515
|
-
const applicationDomain = recorded?.application.baseDomain ?? (workersDev ? "" : normalizeApplicationDomain(args.applicationDomain || await promptText("Application base domain:", "example.
|
|
7266
|
+
const applicationDomain = recorded?.application.baseDomain ?? (workersDev ? "" : normalizeApplicationDomain(args.applicationDomain || await promptText("Application base domain:", { placeholder: "example.com" })));
|
|
6516
7267
|
const zoneName = recorded?.application.zoneName ?? (workersDev ? "" : normalizeApplicationDomain(args.zone || await promptText("Cloudflare zone:", applicationDomain)));
|
|
6517
|
-
const displayName = recorded?.displayName ?? (args.displayName || await promptText("Platform display name:", `${installationSlug} Void`));
|
|
6518
|
-
|
|
7268
|
+
const displayName = recorded?.displayName ?? (args.displayName || await promptText("Platform display name:", `${installationName || installationSlug} Void`));
|
|
7269
|
+
let controlPlaneDomain = recorded ? recorded.controlPlane.apiHostname ?? "" : args.controlPlaneDomain || (process.stdin.isTTY && !args.noControlPlaneDomain ? await promptText("Where do you want to host the Void Platform API? Leave empty for a workers.dev fallback.", { placeholder: applicationDomain || "platform.example.com" }, true) : "");
|
|
6519
7270
|
let dedicatedZone = recorded?.application.dedicatedZone ?? args.dedicatedZone;
|
|
6520
7271
|
if (!workersDev && !recorded && !args.dedicatedZone && !args.dedicatedZoneSpecified && applicationDomain === zoneName && process.stdin.isTTY) {
|
|
6521
7272
|
const catchAll = await confirm({
|
|
@@ -6542,6 +7293,26 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6542
7293
|
}
|
|
6543
7294
|
const checkpoint = existing ? readPendingInstallationSecrets(existing) : null;
|
|
6544
7295
|
const databaseUrl = resolvePlatformInstallDatabaseUrl(checkpoint, existing);
|
|
7296
|
+
let authoritativeProvisionIntents = false;
|
|
7297
|
+
if (existing && !isPlatformSetupDraft(existing)) {
|
|
7298
|
+
if (!databaseUrl) throw new Error("platform: DATABASE_URL is required to verify an interrupted installation before resuming it.");
|
|
7299
|
+
progress = startProgress("Reading the saved installation");
|
|
7300
|
+
const authoritative = await readPlatformDatabaseLifecycleManifest(databaseUrl, existing.id, existing.ownershipNonce);
|
|
7301
|
+
progress?.stop("Read the saved installation.");
|
|
7302
|
+
progress = void 0;
|
|
7303
|
+
if (authoritative) {
|
|
7304
|
+
const { selectAuthoritativePlatformManifest } = await import("./platform-management-DG47jys8.mjs");
|
|
7305
|
+
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
7306
|
+
authoritativeProvisionIntents = true;
|
|
7307
|
+
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
7308
|
+
existing = selected;
|
|
7309
|
+
if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
const savedHyperdrive = readPlatformHyperdriveSelection(checkpoint);
|
|
7313
|
+
const requestedHyperdriveId = args.hyperdrive || savedHyperdrive?.id;
|
|
7314
|
+
const recordedHyperdrive = existing?.resources.find((resource) => resource.key === "hyperdrive");
|
|
7315
|
+
if (savedHyperdrive && requestedHyperdriveId !== savedHyperdrive.id || recordedHyperdrive && requestedHyperdriveId && (requestedHyperdriveId === "create" ? recordedHyperdrive.ownership === "adopted" : recordedHyperdrive.id !== requestedHyperdriveId) || savedHyperdrive && recordedHyperdrive && (recordedHyperdrive.ownership !== "adopted" || recordedHyperdrive.id !== savedHyperdrive.id)) throw new Error("platform: The selected Hyperdrive differs from the saved installation. Resume with its original Hyperdrive.");
|
|
6545
7316
|
const externalHyperdriveId = process.env.VOID_PLATFORM_HYPERDRIVE_ID?.trim() || "";
|
|
6546
7317
|
const externalHyperdriveHost = process.env.VOID_PLATFORM_HYPERDRIVE_ORIGIN_HOST?.trim() || "";
|
|
6547
7318
|
const externalHyperdriveUser = process.env.VOID_PLATFORM_HYPERDRIVE_ORIGIN_USER?.trim() || "";
|
|
@@ -6559,12 +7330,14 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6559
7330
|
host: externalHyperdriveHost,
|
|
6560
7331
|
user: externalHyperdriveUser
|
|
6561
7332
|
} : void 0;
|
|
6562
|
-
if (
|
|
6563
|
-
if (
|
|
7333
|
+
if (requestedHyperdriveId && externalHyperdrive && requestedHyperdriveId !== externalHyperdrive.id) throw new Error("platform: --hyperdrive conflicts with VOID_PLATFORM_HYPERDRIVE_ID.");
|
|
7334
|
+
if (savedHyperdrive && externalHyperdrive && (savedHyperdrive.host.toLowerCase() !== externalHyperdrive.host.toLowerCase() || savedHyperdrive.user !== externalHyperdrive.user)) throw new Error("platform: The Hyperdrive origin differs from the saved installation.");
|
|
7335
|
+
if (savedHyperdrive) externalHyperdrive = recordedHyperdrive ? savedHyperdrive : void 0;
|
|
7336
|
+
if (!externalHyperdrive && requestedHyperdriveId !== "create" && (process.stdin.isTTY || requestedHyperdriveId) && (!existing || isPlatformSetupDraft(existing) || requestedHyperdriveId) && !recordedHyperdrive) {
|
|
6564
7337
|
const suggestedName = existing?.deployment.resources.hyperdrive ?? createPlatformResourceNames(installationSlug).hyperdrive;
|
|
6565
7338
|
const configurations = await client.listHyperdriveConfigs(accountId);
|
|
6566
7339
|
const suggestedNameAvailable = !configurations.some((resource) => resource.name === suggestedName);
|
|
6567
|
-
const choice =
|
|
7340
|
+
const choice = requestedHyperdriveId ? "existing" : await select({
|
|
6568
7341
|
message: "How should Void set up Hyperdrive?",
|
|
6569
7342
|
options: [
|
|
6570
7343
|
...configurations.length > 0 ? [{
|
|
@@ -6584,12 +7357,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6584
7357
|
});
|
|
6585
7358
|
if (isCancel(choice)) throw new Error("platform: Cancelled.");
|
|
6586
7359
|
if (choice === "setup") {
|
|
6587
|
-
note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? ` such as ${suggestedName}` : ""}. Point it at the same dedicated PostgreSQL database you will give Void, use a runtime database user, and disable SQL result caching. If your organization manages Hyperdrive, ask its administrator to create it. Then rerun this command and select it from the list.\nSetup: https://developers.cloudflare.com/hyperdrive/get-started/`, "Set up Hyperdrive first");
|
|
7360
|
+
note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? ` such as ${suggestedName}` : ""}. Point it at the same dedicated PostgreSQL database you will give Void, use a runtime database user, and disable SQL result caching. If your organization manages Hyperdrive, ask its administrator to create it. Then rerun this command and select it from the list.\nSetup: https://developers.cloudflare.com/hyperdrive/get-started/`, import_picocolors.default.bold("Set up Hyperdrive first"));
|
|
6588
7361
|
outro("No Cloudflare or local installation state was changed.");
|
|
6589
7362
|
return;
|
|
6590
7363
|
}
|
|
6591
7364
|
if (choice === "existing") {
|
|
6592
|
-
const selectedId =
|
|
7365
|
+
const selectedId = requestedHyperdriveId ?? await select({
|
|
6593
7366
|
message: "Which Hyperdrive should Void use?",
|
|
6594
7367
|
options: [...configurations].sort((a, b) => Number(b.name === suggestedName) - Number(a.name === suggestedName) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((resource) => ({
|
|
6595
7368
|
value: resource.id,
|
|
@@ -6602,39 +7375,33 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6602
7375
|
if (!selected) throw new Error("platform: The selected Hyperdrive is no longer available.");
|
|
6603
7376
|
const live = await client.getHyperdriveConfig(accountId, selected.id);
|
|
6604
7377
|
if (live.id !== selected.id || live.name !== selected.name || !live.origin.user) throw new Error(`platform: Could not verify the database user for existing Hyperdrive ${selected.name}.`);
|
|
6605
|
-
|
|
7378
|
+
if (savedHyperdrive && (live.origin.host !== savedHyperdrive.host.toLowerCase() || live.origin.user !== savedHyperdrive.user || savedHyperdrive.name && live.name !== savedHyperdrive.name || savedHyperdrive.database && live.origin.database !== savedHyperdrive.database) || existing && !isPlatformSetupDraft(existing) && live.name !== existing.deployment.resources.hyperdrive) throw new Error("platform: The Hyperdrive origin or name differs from the saved installation.");
|
|
7379
|
+
if (!requestedHyperdriveId) note([
|
|
7380
|
+
`${import_picocolors.default.bold("Database:")} ${live.origin.database}`,
|
|
7381
|
+
`${import_picocolors.default.bold("Host:")} ${live.origin.host}`,
|
|
7382
|
+
`${import_picocolors.default.bold("Port:")} ${live.origin.port ?? "configured by Cloudflare Tunnel"}`,
|
|
7383
|
+
`${import_picocolors.default.bold("User:")} ${live.origin.user}`,
|
|
7384
|
+
`${import_picocolors.default.bold("SQL result caching:")} ${live.cachingDisabled ? "disabled" : "enabled"}`
|
|
7385
|
+
].join("\n"), import_picocolors.default.bold(`Existing Hyperdrive ${live.name}`));
|
|
6606
7386
|
if (!live.cachingDisabled) throw new Error(`platform: Disable SQL result caching on existing Hyperdrive ${live.name} before installing.`);
|
|
6607
|
-
const confirmed = args.hyperdrive ? true : await confirm({
|
|
6608
|
-
message: `Use ${live.name} for this platform?`,
|
|
6609
|
-
initialValue: true
|
|
6610
|
-
});
|
|
6611
|
-
if (isCancel(confirmed) || !confirmed) throw new Error("platform: Cancelled.");
|
|
6612
7387
|
externalHyperdrive = {
|
|
6613
7388
|
id: live.id,
|
|
6614
7389
|
name: live.name,
|
|
7390
|
+
database: live.origin.database,
|
|
6615
7391
|
host: live.origin.host,
|
|
6616
7392
|
user: live.origin.user
|
|
6617
7393
|
};
|
|
6618
7394
|
}
|
|
6619
7395
|
}
|
|
6620
|
-
let authoritativeProvisionIntents = false;
|
|
6621
|
-
if (existing && !isPlatformSetupDraft(existing)) {
|
|
6622
|
-
if (!databaseUrl) throw new Error("platform: DATABASE_URL is required to verify an interrupted installation before resuming it.");
|
|
6623
|
-
progress = startProgress("Reading the saved installation");
|
|
6624
|
-
const authoritative = await readPlatformDatabaseLifecycleManifest(databaseUrl, existing.id, existing.ownershipNonce);
|
|
6625
|
-
progress?.stop("Read the saved installation.");
|
|
6626
|
-
progress = void 0;
|
|
6627
|
-
if (authoritative) {
|
|
6628
|
-
const { selectAuthoritativePlatformManifest } = await import("./platform-management-CnyTdcWX.mjs");
|
|
6629
|
-
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
6630
|
-
authoritativeProvisionIntents = true;
|
|
6631
|
-
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
6632
|
-
existing = selected;
|
|
6633
|
-
if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
|
|
6634
|
-
}
|
|
6635
|
-
}
|
|
6636
7396
|
const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication, args);
|
|
7397
|
+
const appAccess = await selectInstallationAppAccess(authenticationSelection, existing?.deployment, args.appAccess);
|
|
7398
|
+
while (!existing && process.stdin.isTTY && !args.controlPlaneDomain && appAccess && appAccess !== "public" && controlPlaneDomain.toLowerCase().replace(/\.$/, "").endsWith(`.${applicationDomain.toLowerCase()}`)) {
|
|
7399
|
+
log.info("App Access covers subdomains. Use the domain apex or workers.dev for the platform API.");
|
|
7400
|
+
controlPlaneDomain = await promptText("Platform API hostname. Leave empty for workers.dev:", { placeholder: applicationDomain }, true);
|
|
7401
|
+
}
|
|
7402
|
+
if (appAccess && runtime.manifest.appAccessSetup !== 1) throw new Error("platform: This runtime does not support guided app access. Use the current packaged runtime.");
|
|
6637
7403
|
const planInput = {
|
|
7404
|
+
appAccess,
|
|
6638
7405
|
authentication: authenticationSelection.authentication,
|
|
6639
7406
|
accountId,
|
|
6640
7407
|
applicationDomain,
|
|
@@ -6649,53 +7416,118 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6649
7416
|
zoneName,
|
|
6650
7417
|
...workersDev ? { workersDev: true } : {}
|
|
6651
7418
|
};
|
|
6652
|
-
|
|
7419
|
+
const planOptions = {
|
|
6653
7420
|
authoritativeProvisionIntents,
|
|
6654
7421
|
allowUnverifiedAiGateway: true,
|
|
6655
7422
|
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
6656
7423
|
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3,
|
|
6657
7424
|
deploymentExecution: runtime.manifest.deploymentExecution === 1
|
|
6658
|
-
}
|
|
6659
|
-
|
|
6660
|
-
|
|
7425
|
+
};
|
|
7426
|
+
let plan = await createPlatformInstallPlan(client, planInput, existing, planOptions);
|
|
7427
|
+
const commandOptions = {
|
|
7428
|
+
commandName: cliCommandName(),
|
|
7429
|
+
accountId,
|
|
7430
|
+
installationSlug,
|
|
7431
|
+
displayName,
|
|
7432
|
+
applicationDomain,
|
|
7433
|
+
zoneName,
|
|
7434
|
+
controlPlaneDomain,
|
|
7435
|
+
...args.dashboardUrl !== void 0 ? { dashboardUrl: args.dashboardUrl } : {},
|
|
7436
|
+
dedicatedZone,
|
|
7437
|
+
workersDev,
|
|
7438
|
+
authConfig: args.authConfig,
|
|
7439
|
+
appAccess,
|
|
7440
|
+
authentication: authenticationSelection.authentication,
|
|
7441
|
+
hyperdrive: !existing || isPlatformSetupDraft(existing) || externalHyperdrive && !recordedHyperdrive ? externalHyperdrive?.id ?? "create" : void 0,
|
|
7442
|
+
runtime: args.runtime,
|
|
7443
|
+
...existing ? { resumeId: existing.id } : {},
|
|
7444
|
+
yes: args.yes || process.stdin.isTTY !== true
|
|
7445
|
+
};
|
|
7446
|
+
if (!existing) savePlatformInstallChoices(platformInstallCommandArguments({
|
|
7447
|
+
...commandOptions,
|
|
7448
|
+
runtime: args.runtime ? resolve(args.runtime) : "",
|
|
7449
|
+
...args.authConfig ? { authConfig: resolve(args.authConfig) } : {},
|
|
7450
|
+
yes: false
|
|
7451
|
+
}).slice(2), plan.account.name);
|
|
7452
|
+
note(formatPlatformInstallPlan(plan), import_picocolors.default.bold(args.plan ? "Read-only installation plan" : "Installation plan"));
|
|
7453
|
+
note(formatPlatformRuntime(runtime), import_picocolors.default.bold("Platform runtime"));
|
|
7454
|
+
const managementAccess = process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN ? void 0 : platformManagementAccessForAuthentication(authenticationSelection, accountId, checkpoint);
|
|
7455
|
+
const needsManagementToken = platformOperatorNeedsManagementToken(operatorCredential, plan, managementAccess);
|
|
6661
7456
|
if (args.plan) {
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
applicationDomain,
|
|
6670
|
-
zoneName,
|
|
6671
|
-
controlPlaneDomain,
|
|
6672
|
-
...args.dashboardUrl !== void 0 ? { dashboardUrl: args.dashboardUrl } : {},
|
|
6673
|
-
dedicatedZone,
|
|
6674
|
-
workersDev,
|
|
6675
|
-
authConfig: args.authConfig,
|
|
6676
|
-
authentication: authenticationSelection.authentication,
|
|
6677
|
-
hyperdrive: !existing || isPlatformSetupDraft(existing) ? externalHyperdrive?.id ?? "create" : void 0,
|
|
6678
|
-
runtime: args.runtime,
|
|
6679
|
-
...existing ? { resumeId: existing.id } : {},
|
|
6680
|
-
yes: args.yes || process.stdin.isTTY !== true
|
|
6681
|
-
}))}\n`);
|
|
7457
|
+
note(formatPlatformSetupPlan(plan, {
|
|
7458
|
+
managementToken: needsManagementToken,
|
|
7459
|
+
managementAccess: !!managementAccess
|
|
7460
|
+
}), import_picocolors.default.bold("Required Credentials"));
|
|
7461
|
+
if (!existing) log.info("Plan saved locally for your next install.");
|
|
7462
|
+
outro("Plan complete and ready to execute.");
|
|
7463
|
+
console.log(`\n${import_picocolors.default.bold(existing ? "Next step: run this command to resume" : "Next, run this command to install the platform")}\n\n${import_picocolors.default.blue(formatPlatformInstallCommand(commandOptions))}\n`);
|
|
6682
7464
|
return;
|
|
6683
7465
|
}
|
|
6684
|
-
assertPlatformOperatorCredentialCanApplyPlan(operatorCredential, plan);
|
|
7466
|
+
if (!process.stdin.isTTY) assertPlatformOperatorCredentialCanApplyPlan(operatorCredential, plan, managementAccess);
|
|
6685
7467
|
if (process.stdin.isTTY && !args.yes) {
|
|
6686
7468
|
const proceed = await confirm({
|
|
6687
7469
|
message: "Apply this plan?",
|
|
6688
|
-
initialValue:
|
|
7470
|
+
initialValue: true
|
|
6689
7471
|
});
|
|
6690
7472
|
if (isCancel(proceed) || !proceed) {
|
|
6691
7473
|
outro("No changes made.");
|
|
6692
7474
|
return;
|
|
6693
7475
|
}
|
|
6694
7476
|
}
|
|
7477
|
+
let managementApiToken = operatorCredential.source === "oauth" ? checkpoint?.managementApiToken || "" : "";
|
|
7478
|
+
if (needsManagementToken || managementApiToken) {
|
|
7479
|
+
let token = managementApiToken;
|
|
7480
|
+
let openedManagementPage = false;
|
|
7481
|
+
const forgetRejectedManagementToken = () => {
|
|
7482
|
+
managementApiToken = "";
|
|
7483
|
+
if (existing && checkpoint?.managementApiToken) {
|
|
7484
|
+
delete checkpoint.managementApiToken;
|
|
7485
|
+
writePendingInstallationSecrets(existing, checkpoint);
|
|
7486
|
+
}
|
|
7487
|
+
token = "";
|
|
7488
|
+
};
|
|
7489
|
+
while (true) {
|
|
7490
|
+
if (!token) {
|
|
7491
|
+
if (!openedManagementPage) {
|
|
7492
|
+
openPlatformSetupPage(platformManagementSetupPage(plan, { access: managementAccess ?? false }));
|
|
7493
|
+
openedManagementPage = true;
|
|
7494
|
+
}
|
|
7495
|
+
token = (await promptSecret("Paste management API token:", true, (value) => value.trim() ? void 0 : "A value is required.")).trim();
|
|
7496
|
+
}
|
|
7497
|
+
const management = new PlatformCloudflareClient(token, { apiBase: client.apiBaseUrl });
|
|
7498
|
+
try {
|
|
7499
|
+
if (!(await management.listAccounts()).some((account) => account.id === accountId)) {
|
|
7500
|
+
forgetRejectedManagementToken();
|
|
7501
|
+
log.warn(`This token cannot access ${plan.account.name}. Check its account scope and try again.`);
|
|
7502
|
+
continue;
|
|
7503
|
+
}
|
|
7504
|
+
const verified = await createPlatformInstallPlan(management, planInput, existing, planOptions);
|
|
7505
|
+
assertPlatformInstallPlanUnchanged(plan, verified, "management API token");
|
|
7506
|
+
if (managementAccess) await verifyPlatformManagementAccess(management, accountId, managementAccess);
|
|
7507
|
+
client = management;
|
|
7508
|
+
plan = verified;
|
|
7509
|
+
managementApiToken = token;
|
|
7510
|
+
break;
|
|
7511
|
+
} catch (error) {
|
|
7512
|
+
if (!isPlatformTokenAuthorizationError(error)) throw error;
|
|
7513
|
+
forgetRejectedManagementToken();
|
|
7514
|
+
log.warn("Cloudflare rejected this token. Check its permissions and account/zone scope, then paste a replacement.");
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
} else if (managementAccess) await verifyPlatformManagementAccess(client, accountId, managementAccess);
|
|
6695
7518
|
if (!existing) existing = savePlatformSetupDraft(plan, runtime.manifest.platformVersion);
|
|
7519
|
+
removeSavedPlatformInstallPlan({
|
|
7520
|
+
...args,
|
|
7521
|
+
account: accountId,
|
|
7522
|
+
name: installationSlug
|
|
7523
|
+
});
|
|
6696
7524
|
const setupManifest = existing;
|
|
6697
7525
|
const credentialSetup = isPlatformSetupDraft(existing);
|
|
6698
7526
|
const values = { ...checkpoint };
|
|
7527
|
+
if (managementAccess) delete values.accessManagementToken;
|
|
7528
|
+
if (managementApiToken) values.managementApiToken = managementApiToken;
|
|
7529
|
+
if (externalHyperdrive) values.externalHyperdriveJson = JSON.stringify(externalHyperdrive);
|
|
7530
|
+
if (managementApiToken || externalHyperdrive || managementAccess) writePendingInstallationSecrets(setupManifest, values);
|
|
6699
7531
|
const saveValues = (updates) => {
|
|
6700
7532
|
Object.assign(values, updates);
|
|
6701
7533
|
if (credentialSetup) writePendingInstallationSecrets(setupManifest, values);
|
|
@@ -6733,7 +7565,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6733
7565
|
plan = await verifyPlatformAiGatewayPlan(client, planInput, existing, plan, authoritativeProvisionIntents);
|
|
6734
7566
|
}
|
|
6735
7567
|
} catch (error) {
|
|
6736
|
-
if (credentialSetup && token === values.cloudflareApiToken &&
|
|
7568
|
+
if (credentialSetup && token === values.cloudflareApiToken && isPlatformTokenAuthorizationError(error, checkingRuntimePermissions ? void 0 : "/ai-gateway/")) {
|
|
6737
7569
|
delete values.cloudflareApiToken;
|
|
6738
7570
|
writePendingInstallationSecrets(setupManifest, values);
|
|
6739
7571
|
rejectedSavedRuntimeToken = true;
|
|
@@ -6850,6 +7682,20 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6850
7682
|
accessCredentialsJson: values.accessCredentialsJson
|
|
6851
7683
|
};
|
|
6852
7684
|
} } : {},
|
|
7685
|
+
...appAccess ? { prepareAppAccess: async () => {
|
|
7686
|
+
progress?.stop("Preparing deployed-app access.");
|
|
7687
|
+
progress = void 0;
|
|
7688
|
+
const setupJson = await prepareInstallationAppAccess({
|
|
7689
|
+
plan,
|
|
7690
|
+
authentication: authenticationSelection,
|
|
7691
|
+
client,
|
|
7692
|
+
managementToken: values.managementApiToken || (operatorCredential.source !== "oauth" ? operatorCredential.token : void 0),
|
|
7693
|
+
checkpoint: values,
|
|
7694
|
+
save: saveAuthenticationValues
|
|
7695
|
+
});
|
|
7696
|
+
progress = startProgress("Continuing platform installation");
|
|
7697
|
+
return setupJson;
|
|
7698
|
+
} } : {},
|
|
6853
7699
|
replanWorkers: async (manifest) => (await createPlatformInstallPlan(client, planInput, manifest, {
|
|
6854
7700
|
authoritativeProvisionIntents: true,
|
|
6855
7701
|
repairMissing: true,
|
|
@@ -6859,7 +7705,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6859
7705
|
})).actions.filter((action) => action.kind === "worker"),
|
|
6860
7706
|
revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
|
|
6861
7707
|
verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
|
|
6862
|
-
const { verifyPlatformInstallationOwnership } = await import("./platform-management-
|
|
7708
|
+
const { verifyPlatformInstallationOwnership } = await import("./platform-management-DG47jys8.mjs");
|
|
6863
7709
|
const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
|
|
6864
7710
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
6865
7711
|
return action.action === "create" && previous && previous.name !== action.name;
|
|
@@ -6874,12 +7720,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6874
7720
|
progress?.stop(result.waitingForDns || result.waitingForSandboxDrain ? "Installation paused. Progress is saved." : "Platform installed.");
|
|
6875
7721
|
progress = void 0;
|
|
6876
7722
|
if (result.waitingForDns) {
|
|
6877
|
-
note(result.zoneNameServers.length > 0 ? `Update the domain at your registrar to use:\n${result.zoneNameServers.join("\n")}\n\nThen rerun: void platform install --resume --name ${result.manifest.id}` : `Wait for ${zoneName} to become active, then rerun:\nvoid platform install --resume --name ${result.manifest.id}`, "Waiting for nameservers");
|
|
7723
|
+
note(result.zoneNameServers.length > 0 ? `Update the domain at your registrar to use:\n${result.zoneNameServers.join("\n")}\n\nThen rerun: void platform install --resume --name ${result.manifest.id}` : `Wait for ${zoneName} to become active, then rerun:\nvoid platform install --resume --name ${result.manifest.id}`, import_picocolors.default.bold("Waiting for nameservers"));
|
|
6878
7724
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
6879
7725
|
return;
|
|
6880
7726
|
}
|
|
6881
7727
|
if (result.waitingForSandboxDrain) {
|
|
6882
|
-
note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan`. Follow any returned `nextCursor` with `--cursor '<value>'`. Then apply with `void platform system sandbox-drain --yes` and rerun until it reports DB-backed `complete: true`. Rerun this install command afterward to enable traffic.", "Waiting for managed Sandbox drain");
|
|
7728
|
+
note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan`. Follow any returned `nextCursor` with `--cursor '<value>'`. Then apply with `void platform system sandbox-drain --yes` and rerun until it reports DB-backed `complete: true`. Rerun this install command afterward to enable traffic.", import_picocolors.default.bold("Waiting for managed Sandbox drain"));
|
|
6883
7729
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
6884
7730
|
return;
|
|
6885
7731
|
}
|
|
@@ -6907,16 +7753,16 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6907
7753
|
}
|
|
6908
7754
|
}
|
|
6909
7755
|
}
|
|
6910
|
-
function
|
|
7756
|
+
function isPlatformTokenAuthorizationError(error, requestPathIncludes) {
|
|
6911
7757
|
for (let depth = 0; depth < 5 && error instanceof Error; depth++, error = error.cause) if (error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status) && (!requestPathIncludes || error.requestPath?.includes(requestPathIncludes))) return true;
|
|
6912
7758
|
return false;
|
|
6913
7759
|
}
|
|
6914
7760
|
function formatPlatformRuntime(runtime) {
|
|
6915
7761
|
return [
|
|
6916
|
-
`${import_picocolors
|
|
6917
|
-
`${import_picocolors
|
|
6918
|
-
`${import_picocolors
|
|
6919
|
-
`${import_picocolors
|
|
7762
|
+
`${import_picocolors.default.bold("Version:")} ${runtime.manifest.platformVersion}`,
|
|
7763
|
+
`${import_picocolors.default.bold("Source:")} ${runtime.source}`,
|
|
7764
|
+
`${import_picocolors.default.bold("Revision:")} ${runtime.manifest.sourceRevision ?? "not recorded"}`,
|
|
7765
|
+
`${import_picocolors.default.bold("Manifest SHA-256:")} ${runtime.manifestSha256}`
|
|
6920
7766
|
].join("\n");
|
|
6921
7767
|
}
|
|
6922
7768
|
async function verifyPlatformAiGatewayPlan(client, input, existing, preview, authoritativeProvisionIntents) {
|
|
@@ -6932,6 +7778,10 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
|
|
|
6932
7778
|
if (error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403) && error.requestPath?.includes("/ai-gateway/")) throw new Error("platform: The runtime token could not verify AI Gateway access. Include Account → AI Gateway → Edit for this account, then retry. No platform resources were created.", { cause: error });
|
|
6933
7779
|
throw error;
|
|
6934
7780
|
}
|
|
7781
|
+
assertPlatformInstallPlanUnchanged(preview, verified, "AI Gateway access");
|
|
7782
|
+
return verified;
|
|
7783
|
+
}
|
|
7784
|
+
function assertPlatformInstallPlanUnchanged(preview, verified, check) {
|
|
6935
7785
|
const infrastructure = (plan) => plan.actions.filter((action) => action.kind !== "ai-gateway").map(({ action, id, key, kind, name }) => ({
|
|
6936
7786
|
action,
|
|
6937
7787
|
id,
|
|
@@ -6939,8 +7789,7 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
|
|
|
6939
7789
|
kind,
|
|
6940
7790
|
name
|
|
6941
7791
|
}));
|
|
6942
|
-
if (verified.installationId !== preview.installationId || JSON.stringify(verified.deployment) !== JSON.stringify(preview.deployment) || JSON.stringify(infrastructure(verified)) !== JSON.stringify(infrastructure(preview))) throw new Error(
|
|
6943
|
-
return verified;
|
|
7792
|
+
if (verified.installationId !== preview.installationId || JSON.stringify(verified.deployment) !== JSON.stringify(preview.deployment) || JSON.stringify(infrastructure(verified)) !== JSON.stringify(infrastructure(preview))) throw new Error(`platform: Resources changed while checking ${check}. Rerun the command to review the updated plan. No platform resources were created.`);
|
|
6944
7793
|
}
|
|
6945
7794
|
function assertNoninteractivePlatformMutationAcknowledged(args, interactive) {
|
|
6946
7795
|
if (args.subcommand === "list" || args.subcommand === "use" || args.subcommand === "status" || args.subcommand === "discover" || args.plan || interactive || args.yes) return;
|
|
@@ -6954,7 +7803,8 @@ async function readAccounts(client) {
|
|
|
6954
7803
|
async function selectAccount(accounts, selector) {
|
|
6955
7804
|
if (selector) {
|
|
6956
7805
|
const matches = accounts.filter((account) => account.id === selector || account.name.toLowerCase() === selector.toLowerCase());
|
|
6957
|
-
if (matches.length
|
|
7806
|
+
if (matches.length === 0) throw new Error(`platform: Cloudflare account ${JSON.stringify(selector)} is unavailable through the current access token. Run ${import_picocolors.default.blue("void cloudflare status")} to see which accounts have access, and re-authenticate if necessary.`);
|
|
7807
|
+
if (matches.length > 1) throw new Error(`platform: Cloudflare account ${JSON.stringify(selector)} is ambiguous. Use --account <id> to select a specific account.`);
|
|
6958
7808
|
return matches[0].id;
|
|
6959
7809
|
}
|
|
6960
7810
|
if (accounts.length === 1) return accounts[0].id;
|
|
@@ -6975,10 +7825,17 @@ async function promptText(message, value, optional = false, validate) {
|
|
|
6975
7825
|
if (optional) return "";
|
|
6976
7826
|
throw new Error(`platform: ${message.replace(/:$/, "")} is required in non-interactive mode.`);
|
|
6977
7827
|
}
|
|
7828
|
+
const defaultValue = typeof value === "string" ? value : void 0;
|
|
6978
7829
|
const input = await text$1({
|
|
6979
7830
|
message,
|
|
6980
|
-
...typeof value === "string" ? {
|
|
6981
|
-
|
|
7831
|
+
...typeof value === "string" ? {
|
|
7832
|
+
placeholder: value,
|
|
7833
|
+
defaultValue
|
|
7834
|
+
} : value,
|
|
7835
|
+
validate: (input) => {
|
|
7836
|
+
const candidate = input || defaultValue;
|
|
7837
|
+
return candidate ? validate?.(candidate) : optional ? void 0 : "A value is required.";
|
|
7838
|
+
}
|
|
6982
7839
|
});
|
|
6983
7840
|
if (isCancel(input) || typeof input === "symbol") throw new Error("platform: Cancelled.");
|
|
6984
7841
|
return input;
|