void 0.20.1 → 0.20.3
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/README.md +5 -1
- package/dist/{auth-W9WII-mN.mjs → auth-DPl6kck4.mjs} +46 -26
- package/dist/{auth-cmd-CAH62yDU.mjs → auth-cmd-CzwquNiP.mjs} +5 -4
- package/dist/auth-link-ElDTgF7j.mjs +28 -0
- package/dist/{build-cmd-CJvZvPQO.mjs → build-cmd-BpJe6boP.mjs} +3 -3
- package/dist/{cache-BlNeQjuP.mjs → cache-D98YTqeE.mjs} +3 -3
- package/dist/{cancel-deploy-CmlAZ9P6.mjs → cancel-deploy-CPbEQLMG.mjs} +3 -3
- package/dist/cf-access-DRsQRe6k.mjs +75 -0
- package/dist/cli/cli.mjs +309 -1958
- package/dist/cli/env-schema-probe.mjs +11 -2
- package/dist/client-BQBrZoCX.mjs +989 -0
- package/dist/{cloudflare-auth-B1QtTO1b.mjs → cloudflare-auth-6M5llVPC.mjs} +2 -2
- package/dist/{cloudflare-cmd-B6_OZx2V.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
- package/dist/{cloudflare-connect-j5D4hhrG.mjs → cloudflare-connect-t1UU5svD.mjs} +2 -2
- package/dist/{cloudflare-operations-CPTpRW6d.mjs → cloudflare-operations-BzWnlC1_.mjs} +1 -1
- package/dist/{config-BQFq7QvD.mjs → config-uNGuFsI2.mjs} +1 -1
- package/dist/{connect-C04Wdy_h.mjs → connect-WCQZ_u3m.mjs} +6 -6
- package/dist/{create-project-ChGZ1DFd.mjs → create-project-D0oXA090.mjs} +7 -7
- package/dist/{db-D2d_mUsB.mjs → db-DJ-9qs3S.mjs} +47 -30
- package/dist/{delete-D8GigDk8.mjs → delete-BZ4-WaGm.mjs} +3 -3
- package/dist/{deploy-iXZ3F0N6.mjs → deploy-BAhhcg5q.mjs} +135 -117
- package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
- package/dist/{domain-B1VmoSr0.mjs → domain-BxAyhxXN.mjs} +4 -4
- package/dist/email-Bj7Cvdwp.mjs +795 -0
- package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
- package/dist/{env-BcQzYgoG.mjs → env-DP_EErve.mjs} +5 -5
- package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
- package/dist/{gen-DI2YwdBM.mjs → gen-B_wPnVTK.mjs} +2 -2
- package/dist/{github-cmd-xItS5Zwf.mjs → github-cmd-C-z_xRrQ.mjs} +15 -21
- package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
- package/dist/help-CwOX-zmI.mjs +2216 -0
- package/dist/{inbound-afAcWeQ9.d.mts → inbound-CH5Mksyy.d.mts} +34 -48
- package/dist/{inbound-2d0zi2yS.mjs → inbound-aVHEUhKo.mjs} +130 -100
- package/dist/index.mjs +54 -17
- package/dist/{init-BD-9THgn.mjs → init-BGktCXgA.mjs} +11 -11
- package/dist/{link-RMdgjF1v.mjs → link-D2kbqhWb.mjs} +4 -4
- package/dist/{list-3F52R_yO.mjs → list-CvkK_G7k.mjs} +4 -4
- package/dist/{login-pV69H-ZO.mjs → login-WIjNc77c.mjs} +28 -10
- package/dist/{logs-DFHHD6wE.mjs → logs-dLUFCapG.mjs} +4 -4
- package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
- package/dist/{node-Dk3H2jmU.mjs → node-Ez5KW5rn.mjs} +2 -2
- package/dist/operator-auth-B3e08unv.mjs +52 -0
- package/dist/operator-client-LUZnlnYk.mjs +82 -0
- package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CKJ7xIRs.mjs} +35 -55
- package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
- package/dist/pages/index.mjs +2 -2
- package/dist/platform-auth-config-CdVWRRJr.mjs +368 -0
- package/dist/platform-auth-protection-Drl0qhrn.mjs +219 -0
- package/dist/platform-auth-recovery-CmKEWpDo.mjs +310 -0
- package/dist/{platform-cmd-DxJ2FRwR.mjs → platform-cmd-5q_k56xS.mjs} +16 -6
- package/dist/{platform-domain-ChvbJkdy.mjs → platform-domain-4GiDlcqx.mjs} +4 -4
- package/dist/{platform-lifecycle-DN4MzJF_.mjs → platform-lifecycle-R9xAxvtG.mjs} +1716 -204
- package/dist/{platform-management-Db2PXw0B.mjs → platform-management-BfWsXHEW.mjs} +35 -7
- package/dist/{platform-recovery-C_YO-tIs.mjs → platform-recovery-CbK-I1FB.mjs} +6 -5
- package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
- package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
- package/dist/prerender-render.d.mts +11 -0
- package/dist/prerender-render.mjs +111 -0
- package/dist/{project-cmd-Mo0V9yKS.mjs → project-cmd-CTdmnzvc.mjs} +32 -14
- package/dist/project-team-CGxsQe3_.mjs +132 -0
- package/dist/project-token-Cirx7uwZ.mjs +75 -0
- package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
- package/dist/{requests-BcKOVpRg.mjs → requests-4Nq59hOr.mjs} +3 -3
- package/dist/{rollback-Bx85-0xh.mjs → rollback-Dr7u0Ljx.mjs} +4 -4
- package/dist/runtime/ai.mjs +3 -2
- package/dist/runtime/email/testing.d.mts +1 -1
- package/dist/runtime/email/testing.mjs +3 -3
- package/dist/runtime/email-protocol.d.mts +15 -0
- package/dist/runtime/email-protocol.mjs +70 -0
- package/dist/runtime/email.d.mts +2 -2
- package/dist/runtime/email.mjs +189 -96
- package/dist/runtime/remote/index.mjs +54 -11
- package/dist/runtime/sandbox.d.mts +4 -56
- package/dist/runtime/sandbox.mjs +81 -220
- package/dist/{secret-ByhJ9AMl.mjs → secret-AcPi-FoA.mjs} +5 -5
- package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
- package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
- package/package.json +12 -7
- package/skills/void/SKILL.md +35 -4
- package/skills/void/docs/guide/deployment.md +16 -14
- package/skills/void/docs/guide/email.md +114 -120
- package/skills/void/docs/guide/platform/administration/access.md +163 -0
- package/skills/void/docs/guide/platform/administration/email.md +121 -0
- package/skills/void/docs/guide/platform/administration/operations.md +97 -0
- package/skills/void/docs/guide/platform/administration/projects.md +54 -0
- package/skills/void/docs/guide/platform/development/local.md +119 -0
- package/skills/void/docs/guide/platform/development/runtime.md +124 -0
- package/skills/void/docs/guide/platform/development/schema-ci.md +95 -0
- package/skills/void/docs/guide/platform/installation/ci.md +55 -0
- package/skills/void/docs/guide/platform/installation/credentials.md +80 -0
- package/skills/void/docs/guide/platform/installation/domains.md +68 -0
- package/skills/void/docs/guide/platform/installation/first-deployment.md +82 -0
- package/skills/void/docs/guide/platform/installation/maintenance.md +137 -0
- package/skills/void/docs/guide/platform/installation/prerequisites.md +86 -0
- package/skills/void/docs/guide/platform/installation/setup.md +169 -0
- package/skills/void/docs/guide/platform/installation/uninstall.md +54 -0
- package/skills/void/docs/guide/platform-administration.md +6 -202
- package/skills/void/docs/guide/platform-development.md +5 -254
- package/skills/void/docs/guide/project-collaboration.md +94 -0
- package/skills/void/docs/guide/sandboxes.md +9 -24
- package/skills/void/docs/guide/self-hosted-platform.md +13 -542
- package/skills/void/docs/reference/api.md +34 -34
- package/skills/void/docs/reference/cli.md +276 -31
- package/skills/void/docs/reference/config.md +1 -1
- package/skills/void/docs/reference/resource-inference.md +10 -10
- package/dist/cf-access-AJ1ehiFR.mjs +0 -42
- package/dist/cf-access-DsSsZUPr.mjs +0 -67
- package/dist/client-Clirrol3.mjs +0 -705
- package/dist/email-uKyQYUVY.mjs +0 -1016
|
@@ -1,111 +1,22 @@
|
|
|
1
1
|
import { r as __toESM } from "./rolldown-runtime-rQ84J-ij.mjs";
|
|
2
2
|
import { o as join } from "./dist-BrsS7cai.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { t as cfAccessHeaders } from "./cf-access-
|
|
5
|
-
import {
|
|
3
|
+
import { $ as promoteInstallationSecrets, A as outro, D as log, E as intro, F as CANCEL_SYMBOL, G as assertPlatformInstallationSlugAvailable, I as isCancel, J as listInstallationManifests, L as settings, M as select, N as symbol, O as multiselect, P as text$1, Q as platformWorkerSecretUpdateIntentState, R as u$1, V as ensurePlatformAccessToken, W as acquirePlatformLifecycleLock, X as normalizePlatformApiUrl, at as savePlatformConnection, c as import_picocolors$1, ct as writePendingInstallationSecrets, et as readInstallationRecoverySecrets, ft as require_picocolors, h as S_BAR_END, i as createSpinner, it as resolveInstallationManifest, j as password, k as note, m as S_BAR, n as cliCommandName, q as latestPlatformWorkerCutoverGeneration, r as cliTitle, st as writeInstallationManifest, tt as readPendingInstallationSecrets, v as S_PASSWORD_MASK, w as confirm } from "./output-B0cfNSx5.mjs";
|
|
4
|
+
import { t as cfAccessHeaders } from "./cf-access-DRsQRe6k.mjs";
|
|
5
|
+
import { f as openBrowser } from "./auth-DPl6kck4.mjs";
|
|
6
6
|
import { o as resolveWranglerArgv } from "./preset-lAy0B0BQ.mjs";
|
|
7
|
-
import { l as runCloudflareAuthCommand, n as announceCloudflareLogin } from "./cloudflare-auth-
|
|
8
|
-
import { n as createDefaultRunWrangler, o as resolvePinnedApiBaseUrl } from "./provision-
|
|
7
|
+
import { l as runCloudflareAuthCommand, n as announceCloudflareLogin } from "./cloudflare-auth-6M5llVPC.mjs";
|
|
8
|
+
import { n as createDefaultRunWrangler, o as resolvePinnedApiBaseUrl } from "./provision-CSJOjjQk.mjs";
|
|
9
9
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
|
+
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { execFile } from "node:child_process";
|
|
13
|
-
import { PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_SANDBOX_DRAIN_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, parsePlatformInstallationManifest, parsePlatformResourceOwnershipMarker, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
13
|
+
import { PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_SANDBOX_DRAIN_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
14
|
+
import { stripVTControlCharacters } from "node:util";
|
|
14
15
|
import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
15
17
|
import { createServer, request } from "node:http";
|
|
16
18
|
import { request as request$1 } from "node:https";
|
|
17
19
|
import { platformDatabaseSchemaDigest, resolvePlatformRuntime } from "@void/platform/installer";
|
|
18
|
-
//#region src/cli/platform-auth.ts
|
|
19
|
-
function assertPlatformOperatorCredentialCanApplyPlan(credential, plan) {
|
|
20
|
-
if (credential.source !== "oauth") return;
|
|
21
|
-
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
22
|
-
const writesDns = plan.actions.some((action) => action.kind === "dns-record" && action.action !== "reuse");
|
|
23
|
-
if (!createsZone && !writesDns) return;
|
|
24
|
-
const permissions = [...createsZone ? ["Account → Zone → Edit"] : [], ...writesDns ? ["Zone → DNS → Edit"] : []];
|
|
25
|
-
throw new Error(`platform: Cloudflare browser OAuth grants Zone Read and cannot apply the planned zone or DNS changes. Set CLOUDFLARE_API_TOKEN with ${permissions.join(" and ")}, scoped to the selected account and application zone, then rerun the command.`);
|
|
26
|
-
}
|
|
27
|
-
async function resolvePlatformCloudflareCredential(dependencies = {}) {
|
|
28
|
-
const { credential } = await resolvePlatformCloudflareCredentialWithInput(async () => void 0, dependencies);
|
|
29
|
-
return credential;
|
|
30
|
-
}
|
|
31
|
-
/** Check saved credentials during input, but never start browser login until it succeeds. */
|
|
32
|
-
async function resolvePlatformCloudflareCredentialWithInput(readInput, dependencies = {}) {
|
|
33
|
-
const controller = new AbortController();
|
|
34
|
-
const runner = dependencies.runWrangler ?? ((args) => runCloudflareAuthCommand(process.cwd(), args, controller.signal));
|
|
35
|
-
const runWrangler = (args, capture) => {
|
|
36
|
-
controller.signal.throwIfAborted();
|
|
37
|
-
return runner(args, capture);
|
|
38
|
-
};
|
|
39
|
-
const pending = readExistingCredential(dependencies, runWrangler).then((credential) => ({
|
|
40
|
-
ok: true,
|
|
41
|
-
credential
|
|
42
|
-
}), (error) => ({
|
|
43
|
-
ok: false,
|
|
44
|
-
error
|
|
45
|
-
}));
|
|
46
|
-
const cancel = () => controller.abort();
|
|
47
|
-
const assertNotCancelled = () => {
|
|
48
|
-
if (controller.signal.aborted) throw new Error("platform: Cancelled.");
|
|
49
|
-
};
|
|
50
|
-
try {
|
|
51
|
-
const input = await readInput();
|
|
52
|
-
process.once("SIGINT", cancel);
|
|
53
|
-
const result = await pending;
|
|
54
|
-
assertNotCancelled();
|
|
55
|
-
if (!result.ok) throw result.error;
|
|
56
|
-
if (result.credential) return {
|
|
57
|
-
input,
|
|
58
|
-
credential: result.credential
|
|
59
|
-
};
|
|
60
|
-
announceCloudflareLogin();
|
|
61
|
-
const login = await runWrangler(["login", "--use-keyring"], true);
|
|
62
|
-
assertNotCancelled();
|
|
63
|
-
if (login.status !== 0) throw new Error("platform: Cloudflare browser login did not complete.");
|
|
64
|
-
const credential = await readWranglerOAuthCredential(runWrangler);
|
|
65
|
-
assertNotCancelled();
|
|
66
|
-
if (!credential) throw new Error("platform: Cloudflare sign-in completed without a usable OAuth credential.");
|
|
67
|
-
return {
|
|
68
|
-
input,
|
|
69
|
-
credential
|
|
70
|
-
};
|
|
71
|
-
} finally {
|
|
72
|
-
process.removeListener("SIGINT", cancel);
|
|
73
|
-
controller.abort();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function readExistingCredential(dependencies, runWrangler) {
|
|
77
|
-
const environmentToken = process.env.CLOUDFLARE_API_TOKEN?.trim() || process.env.CF_API_TOKEN?.trim() || "";
|
|
78
|
-
if (environmentToken) return {
|
|
79
|
-
source: "environment",
|
|
80
|
-
token: environmentToken
|
|
81
|
-
};
|
|
82
|
-
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.");
|
|
83
|
-
if ((await runWrangler([
|
|
84
|
-
"auth",
|
|
85
|
-
"keyring",
|
|
86
|
-
"enable"
|
|
87
|
-
], true)).status !== 0) throw new Error("platform: Could not enable secure Cloudflare credential storage. Refusing to use a plaintext OAuth credential.");
|
|
88
|
-
return await readWranglerOAuthCredential(runWrangler);
|
|
89
|
-
}
|
|
90
|
-
async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflareAuthCommand(process.cwd(), args)) {
|
|
91
|
-
const result = await runWrangler([
|
|
92
|
-
"auth",
|
|
93
|
-
"token",
|
|
94
|
-
"--json"
|
|
95
|
-
], true);
|
|
96
|
-
if (result.status !== 0) return null;
|
|
97
|
-
try {
|
|
98
|
-
const parsed = JSON.parse(result.stdout);
|
|
99
|
-
if (parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
100
|
-
return {
|
|
101
|
-
source: "oauth",
|
|
102
|
-
token: parsed.token.trim()
|
|
103
|
-
};
|
|
104
|
-
} catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
//#endregion
|
|
109
20
|
//#region src/cli/platform-cloudflare.ts
|
|
110
21
|
var PlatformCloudflareApiError = class extends Error {
|
|
111
22
|
status;
|
|
@@ -191,7 +102,8 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
191
102
|
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
192
103
|
...init?.headers
|
|
193
104
|
},
|
|
194
|
-
signal: init?.signal ?? AbortSignal.timeout(3e4)
|
|
105
|
+
signal: init?.signal ?? AbortSignal.timeout(3e4),
|
|
106
|
+
redirect: "manual"
|
|
195
107
|
});
|
|
196
108
|
let envelope;
|
|
197
109
|
try {
|
|
@@ -546,6 +458,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
546
458
|
method: "POST",
|
|
547
459
|
body: JSON.stringify({
|
|
548
460
|
name,
|
|
461
|
+
caching: { disabled: true },
|
|
549
462
|
origin: {
|
|
550
463
|
database: parsed.database,
|
|
551
464
|
host: parsed.host,
|
|
@@ -565,6 +478,12 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
565
478
|
async listWorkerNames(accountId) {
|
|
566
479
|
return (await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/scripts`)).map((entry) => requiredString(entry.id, "Worker name"));
|
|
567
480
|
}
|
|
481
|
+
async disableHyperdriveCaching(accountId, id) {
|
|
482
|
+
await this.request(`/accounts/${encodeURIComponent(accountId)}/hyperdrive/configs/${encodeURIComponent(id)}`, {
|
|
483
|
+
method: "PATCH",
|
|
484
|
+
body: JSON.stringify({ caching: { disabled: true } })
|
|
485
|
+
});
|
|
486
|
+
}
|
|
568
487
|
async putKvValue(accountId, namespaceId, key, value) {
|
|
569
488
|
await this.request(`/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}/values/${encodeURIComponent(key)}`, {
|
|
570
489
|
method: "PUT",
|
|
@@ -773,6 +692,7 @@ function parseHyperdriveConfig(value) {
|
|
|
773
692
|
return {
|
|
774
693
|
id: requiredString(value.id, "Hyperdrive id"),
|
|
775
694
|
name: requiredString(value.name, "Hyperdrive name"),
|
|
695
|
+
cachingDisabled: !!value.caching && typeof value.caching === "object" && !Array.isArray(value.caching) && value.caching.disabled === true,
|
|
776
696
|
origin: {
|
|
777
697
|
database: requiredString(record.database, "Hyperdrive origin database"),
|
|
778
698
|
host: requiredString(record.host, "Hyperdrive origin host").toLowerCase(),
|
|
@@ -783,10 +703,880 @@ function parseHyperdriveConfig(value) {
|
|
|
783
703
|
};
|
|
784
704
|
}
|
|
785
705
|
//#endregion
|
|
706
|
+
//#region src/cli/platform-access-provision.ts
|
|
707
|
+
function object(value, name) {
|
|
708
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`platform: Invalid ${name} response from Cloudflare Access.`);
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
function text(value, name) {
|
|
712
|
+
if (typeof value !== "string" || !value || value.length > 8192) throw new Error(`platform: Cloudflare Access did not return ${name}.`);
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
function ids(value) {
|
|
716
|
+
if (!Array.isArray(value)) return [];
|
|
717
|
+
return value.map((entry) => typeof entry === "string" ? entry : text(object(entry, "policy").id, "policy ID"));
|
|
718
|
+
}
|
|
719
|
+
function sameSet(left, right) {
|
|
720
|
+
return left.length === right.length && [...left].sort().every((id, index) => id === [...right].sort()[index]);
|
|
721
|
+
}
|
|
722
|
+
function fingerprint(value) {
|
|
723
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
724
|
+
}
|
|
725
|
+
var PlatformAccessClient = class {
|
|
726
|
+
client;
|
|
727
|
+
accountId;
|
|
728
|
+
constructor(client, accountId) {
|
|
729
|
+
this.client = client;
|
|
730
|
+
this.accountId = accountId;
|
|
731
|
+
}
|
|
732
|
+
path(suffix) {
|
|
733
|
+
return `/accounts/${encodeURIComponent(this.accountId)}/access/${suffix}`;
|
|
734
|
+
}
|
|
735
|
+
get(suffix) {
|
|
736
|
+
return this.client.request(this.path(suffix));
|
|
737
|
+
}
|
|
738
|
+
create(suffix, body) {
|
|
739
|
+
return this.client.request(this.path(suffix), {
|
|
740
|
+
method: "POST",
|
|
741
|
+
body: JSON.stringify(body)
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
async list(suffix) {
|
|
745
|
+
const result = [];
|
|
746
|
+
for (let page = 1; page <= 100; page++) {
|
|
747
|
+
const value = await this.get(`${suffix}?page=${page}&per_page=100`);
|
|
748
|
+
if (!Array.isArray(value)) throw new Error("platform: Invalid Access inventory response.");
|
|
749
|
+
result.push(...value.map((entry) => object(entry, "inventory")));
|
|
750
|
+
if (value.length < 100) return result;
|
|
751
|
+
}
|
|
752
|
+
throw new Error("platform: Access inventory exceeded the supported pagination limit.");
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
function identityRule(rule) {
|
|
756
|
+
if (rule.email_domain) {
|
|
757
|
+
const domain = object(rule.email_domain, "email domain").domain;
|
|
758
|
+
return typeof domain === "string" && /^(?:[a-z0-9-]+\.)+[a-z]{2,63}$/i.test(domain);
|
|
759
|
+
}
|
|
760
|
+
if (rule.email) {
|
|
761
|
+
const email = object(rule.email, "email rule").email;
|
|
762
|
+
return typeof email === "string" && /^[^\s*@]+@[^\s*@]+\.[^\s*@]+$/.test(email);
|
|
763
|
+
}
|
|
764
|
+
return [
|
|
765
|
+
"email",
|
|
766
|
+
"email_domain",
|
|
767
|
+
"email_list",
|
|
768
|
+
"gsuite",
|
|
769
|
+
"github-organization",
|
|
770
|
+
"saml",
|
|
771
|
+
"okta",
|
|
772
|
+
"group"
|
|
773
|
+
].some((key) => Object.hasOwn(rule, key) && !!rule[key] && typeof rule[key] === "object" && !Array.isArray(rule[key]) && Object.keys(rule[key]).length > 0);
|
|
774
|
+
}
|
|
775
|
+
/** Preserve corporate MFA/device requirements; reject broad policies rather than rewriting them. */
|
|
776
|
+
async function verifyCompanyAccessPolicies(client, policies) {
|
|
777
|
+
const groups = /* @__PURE__ */ new Set();
|
|
778
|
+
const checkedGroups = /* @__PURE__ */ new Set();
|
|
779
|
+
const checkRules = async (value, depth = 0) => {
|
|
780
|
+
if (value === void 0) return;
|
|
781
|
+
if (!Array.isArray(value) || value.length > 100 || depth > 8) throw new Error("platform: Unsupported Access policy rule structure.");
|
|
782
|
+
for (const entry of value) {
|
|
783
|
+
const rule = object(entry, "policy rule");
|
|
784
|
+
if (Object.hasOwn(rule, "everyone") || Object.hasOwn(rule, "any_valid_service_token")) throw new Error("platform: Company Access policies must not allow Everyone or any valid service token.");
|
|
785
|
+
if (rule.group) {
|
|
786
|
+
const id = text(object(rule.group, "Access group").id, "Access group ID");
|
|
787
|
+
if (groups.has(id)) throw new Error("platform: Access groups contain a recursive membership rule.");
|
|
788
|
+
if (checkedGroups.has(id)) continue;
|
|
789
|
+
if (checkedGroups.size >= 100) throw new Error("platform: Access group policy exceeds the inspection limit.");
|
|
790
|
+
groups.add(id);
|
|
791
|
+
const group = object(await client.get(`groups/${encodeURIComponent(id)}`), "Access group");
|
|
792
|
+
await checkRules(group.include, depth + 1);
|
|
793
|
+
await checkRules(group.require, depth + 1);
|
|
794
|
+
if (!Array.isArray(group.include) || !group.include.length) throw new Error("platform: Access groups must contain explicit identity rules.");
|
|
795
|
+
if (!(Array.isArray(group.require) && group.require.some((entry) => identityRule(object(entry, "group rule")))) && !group.include.every((entry) => identityRule(object(entry, "group rule")))) throw new Error("platform: Access groups must restrict company identities.");
|
|
796
|
+
groups.delete(id);
|
|
797
|
+
checkedGroups.add(id);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
let allows = 0;
|
|
802
|
+
for (const policy of policies) {
|
|
803
|
+
if (![
|
|
804
|
+
"allow",
|
|
805
|
+
"deny",
|
|
806
|
+
"non_identity",
|
|
807
|
+
"bypass"
|
|
808
|
+
].includes(String(policy.decision))) throw new Error("platform: Unsupported Access policy decision.");
|
|
809
|
+
if (policy.decision === "bypass") throw new Error("platform: A bypass policy cannot protect a Void platform.");
|
|
810
|
+
if (policy.decision !== "allow") continue;
|
|
811
|
+
allows++;
|
|
812
|
+
await checkRules(policy.include);
|
|
813
|
+
await checkRules(policy.require);
|
|
814
|
+
if (!Array.isArray(policy.include) || !policy.include.length || !(Array.isArray(policy.require) && policy.require.some((entry) => identityRule(object(entry, "policy rule")))) && !policy.include.every((entry) => identityRule(object(entry, "policy rule")))) throw new Error("platform: Select Access allow policies restricted to company identities.");
|
|
815
|
+
}
|
|
816
|
+
if (!allows) throw new Error("platform: Select at least one company Access allow policy.");
|
|
817
|
+
}
|
|
818
|
+
function platformAccessPublicDestinations(application) {
|
|
819
|
+
const destinations = Array.isArray(application.destinations) ? application.destinations : [];
|
|
820
|
+
return [...typeof application.domain === "string" ? [application.domain] : [], ...destinations.flatMap((value) => {
|
|
821
|
+
const entry = object(value, "Access destination");
|
|
822
|
+
return entry.type === "public" && typeof entry.uri === "string" ? [entry.uri] : [];
|
|
823
|
+
})];
|
|
824
|
+
}
|
|
825
|
+
function platformAccessOverlapsOrigin(destination, origin) {
|
|
826
|
+
const pattern = "^" + destination.split("/")[0].split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$";
|
|
827
|
+
return new RegExp(pattern, "i").test(new URL(origin).hostname);
|
|
828
|
+
}
|
|
829
|
+
async function inspectPlatformAccess(client, input) {
|
|
830
|
+
const authDomain = text(object(await client.get("organizations"), "Access organization").auth_domain, "Access team domain");
|
|
831
|
+
const teamDomain = authDomain.startsWith("https://") ? authDomain : `https://${authDomain}`;
|
|
832
|
+
parsePlatformAccessProtection({
|
|
833
|
+
teamDomain,
|
|
834
|
+
applications: [{
|
|
835
|
+
origin: "https://validation.example.com",
|
|
836
|
+
audience: "validation"
|
|
837
|
+
}]
|
|
838
|
+
});
|
|
839
|
+
const identityProviders = (await client.list("identity_providers")).map((provider) => ({
|
|
840
|
+
id: text(provider.id, "identity provider ID"),
|
|
841
|
+
name: text(provider.name ?? provider.id, "identity provider name")
|
|
842
|
+
}));
|
|
843
|
+
if (!input.identityProviderIds.length || !input.policyIds.length || input.identityProviderIds.length > 16 || input.policyIds.length > 16 || new Set(input.identityProviderIds).size !== input.identityProviderIds.length || new Set(input.policyIds).size !== input.policyIds.length) throw new Error("platform: Select explicit identity providers and company policies for Access.");
|
|
844
|
+
if (input.identityProviderIds.some((id) => !identityProviders.some((provider) => provider.id === id))) throw new Error("platform: A selected Access identity provider is unavailable in this account.");
|
|
845
|
+
const policies = await Promise.all(input.policyIds.map(async (id) => object(await client.get(`policies/${encodeURIComponent(id)}`), "Access policy")));
|
|
846
|
+
await verifyCompanyAccessPolicies(client, policies);
|
|
847
|
+
return {
|
|
848
|
+
teamDomain,
|
|
849
|
+
identityProviders,
|
|
850
|
+
policies
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
async function provisionPlatformAccess(client, input, previous, save) {
|
|
854
|
+
if (!input.login && !input.protection) return { resources: [] };
|
|
855
|
+
if (client.accountId !== input.accountId) throw new Error("platform: Access account selection does not match its management client.");
|
|
856
|
+
const inspected = await inspectPlatformAccess(client, input);
|
|
857
|
+
const origins = [...new Set([
|
|
858
|
+
input.apiUrl,
|
|
859
|
+
input.proxyUrl,
|
|
860
|
+
...input.additionalOrigins ?? []
|
|
861
|
+
].map((value) => {
|
|
862
|
+
const url = new URL(value);
|
|
863
|
+
if (url.origin !== value || url.protocol !== "https:") throw new Error("platform: Access requires exact HTTPS origins without credentials, paths, or query strings.");
|
|
864
|
+
return url.origin;
|
|
865
|
+
}))];
|
|
866
|
+
for (const origin of origins) if (!origin.startsWith("https://")) throw new Error("platform: Access protection requires HTTPS platform origins.");
|
|
867
|
+
const state = previous ? structuredClone(previous) : {
|
|
868
|
+
version: 1,
|
|
869
|
+
accountId: input.accountId,
|
|
870
|
+
installationId: input.installationId,
|
|
871
|
+
nonce: randomBytes(16).toString("hex"),
|
|
872
|
+
resources: {}
|
|
873
|
+
};
|
|
874
|
+
if (state.version !== 1 || state.accountId !== input.accountId || state.installationId !== input.installationId || !/^[a-f0-9]{32}$/.test(state.nonce)) throw new Error("platform: Access recovery data belongs to a different installation.");
|
|
875
|
+
save(state);
|
|
876
|
+
const policies = input.policyIds.map((id, index) => ({
|
|
877
|
+
id,
|
|
878
|
+
precedence: index + 1
|
|
879
|
+
}));
|
|
880
|
+
const inventory = await client.list("apps");
|
|
881
|
+
const tokenInventory = input.protection ? await client.list("service_tokens") : [];
|
|
882
|
+
if (input.protection?.applicationId && !input.serviceToken) throw new Error("platform: Connecting an existing protection application requires its admitted service token.");
|
|
883
|
+
if (input.serviceToken) {
|
|
884
|
+
const token = tokenInventory.find((token) => token.id === input.serviceToken.id);
|
|
885
|
+
if (!token || token.client_id !== input.serviceToken.clientId || token.enabled === false || typeof token.expires_at === "string" && Date.parse(token.expires_at) <= Date.now()) throw new Error("platform: The selected Access service token is unavailable, expired, or belongs to another client.");
|
|
886
|
+
const prior = state.resources["service-token"];
|
|
887
|
+
state.resources["service-token"] = {
|
|
888
|
+
id: input.serviceToken.id,
|
|
889
|
+
name: text(token.name, "service token name"),
|
|
890
|
+
fingerprint: fingerprint({ origins }),
|
|
891
|
+
pending: false,
|
|
892
|
+
adopted: prior?.id === input.serviceToken.id ? prior.adopted : true
|
|
893
|
+
};
|
|
894
|
+
save(state);
|
|
895
|
+
}
|
|
896
|
+
if (input.protection) for (const app of inventory) {
|
|
897
|
+
if (app.id === input.protection.applicationId || app.id === state.resources.protection?.id || app.name === state.resources.protection?.name) continue;
|
|
898
|
+
if (platformAccessPublicDestinations(app).some((destination) => origins.some((origin) => platformAccessOverlapsOrigin(destination, origin)) && (destination.includes("/") || origins.some((origin) => destination === new URL(origin).hostname)))) throw new Error("platform: An existing Access application overlaps a platform endpoint. Connect or reconcile it explicitly before provisioning protection.");
|
|
899
|
+
}
|
|
900
|
+
const output = { resources: [] };
|
|
901
|
+
const application = async (key, expected, existingId) => {
|
|
902
|
+
if (existingId) {
|
|
903
|
+
const found = object(await client.get(`apps/${encodeURIComponent(existingId)}`), "Access application");
|
|
904
|
+
if (found.id !== existingId) throw new Error("platform: Access returned a different application ID.");
|
|
905
|
+
state.resources[key] = {
|
|
906
|
+
id: existingId,
|
|
907
|
+
name: text(found.name, "application name"),
|
|
908
|
+
fingerprint: fingerprint(expected),
|
|
909
|
+
pending: true,
|
|
910
|
+
adopted: true
|
|
911
|
+
};
|
|
912
|
+
save(state);
|
|
913
|
+
return found;
|
|
914
|
+
}
|
|
915
|
+
const desiredHash = fingerprint(expected);
|
|
916
|
+
let checkpoint = state.resources[key];
|
|
917
|
+
if (checkpoint && checkpoint.fingerprint !== desiredHash) throw new Error("platform: Access setup changed after provisioning started. Connect the existing application explicitly rather than overwriting it.");
|
|
918
|
+
if (checkpoint?.id) {
|
|
919
|
+
const found = object(await client.get(`apps/${encodeURIComponent(checkpoint.id)}`), "Access application");
|
|
920
|
+
if (found.id !== checkpoint.id || found.name !== checkpoint.name) throw new Error("platform: The recorded Access application changed ownership identity.");
|
|
921
|
+
return found;
|
|
922
|
+
}
|
|
923
|
+
if (checkpoint?.pending) {
|
|
924
|
+
const matching = inventory.filter((app) => app.name === checkpoint.name);
|
|
925
|
+
if (matching.length !== 1) throw new Error("platform: An Access application creation has an uncertain outcome. Inspect Cloudflare and connect the existing application; no creation was retried.");
|
|
926
|
+
checkpoint.id = text(matching[0].id, "application ID");
|
|
927
|
+
save(state);
|
|
928
|
+
return object(await client.get(`apps/${encodeURIComponent(checkpoint.id)}`), "Access application");
|
|
929
|
+
}
|
|
930
|
+
const name = `void-${input.installationId}-${key}`.slice(0, 90) + "-" + state.nonce.slice(0, 16);
|
|
931
|
+
if (inventory.some((app) => app.name === name)) throw new Error("platform: An Access application with this installation identity already exists. Connect it explicitly; no duplicate was created.");
|
|
932
|
+
checkpoint = {
|
|
933
|
+
name,
|
|
934
|
+
fingerprint: desiredHash,
|
|
935
|
+
pending: true
|
|
936
|
+
};
|
|
937
|
+
state.resources[key] = checkpoint;
|
|
938
|
+
save(state);
|
|
939
|
+
try {
|
|
940
|
+
const created = object(await client.create("apps", {
|
|
941
|
+
...expected,
|
|
942
|
+
name
|
|
943
|
+
}), "Access application");
|
|
944
|
+
checkpoint.id = text(created.id, "application ID");
|
|
945
|
+
if (created.saas_app) {
|
|
946
|
+
const oidc = object(created.saas_app, "OIDC application");
|
|
947
|
+
checkpoint.clientId = text(oidc.client_id, "OIDC client ID");
|
|
948
|
+
checkpoint.clientSecret = typeof oidc.client_secret === "string" ? oidc.client_secret : void 0;
|
|
949
|
+
}
|
|
950
|
+
save(state);
|
|
951
|
+
return object(await client.get(`apps/${encodeURIComponent(checkpoint.id)}`), "Access application");
|
|
952
|
+
} catch (error) {
|
|
953
|
+
if (error instanceof PlatformCloudflareApiError && error.status >= 400 && error.status < 500 && ![
|
|
954
|
+
408,
|
|
955
|
+
409,
|
|
956
|
+
429
|
|
957
|
+
].includes(error.status)) {
|
|
958
|
+
checkpoint.pending = false;
|
|
959
|
+
save(state);
|
|
960
|
+
}
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
const completeApplication = (key, app) => {
|
|
965
|
+
const checkpoint = state.resources[key];
|
|
966
|
+
if (!checkpoint?.id || checkpoint.id !== app.id || checkpoint.name !== app.name) throw new Error("platform: The verified Access application changed ownership identity.");
|
|
967
|
+
checkpoint.pending = false;
|
|
968
|
+
save(state);
|
|
969
|
+
};
|
|
970
|
+
if (input.login) {
|
|
971
|
+
const key = `login-${input.login.id}`;
|
|
972
|
+
const app = await application(key, {
|
|
973
|
+
type: "saas",
|
|
974
|
+
allowed_idps: [...input.identityProviderIds],
|
|
975
|
+
auto_redirect_to_identity: input.identityProviderIds.length === 1,
|
|
976
|
+
policies,
|
|
977
|
+
session_duration: "12h",
|
|
978
|
+
saas_app: {
|
|
979
|
+
auth_type: "oidc",
|
|
980
|
+
redirect_uris: [`${input.apiUrl}/auth/callback`],
|
|
981
|
+
grant_types: ["authorization_code_with_pkce"],
|
|
982
|
+
allow_pkce_without_client_secret: false,
|
|
983
|
+
scopes: input.login.requiredGroups?.length ? [
|
|
984
|
+
"openid",
|
|
985
|
+
"email",
|
|
986
|
+
"profile",
|
|
987
|
+
"groups"
|
|
988
|
+
] : [
|
|
989
|
+
"openid",
|
|
990
|
+
"email",
|
|
991
|
+
"profile"
|
|
992
|
+
]
|
|
993
|
+
}
|
|
994
|
+
}, input.login.applicationId);
|
|
995
|
+
await verifyPlatformAccessApplication(client, input, app, "saas");
|
|
996
|
+
const oidc = object(app.saas_app, "OIDC configuration");
|
|
997
|
+
if (oidc.auth_type !== "oidc" || !sameSet(ids(oidc.redirect_uris), [`${input.apiUrl}/auth/callback`]) || !sameSet(ids(oidc.grant_types), ["authorization_code_with_pkce"]) || oidc.allow_pkce_without_client_secret === true) throw new Error("platform: Access OIDC redirect or flow settings do not match Void.");
|
|
998
|
+
const clientId = text(oidc.client_id, "OIDC client ID");
|
|
999
|
+
const clientSecret = input.login.clientSecret ?? state.resources[key]?.clientSecret;
|
|
1000
|
+
if (!clientSecret) throw new Error("platform: The Access application exists but its client secret was not recovered. Supply its secret explicitly; no duplicate application was created.");
|
|
1001
|
+
output.login = {
|
|
1002
|
+
configuration: parsePlatformLoginConfiguration({
|
|
1003
|
+
id: input.login.id,
|
|
1004
|
+
kind: "cloudflare-access",
|
|
1005
|
+
label: input.login.label,
|
|
1006
|
+
issuer: `${inspected.teamDomain}/cdn-cgi/access/sso/oidc/${clientId}`,
|
|
1007
|
+
clientId,
|
|
1008
|
+
requiredGroups: input.login.requiredGroups ?? []
|
|
1009
|
+
}),
|
|
1010
|
+
clientSecret
|
|
1011
|
+
};
|
|
1012
|
+
completeApplication(key, app);
|
|
1013
|
+
}
|
|
1014
|
+
if (input.protection) {
|
|
1015
|
+
let service = input.serviceToken;
|
|
1016
|
+
const tokenKey = "service-token";
|
|
1017
|
+
if (!service) {
|
|
1018
|
+
let saved = state.resources[tokenKey];
|
|
1019
|
+
if (!saved) {
|
|
1020
|
+
saved = {
|
|
1021
|
+
name: `void-${input.installationId}-access-probes-${state.nonce.slice(0, 16)}`,
|
|
1022
|
+
fingerprint: fingerprint({ origins }),
|
|
1023
|
+
pending: true
|
|
1024
|
+
};
|
|
1025
|
+
state.resources[tokenKey] = saved;
|
|
1026
|
+
save(state);
|
|
1027
|
+
if (tokenInventory.some((token) => token.name === saved.name)) throw new Error("platform: An Access service token for this installation already exists. Supply its credentials explicitly; no duplicate was created.");
|
|
1028
|
+
const created = object(await client.create("service_tokens", {
|
|
1029
|
+
name: saved.name,
|
|
1030
|
+
duration: "8760h"
|
|
1031
|
+
}), "Access service token");
|
|
1032
|
+
saved.id = text(created.id, "service token ID");
|
|
1033
|
+
saved.clientId = text(created.client_id, "service token client ID");
|
|
1034
|
+
saved.clientSecret = text(created.client_secret, "service token secret");
|
|
1035
|
+
saved.pending = false;
|
|
1036
|
+
save(state);
|
|
1037
|
+
}
|
|
1038
|
+
if (!saved.id || !saved.clientId || !saved.clientSecret) {
|
|
1039
|
+
const found = (await client.list("service_tokens")).filter((token) => token.name === saved.name);
|
|
1040
|
+
if (found.length === 1) {
|
|
1041
|
+
saved.id = text(found[0].id, "service token ID");
|
|
1042
|
+
save(state);
|
|
1043
|
+
}
|
|
1044
|
+
throw new Error("platform: Access service-token creation has an uncertain outcome or its secret was lost. Supply an existing admitted service token; no creation was retried.");
|
|
1045
|
+
}
|
|
1046
|
+
if (saved.fingerprint !== fingerprint({ origins })) throw new Error("platform: The saved Access service token belongs to different platform origins. Connect the intended service token explicitly.");
|
|
1047
|
+
service = {
|
|
1048
|
+
id: saved.id,
|
|
1049
|
+
clientId: saved.clientId,
|
|
1050
|
+
clientSecret: saved.clientSecret
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const app = await application("protection", {
|
|
1054
|
+
type: "self_hosted",
|
|
1055
|
+
domain: new URL(input.apiUrl).hostname,
|
|
1056
|
+
destinations: origins.map((origin) => ({
|
|
1057
|
+
type: "public",
|
|
1058
|
+
uri: new URL(origin).hostname
|
|
1059
|
+
})),
|
|
1060
|
+
allowed_idps: [...input.identityProviderIds],
|
|
1061
|
+
auto_redirect_to_identity: input.identityProviderIds.length === 1,
|
|
1062
|
+
session_duration: "12h",
|
|
1063
|
+
policies: [...policies, {
|
|
1064
|
+
name: "Void installer and CI",
|
|
1065
|
+
decision: "non_identity",
|
|
1066
|
+
precedence: 0,
|
|
1067
|
+
include: [{ service_token: { token_id: service.id } }]
|
|
1068
|
+
}]
|
|
1069
|
+
}, input.protection.applicationId);
|
|
1070
|
+
await verifyPlatformAccessApplication(client, input, app, "self_hosted", service.id);
|
|
1071
|
+
const covered = platformAccessPublicDestinations(app);
|
|
1072
|
+
if (origins.some((origin) => !covered.includes(new URL(origin).hostname))) throw new Error("platform: Access must protect both the API and proxy origins without path exclusions.");
|
|
1073
|
+
const audience = text(app.aud, "application audience");
|
|
1074
|
+
output.protection = parsePlatformAccessProtection({
|
|
1075
|
+
teamDomain: inspected.teamDomain,
|
|
1076
|
+
applications: origins.map((origin) => ({
|
|
1077
|
+
origin,
|
|
1078
|
+
audience
|
|
1079
|
+
}))
|
|
1080
|
+
});
|
|
1081
|
+
output.credentials = Object.fromEntries(origins.map((origin) => [origin, {
|
|
1082
|
+
CF_ACCESS_CLIENT_ID: service.clientId,
|
|
1083
|
+
CF_ACCESS_CLIENT_SECRET: service.clientSecret
|
|
1084
|
+
}]));
|
|
1085
|
+
completeApplication("protection", app);
|
|
1086
|
+
const savedService = state.resources[tokenKey];
|
|
1087
|
+
if (savedService?.id === service.id) {
|
|
1088
|
+
savedService.pending = false;
|
|
1089
|
+
save(state);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
output.resources = Object.entries(state.resources).flatMap(([key, value]) => value.id ? [{
|
|
1093
|
+
key,
|
|
1094
|
+
id: value.id,
|
|
1095
|
+
name: value.name,
|
|
1096
|
+
adopted: value.adopted === true
|
|
1097
|
+
}] : []);
|
|
1098
|
+
return output;
|
|
1099
|
+
}
|
|
1100
|
+
async function verifyPlatformAccessApplication(client, input, app, type, machineId) {
|
|
1101
|
+
if (app.type !== type || !sameSet(ids(app.allowed_idps), input.identityProviderIds)) throw new Error("platform: Access application identity-provider configuration does not match the reviewed plan.");
|
|
1102
|
+
const currentPolicies = await client.list(`apps/${encodeURIComponent(text(app.id, "application ID"))}/policies`);
|
|
1103
|
+
await verifyCompanyAccessPolicies(client, currentPolicies);
|
|
1104
|
+
if (!sameSet(currentPolicies.filter((policy) => policy.decision === "allow" || policy.decision === "deny").map((policy) => text(policy.id, "policy ID")), input.policyIds)) throw new Error("platform: Access application policies do not match the reviewed company policies.");
|
|
1105
|
+
for (const policy of currentPolicies.filter((policy) => policy.decision === "non_identity")) {
|
|
1106
|
+
const includes = Array.isArray(policy.include) ? policy.include : [];
|
|
1107
|
+
if (!machineId || includes.length !== 1 || object(includes[0], "service policy").service_token === void 0 || object(object(includes[0], "service policy").service_token, "service token rule").token_id !== machineId) throw new Error("platform: Access application contains an unexpected machine policy.");
|
|
1108
|
+
}
|
|
1109
|
+
if (machineId && !currentPolicies.some((policy) => policy.decision === "non_identity")) throw new Error("platform: The platform service token is not admitted by Access.");
|
|
1110
|
+
}
|
|
1111
|
+
//#endregion
|
|
1112
|
+
//#region src/cli/platform-access-setup.ts
|
|
1113
|
+
function platformAccessResourceInventory(encoded, installationId) {
|
|
1114
|
+
if (!encoded) return [];
|
|
1115
|
+
const state = JSON.parse(encoded);
|
|
1116
|
+
if (state.version !== 1 || state.installationId !== installationId || typeof state.accountId !== "string" || !state.resources) throw new Error("platform: Access resource recovery data belongs to another installation.");
|
|
1117
|
+
return Object.entries(state.resources).flatMap(([key, resource]) => resource.id ? [{
|
|
1118
|
+
key,
|
|
1119
|
+
kind: key.includes("service-token") ? "service-token" : "application",
|
|
1120
|
+
accountId: state.accountId,
|
|
1121
|
+
id: resource.id,
|
|
1122
|
+
name: resource.name,
|
|
1123
|
+
ownership: resource.adopted ? "adopted" : "created"
|
|
1124
|
+
}] : []);
|
|
1125
|
+
}
|
|
1126
|
+
function checkpointPlatformAccessResources(manifest, encoded, updatedAt) {
|
|
1127
|
+
const resources = platformAccessResourceInventory(encoded, manifest.id);
|
|
1128
|
+
const previous = manifest.authenticationResources ?? [];
|
|
1129
|
+
return resources.length ? {
|
|
1130
|
+
...manifest,
|
|
1131
|
+
authenticationResources: [...previous, ...resources.filter((resource) => !previous.some((saved) => saved.accountId === resource.accountId && saved.kind === resource.kind && saved.id === resource.id))],
|
|
1132
|
+
updatedAt
|
|
1133
|
+
} : manifest;
|
|
1134
|
+
}
|
|
1135
|
+
function isPlatformAuthenticationPreparationKey(name) {
|
|
1136
|
+
return name.startsWith("authenticationConnection:") || [
|
|
1137
|
+
"accessManagementToken",
|
|
1138
|
+
"accessSetupSpecJson",
|
|
1139
|
+
"accessExistingServiceTokenJson",
|
|
1140
|
+
"accessExistingLoginSecret"
|
|
1141
|
+
].includes(name);
|
|
1142
|
+
}
|
|
1143
|
+
/** Validate before planning; credential references are resolved only during provisioning. */
|
|
1144
|
+
function parsePlatformAccessSetupSpec(input) {
|
|
1145
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("platform: cloudflareAccess must contain an Access setup configuration.");
|
|
1146
|
+
const value = input;
|
|
1147
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1148
|
+
"mode",
|
|
1149
|
+
"accountId",
|
|
1150
|
+
"identityProviderIds",
|
|
1151
|
+
"policyIds",
|
|
1152
|
+
"loginApplicationId",
|
|
1153
|
+
"protectionApplicationId",
|
|
1154
|
+
"loginClientSecretEnv",
|
|
1155
|
+
"serviceToken"
|
|
1156
|
+
]);
|
|
1157
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) throw new Error("platform: Unknown Access setup field. Use environment references for credentials.");
|
|
1158
|
+
if (value.mode !== "create" && value.mode !== "existing") throw new Error("platform: Access setup mode must be create or existing.");
|
|
1159
|
+
for (const key of [
|
|
1160
|
+
"accountId",
|
|
1161
|
+
"loginApplicationId",
|
|
1162
|
+
"protectionApplicationId",
|
|
1163
|
+
"loginClientSecretEnv"
|
|
1164
|
+
]) if (value[key] !== void 0 && (typeof value[key] !== "string" || !value[key] || String(value[key]).length > 256)) throw new Error(`platform: Invalid Access ${key}.`);
|
|
1165
|
+
for (const key of ["identityProviderIds", "policyIds"]) {
|
|
1166
|
+
const ids = value[key];
|
|
1167
|
+
if (ids !== void 0 && (!Array.isArray(ids) || !ids.length || ids.length > 16 || ids.some((id) => typeof id !== "string" || !id || id.length > 256) || new Set(ids).size !== ids.length)) throw new Error(`platform: Invalid Access ${key}.`);
|
|
1168
|
+
}
|
|
1169
|
+
if (value.loginClientSecretEnv && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(value.loginClientSecretEnv))) throw new Error("platform: Invalid Access client secret environment reference.");
|
|
1170
|
+
if (value.serviceToken !== void 0) {
|
|
1171
|
+
const token = value.serviceToken;
|
|
1172
|
+
if (!token || typeof token !== "object" || Array.isArray(token) || Object.keys(token).some((key) => ![
|
|
1173
|
+
"id",
|
|
1174
|
+
"clientIdEnv",
|
|
1175
|
+
"clientSecretEnv"
|
|
1176
|
+
].includes(key))) throw new Error("platform: Access service tokens require an ID and environment references.");
|
|
1177
|
+
const fields = token;
|
|
1178
|
+
if (typeof fields.id !== "string" || !fields.id || fields.id.length > 256 || ["clientIdEnv", "clientSecretEnv"].some((key) => typeof fields[key] !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(fields[key]))) throw new Error("platform: Invalid Access service token environment references.");
|
|
1179
|
+
}
|
|
1180
|
+
return value;
|
|
1181
|
+
}
|
|
1182
|
+
function answer$1(value) {
|
|
1183
|
+
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
1184
|
+
return value;
|
|
1185
|
+
}
|
|
1186
|
+
function secretReference(name) {
|
|
1187
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !process.env[name]) throw new Error("platform: Supply the referenced Access credential in the environment.");
|
|
1188
|
+
return process.env[name];
|
|
1189
|
+
}
|
|
1190
|
+
/** Only scoped credentials for the reviewed platform origins enter the process environment. */
|
|
1191
|
+
function activatePlatformAccessCredentials(encoded, origins) {
|
|
1192
|
+
if (!encoded) return;
|
|
1193
|
+
const entries = JSON.parse(encoded);
|
|
1194
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) throw new Error("platform: Invalid saved Access credentials.");
|
|
1195
|
+
const allowed = new Set(origins.map((origin) => new URL(origin).origin));
|
|
1196
|
+
for (const [origin, value] of Object.entries(entries)) {
|
|
1197
|
+
if (!allowed.has(origin) || !value || typeof value !== "object" || Array.isArray(value)) throw new Error("platform: Access credentials must be scoped to this platform’s API or proxy.");
|
|
1198
|
+
const headers = cfAccessHeaders({ VOID_ACCESS_CREDENTIALS: JSON.stringify({ [origin]: value }) }, origin);
|
|
1199
|
+
if (!Object.keys(headers).length) throw new Error("platform: Saved Access credentials are incomplete.");
|
|
1200
|
+
}
|
|
1201
|
+
const current = process.env.VOID_ACCESS_CREDENTIALS ? JSON.parse(process.env.VOID_ACCESS_CREDENTIALS) : {};
|
|
1202
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) throw new Error("platform: VOID_ACCESS_CREDENTIALS must contain an origin-keyed object.");
|
|
1203
|
+
process.env.VOID_ACCESS_CREDENTIALS = JSON.stringify({
|
|
1204
|
+
...current,
|
|
1205
|
+
...entries
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
async function preparePlatformAccessSetup(input) {
|
|
1209
|
+
if (!input.login && !input.protection) return;
|
|
1210
|
+
let spec = input.spec ?? (input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : void 0);
|
|
1211
|
+
if (!spec) {
|
|
1212
|
+
if (!process.stdin.isTTY) {
|
|
1213
|
+
if (input.protection) throw new Error("platform: Protection requires cloudflareAccess create or existing application verification in --auth-config.");
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const mode = answer$1(await select({
|
|
1217
|
+
message: "Cloudflare Access setup:",
|
|
1218
|
+
options: [
|
|
1219
|
+
{
|
|
1220
|
+
value: "create",
|
|
1221
|
+
label: "Create applications automatically in this Cloudflare account"
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
value: "existing",
|
|
1225
|
+
label: "Connect existing applications (including another account)"
|
|
1226
|
+
},
|
|
1227
|
+
...!input.protection ? [{
|
|
1228
|
+
value: "manual",
|
|
1229
|
+
label: "Connect an existing OIDC issuer without Cloudflare management access"
|
|
1230
|
+
}] : []
|
|
1231
|
+
]
|
|
1232
|
+
}));
|
|
1233
|
+
if (mode === "manual") return;
|
|
1234
|
+
spec = { mode };
|
|
1235
|
+
if (mode === "existing") {
|
|
1236
|
+
spec.accountId = answer$1(await text$1({
|
|
1237
|
+
message: "Cloudflare identity account ID:",
|
|
1238
|
+
initialValue: input.plan.account.id
|
|
1239
|
+
}));
|
|
1240
|
+
if (input.login) spec.loginApplicationId = answer$1(await text$1({ message: "Existing Access OIDC application ID:" }));
|
|
1241
|
+
if (input.protection) spec.protectionApplicationId = answer$1(await text$1({ message: "Existing Access protection application ID:" }));
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
spec = parsePlatformAccessSetupSpec(spec);
|
|
1245
|
+
input.save({ accessSetupSpecJson: JSON.stringify(spec) });
|
|
1246
|
+
const accountId = spec.accountId ?? input.plan.account.id;
|
|
1247
|
+
const credential = process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN || input.checkpoint.accessManagementToken;
|
|
1248
|
+
let management = credential ? new PlatformCloudflareClient(credential, { apiBase: input.client.apiBaseUrl }) : input.client;
|
|
1249
|
+
let client = new PlatformAccessClient(management, accountId);
|
|
1250
|
+
let providers;
|
|
1251
|
+
let policies;
|
|
1252
|
+
try {
|
|
1253
|
+
await client.get("organizations");
|
|
1254
|
+
providers = await client.list("identity_providers");
|
|
1255
|
+
policies = await client.list("policies");
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if (!(error instanceof PlatformCloudflareApiError) || ![401, 403].includes(error.status) || !process.stdin.isTTY || credential) throw error;
|
|
1258
|
+
console.error(`Access setup for account ${accountId} needs Access applications/policies permissions and organization/identity-provider/group read access${input.protection ? ", plus service-token permissions" : ""}.`);
|
|
1259
|
+
const token = answer$1(await password({ message: "Access setup API token (kept out of the platform runtime):" }));
|
|
1260
|
+
input.save({ accessManagementToken: token });
|
|
1261
|
+
management = new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl });
|
|
1262
|
+
client = new PlatformAccessClient(management, accountId);
|
|
1263
|
+
await client.get("organizations");
|
|
1264
|
+
providers = await client.list("identity_providers");
|
|
1265
|
+
policies = await client.list("policies");
|
|
1266
|
+
}
|
|
1267
|
+
const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ? answer$1(await multiselect({
|
|
1268
|
+
message: "Company identity providers:",
|
|
1269
|
+
required: true,
|
|
1270
|
+
options: providers.filter((provider) => typeof provider.id === "string").map((provider) => ({
|
|
1271
|
+
value: String(provider.id),
|
|
1272
|
+
label: stripVTControlCharacters(String(provider.name ?? provider.id))
|
|
1273
|
+
}))
|
|
1274
|
+
})) : []);
|
|
1275
|
+
const policyIds = spec.policyIds ?? (process.stdin.isTTY ? answer$1(await multiselect({
|
|
1276
|
+
message: "Existing company Access policies (MFA and device requirements are preserved):",
|
|
1277
|
+
required: true,
|
|
1278
|
+
options: policies.filter((policy) => typeof policy.id === "string" && ["allow", "deny"].includes(String(policy.decision))).map((policy) => ({
|
|
1279
|
+
value: String(policy.id),
|
|
1280
|
+
label: stripVTControlCharacters(String(policy.name ?? policy.id))
|
|
1281
|
+
}))
|
|
1282
|
+
})) : []);
|
|
1283
|
+
if (!identityProviderIds.length || !policyIds.length) throw new Error("platform: Access setup requires explicit identityProviderIds and policyIds.");
|
|
1284
|
+
spec = {
|
|
1285
|
+
...spec,
|
|
1286
|
+
identityProviderIds,
|
|
1287
|
+
policyIds
|
|
1288
|
+
};
|
|
1289
|
+
input.save({ accessSetupSpecJson: JSON.stringify(spec) });
|
|
1290
|
+
if (spec.mode === "existing" && (input.login && !spec.loginApplicationId || input.protection && !spec.protectionApplicationId)) throw new Error("platform: Existing Access setup requires application IDs for each selected capability.");
|
|
1291
|
+
let serviceToken = spec.serviceToken ? {
|
|
1292
|
+
id: spec.serviceToken.id,
|
|
1293
|
+
clientId: secretReference(spec.serviceToken.clientIdEnv),
|
|
1294
|
+
clientSecret: secretReference(spec.serviceToken.clientSecretEnv)
|
|
1295
|
+
} : void 0;
|
|
1296
|
+
if (!serviceToken && spec.mode === "existing" && input.protection) {
|
|
1297
|
+
if (input.checkpoint.accessExistingServiceTokenJson) serviceToken = JSON.parse(input.checkpoint.accessExistingServiceTokenJson);
|
|
1298
|
+
else if (process.stdin.isTTY) {
|
|
1299
|
+
serviceToken = {
|
|
1300
|
+
id: answer$1(await text$1({ message: "Existing admitted Access service token ID:" })),
|
|
1301
|
+
clientId: answer$1(await password({ message: "Access service token client ID:" })),
|
|
1302
|
+
clientSecret: answer$1(await password({ message: "Access service token client secret:" }))
|
|
1303
|
+
};
|
|
1304
|
+
input.save({ accessExistingServiceTokenJson: JSON.stringify(serviceToken) });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (spec.mode === "existing" && input.protection && !serviceToken) throw new Error("platform: Supply the service token already admitted by the existing protection application.");
|
|
1308
|
+
const reviewed = {
|
|
1309
|
+
accountId,
|
|
1310
|
+
login: input.login?.id ?? null,
|
|
1311
|
+
protection: input.protection,
|
|
1312
|
+
identityProviderIds,
|
|
1313
|
+
policyIds,
|
|
1314
|
+
origins: input.protection ? [
|
|
1315
|
+
input.plan.apiUrl,
|
|
1316
|
+
input.plan.proxyUrl,
|
|
1317
|
+
...input.additionalOrigins ?? []
|
|
1318
|
+
] : [],
|
|
1319
|
+
mode: spec.mode
|
|
1320
|
+
};
|
|
1321
|
+
console.log(JSON.stringify({ accessPlan: reviewed }, null, 2));
|
|
1322
|
+
if (!input.yes) {
|
|
1323
|
+
if (!process.stdin.isTTY || !answer$1(await confirm({
|
|
1324
|
+
message: "Apply this Access configuration?",
|
|
1325
|
+
initialValue: false
|
|
1326
|
+
}))) throw new Error("platform: Access setup changes require confirmation or --yes.");
|
|
1327
|
+
}
|
|
1328
|
+
const previous = input.checkpoint.accessProvisioningJson ? JSON.parse(input.checkpoint.accessProvisioningJson) : {
|
|
1329
|
+
version: 1,
|
|
1330
|
+
accountId,
|
|
1331
|
+
installationId: input.plan.installationId,
|
|
1332
|
+
nonce: createHash("sha256").update(`void-access:${input.ownershipNonce}`).digest("hex").slice(0, 32),
|
|
1333
|
+
resources: {}
|
|
1334
|
+
};
|
|
1335
|
+
let loginClientSecret = spec.loginClientSecretEnv ? secretReference(spec.loginClientSecretEnv) : input.checkpoint.accessExistingLoginSecret;
|
|
1336
|
+
if (input.login && spec.mode === "existing" && !loginClientSecret && process.stdin.isTTY) {
|
|
1337
|
+
loginClientSecret = answer$1(await password({ message: "Existing Access OIDC client secret:" }));
|
|
1338
|
+
input.save({ accessExistingLoginSecret: loginClientSecret });
|
|
1339
|
+
}
|
|
1340
|
+
const result = await provisionPlatformAccess(client, {
|
|
1341
|
+
accountId,
|
|
1342
|
+
installationId: input.plan.installationId,
|
|
1343
|
+
apiUrl: input.plan.apiUrl,
|
|
1344
|
+
proxyUrl: input.plan.proxyUrl,
|
|
1345
|
+
additionalOrigins: input.additionalOrigins,
|
|
1346
|
+
identityProviderIds,
|
|
1347
|
+
policyIds,
|
|
1348
|
+
...input.login ? { login: {
|
|
1349
|
+
...input.login,
|
|
1350
|
+
applicationId: spec.loginApplicationId,
|
|
1351
|
+
...loginClientSecret ? { clientSecret: loginClientSecret } : {}
|
|
1352
|
+
} } : {},
|
|
1353
|
+
...input.protection ? { protection: { applicationId: spec.protectionApplicationId } } : {},
|
|
1354
|
+
serviceToken
|
|
1355
|
+
}, previous, (state) => input.save({ accessProvisioningJson: JSON.stringify(state) }));
|
|
1356
|
+
if (result.credentials) {
|
|
1357
|
+
const accessCredentialsJson = JSON.stringify(result.credentials);
|
|
1358
|
+
input.save({ accessCredentialsJson });
|
|
1359
|
+
activatePlatformAccessCredentials(accessCredentialsJson, [
|
|
1360
|
+
input.plan.apiUrl,
|
|
1361
|
+
input.plan.proxyUrl,
|
|
1362
|
+
...input.additionalOrigins ?? []
|
|
1363
|
+
]);
|
|
1364
|
+
}
|
|
1365
|
+
return result;
|
|
1366
|
+
}
|
|
1367
|
+
//#endregion
|
|
1368
|
+
//#region src/cli/platform-auth.ts
|
|
1369
|
+
function assertPlatformOperatorCredentialCanApplyPlan(credential, plan) {
|
|
1370
|
+
if (credential.source !== "oauth") return;
|
|
1371
|
+
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1372
|
+
const writesDns = plan.actions.some((action) => action.kind === "dns-record" && action.action !== "reuse");
|
|
1373
|
+
if (!createsZone && !writesDns) return;
|
|
1374
|
+
const permissions = [...createsZone ? ["Account → Zone → Edit"] : [], ...writesDns ? ["Zone → DNS → Edit"] : []];
|
|
1375
|
+
throw new Error(`platform: Cloudflare browser OAuth grants Zone Read and cannot apply the planned zone or DNS changes. Set CLOUDFLARE_API_TOKEN with ${permissions.join(" and ")}, scoped to the selected account and application zone, then rerun the command.`);
|
|
1376
|
+
}
|
|
1377
|
+
async function resolvePlatformCloudflareCredential(dependencies = {}) {
|
|
1378
|
+
const { credential } = await resolvePlatformCloudflareCredentialWithInput(async () => void 0, dependencies);
|
|
1379
|
+
return credential;
|
|
1380
|
+
}
|
|
1381
|
+
/** Check saved credentials during input, but never start browser login until it succeeds. */
|
|
1382
|
+
async function resolvePlatformCloudflareCredentialWithInput(readInput, dependencies = {}) {
|
|
1383
|
+
const controller = new AbortController();
|
|
1384
|
+
const runner = dependencies.runWrangler ?? ((args) => runCloudflareAuthCommand(process.cwd(), args, controller.signal));
|
|
1385
|
+
const runWrangler = (args, capture) => {
|
|
1386
|
+
controller.signal.throwIfAborted();
|
|
1387
|
+
return runner(args, capture);
|
|
1388
|
+
};
|
|
1389
|
+
const pending = readExistingCredential(dependencies, runWrangler).then((credential) => ({
|
|
1390
|
+
ok: true,
|
|
1391
|
+
credential
|
|
1392
|
+
}), (error) => ({
|
|
1393
|
+
ok: false,
|
|
1394
|
+
error
|
|
1395
|
+
}));
|
|
1396
|
+
const cancel = () => controller.abort();
|
|
1397
|
+
const assertNotCancelled = () => {
|
|
1398
|
+
if (controller.signal.aborted) throw new Error("platform: Cancelled.");
|
|
1399
|
+
};
|
|
1400
|
+
try {
|
|
1401
|
+
const input = await readInput();
|
|
1402
|
+
process.once("SIGINT", cancel);
|
|
1403
|
+
const result = await pending;
|
|
1404
|
+
assertNotCancelled();
|
|
1405
|
+
if (!result.ok) throw result.error;
|
|
1406
|
+
if (result.credential) return {
|
|
1407
|
+
input,
|
|
1408
|
+
credential: result.credential
|
|
1409
|
+
};
|
|
1410
|
+
announceCloudflareLogin();
|
|
1411
|
+
const login = await runWrangler(["login", "--use-keyring"], true);
|
|
1412
|
+
assertNotCancelled();
|
|
1413
|
+
if (login.status !== 0) throw new Error("platform: Cloudflare browser login did not complete.");
|
|
1414
|
+
const credential = await readWranglerOAuthCredential(runWrangler);
|
|
1415
|
+
assertNotCancelled();
|
|
1416
|
+
if (!credential) throw new Error("platform: Cloudflare sign-in completed without a usable OAuth credential.");
|
|
1417
|
+
return {
|
|
1418
|
+
input,
|
|
1419
|
+
credential
|
|
1420
|
+
};
|
|
1421
|
+
} finally {
|
|
1422
|
+
process.removeListener("SIGINT", cancel);
|
|
1423
|
+
controller.abort();
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
async function readExistingCredential(dependencies, runWrangler) {
|
|
1427
|
+
const environmentToken = process.env.CLOUDFLARE_API_TOKEN?.trim() || process.env.CF_API_TOKEN?.trim() || "";
|
|
1428
|
+
if (environmentToken) return {
|
|
1429
|
+
source: "environment",
|
|
1430
|
+
token: environmentToken
|
|
1431
|
+
};
|
|
1432
|
+
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.");
|
|
1433
|
+
if ((await runWrangler([
|
|
1434
|
+
"auth",
|
|
1435
|
+
"keyring",
|
|
1436
|
+
"enable"
|
|
1437
|
+
], true)).status !== 0) throw new Error("platform: Could not enable secure Cloudflare credential storage. Refusing to use a plaintext OAuth credential.");
|
|
1438
|
+
return await readWranglerOAuthCredential(runWrangler);
|
|
1439
|
+
}
|
|
1440
|
+
async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflareAuthCommand(process.cwd(), args)) {
|
|
1441
|
+
const result = await runWrangler([
|
|
1442
|
+
"auth",
|
|
1443
|
+
"token",
|
|
1444
|
+
"--json"
|
|
1445
|
+
], true);
|
|
1446
|
+
if (result.status !== 0) return null;
|
|
1447
|
+
try {
|
|
1448
|
+
const parsed = JSON.parse(result.stdout);
|
|
1449
|
+
if (parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
1450
|
+
return {
|
|
1451
|
+
source: "oauth",
|
|
1452
|
+
token: parsed.token.trim()
|
|
1453
|
+
};
|
|
1454
|
+
} catch {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
//#endregion
|
|
1459
|
+
//#region src/cli/platform-access-preflight.ts
|
|
1460
|
+
/** Recheck live protection without invoking any provisioning or configuration write. */
|
|
1461
|
+
async function verifyInstalledPlatformAccessProtection(input) {
|
|
1462
|
+
const protection = parsePlatformAccessProtection(input.protection);
|
|
1463
|
+
if (!protection) return;
|
|
1464
|
+
const encoded = input.checkpoint.accessProvisioningJson;
|
|
1465
|
+
const specEncoded = input.checkpoint.accessSetupSpecJson;
|
|
1466
|
+
if (!encoded || !specEncoded) throw new Error("platform: Restore the current encrypted Access setup checkpoint before maintaining this protected installation.");
|
|
1467
|
+
platformAccessResourceInventory(encoded, input.manifest.id);
|
|
1468
|
+
const state = JSON.parse(encoded);
|
|
1469
|
+
const spec = parsePlatformAccessSetupSpec(JSON.parse(specEncoded));
|
|
1470
|
+
if (state.accountId !== (spec.accountId ?? input.manifest.account.id) || state.nonce !== createHash("sha256").update(`void-access:${input.manifest.ownershipNonce}`).digest("hex").slice(0, 32)) throw new Error("platform: The saved Access setup does not match this installation and identity account.");
|
|
1471
|
+
const application = state.resources.protection;
|
|
1472
|
+
const service = state.resources["service-token"];
|
|
1473
|
+
if (!application?.id || application.pending || !service?.id || service.pending || spec.protectionApplicationId && spec.protectionApplicationId !== application.id) throw new Error("platform: Complete or restore the Access application and service-token checkpoint before maintenance.");
|
|
1474
|
+
if (!input.client.request || !input.client.listWorkerDomains || !input.client.listWorkerServiceRoutes) throw new Error("platform: Read-only Access and Worker origin verification is required before maintaining protection.");
|
|
1475
|
+
const token = process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN || input.checkpoint.accessManagementToken;
|
|
1476
|
+
const access = new PlatformAccessClient(token ? new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }) : { request: input.client.request.bind(input.client) }, state.accountId);
|
|
1477
|
+
const policy = {
|
|
1478
|
+
identityProviderIds: spec.identityProviderIds ?? [],
|
|
1479
|
+
policyIds: spec.policyIds ?? []
|
|
1480
|
+
};
|
|
1481
|
+
if ((await inspectPlatformAccess(access, policy)).teamDomain !== protection.teamDomain) throw new Error("platform: The live Access team domain does not match the active platform protection.");
|
|
1482
|
+
const app = await access.get(`apps/${encodeURIComponent(application.id)}`);
|
|
1483
|
+
if (!app || app.id !== application.id || app.name !== application.name) throw new Error("platform: The saved Access protection application no longer matches its resource.");
|
|
1484
|
+
await verifyPlatformAccessApplication(access, policy, app, "self_hosted", service.id);
|
|
1485
|
+
const origins = protection.applications.map((entry) => entry.origin);
|
|
1486
|
+
if ([
|
|
1487
|
+
input.plan.apiUrl,
|
|
1488
|
+
input.plan.proxyUrl,
|
|
1489
|
+
...input.manifest.endpoints.dashboardUrl ? [input.manifest.endpoints.dashboardUrl] : []
|
|
1490
|
+
].map((value) => new URL(value).origin).some((origin) => !origins.includes(origin)) || protection.applications.some((entry) => entry.audience !== app.aud || !platformAccessPublicDestinations(app).includes(new URL(entry.origin).hostname))) throw new Error("platform: Access audiences and complete origin coverage must match the active platform protection. Restore the current Access checkpoint or repair its applications.");
|
|
1491
|
+
for (const other of await access.list("apps")) {
|
|
1492
|
+
if (other.id === app.id) continue;
|
|
1493
|
+
if (platformAccessPublicDestinations(other).some((destination) => origins.some((origin) => platformAccessOverlapsOrigin(destination, origin)) && (destination.includes("/") || origins.some((origin) => destination === new URL(origin).hostname)))) throw new Error("platform: Another Access application overlaps a protected platform origin. Reconcile it before maintenance.");
|
|
1494
|
+
}
|
|
1495
|
+
const liveToken = (await access.list("service_tokens")).find((entry) => entry.id === service.id);
|
|
1496
|
+
const expiry = liveToken?.expires_at;
|
|
1497
|
+
if (!liveToken || liveToken.enabled === false || typeof expiry === "string" && (!Number.isFinite(Date.parse(expiry)) || Date.parse(expiry) <= Date.now())) throw new Error("platform: The platform Access service token is unavailable or expired. Restore its admitted credentials before maintenance.");
|
|
1498
|
+
for (const origin of origins) {
|
|
1499
|
+
const headers = cfAccessHeaders(input.checkpoint.accessCredentialsJson ? { VOID_ACCESS_CREDENTIALS: input.checkpoint.accessCredentialsJson } : process.env, origin);
|
|
1500
|
+
if (headers["CF-Access-Client-Id"] !== liveToken.client_id || !headers["CF-Access-Client-Secret"]) throw new Error("platform: Restore the origin-scoped Access service credentials admitted by the active protection application.");
|
|
1501
|
+
}
|
|
1502
|
+
const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
|
|
1503
|
+
if (existingWorkers.length) {
|
|
1504
|
+
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-Drl0qhrn.mjs");
|
|
1505
|
+
await verifyProtectionWorkerOrigins({
|
|
1506
|
+
request: input.client.request.bind(input.client),
|
|
1507
|
+
listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
|
|
1508
|
+
listWorkerServiceRoutes: input.client.listWorkerServiceRoutes.bind(input.client)
|
|
1509
|
+
}, input.manifest, origins, false, { workerNames: existingWorkers });
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/cli/platform-auth-bootstrap.ts
|
|
1514
|
+
/** Checkpoint a replacement before submission so an interrupted response is safely replayable. */
|
|
1515
|
+
async function resumeInstalledPlatformAuthentication(input, initialize = initializeInstalledPlatformAuthentication) {
|
|
1516
|
+
const result = await initialize(input.apiUrl, input.installationId, input.setupToken, input.setupJson);
|
|
1517
|
+
if (!result.setupPending || !result.expiresAt || Date.parse(result.expiresAt) > Date.now()) return result;
|
|
1518
|
+
const authBootstrapCode = randomBytes(32).toString("hex");
|
|
1519
|
+
const authenticationSetupJson = JSON.stringify({
|
|
1520
|
+
...parsePlatformAuthenticationSetup(JSON.parse(input.setupJson)),
|
|
1521
|
+
resetPending: true,
|
|
1522
|
+
bootstrapCredentialHash: createHash("sha256").update(authBootstrapCode).digest("hex")
|
|
1523
|
+
});
|
|
1524
|
+
input.checkpoint({
|
|
1525
|
+
authenticationSetupJson,
|
|
1526
|
+
authBootstrapCode
|
|
1527
|
+
});
|
|
1528
|
+
return initialize(input.apiUrl, input.installationId, input.setupToken, authenticationSetupJson);
|
|
1529
|
+
}
|
|
1530
|
+
async function initializeInstalledPlatformAuthentication(apiUrl, installationId, setupToken, setupJson, fetcher = fetch) {
|
|
1531
|
+
const origin = normalizePlatformApiUrl(apiUrl);
|
|
1532
|
+
const setup = parsePlatformAuthenticationSetup(JSON.parse(setupJson));
|
|
1533
|
+
if (setup.protection) ensurePlatformAccessToken(origin, false, true);
|
|
1534
|
+
const body = JSON.stringify(setup);
|
|
1535
|
+
const signature = createHmac("sha256", setupToken).update(`${PLATFORM_AUTH_SETUP_PROOF_PREFIX}\n${installationId}\n${body}`).digest("base64url");
|
|
1536
|
+
const response = await fetcher(`${origin}/internal/platform/auth-initialize`, {
|
|
1537
|
+
method: "POST",
|
|
1538
|
+
headers: {
|
|
1539
|
+
"Content-Type": "application/json",
|
|
1540
|
+
"X-Void-Auth-Setup": signature,
|
|
1541
|
+
...cfAccessHeaders(process.env, origin)
|
|
1542
|
+
},
|
|
1543
|
+
body,
|
|
1544
|
+
redirect: "manual",
|
|
1545
|
+
signal: AbortSignal.timeout(3e4)
|
|
1546
|
+
});
|
|
1547
|
+
if (!response.ok) throw new Error(`platform: Authentication setup failed (HTTP ${response.status}). Its outcome may be unknown; preserve the encrypted setup checkpoint and resume installation.`);
|
|
1548
|
+
const result = await response.json();
|
|
1549
|
+
if (typeof result.setupPending !== "boolean" || result.setupPending && (typeof result.expiresAt !== "string" || !Number.isFinite(Date.parse(result.expiresAt)))) throw new Error("platform: The platform returned an invalid authentication setup response.");
|
|
1550
|
+
return {
|
|
1551
|
+
setupPending: result.setupPending,
|
|
1552
|
+
expiresAt: typeof result.expiresAt === "string" ? result.expiresAt : null
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
//#endregion
|
|
1556
|
+
//#region src/cli/platform-auth-database.ts
|
|
1557
|
+
/** Authentication requires fresh reads; mutate only the installation-owned configuration. */
|
|
1558
|
+
async function verifyPlatformAuthenticationDatabase(client, accountId, resource, databaseUrl) {
|
|
1559
|
+
if (resource.kind !== "hyperdrive" || resource.key !== "hyperdrive" || resource.state !== "ready") throw new Error("platform: Authentication database verification requires the platform Hyperdrive resource.");
|
|
1560
|
+
const expected = hyperdriveOriginFromConnectionString(databaseUrl);
|
|
1561
|
+
const read = async () => {
|
|
1562
|
+
const current = await client.getHyperdriveConfig(accountId, resource.id);
|
|
1563
|
+
if (current.id !== resource.id || current.name !== resource.name || !hyperdriveOriginMatches(current.origin, expected)) throw new Error("platform: Hyperdrive does not match this installation’s pinned database. No authentication configuration was activated.");
|
|
1564
|
+
return current;
|
|
1565
|
+
};
|
|
1566
|
+
if ((await read()).cachingDisabled === true) return;
|
|
1567
|
+
if (resource.ownership !== "created") throw new Error("platform: The adopted Hyperdrive must have SQL result caching disabled by its owner before authentication can be configured.");
|
|
1568
|
+
await client.disableHyperdriveCaching(accountId, resource.id);
|
|
1569
|
+
if ((await read()).cachingDisabled !== true) throw new Error("platform: Could not verify uncached Hyperdrive reads. Authentication setup remains closed.");
|
|
1570
|
+
}
|
|
1571
|
+
//#endregion
|
|
786
1572
|
//#region src/cli/platform-plan.ts
|
|
787
1573
|
async function createPlatformInstallPlan(client, input, existing, options) {
|
|
788
1574
|
const account = resolveAccount(await client.listAccounts(), input.accountId);
|
|
789
1575
|
if (existing && existing.account.id !== account.id) throw new Error(`platform: Refusing to resume installation ${existing.id} in a different Cloudflare account.`);
|
|
1576
|
+
const providedEmailSharedZoneId = input.emailSharedZoneId?.trim();
|
|
1577
|
+
const providedEmailSenderDomain = input.emailSenderDomain?.trim();
|
|
1578
|
+
if (existing?.deployment.emailSharedZoneId && providedEmailSharedZoneId && existing.deployment.emailSharedZoneId !== providedEmailSharedZoneId) throw new Error("platform: The shared email zone is already recorded for this installation.");
|
|
1579
|
+
if (existing?.deployment.emailSenderDomain && providedEmailSenderDomain && existing.deployment.emailSenderDomain !== normalizeApplicationDomain(providedEmailSenderDomain)) throw new Error("platform: The shared email sender domain is already recorded for this installation.");
|
|
790
1580
|
const installationSlug = normalizeInstallationSlug(input.installationSlug);
|
|
791
1581
|
const installationId = existing?.id ?? `${installationSlug}-${createHash("sha256").update(JSON.stringify({
|
|
792
1582
|
accountId: account.id,
|
|
@@ -802,7 +1592,24 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
802
1592
|
} : void 0);
|
|
803
1593
|
if (workersDev && workersDev.subdomain !== accountSubdomain) throw new Error("platform: The account workers.dev subdomain changed. Restore it before continuing.");
|
|
804
1594
|
const testing = !!workersDev && !input.zoneName;
|
|
1595
|
+
const emailSharedZoneId = (existing?.deployment.emailSharedZoneId ?? providedEmailSharedZoneId)?.trim();
|
|
1596
|
+
const emailSenderDomain = existing?.deployment.emailSenderDomain ?? providedEmailSenderDomain;
|
|
1597
|
+
if (!!emailSharedZoneId !== !!emailSenderDomain) throw new Error("platform: Set VOID_EMAIL_SENDER_DOMAIN and VOID_EMAIL_SHARED_ZONE_ID together to enable email.");
|
|
1598
|
+
const normalizedEmailSenderDomain = emailSenderDomain ? normalizeApplicationDomain(emailSenderDomain) : void 0;
|
|
1599
|
+
const enableEmail = options?.includeEmailGateway !== false && !!emailSharedZoneId && !!normalizedEmailSenderDomain;
|
|
1600
|
+
const capabilities = {
|
|
1601
|
+
...existing?.deployment.capabilities,
|
|
1602
|
+
...options?.enableSandbox ? { sandbox: true } : {},
|
|
1603
|
+
...input.authentication ? {
|
|
1604
|
+
githubOAuth: input.authentication.providers.includes("github"),
|
|
1605
|
+
googleOAuth: input.authentication.providers.includes("google")
|
|
1606
|
+
} : {},
|
|
1607
|
+
...enableEmail ? { email: true } : {}
|
|
1608
|
+
};
|
|
1609
|
+
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.");
|
|
805
1610
|
const deployment = createPlatformDeploymentManifest({
|
|
1611
|
+
authentication: input.authentication ?? existing?.deployment.authentication,
|
|
1612
|
+
...Object.keys(capabilities).length > 0 ? { capabilities } : {},
|
|
806
1613
|
application: {
|
|
807
1614
|
baseDomain: testing ? `${accountSubdomain}.workers.dev` : input.applicationDomain,
|
|
808
1615
|
dedicatedZone: input.dedicatedZone,
|
|
@@ -816,10 +1623,28 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
816
1623
|
proxyHostname: null
|
|
817
1624
|
},
|
|
818
1625
|
displayName: input.displayName,
|
|
1626
|
+
...emailSharedZoneId ? { emailSharedZoneId } : {},
|
|
1627
|
+
...normalizedEmailSenderDomain ? { emailSenderDomain: normalizedEmailSenderDomain } : {},
|
|
819
1628
|
installationSlug,
|
|
820
1629
|
resources: existing?.deployment.resources ?? createPlatformResourceNames(installationSlug)
|
|
821
1630
|
});
|
|
822
|
-
|
|
1631
|
+
const { emailSharedZoneId: configuredEmailZone, emailSenderDomain: configuredSenderDomain, ...deploymentWithoutEmailZone } = deployment;
|
|
1632
|
+
const { emailSharedZoneId: recordedEmailZone, emailSenderDomain: recordedSenderDomain, ...existingWithoutEmailZone } = existing?.deployment ?? deployment;
|
|
1633
|
+
const existingWithSandbox = existing && options?.enableSandbox ? {
|
|
1634
|
+
...existingWithoutEmailZone,
|
|
1635
|
+
capabilities: {
|
|
1636
|
+
...existingWithoutEmailZone.capabilities,
|
|
1637
|
+
sandbox: true
|
|
1638
|
+
}
|
|
1639
|
+
} : existingWithoutEmailZone;
|
|
1640
|
+
const comparableExistingWithoutEmailZone = existing && deployment.capabilities.email && !existing.deployment.capabilities.email ? {
|
|
1641
|
+
...existingWithSandbox,
|
|
1642
|
+
capabilities: {
|
|
1643
|
+
...existingWithSandbox.capabilities,
|
|
1644
|
+
email: true
|
|
1645
|
+
}
|
|
1646
|
+
} : existingWithSandbox;
|
|
1647
|
+
if (existing && (JSON.stringify(comparableExistingWithoutEmailZone) !== JSON.stringify(deploymentWithoutEmailZone) || recordedEmailZone !== void 0 && recordedEmailZone !== configuredEmailZone || recordedSenderDomain !== void 0 && recordedSenderDomain !== configuredSenderDomain)) throw new Error(`platform: --resume inputs do not match installation ${existing.id}. Use its recorded configuration.`);
|
|
823
1648
|
const names = deployment.resources;
|
|
824
1649
|
const wildcardHostname = `*.${deployment.application.baseDomain}`;
|
|
825
1650
|
const zone = testing ? null : await client.findZone(account.id, deployment.application.zoneName);
|
|
@@ -871,6 +1696,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
871
1696
|
for (const [key, name] of [
|
|
872
1697
|
["api-worker", names.apiWorker],
|
|
873
1698
|
["proxy-worker", names.proxyWorker],
|
|
1699
|
+
["email-gateway-worker", names.emailGatewayWorker],
|
|
874
1700
|
["tail-worker", names.tailWorker],
|
|
875
1701
|
["dispatch-worker", names.dispatchWorker]
|
|
876
1702
|
]) if (workers.includes(name) && hasUnrecordedProvisionIntent(existing, key, "worker", name)) {
|
|
@@ -936,6 +1762,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
936
1762
|
for (const [key, name] of [
|
|
937
1763
|
["api-worker", names.apiWorker],
|
|
938
1764
|
["proxy-worker", names.proxyWorker],
|
|
1765
|
+
...options?.includeEmailGateway === false ? [] : [["email-gateway-worker", names.emailGatewayWorker]],
|
|
939
1766
|
["tail-worker", names.tailWorker],
|
|
940
1767
|
["dispatch-worker", names.dispatchWorker]
|
|
941
1768
|
]) addNamedPlan(actions, existing, key, "worker", name, workers.includes(name) ? [{
|
|
@@ -972,7 +1799,11 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
|
|
|
972
1799
|
displayName: deployment.displayName,
|
|
973
1800
|
installationSlug: deployment.installationSlug,
|
|
974
1801
|
zoneName: deployment.application.zoneName
|
|
975
|
-
}, manifest,
|
|
1802
|
+
}, manifest, {
|
|
1803
|
+
...options,
|
|
1804
|
+
includeEmailGateway: original.actions.some((entry) => entry.key === "email-gateway-worker"),
|
|
1805
|
+
enableSandbox: original.deployment.capabilities.sandbox
|
|
1806
|
+
})).actions.find((entry) => entry.key === action.key);
|
|
976
1807
|
if (!current || current.kind !== action.kind || current.name !== action.name || current.action !== action.action) throw new Error(`platform: Cloudflare changed after the installation plan was approved for ${action.kind} ${JSON.stringify(action.name)}. Review a new plan before retrying.`);
|
|
977
1808
|
if (action.kind === "worker" && action.action === "update") {
|
|
978
1809
|
if (!matchesResourceMarker((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, action.key, "worker", {
|
|
@@ -1171,6 +2002,7 @@ const RESOURCE_NAME_KEY_BY_ACTION = {
|
|
|
1171
2002
|
"isr-cache-kv": "isrCacheKv",
|
|
1172
2003
|
"legacy-assets-kv": "legacyAssetsKv",
|
|
1173
2004
|
"proxy-worker": "proxyWorker",
|
|
2005
|
+
"email-gateway-worker": "emailGatewayWorker",
|
|
1174
2006
|
"routing-kv": "routingKv",
|
|
1175
2007
|
"tail-worker": "tailWorker",
|
|
1176
2008
|
"usage-queue": "usageQueue"
|
|
@@ -1214,6 +2046,12 @@ function formatPlatformInstallPlan(plan) {
|
|
|
1214
2046
|
plan.deployment.application.zoneName ? `${import_picocolors$1.default.bold("Applications:")} *.${plan.deployment.application.baseDomain}` : `${import_picocolors$1.default.bold("Applications:")} workers.dev (testing); add a domain with void platform domain set <domain>`,
|
|
1215
2047
|
`${import_picocolors$1.default.bold("Control plane:")} ${plan.apiUrl}`,
|
|
1216
2048
|
`${import_picocolors$1.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
|
|
2049
|
+
`${import_picocolors$1.default.bold("Login methods:")} ${plan.deployment.authentication.providers.join(", ")}`,
|
|
2050
|
+
`${import_picocolors$1.default.bold("Access protection:")} ${plan.deployment.authentication.accessProtection ? "Enabled" : "Not enabled"}`,
|
|
2051
|
+
`${import_picocolors$1.default.bold("Login callback:")} ${plan.apiUrl}/auth/callback`,
|
|
2052
|
+
...plan.deployment.authentication.providers.includes("cloudflare-access") ? ["Access login: create a dedicated OIDC application or connect an existing issuer."] : [],
|
|
2053
|
+
...plan.deployment.authentication.accessProtection ? [`Access protection: verify an application covering ${plan.apiUrl} and ${plan.proxyUrl}, with a separate scoped service token for installation and CI.`] : [],
|
|
2054
|
+
`${import_picocolors$1.default.bold("Database:")} Verify uncached Hyperdrive reads for authentication`,
|
|
1217
2055
|
""
|
|
1218
2056
|
];
|
|
1219
2057
|
for (const action of plan.actions) lines.push(`${action.action.padEnd(6)} ${action.kind.padEnd(19)} ${action.name}`);
|
|
@@ -1228,6 +2066,7 @@ const COMPLETE_WORKER_SECRET_UPDATE_NAMES = [
|
|
|
1228
2066
|
"R2_ACCESS_KEY_ID",
|
|
1229
2067
|
"R2_SECRET_ACCESS_KEY"
|
|
1230
2068
|
];
|
|
2069
|
+
const EMAIL_WORKER_SECRET_UPDATE_NAME = "EMAIL_SIGNING_SECRET";
|
|
1231
2070
|
/**
|
|
1232
2071
|
* Prevents a host without the encrypted values for an interrupted credential
|
|
1233
2072
|
* update from carrying an opaque, possibly partial update into another cutover.
|
|
@@ -1236,7 +2075,7 @@ function assertPlatformWorkerSecretUpdateAuthority(input) {
|
|
|
1236
2075
|
const unresolved = unresolvedWorkerSecretUpdate(input.manifest);
|
|
1237
2076
|
if (!unresolved) return;
|
|
1238
2077
|
const latestWorkerGeneration = readLatestWorkerGeneration(input.manifest);
|
|
1239
|
-
if (unresolved.generation === latestWorkerGeneration && input.localIntent?.id === unresolved.id && (input.localIntent.generation === null || input.localIntent.generation === unresolved.generation) || hasCompleteWorkerSecretReplacement(input.explicitUpdates)) return;
|
|
2078
|
+
if (unresolved.generation === latestWorkerGeneration && input.localIntent?.id === unresolved.id && (input.localIntent.generation === null || input.localIntent.generation === unresolved.generation) || hasCompleteWorkerSecretReplacement(input.explicitUpdates, input.requiresEmailSigningSecret)) return;
|
|
1240
2079
|
throw new Error("platform: An unresolved Worker credential update is owned by another encrypted recovery checkpoint. Resume on the host that owns that checkpoint, restore its encrypted pending recovery material, or provide all supported Worker credential values together as a complete replacement.");
|
|
1241
2080
|
}
|
|
1242
2081
|
function unresolvedWorkerSecretUpdate(manifest) {
|
|
@@ -1264,8 +2103,8 @@ function readLatestWorkerGeneration(manifest) {
|
|
|
1264
2103
|
return match ? [Number(match[1])] : [];
|
|
1265
2104
|
}));
|
|
1266
2105
|
}
|
|
1267
|
-
function hasCompleteWorkerSecretReplacement(updates) {
|
|
1268
|
-
return COMPLETE_WORKER_SECRET_UPDATE_NAMES.every((name) => typeof updates[name] === "string" && updates[name] !== "");
|
|
2106
|
+
function hasCompleteWorkerSecretReplacement(updates, requiresEmailSigningSecret) {
|
|
2107
|
+
return COMPLETE_WORKER_SECRET_UPDATE_NAMES.every((name) => typeof updates[name] === "string" && updates[name] !== "") && (!requiresEmailSigningSecret || typeof updates[EMAIL_WORKER_SECRET_UPDATE_NAME] === "string" && updates[EMAIL_WORKER_SECRET_UPDATE_NAME] !== "");
|
|
1269
2108
|
}
|
|
1270
2109
|
//#endregion
|
|
1271
2110
|
//#region src/cli/platform-traffic.ts
|
|
@@ -1339,11 +2178,29 @@ function recordPlatformWorkerTraffic(manifest, workerName, inventory, updatedAt)
|
|
|
1339
2178
|
const REMOTE_INSTALLATION_MANIFEST_KEY = PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY;
|
|
1340
2179
|
const PLATFORM_WORKER_SECRET_UPDATE_NAMES = [
|
|
1341
2180
|
"CF_API_TOKEN",
|
|
2181
|
+
"EMAIL_SIGNING_SECRET",
|
|
1342
2182
|
"GITHUB_CLIENT_ID",
|
|
1343
2183
|
"GITHUB_CLIENT_SECRET",
|
|
1344
2184
|
"R2_ACCESS_KEY_ID",
|
|
1345
2185
|
"R2_SECRET_ACCESS_KEY"
|
|
1346
2186
|
];
|
|
2187
|
+
/** Preserve database-authoritative authentication choices while upgrading old manifests. */
|
|
2188
|
+
function mergePlatformLiveAuthenticationDeployment(deployment, live) {
|
|
2189
|
+
const providers = live.enabledProviders.length > 0 ? live.enabledProviders : deployment.authentication.providers;
|
|
2190
|
+
return createPlatformDeploymentManifest({
|
|
2191
|
+
...deployment,
|
|
2192
|
+
authentication: {
|
|
2193
|
+
providers,
|
|
2194
|
+
...live.sessionsRequired ? { configurable: true } : {},
|
|
2195
|
+
...live.protectedByAccess ? { accessProtection: true } : {}
|
|
2196
|
+
},
|
|
2197
|
+
capabilities: {
|
|
2198
|
+
...deployment.capabilities,
|
|
2199
|
+
githubOAuth: providers.includes("github"),
|
|
2200
|
+
googleOAuth: providers.includes("google")
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
1347
2204
|
var PlatformWorkerRecoveryIncompleteError = class extends Error {
|
|
1348
2205
|
constructor(message, options) {
|
|
1349
2206
|
super(message, options);
|
|
@@ -1364,9 +2221,28 @@ function createPlatformInstallationSecrets(input) {
|
|
|
1364
2221
|
const existingInstallation = input.existingInstallation === true;
|
|
1365
2222
|
const restoredIdentity = (explicit, name) => existingInstallation ? checkpoint[name] || explicit || "" : explicit || checkpoint[name] || "";
|
|
1366
2223
|
const configured = (explicit, name) => explicit || checkpoint[name] || "";
|
|
2224
|
+
const authenticationSetupJson = configured(input.authenticationSetupJson ?? "", "authenticationSetupJson");
|
|
2225
|
+
const authSetupToken = configured(input.authSetupToken ?? "", "authSetupToken");
|
|
2226
|
+
const authBootstrapCode = configured(input.authBootstrapCode ?? "", "authBootstrapCode");
|
|
2227
|
+
const modernAuthentication = input.configurableAuthentication === true || !!authenticationSetupJson;
|
|
2228
|
+
if (modernAuthentication && !existingInstallation && (authSetupToken.length < 32 || !/^[a-f0-9]{64}$/.test(authBootstrapCode))) throw new Error("platform: Valid setup authority and an administrator setup code are required.");
|
|
2229
|
+
if (authenticationSetupJson) {
|
|
2230
|
+
const setup = parsePlatformAuthenticationSetup(JSON.parse(authenticationSetupJson));
|
|
2231
|
+
if (!existingInstallation && (authSetupToken.length < 32 || !/^[a-f0-9]{64}$/.test(authBootstrapCode) || createHash("sha256").update(authBootstrapCode).digest("hex") !== setup.bootstrapCredentialHash)) throw new Error("platform: Valid installation setup authority and a matching administrator setup code are required.");
|
|
2232
|
+
}
|
|
1367
2233
|
const jwtSecret = restoredIdentity(input.jwtSecret || "", "jwtSecret");
|
|
1368
2234
|
const resolvedJwtSecret = input.existingInstallation ? jwtSecret : required(jwtSecret, "VOID_PLATFORM_JWT_SECRET");
|
|
1369
2235
|
if (!existingInstallation && Buffer.byteLength(resolvedJwtSecret, "utf8") < 32) throw new Error("platform: VOID_PLATFORM_JWT_SECRET must contain at least 32 bytes of secret material unique to this installation.");
|
|
2236
|
+
const emailEnabled = input.emailEnabled === true;
|
|
2237
|
+
const existingEmailEnabled = input.existingEmailEnabled === true;
|
|
2238
|
+
const emailSigningSecretInScope = emailEnabled || existingEmailEnabled;
|
|
2239
|
+
let emailSigningSecret = emailSigningSecretInScope ? restoredIdentity(input.emailSigningSecret || "", "emailSigningSecret") : checkpoint.emailSigningSecret || "";
|
|
2240
|
+
if (emailEnabled && !emailSigningSecret) {
|
|
2241
|
+
if (existingInstallation && existingEmailEnabled) throw new Error("platform: VOID_PLATFORM_EMAIL_SIGNING_SECRET is required to maintain an email-enabled installation without its encrypted recovery state.");
|
|
2242
|
+
emailSigningSecret = randomBytes(32).toString("base64");
|
|
2243
|
+
}
|
|
2244
|
+
if (emailSigningSecretInScope && emailSigningSecret && Buffer.byteLength(emailSigningSecret, "utf8") < 32) throw new Error("platform: VOID_PLATFORM_EMAIL_SIGNING_SECRET must contain at least 32 bytes of secret material unique to this installation.");
|
|
2245
|
+
if (emailSigningSecretInScope && emailSigningSecret && emailSigningSecret === resolvedJwtSecret) throw new Error("platform: VOID_PLATFORM_EMAIL_SIGNING_SECRET must be different from VOID_PLATFORM_JWT_SECRET.");
|
|
1370
2246
|
const projectSecretKeyring = resolveProjectSecretKeyring({
|
|
1371
2247
|
checkpoint,
|
|
1372
2248
|
existingInstallation,
|
|
@@ -1376,22 +2252,34 @@ function createPlatformInstallationSecrets(input) {
|
|
|
1376
2252
|
required
|
|
1377
2253
|
});
|
|
1378
2254
|
const pendingWorkerSecretUpdateIntent = parseWorkerSecretUpdateIntent(checkpoint.workerSecretUpdatesJson);
|
|
2255
|
+
const emailSigningSecretUpdateAuthorized = emailSigningSecretInScope && !!emailSigningSecret && (emailEnabled && (!existingEmailEnabled || !checkpoint.emailSigningSecret) || !!input.emailSigningSecret);
|
|
1379
2256
|
const explicitWorkerSecretUpdates = withoutEmptySecrets({
|
|
1380
2257
|
CF_API_TOKEN: input.cloudflareApiToken,
|
|
2258
|
+
...emailSigningSecretUpdateAuthorized ? { EMAIL_SIGNING_SECRET: emailSigningSecret } : {},
|
|
1381
2259
|
GITHUB_CLIENT_ID: input.githubClientId,
|
|
1382
2260
|
GITHUB_CLIENT_SECRET: input.githubClientSecret,
|
|
1383
2261
|
R2_ACCESS_KEY_ID: input.r2AccessKeyId,
|
|
1384
2262
|
R2_SECRET_ACCESS_KEY: input.r2SecretAccessKey
|
|
1385
2263
|
});
|
|
1386
2264
|
return {
|
|
2265
|
+
authenticationPreparation: Object.fromEntries(Object.entries(checkpoint).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
2266
|
+
...modernAuthentication ? {
|
|
2267
|
+
authenticationSetupJson,
|
|
2268
|
+
authSetupToken,
|
|
2269
|
+
authBootstrapCode
|
|
2270
|
+
} : {},
|
|
2271
|
+
...checkpoint.accessProvisioningJson ? { accessProvisioningJson: checkpoint.accessProvisioningJson } : {},
|
|
2272
|
+
...checkpoint.accessCredentialsJson ? { accessCredentialsJson: checkpoint.accessCredentialsJson } : {},
|
|
1387
2273
|
workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
|
|
1388
2274
|
pendingWorkerSecretUpdateIntent: input.updateExistingWorkers ? pendingWorkerSecretUpdateIntent : void 0,
|
|
1389
|
-
bootstrapGitHubId: existingInstallation ? configured(input.bootstrapGitHubId, "bootstrapGitHubId") : required(configured(input.bootstrapGitHubId, "bootstrapGitHubId"), "resolved administrator GitHub account id"),
|
|
1390
|
-
bootstrapGitHubLogin: existingInstallation ? configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin") : required(configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin"), "VOID_PLATFORM_ADMIN_GITHUB_LOGIN"),
|
|
2275
|
+
bootstrapGitHubId: existingInstallation || modernAuthentication ? configured(input.bootstrapGitHubId, "bootstrapGitHubId") : required(configured(input.bootstrapGitHubId, "bootstrapGitHubId"), "resolved administrator GitHub account id"),
|
|
2276
|
+
bootstrapGitHubLogin: existingInstallation || modernAuthentication ? configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin") : required(configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin"), "VOID_PLATFORM_ADMIN_GITHUB_LOGIN"),
|
|
1391
2277
|
cloudflareApiToken: existingInstallation ? configured(input.cloudflareApiToken, "cloudflareApiToken") : required(configured(input.cloudflareApiToken, "cloudflareApiToken"), "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN"),
|
|
1392
2278
|
databaseUrl: required(input.databaseUrl || checkpoint.databaseUrl || "", "DATABASE_URL"),
|
|
1393
|
-
|
|
1394
|
-
|
|
2279
|
+
emailEnabled,
|
|
2280
|
+
emailSigningSecret,
|
|
2281
|
+
githubClientId: existingInstallation || modernAuthentication ? configured(input.githubClientId, "githubClientId") : required(configured(input.githubClientId, "githubClientId"), "VOID_PLATFORM_GITHUB_CLIENT_ID"),
|
|
2282
|
+
githubClientSecret: existingInstallation || modernAuthentication ? configured(input.githubClientSecret, "githubClientSecret") : required(configured(input.githubClientSecret, "githubClientSecret"), "VOID_PLATFORM_GITHUB_CLIENT_SECRET"),
|
|
1395
2283
|
jwtSecret: resolvedJwtSecret,
|
|
1396
2284
|
projectSecretActiveKeyVersion: projectSecretKeyring.activeVersion,
|
|
1397
2285
|
projectSecretKey: projectSecretKeyring.v1Key,
|
|
@@ -1450,6 +2338,14 @@ function assertCanonicalProjectSecretKey(value, name, uniqueToInstallation = fal
|
|
|
1450
2338
|
}
|
|
1451
2339
|
function secretCheckpoint(secrets, workerSecretUpdateIntent) {
|
|
1452
2340
|
return {
|
|
2341
|
+
...Object.fromEntries(Object.entries(secrets.authenticationPreparation ?? {}).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
2342
|
+
...secrets.authSetupToken ? {
|
|
2343
|
+
authenticationSetupJson: secrets.authenticationSetupJson ?? "",
|
|
2344
|
+
authSetupToken: secrets.authSetupToken ?? "",
|
|
2345
|
+
authBootstrapCode: secrets.authBootstrapCode ?? ""
|
|
2346
|
+
} : {},
|
|
2347
|
+
...secrets.accessProvisioningJson ? { accessProvisioningJson: secrets.accessProvisioningJson } : {},
|
|
2348
|
+
...secrets.accessCredentialsJson ? { accessCredentialsJson: secrets.accessCredentialsJson } : {},
|
|
1453
2349
|
bootstrapGitHubId: secrets.bootstrapGitHubId,
|
|
1454
2350
|
bootstrapGitHubLogin: secrets.bootstrapGitHubLogin,
|
|
1455
2351
|
cloudflareApiToken: secrets.cloudflareApiToken,
|
|
@@ -1457,6 +2353,7 @@ function secretCheckpoint(secrets, workerSecretUpdateIntent) {
|
|
|
1457
2353
|
githubClientId: secrets.githubClientId,
|
|
1458
2354
|
githubClientSecret: secrets.githubClientSecret,
|
|
1459
2355
|
jwtSecret: secrets.jwtSecret,
|
|
2356
|
+
emailSigningSecret: secrets.emailSigningSecret,
|
|
1460
2357
|
projectSecretActiveKeyVersion: secrets.projectSecretActiveKeyVersion,
|
|
1461
2358
|
projectSecretKey: secrets.projectSecretKey,
|
|
1462
2359
|
projectSecretKeysJson: secrets.projectSecretKeysJson,
|
|
@@ -1499,6 +2396,22 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1499
2396
|
const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
|
|
1500
2397
|
const runtimeDirectory = resolvedRuntime.directory;
|
|
1501
2398
|
const runtime = resolvedRuntime.manifest;
|
|
2399
|
+
const hasEmailGatewayArtifact = runtime.artifacts.emailGateway !== void 0;
|
|
2400
|
+
if (runtime.emailProtocolVersion !== 1 && plan.deployment.capabilities.email) throw new Error("platform: This runtime cannot serve enabled platform email without email protocol version 1.");
|
|
2401
|
+
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.");
|
|
2402
|
+
const workerKeys = hasEmailGatewayArtifact ? [
|
|
2403
|
+
"api",
|
|
2404
|
+
"proxy",
|
|
2405
|
+
"emailGateway",
|
|
2406
|
+
"tail",
|
|
2407
|
+
"dispatch"
|
|
2408
|
+
] : [
|
|
2409
|
+
"api",
|
|
2410
|
+
"proxy",
|
|
2411
|
+
"tail",
|
|
2412
|
+
"dispatch"
|
|
2413
|
+
];
|
|
2414
|
+
if (plan.deployment.authentication.configurable && runtime.authenticationProtocolVersion !== 1) throw new Error("platform: This runtime does not support configurable authentication. Upgrade Void or use a compatible runtime.");
|
|
1502
2415
|
if (plan.deployment.application.workersDev && runtime.applicationRoutingVersion !== 1) throw new Error("platform: This runtime does not support workers.dev application routing. Use a current platform runtime.");
|
|
1503
2416
|
verifyRuntimeArtifacts(runtimeDirectory, runtime);
|
|
1504
2417
|
const runtimeProvenance = {
|
|
@@ -1512,11 +2425,14 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1512
2425
|
const hadPriorWorkers = existing !== null && (existing.resources.some((resource) => resource.kind === "worker" && resource.state === "ready") || plan.actions.some((action) => action.kind === "worker" && action.action !== "create"));
|
|
1513
2426
|
const wasDisabled = existing?.status === "disabled";
|
|
1514
2427
|
const preserveDisabled = existing?.desiredState === "disabled";
|
|
2428
|
+
const shouldBootstrapSharedEmail = !preserveDisabled && plan.deployment.capabilities.email && runtime.emailProtocolVersion === 1 && !!plan.deployment.emailSharedZoneId;
|
|
1515
2429
|
const previousPublicEndpoints = stablePublicEndpointState(existing);
|
|
2430
|
+
const authenticationProtocolTransition = hadPriorWorkers && existing?.deployment.authentication.configurable !== true && runtime.authenticationProtocolVersion === 1;
|
|
1516
2431
|
let workerCutoverGeneration = 0;
|
|
1517
2432
|
let workerRollbacks = [];
|
|
1518
2433
|
let workerDeploymentCommitted = false;
|
|
1519
2434
|
let workerTrafficGuarded = false;
|
|
2435
|
+
let authenticationEnforcementActive = false;
|
|
1520
2436
|
let manifest = existing;
|
|
1521
2437
|
if (!manifest) {
|
|
1522
2438
|
const createdAt = now();
|
|
@@ -1548,7 +2464,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1548
2464
|
manifest = setInstallationDesiredState(manifest, "disabled", now());
|
|
1549
2465
|
writeInstallationManifest(manifest);
|
|
1550
2466
|
}
|
|
1551
|
-
assertRequiredPlatformSecrets(plan, manifest, secrets);
|
|
2467
|
+
assertRequiredPlatformSecrets(plan, manifest, secrets, !!dependencies.prepareAuthentication);
|
|
1552
2468
|
const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
|
|
1553
2469
|
writePendingInstallationSecrets(manifest, {
|
|
1554
2470
|
...pendingSecrets,
|
|
@@ -1581,22 +2497,83 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1581
2497
|
assertPlatformWorkerSecretUpdateAuthority({
|
|
1582
2498
|
explicitUpdates: explicitWorkerSecretUpdates,
|
|
1583
2499
|
localIntent: secrets.pendingWorkerSecretUpdateIntent,
|
|
1584
|
-
manifest
|
|
2500
|
+
manifest,
|
|
2501
|
+
requiresEmailSigningSecret: manifest.deployment.capabilities.email || plan.deployment.capabilities.email
|
|
1585
2502
|
});
|
|
1586
2503
|
} catch (error) {
|
|
1587
2504
|
await releaseDatabaseLock();
|
|
1588
2505
|
throw error;
|
|
1589
2506
|
}
|
|
1590
2507
|
if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
|
|
1591
|
-
|
|
2508
|
+
let workerDeploymentSecrets = {
|
|
1592
2509
|
...secrets,
|
|
1593
2510
|
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
1594
2511
|
};
|
|
1595
2512
|
try {
|
|
1596
2513
|
assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
|
|
1597
2514
|
if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
|
|
2515
|
+
const liveAuthentication = await releaseDatabaseLock.readAuthenticationState?.();
|
|
2516
|
+
if (liveAuthentication) {
|
|
2517
|
+
authenticationEnforcementActive = liveAuthentication.sessionsRequired;
|
|
2518
|
+
if (liveAuthentication.sessionsRequired && runtime.authenticationProtocolVersion !== 1) throw new Error("platform: This runtime cannot enforce the active authentication configuration. Use an authentication-compatible runtime.");
|
|
2519
|
+
const deployment = mergePlatformLiveAuthenticationDeployment(plan.deployment, liveAuthentication);
|
|
2520
|
+
plan = {
|
|
2521
|
+
...plan,
|
|
2522
|
+
deployment
|
|
2523
|
+
};
|
|
2524
|
+
if (!authenticationProtocolTransition) manifest = {
|
|
2525
|
+
...manifest,
|
|
2526
|
+
deployment
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
if (!liveAuthentication && manifest.deployment.authentication.configurable && !plan.deployment.authentication.configurable) {
|
|
2530
|
+
const authentication = manifest.deployment.authentication;
|
|
2531
|
+
const providers = authentication.providers;
|
|
2532
|
+
plan = {
|
|
2533
|
+
...plan,
|
|
2534
|
+
deployment: createPlatformDeploymentManifest({
|
|
2535
|
+
...plan.deployment,
|
|
2536
|
+
authentication,
|
|
2537
|
+
capabilities: {
|
|
2538
|
+
...plan.deployment.capabilities,
|
|
2539
|
+
githubOAuth: providers.includes("github"),
|
|
2540
|
+
googleOAuth: providers.includes("google")
|
|
2541
|
+
}
|
|
2542
|
+
})
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
if (authenticationProtocolTransition && !plan.deployment.authentication.configurable) {
|
|
2546
|
+
const providers = plan.deployment.authentication.providers;
|
|
2547
|
+
const deployment = createPlatformDeploymentManifest({
|
|
2548
|
+
...plan.deployment,
|
|
2549
|
+
authentication: {
|
|
2550
|
+
providers,
|
|
2551
|
+
configurable: true
|
|
2552
|
+
},
|
|
2553
|
+
capabilities: {
|
|
2554
|
+
...plan.deployment.capabilities,
|
|
2555
|
+
githubOAuth: providers.includes("github"),
|
|
2556
|
+
googleOAuth: providers.includes("google")
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
plan = {
|
|
2560
|
+
...plan,
|
|
2561
|
+
deployment
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
const activateAccessCredentials = () => {
|
|
2565
|
+
if (!secrets.accessCredentialsJson) return;
|
|
2566
|
+
const credentials = JSON.parse(secrets.accessCredentialsJson);
|
|
2567
|
+
if (!credentials || typeof credentials !== "object" || Array.isArray(credentials)) throw new Error("platform: Invalid saved Access credentials.");
|
|
2568
|
+
const origins = [plan.apiUrl, plan.proxyUrl];
|
|
2569
|
+
activatePlatformAccessCredentials(JSON.stringify(Object.fromEntries(Object.entries(credentials).filter(([origin]) => origins.includes(origin)))), origins);
|
|
2570
|
+
};
|
|
2571
|
+
activateAccessCredentials();
|
|
2572
|
+
manifest = checkpointAuthenticationResources(manifest, secrets, now());
|
|
2573
|
+
writeInstallationManifest(manifest);
|
|
2574
|
+
await releaseDatabaseLock.checkpoint?.(manifest);
|
|
1598
2575
|
dependencies.onProgress?.("Checking and creating Cloudflare resources");
|
|
1599
|
-
if (JSON.stringify(manifest.deployment) !== JSON.stringify(plan.deployment)) {
|
|
2576
|
+
if (!authenticationProtocolTransition && JSON.stringify(manifest.deployment) !== JSON.stringify(plan.deployment)) {
|
|
1600
2577
|
manifest = {
|
|
1601
2578
|
...manifest,
|
|
1602
2579
|
deployment: plan.deployment,
|
|
@@ -1610,6 +2587,61 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1610
2587
|
manifest = transitionInstallation(manifest, "provisioning", now());
|
|
1611
2588
|
writeInstallationManifest(manifest);
|
|
1612
2589
|
}
|
|
2590
|
+
if (dependencies.prepareAuthentication) {
|
|
2591
|
+
dependencies.onProgress?.("Preparing platform authentication");
|
|
2592
|
+
try {
|
|
2593
|
+
secrets = {
|
|
2594
|
+
...secrets,
|
|
2595
|
+
...await dependencies.prepareAuthentication(manifest, secrets)
|
|
2596
|
+
};
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
const latest = readPendingInstallationSecrets(manifest) ?? {};
|
|
2599
|
+
secrets = {
|
|
2600
|
+
...secrets,
|
|
2601
|
+
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
2602
|
+
authenticationSetupJson: latest.authenticationSetupJson || secrets.authenticationSetupJson,
|
|
2603
|
+
authSetupToken: latest.authSetupToken || secrets.authSetupToken,
|
|
2604
|
+
authBootstrapCode: latest.authBootstrapCode || secrets.authBootstrapCode,
|
|
2605
|
+
accessProvisioningJson: latest.accessProvisioningJson || secrets.accessProvisioningJson,
|
|
2606
|
+
accessCredentialsJson: latest.accessCredentialsJson || secrets.accessCredentialsJson
|
|
2607
|
+
};
|
|
2608
|
+
manifest = checkpointAuthenticationResources(manifest, secrets, now());
|
|
2609
|
+
writeInstallationManifest(manifest);
|
|
2610
|
+
await releaseDatabaseLock.checkpoint?.(manifest);
|
|
2611
|
+
throw error;
|
|
2612
|
+
}
|
|
2613
|
+
const latest = readPendingInstallationSecrets(manifest) ?? {};
|
|
2614
|
+
secrets = {
|
|
2615
|
+
...secrets,
|
|
2616
|
+
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name)))
|
|
2617
|
+
};
|
|
2618
|
+
manifest = checkpointAuthenticationResources(manifest, secrets, now());
|
|
2619
|
+
writeInstallationManifest(manifest);
|
|
2620
|
+
await releaseDatabaseLock.checkpoint?.(manifest);
|
|
2621
|
+
activateAccessCredentials();
|
|
2622
|
+
assertRequiredPlatformSecrets(plan, manifest, secrets);
|
|
2623
|
+
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.");
|
|
2624
|
+
workerDeploymentSecrets = {
|
|
2625
|
+
...secrets,
|
|
2626
|
+
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
2627
|
+
};
|
|
2628
|
+
writePendingInstallationSecrets(manifest, {
|
|
2629
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
2630
|
+
databaseClaimState: "claimed"
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
if (plan.deployment.authentication.accessProtection) {
|
|
2634
|
+
const protection = liveAuthentication ? liveAuthentication.protection : secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(secrets.authenticationSetupJson)).protection : null;
|
|
2635
|
+
if (!protection) throw new Error("platform: The active Access protection configuration is unavailable. Restore its database and encrypted setup checkpoint before maintenance.");
|
|
2636
|
+
dependencies.onProgress?.("Verifying current Access policies and protected origins");
|
|
2637
|
+
await (dependencies.verifyAccessProtection ?? verifyInstalledPlatformAccessProtection)({
|
|
2638
|
+
client: dependencies.client,
|
|
2639
|
+
manifest,
|
|
2640
|
+
plan,
|
|
2641
|
+
protection,
|
|
2642
|
+
checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent)
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
1613
2645
|
const routingAction = plan.actions.find((action) => action.key === "routing-kv");
|
|
1614
2646
|
if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
|
|
1615
2647
|
if (routingAction.action === "reuse") {
|
|
@@ -1692,6 +2724,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1692
2724
|
await writeR2OwnershipMarkerIfNeeded(dependencies.client, plan, manifest, action);
|
|
1693
2725
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
1694
2726
|
}
|
|
2727
|
+
dependencies.onProgress?.("Verifying uncached authentication database reads");
|
|
2728
|
+
const authenticationDatabase = manifest.resources.find((resource) => resource.key === "hyperdrive");
|
|
2729
|
+
if (!authenticationDatabase) throw new Error("platform: The authentication database resource is missing.");
|
|
2730
|
+
await verifyPlatformAuthenticationDatabase(dependencies.client, plan.account.id, authenticationDatabase, secrets.databaseUrl);
|
|
1695
2731
|
dependencies.onProgress?.("Checking the platform database schema");
|
|
1696
2732
|
const rollbackDatabase = dependencies.databaseRollbackSource;
|
|
1697
2733
|
const migrationStep = `database-schema:${runtime.database.schemaDigest}`;
|
|
@@ -1744,7 +2780,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1744
2780
|
const targetSandboxDrainProtocol = runtime.safetyProtocols?.sandboxDrain ?? 0;
|
|
1745
2781
|
const prepareSandboxDrain = dependencies.prepareSandboxDrain ?? releaseDatabaseLock.prepareSandboxDrain;
|
|
1746
2782
|
if (targetSandboxDrainProtocol >= 1 && !prepareSandboxDrain) throw new Error("platform: The PostgreSQL lifecycle lock cannot establish the sandbox-drain safety checkpoint.");
|
|
1747
|
-
const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
|
|
2783
|
+
const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 3 ? 3 : targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
|
|
2784
|
+
if (sandboxDrainPreparation && sandboxDrainPreparation.admissionProtocol < targetSandboxDrainProtocol) throw new Error("platform: The database did not arm the Sandbox admission protocol required by this runtime.");
|
|
1748
2785
|
const sandboxDrainRoutingKvId = requireId(Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id])), "routing-kv");
|
|
1749
2786
|
const writeSandboxDrainMirror = async (preparation) => {
|
|
1750
2787
|
await dependencies.client.putKvValue(plan.account.id, sandboxDrainRoutingKvId, PLATFORM_SANDBOX_DRAIN_KEY, preparation.mirrorValue);
|
|
@@ -1771,17 +2808,24 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1771
2808
|
manifest = recordInstallationStep(manifest, {
|
|
1772
2809
|
completedAt: now(),
|
|
1773
2810
|
key: bootstrapStep,
|
|
1774
|
-
resultHash: hash(secrets.bootstrapGitHubId)
|
|
2811
|
+
resultHash: hash(secrets.authenticationSetupJson ?? secrets.bootstrapGitHubId)
|
|
1775
2812
|
}, now());
|
|
1776
2813
|
writeInstallationManifest(manifest);
|
|
1777
2814
|
}
|
|
1778
2815
|
const entrypoints = Object.fromEntries(Object.entries(runtime.artifacts).map(([key, artifact]) => [key, join(runtimeDirectory, artifact.path)]));
|
|
1779
|
-
|
|
2816
|
+
let interruptedCutover = interruptedWorkerCutover(manifest);
|
|
2817
|
+
if (interruptedCutover && authenticationProtocolTransition && authenticationEnforcementActive) {
|
|
2818
|
+
manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "committed", now());
|
|
2819
|
+
manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, false, now());
|
|
2820
|
+
writeInstallationManifest(manifest);
|
|
2821
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
2822
|
+
interruptedCutover = null;
|
|
2823
|
+
}
|
|
1780
2824
|
const recoveringUnsafeTraffic = (interruptedCutover?.unsafeTraffic.size ?? 0) > 0;
|
|
1781
|
-
const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create");
|
|
2825
|
+
const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
1782
2826
|
const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
|
|
1783
2827
|
const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
|
|
1784
|
-
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || requiresSandboxDrain;
|
|
2828
|
+
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain;
|
|
1785
2829
|
if (requiresTrafficGuard) {
|
|
1786
2830
|
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
1787
2831
|
workerTrafficGuarded = true;
|
|
@@ -1797,6 +2841,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1797
2841
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
1798
2842
|
}
|
|
1799
2843
|
const configInput = {
|
|
2844
|
+
authenticationDatabaseUncached: true,
|
|
1800
2845
|
accountId: plan.account.id,
|
|
1801
2846
|
apiUrl: plan.apiUrl,
|
|
1802
2847
|
deployment: plan.deployment,
|
|
@@ -1805,6 +2850,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1805
2850
|
ownershipNonce: manifest.ownershipNonce,
|
|
1806
2851
|
platformVersion: version,
|
|
1807
2852
|
proxyUrl: plan.proxyUrl,
|
|
2853
|
+
emailGatewayUrl: `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev`,
|
|
1808
2854
|
resources: {
|
|
1809
2855
|
hyperdrive: requireId(ids, "hyperdrive"),
|
|
1810
2856
|
isrCacheKv: requireId(ids, "isr-cache-kv"),
|
|
@@ -1831,7 +2877,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1831
2877
|
trafficState: previousPublicEndpoints ? "enabled" : "staging"
|
|
1832
2878
|
});
|
|
1833
2879
|
const managedRoutes = platformManagedWorkerTraffic(plan.deployment).routes;
|
|
1834
|
-
let workerActions = plan.actions.filter((action) => action.kind === "worker");
|
|
2880
|
+
let workerActions = plan.actions.filter((action) => action.kind === "worker" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
1835
2881
|
if (interruptedCutover) {
|
|
1836
2882
|
for (const key of [...interruptedCutover.staged.keys()].reverse()) {
|
|
1837
2883
|
const snapshot = interruptedCutover.staged.get(key);
|
|
@@ -1866,7 +2912,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1866
2912
|
previousTriggers: mergePreservedTriggerRoutes(withoutPlatformCustomDomainTriggers(snapshot.previousTriggers), preservedRoutes),
|
|
1867
2913
|
previousVersionId: snapshot.previousVersionId,
|
|
1868
2914
|
preserveCustomDomains: interruptedCutover.unsafeTraffic.has(key),
|
|
1869
|
-
inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, `${key}-worker`, snapshot.workerName),
|
|
2915
|
+
inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`, snapshot.workerName),
|
|
1870
2916
|
removeCustomDomains: async () => {
|
|
1871
2917
|
if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
|
|
1872
2918
|
await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(manifest, /* @__PURE__ */ new Set([snapshot.workerName])));
|
|
@@ -1892,7 +2938,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1892
2938
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
1893
2939
|
if (retainedNewWorker) {
|
|
1894
2940
|
if (!dependencies.replanWorkers) throw new Error("platform: Recovering retained Workers requires a fresh Cloudflare installation plan.");
|
|
1895
|
-
workerActions = validateReplannedWorkerActions(plan, manifest, await dependencies.replanWorkers(manifest));
|
|
2941
|
+
workerActions = validateReplannedWorkerActions(plan, manifest, await dependencies.replanWorkers(manifest)).filter((action) => hasEmailGatewayArtifact || action.key !== "email-gateway-worker");
|
|
1896
2942
|
}
|
|
1897
2943
|
}
|
|
1898
2944
|
workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
|
|
@@ -1906,14 +2952,9 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1906
2952
|
};
|
|
1907
2953
|
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
1908
2954
|
}
|
|
1909
|
-
for (const key of
|
|
1910
|
-
"api",
|
|
1911
|
-
"proxy",
|
|
1912
|
-
"tail",
|
|
1913
|
-
"dispatch"
|
|
1914
|
-
]) {
|
|
2955
|
+
for (const key of workerKeys) {
|
|
1915
2956
|
dependencies.onProgress?.(`Deploying the ${key} service`);
|
|
1916
|
-
const action = workerActions.find((entry) => entry.key === `${key}-worker`);
|
|
2957
|
+
const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
|
|
1917
2958
|
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
1918
2959
|
manifest = recordProvisionIntent(manifest, action, now());
|
|
1919
2960
|
writeInstallationManifest(manifest);
|
|
@@ -1982,6 +3023,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
1982
3023
|
if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
|
|
1983
3024
|
manifest = {
|
|
1984
3025
|
...manifest,
|
|
3026
|
+
deployment: plan.deployment,
|
|
1985
3027
|
platformVersion: version,
|
|
1986
3028
|
runtime: runtimeProvenance,
|
|
1987
3029
|
updatedAt: now(),
|
|
@@ -2000,17 +3042,63 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2000
3042
|
zoneNameServers: zone?.nameServers ?? []
|
|
2001
3043
|
};
|
|
2002
3044
|
}
|
|
3045
|
+
let authenticationCutoverCommitted = false;
|
|
3046
|
+
if (authenticationProtocolTransition && requiresStagedCommit) {
|
|
3047
|
+
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
|
|
3048
|
+
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
|
|
3049
|
+
writeInstallationManifest(manifest);
|
|
3050
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3051
|
+
workerRollbacks = [];
|
|
3052
|
+
authenticationCutoverCommitted = true;
|
|
3053
|
+
}
|
|
3054
|
+
let authenticationStep = secrets.authenticationSetupJson ? "authentication-initialized:v1:" + hash(secrets.authenticationSetupJson) : "";
|
|
3055
|
+
let setupPending = false;
|
|
2003
3056
|
manifest = transitionInstallation(manifest, "verifying", now());
|
|
2004
3057
|
writeInstallationManifest(manifest);
|
|
3058
|
+
if (plan.deployment.authentication.configurable && authenticationStep) {
|
|
3059
|
+
if (!secrets.authenticationSetupJson || !secrets.authSetupToken) throw new Error("platform: Restore the encrypted authentication setup checkpoint before resuming.");
|
|
3060
|
+
dependencies.onProgress?.("Initializing platform authentication");
|
|
3061
|
+
if ((await resumeInstalledPlatformAuthentication({
|
|
3062
|
+
apiUrl: plan.apiUrl,
|
|
3063
|
+
installationId: manifest.id,
|
|
3064
|
+
setupToken: secrets.authSetupToken,
|
|
3065
|
+
setupJson: secrets.authenticationSetupJson,
|
|
3066
|
+
checkpoint: (replacement) => {
|
|
3067
|
+
secrets = {
|
|
3068
|
+
...secrets,
|
|
3069
|
+
...replacement
|
|
3070
|
+
};
|
|
3071
|
+
authenticationStep = "authentication-initialized:v1:" + hash(replacement.authenticationSetupJson);
|
|
3072
|
+
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
3073
|
+
}
|
|
3074
|
+
}, dependencies.initializeAuthentication ?? initializeInstalledPlatformAuthentication)).setupPending) {
|
|
3075
|
+
setupPending = true;
|
|
3076
|
+
dependencies.onProgress?.(`Administrator setup pending: ${plan.apiUrl}/setup`);
|
|
3077
|
+
}
|
|
3078
|
+
manifest = recordInstallationStep(manifest, {
|
|
3079
|
+
key: authenticationStep,
|
|
3080
|
+
completedAt: now(),
|
|
3081
|
+
resultHash: hash(secrets.authenticationSetupJson)
|
|
3082
|
+
}, now());
|
|
3083
|
+
writeInstallationManifest(manifest);
|
|
3084
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3085
|
+
}
|
|
2005
3086
|
dependencies.onProgress?.("Verifying platform health");
|
|
3087
|
+
const emailGatewayUrl = hasEmailGatewayArtifact ? `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev` : void 0;
|
|
3088
|
+
if (plan.deployment.authentication.accessProtection) {
|
|
3089
|
+
ensurePlatformAccessToken(plan.apiUrl, false, true);
|
|
3090
|
+
ensurePlatformAccessToken(plan.proxyUrl, false, true);
|
|
3091
|
+
}
|
|
2006
3092
|
const healthCheckContext = {
|
|
2007
3093
|
accountName: plan.account.name,
|
|
2008
3094
|
workers: [
|
|
2009
3095
|
plan.deployment.resources.apiWorker,
|
|
2010
3096
|
plan.deployment.resources.proxyWorker,
|
|
3097
|
+
...hasEmailGatewayArtifact ? [plan.deployment.resources.emailGatewayWorker] : [],
|
|
2011
3098
|
plan.deployment.resources.dispatchWorker
|
|
2012
3099
|
],
|
|
2013
|
-
resumeCommand: dependencies.healthCheckResumeCommand
|
|
3100
|
+
resumeCommand: dependencies.healthCheckResumeCommand,
|
|
3101
|
+
...emailGatewayUrl ? { emailGatewayUrl } : {}
|
|
2014
3102
|
};
|
|
2015
3103
|
await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ? `https://${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev` : void 0, healthCheckContext);
|
|
2016
3104
|
if (requiresSandboxDrain) {
|
|
@@ -2039,11 +3127,13 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2039
3127
|
await writeSandboxDrainMirror(completedDrain);
|
|
2040
3128
|
}
|
|
2041
3129
|
if (requiresStagedCommit) {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
3130
|
+
if (!authenticationCutoverCommitted) {
|
|
3131
|
+
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
|
|
3132
|
+
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
|
|
3133
|
+
writeInstallationManifest(manifest);
|
|
3134
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3135
|
+
workerRollbacks = [];
|
|
3136
|
+
}
|
|
2047
3137
|
workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
|
|
2048
3138
|
if (workerSecretUpdateIntent) {
|
|
2049
3139
|
manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "pending", workerSecretUpdateIntent.id, now());
|
|
@@ -2055,14 +3145,9 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2055
3145
|
};
|
|
2056
3146
|
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
2057
3147
|
}
|
|
2058
|
-
for (const key of
|
|
2059
|
-
"api",
|
|
2060
|
-
"proxy",
|
|
2061
|
-
"tail",
|
|
2062
|
-
"dispatch"
|
|
2063
|
-
]) {
|
|
3148
|
+
for (const key of workerKeys) {
|
|
2064
3149
|
dependencies.onProgress?.(`Enabling the ${key} service`);
|
|
2065
|
-
const action = workerActions.find((entry) => entry.key === `${key}-worker`);
|
|
3150
|
+
const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
|
|
2066
3151
|
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
2067
3152
|
const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
|
|
2068
3153
|
const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
|
|
@@ -2116,9 +3201,20 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2116
3201
|
const verifyCurrentWorkerCutover = async () => {
|
|
2117
3202
|
if (!requiresStagedCommit) return;
|
|
2118
3203
|
if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(manifest, workerCutoverGeneration);
|
|
2119
|
-
else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(manifest, workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler);
|
|
3204
|
+
else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(manifest, workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler, workerKeys);
|
|
2120
3205
|
};
|
|
2121
3206
|
await verifyCurrentWorkerCutover();
|
|
3207
|
+
let emailOperationId;
|
|
3208
|
+
if (shouldBootstrapSharedEmail) {
|
|
3209
|
+
workerDeploymentCommitted = true;
|
|
3210
|
+
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
|
|
3211
|
+
if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
|
|
3212
|
+
writeInstallationManifest(manifest);
|
|
3213
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3214
|
+
workerRollbacks = [];
|
|
3215
|
+
dependencies.onProgress?.("Preparing shared email routing");
|
|
3216
|
+
emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce));
|
|
3217
|
+
}
|
|
2122
3218
|
if (dependencies.client.deleteKvValue) await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
|
|
2123
3219
|
else if (wasDisabled || !previousPublicEndpoints) throw new Error("platform: Routing disablement removal is unavailable.");
|
|
2124
3220
|
if (requiresStagedCommit) {
|
|
@@ -2132,6 +3228,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2132
3228
|
if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
|
|
2133
3229
|
manifest = {
|
|
2134
3230
|
...manifest,
|
|
3231
|
+
deployment: plan.deployment,
|
|
2135
3232
|
platformVersion: version,
|
|
2136
3233
|
runtime: runtimeProvenance,
|
|
2137
3234
|
safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
|
|
@@ -2147,6 +3244,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2147
3244
|
saveInstalledPlatformConnection(plan, manifest);
|
|
2148
3245
|
return {
|
|
2149
3246
|
manifest,
|
|
3247
|
+
...emailOperationId ? { emailOperationId } : {},
|
|
3248
|
+
setupPending,
|
|
2150
3249
|
waitingForDns: false,
|
|
2151
3250
|
waitingForSandboxDrain: false,
|
|
2152
3251
|
zoneNameServers: zone?.nameServers ?? []
|
|
@@ -2159,7 +3258,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2159
3258
|
} catch (guardError) {
|
|
2160
3259
|
failure = new Error(`platform: ${errorMessage(error)} The installation also failed to retain its routing safety gate: ${errorMessage(guardError)}`, { cause: error });
|
|
2161
3260
|
}
|
|
2162
|
-
if (!workerDeploymentCommitted && workerRollbacks.length > 0) {
|
|
3261
|
+
if (!workerDeploymentCommitted && workerRollbacks.length > 0 && !(authenticationEnforcementActive && authenticationProtocolTransition)) {
|
|
2163
3262
|
const rollbackFailures = [];
|
|
2164
3263
|
for (const rollback of workerRollbacks.reverse()) try {
|
|
2165
3264
|
await rollback();
|
|
@@ -2173,7 +3272,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2173
3272
|
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, previousPublicEndpoints, now());
|
|
2174
3273
|
writeInstallationManifest(manifest);
|
|
2175
3274
|
}
|
|
2176
|
-
}
|
|
3275
|
+
} else if (authenticationEnforcementActive && authenticationProtocolTransition) workerRollbacks = [];
|
|
2177
3276
|
if (manifest.status !== "failed" && manifest.status !== "uninstalled") try {
|
|
2178
3277
|
manifest = transitionInstallation(manifest, "failed", now());
|
|
2179
3278
|
writeInstallationManifest(manifest);
|
|
@@ -2184,6 +3283,9 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2184
3283
|
await releaseDatabaseLock();
|
|
2185
3284
|
}
|
|
2186
3285
|
}
|
|
3286
|
+
function checkpointAuthenticationResources(manifest, secrets, updatedAt) {
|
|
3287
|
+
return checkpointPlatformAccessResources(manifest, secrets.accessProvisioningJson, updatedAt);
|
|
3288
|
+
}
|
|
2187
3289
|
async function writeR2OwnershipMarkerIfNeeded(client, plan, manifest, action) {
|
|
2188
3290
|
if (action.kind !== "r2-bucket") return;
|
|
2189
3291
|
const bucket = manifest.resources.find((resource) => resource.key === action.key && resource.state === "ready");
|
|
@@ -2424,8 +3526,10 @@ function apiWorkerSecrets(secrets, accountId, zoneId) {
|
|
|
2424
3526
|
CF_ZONE_ID: zoneId,
|
|
2425
3527
|
GITHUB_CLIENT_ID: secrets.githubClientId,
|
|
2426
3528
|
GITHUB_CLIENT_SECRET: secrets.githubClientSecret,
|
|
3529
|
+
...secrets.emailEnabled ? { EMAIL_SIGNING_SECRET: secrets.emailSigningSecret } : {},
|
|
2427
3530
|
JWT_SECRET: secrets.jwtSecret,
|
|
2428
3531
|
PLATFORM_BOOTSTRAP_GITHUB_ID: secrets.bootstrapGitHubId,
|
|
3532
|
+
PLATFORM_AUTH_SETUP_TOKEN: secrets.authSetupToken ?? "",
|
|
2429
3533
|
...secrets.projectSecretKeysJson && secrets.projectSecretActiveKeyVersion ? {
|
|
2430
3534
|
PROJECT_SECRET_ACTIVE_KEY_VERSION: secrets.projectSecretActiveKeyVersion,
|
|
2431
3535
|
PROJECT_SECRET_KEYS_JSON: secrets.projectSecretKeysJson
|
|
@@ -2436,15 +3540,23 @@ function apiWorkerSecrets(secrets, accountId, zoneId) {
|
|
|
2436
3540
|
}
|
|
2437
3541
|
/** Existing Workers inherit live secrets, including complete encryption keyrings. */
|
|
2438
3542
|
function platformWorkerSecretPayload(worker, secrets, accountId, zoneId, existingWorker) {
|
|
2439
|
-
if (worker !== "api" && worker !== "proxy") return null;
|
|
2440
|
-
if (!existingWorker)
|
|
3543
|
+
if (worker !== "api" && worker !== "proxy" && worker !== "emailGateway") return null;
|
|
3544
|
+
if (!existingWorker) {
|
|
3545
|
+
if (worker === "api") return apiWorkerSecrets(secrets, accountId, zoneId);
|
|
3546
|
+
if (worker === "emailGateway") {
|
|
3547
|
+
const payload = withoutEmptySecrets({ EMAIL_SIGNING_SECRET: secrets.emailEnabled ? secrets.emailSigningSecret : "" });
|
|
3548
|
+
return Object.keys(payload).length > 0 ? payload : null;
|
|
3549
|
+
}
|
|
3550
|
+
return proxyWorkerSecrets(secrets, accountId);
|
|
3551
|
+
}
|
|
2441
3552
|
const allowed = worker === "api" ? [
|
|
2442
3553
|
"CF_API_TOKEN",
|
|
3554
|
+
"EMAIL_SIGNING_SECRET",
|
|
2443
3555
|
"GITHUB_CLIENT_ID",
|
|
2444
3556
|
"GITHUB_CLIENT_SECRET",
|
|
2445
3557
|
"R2_ACCESS_KEY_ID",
|
|
2446
3558
|
"R2_SECRET_ACCESS_KEY"
|
|
2447
|
-
] : ["CF_API_TOKEN"];
|
|
3559
|
+
] : worker === "proxy" ? ["CF_API_TOKEN", "EMAIL_SIGNING_SECRET"] : ["EMAIL_SIGNING_SECRET"];
|
|
2448
3560
|
const updates = Object.fromEntries(Object.entries(secrets.workerSecretUpdates ?? {}).filter(([name, value]) => allowed.includes(name) && typeof value === "string" && value !== ""));
|
|
2449
3561
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
2450
3562
|
}
|
|
@@ -2469,26 +3581,34 @@ function proxyWorkerSecrets(secrets, accountId) {
|
|
|
2469
3581
|
return withoutEmptySecrets({
|
|
2470
3582
|
CF_ACCOUNT_ID: accountId,
|
|
2471
3583
|
CF_API_TOKEN: secrets.cloudflareApiToken,
|
|
3584
|
+
...secrets.emailEnabled ? { EMAIL_SIGNING_SECRET: secrets.emailSigningSecret } : {},
|
|
2472
3585
|
JWT_SECRET: secrets.jwtSecret
|
|
2473
3586
|
});
|
|
2474
3587
|
}
|
|
2475
3588
|
function withoutEmptySecrets(secrets) {
|
|
2476
3589
|
return Object.fromEntries(Object.entries(secrets).filter(([, value]) => value !== ""));
|
|
2477
3590
|
}
|
|
2478
|
-
function assertRequiredPlatformSecrets(plan, manifest, secrets) {
|
|
3591
|
+
function assertRequiredPlatformSecrets(plan, manifest, secrets, authenticationWillBePrepared = false) {
|
|
2479
3592
|
const missing = /* @__PURE__ */ new Set();
|
|
2480
3593
|
const requireSecret = (value, name) => {
|
|
2481
3594
|
if (!value) missing.add(name);
|
|
2482
3595
|
};
|
|
2483
|
-
if (!manifest.steps.some((step) => step.key
|
|
3596
|
+
if (plan.deployment.authentication.configurable && !manifest.steps.some((step) => step.key.startsWith("authentication-initialized:v1:"))) {
|
|
3597
|
+
if (!authenticationWillBePrepared) requireSecret(secrets.authenticationSetupJson ?? "", "authentication setup configuration");
|
|
3598
|
+
requireSecret(secrets.authSetupToken ?? "", "installation authentication setup authority");
|
|
3599
|
+
requireSecret(secrets.authBootstrapCode ?? "", "administrator setup code");
|
|
3600
|
+
} else if (!plan.deployment.authentication.configurable && !manifest.steps.some((step) => step.key === "bootstrap-access:v1")) {
|
|
2484
3601
|
requireSecret(secrets.bootstrapGitHubId, "VOID_PLATFORM_ADMIN_GITHUB_ID");
|
|
2485
3602
|
requireSecret(secrets.bootstrapGitHubLogin, "VOID_PLATFORM_ADMIN_GITHUB_LOGIN");
|
|
2486
3603
|
}
|
|
2487
3604
|
if (plan.actions.some((action) => action.key === "api-worker" && action.kind === "worker" && action.action === "create")) {
|
|
2488
3605
|
requireSecret(secrets.cloudflareApiToken, "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN");
|
|
2489
|
-
|
|
2490
|
-
|
|
3606
|
+
if (!plan.deployment.authentication.configurable) {
|
|
3607
|
+
requireSecret(secrets.githubClientId, "VOID_PLATFORM_GITHUB_CLIENT_ID");
|
|
3608
|
+
requireSecret(secrets.githubClientSecret, "VOID_PLATFORM_GITHUB_CLIENT_SECRET");
|
|
3609
|
+
}
|
|
2491
3610
|
requireSecret(secrets.jwtSecret, "VOID_PLATFORM_JWT_SECRET");
|
|
3611
|
+
if (plan.deployment.capabilities.email) requireSecret(secrets.emailSigningSecret, "VOID_PLATFORM_EMAIL_SIGNING_SECRET");
|
|
2492
3612
|
requireSecret(secrets.projectSecretKeysJson, "VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON (or VOID_PLATFORM_PROJECT_SECRET_KEY for v1)");
|
|
2493
3613
|
requireSecret(secrets.projectSecretActiveKeyVersion, "VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION (or VOID_PLATFORM_PROJECT_SECRET_KEY for v1)");
|
|
2494
3614
|
requireSecret(secrets.r2AccessKeyId, "VOID_PLATFORM_R2_ACCESS_KEY_ID");
|
|
@@ -2497,6 +3617,10 @@ function assertRequiredPlatformSecrets(plan, manifest, secrets) {
|
|
|
2497
3617
|
if (plan.actions.some((action) => action.key === "proxy-worker" && action.kind === "worker" && action.action === "create")) {
|
|
2498
3618
|
requireSecret(secrets.cloudflareApiToken, "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN");
|
|
2499
3619
|
requireSecret(secrets.jwtSecret, "VOID_PLATFORM_JWT_SECRET");
|
|
3620
|
+
if (plan.deployment.capabilities.email) requireSecret(secrets.emailSigningSecret, "VOID_PLATFORM_EMAIL_SIGNING_SECRET");
|
|
3621
|
+
}
|
|
3622
|
+
if (plan.actions.some((action) => action.key === "email-gateway-worker" && action.kind === "worker" && action.action === "create")) {
|
|
3623
|
+
if (plan.deployment.capabilities.email) requireSecret(secrets.emailSigningSecret, "VOID_PLATFORM_EMAIL_SIGNING_SECRET");
|
|
2500
3624
|
}
|
|
2501
3625
|
if (missing.size > 0) throw new Error(`platform: Creating a missing platform Worker requires: ${[...missing].sort().join(", ")}. Restore these values from your secret manager and retry.`);
|
|
2502
3626
|
}
|
|
@@ -2681,6 +3805,17 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
|
|
|
2681
3805
|
await client.query("SELECT pg_advisory_unlock(hashtextextended($1, 0))", [lockIdentity]);
|
|
2682
3806
|
};
|
|
2683
3807
|
release.authoritativeManifest = authoritativeManifest;
|
|
3808
|
+
release.readAuthenticationState = async () => {
|
|
3809
|
+
if (!(await client.query("SELECT to_regclass('public.platform_auth_settings') IS NOT NULL AS present")).rows[0]?.present) return null;
|
|
3810
|
+
const [result, enabledProviders] = await Promise.all([client.query("SELECT sessions_required, protection FROM platform_auth_settings WHERE singleton = true"), client.query("SELECT DISTINCT kind FROM platform_login_connections WHERE enabled = true ORDER BY kind")]);
|
|
3811
|
+
const settings = result.rows[0];
|
|
3812
|
+
return settings ? {
|
|
3813
|
+
enabledProviders: enabledProviders.rows.map((row) => row.kind),
|
|
3814
|
+
sessionsRequired: settings.sessions_required,
|
|
3815
|
+
protectedByAccess: settings.protection !== null,
|
|
3816
|
+
protection: parsePlatformAccessProtection(settings.protection)
|
|
3817
|
+
} : null;
|
|
3818
|
+
};
|
|
2684
3819
|
release.requiredSafetyProtocols = await readRequiredPlatformSafetyProtocols(client);
|
|
2685
3820
|
release.checkpoint = async (manifest) => {
|
|
2686
3821
|
if (released) throw new Error("platform: Cannot checkpoint lifecycle state after releasing its lock.");
|
|
@@ -2711,14 +3846,14 @@ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionPr
|
|
|
2711
3846
|
AND table_name = 'sandbox_drain_checkpoints'
|
|
2712
3847
|
AND column_name = 'admission_protocol'
|
|
2713
3848
|
) AS "present"`)).rows[0]?.present === true;
|
|
2714
|
-
if (requiredAdmissionProtocol
|
|
3849
|
+
if (requiredAdmissionProtocol >= 2 && !hasAdmissionColumn) throw new Error("platform: The runtime database does not include the sandbox deployment admission barrier.");
|
|
2715
3850
|
const readCheckpoint = () => client.query(`SELECT ${hasAdmissionColumn ? "\"admission_protocol\"" : "0 AS \"admission_protocol\""}, "generation", "state", "phase", "cursor", "started_at", "updated_at", "completed_at"
|
|
2716
3851
|
FROM "sandbox_drain_checkpoints"
|
|
2717
3852
|
WHERE "singleton" = true`);
|
|
2718
3853
|
const classify = (row) => {
|
|
2719
3854
|
if (!row) return null;
|
|
2720
3855
|
const cursor = row.cursor;
|
|
2721
|
-
if (!row.generation || row.admission_protocol !== 0 && row.admission_protocol !== 2 || !row.started_at || !row.updated_at || row.state !== "draining" && row.state !== "complete" || row.phase !== "deployments" && row.phase !== "resources" || !cursor || cursor.pass !== "1" && cursor.pass !== "2" || row.state === "complete" && (!row.completed_at || row.phase !== "resources" || cursor.pass !== "2") || row.state === "draining" && row.completed_at !== null) throw new Error("platform: The authoritative sandbox-drain database checkpoint is invalid. Traffic remains disabled.");
|
|
3856
|
+
if (!row.generation || row.admission_protocol !== 0 && row.admission_protocol !== 2 && row.admission_protocol !== 3 || !row.started_at || !row.updated_at || row.state !== "draining" && row.state !== "complete" || row.phase !== "deployments" && row.phase !== "resources" || !cursor || cursor.pass !== "1" && cursor.pass !== "2" || row.state === "complete" && (!row.completed_at || row.phase !== "resources" || cursor.pass !== "2") || row.state === "draining" && row.completed_at !== null) throw new Error("platform: The authoritative sandbox-drain database checkpoint is invalid. Traffic remains disabled.");
|
|
2722
3857
|
return {
|
|
2723
3858
|
admissionProtocol: row.admission_protocol,
|
|
2724
3859
|
generation: row.generation,
|
|
@@ -2735,7 +3870,23 @@ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionPr
|
|
|
2735
3870
|
};
|
|
2736
3871
|
const existingState = classify((await readCheckpoint()).rows[0]);
|
|
2737
3872
|
if (existingState && (requiredAdmissionProtocol === 0 || existingState.admissionProtocol >= requiredAdmissionProtocol)) return existingState;
|
|
2738
|
-
if (requiredAdmissionProtocol
|
|
3873
|
+
if (requiredAdmissionProtocol >= 2) {
|
|
3874
|
+
if (existingState && existingState.admissionProtocol >= 2) {
|
|
3875
|
+
await client.query("BEGIN");
|
|
3876
|
+
try {
|
|
3877
|
+
await client.query("LOCK TABLE \"deployments\" IN SHARE MODE");
|
|
3878
|
+
await client.query(`UPDATE "sandbox_drain_checkpoints"
|
|
3879
|
+
SET "admission_protocol" = GREATEST("admission_protocol", $1), "updated_at" = now()
|
|
3880
|
+
WHERE "singleton" = true`, [requiredAdmissionProtocol]);
|
|
3881
|
+
const advanced = classify((await readCheckpoint()).rows[0]);
|
|
3882
|
+
if (!advanced || advanced.admissionProtocol < requiredAdmissionProtocol) throw new Error("platform: The Sandbox controller admission barrier could not be armed.");
|
|
3883
|
+
await client.query("COMMIT");
|
|
3884
|
+
return advanced;
|
|
3885
|
+
} catch (error) {
|
|
3886
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
3887
|
+
throw error;
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
2739
3890
|
const generation = randomUUID();
|
|
2740
3891
|
await client.query("BEGIN");
|
|
2741
3892
|
try {
|
|
@@ -2768,8 +3919,10 @@ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionPr
|
|
|
2768
3919
|
WHERE "singleton" = true
|
|
2769
3920
|
AND "admission_protocol" = 2
|
|
2770
3921
|
AND "generation" = $1`, [generation]);
|
|
3922
|
+
if (requiredAdmissionProtocol >= 3) await client.query(`UPDATE "sandbox_drain_checkpoints" SET "admission_protocol" = $1
|
|
3923
|
+
WHERE "singleton" = true`, [requiredAdmissionProtocol]);
|
|
2771
3924
|
const armed = classify((await readCheckpoint()).rows[0]);
|
|
2772
|
-
if (!armed || armed.admissionProtocol
|
|
3925
|
+
if (!armed || armed.admissionProtocol < requiredAdmissionProtocol) throw new Error("platform: The sandbox deployment admission barrier could not be armed.");
|
|
2773
3926
|
await client.query("COMMIT");
|
|
2774
3927
|
return armed;
|
|
2775
3928
|
} catch (error) {
|
|
@@ -2812,12 +3965,17 @@ async function readRequiredPlatformSafetyProtocols(client) {
|
|
|
2812
3965
|
AND table_name = 'sandbox_drain_checkpoints'
|
|
2813
3966
|
AND column_name = 'admission_protocol'
|
|
2814
3967
|
) AS "present"`)).rows[0]?.present === true) {
|
|
2815
|
-
|
|
3968
|
+
const admission = await client.query(`SELECT EXISTS(
|
|
2816
3969
|
SELECT 1
|
|
2817
3970
|
FROM "sandbox_drain_checkpoints"
|
|
2818
3971
|
WHERE "singleton" = true
|
|
2819
3972
|
AND "admission_protocol" >= 2
|
|
2820
|
-
) AS "required"
|
|
3973
|
+
) AS "required", (SELECT "admission_protocol" FROM "sandbox_drain_checkpoints"
|
|
3974
|
+
WHERE "singleton" = true) AS "protocol"`);
|
|
3975
|
+
if (admission.rows[0]?.required === true) {
|
|
3976
|
+
if (admission.rows[0].protocol != null && admission.rows[0].protocol > 3) throw new Error("platform: The database requires a newer Sandbox controller protocol.");
|
|
3977
|
+
return { sandboxDrain: admission.rows[0].protocol === 3 ? 3 : 2 };
|
|
3978
|
+
}
|
|
2821
3979
|
}
|
|
2822
3980
|
return { sandboxDrain: (await client.query(`SELECT EXISTS(
|
|
2823
3981
|
SELECT 1
|
|
@@ -3582,7 +4740,7 @@ function interruptedWorkerCutover(manifest) {
|
|
|
3582
4740
|
generations.set(generation, entry);
|
|
3583
4741
|
continue;
|
|
3584
4742
|
}
|
|
3585
|
-
const unsafeTraffic = /^worker-cutover:(\d+):unsafe-traffic:(api|proxy|tail|dispatch)$/.exec(step.key);
|
|
4743
|
+
const unsafeTraffic = /^worker-cutover:(\d+):unsafe-traffic:(api|proxy|emailGateway|tail|dispatch)$/.exec(step.key);
|
|
3586
4744
|
if (unsafeTraffic) {
|
|
3587
4745
|
const generation = Number(unsafeTraffic[1]);
|
|
3588
4746
|
const entry = generations.get(generation) ?? {
|
|
@@ -3594,7 +4752,7 @@ function interruptedWorkerCutover(manifest) {
|
|
|
3594
4752
|
generations.set(generation, entry);
|
|
3595
4753
|
continue;
|
|
3596
4754
|
}
|
|
3597
|
-
const staged = /^worker-cutover:(\d+):staged:(api|proxy|tail|dispatch)$/.exec(step.key);
|
|
4755
|
+
const staged = /^worker-cutover:(\d+):staged:(api|proxy|emailGateway|tail|dispatch)$/.exec(step.key);
|
|
3598
4756
|
if (!staged) continue;
|
|
3599
4757
|
const generation = Number(staged[1]);
|
|
3600
4758
|
const snapshot = parseWorkerCutoverSnapshot(step.resultHash);
|
|
@@ -3614,17 +4772,18 @@ function interruptedWorkerCutover(manifest) {
|
|
|
3614
4772
|
unsafeTraffic: unresolved[1].unsafeTraffic
|
|
3615
4773
|
} : null;
|
|
3616
4774
|
}
|
|
3617
|
-
async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs, accountId, runner = runPlatformWrangler
|
|
4775
|
+
async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs, accountId, runner = runPlatformWrangler, workerKeys = [
|
|
4776
|
+
"api",
|
|
4777
|
+
"proxy",
|
|
4778
|
+
"emailGateway",
|
|
4779
|
+
"tail",
|
|
4780
|
+
"dispatch"
|
|
4781
|
+
]) {
|
|
3618
4782
|
const cutover = interruptedWorkerCutover(manifest);
|
|
3619
|
-
if (cutover?.generation !== generation || cutover.staged.size !==
|
|
4783
|
+
if (cutover?.generation !== generation || cutover.staged.size !== workerKeys.length) throw new Error(`platform: Worker cutover generation ${generation} is missing a complete immutable-version checkpoint.`);
|
|
3620
4784
|
const directory = mkdtempSync(join(tmpdir(), "void-platform-cutover-verify-"));
|
|
3621
4785
|
try {
|
|
3622
|
-
for (const key of
|
|
3623
|
-
"api",
|
|
3624
|
-
"proxy",
|
|
3625
|
-
"tail",
|
|
3626
|
-
"dispatch"
|
|
3627
|
-
]) {
|
|
4786
|
+
for (const key of workerKeys) {
|
|
3628
4787
|
const snapshot = cutover.staged.get(key);
|
|
3629
4788
|
if (!snapshot) throw new Error(`platform: Worker cutover generation ${generation} is missing its ${key} checkpoint.`);
|
|
3630
4789
|
const config = configs[key];
|
|
@@ -3691,7 +4850,7 @@ var PlatformAccessBlockedError = class extends Error {
|
|
|
3691
4850
|
"Void’s own authentication stays enabled. Keep account-wide Default-Deny on.",
|
|
3692
4851
|
"",
|
|
3693
4852
|
"For platforms that must remain Access-protected:",
|
|
3694
|
-
"https://void.cloud/guide/
|
|
4853
|
+
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
3695
4854
|
].join("\n"));
|
|
3696
4855
|
}
|
|
3697
4856
|
};
|
|
@@ -3713,11 +4872,12 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
|
|
|
3713
4872
|
signal: AbortSignal.timeout(3e4)
|
|
3714
4873
|
});
|
|
3715
4874
|
const dispatchUrl = dispatchOrigin ?? `https://test.${applicationBaseDomain}`;
|
|
3716
|
-
const [health, discovery, proxy, dispatch] = await Promise.all([
|
|
4875
|
+
const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
|
|
3717
4876
|
request(`${apiUrl}/health`),
|
|
3718
4877
|
request(`${apiUrl}/.well-known/void-platform`),
|
|
3719
4878
|
request(`${proxyUrl}/health`),
|
|
3720
|
-
request(`${dispatchUrl}/health`)
|
|
4879
|
+
request(`${dispatchUrl}/health`),
|
|
4880
|
+
context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
|
|
3721
4881
|
]);
|
|
3722
4882
|
if (!health.ok) throw await platformProbeFailure("Health check", apiUrl, health, context);
|
|
3723
4883
|
if (!discovery.ok) throw await platformProbeFailure("Discovery check", apiUrl, discovery, context);
|
|
@@ -3727,6 +4887,11 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
|
|
|
3727
4887
|
if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
|
|
3728
4888
|
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", proxyUrl, proxy, context);
|
|
3729
4889
|
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", dispatchUrl, dispatch, context);
|
|
4890
|
+
if (emailGateway) {
|
|
4891
|
+
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", context.emailGatewayUrl, emailGateway, context);
|
|
4892
|
+
const identity = await emailGateway.json();
|
|
4893
|
+
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.");
|
|
4894
|
+
}
|
|
3730
4895
|
}
|
|
3731
4896
|
async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fetch) {
|
|
3732
4897
|
const response = await fetcher(`${apiUrl}/internal/platform/sandbox-drain`, {
|
|
@@ -3734,7 +4899,8 @@ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fe
|
|
|
3734
4899
|
"X-Void-Platform-Probe": healthProbeToken,
|
|
3735
4900
|
...cfAccessHeaders(process.env, apiUrl)
|
|
3736
4901
|
},
|
|
3737
|
-
signal: AbortSignal.timeout(3e4)
|
|
4902
|
+
signal: AbortSignal.timeout(3e4),
|
|
4903
|
+
redirect: "manual"
|
|
3738
4904
|
});
|
|
3739
4905
|
if (!response.ok) throw new Error(`platform: Sandbox drain probe failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
|
|
3740
4906
|
let value;
|
|
@@ -3750,10 +4916,268 @@ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fe
|
|
|
3750
4916
|
if (status.version !== 1 || !validState || typeof status.complete !== "boolean" || !validPhase || status.generation !== null && (typeof status.generation !== "string" || status.generation.length === 0) || status.updatedAt !== null && (typeof status.updatedAt !== "string" || !Number.isFinite(Date.parse(status.updatedAt))) || status.state === "required" && (status.complete || status.generation !== null || status.updatedAt !== null || status.phase !== null) || status.state === "draining" && (status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null) || status.state === "complete" && (!status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
|
|
3751
4917
|
return status.state === "complete";
|
|
3752
4918
|
}
|
|
4919
|
+
async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
|
|
4920
|
+
const response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
|
|
4921
|
+
method: "POST",
|
|
4922
|
+
headers: {
|
|
4923
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
4924
|
+
...cfAccessHeaders(process.env, apiUrl)
|
|
4925
|
+
},
|
|
4926
|
+
signal: AbortSignal.timeout(3e4),
|
|
4927
|
+
redirect: "manual"
|
|
4928
|
+
});
|
|
4929
|
+
let value;
|
|
4930
|
+
try {
|
|
4931
|
+
value = await response.json();
|
|
4932
|
+
} catch {
|
|
4933
|
+
throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
|
|
4934
|
+
}
|
|
4935
|
+
const status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4936
|
+
if (!response.ok || status?.version !== 1 || status.state !== "ready" || typeof status.operationId !== "string" || status.operationId.length === 0) {
|
|
4937
|
+
const detail = status && typeof status.operationId === "string" ? ` Operation ${status.operationId}: ${typeof status.operationStatus === "string" ? status.operationStatus : "incomplete"}.` : status && typeof status.operationStatus === "string" ? ` Operation status: ${status.operationStatus}.` : "";
|
|
4938
|
+
throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
|
|
4939
|
+
}
|
|
4940
|
+
return status.operationId;
|
|
4941
|
+
}
|
|
3753
4942
|
function createPlatformHealthProbeToken(ownershipNonce) {
|
|
3754
4943
|
return createHash("sha256").update("void-platform-health-probe:v1\0").update(ownershipNonce).digest("base64url");
|
|
3755
4944
|
}
|
|
3756
4945
|
//#endregion
|
|
4946
|
+
//#region src/cli/platform-install-auth.ts
|
|
4947
|
+
function answer(value) {
|
|
4948
|
+
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
4949
|
+
return value;
|
|
4950
|
+
}
|
|
4951
|
+
async function selectInstallationAuthentication(file, recorded) {
|
|
4952
|
+
if (file) {
|
|
4953
|
+
const input = JSON.parse(await readFile(file, "utf8"));
|
|
4954
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || !("connections" in input) || !Array.isArray(input.connections) || !input.connections.length || input.connections.length > 32) throw new Error("platform: An authentication file must define between 1 and 32 connections.");
|
|
4955
|
+
const entries = input.connections;
|
|
4956
|
+
if (entries.some((entry) => !entry || typeof entry !== "object" || !entry.configuration || typeof entry.configuration !== "object" || Array.isArray(entry.configuration) || !PLATFORM_LOGIN_KINDS.includes(entry.configuration.kind) || entry.clientSecret !== void 0 || entry.configuration.clientSecret !== void 0)) throw new Error("platform: Each connection needs a supported configuration and a clientSecretEnv reference; plaintext secrets are not accepted in the file.");
|
|
4957
|
+
const selected = input;
|
|
4958
|
+
if (selected.cloudflareAccess !== void 0) selected.cloudflareAccess = parsePlatformAccessSetupSpec(selected.cloudflareAccess);
|
|
4959
|
+
const authentication = {
|
|
4960
|
+
providers: [...new Set(entries.map((entry) => entry.configuration.kind))],
|
|
4961
|
+
configurable: true,
|
|
4962
|
+
...selected.protection ? { accessProtection: true } : {}
|
|
4963
|
+
};
|
|
4964
|
+
if (recorded && JSON.stringify(authentication) !== JSON.stringify(recorded)) throw new Error("platform: Resume with the recorded login methods and protection settings.");
|
|
4965
|
+
return {
|
|
4966
|
+
authentication,
|
|
4967
|
+
file: selected
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
if (recorded) return { authentication: recorded };
|
|
4971
|
+
if (!process.stdin.isTTY) return { authentication: { providers: ["github"] } };
|
|
4972
|
+
const providers = answer(await multiselect({
|
|
4973
|
+
message: "Platform login methods:",
|
|
4974
|
+
initialValues: ["github"],
|
|
4975
|
+
required: true,
|
|
4976
|
+
options: [
|
|
4977
|
+
{
|
|
4978
|
+
value: "github",
|
|
4979
|
+
label: "GitHub"
|
|
4980
|
+
},
|
|
4981
|
+
{
|
|
4982
|
+
value: "google",
|
|
4983
|
+
label: "Google"
|
|
4984
|
+
},
|
|
4985
|
+
{
|
|
4986
|
+
value: "oidc",
|
|
4987
|
+
label: "Generic OIDC"
|
|
4988
|
+
},
|
|
4989
|
+
{
|
|
4990
|
+
value: "cloudflare-access",
|
|
4991
|
+
label: "Cloudflare Access login"
|
|
4992
|
+
}
|
|
4993
|
+
]
|
|
4994
|
+
}));
|
|
4995
|
+
const protection = answer(await confirm({
|
|
4996
|
+
message: "Also protect the platform UI and APIs with Cloudflare Access?",
|
|
4997
|
+
initialValue: false
|
|
4998
|
+
}));
|
|
4999
|
+
return { authentication: {
|
|
5000
|
+
providers,
|
|
5001
|
+
...protection || providers.length !== 1 || providers[0] !== "github" ? { configurable: true } : {},
|
|
5002
|
+
...protection ? { accessProtection: true } : {}
|
|
5003
|
+
} };
|
|
5004
|
+
}
|
|
5005
|
+
async function collectInstallationAuthentication(selection, plan, checkpoint, save, access) {
|
|
5006
|
+
if (!selection.authentication.configurable) return;
|
|
5007
|
+
activatePlatformAccessCredentials(checkpoint.accessCredentialsJson, [plan.apiUrl, plan.proxyUrl]);
|
|
5008
|
+
if (checkpoint.authenticationSetupJson && !selection.file) {
|
|
5009
|
+
parsePlatformAuthenticationSetup(JSON.parse(checkpoint.authenticationSetupJson));
|
|
5010
|
+
return {
|
|
5011
|
+
authenticationSetupJson: checkpoint.authenticationSetupJson,
|
|
5012
|
+
authSetupToken: checkpoint.authSetupToken ?? "",
|
|
5013
|
+
authBootstrapCode: checkpoint.authBootstrapCode ?? ""
|
|
5014
|
+
};
|
|
5015
|
+
}
|
|
5016
|
+
const prompt = async (message, initialValue = "", masked = false) => {
|
|
5017
|
+
if (!process.stdin.isTTY) throw new Error(`platform: ${message} Supply it through --auth-config and environment references.`);
|
|
5018
|
+
return masked ? answer(await password({ message })) : answer(await text$1({
|
|
5019
|
+
message,
|
|
5020
|
+
initialValue
|
|
5021
|
+
}));
|
|
5022
|
+
};
|
|
5023
|
+
const entries = selection.file?.connections ?? selection.authentication.providers.map((kind) => ({
|
|
5024
|
+
configuration: {
|
|
5025
|
+
kind,
|
|
5026
|
+
id: kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind,
|
|
5027
|
+
label: kind === "github" ? "GitHub" : kind === "google" ? "Google" : kind === "oidc" ? "Company SSO" : "Cloudflare Access"
|
|
5028
|
+
},
|
|
5029
|
+
clientSecretEnv: void 0
|
|
5030
|
+
}));
|
|
5031
|
+
const connections = [];
|
|
5032
|
+
const accessEntry = entries.find((entry) => entry.configuration.kind === "cloudflare-access");
|
|
5033
|
+
const prepared = access && (selection.authentication.accessProtection || accessEntry && (!selection.file || selection.file.cloudflareAccess)) ? await preparePlatformAccessSetup({
|
|
5034
|
+
plan,
|
|
5035
|
+
ownershipNonce: access.ownershipNonce,
|
|
5036
|
+
protection: !!selection.authentication.accessProtection,
|
|
5037
|
+
...accessEntry ? { login: {
|
|
5038
|
+
id: String(accessEntry.configuration.id ?? "access"),
|
|
5039
|
+
label: String(accessEntry.configuration.label ?? "Cloudflare Access"),
|
|
5040
|
+
...Array.isArray(accessEntry.configuration.requiredGroups) ? { requiredGroups: accessEntry.configuration.requiredGroups } : {}
|
|
5041
|
+
} } : {},
|
|
5042
|
+
spec: selection.file?.cloudflareAccess,
|
|
5043
|
+
client: access.client,
|
|
5044
|
+
checkpoint,
|
|
5045
|
+
save,
|
|
5046
|
+
yes: access.yes
|
|
5047
|
+
}) : void 0;
|
|
5048
|
+
console.log(`Login callback URL: ${plan.apiUrl}/auth/callback`);
|
|
5049
|
+
for (const entry of entries) {
|
|
5050
|
+
const config = { ...entry.configuration };
|
|
5051
|
+
const kind = config.kind;
|
|
5052
|
+
if (!config.id) config.id = kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind;
|
|
5053
|
+
if (prepared?.login && prepared.login.configuration.id === config.id) {
|
|
5054
|
+
const connection = prepared.login;
|
|
5055
|
+
save({ [`authenticationConnection:${connection.configuration.id}`]: JSON.stringify(connection) });
|
|
5056
|
+
connections.push(connection);
|
|
5057
|
+
continue;
|
|
5058
|
+
}
|
|
5059
|
+
const partialKey = `authenticationConnection:${String(config.id)}`;
|
|
5060
|
+
const stored = checkpoint[partialKey] ? JSON.parse(checkpoint[partialKey]) : void 0;
|
|
5061
|
+
Object.assign(config, {
|
|
5062
|
+
...stored?.configuration,
|
|
5063
|
+
...config
|
|
5064
|
+
});
|
|
5065
|
+
if (!config.label) config.label = kind;
|
|
5066
|
+
if ((kind === "oidc" || kind === "cloudflare-access") && !config.issuer) config.issuer = await prompt(`${config.label}: issuer URL:`);
|
|
5067
|
+
if (!config.clientId) config.clientId = await prompt(`${config.label}: client ID:`);
|
|
5068
|
+
if (kind === "google" && config.allowedDomains === void 0 && !selection.file) config.allowedDomains = (await prompt("Allowed Google Workspace domains (comma-separated, or blank):")).split(",").map((value) => value.trim()).filter(Boolean);
|
|
5069
|
+
const parsed = parsePlatformLoginConfiguration(config);
|
|
5070
|
+
let secret;
|
|
5071
|
+
if (entry.clientSecretEnv) {
|
|
5072
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.clientSecretEnv)) throw new Error("platform: Invalid clientSecretEnv reference.");
|
|
5073
|
+
secret = process.env[entry.clientSecretEnv];
|
|
5074
|
+
if (!secret) throw new Error(`platform: ${entry.clientSecretEnv} must contain the login client secret.`);
|
|
5075
|
+
} else if (stored?.clientSecret && stored.configuration?.clientId === parsed.clientId && stored.configuration?.issuer === parsed.issuer) secret = stored.clientSecret;
|
|
5076
|
+
else secret = await prompt(`${config.label}: client secret:`, "", true);
|
|
5077
|
+
const connection = {
|
|
5078
|
+
configuration: parsed,
|
|
5079
|
+
clientSecret: secret
|
|
5080
|
+
};
|
|
5081
|
+
save({ [partialKey]: JSON.stringify(connection) });
|
|
5082
|
+
connections.push(connection);
|
|
5083
|
+
}
|
|
5084
|
+
const administratorConnectionId = selection.file?.administratorConnectionId ?? (connections.length === 1 ? connections[0].configuration.id : process.stdin.isTTY ? answer(await select({
|
|
5085
|
+
message: "Login method for the first administrator:",
|
|
5086
|
+
options: connections.map((entry) => ({
|
|
5087
|
+
value: entry.configuration.id,
|
|
5088
|
+
label: entry.configuration.label
|
|
5089
|
+
}))
|
|
5090
|
+
})) : connections[0].configuration.id);
|
|
5091
|
+
let protection = prepared?.protection ?? selection.file?.protection ?? null;
|
|
5092
|
+
if (selection.authentication.accessProtection && !protection) {
|
|
5093
|
+
const teamDomain = await prompt("Access team origin (https://team.cloudflareaccess.com):");
|
|
5094
|
+
const apiAudience = await prompt(`Access application audience for ${plan.apiUrl}:`);
|
|
5095
|
+
const proxyAudience = await prompt(`Access application audience for ${plan.proxyUrl}:`, apiAudience);
|
|
5096
|
+
protection = {
|
|
5097
|
+
teamDomain,
|
|
5098
|
+
applications: [{
|
|
5099
|
+
origin: plan.apiUrl,
|
|
5100
|
+
audience: apiAudience
|
|
5101
|
+
}, {
|
|
5102
|
+
origin: plan.proxyUrl,
|
|
5103
|
+
audience: proxyAudience
|
|
5104
|
+
}]
|
|
5105
|
+
};
|
|
5106
|
+
}
|
|
5107
|
+
protection = parsePlatformAccessProtection(protection);
|
|
5108
|
+
let admissionInput = selection.file?.admission ?? (protection ? {
|
|
5109
|
+
mode: "company",
|
|
5110
|
+
access: true,
|
|
5111
|
+
connections: []
|
|
5112
|
+
} : { mode: "restricted" });
|
|
5113
|
+
if (!selection.file && !protection && process.stdin.isTTY) {
|
|
5114
|
+
const eligible = connections.filter((entry) => entry.configuration.kind !== "github" && (entry.configuration.kind !== "google" || entry.configuration.allowedDomains.length > 0));
|
|
5115
|
+
const mode = answer(await select({
|
|
5116
|
+
message: "Who can create an account?",
|
|
5117
|
+
initialValue: eligible.length ? "company" : "restricted",
|
|
5118
|
+
options: [
|
|
5119
|
+
{
|
|
5120
|
+
value: "restricted",
|
|
5121
|
+
label: "Invited or allowlisted users"
|
|
5122
|
+
},
|
|
5123
|
+
...eligible.length ? [{
|
|
5124
|
+
value: "company",
|
|
5125
|
+
label: "Users approved by company login methods — automatic accounts"
|
|
5126
|
+
}] : [],
|
|
5127
|
+
{
|
|
5128
|
+
value: "public",
|
|
5129
|
+
label: "Anyone who completes an enabled login"
|
|
5130
|
+
}
|
|
5131
|
+
]
|
|
5132
|
+
}));
|
|
5133
|
+
const ids = mode === "company" ? answer(await multiselect({
|
|
5134
|
+
message: "Company login methods that qualify new users:",
|
|
5135
|
+
required: true,
|
|
5136
|
+
initialValues: eligible.map((entry) => entry.configuration.id),
|
|
5137
|
+
options: eligible.map((entry) => ({
|
|
5138
|
+
value: entry.configuration.id,
|
|
5139
|
+
label: entry.configuration.label
|
|
5140
|
+
}))
|
|
5141
|
+
})) : [];
|
|
5142
|
+
admissionInput = mode === "company" ? {
|
|
5143
|
+
mode,
|
|
5144
|
+
connections: ids,
|
|
5145
|
+
access: false
|
|
5146
|
+
} : { mode };
|
|
5147
|
+
}
|
|
5148
|
+
const admission = parsePlatformAdmissionPolicy(admissionInput);
|
|
5149
|
+
let authBootstrapCode = checkpoint.authBootstrapCode || randomBytes(32).toString("hex");
|
|
5150
|
+
const authSetupToken = checkpoint.authSetupToken || randomBytes(32).toString("hex");
|
|
5151
|
+
let setup = parsePlatformAuthenticationSetup({
|
|
5152
|
+
connections,
|
|
5153
|
+
administratorConnectionId,
|
|
5154
|
+
admission,
|
|
5155
|
+
protection,
|
|
5156
|
+
bootstrapCredentialHash: createHash("sha256").update(authBootstrapCode).digest("hex")
|
|
5157
|
+
});
|
|
5158
|
+
if (selection.file && checkpoint.authenticationSetupJson) {
|
|
5159
|
+
const previous = parsePlatformAuthenticationSetup(JSON.parse(checkpoint.authenticationSetupJson));
|
|
5160
|
+
if (JSON.stringify({
|
|
5161
|
+
...previous,
|
|
5162
|
+
resetPending: void 0
|
|
5163
|
+
}) !== JSON.stringify(setup)) {
|
|
5164
|
+
authBootstrapCode = randomBytes(32).toString("hex");
|
|
5165
|
+
setup = parsePlatformAuthenticationSetup({
|
|
5166
|
+
...setup,
|
|
5167
|
+
resetPending: true,
|
|
5168
|
+
bootstrapCredentialHash: createHash("sha256").update(authBootstrapCode).digest("hex")
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
const result = {
|
|
5173
|
+
authenticationSetupJson: JSON.stringify(setup),
|
|
5174
|
+
authBootstrapCode,
|
|
5175
|
+
authSetupToken
|
|
5176
|
+
};
|
|
5177
|
+
save(result);
|
|
5178
|
+
return result;
|
|
5179
|
+
}
|
|
5180
|
+
//#endregion
|
|
3757
5181
|
//#region src/cli/platform-setup-draft.ts
|
|
3758
5182
|
const STARTED = "credential-setup:v1";
|
|
3759
5183
|
const COMPLETED = "credential-setup-complete:v1";
|
|
@@ -3808,9 +5232,9 @@ function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
|
|
|
3808
5232
|
return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + page.title + "\x1B]8;;\x1B\\" : page.title + ": " + page.url;
|
|
3809
5233
|
}
|
|
3810
5234
|
function platformSetupFieldHelp(key) {
|
|
3811
|
-
const docs = (section, title) => platformSetupLink({
|
|
5235
|
+
const docs = (page, section, title) => platformSetupLink({
|
|
3812
5236
|
title,
|
|
3813
|
-
url:
|
|
5237
|
+
url: `https://void.cloud/guide/platform/installation/${page}#${section}`
|
|
3814
5238
|
});
|
|
3815
5239
|
switch (key) {
|
|
3816
5240
|
case "databaseUrl": return {
|
|
@@ -3818,10 +5242,10 @@ function platformSetupFieldHelp(key) {
|
|
|
3818
5242
|
description: "Stores platform users, projects, and deployments; use an empty hosted database, e.g. " + platformSetupLink({
|
|
3819
5243
|
title: "Neon",
|
|
3820
5244
|
url: "https://neon.com/"
|
|
3821
|
-
}) + ". " + docs("platform-database", "Database setup")
|
|
5245
|
+
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
3822
5246
|
};
|
|
3823
|
-
case "jwtSecret": return { description: "Signs platform login tokens. Run openssl rand -base64 32; save the key in your password manager. " + docs("signing-and-encryption-keys", "Key setup") };
|
|
3824
|
-
case "projectSecretKey": return { description: "Encrypts app secrets. Run openssl rand -base64 32 separately; save this key for recovery. " + docs("signing-and-encryption-keys", "Key setup") };
|
|
5247
|
+
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") };
|
|
5248
|
+
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") };
|
|
3825
5249
|
default: return;
|
|
3826
5250
|
}
|
|
3827
5251
|
}
|
|
@@ -3885,7 +5309,29 @@ function platformSetupPages(plan) {
|
|
|
3885
5309
|
...plan.deployment.capabilities.customDomains ? [{
|
|
3886
5310
|
key: "ssl_and_certificates",
|
|
3887
5311
|
type: "edit"
|
|
3888
|
-
}] : []
|
|
5312
|
+
}] : [],
|
|
5313
|
+
...plan.deployment.capabilities.email ? [
|
|
5314
|
+
{
|
|
5315
|
+
key: "zone",
|
|
5316
|
+
type: "read"
|
|
5317
|
+
},
|
|
5318
|
+
{
|
|
5319
|
+
key: "zone_settings",
|
|
5320
|
+
type: "edit"
|
|
5321
|
+
},
|
|
5322
|
+
{
|
|
5323
|
+
key: "email_routing_address",
|
|
5324
|
+
type: "edit"
|
|
5325
|
+
},
|
|
5326
|
+
{
|
|
5327
|
+
key: "email_routing_rule",
|
|
5328
|
+
type: "edit"
|
|
5329
|
+
},
|
|
5330
|
+
{
|
|
5331
|
+
key: "email_sending",
|
|
5332
|
+
type: "edit"
|
|
5333
|
+
}
|
|
5334
|
+
] : []
|
|
3889
5335
|
];
|
|
3890
5336
|
return {
|
|
3891
5337
|
runtime: {
|
|
@@ -3901,6 +5347,7 @@ function platformSetupPages(plan) {
|
|
|
3901
5347
|
"Read: Account Settings, Account Analytics, Workers Tail.",
|
|
3902
5348
|
...plan.deployment.application.zoneName ? ["Zone " + plan.deployment.application.zoneName + ": Cache Purge: Purge."] : [],
|
|
3903
5349
|
...plan.deployment.capabilities.customDomains ? ["Custom project domains also need SSL and Certificates: Edit on that zone."] : [],
|
|
5350
|
+
...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."] : [],
|
|
3904
5351
|
"If the form differs, match the permissions above. Create the token, then paste it here."
|
|
3905
5352
|
].join("\n")
|
|
3906
5353
|
},
|
|
@@ -3933,8 +5380,9 @@ function formatPlatformSetupPlan(plan) {
|
|
|
3933
5380
|
platformSetupLink(pages.r2),
|
|
3934
5381
|
pages.r2.instructions,
|
|
3935
5382
|
"",
|
|
3936
|
-
platformSetupLink(pages.github),
|
|
3937
|
-
|
|
5383
|
+
...plan.deployment.authentication.providers.includes("github") ? [platformSetupLink(pages.github), pages.github.instructions] : [],
|
|
5384
|
+
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => `${provider === "google" ? "Google OAuth client" : provider === "oidc" ? "OIDC connection" : "Cloudflare Access OIDC application"}: client ID and secret; callback ${plan.apiUrl}/auth/callback.`),
|
|
5385
|
+
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access protection: configure API and proxy origins and admitted installer/CI credentials."] : []
|
|
3938
5386
|
].join("\n");
|
|
3939
5387
|
}
|
|
3940
5388
|
function openPlatformSetupPage(page, dependencies = {}) {
|
|
@@ -3992,14 +5440,14 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
|
3992
5440
|
}
|
|
3993
5441
|
async function runPlatformLifecycleCommand(args) {
|
|
3994
5442
|
if (args.subcommand === "domain") {
|
|
3995
|
-
const { runPlatformDomainCommand } = await import("./platform-domain-
|
|
5443
|
+
const { runPlatformDomainCommand } = await import("./platform-domain-4GiDlcqx.mjs");
|
|
3996
5444
|
await runPlatformDomainCommand(args);
|
|
3997
5445
|
return;
|
|
3998
5446
|
}
|
|
3999
5447
|
if (args.subcommand === "discover") {
|
|
4000
5448
|
const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
|
|
4001
5449
|
const accountId = await selectAccount(await readAccounts(client), args.account);
|
|
4002
|
-
const { recoverPlatformInstallations } = await import("./platform-recovery-
|
|
5450
|
+
const { recoverPlatformInstallations } = await import("./platform-recovery-CbK-I1FB.mjs");
|
|
4003
5451
|
const recovered = await recoverPlatformInstallations(client, accountId, void 0, args.selector);
|
|
4004
5452
|
if (recovered.length === 0) {
|
|
4005
5453
|
log.info(`No recoverable Void platform installations were found in ${accountId}.`);
|
|
@@ -4009,7 +5457,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4009
5457
|
return;
|
|
4010
5458
|
}
|
|
4011
5459
|
if (args.subcommand !== "install") {
|
|
4012
|
-
const { runInstalledPlatformCommand } = await import("./platform-management-
|
|
5460
|
+
const { runInstalledPlatformCommand } = await import("./platform-management-BfWsXHEW.mjs");
|
|
4013
5461
|
await runInstalledPlatformCommand(args);
|
|
4014
5462
|
return;
|
|
4015
5463
|
}
|
|
@@ -4159,7 +5607,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4159
5607
|
progress?.stop("Read the saved installation.");
|
|
4160
5608
|
progress = void 0;
|
|
4161
5609
|
if (authoritative) {
|
|
4162
|
-
const { selectAuthoritativePlatformManifest } = await import("./platform-management-
|
|
5610
|
+
const { selectAuthoritativePlatformManifest } = await import("./platform-management-BfWsXHEW.mjs");
|
|
4163
5611
|
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
4164
5612
|
authoritativeProvisionIntents = true;
|
|
4165
5613
|
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
@@ -4167,19 +5615,25 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4167
5615
|
if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
|
|
4168
5616
|
}
|
|
4169
5617
|
}
|
|
5618
|
+
const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication);
|
|
4170
5619
|
const planInput = {
|
|
5620
|
+
authentication: authenticationSelection.authentication,
|
|
4171
5621
|
accountId,
|
|
4172
5622
|
applicationDomain,
|
|
4173
5623
|
controlPlaneDomain,
|
|
4174
5624
|
dedicatedZone,
|
|
4175
5625
|
displayName,
|
|
5626
|
+
...process.env.VOID_EMAIL_SHARED_ZONE_ID ? { emailSharedZoneId: process.env.VOID_EMAIL_SHARED_ZONE_ID } : {},
|
|
5627
|
+
...process.env.VOID_EMAIL_SENDER_DOMAIN ? { emailSenderDomain: process.env.VOID_EMAIL_SENDER_DOMAIN } : {},
|
|
4176
5628
|
installationSlug,
|
|
4177
5629
|
zoneName,
|
|
4178
5630
|
...workersDev ? { workersDev: true } : {}
|
|
4179
5631
|
};
|
|
4180
5632
|
let plan = await createPlatformInstallPlan(client, planInput, existing, {
|
|
4181
5633
|
authoritativeProvisionIntents,
|
|
4182
|
-
allowUnverifiedAiGateway: true
|
|
5634
|
+
allowUnverifiedAiGateway: true,
|
|
5635
|
+
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
5636
|
+
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3
|
|
4183
5637
|
});
|
|
4184
5638
|
note(formatPlatformInstallPlan(plan), args.plan ? "Read-only installation plan" : "Installation plan");
|
|
4185
5639
|
note(formatPlatformRuntime(runtime), "Platform runtime");
|
|
@@ -4258,22 +5712,44 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4258
5712
|
await verifyRuntimeToken(runtimeCloudflareApiToken);
|
|
4259
5713
|
}
|
|
4260
5714
|
saveValues({ cloudflareApiToken: runtimeCloudflareApiToken });
|
|
4261
|
-
const
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4264
|
-
|
|
5715
|
+
const deferAuthentication = !!authenticationSelection.authentication.accessProtection || authenticationSelection.authentication.providers.includes("cloudflare-access");
|
|
5716
|
+
const saveAuthenticationValues = (updates) => {
|
|
5717
|
+
Object.assign(values, updates);
|
|
5718
|
+
const latest = readPendingInstallationSecrets(setupManifest) ?? {};
|
|
5719
|
+
writePendingInstallationSecrets(setupManifest, {
|
|
5720
|
+
...latest,
|
|
5721
|
+
...updates
|
|
5722
|
+
});
|
|
5723
|
+
};
|
|
5724
|
+
const authenticationSecrets = deferAuthentication ? {
|
|
5725
|
+
authSetupToken: values.authSetupToken || randomBytes(32).toString("hex"),
|
|
5726
|
+
authBootstrapCode: values.authBootstrapCode || randomBytes(32).toString("hex")
|
|
5727
|
+
} : await collectInstallationAuthentication(authenticationSelection, plan, values, saveAuthenticationValues);
|
|
5728
|
+
if (authenticationSecrets) saveAuthenticationValues(authenticationSecrets);
|
|
5729
|
+
let bootstrapGitHubLogin = "";
|
|
5730
|
+
let bootstrapGitHubId = "";
|
|
5731
|
+
let githubClientId = "";
|
|
5732
|
+
let githubClientSecret = "";
|
|
5733
|
+
if (!authenticationSecrets) {
|
|
5734
|
+
const previousLogin = values.bootstrapGitHubLogin;
|
|
5735
|
+
bootstrapGitHubLogin = await collect("bootstrapGitHubLogin", "VOID_PLATFORM_ADMIN_GITHUB_LOGIN", "Initial administrator GitHub username:", false);
|
|
5736
|
+
bootstrapGitHubId = (previousLogin === bootstrapGitHubLogin ? values.bootstrapGitHubId : "") || process.env.VOID_PLATFORM_ADMIN_GITHUB_ID || await resolveBootstrapGitHubId(bootstrapGitHubLogin);
|
|
5737
|
+
saveValues({ bootstrapGitHubId });
|
|
5738
|
+
}
|
|
4265
5739
|
const installationDatabaseUrl = databaseUrl || await collect("databaseUrl", "VOID_PLATFORM_DATABASE_URL", "PostgreSQL DATABASE_URL:");
|
|
4266
5740
|
saveValues({ databaseUrl: installationDatabaseUrl });
|
|
4267
|
-
if (!
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
5741
|
+
if (!authenticationSecrets) {
|
|
5742
|
+
if (!configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") || !configured("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET")) openPlatformSetupPage(configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") ? {
|
|
5743
|
+
...pages.github,
|
|
5744
|
+
url: "https://github.com/settings/developers"
|
|
5745
|
+
} : pages.github);
|
|
5746
|
+
githubClientId = await collect("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID", "GitHub OAuth client ID:", false, false);
|
|
5747
|
+
githubClientSecret = await collect("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET", "GitHub OAuth client secret:", true, false);
|
|
5748
|
+
saveValues({
|
|
5749
|
+
githubClientId,
|
|
5750
|
+
githubClientSecret
|
|
5751
|
+
});
|
|
5752
|
+
}
|
|
4277
5753
|
if (!configured("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID") || !configured("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY")) openPlatformSetupPage(pages.r2);
|
|
4278
5754
|
const r2AccessKeyId = await collect("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID", "Access Key ID:", false, false);
|
|
4279
5755
|
const r2SecretAccessKey = await collect("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY", "Secret Access Key:", true, false);
|
|
@@ -4283,11 +5759,16 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4283
5759
|
});
|
|
4284
5760
|
const hasFullProjectSecretKeyring = Boolean(checkpoint?.projectSecretKeysJson || checkpoint?.projectSecretActiveKeyVersion || process.env.VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON || process.env.VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION);
|
|
4285
5761
|
const secrets = createPlatformInstallationSecrets({
|
|
5762
|
+
configurableAuthentication: !!authenticationSelection.authentication.configurable,
|
|
5763
|
+
...authenticationSecrets,
|
|
4286
5764
|
bootstrapGitHubId,
|
|
4287
5765
|
bootstrapGitHubLogin,
|
|
4288
5766
|
cloudflareApiToken: runtimeCloudflareApiToken,
|
|
4289
5767
|
checkpoint: values,
|
|
4290
5768
|
databaseUrl: installationDatabaseUrl,
|
|
5769
|
+
emailEnabled: plan.deployment.capabilities.email,
|
|
5770
|
+
emailSigningSecret: process.env.VOID_PLATFORM_EMAIL_SIGNING_SECRET || "",
|
|
5771
|
+
existingEmailEnabled: existing.deployment.capabilities.email,
|
|
4291
5772
|
githubClientId,
|
|
4292
5773
|
githubClientSecret,
|
|
4293
5774
|
r2AccessKeyId,
|
|
@@ -4303,7 +5784,10 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4303
5784
|
});
|
|
4304
5785
|
progress = startProgress("Installing the platform");
|
|
4305
5786
|
if (credentialSetup) {
|
|
4306
|
-
saveValues({
|
|
5787
|
+
saveValues({
|
|
5788
|
+
projectSecretKey: secrets.projectSecretKey,
|
|
5789
|
+
...secrets.emailEnabled ? { emailSigningSecret: secrets.emailSigningSecret } : {}
|
|
5790
|
+
});
|
|
4307
5791
|
existing = completePlatformSetupDraft(existing);
|
|
4308
5792
|
}
|
|
4309
5793
|
const result = await installPlatform(plan, secrets, existing, {
|
|
@@ -4312,13 +5796,30 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4312
5796
|
runtime,
|
|
4313
5797
|
healthCheckResumeCommand: `${cliCommandName()} platform install --resume --name ${existing.id}`,
|
|
4314
5798
|
onProgress: (message) => progress?.message(message),
|
|
5799
|
+
...deferAuthentication ? { prepareAuthentication: async (manifest) => {
|
|
5800
|
+
progress?.stop("Preparing Cloudflare Access configuration.");
|
|
5801
|
+
progress = void 0;
|
|
5802
|
+
const result = await collectInstallationAuthentication(authenticationSelection, plan, values, saveAuthenticationValues, {
|
|
5803
|
+
client,
|
|
5804
|
+
ownershipNonce: manifest.ownershipNonce,
|
|
5805
|
+
yes: args.yes
|
|
5806
|
+
});
|
|
5807
|
+
progress = startProgress("Continuing platform installation");
|
|
5808
|
+
return {
|
|
5809
|
+
...result,
|
|
5810
|
+
accessProvisioningJson: values.accessProvisioningJson,
|
|
5811
|
+
accessCredentialsJson: values.accessCredentialsJson
|
|
5812
|
+
};
|
|
5813
|
+
} } : {},
|
|
4315
5814
|
replanWorkers: async (manifest) => (await createPlatformInstallPlan(client, planInput, manifest, {
|
|
4316
5815
|
authoritativeProvisionIntents: true,
|
|
4317
|
-
repairMissing: true
|
|
5816
|
+
repairMissing: true,
|
|
5817
|
+
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
5818
|
+
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3
|
|
4318
5819
|
})).actions.filter((action) => action.kind === "worker"),
|
|
4319
5820
|
revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
|
|
4320
5821
|
verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
|
|
4321
|
-
const { verifyPlatformInstallationOwnership } = await import("./platform-management-
|
|
5822
|
+
const { verifyPlatformInstallationOwnership } = await import("./platform-management-BfWsXHEW.mjs");
|
|
4322
5823
|
const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
|
|
4323
5824
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
4324
5825
|
return action.action === "create" && previous && previous.name !== action.name;
|
|
@@ -4344,6 +5845,13 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
4344
5845
|
}
|
|
4345
5846
|
log.success(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
|
|
4346
5847
|
log.info(`API URL: ${plan.apiUrl}`);
|
|
5848
|
+
if (result.emailOperationId) log.info(`Shared email routing ready (operation ${result.emailOperationId}).`);
|
|
5849
|
+
const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
5850
|
+
if (result.setupPending && completedAuthCode) {
|
|
5851
|
+
log.info(`First administrator setup: ${plan.apiUrl}/setup`);
|
|
5852
|
+
console.log(`One-time administrator setup code: ${completedAuthCode}`);
|
|
5853
|
+
log.info("Confirm your identity in the browser, then test and enable any additional login methods in Settings.");
|
|
5854
|
+
}
|
|
4347
5855
|
if (!plan.deployment.application.zoneName) log.info("Apps will use workers.dev addresses. When your domain is ready: void platform domain set <domain>");
|
|
4348
5856
|
outro("Run `void auth login`, then deploy an app with `void deploy --platform void`.");
|
|
4349
5857
|
} catch (error) {
|
|
@@ -4374,7 +5882,11 @@ function formatPlatformRuntime(runtime) {
|
|
|
4374
5882
|
async function verifyPlatformAiGatewayPlan(client, input, existing, preview, authoritativeProvisionIntents) {
|
|
4375
5883
|
let verified;
|
|
4376
5884
|
try {
|
|
4377
|
-
verified = await createPlatformInstallPlan(client, input, existing, {
|
|
5885
|
+
verified = await createPlatformInstallPlan(client, input, existing, {
|
|
5886
|
+
authoritativeProvisionIntents,
|
|
5887
|
+
includeEmailGateway: preview.actions.some((entry) => entry.key === "email-gateway-worker"),
|
|
5888
|
+
enableSandbox: preview.deployment.capabilities.sandbox
|
|
5889
|
+
});
|
|
4378
5890
|
} catch (error) {
|
|
4379
5891
|
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 });
|
|
4380
5892
|
throw error;
|
|
@@ -4422,7 +5934,7 @@ async function promptText(message, value, optional = false, validate) {
|
|
|
4422
5934
|
if (optional) return "";
|
|
4423
5935
|
throw new Error(`platform: ${message.replace(/:$/, "")} is required in non-interactive mode.`);
|
|
4424
5936
|
}
|
|
4425
|
-
const input = await text({
|
|
5937
|
+
const input = await text$1({
|
|
4426
5938
|
message,
|
|
4427
5939
|
...typeof value === "string" ? { initialValue: value } : value,
|
|
4428
5940
|
validate: (input) => input ? validate?.(input) : optional ? void 0 : "A value is required."
|
|
@@ -4439,7 +5951,7 @@ async function promptSecret(message, masked = true, validate, placeholder) {
|
|
|
4439
5951
|
const value = masked ? placeholder ? await passwordWithPlaceholder({
|
|
4440
5952
|
...options,
|
|
4441
5953
|
placeholder
|
|
4442
|
-
}) : await password(options) : await text({
|
|
5954
|
+
}) : await password(options) : await text$1({
|
|
4443
5955
|
...options,
|
|
4444
5956
|
placeholder
|
|
4445
5957
|
});
|
|
@@ -4447,4 +5959,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
|
|
|
4447
5959
|
return value;
|
|
4448
5960
|
}
|
|
4449
5961
|
//#endregion
|
|
4450
|
-
export {
|
|
5962
|
+
export { PlatformCloudflareClient as C, verifyPlatformRuntimeTokenPermissions as D, hyperdriveOriginMatches as E, preparePlatformAccessSetup as S, hyperdriveOriginFromConnectionString as T, revalidatePlatformInstallAction as _, createPlatformInstallationSecrets as a, assertNoninteractivePlatformMutationAcknowledged, checkpointPlatformAccessResources as b, platformDatabaseClientUrl as c, recordPlatformWorkerTraffic as d, trackedPlatformWorkerTraffic as f, platformProvisionIntentHash as g, platformAdoptionIntentHash as h, createPlatformHealthProbeToken as i, recordResource as l, formatPlatformInstallPlan as m, acquirePlatformDatabaseLifecycleLock as n, installPlatform as o, createPlatformInstallPlan as p, acquirePlatformDatabaseLifecycleLockConnection as r, resolvePlatformInstallDatabaseUrl, runPlatformLifecycleCommand, loadPlatformRuntime as s, REMOTE_INSTALLATION_MANIFEST_KEY as t, resolveBootstrapGitHubId as u, assertPlatformOperatorCredentialCanApplyPlan as v, hyperdriveOriginConfigHash as w, parsePlatformAccessSetupSpec as x, resolvePlatformCloudflareCredential as y };
|