void 0.20.0 → 0.20.2

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.
Files changed (93) hide show
  1. package/README.md +5 -1
  2. package/dist/{auth-W9WII-mN.mjs → auth-DPl6kck4.mjs} +46 -26
  3. package/dist/{auth-cmd-CYVhSNFy.mjs → auth-cmd-gniL2fNt.mjs} +5 -4
  4. package/dist/auth-link-NZdjCmSc.mjs +28 -0
  5. package/dist/{build-cmd-CkVD1uOh.mjs → build-cmd-sI18tX_O.mjs} +3 -3
  6. package/dist/{cache-QcUfR-ff.mjs → cache-IHn5MwBC.mjs} +3 -3
  7. package/dist/{cancel-deploy-DsvWKFTe.mjs → cancel-deploy-C5qTOdLi.mjs} +3 -3
  8. package/dist/cf-access-DRsQRe6k.mjs +75 -0
  9. package/dist/cli/cli.mjs +364 -1883
  10. package/dist/cli/env-schema-probe.mjs +11 -2
  11. package/dist/{client-CyCHWSO_.mjs → client-dHfSJvAN.mjs} +336 -58
  12. package/dist/{cloudflare-auth-DRkGe7-s.mjs → cloudflare-auth-6M5llVPC.mjs} +53 -6
  13. package/dist/{cloudflare-cmd-D3ME2GAe.mjs → cloudflare-cmd-4RPGN3KB.mjs} +2 -2
  14. package/dist/{cloudflare-connect-BivMefMA.mjs → cloudflare-connect-t1UU5svD.mjs} +2 -2
  15. package/dist/{cloudflare-operations-CPTpRW6d.mjs → cloudflare-operations-BzWnlC1_.mjs} +1 -1
  16. package/dist/{config-BQFq7QvD.mjs → config-uNGuFsI2.mjs} +1 -1
  17. package/dist/{connect-D9Yr-T5f.mjs → connect-Bfk31O_8.mjs} +6 -6
  18. package/dist/{create-project-DMV-csEm.mjs → create-project-Bk9Z0-Jg.mjs} +6 -5
  19. package/dist/{db-BxoUWL0F.mjs → db-BkRoptAt.mjs} +75 -48
  20. package/dist/{delete-iJOqxBz1.mjs → delete-DouASY9P.mjs} +3 -3
  21. package/dist/{deploy-CcDoeBIf.mjs → deploy-DTaWUS1S.mjs} +137 -128
  22. package/dist/{dev-inbox-DkgRWLkW.mjs → dev-inbox-P0u4tM8Y.mjs} +1 -1
  23. package/dist/{domain-gu_iaHmn.mjs → domain-1RhhOVrC.mjs} +4 -4
  24. package/dist/email-C-lGh51B.mjs +795 -0
  25. package/dist/{env-D4Emu-M_.mjs → env-DBKmK4vc.mjs} +1 -0
  26. package/dist/{env-DmU2To0C.mjs → env-DJHsPE7Z.mjs} +5 -5
  27. package/dist/{env-validation-ENpMy6Ez.mjs → env-validation-CF6KvTRf.mjs} +3 -1
  28. package/dist/{gen-BKw6qHIg.mjs → gen-B_wPnVTK.mjs} +3 -3
  29. package/dist/generate-RTK8_kK1.mjs +47 -0
  30. package/dist/{github-cmd-DJS5Ab-H.mjs → github-cmd-PW7ZnWTp.mjs} +3 -3
  31. package/dist/{headers-D8QfRX9Y.mjs → headers-BAHwgHdW.mjs} +1 -1
  32. package/dist/help-CwOX-zmI.mjs +2216 -0
  33. package/dist/{inbound-BJ70in1n.d.mts → inbound-CH5Mksyy.d.mts} +37 -42
  34. package/dist/{inbound-CNKxb3FY.mjs → inbound-aVHEUhKo.mjs} +143 -101
  35. package/dist/index.mjs +69 -30
  36. package/dist/{init-Dx5cgKqK.mjs → init-BWZ7q5Z4.mjs} +11 -11
  37. package/dist/{link-D_rm5sRH.mjs → link-Rmvu2Wl_.mjs} +4 -4
  38. package/dist/{list-M8XShti7.mjs → list-DEE2S6mY.mjs} +4 -4
  39. package/dist/{local-d1-BE8KBbMy.mjs → local-d1-D2I6Ox5F.mjs} +1 -1
  40. package/dist/{login-C8-UuLnp.mjs → login-Uvferzmm.mjs} +28 -10
  41. package/dist/{logs-DwFU7dMW.mjs → logs-27FenuiC.mjs} +4 -4
  42. package/dist/{mime-BJD7d_qL.mjs → mime-D5Nmdzf7.mjs} +23 -9
  43. package/dist/{node-BkaXcpAc.mjs → node-Ez5KW5rn.mjs} +3 -3
  44. package/dist/operator-auth-B3e08unv.mjs +52 -0
  45. package/dist/operator-client-LUZnlnYk.mjs +82 -0
  46. package/dist/{operator-cmd-DYWRbWUA.mjs → operator-cmd-CjOTmAYE.mjs} +35 -55
  47. package/dist/{output-tFQLLj26.mjs → output-B0cfNSx5.mjs} +316 -2
  48. package/dist/pages/index.mjs +2 -2
  49. package/dist/platform-auth-config-DrbQXXiW.mjs +368 -0
  50. package/dist/platform-auth-protection-Bhtvp0B_.mjs +219 -0
  51. package/dist/platform-auth-recovery-CeOKGVeJ.mjs +310 -0
  52. package/dist/{platform-cmd-BEOVv1PN.mjs → platform-cmd-BFhieCdV.mjs} +16 -6
  53. package/dist/{platform-domain-BszUfiS8.mjs → platform-domain-C74PULqV.mjs} +4 -4
  54. package/dist/{platform-lifecycle-CjSr6xf0.mjs → platform-lifecycle-BwAIgz-t.mjs} +1667 -191
  55. package/dist/{platform-management-W30iCaTL.mjs → platform-management-COogu_Se.mjs} +33 -7
  56. package/dist/{platform-recovery-pHHv4ZSg.mjs → platform-recovery-ewqLefp1.mjs} +6 -5
  57. package/dist/{prepare-CBetXvsN.mjs → prepare-CtDJjoOj.mjs} +2 -2
  58. package/dist/{prepare-BfJvFUtJ.mjs → prepare-blNRQvQl.mjs} +2 -2
  59. package/dist/prerender-render.d.mts +11 -0
  60. package/dist/prerender-render.mjs +111 -0
  61. package/dist/{project-cmd-DCpk1cDt.mjs → project-cmd-DmZK9Hxf.mjs} +32 -14
  62. package/dist/project-team-D8jOJMUJ.mjs +130 -0
  63. package/dist/project-token-DA34bf-C.mjs +75 -0
  64. package/dist/{provision-Blnstcm2.mjs → provision-CSJOjjQk.mjs} +2 -0
  65. package/dist/{requests-Dn8Vheh1.mjs → requests-CUExwGQQ.mjs} +3 -3
  66. package/dist/{rollback-CtlPXEBi.mjs → rollback-CDNGU1gr.mjs} +4 -4
  67. package/dist/{runner-mysql-CgRFl3s6.mjs → runner-mysql-7BPUNGmL.mjs} +1 -1
  68. package/dist/{runner-pg-DCkWPsWS.mjs → runner-pg-BkEza-dX.mjs} +1 -1
  69. package/dist/runtime/ai.mjs +3 -2
  70. package/dist/runtime/email/testing.d.mts +1 -1
  71. package/dist/runtime/email/testing.mjs +3 -3
  72. package/dist/runtime/email-protocol.d.mts +15 -0
  73. package/dist/runtime/email-protocol.mjs +70 -0
  74. package/dist/runtime/email.d.mts +2 -2
  75. package/dist/runtime/email.mjs +189 -96
  76. package/dist/runtime/remote/index.mjs +5 -3
  77. package/dist/{secret-BqTxGqki.mjs → secret-Bzzi2e9E.mjs} +5 -5
  78. package/dist/{skills-Q46GZMO-.mjs → skills-C0RvGjeE.mjs} +1 -1
  79. package/dist/sqlite-validation-BzKMWnO4.mjs +25 -0
  80. package/dist/{subcommand-prompt-WfySCQ7S.mjs → subcommand-prompt-Bmyn5Rlc.mjs} +1 -1
  81. package/dist/{validate-Bihr8WBi.mjs → validate-tBBN_dXH.mjs} +1 -0
  82. package/package.json +12 -7
  83. package/skills/void/SKILL.md +35 -4
  84. package/skills/void/docs/guide/deployment.md +2 -0
  85. package/skills/void/docs/guide/email.md +121 -98
  86. package/skills/void/docs/guide/platform-administration.md +214 -2
  87. package/skills/void/docs/guide/platform-development.md +93 -2
  88. package/skills/void/docs/guide/project-collaboration.md +94 -0
  89. package/skills/void/docs/guide/self-hosted-platform.md +184 -30
  90. package/skills/void/docs/reference/cli.md +294 -15
  91. package/dist/cf-access-AJ1ehiFR.mjs +0 -42
  92. package/dist/cf-access-DsSsZUPr.mjs +0 -67
  93. package/dist/email-r6DHJyAB.mjs +0 -262
@@ -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 resolveInstallationManifest, A as password, B as assertPlatformInstallationSlugAvailable, D as log, E as intro, F as isCancel, H as latestPlatformWorkerCutoverGeneration, I as settings, J as promoteInstallationSecrets, L as u$1, M as symbol, N as text, O as note, P as CANCEL_SYMBOL, U as listInstallationManifests, X as readPendingInstallationSecrets, c as import_picocolors$1, et as savePlatformConnection, h as S_BAR_END, i as createSpinner, j as select, k as outro, m as S_BAR, n as cliCommandName, nt as writeInstallationManifest, q as platformWorkerSecretUpdateIntentState, r as cliTitle, rt as writePendingInstallationSecrets, st as require_picocolors, v as S_PASSWORD_MASK, w as confirm, z as acquirePlatformLifecycleLock } from "./output-tFQLLj26.mjs";
4
- import { t as cfAccessHeaders } from "./cf-access-AJ1ehiFR.mjs";
5
- import { d as openBrowser } from "./auth-W9WII-mN.mjs";
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 { c as runCloudflareAuthCommand, n as announceCloudflareLogin } from "./cloudflare-auth-DRkGe7-s.mjs";
8
- import { n as createDefaultRunWrangler, o as resolvePinnedApiBaseUrl } from "./provision-Blnstcm2.mjs";
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-Bhtvp0B_.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,23 @@ 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
+ ...input.authentication ? {
1603
+ githubOAuth: input.authentication.providers.includes("github"),
1604
+ googleOAuth: input.authentication.providers.includes("google")
1605
+ } : {},
1606
+ ...enableEmail ? { email: true } : {}
1607
+ };
1608
+ 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
1609
  const deployment = createPlatformDeploymentManifest({
1610
+ authentication: input.authentication ?? existing?.deployment.authentication,
1611
+ ...Object.keys(capabilities).length > 0 ? { capabilities } : {},
806
1612
  application: {
807
1613
  baseDomain: testing ? `${accountSubdomain}.workers.dev` : input.applicationDomain,
808
1614
  dedicatedZone: input.dedicatedZone,
@@ -816,10 +1622,21 @@ async function createPlatformInstallPlan(client, input, existing, options) {
816
1622
  proxyHostname: null
817
1623
  },
818
1624
  displayName: input.displayName,
1625
+ ...emailSharedZoneId ? { emailSharedZoneId } : {},
1626
+ ...normalizedEmailSenderDomain ? { emailSenderDomain: normalizedEmailSenderDomain } : {},
819
1627
  installationSlug,
820
1628
  resources: existing?.deployment.resources ?? createPlatformResourceNames(installationSlug)
821
1629
  });
822
- if (existing && JSON.stringify(existing.deployment) !== JSON.stringify(deployment)) throw new Error(`platform: --resume inputs do not match installation ${existing.id}. Use its recorded configuration.`);
1630
+ const { emailSharedZoneId: configuredEmailZone, emailSenderDomain: configuredSenderDomain, ...deploymentWithoutEmailZone } = deployment;
1631
+ const { emailSharedZoneId: recordedEmailZone, emailSenderDomain: recordedSenderDomain, ...existingWithoutEmailZone } = existing?.deployment ?? deployment;
1632
+ const comparableExistingWithoutEmailZone = existing && deployment.capabilities.email && !existing.deployment.capabilities.email ? {
1633
+ ...existingWithoutEmailZone,
1634
+ capabilities: {
1635
+ ...existingWithoutEmailZone.capabilities,
1636
+ email: true
1637
+ }
1638
+ } : existingWithoutEmailZone;
1639
+ 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
1640
  const names = deployment.resources;
824
1641
  const wildcardHostname = `*.${deployment.application.baseDomain}`;
825
1642
  const zone = testing ? null : await client.findZone(account.id, deployment.application.zoneName);
@@ -871,6 +1688,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
871
1688
  for (const [key, name] of [
872
1689
  ["api-worker", names.apiWorker],
873
1690
  ["proxy-worker", names.proxyWorker],
1691
+ ["email-gateway-worker", names.emailGatewayWorker],
874
1692
  ["tail-worker", names.tailWorker],
875
1693
  ["dispatch-worker", names.dispatchWorker]
876
1694
  ]) if (workers.includes(name) && hasUnrecordedProvisionIntent(existing, key, "worker", name)) {
@@ -936,6 +1754,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
936
1754
  for (const [key, name] of [
937
1755
  ["api-worker", names.apiWorker],
938
1756
  ["proxy-worker", names.proxyWorker],
1757
+ ...options?.includeEmailGateway === false ? [] : [["email-gateway-worker", names.emailGatewayWorker]],
939
1758
  ["tail-worker", names.tailWorker],
940
1759
  ["dispatch-worker", names.dispatchWorker]
941
1760
  ]) addNamedPlan(actions, existing, key, "worker", name, workers.includes(name) ? [{
@@ -972,7 +1791,10 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
972
1791
  displayName: deployment.displayName,
973
1792
  installationSlug: deployment.installationSlug,
974
1793
  zoneName: deployment.application.zoneName
975
- }, manifest, options)).actions.find((entry) => entry.key === action.key);
1794
+ }, manifest, {
1795
+ ...options,
1796
+ includeEmailGateway: original.actions.some((entry) => entry.key === "email-gateway-worker")
1797
+ })).actions.find((entry) => entry.key === action.key);
976
1798
  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
1799
  if (action.kind === "worker" && action.action === "update") {
978
1800
  if (!matchesResourceMarker((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, action.key, "worker", {
@@ -1171,6 +1993,7 @@ const RESOURCE_NAME_KEY_BY_ACTION = {
1171
1993
  "isr-cache-kv": "isrCacheKv",
1172
1994
  "legacy-assets-kv": "legacyAssetsKv",
1173
1995
  "proxy-worker": "proxyWorker",
1996
+ "email-gateway-worker": "emailGatewayWorker",
1174
1997
  "routing-kv": "routingKv",
1175
1998
  "tail-worker": "tailWorker",
1176
1999
  "usage-queue": "usageQueue"
@@ -1214,6 +2037,12 @@ function formatPlatformInstallPlan(plan) {
1214
2037
  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
2038
  `${import_picocolors$1.default.bold("Control plane:")} ${plan.apiUrl}`,
1216
2039
  `${import_picocolors$1.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
2040
+ `${import_picocolors$1.default.bold("Login methods:")} ${plan.deployment.authentication.providers.join(", ")}`,
2041
+ `${import_picocolors$1.default.bold("Access protection:")} ${plan.deployment.authentication.accessProtection ? "Enabled" : "Not enabled"}`,
2042
+ `${import_picocolors$1.default.bold("Login callback:")} ${plan.apiUrl}/auth/callback`,
2043
+ ...plan.deployment.authentication.providers.includes("cloudflare-access") ? ["Access login: create a dedicated OIDC application or connect an existing issuer."] : [],
2044
+ ...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.`] : [],
2045
+ `${import_picocolors$1.default.bold("Database:")} Verify uncached Hyperdrive reads for authentication`,
1217
2046
  ""
1218
2047
  ];
1219
2048
  for (const action of plan.actions) lines.push(`${action.action.padEnd(6)} ${action.kind.padEnd(19)} ${action.name}`);
@@ -1228,6 +2057,7 @@ const COMPLETE_WORKER_SECRET_UPDATE_NAMES = [
1228
2057
  "R2_ACCESS_KEY_ID",
1229
2058
  "R2_SECRET_ACCESS_KEY"
1230
2059
  ];
2060
+ const EMAIL_WORKER_SECRET_UPDATE_NAME = "EMAIL_SIGNING_SECRET";
1231
2061
  /**
1232
2062
  * Prevents a host without the encrypted values for an interrupted credential
1233
2063
  * update from carrying an opaque, possibly partial update into another cutover.
@@ -1236,7 +2066,7 @@ function assertPlatformWorkerSecretUpdateAuthority(input) {
1236
2066
  const unresolved = unresolvedWorkerSecretUpdate(input.manifest);
1237
2067
  if (!unresolved) return;
1238
2068
  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;
2069
+ 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
2070
  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
2071
  }
1242
2072
  function unresolvedWorkerSecretUpdate(manifest) {
@@ -1264,8 +2094,8 @@ function readLatestWorkerGeneration(manifest) {
1264
2094
  return match ? [Number(match[1])] : [];
1265
2095
  }));
1266
2096
  }
1267
- function hasCompleteWorkerSecretReplacement(updates) {
1268
- return COMPLETE_WORKER_SECRET_UPDATE_NAMES.every((name) => typeof updates[name] === "string" && updates[name] !== "");
2097
+ function hasCompleteWorkerSecretReplacement(updates, requiresEmailSigningSecret) {
2098
+ 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
2099
  }
1270
2100
  //#endregion
1271
2101
  //#region src/cli/platform-traffic.ts
@@ -1339,11 +2169,29 @@ function recordPlatformWorkerTraffic(manifest, workerName, inventory, updatedAt)
1339
2169
  const REMOTE_INSTALLATION_MANIFEST_KEY = PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY;
1340
2170
  const PLATFORM_WORKER_SECRET_UPDATE_NAMES = [
1341
2171
  "CF_API_TOKEN",
2172
+ "EMAIL_SIGNING_SECRET",
1342
2173
  "GITHUB_CLIENT_ID",
1343
2174
  "GITHUB_CLIENT_SECRET",
1344
2175
  "R2_ACCESS_KEY_ID",
1345
2176
  "R2_SECRET_ACCESS_KEY"
1346
2177
  ];
2178
+ /** Preserve database-authoritative authentication choices while upgrading old manifests. */
2179
+ function mergePlatformLiveAuthenticationDeployment(deployment, live) {
2180
+ const providers = live.enabledProviders.length > 0 ? live.enabledProviders : deployment.authentication.providers;
2181
+ return createPlatformDeploymentManifest({
2182
+ ...deployment,
2183
+ authentication: {
2184
+ providers,
2185
+ ...live.sessionsRequired ? { configurable: true } : {},
2186
+ ...live.protectedByAccess ? { accessProtection: true } : {}
2187
+ },
2188
+ capabilities: {
2189
+ ...deployment.capabilities,
2190
+ githubOAuth: providers.includes("github"),
2191
+ googleOAuth: providers.includes("google")
2192
+ }
2193
+ });
2194
+ }
1347
2195
  var PlatformWorkerRecoveryIncompleteError = class extends Error {
1348
2196
  constructor(message, options) {
1349
2197
  super(message, options);
@@ -1364,9 +2212,28 @@ function createPlatformInstallationSecrets(input) {
1364
2212
  const existingInstallation = input.existingInstallation === true;
1365
2213
  const restoredIdentity = (explicit, name) => existingInstallation ? checkpoint[name] || explicit || "" : explicit || checkpoint[name] || "";
1366
2214
  const configured = (explicit, name) => explicit || checkpoint[name] || "";
2215
+ const authenticationSetupJson = configured(input.authenticationSetupJson ?? "", "authenticationSetupJson");
2216
+ const authSetupToken = configured(input.authSetupToken ?? "", "authSetupToken");
2217
+ const authBootstrapCode = configured(input.authBootstrapCode ?? "", "authBootstrapCode");
2218
+ const modernAuthentication = input.configurableAuthentication === true || !!authenticationSetupJson;
2219
+ 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.");
2220
+ if (authenticationSetupJson) {
2221
+ const setup = parsePlatformAuthenticationSetup(JSON.parse(authenticationSetupJson));
2222
+ 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.");
2223
+ }
1367
2224
  const jwtSecret = restoredIdentity(input.jwtSecret || "", "jwtSecret");
1368
2225
  const resolvedJwtSecret = input.existingInstallation ? jwtSecret : required(jwtSecret, "VOID_PLATFORM_JWT_SECRET");
1369
2226
  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.");
2227
+ const emailEnabled = input.emailEnabled === true;
2228
+ const existingEmailEnabled = input.existingEmailEnabled === true;
2229
+ const emailSigningSecretInScope = emailEnabled || existingEmailEnabled;
2230
+ let emailSigningSecret = emailSigningSecretInScope ? restoredIdentity(input.emailSigningSecret || "", "emailSigningSecret") : checkpoint.emailSigningSecret || "";
2231
+ if (emailEnabled && !emailSigningSecret) {
2232
+ 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.");
2233
+ emailSigningSecret = randomBytes(32).toString("base64");
2234
+ }
2235
+ 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.");
2236
+ if (emailSigningSecretInScope && emailSigningSecret && emailSigningSecret === resolvedJwtSecret) throw new Error("platform: VOID_PLATFORM_EMAIL_SIGNING_SECRET must be different from VOID_PLATFORM_JWT_SECRET.");
1370
2237
  const projectSecretKeyring = resolveProjectSecretKeyring({
1371
2238
  checkpoint,
1372
2239
  existingInstallation,
@@ -1376,22 +2243,34 @@ function createPlatformInstallationSecrets(input) {
1376
2243
  required
1377
2244
  });
1378
2245
  const pendingWorkerSecretUpdateIntent = parseWorkerSecretUpdateIntent(checkpoint.workerSecretUpdatesJson);
2246
+ const emailSigningSecretUpdateAuthorized = emailSigningSecretInScope && !!emailSigningSecret && (emailEnabled && (!existingEmailEnabled || !checkpoint.emailSigningSecret) || !!input.emailSigningSecret);
1379
2247
  const explicitWorkerSecretUpdates = withoutEmptySecrets({
1380
2248
  CF_API_TOKEN: input.cloudflareApiToken,
2249
+ ...emailSigningSecretUpdateAuthorized ? { EMAIL_SIGNING_SECRET: emailSigningSecret } : {},
1381
2250
  GITHUB_CLIENT_ID: input.githubClientId,
1382
2251
  GITHUB_CLIENT_SECRET: input.githubClientSecret,
1383
2252
  R2_ACCESS_KEY_ID: input.r2AccessKeyId,
1384
2253
  R2_SECRET_ACCESS_KEY: input.r2SecretAccessKey
1385
2254
  });
1386
2255
  return {
2256
+ authenticationPreparation: Object.fromEntries(Object.entries(checkpoint).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
2257
+ ...modernAuthentication ? {
2258
+ authenticationSetupJson,
2259
+ authSetupToken,
2260
+ authBootstrapCode
2261
+ } : {},
2262
+ ...checkpoint.accessProvisioningJson ? { accessProvisioningJson: checkpoint.accessProvisioningJson } : {},
2263
+ ...checkpoint.accessCredentialsJson ? { accessCredentialsJson: checkpoint.accessCredentialsJson } : {},
1387
2264
  workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
1388
2265
  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"),
2266
+ bootstrapGitHubId: existingInstallation || modernAuthentication ? configured(input.bootstrapGitHubId, "bootstrapGitHubId") : required(configured(input.bootstrapGitHubId, "bootstrapGitHubId"), "resolved administrator GitHub account id"),
2267
+ bootstrapGitHubLogin: existingInstallation || modernAuthentication ? configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin") : required(configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin"), "VOID_PLATFORM_ADMIN_GITHUB_LOGIN"),
1391
2268
  cloudflareApiToken: existingInstallation ? configured(input.cloudflareApiToken, "cloudflareApiToken") : required(configured(input.cloudflareApiToken, "cloudflareApiToken"), "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN"),
1392
2269
  databaseUrl: required(input.databaseUrl || checkpoint.databaseUrl || "", "DATABASE_URL"),
1393
- githubClientId: existingInstallation ? configured(input.githubClientId, "githubClientId") : required(configured(input.githubClientId, "githubClientId"), "VOID_PLATFORM_GITHUB_CLIENT_ID"),
1394
- githubClientSecret: existingInstallation ? configured(input.githubClientSecret, "githubClientSecret") : required(configured(input.githubClientSecret, "githubClientSecret"), "VOID_PLATFORM_GITHUB_CLIENT_SECRET"),
2270
+ emailEnabled,
2271
+ emailSigningSecret,
2272
+ githubClientId: existingInstallation || modernAuthentication ? configured(input.githubClientId, "githubClientId") : required(configured(input.githubClientId, "githubClientId"), "VOID_PLATFORM_GITHUB_CLIENT_ID"),
2273
+ githubClientSecret: existingInstallation || modernAuthentication ? configured(input.githubClientSecret, "githubClientSecret") : required(configured(input.githubClientSecret, "githubClientSecret"), "VOID_PLATFORM_GITHUB_CLIENT_SECRET"),
1395
2274
  jwtSecret: resolvedJwtSecret,
1396
2275
  projectSecretActiveKeyVersion: projectSecretKeyring.activeVersion,
1397
2276
  projectSecretKey: projectSecretKeyring.v1Key,
@@ -1450,6 +2329,14 @@ function assertCanonicalProjectSecretKey(value, name, uniqueToInstallation = fal
1450
2329
  }
1451
2330
  function secretCheckpoint(secrets, workerSecretUpdateIntent) {
1452
2331
  return {
2332
+ ...Object.fromEntries(Object.entries(secrets.authenticationPreparation ?? {}).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
2333
+ ...secrets.authSetupToken ? {
2334
+ authenticationSetupJson: secrets.authenticationSetupJson ?? "",
2335
+ authSetupToken: secrets.authSetupToken ?? "",
2336
+ authBootstrapCode: secrets.authBootstrapCode ?? ""
2337
+ } : {},
2338
+ ...secrets.accessProvisioningJson ? { accessProvisioningJson: secrets.accessProvisioningJson } : {},
2339
+ ...secrets.accessCredentialsJson ? { accessCredentialsJson: secrets.accessCredentialsJson } : {},
1453
2340
  bootstrapGitHubId: secrets.bootstrapGitHubId,
1454
2341
  bootstrapGitHubLogin: secrets.bootstrapGitHubLogin,
1455
2342
  cloudflareApiToken: secrets.cloudflareApiToken,
@@ -1457,6 +2344,7 @@ function secretCheckpoint(secrets, workerSecretUpdateIntent) {
1457
2344
  githubClientId: secrets.githubClientId,
1458
2345
  githubClientSecret: secrets.githubClientSecret,
1459
2346
  jwtSecret: secrets.jwtSecret,
2347
+ emailSigningSecret: secrets.emailSigningSecret,
1460
2348
  projectSecretActiveKeyVersion: secrets.projectSecretActiveKeyVersion,
1461
2349
  projectSecretKey: secrets.projectSecretKey,
1462
2350
  projectSecretKeysJson: secrets.projectSecretKeysJson,
@@ -1499,6 +2387,22 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1499
2387
  const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
1500
2388
  const runtimeDirectory = resolvedRuntime.directory;
1501
2389
  const runtime = resolvedRuntime.manifest;
2390
+ const hasEmailGatewayArtifact = runtime.artifacts.emailGateway !== void 0;
2391
+ if (runtime.emailProtocolVersion !== 1 && plan.deployment.capabilities.email) throw new Error("platform: This runtime cannot serve enabled platform email without email protocol version 1.");
2392
+ 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.");
2393
+ const workerKeys = hasEmailGatewayArtifact ? [
2394
+ "api",
2395
+ "proxy",
2396
+ "emailGateway",
2397
+ "tail",
2398
+ "dispatch"
2399
+ ] : [
2400
+ "api",
2401
+ "proxy",
2402
+ "tail",
2403
+ "dispatch"
2404
+ ];
2405
+ 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
2406
  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
2407
  verifyRuntimeArtifacts(runtimeDirectory, runtime);
1504
2408
  const runtimeProvenance = {
@@ -1512,11 +2416,14 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1512
2416
  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
2417
  const wasDisabled = existing?.status === "disabled";
1514
2418
  const preserveDisabled = existing?.desiredState === "disabled";
2419
+ const shouldBootstrapSharedEmail = !preserveDisabled && plan.deployment.capabilities.email && runtime.emailProtocolVersion === 1 && !!plan.deployment.emailSharedZoneId;
1515
2420
  const previousPublicEndpoints = stablePublicEndpointState(existing);
2421
+ const authenticationProtocolTransition = hadPriorWorkers && existing?.deployment.authentication.configurable !== true && runtime.authenticationProtocolVersion === 1;
1516
2422
  let workerCutoverGeneration = 0;
1517
2423
  let workerRollbacks = [];
1518
2424
  let workerDeploymentCommitted = false;
1519
2425
  let workerTrafficGuarded = false;
2426
+ let authenticationEnforcementActive = false;
1520
2427
  let manifest = existing;
1521
2428
  if (!manifest) {
1522
2429
  const createdAt = now();
@@ -1548,7 +2455,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1548
2455
  manifest = setInstallationDesiredState(manifest, "disabled", now());
1549
2456
  writeInstallationManifest(manifest);
1550
2457
  }
1551
- assertRequiredPlatformSecrets(plan, manifest, secrets);
2458
+ assertRequiredPlatformSecrets(plan, manifest, secrets, !!dependencies.prepareAuthentication);
1552
2459
  const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
1553
2460
  writePendingInstallationSecrets(manifest, {
1554
2461
  ...pendingSecrets,
@@ -1581,22 +2488,83 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1581
2488
  assertPlatformWorkerSecretUpdateAuthority({
1582
2489
  explicitUpdates: explicitWorkerSecretUpdates,
1583
2490
  localIntent: secrets.pendingWorkerSecretUpdateIntent,
1584
- manifest
2491
+ manifest,
2492
+ requiresEmailSigningSecret: manifest.deployment.capabilities.email || plan.deployment.capabilities.email
1585
2493
  });
1586
2494
  } catch (error) {
1587
2495
  await releaseDatabaseLock();
1588
2496
  throw error;
1589
2497
  }
1590
2498
  if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
1591
- const workerDeploymentSecrets = {
2499
+ let workerDeploymentSecrets = {
1592
2500
  ...secrets,
1593
2501
  workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
1594
2502
  };
1595
2503
  try {
1596
2504
  assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
1597
2505
  if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
2506
+ const liveAuthentication = await releaseDatabaseLock.readAuthenticationState?.();
2507
+ if (liveAuthentication) {
2508
+ authenticationEnforcementActive = liveAuthentication.sessionsRequired;
2509
+ if (liveAuthentication.sessionsRequired && runtime.authenticationProtocolVersion !== 1) throw new Error("platform: This runtime cannot enforce the active authentication configuration. Use an authentication-compatible runtime.");
2510
+ const deployment = mergePlatformLiveAuthenticationDeployment(plan.deployment, liveAuthentication);
2511
+ plan = {
2512
+ ...plan,
2513
+ deployment
2514
+ };
2515
+ if (!authenticationProtocolTransition) manifest = {
2516
+ ...manifest,
2517
+ deployment
2518
+ };
2519
+ }
2520
+ if (!liveAuthentication && manifest.deployment.authentication.configurable && !plan.deployment.authentication.configurable) {
2521
+ const authentication = manifest.deployment.authentication;
2522
+ const providers = authentication.providers;
2523
+ plan = {
2524
+ ...plan,
2525
+ deployment: createPlatformDeploymentManifest({
2526
+ ...plan.deployment,
2527
+ authentication,
2528
+ capabilities: {
2529
+ ...plan.deployment.capabilities,
2530
+ githubOAuth: providers.includes("github"),
2531
+ googleOAuth: providers.includes("google")
2532
+ }
2533
+ })
2534
+ };
2535
+ }
2536
+ if (authenticationProtocolTransition && !plan.deployment.authentication.configurable) {
2537
+ const providers = plan.deployment.authentication.providers;
2538
+ const deployment = createPlatformDeploymentManifest({
2539
+ ...plan.deployment,
2540
+ authentication: {
2541
+ providers,
2542
+ configurable: true
2543
+ },
2544
+ capabilities: {
2545
+ ...plan.deployment.capabilities,
2546
+ githubOAuth: providers.includes("github"),
2547
+ googleOAuth: providers.includes("google")
2548
+ }
2549
+ });
2550
+ plan = {
2551
+ ...plan,
2552
+ deployment
2553
+ };
2554
+ }
2555
+ const activateAccessCredentials = () => {
2556
+ if (!secrets.accessCredentialsJson) return;
2557
+ const credentials = JSON.parse(secrets.accessCredentialsJson);
2558
+ if (!credentials || typeof credentials !== "object" || Array.isArray(credentials)) throw new Error("platform: Invalid saved Access credentials.");
2559
+ const origins = [plan.apiUrl, plan.proxyUrl];
2560
+ activatePlatformAccessCredentials(JSON.stringify(Object.fromEntries(Object.entries(credentials).filter(([origin]) => origins.includes(origin)))), origins);
2561
+ };
2562
+ activateAccessCredentials();
2563
+ manifest = checkpointAuthenticationResources(manifest, secrets, now());
2564
+ writeInstallationManifest(manifest);
2565
+ await releaseDatabaseLock.checkpoint?.(manifest);
1598
2566
  dependencies.onProgress?.("Checking and creating Cloudflare resources");
1599
- if (JSON.stringify(manifest.deployment) !== JSON.stringify(plan.deployment)) {
2567
+ if (!authenticationProtocolTransition && JSON.stringify(manifest.deployment) !== JSON.stringify(plan.deployment)) {
1600
2568
  manifest = {
1601
2569
  ...manifest,
1602
2570
  deployment: plan.deployment,
@@ -1610,6 +2578,61 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1610
2578
  manifest = transitionInstallation(manifest, "provisioning", now());
1611
2579
  writeInstallationManifest(manifest);
1612
2580
  }
2581
+ if (dependencies.prepareAuthentication) {
2582
+ dependencies.onProgress?.("Preparing platform authentication");
2583
+ try {
2584
+ secrets = {
2585
+ ...secrets,
2586
+ ...await dependencies.prepareAuthentication(manifest, secrets)
2587
+ };
2588
+ } catch (error) {
2589
+ const latest = readPendingInstallationSecrets(manifest) ?? {};
2590
+ secrets = {
2591
+ ...secrets,
2592
+ authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
2593
+ authenticationSetupJson: latest.authenticationSetupJson || secrets.authenticationSetupJson,
2594
+ authSetupToken: latest.authSetupToken || secrets.authSetupToken,
2595
+ authBootstrapCode: latest.authBootstrapCode || secrets.authBootstrapCode,
2596
+ accessProvisioningJson: latest.accessProvisioningJson || secrets.accessProvisioningJson,
2597
+ accessCredentialsJson: latest.accessCredentialsJson || secrets.accessCredentialsJson
2598
+ };
2599
+ manifest = checkpointAuthenticationResources(manifest, secrets, now());
2600
+ writeInstallationManifest(manifest);
2601
+ await releaseDatabaseLock.checkpoint?.(manifest);
2602
+ throw error;
2603
+ }
2604
+ const latest = readPendingInstallationSecrets(manifest) ?? {};
2605
+ secrets = {
2606
+ ...secrets,
2607
+ authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name)))
2608
+ };
2609
+ manifest = checkpointAuthenticationResources(manifest, secrets, now());
2610
+ writeInstallationManifest(manifest);
2611
+ await releaseDatabaseLock.checkpoint?.(manifest);
2612
+ activateAccessCredentials();
2613
+ assertRequiredPlatformSecrets(plan, manifest, secrets);
2614
+ 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.");
2615
+ workerDeploymentSecrets = {
2616
+ ...secrets,
2617
+ workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
2618
+ };
2619
+ writePendingInstallationSecrets(manifest, {
2620
+ ...secretCheckpoint(secrets, workerSecretUpdateIntent),
2621
+ databaseClaimState: "claimed"
2622
+ });
2623
+ }
2624
+ if (plan.deployment.authentication.accessProtection) {
2625
+ const protection = liveAuthentication ? liveAuthentication.protection : secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(secrets.authenticationSetupJson)).protection : null;
2626
+ if (!protection) throw new Error("platform: The active Access protection configuration is unavailable. Restore its database and encrypted setup checkpoint before maintenance.");
2627
+ dependencies.onProgress?.("Verifying current Access policies and protected origins");
2628
+ await (dependencies.verifyAccessProtection ?? verifyInstalledPlatformAccessProtection)({
2629
+ client: dependencies.client,
2630
+ manifest,
2631
+ plan,
2632
+ protection,
2633
+ checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent)
2634
+ });
2635
+ }
1613
2636
  const routingAction = plan.actions.find((action) => action.key === "routing-kv");
1614
2637
  if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
1615
2638
  if (routingAction.action === "reuse") {
@@ -1692,6 +2715,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1692
2715
  await writeR2OwnershipMarkerIfNeeded(dependencies.client, plan, manifest, action);
1693
2716
  await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1694
2717
  }
2718
+ dependencies.onProgress?.("Verifying uncached authentication database reads");
2719
+ const authenticationDatabase = manifest.resources.find((resource) => resource.key === "hyperdrive");
2720
+ if (!authenticationDatabase) throw new Error("platform: The authentication database resource is missing.");
2721
+ await verifyPlatformAuthenticationDatabase(dependencies.client, plan.account.id, authenticationDatabase, secrets.databaseUrl);
1695
2722
  dependencies.onProgress?.("Checking the platform database schema");
1696
2723
  const rollbackDatabase = dependencies.databaseRollbackSource;
1697
2724
  const migrationStep = `database-schema:${runtime.database.schemaDigest}`;
@@ -1771,17 +2798,24 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1771
2798
  manifest = recordInstallationStep(manifest, {
1772
2799
  completedAt: now(),
1773
2800
  key: bootstrapStep,
1774
- resultHash: hash(secrets.bootstrapGitHubId)
2801
+ resultHash: hash(secrets.authenticationSetupJson ?? secrets.bootstrapGitHubId)
1775
2802
  }, now());
1776
2803
  writeInstallationManifest(manifest);
1777
2804
  }
1778
2805
  const entrypoints = Object.fromEntries(Object.entries(runtime.artifacts).map(([key, artifact]) => [key, join(runtimeDirectory, artifact.path)]));
1779
- const interruptedCutover = interruptedWorkerCutover(manifest);
2806
+ let interruptedCutover = interruptedWorkerCutover(manifest);
2807
+ if (interruptedCutover && authenticationProtocolTransition && authenticationEnforcementActive) {
2808
+ manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "committed", now());
2809
+ manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, false, now());
2810
+ writeInstallationManifest(manifest);
2811
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2812
+ interruptedCutover = null;
2813
+ }
1780
2814
  const recoveringUnsafeTraffic = (interruptedCutover?.unsafeTraffic.size ?? 0) > 0;
1781
- const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create");
2815
+ const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
1782
2816
  const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
1783
2817
  const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
1784
- const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || requiresSandboxDrain;
2818
+ const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain;
1785
2819
  if (requiresTrafficGuard) {
1786
2820
  await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
1787
2821
  workerTrafficGuarded = true;
@@ -1797,6 +2831,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1797
2831
  await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1798
2832
  }
1799
2833
  const configInput = {
2834
+ authenticationDatabaseUncached: true,
1800
2835
  accountId: plan.account.id,
1801
2836
  apiUrl: plan.apiUrl,
1802
2837
  deployment: plan.deployment,
@@ -1805,6 +2840,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1805
2840
  ownershipNonce: manifest.ownershipNonce,
1806
2841
  platformVersion: version,
1807
2842
  proxyUrl: plan.proxyUrl,
2843
+ emailGatewayUrl: `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev`,
1808
2844
  resources: {
1809
2845
  hyperdrive: requireId(ids, "hyperdrive"),
1810
2846
  isrCacheKv: requireId(ids, "isr-cache-kv"),
@@ -1831,7 +2867,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1831
2867
  trafficState: previousPublicEndpoints ? "enabled" : "staging"
1832
2868
  });
1833
2869
  const managedRoutes = platformManagedWorkerTraffic(plan.deployment).routes;
1834
- let workerActions = plan.actions.filter((action) => action.kind === "worker");
2870
+ let workerActions = plan.actions.filter((action) => action.kind === "worker" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
1835
2871
  if (interruptedCutover) {
1836
2872
  for (const key of [...interruptedCutover.staged.keys()].reverse()) {
1837
2873
  const snapshot = interruptedCutover.staged.get(key);
@@ -1866,7 +2902,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1866
2902
  previousTriggers: mergePreservedTriggerRoutes(withoutPlatformCustomDomainTriggers(snapshot.previousTriggers), preservedRoutes),
1867
2903
  previousVersionId: snapshot.previousVersionId,
1868
2904
  preserveCustomDomains: interruptedCutover.unsafeTraffic.has(key),
1869
- inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, `${key}-worker`, snapshot.workerName),
2905
+ inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`, snapshot.workerName),
1870
2906
  removeCustomDomains: async () => {
1871
2907
  if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
1872
2908
  await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(manifest, /* @__PURE__ */ new Set([snapshot.workerName])));
@@ -1892,7 +2928,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1892
2928
  await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1893
2929
  if (retainedNewWorker) {
1894
2930
  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));
2931
+ workerActions = validateReplannedWorkerActions(plan, manifest, await dependencies.replanWorkers(manifest)).filter((action) => hasEmailGatewayArtifact || action.key !== "email-gateway-worker");
1896
2932
  }
1897
2933
  }
1898
2934
  workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
@@ -1906,14 +2942,9 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1906
2942
  };
1907
2943
  writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
1908
2944
  }
1909
- for (const key of [
1910
- "api",
1911
- "proxy",
1912
- "tail",
1913
- "dispatch"
1914
- ]) {
2945
+ for (const key of workerKeys) {
1915
2946
  dependencies.onProgress?.(`Deploying the ${key} service`);
1916
- const action = workerActions.find((entry) => entry.key === `${key}-worker`);
2947
+ const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
1917
2948
  const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
1918
2949
  manifest = recordProvisionIntent(manifest, action, now());
1919
2950
  writeInstallationManifest(manifest);
@@ -1982,6 +3013,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
1982
3013
  if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
1983
3014
  manifest = {
1984
3015
  ...manifest,
3016
+ deployment: plan.deployment,
1985
3017
  platformVersion: version,
1986
3018
  runtime: runtimeProvenance,
1987
3019
  updatedAt: now(),
@@ -2000,17 +3032,63 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2000
3032
  zoneNameServers: zone?.nameServers ?? []
2001
3033
  };
2002
3034
  }
3035
+ let authenticationCutoverCommitted = false;
3036
+ if (authenticationProtocolTransition && requiresStagedCommit) {
3037
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
3038
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
3039
+ writeInstallationManifest(manifest);
3040
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
3041
+ workerRollbacks = [];
3042
+ authenticationCutoverCommitted = true;
3043
+ }
3044
+ let authenticationStep = secrets.authenticationSetupJson ? "authentication-initialized:v1:" + hash(secrets.authenticationSetupJson) : "";
3045
+ let setupPending = false;
2003
3046
  manifest = transitionInstallation(manifest, "verifying", now());
2004
3047
  writeInstallationManifest(manifest);
3048
+ if (plan.deployment.authentication.configurable && authenticationStep) {
3049
+ if (!secrets.authenticationSetupJson || !secrets.authSetupToken) throw new Error("platform: Restore the encrypted authentication setup checkpoint before resuming.");
3050
+ dependencies.onProgress?.("Initializing platform authentication");
3051
+ if ((await resumeInstalledPlatformAuthentication({
3052
+ apiUrl: plan.apiUrl,
3053
+ installationId: manifest.id,
3054
+ setupToken: secrets.authSetupToken,
3055
+ setupJson: secrets.authenticationSetupJson,
3056
+ checkpoint: (replacement) => {
3057
+ secrets = {
3058
+ ...secrets,
3059
+ ...replacement
3060
+ };
3061
+ authenticationStep = "authentication-initialized:v1:" + hash(replacement.authenticationSetupJson);
3062
+ writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
3063
+ }
3064
+ }, dependencies.initializeAuthentication ?? initializeInstalledPlatformAuthentication)).setupPending) {
3065
+ setupPending = true;
3066
+ dependencies.onProgress?.(`Administrator setup pending: ${plan.apiUrl}/setup`);
3067
+ }
3068
+ manifest = recordInstallationStep(manifest, {
3069
+ key: authenticationStep,
3070
+ completedAt: now(),
3071
+ resultHash: hash(secrets.authenticationSetupJson)
3072
+ }, now());
3073
+ writeInstallationManifest(manifest);
3074
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
3075
+ }
2005
3076
  dependencies.onProgress?.("Verifying platform health");
3077
+ const emailGatewayUrl = hasEmailGatewayArtifact ? `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev` : void 0;
3078
+ if (plan.deployment.authentication.accessProtection) {
3079
+ ensurePlatformAccessToken(plan.apiUrl, false, true);
3080
+ ensurePlatformAccessToken(plan.proxyUrl, false, true);
3081
+ }
2006
3082
  const healthCheckContext = {
2007
3083
  accountName: plan.account.name,
2008
3084
  workers: [
2009
3085
  plan.deployment.resources.apiWorker,
2010
3086
  plan.deployment.resources.proxyWorker,
3087
+ ...hasEmailGatewayArtifact ? [plan.deployment.resources.emailGatewayWorker] : [],
2011
3088
  plan.deployment.resources.dispatchWorker
2012
3089
  ],
2013
- resumeCommand: dependencies.healthCheckResumeCommand
3090
+ resumeCommand: dependencies.healthCheckResumeCommand,
3091
+ ...emailGatewayUrl ? { emailGatewayUrl } : {}
2014
3092
  };
2015
3093
  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
3094
  if (requiresSandboxDrain) {
@@ -2039,11 +3117,13 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2039
3117
  await writeSandboxDrainMirror(completedDrain);
2040
3118
  }
2041
3119
  if (requiresStagedCommit) {
2042
- manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
2043
- manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
2044
- writeInstallationManifest(manifest);
2045
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2046
- workerRollbacks = [];
3120
+ if (!authenticationCutoverCommitted) {
3121
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
3122
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
3123
+ writeInstallationManifest(manifest);
3124
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
3125
+ workerRollbacks = [];
3126
+ }
2047
3127
  workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
2048
3128
  if (workerSecretUpdateIntent) {
2049
3129
  manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "pending", workerSecretUpdateIntent.id, now());
@@ -2055,14 +3135,9 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2055
3135
  };
2056
3136
  writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
2057
3137
  }
2058
- for (const key of [
2059
- "api",
2060
- "proxy",
2061
- "tail",
2062
- "dispatch"
2063
- ]) {
3138
+ for (const key of workerKeys) {
2064
3139
  dependencies.onProgress?.(`Enabling the ${key} service`);
2065
- const action = workerActions.find((entry) => entry.key === `${key}-worker`);
3140
+ const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
2066
3141
  const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
2067
3142
  const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
2068
3143
  const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
@@ -2116,9 +3191,20 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2116
3191
  const verifyCurrentWorkerCutover = async () => {
2117
3192
  if (!requiresStagedCommit) return;
2118
3193
  if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(manifest, workerCutoverGeneration);
2119
- else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(manifest, workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler);
3194
+ else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(manifest, workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler, workerKeys);
2120
3195
  };
2121
3196
  await verifyCurrentWorkerCutover();
3197
+ let emailOperationId;
3198
+ if (shouldBootstrapSharedEmail) {
3199
+ workerDeploymentCommitted = true;
3200
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
3201
+ if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
3202
+ writeInstallationManifest(manifest);
3203
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
3204
+ workerRollbacks = [];
3205
+ dependencies.onProgress?.("Preparing shared email routing");
3206
+ emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce));
3207
+ }
2122
3208
  if (dependencies.client.deleteKvValue) await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
2123
3209
  else if (wasDisabled || !previousPublicEndpoints) throw new Error("platform: Routing disablement removal is unavailable.");
2124
3210
  if (requiresStagedCommit) {
@@ -2132,6 +3218,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2132
3218
  if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
2133
3219
  manifest = {
2134
3220
  ...manifest,
3221
+ deployment: plan.deployment,
2135
3222
  platformVersion: version,
2136
3223
  runtime: runtimeProvenance,
2137
3224
  safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
@@ -2147,6 +3234,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2147
3234
  saveInstalledPlatformConnection(plan, manifest);
2148
3235
  return {
2149
3236
  manifest,
3237
+ ...emailOperationId ? { emailOperationId } : {},
3238
+ setupPending,
2150
3239
  waitingForDns: false,
2151
3240
  waitingForSandboxDrain: false,
2152
3241
  zoneNameServers: zone?.nameServers ?? []
@@ -2159,7 +3248,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2159
3248
  } catch (guardError) {
2160
3249
  failure = new Error(`platform: ${errorMessage(error)} The installation also failed to retain its routing safety gate: ${errorMessage(guardError)}`, { cause: error });
2161
3250
  }
2162
- if (!workerDeploymentCommitted && workerRollbacks.length > 0) {
3251
+ if (!workerDeploymentCommitted && workerRollbacks.length > 0 && !(authenticationEnforcementActive && authenticationProtocolTransition)) {
2163
3252
  const rollbackFailures = [];
2164
3253
  for (const rollback of workerRollbacks.reverse()) try {
2165
3254
  await rollback();
@@ -2173,7 +3262,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2173
3262
  manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, previousPublicEndpoints, now());
2174
3263
  writeInstallationManifest(manifest);
2175
3264
  }
2176
- }
3265
+ } else if (authenticationEnforcementActive && authenticationProtocolTransition) workerRollbacks = [];
2177
3266
  if (manifest.status !== "failed" && manifest.status !== "uninstalled") try {
2178
3267
  manifest = transitionInstallation(manifest, "failed", now());
2179
3268
  writeInstallationManifest(manifest);
@@ -2184,6 +3273,9 @@ async function installPlatform(plan, secrets, existing, dependencies) {
2184
3273
  await releaseDatabaseLock();
2185
3274
  }
2186
3275
  }
3276
+ function checkpointAuthenticationResources(manifest, secrets, updatedAt) {
3277
+ return checkpointPlatformAccessResources(manifest, secrets.accessProvisioningJson, updatedAt);
3278
+ }
2187
3279
  async function writeR2OwnershipMarkerIfNeeded(client, plan, manifest, action) {
2188
3280
  if (action.kind !== "r2-bucket") return;
2189
3281
  const bucket = manifest.resources.find((resource) => resource.key === action.key && resource.state === "ready");
@@ -2424,8 +3516,10 @@ function apiWorkerSecrets(secrets, accountId, zoneId) {
2424
3516
  CF_ZONE_ID: zoneId,
2425
3517
  GITHUB_CLIENT_ID: secrets.githubClientId,
2426
3518
  GITHUB_CLIENT_SECRET: secrets.githubClientSecret,
3519
+ ...secrets.emailEnabled ? { EMAIL_SIGNING_SECRET: secrets.emailSigningSecret } : {},
2427
3520
  JWT_SECRET: secrets.jwtSecret,
2428
3521
  PLATFORM_BOOTSTRAP_GITHUB_ID: secrets.bootstrapGitHubId,
3522
+ PLATFORM_AUTH_SETUP_TOKEN: secrets.authSetupToken ?? "",
2429
3523
  ...secrets.projectSecretKeysJson && secrets.projectSecretActiveKeyVersion ? {
2430
3524
  PROJECT_SECRET_ACTIVE_KEY_VERSION: secrets.projectSecretActiveKeyVersion,
2431
3525
  PROJECT_SECRET_KEYS_JSON: secrets.projectSecretKeysJson
@@ -2436,15 +3530,23 @@ function apiWorkerSecrets(secrets, accountId, zoneId) {
2436
3530
  }
2437
3531
  /** Existing Workers inherit live secrets, including complete encryption keyrings. */
2438
3532
  function platformWorkerSecretPayload(worker, secrets, accountId, zoneId, existingWorker) {
2439
- if (worker !== "api" && worker !== "proxy") return null;
2440
- if (!existingWorker) return worker === "api" ? apiWorkerSecrets(secrets, accountId, zoneId) : proxyWorkerSecrets(secrets, accountId);
3533
+ if (worker !== "api" && worker !== "proxy" && worker !== "emailGateway") return null;
3534
+ if (!existingWorker) {
3535
+ if (worker === "api") return apiWorkerSecrets(secrets, accountId, zoneId);
3536
+ if (worker === "emailGateway") {
3537
+ const payload = withoutEmptySecrets({ EMAIL_SIGNING_SECRET: secrets.emailEnabled ? secrets.emailSigningSecret : "" });
3538
+ return Object.keys(payload).length > 0 ? payload : null;
3539
+ }
3540
+ return proxyWorkerSecrets(secrets, accountId);
3541
+ }
2441
3542
  const allowed = worker === "api" ? [
2442
3543
  "CF_API_TOKEN",
3544
+ "EMAIL_SIGNING_SECRET",
2443
3545
  "GITHUB_CLIENT_ID",
2444
3546
  "GITHUB_CLIENT_SECRET",
2445
3547
  "R2_ACCESS_KEY_ID",
2446
3548
  "R2_SECRET_ACCESS_KEY"
2447
- ] : ["CF_API_TOKEN"];
3549
+ ] : worker === "proxy" ? ["CF_API_TOKEN", "EMAIL_SIGNING_SECRET"] : ["EMAIL_SIGNING_SECRET"];
2448
3550
  const updates = Object.fromEntries(Object.entries(secrets.workerSecretUpdates ?? {}).filter(([name, value]) => allowed.includes(name) && typeof value === "string" && value !== ""));
2449
3551
  return Object.keys(updates).length > 0 ? updates : null;
2450
3552
  }
@@ -2469,26 +3571,34 @@ function proxyWorkerSecrets(secrets, accountId) {
2469
3571
  return withoutEmptySecrets({
2470
3572
  CF_ACCOUNT_ID: accountId,
2471
3573
  CF_API_TOKEN: secrets.cloudflareApiToken,
3574
+ ...secrets.emailEnabled ? { EMAIL_SIGNING_SECRET: secrets.emailSigningSecret } : {},
2472
3575
  JWT_SECRET: secrets.jwtSecret
2473
3576
  });
2474
3577
  }
2475
3578
  function withoutEmptySecrets(secrets) {
2476
3579
  return Object.fromEntries(Object.entries(secrets).filter(([, value]) => value !== ""));
2477
3580
  }
2478
- function assertRequiredPlatformSecrets(plan, manifest, secrets) {
3581
+ function assertRequiredPlatformSecrets(plan, manifest, secrets, authenticationWillBePrepared = false) {
2479
3582
  const missing = /* @__PURE__ */ new Set();
2480
3583
  const requireSecret = (value, name) => {
2481
3584
  if (!value) missing.add(name);
2482
3585
  };
2483
- if (!manifest.steps.some((step) => step.key === "bootstrap-access:v1")) {
3586
+ if (plan.deployment.authentication.configurable && !manifest.steps.some((step) => step.key.startsWith("authentication-initialized:v1:"))) {
3587
+ if (!authenticationWillBePrepared) requireSecret(secrets.authenticationSetupJson ?? "", "authentication setup configuration");
3588
+ requireSecret(secrets.authSetupToken ?? "", "installation authentication setup authority");
3589
+ requireSecret(secrets.authBootstrapCode ?? "", "administrator setup code");
3590
+ } else if (!plan.deployment.authentication.configurable && !manifest.steps.some((step) => step.key === "bootstrap-access:v1")) {
2484
3591
  requireSecret(secrets.bootstrapGitHubId, "VOID_PLATFORM_ADMIN_GITHUB_ID");
2485
3592
  requireSecret(secrets.bootstrapGitHubLogin, "VOID_PLATFORM_ADMIN_GITHUB_LOGIN");
2486
3593
  }
2487
3594
  if (plan.actions.some((action) => action.key === "api-worker" && action.kind === "worker" && action.action === "create")) {
2488
3595
  requireSecret(secrets.cloudflareApiToken, "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN");
2489
- requireSecret(secrets.githubClientId, "VOID_PLATFORM_GITHUB_CLIENT_ID");
2490
- requireSecret(secrets.githubClientSecret, "VOID_PLATFORM_GITHUB_CLIENT_SECRET");
3596
+ if (!plan.deployment.authentication.configurable) {
3597
+ requireSecret(secrets.githubClientId, "VOID_PLATFORM_GITHUB_CLIENT_ID");
3598
+ requireSecret(secrets.githubClientSecret, "VOID_PLATFORM_GITHUB_CLIENT_SECRET");
3599
+ }
2491
3600
  requireSecret(secrets.jwtSecret, "VOID_PLATFORM_JWT_SECRET");
3601
+ if (plan.deployment.capabilities.email) requireSecret(secrets.emailSigningSecret, "VOID_PLATFORM_EMAIL_SIGNING_SECRET");
2492
3602
  requireSecret(secrets.projectSecretKeysJson, "VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON (or VOID_PLATFORM_PROJECT_SECRET_KEY for v1)");
2493
3603
  requireSecret(secrets.projectSecretActiveKeyVersion, "VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION (or VOID_PLATFORM_PROJECT_SECRET_KEY for v1)");
2494
3604
  requireSecret(secrets.r2AccessKeyId, "VOID_PLATFORM_R2_ACCESS_KEY_ID");
@@ -2497,6 +3607,10 @@ function assertRequiredPlatformSecrets(plan, manifest, secrets) {
2497
3607
  if (plan.actions.some((action) => action.key === "proxy-worker" && action.kind === "worker" && action.action === "create")) {
2498
3608
  requireSecret(secrets.cloudflareApiToken, "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN");
2499
3609
  requireSecret(secrets.jwtSecret, "VOID_PLATFORM_JWT_SECRET");
3610
+ if (plan.deployment.capabilities.email) requireSecret(secrets.emailSigningSecret, "VOID_PLATFORM_EMAIL_SIGNING_SECRET");
3611
+ }
3612
+ if (plan.actions.some((action) => action.key === "email-gateway-worker" && action.kind === "worker" && action.action === "create")) {
3613
+ if (plan.deployment.capabilities.email) requireSecret(secrets.emailSigningSecret, "VOID_PLATFORM_EMAIL_SIGNING_SECRET");
2500
3614
  }
2501
3615
  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
3616
  }
@@ -2681,6 +3795,17 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
2681
3795
  await client.query("SELECT pg_advisory_unlock(hashtextextended($1, 0))", [lockIdentity]);
2682
3796
  };
2683
3797
  release.authoritativeManifest = authoritativeManifest;
3798
+ release.readAuthenticationState = async () => {
3799
+ if (!(await client.query("SELECT to_regclass('public.platform_auth_settings') IS NOT NULL AS present")).rows[0]?.present) return null;
3800
+ 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")]);
3801
+ const settings = result.rows[0];
3802
+ return settings ? {
3803
+ enabledProviders: enabledProviders.rows.map((row) => row.kind),
3804
+ sessionsRequired: settings.sessions_required,
3805
+ protectedByAccess: settings.protection !== null,
3806
+ protection: parsePlatformAccessProtection(settings.protection)
3807
+ } : null;
3808
+ };
2684
3809
  release.requiredSafetyProtocols = await readRequiredPlatformSafetyProtocols(client);
2685
3810
  release.checkpoint = async (manifest) => {
2686
3811
  if (released) throw new Error("platform: Cannot checkpoint lifecycle state after releasing its lock.");
@@ -3582,7 +4707,7 @@ function interruptedWorkerCutover(manifest) {
3582
4707
  generations.set(generation, entry);
3583
4708
  continue;
3584
4709
  }
3585
- const unsafeTraffic = /^worker-cutover:(\d+):unsafe-traffic:(api|proxy|tail|dispatch)$/.exec(step.key);
4710
+ const unsafeTraffic = /^worker-cutover:(\d+):unsafe-traffic:(api|proxy|emailGateway|tail|dispatch)$/.exec(step.key);
3586
4711
  if (unsafeTraffic) {
3587
4712
  const generation = Number(unsafeTraffic[1]);
3588
4713
  const entry = generations.get(generation) ?? {
@@ -3594,7 +4719,7 @@ function interruptedWorkerCutover(manifest) {
3594
4719
  generations.set(generation, entry);
3595
4720
  continue;
3596
4721
  }
3597
- const staged = /^worker-cutover:(\d+):staged:(api|proxy|tail|dispatch)$/.exec(step.key);
4722
+ const staged = /^worker-cutover:(\d+):staged:(api|proxy|emailGateway|tail|dispatch)$/.exec(step.key);
3598
4723
  if (!staged) continue;
3599
4724
  const generation = Number(staged[1]);
3600
4725
  const snapshot = parseWorkerCutoverSnapshot(step.resultHash);
@@ -3614,17 +4739,18 @@ function interruptedWorkerCutover(manifest) {
3614
4739
  unsafeTraffic: unresolved[1].unsafeTraffic
3615
4740
  } : null;
3616
4741
  }
3617
- async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs, accountId, runner = runPlatformWrangler) {
4742
+ async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs, accountId, runner = runPlatformWrangler, workerKeys = [
4743
+ "api",
4744
+ "proxy",
4745
+ "emailGateway",
4746
+ "tail",
4747
+ "dispatch"
4748
+ ]) {
3618
4749
  const cutover = interruptedWorkerCutover(manifest);
3619
- if (cutover?.generation !== generation || cutover.staged.size !== 4) throw new Error(`platform: Worker cutover generation ${generation} is missing a complete immutable-version checkpoint.`);
4750
+ 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
4751
  const directory = mkdtempSync(join(tmpdir(), "void-platform-cutover-verify-"));
3621
4752
  try {
3622
- for (const key of [
3623
- "api",
3624
- "proxy",
3625
- "tail",
3626
- "dispatch"
3627
- ]) {
4753
+ for (const key of workerKeys) {
3628
4754
  const snapshot = cutover.staged.get(key);
3629
4755
  if (!snapshot) throw new Error(`platform: Worker cutover generation ${generation} is missing its ${key} checkpoint.`);
3630
4756
  const config = configs[key];
@@ -3713,11 +4839,12 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
3713
4839
  signal: AbortSignal.timeout(3e4)
3714
4840
  });
3715
4841
  const dispatchUrl = dispatchOrigin ?? `https://test.${applicationBaseDomain}`;
3716
- const [health, discovery, proxy, dispatch] = await Promise.all([
4842
+ const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
3717
4843
  request(`${apiUrl}/health`),
3718
4844
  request(`${apiUrl}/.well-known/void-platform`),
3719
4845
  request(`${proxyUrl}/health`),
3720
- request(`${dispatchUrl}/health`)
4846
+ request(`${dispatchUrl}/health`),
4847
+ context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
3721
4848
  ]);
3722
4849
  if (!health.ok) throw await platformProbeFailure("Health check", apiUrl, health, context);
3723
4850
  if (!discovery.ok) throw await platformProbeFailure("Discovery check", apiUrl, discovery, context);
@@ -3727,6 +4854,11 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
3727
4854
  if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
3728
4855
  if (!proxy.ok) throw await platformProbeFailure("Proxy health check", proxyUrl, proxy, context);
3729
4856
  if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", dispatchUrl, dispatch, context);
4857
+ if (emailGateway) {
4858
+ if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", context.emailGatewayUrl, emailGateway, context);
4859
+ const identity = await emailGateway.json();
4860
+ 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.");
4861
+ }
3730
4862
  }
3731
4863
  async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fetch) {
3732
4864
  const response = await fetcher(`${apiUrl}/internal/platform/sandbox-drain`, {
@@ -3734,7 +4866,8 @@ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fe
3734
4866
  "X-Void-Platform-Probe": healthProbeToken,
3735
4867
  ...cfAccessHeaders(process.env, apiUrl)
3736
4868
  },
3737
- signal: AbortSignal.timeout(3e4)
4869
+ signal: AbortSignal.timeout(3e4),
4870
+ redirect: "manual"
3738
4871
  });
3739
4872
  if (!response.ok) throw new Error(`platform: Sandbox drain probe failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
3740
4873
  let value;
@@ -3750,10 +4883,268 @@ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fe
3750
4883
  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
4884
  return status.state === "complete";
3752
4885
  }
4886
+ async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
4887
+ const response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
4888
+ method: "POST",
4889
+ headers: {
4890
+ "X-Void-Platform-Probe": healthProbeToken,
4891
+ ...cfAccessHeaders(process.env, apiUrl)
4892
+ },
4893
+ signal: AbortSignal.timeout(3e4),
4894
+ redirect: "manual"
4895
+ });
4896
+ let value;
4897
+ try {
4898
+ value = await response.json();
4899
+ } catch {
4900
+ throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
4901
+ }
4902
+ const status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
4903
+ if (!response.ok || status?.version !== 1 || status.state !== "ready" || typeof status.operationId !== "string" || status.operationId.length === 0) {
4904
+ 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}.` : "";
4905
+ throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
4906
+ }
4907
+ return status.operationId;
4908
+ }
3753
4909
  function createPlatformHealthProbeToken(ownershipNonce) {
3754
4910
  return createHash("sha256").update("void-platform-health-probe:v1\0").update(ownershipNonce).digest("base64url");
3755
4911
  }
3756
4912
  //#endregion
4913
+ //#region src/cli/platform-install-auth.ts
4914
+ function answer(value) {
4915
+ if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
4916
+ return value;
4917
+ }
4918
+ async function selectInstallationAuthentication(file, recorded) {
4919
+ if (file) {
4920
+ const input = JSON.parse(await readFile(file, "utf8"));
4921
+ 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.");
4922
+ const entries = input.connections;
4923
+ 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.");
4924
+ const selected = input;
4925
+ if (selected.cloudflareAccess !== void 0) selected.cloudflareAccess = parsePlatformAccessSetupSpec(selected.cloudflareAccess);
4926
+ const authentication = {
4927
+ providers: [...new Set(entries.map((entry) => entry.configuration.kind))],
4928
+ configurable: true,
4929
+ ...selected.protection ? { accessProtection: true } : {}
4930
+ };
4931
+ if (recorded && JSON.stringify(authentication) !== JSON.stringify(recorded)) throw new Error("platform: Resume with the recorded login methods and protection settings.");
4932
+ return {
4933
+ authentication,
4934
+ file: selected
4935
+ };
4936
+ }
4937
+ if (recorded) return { authentication: recorded };
4938
+ if (!process.stdin.isTTY) return { authentication: { providers: ["github"] } };
4939
+ const providers = answer(await multiselect({
4940
+ message: "Platform login methods:",
4941
+ initialValues: ["github"],
4942
+ required: true,
4943
+ options: [
4944
+ {
4945
+ value: "github",
4946
+ label: "GitHub"
4947
+ },
4948
+ {
4949
+ value: "google",
4950
+ label: "Google"
4951
+ },
4952
+ {
4953
+ value: "oidc",
4954
+ label: "Generic OIDC"
4955
+ },
4956
+ {
4957
+ value: "cloudflare-access",
4958
+ label: "Cloudflare Access login"
4959
+ }
4960
+ ]
4961
+ }));
4962
+ const protection = answer(await confirm({
4963
+ message: "Also protect the platform UI and APIs with Cloudflare Access?",
4964
+ initialValue: false
4965
+ }));
4966
+ return { authentication: {
4967
+ providers,
4968
+ ...protection || providers.length !== 1 || providers[0] !== "github" ? { configurable: true } : {},
4969
+ ...protection ? { accessProtection: true } : {}
4970
+ } };
4971
+ }
4972
+ async function collectInstallationAuthentication(selection, plan, checkpoint, save, access) {
4973
+ if (!selection.authentication.configurable) return;
4974
+ activatePlatformAccessCredentials(checkpoint.accessCredentialsJson, [plan.apiUrl, plan.proxyUrl]);
4975
+ if (checkpoint.authenticationSetupJson && !selection.file) {
4976
+ parsePlatformAuthenticationSetup(JSON.parse(checkpoint.authenticationSetupJson));
4977
+ return {
4978
+ authenticationSetupJson: checkpoint.authenticationSetupJson,
4979
+ authSetupToken: checkpoint.authSetupToken ?? "",
4980
+ authBootstrapCode: checkpoint.authBootstrapCode ?? ""
4981
+ };
4982
+ }
4983
+ const prompt = async (message, initialValue = "", masked = false) => {
4984
+ if (!process.stdin.isTTY) throw new Error(`platform: ${message} Supply it through --auth-config and environment references.`);
4985
+ return masked ? answer(await password({ message })) : answer(await text$1({
4986
+ message,
4987
+ initialValue
4988
+ }));
4989
+ };
4990
+ const entries = selection.file?.connections ?? selection.authentication.providers.map((kind) => ({
4991
+ configuration: {
4992
+ kind,
4993
+ id: kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind,
4994
+ label: kind === "github" ? "GitHub" : kind === "google" ? "Google" : kind === "oidc" ? "Company SSO" : "Cloudflare Access"
4995
+ },
4996
+ clientSecretEnv: void 0
4997
+ }));
4998
+ const connections = [];
4999
+ const accessEntry = entries.find((entry) => entry.configuration.kind === "cloudflare-access");
5000
+ const prepared = access && (selection.authentication.accessProtection || accessEntry && (!selection.file || selection.file.cloudflareAccess)) ? await preparePlatformAccessSetup({
5001
+ plan,
5002
+ ownershipNonce: access.ownershipNonce,
5003
+ protection: !!selection.authentication.accessProtection,
5004
+ ...accessEntry ? { login: {
5005
+ id: String(accessEntry.configuration.id ?? "access"),
5006
+ label: String(accessEntry.configuration.label ?? "Cloudflare Access"),
5007
+ ...Array.isArray(accessEntry.configuration.requiredGroups) ? { requiredGroups: accessEntry.configuration.requiredGroups } : {}
5008
+ } } : {},
5009
+ spec: selection.file?.cloudflareAccess,
5010
+ client: access.client,
5011
+ checkpoint,
5012
+ save,
5013
+ yes: access.yes
5014
+ }) : void 0;
5015
+ console.log(`Login callback URL: ${plan.apiUrl}/auth/callback`);
5016
+ for (const entry of entries) {
5017
+ const config = { ...entry.configuration };
5018
+ const kind = config.kind;
5019
+ if (!config.id) config.id = kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind;
5020
+ if (prepared?.login && prepared.login.configuration.id === config.id) {
5021
+ const connection = prepared.login;
5022
+ save({ [`authenticationConnection:${connection.configuration.id}`]: JSON.stringify(connection) });
5023
+ connections.push(connection);
5024
+ continue;
5025
+ }
5026
+ const partialKey = `authenticationConnection:${String(config.id)}`;
5027
+ const stored = checkpoint[partialKey] ? JSON.parse(checkpoint[partialKey]) : void 0;
5028
+ Object.assign(config, {
5029
+ ...stored?.configuration,
5030
+ ...config
5031
+ });
5032
+ if (!config.label) config.label = kind;
5033
+ if ((kind === "oidc" || kind === "cloudflare-access") && !config.issuer) config.issuer = await prompt(`${config.label}: issuer URL:`);
5034
+ if (!config.clientId) config.clientId = await prompt(`${config.label}: client ID:`);
5035
+ 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);
5036
+ const parsed = parsePlatformLoginConfiguration(config);
5037
+ let secret;
5038
+ if (entry.clientSecretEnv) {
5039
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.clientSecretEnv)) throw new Error("platform: Invalid clientSecretEnv reference.");
5040
+ secret = process.env[entry.clientSecretEnv];
5041
+ if (!secret) throw new Error(`platform: ${entry.clientSecretEnv} must contain the login client secret.`);
5042
+ } else if (stored?.clientSecret && stored.configuration?.clientId === parsed.clientId && stored.configuration?.issuer === parsed.issuer) secret = stored.clientSecret;
5043
+ else secret = await prompt(`${config.label}: client secret:`, "", true);
5044
+ const connection = {
5045
+ configuration: parsed,
5046
+ clientSecret: secret
5047
+ };
5048
+ save({ [partialKey]: JSON.stringify(connection) });
5049
+ connections.push(connection);
5050
+ }
5051
+ const administratorConnectionId = selection.file?.administratorConnectionId ?? (connections.length === 1 ? connections[0].configuration.id : process.stdin.isTTY ? answer(await select({
5052
+ message: "Login method for the first administrator:",
5053
+ options: connections.map((entry) => ({
5054
+ value: entry.configuration.id,
5055
+ label: entry.configuration.label
5056
+ }))
5057
+ })) : connections[0].configuration.id);
5058
+ let protection = prepared?.protection ?? selection.file?.protection ?? null;
5059
+ if (selection.authentication.accessProtection && !protection) {
5060
+ const teamDomain = await prompt("Access team origin (https://team.cloudflareaccess.com):");
5061
+ const apiAudience = await prompt(`Access application audience for ${plan.apiUrl}:`);
5062
+ const proxyAudience = await prompt(`Access application audience for ${plan.proxyUrl}:`, apiAudience);
5063
+ protection = {
5064
+ teamDomain,
5065
+ applications: [{
5066
+ origin: plan.apiUrl,
5067
+ audience: apiAudience
5068
+ }, {
5069
+ origin: plan.proxyUrl,
5070
+ audience: proxyAudience
5071
+ }]
5072
+ };
5073
+ }
5074
+ protection = parsePlatformAccessProtection(protection);
5075
+ let admissionInput = selection.file?.admission ?? (protection ? {
5076
+ mode: "company",
5077
+ access: true,
5078
+ connections: []
5079
+ } : { mode: "restricted" });
5080
+ if (!selection.file && !protection && process.stdin.isTTY) {
5081
+ const eligible = connections.filter((entry) => entry.configuration.kind !== "github" && (entry.configuration.kind !== "google" || entry.configuration.allowedDomains.length > 0));
5082
+ const mode = answer(await select({
5083
+ message: "Who can create an account?",
5084
+ initialValue: eligible.length ? "company" : "restricted",
5085
+ options: [
5086
+ {
5087
+ value: "restricted",
5088
+ label: "Invited or allowlisted users"
5089
+ },
5090
+ ...eligible.length ? [{
5091
+ value: "company",
5092
+ label: "Users approved by company login methods — automatic accounts"
5093
+ }] : [],
5094
+ {
5095
+ value: "public",
5096
+ label: "Anyone who completes an enabled login"
5097
+ }
5098
+ ]
5099
+ }));
5100
+ const ids = mode === "company" ? answer(await multiselect({
5101
+ message: "Company login methods that qualify new users:",
5102
+ required: true,
5103
+ initialValues: eligible.map((entry) => entry.configuration.id),
5104
+ options: eligible.map((entry) => ({
5105
+ value: entry.configuration.id,
5106
+ label: entry.configuration.label
5107
+ }))
5108
+ })) : [];
5109
+ admissionInput = mode === "company" ? {
5110
+ mode,
5111
+ connections: ids,
5112
+ access: false
5113
+ } : { mode };
5114
+ }
5115
+ const admission = parsePlatformAdmissionPolicy(admissionInput);
5116
+ let authBootstrapCode = checkpoint.authBootstrapCode || randomBytes(32).toString("hex");
5117
+ const authSetupToken = checkpoint.authSetupToken || randomBytes(32).toString("hex");
5118
+ let setup = parsePlatformAuthenticationSetup({
5119
+ connections,
5120
+ administratorConnectionId,
5121
+ admission,
5122
+ protection,
5123
+ bootstrapCredentialHash: createHash("sha256").update(authBootstrapCode).digest("hex")
5124
+ });
5125
+ if (selection.file && checkpoint.authenticationSetupJson) {
5126
+ const previous = parsePlatformAuthenticationSetup(JSON.parse(checkpoint.authenticationSetupJson));
5127
+ if (JSON.stringify({
5128
+ ...previous,
5129
+ resetPending: void 0
5130
+ }) !== JSON.stringify(setup)) {
5131
+ authBootstrapCode = randomBytes(32).toString("hex");
5132
+ setup = parsePlatformAuthenticationSetup({
5133
+ ...setup,
5134
+ resetPending: true,
5135
+ bootstrapCredentialHash: createHash("sha256").update(authBootstrapCode).digest("hex")
5136
+ });
5137
+ }
5138
+ }
5139
+ const result = {
5140
+ authenticationSetupJson: JSON.stringify(setup),
5141
+ authBootstrapCode,
5142
+ authSetupToken
5143
+ };
5144
+ save(result);
5145
+ return result;
5146
+ }
5147
+ //#endregion
3757
5148
  //#region src/cli/platform-setup-draft.ts
3758
5149
  const STARTED = "credential-setup:v1";
3759
5150
  const COMPLETED = "credential-setup-complete:v1";
@@ -3885,7 +5276,29 @@ function platformSetupPages(plan) {
3885
5276
  ...plan.deployment.capabilities.customDomains ? [{
3886
5277
  key: "ssl_and_certificates",
3887
5278
  type: "edit"
3888
- }] : []
5279
+ }] : [],
5280
+ ...plan.deployment.capabilities.email ? [
5281
+ {
5282
+ key: "zone",
5283
+ type: "read"
5284
+ },
5285
+ {
5286
+ key: "zone_settings",
5287
+ type: "edit"
5288
+ },
5289
+ {
5290
+ key: "email_routing_address",
5291
+ type: "edit"
5292
+ },
5293
+ {
5294
+ key: "email_routing_rule",
5295
+ type: "edit"
5296
+ },
5297
+ {
5298
+ key: "email_sending",
5299
+ type: "edit"
5300
+ }
5301
+ ] : []
3889
5302
  ];
3890
5303
  return {
3891
5304
  runtime: {
@@ -3901,6 +5314,7 @@ function platformSetupPages(plan) {
3901
5314
  "Read: Account Settings, Account Analytics, Workers Tail.",
3902
5315
  ...plan.deployment.application.zoneName ? ["Zone " + plan.deployment.application.zoneName + ": Cache Purge: Purge."] : [],
3903
5316
  ...plan.deployment.capabilities.customDomains ? ["Custom project domains also need SSL and Certificates: Edit on that zone."] : [],
5317
+ ...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
5318
  "If the form differs, match the permissions above. Create the token, then paste it here."
3905
5319
  ].join("\n")
3906
5320
  },
@@ -3933,8 +5347,9 @@ function formatPlatformSetupPlan(plan) {
3933
5347
  platformSetupLink(pages.r2),
3934
5348
  pages.r2.instructions,
3935
5349
  "",
3936
- platformSetupLink(pages.github),
3937
- pages.github.instructions
5350
+ ...plan.deployment.authentication.providers.includes("github") ? [platformSetupLink(pages.github), pages.github.instructions] : [],
5351
+ ...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.`),
5352
+ ...plan.deployment.authentication.accessProtection ? ["Cloudflare Access protection: configure API and proxy origins and admitted installer/CI credentials."] : []
3938
5353
  ].join("\n");
3939
5354
  }
3940
5355
  function openPlatformSetupPage(page, dependencies = {}) {
@@ -3992,14 +5407,14 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
3992
5407
  }
3993
5408
  async function runPlatformLifecycleCommand(args) {
3994
5409
  if (args.subcommand === "domain") {
3995
- const { runPlatformDomainCommand } = await import("./platform-domain-BszUfiS8.mjs");
5410
+ const { runPlatformDomainCommand } = await import("./platform-domain-C74PULqV.mjs");
3996
5411
  await runPlatformDomainCommand(args);
3997
5412
  return;
3998
5413
  }
3999
5414
  if (args.subcommand === "discover") {
4000
5415
  const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
4001
5416
  const accountId = await selectAccount(await readAccounts(client), args.account);
4002
- const { recoverPlatformInstallations } = await import("./platform-recovery-pHHv4ZSg.mjs");
5417
+ const { recoverPlatformInstallations } = await import("./platform-recovery-ewqLefp1.mjs");
4003
5418
  const recovered = await recoverPlatformInstallations(client, accountId, void 0, args.selector);
4004
5419
  if (recovered.length === 0) {
4005
5420
  log.info(`No recoverable Void platform installations were found in ${accountId}.`);
@@ -4009,7 +5424,7 @@ async function runPlatformLifecycleCommand(args) {
4009
5424
  return;
4010
5425
  }
4011
5426
  if (args.subcommand !== "install") {
4012
- const { runInstalledPlatformCommand } = await import("./platform-management-W30iCaTL.mjs");
5427
+ const { runInstalledPlatformCommand } = await import("./platform-management-COogu_Se.mjs");
4013
5428
  await runInstalledPlatformCommand(args);
4014
5429
  return;
4015
5430
  }
@@ -4159,7 +5574,7 @@ async function runPlatformLifecycleCommand(args) {
4159
5574
  progress?.stop("Read the saved installation.");
4160
5575
  progress = void 0;
4161
5576
  if (authoritative) {
4162
- const { selectAuthoritativePlatformManifest } = await import("./platform-management-W30iCaTL.mjs");
5577
+ const { selectAuthoritativePlatformManifest } = await import("./platform-management-COogu_Se.mjs");
4163
5578
  const selected = selectAuthoritativePlatformManifest(existing, authoritative);
4164
5579
  authoritativeProvisionIntents = true;
4165
5580
  if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
@@ -4167,19 +5582,24 @@ async function runPlatformLifecycleCommand(args) {
4167
5582
  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
5583
  }
4169
5584
  }
5585
+ const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication);
4170
5586
  const planInput = {
5587
+ authentication: authenticationSelection.authentication,
4171
5588
  accountId,
4172
5589
  applicationDomain,
4173
5590
  controlPlaneDomain,
4174
5591
  dedicatedZone,
4175
5592
  displayName,
5593
+ ...process.env.VOID_EMAIL_SHARED_ZONE_ID ? { emailSharedZoneId: process.env.VOID_EMAIL_SHARED_ZONE_ID } : {},
5594
+ ...process.env.VOID_EMAIL_SENDER_DOMAIN ? { emailSenderDomain: process.env.VOID_EMAIL_SENDER_DOMAIN } : {},
4176
5595
  installationSlug,
4177
5596
  zoneName,
4178
5597
  ...workersDev ? { workersDev: true } : {}
4179
5598
  };
4180
5599
  let plan = await createPlatformInstallPlan(client, planInput, existing, {
4181
5600
  authoritativeProvisionIntents,
4182
- allowUnverifiedAiGateway: true
5601
+ allowUnverifiedAiGateway: true,
5602
+ includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0
4183
5603
  });
4184
5604
  note(formatPlatformInstallPlan(plan), args.plan ? "Read-only installation plan" : "Installation plan");
4185
5605
  note(formatPlatformRuntime(runtime), "Platform runtime");
@@ -4258,22 +5678,44 @@ async function runPlatformLifecycleCommand(args) {
4258
5678
  await verifyRuntimeToken(runtimeCloudflareApiToken);
4259
5679
  }
4260
5680
  saveValues({ cloudflareApiToken: runtimeCloudflareApiToken });
4261
- const previousLogin = values.bootstrapGitHubLogin;
4262
- const bootstrapGitHubLogin = await collect("bootstrapGitHubLogin", "VOID_PLATFORM_ADMIN_GITHUB_LOGIN", "Initial administrator GitHub username:", false);
4263
- const bootstrapGitHubId = (previousLogin === bootstrapGitHubLogin ? values.bootstrapGitHubId : "") || process.env.VOID_PLATFORM_ADMIN_GITHUB_ID || await resolveBootstrapGitHubId(bootstrapGitHubLogin);
4264
- saveValues({ bootstrapGitHubId });
5681
+ const deferAuthentication = !!authenticationSelection.authentication.accessProtection || authenticationSelection.authentication.providers.includes("cloudflare-access");
5682
+ const saveAuthenticationValues = (updates) => {
5683
+ Object.assign(values, updates);
5684
+ const latest = readPendingInstallationSecrets(setupManifest) ?? {};
5685
+ writePendingInstallationSecrets(setupManifest, {
5686
+ ...latest,
5687
+ ...updates
5688
+ });
5689
+ };
5690
+ const authenticationSecrets = deferAuthentication ? {
5691
+ authSetupToken: values.authSetupToken || randomBytes(32).toString("hex"),
5692
+ authBootstrapCode: values.authBootstrapCode || randomBytes(32).toString("hex")
5693
+ } : await collectInstallationAuthentication(authenticationSelection, plan, values, saveAuthenticationValues);
5694
+ if (authenticationSecrets) saveAuthenticationValues(authenticationSecrets);
5695
+ let bootstrapGitHubLogin = "";
5696
+ let bootstrapGitHubId = "";
5697
+ let githubClientId = "";
5698
+ let githubClientSecret = "";
5699
+ if (!authenticationSecrets) {
5700
+ const previousLogin = values.bootstrapGitHubLogin;
5701
+ bootstrapGitHubLogin = await collect("bootstrapGitHubLogin", "VOID_PLATFORM_ADMIN_GITHUB_LOGIN", "Initial administrator GitHub username:", false);
5702
+ bootstrapGitHubId = (previousLogin === bootstrapGitHubLogin ? values.bootstrapGitHubId : "") || process.env.VOID_PLATFORM_ADMIN_GITHUB_ID || await resolveBootstrapGitHubId(bootstrapGitHubLogin);
5703
+ saveValues({ bootstrapGitHubId });
5704
+ }
4265
5705
  const installationDatabaseUrl = databaseUrl || await collect("databaseUrl", "VOID_PLATFORM_DATABASE_URL", "PostgreSQL DATABASE_URL:");
4266
5706
  saveValues({ databaseUrl: installationDatabaseUrl });
4267
- if (!configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") || !configured("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET")) openPlatformSetupPage(configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") ? {
4268
- ...pages.github,
4269
- url: "https://github.com/settings/developers"
4270
- } : pages.github);
4271
- const githubClientId = await collect("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID", "GitHub OAuth client ID:", false, false);
4272
- const githubClientSecret = await collect("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET", "GitHub OAuth client secret:", true, false);
4273
- saveValues({
4274
- githubClientId,
4275
- githubClientSecret
4276
- });
5707
+ if (!authenticationSecrets) {
5708
+ if (!configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") || !configured("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET")) openPlatformSetupPage(configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") ? {
5709
+ ...pages.github,
5710
+ url: "https://github.com/settings/developers"
5711
+ } : pages.github);
5712
+ githubClientId = await collect("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID", "GitHub OAuth client ID:", false, false);
5713
+ githubClientSecret = await collect("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET", "GitHub OAuth client secret:", true, false);
5714
+ saveValues({
5715
+ githubClientId,
5716
+ githubClientSecret
5717
+ });
5718
+ }
4277
5719
  if (!configured("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID") || !configured("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY")) openPlatformSetupPage(pages.r2);
4278
5720
  const r2AccessKeyId = await collect("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID", "Access Key ID:", false, false);
4279
5721
  const r2SecretAccessKey = await collect("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY", "Secret Access Key:", true, false);
@@ -4283,11 +5725,16 @@ async function runPlatformLifecycleCommand(args) {
4283
5725
  });
4284
5726
  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
5727
  const secrets = createPlatformInstallationSecrets({
5728
+ configurableAuthentication: !!authenticationSelection.authentication.configurable,
5729
+ ...authenticationSecrets,
4286
5730
  bootstrapGitHubId,
4287
5731
  bootstrapGitHubLogin,
4288
5732
  cloudflareApiToken: runtimeCloudflareApiToken,
4289
5733
  checkpoint: values,
4290
5734
  databaseUrl: installationDatabaseUrl,
5735
+ emailEnabled: plan.deployment.capabilities.email,
5736
+ emailSigningSecret: process.env.VOID_PLATFORM_EMAIL_SIGNING_SECRET || "",
5737
+ existingEmailEnabled: existing.deployment.capabilities.email,
4291
5738
  githubClientId,
4292
5739
  githubClientSecret,
4293
5740
  r2AccessKeyId,
@@ -4303,7 +5750,10 @@ async function runPlatformLifecycleCommand(args) {
4303
5750
  });
4304
5751
  progress = startProgress("Installing the platform");
4305
5752
  if (credentialSetup) {
4306
- saveValues({ projectSecretKey: secrets.projectSecretKey });
5753
+ saveValues({
5754
+ projectSecretKey: secrets.projectSecretKey,
5755
+ ...secrets.emailEnabled ? { emailSigningSecret: secrets.emailSigningSecret } : {}
5756
+ });
4307
5757
  existing = completePlatformSetupDraft(existing);
4308
5758
  }
4309
5759
  const result = await installPlatform(plan, secrets, existing, {
@@ -4312,13 +5762,29 @@ async function runPlatformLifecycleCommand(args) {
4312
5762
  runtime,
4313
5763
  healthCheckResumeCommand: `${cliCommandName()} platform install --resume --name ${existing.id}`,
4314
5764
  onProgress: (message) => progress?.message(message),
5765
+ ...deferAuthentication ? { prepareAuthentication: async (manifest) => {
5766
+ progress?.stop("Preparing Cloudflare Access configuration.");
5767
+ progress = void 0;
5768
+ const result = await collectInstallationAuthentication(authenticationSelection, plan, values, saveAuthenticationValues, {
5769
+ client,
5770
+ ownershipNonce: manifest.ownershipNonce,
5771
+ yes: args.yes
5772
+ });
5773
+ progress = startProgress("Continuing platform installation");
5774
+ return {
5775
+ ...result,
5776
+ accessProvisioningJson: values.accessProvisioningJson,
5777
+ accessCredentialsJson: values.accessCredentialsJson
5778
+ };
5779
+ } } : {},
4315
5780
  replanWorkers: async (manifest) => (await createPlatformInstallPlan(client, planInput, manifest, {
4316
5781
  authoritativeProvisionIntents: true,
4317
- repairMissing: true
5782
+ repairMissing: true,
5783
+ includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0
4318
5784
  })).actions.filter((action) => action.kind === "worker"),
4319
5785
  revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
4320
5786
  verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
4321
- const { verifyPlatformInstallationOwnership } = await import("./platform-management-W30iCaTL.mjs");
5787
+ const { verifyPlatformInstallationOwnership } = await import("./platform-management-COogu_Se.mjs");
4322
5788
  const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
4323
5789
  const previous = manifest.resources.find((resource) => resource.key === action.key);
4324
5790
  return action.action === "create" && previous && previous.name !== action.name;
@@ -4344,6 +5810,13 @@ async function runPlatformLifecycleCommand(args) {
4344
5810
  }
4345
5811
  log.success(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
4346
5812
  log.info(`API URL: ${plan.apiUrl}`);
5813
+ if (result.emailOperationId) log.info(`Shared email routing ready (operation ${result.emailOperationId}).`);
5814
+ const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
5815
+ if (result.setupPending && completedAuthCode) {
5816
+ log.info(`First administrator setup: ${plan.apiUrl}/setup`);
5817
+ console.log(`One-time administrator setup code: ${completedAuthCode}`);
5818
+ log.info("Confirm your identity in the browser, then test and enable any additional login methods in Settings.");
5819
+ }
4347
5820
  if (!plan.deployment.application.zoneName) log.info("Apps will use workers.dev addresses. When your domain is ready: void platform domain set <domain>");
4348
5821
  outro("Run `void auth login`, then deploy an app with `void deploy --platform void`.");
4349
5822
  } catch (error) {
@@ -4374,7 +5847,10 @@ function formatPlatformRuntime(runtime) {
4374
5847
  async function verifyPlatformAiGatewayPlan(client, input, existing, preview, authoritativeProvisionIntents) {
4375
5848
  let verified;
4376
5849
  try {
4377
- verified = await createPlatformInstallPlan(client, input, existing, { authoritativeProvisionIntents });
5850
+ verified = await createPlatformInstallPlan(client, input, existing, {
5851
+ authoritativeProvisionIntents,
5852
+ includeEmailGateway: preview.actions.some((entry) => entry.key === "email-gateway-worker")
5853
+ });
4378
5854
  } catch (error) {
4379
5855
  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
5856
  throw error;
@@ -4422,7 +5898,7 @@ async function promptText(message, value, optional = false, validate) {
4422
5898
  if (optional) return "";
4423
5899
  throw new Error(`platform: ${message.replace(/:$/, "")} is required in non-interactive mode.`);
4424
5900
  }
4425
- const input = await text({
5901
+ const input = await text$1({
4426
5902
  message,
4427
5903
  ...typeof value === "string" ? { initialValue: value } : value,
4428
5904
  validate: (input) => input ? validate?.(input) : optional ? void 0 : "A value is required."
@@ -4439,7 +5915,7 @@ async function promptSecret(message, masked = true, validate, placeholder) {
4439
5915
  const value = masked ? placeholder ? await passwordWithPlaceholder({
4440
5916
  ...options,
4441
5917
  placeholder
4442
- }) : await password(options) : await text({
5918
+ }) : await password(options) : await text$1({
4443
5919
  ...options,
4444
5920
  placeholder
4445
5921
  });
@@ -4447,4 +5923,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
4447
5923
  return value;
4448
5924
  }
4449
5925
  //#endregion
4450
- export { resolvePlatformCloudflareCredential as S, hyperdriveOriginConfigHash as _, installPlatform as a, assertNoninteractivePlatformMutationAcknowledged, verifyPlatformRuntimeTokenPermissions as b, resolveBootstrapGitHubId as c, createPlatformInstallPlan as d, formatPlatformInstallPlan as f, PlatformCloudflareClient as g, revalidatePlatformInstallAction as h, createPlatformInstallationSecrets as i, recordPlatformWorkerTraffic as l, platformProvisionIntentHash as m, acquirePlatformDatabaseLifecycleLock as n, loadPlatformRuntime as o, platformAdoptionIntentHash as p, createPlatformHealthProbeToken as r, resolvePlatformInstallDatabaseUrl, runPlatformLifecycleCommand, recordResource as s, REMOTE_INSTALLATION_MANIFEST_KEY as t, trackedPlatformWorkerTraffic as u, hyperdriveOriginFromConnectionString as v, assertPlatformOperatorCredentialCanApplyPlan as x, hyperdriveOriginMatches as y };
5926
+ 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 };