void 0.21.9 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/{account-cmd-DZZGK80W.mjs → account-cmd-C84Ee8cO.mjs} +5 -5
- package/dist/{scan-ClYmX3sa.mjs → application-analysis-BVsVqc11.mjs} +135 -18
- package/dist/application-routing-B7QEjqSR.mjs +27 -0
- package/dist/{auth-CZuiVsFh.mjs → auth-CCjt0hcq.mjs} +1 -1
- package/dist/{auth-DLNN0D3Z.mjs → auth-CSkdO2Bb.mjs} +4 -47
- package/dist/{auth-link-B_CeugBw.mjs → auth-link-CioEg6uY.mjs} +5 -5
- package/dist/{auth-router-BgEFRuvZ.mjs → auth-router-BsR981d4.mjs} +5 -5
- package/dist/{better-auth-shared-rsBGBvWJ.mjs → better-auth-shared-hy6RPh9W.mjs} +13 -2
- package/dist/{build-cmd-Dpt0jd-x.mjs → build-cmd-LzvNJORH.mjs} +19 -7
- package/dist/{cache-7_UeZdTk.mjs → cache-C4MvnrMH.mjs} +4 -4
- package/dist/{cancel-deploy-D23R1RXT.mjs → cancel-deploy-abUxpP2n.mjs} +4 -4
- package/dist/{cf-build-output-CGT03qCD.mjs → cf-build-output-BPqOT964.mjs} +141 -63
- package/dist/cf-build-output-_0HNWysu.mjs +2 -0
- package/dist/cli/cli.mjs +114 -460
- package/dist/cli/cloudflare-operation-process.mjs +1103 -0
- package/dist/cli/env-schema-probe.d.mts +2 -1
- package/dist/cli/env-schema-probe.mjs +3 -3
- package/dist/client-Cu7jWiF1.mjs +2 -0
- package/dist/{client-QTl6ko_D.mjs → client-RV8NVeB8.mjs} +165 -21
- package/dist/{cloudflare-auth-C4_GPZr0.mjs → cloudflare-auth-Qdc7tw8F.mjs} +41 -43
- package/dist/{cloudflare-cmd-bSmZ1d5L.mjs → cloudflare-cmd-BcrVyTmJ.mjs} +9 -10
- package/dist/cloudflare-config-Bktvwtpf.mjs +182 -0
- package/dist/{cloudflare-connect-jAwacxxP.mjs → cloudflare-connect-B8uPZ4nx.mjs} +4 -4
- package/dist/{cloudflare-operations-BipGMJ5O.mjs → cloudflare-operations-AiWashgg.mjs} +1 -1
- package/dist/{cloudflare-operations-sMZXpk_S.mjs → cloudflare-operations-fxHb-byx.mjs} +118 -203
- package/dist/{preset-UHj9ARyP.mjs → cloudflare-process-B-wekeR6.mjs} +93 -6
- package/dist/{config-CF69HgXc.d.mts → config-BMHb8RCj.d.mts} +2 -2
- package/dist/{config-CafTW6Cz.mjs → config-Br_JZD6u.mjs} +2 -7
- package/dist/config-C_XRIPx2.mjs +89 -0
- package/dist/{config-s7Xj7tPb.mjs → config-CyQ-wVd7.mjs} +1 -1
- package/dist/config-entry.d.mts +1 -1
- package/dist/{config-write-BSduPMY8.mjs → config-write-B1f88wJA.mjs} +1 -1
- package/dist/{connect-BsUSRzln.mjs → connect-Bd4kJd9U.mjs} +6 -6
- package/dist/{create-project-BniEV0OW.mjs → create-project-Boczwj5r.mjs} +1 -1
- package/dist/{create-project-DBfFSZKY.mjs → create-project-bMf6ffLZ.mjs} +9 -5
- package/dist/{db-hRrvZaq_.mjs → db-8uG64XLl.mjs} +26 -26
- package/dist/{delete-D6dZ9B6B.mjs → delete-NvbiyeJf.mjs} +4 -4
- package/dist/{deploy-WaAQez1O.mjs → deploy-DzAIwqNU.mjs} +944 -607
- package/dist/{deploy-DDz7c8LK.mjs → deploy-bjXdFCtn.mjs} +1 -1
- package/dist/{dist-BR1quN_w.mjs → dist-AoCzRTJE.mjs} +226 -60
- package/dist/{dist-C5fND3R0.mjs → dist-BuDuKZJv.mjs} +1 -1
- package/dist/{dist-Dn6nn2IU.mjs → dist-CTBk70IR.mjs} +42 -42
- package/dist/{domain-Dmhvb2oU.mjs → domain-y5Tvydvo.mjs} +5 -5
- package/dist/{email-DFi-s2t4.mjs → email-B3umsW75.mjs} +15 -30
- package/dist/{env-Csi-tMbT.mjs → env-BS6qYHDb.mjs} +6 -6
- package/dist/{env-public-D_6u46fX.d.mts → env-public-BX_r8HR6.d.mts} +2 -1
- package/dist/{env-validation-BB4GkLxn.mjs → env-validation-BPn7vk-V.mjs} +18 -71
- package/dist/{env-validation-BXge7uyK.mjs → env-validation-DbTg7-ar.mjs} +1 -1
- package/dist/{fetch-CXDChK7B.mjs → fetch-BIZJh7vR.mjs} +2 -2
- package/dist/{fetch-stream-AOByI7Ki.mjs → fetch-stream-IvCYKyQL.mjs} +24 -4
- package/dist/{gen-Dz3X1Qab.mjs → gen-B580--vC.mjs} +5 -5
- package/dist/gen-BVaUUumi.mjs +2 -0
- package/dist/{github-cmd-BED3_9JD.mjs → github-cmd-v45BdfKX.mjs} +6 -8
- package/dist/{handler-BXJTXd02.d.mts → handler-CZ4nAylQ.d.mts} +2 -1
- package/dist/{headers-BOg_velo.mjs → headers-DWi2IXWx.mjs} +1 -1
- package/dist/help-DofyZuY7.mjs +2 -0
- package/dist/{help-DC7gdz7L.mjs → help-daGKjXGk.mjs} +268 -666
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +305 -1490
- package/dist/info-B97bTX9N.mjs +113 -0
- package/dist/{init-WO0JlPx8.mjs → init-Bvy7zrBo.mjs} +51 -20
- package/dist/limits-Bq5LG8Id.d.mts +27 -0
- package/dist/limits-Cjuk2VPm.mjs +68 -0
- package/dist/{link-CsHOinF7.mjs → link-CDqqCjFl.mjs} +5 -5
- package/dist/{list-CDb-4bZ1.mjs → list-CZj0dzKY.mjs} +5 -5
- package/dist/{live-CKiJilLr.d.mts → live-Chw1eIMv.d.mts} +1 -1
- package/dist/{local-d1-CC8sKFGu.mjs → local-d1-2CMnpuW_.mjs} +2 -2
- package/dist/login-DYt_An22.mjs +2 -0
- package/dist/{login-DvqXsfGs.mjs → login-UZKFM_u7.mjs} +5 -5
- package/dist/{logs-BdfiOezj.mjs → logs-CUZ6t9t3.mjs} +5 -5
- package/dist/migrate-8_2u55MD.mjs +2 -0
- package/dist/{migrate-B8KuoYsO.mjs → migrate-DHul7PRV.mjs} +5 -4
- package/dist/{node-BM43oz4G.mjs → node-BkyRWRx8.mjs} +2 -2
- package/dist/operator-args-CLgKGlwU.mjs +690 -0
- package/dist/{operator-cmd-03819ATr.mjs → operator-cmd-DBA6dl0m.mjs} +69 -23
- package/dist/output-Dm_A4Tbv.mjs +81 -0
- package/dist/pages/client.d.mts +34 -2
- package/dist/pages/client.mjs +59 -3
- package/dist/pages/index.d.mts +1 -1
- package/dist/pages/index.mjs +3 -3
- package/dist/pages/islands-plugin.d.mts +16 -6
- package/dist/pages/islands-plugin.mjs +2 -2
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +2 -308
- package/dist/{parse-filename-DioPHiR9.mjs → parse-filename-CUbj-1MP.mjs} +35 -1
- package/dist/{output-CCH48AMM.mjs → picocolors-BTps1_gs.mjs} +2 -76
- package/dist/plan-D1Q5rf-r.mjs +2 -0
- package/dist/plan-NPpwZ_kc.mjs +58 -0
- package/dist/platform-args-BJdRtlLq.mjs +506 -0
- package/dist/platform-args-D3RXyR6h.mjs +2 -0
- package/dist/{platform-auth-config-z92h63q5.mjs → platform-auth-config-B56E9YP1.mjs} +4 -4
- package/dist/{platform-auth-protection-DeE3yr2R.mjs → platform-auth-protection-BDWO_aER.mjs} +3 -3
- package/dist/{platform-auth-recovery-o_340Ixx.mjs → platform-auth-recovery-1gg4CSRe.mjs} +4 -4
- package/dist/{platform-cmd-C4ZV3Vpy.mjs → platform-cmd-Bt-1w6pY.mjs} +1 -1
- package/dist/{platform-cmd-DNl8WosH.mjs → platform-cmd-DMcQStSc.mjs} +26 -5
- package/dist/{platform-domain-CU1JtJkW.mjs → platform-domain-BNkcz0OB.mjs} +36 -9
- package/dist/{platform-lifecycle-Bc047IeT.mjs → platform-lifecycle-9OyBALhH.mjs} +850 -276
- package/dist/{platform-lifecycle-Bs2qx1E3.mjs → platform-lifecycle-BE6C_jxh.mjs} +1 -1
- package/dist/{platform-management-CVpGI9Y5.mjs → platform-management-CjwLVQwN.mjs} +59 -11
- package/dist/{platform-management-C8tt6D8h.mjs → platform-management-CnyTdcWX.mjs} +1 -1
- package/dist/platform-plans-config-BNGKGr4P.mjs +359 -0
- package/dist/{platform-recovery-dvUaptLr.mjs → platform-recovery-D6KSpuFm.mjs} +2 -2
- package/dist/{plugin-inference-BMfKRSqE.mjs → plugin-inference-CXWnn79A.mjs} +174 -64
- package/dist/{prepare-DyZ-Yok5.mjs → prepare-B5Mkic5u.mjs} +3 -3
- package/dist/{prepare-C3kt3Rst.mjs → prepare-DOsL0CC9.mjs} +4 -20
- package/dist/prepare-cgvDMtSb.mjs +2 -0
- package/dist/{project-cmd-B44I8J_W.mjs → project-cmd-CNzrqvBv.mjs} +30 -16
- package/dist/{project-team-BkbYIsXN.mjs → project-team-DQOWPfQT.mjs} +4 -4
- package/dist/{project-token-C8xEEQnB.mjs → project-token-CTDYU0Cc.mjs} +4 -4
- package/dist/project-zero-trust-BNAkW-_0.mjs +63 -0
- package/dist/{protocol-ZH3jP4a7.d.mts → protocol-CjF_iI9X.d.mts} +2 -2
- package/dist/protocol-U7bfjHmA.mjs +331 -0
- package/dist/{provision-DNtrtaVD.mjs → provision-C4ORE7G7.mjs} +1 -1
- package/dist/{provision-BhreDAOS.mjs → provision-CJFgTZY8.mjs} +111 -215
- package/dist/{requests-DFyhBMaf.mjs → requests-MhxYnau8.mjs} +4 -4
- package/dist/resource-name-C7LVpcRm.mjs +11 -0
- package/dist/{rollback-DHHxXZiS.mjs → rollback-CJ6iSDoU.mjs} +5 -5
- package/dist/route-url-CG7U-cRN.mjs +15 -0
- package/dist/{runner-B8wXwWlo.mjs → runner-CXA9Fh8h.mjs} +1 -1
- package/dist/{runner-p-dMs2UN.mjs → runner-Ol0TNjk6.mjs} +2 -2
- package/dist/runtime/ai.d.mts +12 -8
- package/dist/runtime/ai.mjs +84 -17
- package/dist/runtime/better-auth-mysql.mjs +1 -1
- package/dist/runtime/better-auth-pg.mjs +1 -1
- package/dist/runtime/better-auth.mjs +1 -1
- package/dist/runtime/client-react.mjs +2 -2
- package/dist/runtime/client-solid.mjs +2 -2
- package/dist/runtime/client-svelte.mjs +2 -2
- package/dist/runtime/client-vue.mjs +2 -2
- package/dist/runtime/client.mjs +2 -2
- package/dist/runtime/durable.d.mts +3 -1
- package/dist/runtime/durable.mjs +4 -1
- package/dist/runtime/email/testing.mjs +1 -1
- package/dist/runtime/env-public.d.mts +1 -1
- package/dist/runtime/fetch-stream.mjs +1 -1
- package/dist/runtime/fetch.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/kv.mjs +0 -1
- package/dist/runtime/limits.d.mts +2 -0
- package/dist/runtime/limits.mjs +2 -0
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live-server.mjs +25 -16
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/migration-handler.mjs +62 -42
- package/dist/runtime/route-url.d.mts +4 -0
- package/dist/runtime/route-url.mjs +2 -0
- package/dist/runtime/routing.d.mts +180 -0
- package/dist/runtime/routing.mjs +1082 -0
- package/dist/runtime/sandbox-container.d.mts +3 -0
- package/dist/runtime/sandbox-container.mjs +2 -0
- package/dist/runtime/sandbox.d.mts +4 -32
- package/dist/runtime/sandbox.mjs +151 -75
- package/dist/runtime/sse.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +4 -2
- package/dist/runtime/ws-server.mjs +27 -2
- package/dist/runtime/ws.d.mts +2 -2
- package/dist/runtime/ws.mjs +8 -6
- package/dist/sandbox-XZAqzFlG.d.mts +52 -0
- package/dist/sandbox-container-Bo0eiFKz.d.mts +73 -0
- package/dist/sandbox-container-C6ItmVuN.mjs +281 -0
- package/dist/{scan-4tfN-PSn.mjs → scan-C7okrLyM.mjs} +4 -35
- package/dist/{secret-DN9sSNiV.mjs → secret-Dli5fP0B.mjs} +6 -6
- package/dist/{skills-O6FUaizK.mjs → skills-D1II1Juz.mjs} +1 -1
- package/dist/{sse-BaC1jXko.mjs → sse-CQNaDFFV.mjs} +6 -3
- package/dist/{subcommand-prompt-BuGYkAkC.mjs → subcommand-prompt-CY1C4fvl.mjs} +2 -2
- package/dist/validate-Dq_L3s0S.mjs +2 -0
- package/dist/{validate-CIUwFpjB.mjs → validate-ctOrgiS3.mjs} +2 -1
- package/dist/{wrangler-BymcxrRa.mjs → wrangler-7K-bW_DL.mjs} +14 -235
- package/dist/{ws-BwcqizuH.d.mts → ws-CL1w7GXU.d.mts} +13 -2
- package/package.json +48 -33
- package/sandbox.Dockerfile +4 -0
- package/schema.json +10 -22
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +34 -157
- package/skills/void/SKILL.md +50 -133
- package/skills/void/docs/guide/ai.md +94 -84
- package/skills/void/docs/guide/app-types.md +3 -32
- package/skills/void/docs/guide/auth.md +12 -116
- package/skills/void/docs/guide/database/d1.md +9 -54
- package/skills/void/docs/guide/database/mysql.md +1 -1
- package/skills/void/docs/guide/database/postgresql.md +5 -26
- package/skills/void/docs/guide/database.md +23 -75
- package/skills/void/docs/guide/deployment.md +27 -113
- package/skills/void/docs/guide/durable-state.md +43 -18
- package/skills/void/docs/guide/edge/headers.md +3 -47
- package/skills/void/docs/guide/edge/prerendering.md +5 -20
- package/skills/void/docs/guide/edge/redirects.md +11 -64
- package/skills/void/docs/guide/edge/revalidation.md +6 -19
- package/skills/void/docs/guide/edge/rewrites.md +56 -284
- package/skills/void/docs/guide/edge/static-assets.md +23 -72
- package/skills/void/docs/guide/email/domains.md +112 -0
- package/skills/void/docs/guide/email/receiving.md +139 -0
- package/skills/void/docs/guide/email/sending.md +231 -0
- package/skills/void/docs/guide/email.md +13 -619
- package/skills/void/docs/guide/env-migration.md +11 -11
- package/skills/void/docs/guide/env-vars.md +9 -29
- package/skills/void/docs/guide/index.md +0 -15
- package/skills/void/docs/guide/jobs.md +3 -18
- package/skills/void/docs/guide/kv.md +5 -11
- package/skills/void/docs/guide/live.md +5 -56
- package/skills/void/docs/guide/pages-routing/actions-and-forms.md +78 -125
- package/skills/void/docs/guide/pages-routing/head.md +10 -10
- package/skills/void/docs/guide/pages-routing/islands.md +6 -36
- package/skills/void/docs/guide/pages-routing/layouts.md +6 -128
- package/skills/void/docs/guide/pages-routing/loaders.md +3 -19
- package/skills/void/docs/guide/pages-routing/markdown.md +13 -171
- package/skills/void/docs/guide/pages-routing/overview.md +7 -17
- package/skills/void/docs/guide/pages-routing/view-transitions.md +1 -1
- package/skills/void/docs/guide/platform/administration/access.md +1 -4
- package/skills/void/docs/guide/platform/administration/email.md +35 -8
- package/skills/void/docs/guide/platform/administration/operations.md +26 -4
- package/skills/void/docs/guide/platform/administration/plans.md +126 -0
- package/skills/void/docs/guide/platform/administration/projects.md +5 -2
- package/skills/void/docs/guide/platform/administration/zero-trust.md +189 -0
- package/skills/void/docs/guide/platform/development/local.md +2 -2
- package/skills/void/docs/guide/platform/development/runtime.md +3 -13
- package/skills/void/docs/guide/platform/development/schema-ci.md +0 -58
- package/skills/void/docs/guide/platform/installation/credentials.md +6 -4
- package/skills/void/docs/guide/platform/installation/domains.md +31 -3
- package/skills/void/docs/guide/platform/installation/first-deployment.md +6 -0
- package/skills/void/docs/guide/platform/installation/maintenance.md +3 -1
- package/skills/void/docs/guide/platform/installation/prerequisites.md +21 -16
- package/skills/void/docs/guide/platform/installation/setup.md +9 -5
- package/skills/void/docs/guide/platform/installation/uninstall.md +17 -2
- package/skills/void/docs/guide/platform-administration.md +2 -0
- package/skills/void/docs/guide/queues.md +7 -9
- package/skills/void/docs/guide/quickstart.md +38 -37
- package/skills/void/docs/guide/remote-dev.md +4 -9
- package/skills/void/docs/guide/sandboxes.md +78 -41
- package/skills/void/docs/guide/server-routing.md +9 -72
- package/skills/void/docs/guide/sse.md +4 -18
- package/skills/void/docs/guide/ssg.md +3 -15
- package/skills/void/docs/guide/ssr.md +14 -62
- package/skills/void/docs/guide/storage.md +9 -4
- package/skills/void/docs/guide/type-safety.md +3 -14
- package/skills/void/docs/guide/typed-fetch.md +3 -7
- package/skills/void/docs/guide/websockets.md +68 -40
- package/skills/void/docs/integrations/agents.md +3 -3
- package/skills/void/docs/integrations/cloudflare.md +85 -316
- package/skills/void/docs/integrations/frameworks/analog.md +5 -64
- package/skills/void/docs/integrations/frameworks/astro.md +4 -73
- package/skills/void/docs/integrations/frameworks/nuxt.md +5 -62
- package/skills/void/docs/integrations/frameworks/overview.md +11 -54
- package/skills/void/docs/integrations/frameworks/react-router.md +5 -60
- package/skills/void/docs/integrations/frameworks/sveltekit.md +6 -65
- package/skills/void/docs/integrations/frameworks/tanstack-start.md +4 -62
- package/skills/void/docs/integrations/nodejs-bun-deno.md +5 -69
- package/skills/void/docs/reference/api/auth.md +156 -0
- package/skills/void/docs/reference/api/client.md +87 -0
- package/skills/void/docs/reference/api/database.md +95 -0
- package/skills/void/docs/reference/api/durable.md +46 -0
- package/skills/void/docs/reference/api/env.md +50 -0
- package/skills/void/docs/reference/api/handlers.md +254 -0
- package/skills/void/docs/reference/api/pages.md +241 -0
- package/skills/void/docs/reference/api/plugin.md +39 -0
- package/skills/void/docs/reference/api/resources.md +109 -0
- package/skills/void/docs/reference/api/rewrites.md +76 -0
- package/skills/void/docs/reference/api/types.md +92 -0
- package/skills/void/docs/reference/api.md +56 -1218
- package/skills/void/docs/reference/cli/auth.md +88 -0
- package/skills/void/docs/reference/cli/database.md +128 -0
- package/skills/void/docs/reference/cli/deploy.md +85 -0
- package/skills/void/docs/reference/cli/domains.md +41 -0
- package/skills/void/docs/reference/cli/email.md +129 -0
- package/skills/void/docs/reference/cli/generate.md +116 -0
- package/skills/void/docs/reference/cli/github.md +189 -0
- package/skills/void/docs/reference/cli/platform-config.md +92 -0
- package/skills/void/docs/reference/cli/platform-email.md +81 -0
- package/skills/void/docs/reference/cli/platform-installation.md +127 -0
- package/skills/void/docs/reference/cli/platform-operations.md +90 -0
- package/skills/void/docs/reference/cli/platform-users.md +89 -0
- package/skills/void/docs/reference/cli/platform-zero-trust.md +45 -0
- package/skills/void/docs/reference/cli/platform.md +70 -0
- package/skills/void/docs/reference/cli/project.md +214 -0
- package/skills/void/docs/reference/cli/secrets.md +76 -0
- package/skills/void/docs/reference/cli/setup.md +70 -0
- package/skills/void/docs/reference/cli.md +33 -1621
- package/skills/void/docs/reference/config.md +26 -32
- package/skills/void/docs/reference/resource-inference.md +3 -58
- package/skills/void/docs/reference/structure.md +14 -41
- package/dist/canonical-json-DuDiiUsQ.mjs +0 -13
- package/dist/cli/cf-compat.mjs +0 -968
- package/dist/client-4cDVv7BO.mjs +0 -2
- package/dist/gen-Vnv2f65C.mjs +0 -2
- package/dist/help-DQMfeKMz.mjs +0 -2
- package/dist/login-DFQk7rbW.mjs +0 -2
- package/dist/migrate-TsHGBnDA.mjs +0 -2
- package/dist/plan-BEZ8VJW0.mjs +0 -256
- package/dist/plan-DpuOr14e.mjs +0 -2
- package/dist/prepare-BZXkjdNe.mjs +0 -2
- package/dist/validate-EKmJWxmy.mjs +0 -2
- /package/dist/cli/{cf-compat.d.mts → cloudflare-operation-process.d.mts} +0 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { n as __toESM } from "./rolldown-runtime-DXIUcv95.mjs";
|
|
2
2
|
import { a as join, n as dirname, u as resolve } from "./dist-BVqXeR_h.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { t as require_picocolors } from "./picocolors-BTps1_gs.mjs";
|
|
4
4
|
import { t as cfAccessHeaders } from "./cf-access-DRsQRe6k.mjs";
|
|
5
|
-
import { d as openBrowser } from "./auth-
|
|
5
|
+
import { d as openBrowser } from "./auth-CSkdO2Bb.mjs";
|
|
6
6
|
import { a as listInstallationManifests, d as readInstallationRecoverySecrets, f as readPendingInstallationSecrets, g as savePlatformConnection, h as resolveInstallationManifest, i as latestPlatformWorkerCutoverGeneration, l as platformWorkerSecretUpdateIntentState, n as assertPlatformInstallationSlugAvailable, s as normalizePlatformApiUrl, t as acquirePlatformLifecycleLock, u as promoteInstallationSecrets, v as writeInstallationManifest, y as writePendingInstallationSecrets } from "./platform-registry-QBa4DK32.mjs";
|
|
7
7
|
import { n as ensurePlatformAccessToken } from "./cf-access-P0iZnu0q.mjs";
|
|
8
|
-
import { B as text$1, E as confirm, F as password, G as settings, I as select, K as u$1, M as multiselect, N as note, O as intro, P as outro, R as symbol, U as CANCEL_SYMBOL, W as isCancel, g as S_PASSWORD_MASK, i as S_BAR_END, j as log, r as S_BAR } from "./dist-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-
|
|
8
|
+
import { B as text$1, E as confirm, F as password, G as settings, I as select, K as u$1, M as multiselect, N as note, O as intro, P as outro, R as symbol, U as CANCEL_SYMBOL, W as isCancel, g as S_PASSWORD_MASK, i as S_BAR_END, j as log, r as S_BAR } from "./dist-CTBk70IR.mjs";
|
|
9
|
+
import { c as resolveCloudflareOperationArgv } from "./cloudflare-process-B-wekeR6.mjs";
|
|
10
|
+
import { o as runCloudflareAuthCommand } from "./cloudflare-auth-Qdc7tw8F.mjs";
|
|
11
|
+
import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-CJFgTZY8.mjs";
|
|
12
|
+
import { c as import_picocolors$1, i as createSpinner, n as cliCommandName, r as cliTitle } from "./output-Dm_A4Tbv.mjs";
|
|
12
13
|
import { copyFileSync, cpSync, existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
15
16
|
import { tmpdir } from "node:os";
|
|
16
17
|
import { execFile } from "node:child_process";
|
|
17
|
-
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,
|
|
18
|
+
import { PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
18
19
|
import { stripVTControlCharacters } from "node:util";
|
|
19
20
|
import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
20
21
|
import { readFile } from "node:fs/promises";
|
|
@@ -652,7 +653,7 @@ function parsePostgresConnectionString(value) {
|
|
|
652
653
|
let mtls;
|
|
653
654
|
if (sslmode) {
|
|
654
655
|
if (sslmode !== "require" && sslmode !== "verify-ca" && sslmode !== "verify-full") throw new Error("platform: Unsupported sslmode. Cloudflare Hyperdrive supports \"require\", \"verify-ca\", or \"verify-full\".");
|
|
655
|
-
mtls = { sslmode };
|
|
656
|
+
mtls = { sslmode: "require" };
|
|
656
657
|
}
|
|
657
658
|
return {
|
|
658
659
|
database,
|
|
@@ -716,6 +717,14 @@ function text(value, name) {
|
|
|
716
717
|
if (typeof value !== "string" || !value || value.length > 8192) throw new Error(`platform: Cloudflare Access did not return ${name}.`);
|
|
717
718
|
return value;
|
|
718
719
|
}
|
|
720
|
+
function platformAccessIdentityProvider(provider) {
|
|
721
|
+
const id = text(provider.id, "identity provider ID");
|
|
722
|
+
const name = provider.name;
|
|
723
|
+
return {
|
|
724
|
+
id,
|
|
725
|
+
name: name == null || typeof name === "string" && !name.trim() ? id : text(name, "identity provider name")
|
|
726
|
+
};
|
|
727
|
+
}
|
|
719
728
|
function ids(value) {
|
|
720
729
|
if (!Array.isArray(value)) return [];
|
|
721
730
|
return value.map((entry) => typeof entry === "string" ? entry : text(object(entry, "policy").id, "policy ID"));
|
|
@@ -756,6 +765,11 @@ var PlatformAccessClient = class {
|
|
|
756
765
|
throw new Error("platform: Access inventory exceeded the supported pagination limit.");
|
|
757
766
|
}
|
|
758
767
|
};
|
|
768
|
+
/**
|
|
769
|
+
* Project Zero Trust keeps its own, broader copy of this list in
|
|
770
|
+
* platform/packages/api/src/zero-trust/cloudflare.ts; review both when adding
|
|
771
|
+
* a rule type.
|
|
772
|
+
*/
|
|
759
773
|
function identityRule(rule) {
|
|
760
774
|
if (rule.email_domain) {
|
|
761
775
|
const domain = object(rule.email_domain, "email domain").domain;
|
|
@@ -840,10 +854,7 @@ async function inspectPlatformAccess(client, input) {
|
|
|
840
854
|
audience: "validation"
|
|
841
855
|
}]
|
|
842
856
|
});
|
|
843
|
-
const identityProviders = (await client.list("identity_providers")).map(
|
|
844
|
-
id: text(provider.id, "identity provider ID"),
|
|
845
|
-
name: text(provider.name ?? provider.id, "identity provider name")
|
|
846
|
-
}));
|
|
857
|
+
const identityProviders = (await client.list("identity_providers")).map(platformAccessIdentityProvider);
|
|
847
858
|
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.");
|
|
848
859
|
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.");
|
|
849
860
|
const policies = await Promise.all(input.policyIds.map(async (id) => object(await client.get(`policies/${encodeURIComponent(id)}`), "Access policy")));
|
|
@@ -1271,9 +1282,9 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1271
1282
|
const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ? answer$1(await multiselect({
|
|
1272
1283
|
message: "Company identity providers:",
|
|
1273
1284
|
required: true,
|
|
1274
|
-
options: providers.filter((provider) => typeof provider.id === "string").map((provider) => ({
|
|
1275
|
-
value:
|
|
1276
|
-
label: stripVTControlCharacters(
|
|
1285
|
+
options: providers.filter((provider) => typeof provider.id === "string").map(platformAccessIdentityProvider).map((provider) => ({
|
|
1286
|
+
value: provider.id,
|
|
1287
|
+
label: stripVTControlCharacters(provider.name)
|
|
1277
1288
|
}))
|
|
1278
1289
|
})) : []);
|
|
1279
1290
|
const policyIds = spec.policyIds ?? (process.stdin.isTTY ? answer$1(await multiselect({
|
|
@@ -1411,10 +1422,9 @@ async function resolvePlatformCloudflareCredentialWithInput(readInput, dependenc
|
|
|
1411
1422
|
input,
|
|
1412
1423
|
credential: result.credential
|
|
1413
1424
|
};
|
|
1414
|
-
|
|
1415
|
-
const login = await runWrangler(["login"], false);
|
|
1425
|
+
const login = await runWrangler({ type: "auth.login" }, false);
|
|
1416
1426
|
assertNotCancelled();
|
|
1417
|
-
if (login.status !== 0) throw new Error("platform: Cloudflare browser login did not complete.");
|
|
1427
|
+
if (login.status !== 0 || login.signal || login.error) throw new Error("platform: Cloudflare browser login did not complete.");
|
|
1418
1428
|
const credential = await readWranglerOAuthCredential(runWrangler);
|
|
1419
1429
|
assertNotCancelled();
|
|
1420
1430
|
if (!credential) throw new Error("platform: Cloudflare sign-in completed without a usable OAuth credential.");
|
|
@@ -1437,15 +1447,11 @@ async function readExistingCredential(dependencies, runWrangler) {
|
|
|
1437
1447
|
return await readWranglerOAuthCredential(runWrangler);
|
|
1438
1448
|
}
|
|
1439
1449
|
async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflareAuthCommand(process.cwd(), args)) {
|
|
1440
|
-
const result = await runWrangler(
|
|
1441
|
-
|
|
1442
|
-
"token",
|
|
1443
|
-
"--json"
|
|
1444
|
-
], true);
|
|
1445
|
-
if (result.status !== 0) return null;
|
|
1450
|
+
const result = await runWrangler({ type: "auth.token" }, true);
|
|
1451
|
+
if (result.status !== 0 || result.signal || result.error) return null;
|
|
1446
1452
|
try {
|
|
1447
|
-
const parsed =
|
|
1448
|
-
if (parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
1453
|
+
const parsed = result.data;
|
|
1454
|
+
if (!parsed || parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
1449
1455
|
return {
|
|
1450
1456
|
source: "oauth",
|
|
1451
1457
|
token: parsed.token.trim()
|
|
@@ -1485,6 +1491,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1485
1491
|
if ([
|
|
1486
1492
|
input.plan.apiUrl,
|
|
1487
1493
|
input.plan.proxyUrl,
|
|
1494
|
+
...input.dashboardUrl ? [input.dashboardUrl] : [],
|
|
1488
1495
|
...input.manifest.endpoints.dashboardUrl ? [input.manifest.endpoints.dashboardUrl] : []
|
|
1489
1496
|
].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.");
|
|
1490
1497
|
for (const other of await access.list("apps")) {
|
|
@@ -1500,7 +1507,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1500
1507
|
}
|
|
1501
1508
|
const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
|
|
1502
1509
|
if (existingWorkers.length) {
|
|
1503
|
-
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-
|
|
1510
|
+
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-BDWO_aER.mjs");
|
|
1504
1511
|
await verifyProtectionWorkerOrigins({
|
|
1505
1512
|
request: input.client.request.bind(input.client),
|
|
1506
1513
|
listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
|
|
@@ -1593,6 +1600,7 @@ async function verifyPlatformAuthenticationDatabase(client, accountId, resource,
|
|
|
1593
1600
|
//#endregion
|
|
1594
1601
|
//#region src/cli/platform-plan.ts
|
|
1595
1602
|
async function createPlatformInstallPlan(client, input, existing, options) {
|
|
1603
|
+
const dashboardUrl = input.dashboardUrl !== void 0 ? normalizePlatformDashboardUrl(input.dashboardUrl) : existing ? existing.deployment.controlPlane.dashboardUrl ?? (existing.endpoints.dashboardUrl ? normalizePlatformDashboardUrl(existing.endpoints.dashboardUrl) : null) ?? platformDashboardUrl(existing.deployment) : null;
|
|
1596
1604
|
const account = resolveAccount(await client.listAccounts(), input.accountId);
|
|
1597
1605
|
if (existing && existing.account.id !== account.id) throw new Error(`platform: Refusing to resume installation ${existing.id} in a different Cloudflare account.`);
|
|
1598
1606
|
const providedEmailSharedZoneId = input.emailSharedZoneId?.trim();
|
|
@@ -1641,7 +1649,8 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1641
1649
|
},
|
|
1642
1650
|
controlPlane: {
|
|
1643
1651
|
apiHostname: input.controlPlaneDomain || null,
|
|
1644
|
-
dashboardHostname: null,
|
|
1652
|
+
dashboardHostname: dashboardUrl ? new URL(dashboardUrl).hostname : null,
|
|
1653
|
+
...dashboardUrl ? { dashboardUrl } : {},
|
|
1645
1654
|
proxyHostname: null
|
|
1646
1655
|
},
|
|
1647
1656
|
displayName: input.displayName,
|
|
@@ -1659,13 +1668,21 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1659
1668
|
sandbox: true
|
|
1660
1669
|
}
|
|
1661
1670
|
} : existingWithoutEmailZone;
|
|
1662
|
-
const
|
|
1671
|
+
const existingWithDashboard = {
|
|
1663
1672
|
...existingWithSandbox,
|
|
1673
|
+
controlPlane: {
|
|
1674
|
+
...existingWithSandbox.controlPlane,
|
|
1675
|
+
dashboardHostname: deployment.controlPlane.dashboardHostname,
|
|
1676
|
+
...deployment.controlPlane.dashboardUrl ? { dashboardUrl: deployment.controlPlane.dashboardUrl } : {}
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
const comparableExistingWithoutEmailZone = existing && deployment.capabilities.email && !existing.deployment.capabilities.email ? {
|
|
1680
|
+
...existingWithDashboard,
|
|
1664
1681
|
capabilities: {
|
|
1665
|
-
...
|
|
1682
|
+
...existingWithDashboard.capabilities,
|
|
1666
1683
|
email: true
|
|
1667
1684
|
}
|
|
1668
|
-
} :
|
|
1685
|
+
} : existingWithDashboard;
|
|
1669
1686
|
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.`);
|
|
1670
1687
|
const names = deployment.resources;
|
|
1671
1688
|
const wildcardHostname = `*.${deployment.application.baseDomain}`;
|
|
@@ -1689,7 +1706,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1689
1706
|
}
|
|
1690
1707
|
throw error;
|
|
1691
1708
|
});
|
|
1692
|
-
const [workersSubdomain, aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
|
|
1709
|
+
const [workersSubdomain, aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, deploymentQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
|
|
1693
1710
|
Promise.resolve(accountSubdomain),
|
|
1694
1711
|
gatewayInventory,
|
|
1695
1712
|
client.listKvNamespaces(account.id, names.routingKv),
|
|
@@ -1698,6 +1715,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1698
1715
|
client.listR2Buckets(account.id),
|
|
1699
1716
|
client.listQueues(account.id, names.usageQueue),
|
|
1700
1717
|
client.listQueues(account.id, names.cronQueue),
|
|
1718
|
+
options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue") ? client.listQueues(account.id, names.deploymentQueue) : Promise.resolve([]),
|
|
1701
1719
|
client.listDispatchNamespaces(account.id),
|
|
1702
1720
|
client.listHyperdriveConfigs(account.id),
|
|
1703
1721
|
client.listWorkerNames(account.id)
|
|
@@ -1732,11 +1750,13 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1732
1750
|
const expectedPatterns = new Set(createApplicationRoutes(deployment.application).map((route) => route.pattern));
|
|
1733
1751
|
const expectedWorker = deployment.resources.dispatchWorker;
|
|
1734
1752
|
const ownedWorker = intentProofs.has("dispatch-worker") || existing?.resources.some((resource) => resource.key === "dispatch-worker" && resource.kind === "worker" && resource.name === expectedWorker && resource.state === "ready");
|
|
1735
|
-
const
|
|
1753
|
+
const [workerRoutes, workerDomains] = await Promise.all([client.listWorkerRoutes(zone.id), client.listWorkerDomains(account.id)]);
|
|
1754
|
+
const conflicts = workerRoutes.filter((route) => expectedPatterns.has(route.pattern) && (route.script !== expectedWorker || !ownedWorker));
|
|
1736
1755
|
if (conflicts.length > 0) {
|
|
1737
1756
|
const conflict = conflicts[0];
|
|
1738
1757
|
throw new Error(`platform: Application route ${JSON.stringify(conflict.pattern)} is already assigned to ${JSON.stringify(conflict.script ?? "no Worker")}. Choose another application domain.`);
|
|
1739
1758
|
}
|
|
1759
|
+
assertApplicationRoutesPreserveWorkerDomains([...expectedPatterns], expectedWorker, ownedWorker === true, zone.name, workerRoutes, workerDomains);
|
|
1740
1760
|
}
|
|
1741
1761
|
if (deployment.controlPlane.apiHostname) {
|
|
1742
1762
|
const apiHostname = deployment.controlPlane.apiHostname;
|
|
@@ -1779,6 +1799,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1779
1799
|
addNamedPlan(actions, existing, "assets-bucket", "r2-bucket", names.assetsBucket, r2Buckets.filter((resource) => resource.name === names.assetsBucket), "reuse", options, remoteOwnership, intentProofs, ambiguousResources);
|
|
1780
1800
|
addNamedPlan(actions, existing, "usage-queue", "queue", names.usageQueue, usageQueues, "reuse", options, remoteOwnership);
|
|
1781
1801
|
addNamedPlan(actions, existing, "cron-queue", "queue", names.cronQueue, cronQueues, "reuse", options, remoteOwnership);
|
|
1802
|
+
if (options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue")) addNamedPlan(actions, existing, "deployment-queue", "queue", names.deploymentQueue, deploymentQueues, "reuse", options, remoteOwnership);
|
|
1782
1803
|
addNamedPlan(actions, existing, "dispatch-namespace", "dispatch-namespace", names.dispatchNamespace, dispatchNamespaces.filter((resource) => resource.name === names.dispatchNamespace), "reuse", options, remoteOwnership);
|
|
1783
1804
|
const recordedHyperdrive = existing?.resources.find((resource) => resource.key === "hyperdrive");
|
|
1784
1805
|
const externalHyperdriveId = input.externalHyperdrive?.id || (recordedHyperdrive?.ownership === "adopted" ? recordedHyperdrive.id : "");
|
|
@@ -1831,6 +1852,18 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1831
1852
|
};
|
|
1832
1853
|
}
|
|
1833
1854
|
async function revalidatePlatformInstallAction(client, original, manifest, action, options) {
|
|
1855
|
+
if (options?.stableWorkerUpdate && !options.repairMissing && action.kind === "worker" && action.action === "update" && original.actions.some((entry) => entry.key === action.key && entry.kind === action.kind && entry.action === action.action && entry.id === action.id && entry.name === action.name) && original.actions.every((entry) => {
|
|
1856
|
+
const resource = manifest.resources.find((candidate) => candidate.key === entry.key);
|
|
1857
|
+
return (entry.kind === "worker" ? entry.action === "update" : entry.action === "reuse") && resource?.kind === entry.kind && resource.state === "ready" && resource.id === entry.id && resource.name === entry.name;
|
|
1858
|
+
})) {
|
|
1859
|
+
const recorded = manifest.resources.find((resource) => resource.key === action.key);
|
|
1860
|
+
if (!recorded || recorded.kind !== "worker" || recorded.state !== "ready" || recorded.id !== action.name || recorded.name !== action.name) throw new Error(`platform: Worker ${JSON.stringify(action.name)} no longer matches the approved installation plan. Review a new plan before retrying.`);
|
|
1861
|
+
if (!matchesResourceMarker((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, action.key, "worker", {
|
|
1862
|
+
id: action.name,
|
|
1863
|
+
name: action.name
|
|
1864
|
+
})) throw new Error(`platform: Cloudflare Worker ${JSON.stringify(action.name)} no longer has this installation's ownership marker. Review a new plan before retrying.`);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1834
1867
|
const deployment = original.deployment;
|
|
1835
1868
|
const current = (await createPlatformInstallPlan(client, {
|
|
1836
1869
|
accountId: original.account.id,
|
|
@@ -1844,7 +1877,8 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
|
|
|
1844
1877
|
}, manifest, {
|
|
1845
1878
|
...options,
|
|
1846
1879
|
includeEmailGateway: original.actions.some((entry) => entry.key === "email-gateway-worker"),
|
|
1847
|
-
enableSandbox: original.deployment.capabilities.sandbox
|
|
1880
|
+
enableSandbox: original.deployment.capabilities.sandbox,
|
|
1881
|
+
deploymentExecution: original.actions.some((entry) => entry.key === "deployment-queue")
|
|
1848
1882
|
})).actions.find((entry) => entry.key === action.key);
|
|
1849
1883
|
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.`);
|
|
1850
1884
|
if (action.kind === "worker" && action.action === "update") {
|
|
@@ -2038,6 +2072,7 @@ const RESOURCE_NAME_KEY_BY_ACTION = {
|
|
|
2038
2072
|
"api-worker": "apiWorker",
|
|
2039
2073
|
"assets-bucket": "assetsBucket",
|
|
2040
2074
|
"cron-queue": "cronQueue",
|
|
2075
|
+
"deployment-queue": "deploymentQueue",
|
|
2041
2076
|
"dispatch-namespace": "dispatchNamespace",
|
|
2042
2077
|
"dispatch-worker": "dispatchWorker",
|
|
2043
2078
|
hyperdrive: "hyperdrive",
|
|
@@ -2049,6 +2084,33 @@ const RESOURCE_NAME_KEY_BY_ACTION = {
|
|
|
2049
2084
|
"tail-worker": "tailWorker",
|
|
2050
2085
|
"usage-queue": "usageQueue"
|
|
2051
2086
|
};
|
|
2087
|
+
function assertApplicationRoutesPreserveWorkerDomains(patterns, dispatchWorker, ownedDispatchWorker, zoneName, workerRoutes, workerDomains) {
|
|
2088
|
+
const plannedRoutes = patterns.map(parseWorkerRoutePattern);
|
|
2089
|
+
const existingRoutes = workerRoutes.map((route) => ({
|
|
2090
|
+
...parseWorkerRoutePattern(route.pattern),
|
|
2091
|
+
script: route.script
|
|
2092
|
+
}));
|
|
2093
|
+
for (const domain of workerDomains) {
|
|
2094
|
+
const hostname = domain.hostname.toLowerCase();
|
|
2095
|
+
if (!(domain.zoneName ? domain.zoneName.toLowerCase() === zoneName.toLowerCase() : hostname === zoneName.toLowerCase() || hostname.endsWith(`.${zoneName.toLowerCase()}`)) || domain.service === dispatchWorker && ownedDispatchWorker) continue;
|
|
2096
|
+
for (const planned of plannedRoutes) {
|
|
2097
|
+
if (!workerRouteHostMatches(planned.host, hostname)) continue;
|
|
2098
|
+
if (!["http", "https"].every((scheme) => existingRoutes.some((route) => route.script !== dispatchWorker && route.path === "/*" && (!route.scheme || route.scheme === scheme) && workerRouteHostMatches(route.host, hostname) && route.host.replace(/^\*/, "").length > planned.host.replace(/^\*/, "").length))) throw new Error(`platform: Application route ${JSON.stringify(planned.pattern)} would intercept Worker custom domain ${JSON.stringify(domain.hostname)}, currently served by ${JSON.stringify(domain.service)}. Choose another application domain, or use \`void platform install --workers-dev\` for a new installation and add a suitable domain later.`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
function parseWorkerRoutePattern(pattern) {
|
|
2103
|
+
const match = /^(?:(https?):\/\/)?([^/]+)(\/.*)?$/.exec(pattern);
|
|
2104
|
+
return {
|
|
2105
|
+
host: match?.[2]?.toLowerCase() ?? "",
|
|
2106
|
+
path: match?.[3] ?? "/",
|
|
2107
|
+
pattern,
|
|
2108
|
+
scheme: match?.[1] ?? null
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
function workerRouteHostMatches(host, hostname) {
|
|
2112
|
+
return host.startsWith("*") ? hostname.endsWith(host.slice(1)) : host === hostname;
|
|
2113
|
+
}
|
|
2052
2114
|
function platformProvisionIntentHash(manifest, key, kind, name, previousResourceId = null) {
|
|
2053
2115
|
return createHash("sha256").update(JSON.stringify({
|
|
2054
2116
|
key,
|
|
@@ -2084,10 +2146,12 @@ async function readRemoteOwnership(client, accountId, routingNamespaces, existin
|
|
|
2084
2146
|
}
|
|
2085
2147
|
function formatPlatformInstallPlan(plan) {
|
|
2086
2148
|
const lines = [
|
|
2087
|
-
`${import_picocolors$1.default.bold(`${plan.deployment.displayName} in ${plan.account.name}`)}
|
|
2088
|
-
|
|
2149
|
+
`${import_picocolors$1.default.bold(`${plan.deployment.displayName} in ${plan.account.name}`)}`,
|
|
2150
|
+
`${import_picocolors$1.default.bold("Account:")} ${plan.account.id}`,
|
|
2151
|
+
plan.deployment.application.zoneName ? `${import_picocolors$1.default.bold("Applications:")} *.${plan.deployment.application.baseDomain}` : `${import_picocolors$1.default.bold("Applications:")} workers.dev for testing. Add a domain with void platform domain set <domain>`,
|
|
2089
2152
|
`${import_picocolors$1.default.bold("Control plane:")} ${plan.apiUrl}`,
|
|
2090
2153
|
`${import_picocolors$1.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
|
|
2154
|
+
...platformDashboardUrl(plan.deployment) ? [`${import_picocolors$1.default.bold("Dashboard:")} ${platformDashboardUrl(plan.deployment)} (deploy separately)`] : [],
|
|
2091
2155
|
`${import_picocolors$1.default.bold("Login methods:")} ${plan.deployment.authentication.providers.join(", ")}`,
|
|
2092
2156
|
`${import_picocolors$1.default.bold("Access protection:")} ${plan.deployment.authentication.accessProtection ? "Enabled" : "Not enabled"}`,
|
|
2093
2157
|
`${import_picocolors$1.default.bold("Login callback:")} ${plan.apiUrl}/auth/callback`,
|
|
@@ -2216,6 +2280,284 @@ function recordPlatformWorkerTraffic(manifest, workerName, inventory, updatedAt)
|
|
|
2216
2280
|
return next;
|
|
2217
2281
|
}
|
|
2218
2282
|
//#endregion
|
|
2283
|
+
//#region src/cli/platform-trigger-proof.ts
|
|
2284
|
+
function record(value) {
|
|
2285
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2286
|
+
}
|
|
2287
|
+
function sameStrings(actual, expected) {
|
|
2288
|
+
const sortedExpected = [...expected].sort();
|
|
2289
|
+
return actual.length === expected.length && [...actual].sort().every((value, index) => value === sortedExpected[index]);
|
|
2290
|
+
}
|
|
2291
|
+
function desiredQueue(value) {
|
|
2292
|
+
const queue = record(value);
|
|
2293
|
+
if (!queue || typeof queue.queue !== "string" || Object.keys(queue).some((key) => ![
|
|
2294
|
+
"queue",
|
|
2295
|
+
"max_batch_size",
|
|
2296
|
+
"max_batch_timeout",
|
|
2297
|
+
"max_retries",
|
|
2298
|
+
"max_concurrency",
|
|
2299
|
+
"retry_delay",
|
|
2300
|
+
"dead_letter_queue"
|
|
2301
|
+
].includes(key)) || [
|
|
2302
|
+
"max_batch_size",
|
|
2303
|
+
"max_batch_timeout",
|
|
2304
|
+
"max_retries",
|
|
2305
|
+
"max_concurrency",
|
|
2306
|
+
"retry_delay"
|
|
2307
|
+
].some((key) => queue[key] !== void 0 && typeof queue[key] !== "number") || queue.dead_letter_queue !== void 0 && typeof queue.dead_letter_queue !== "string") return null;
|
|
2308
|
+
return queue;
|
|
2309
|
+
}
|
|
2310
|
+
function matchesQueueConsumer(liveValue, desired, workerName) {
|
|
2311
|
+
const live = record(liveValue);
|
|
2312
|
+
const settings = record(live?.settings);
|
|
2313
|
+
if (!live || live.type !== "worker" || live.script_name !== void 0 && live.script_name !== workerName || live.queue_name !== desired.queue || !settings) return false;
|
|
2314
|
+
const expected = {
|
|
2315
|
+
batch_size: desired.max_batch_size ?? 10,
|
|
2316
|
+
max_wait_time_ms: Number(desired.max_batch_timeout ?? 5) * 1e3,
|
|
2317
|
+
max_retries: desired.max_retries ?? 3,
|
|
2318
|
+
max_concurrency: desired.max_concurrency ?? null,
|
|
2319
|
+
retry_delay: desired.retry_delay ?? 0
|
|
2320
|
+
};
|
|
2321
|
+
return Object.entries(expected).every(([key, value]) => key === "max_concurrency" ? (settings[key] ?? null) === value : settings[key] === value) && (live.dead_letter_queue ?? "") === (desired.dead_letter_queue ?? "");
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Read every surface that `triggers deploy` can mutate for a platform Worker. An incomplete
|
|
2325
|
+
* response, unsupported trigger kind, or read failure requires normal trigger publication.
|
|
2326
|
+
*/
|
|
2327
|
+
async function platformWorkerLiveTriggersMatch(options) {
|
|
2328
|
+
const { accountId, workerName, desired, readCloudflare } = options;
|
|
2329
|
+
if (desired.preserve || desired.addresses !== void 0 || desired.workflows.length > 0 || !desired.crons.every((cron) => typeof cron === "string")) return false;
|
|
2330
|
+
const queues = desired.queueConsumers.map(desiredQueue);
|
|
2331
|
+
if (queues.some((queue) => queue === null)) return false;
|
|
2332
|
+
const base = `/accounts/${encodeURIComponent(accountId)}`;
|
|
2333
|
+
const script = `${base}/workers/scripts/${encodeURIComponent(workerName)}`;
|
|
2334
|
+
try {
|
|
2335
|
+
const [workerValue, schedulesValue, workflowsValue, nextWorkflowsValue] = await Promise.all([
|
|
2336
|
+
readCloudflare(`${base}/workers/workers/${encodeURIComponent(workerName)}`),
|
|
2337
|
+
readCloudflare(`${script}/schedules`),
|
|
2338
|
+
readCloudflare(`${base}/workflows?page=1&per_page=100`),
|
|
2339
|
+
readCloudflare(`${base}/workflows?page=2&per_page=100`)
|
|
2340
|
+
]);
|
|
2341
|
+
const worker = record(workerValue);
|
|
2342
|
+
const subdomain = record(worker?.subdomain);
|
|
2343
|
+
const schedules = record(schedulesValue)?.schedules;
|
|
2344
|
+
if (worker?.name !== workerName || !subdomain || !desired.preserveWorkersDev && subdomain.enabled !== desired.workers_dev || !desired.preservePreviewUrls && subdomain.previews_enabled !== desired.preview_urls || !Array.isArray(schedules) || !Array.isArray(worker.references && record(worker.references)?.queues) || !Array.isArray(workflowsValue) || workflowsValue.length >= 100 || !Array.isArray(nextWorkflowsValue) || nextWorkflowsValue.length !== 0) return false;
|
|
2345
|
+
const liveCrons = schedules.map((entry) => record(entry)?.cron);
|
|
2346
|
+
if (!liveCrons.every((cron) => typeof cron === "string") || !sameStrings(liveCrons, desired.crons) || workflowsValue.some((entry) => {
|
|
2347
|
+
const workflow = record(entry);
|
|
2348
|
+
return !workflow || typeof workflow.script_name !== "string" || workflow.script_name === workerName;
|
|
2349
|
+
})) return false;
|
|
2350
|
+
const references = record(worker.references).queues;
|
|
2351
|
+
if (references.length !== queues.length) return false;
|
|
2352
|
+
const matched = /* @__PURE__ */ new Set();
|
|
2353
|
+
const consumers = await Promise.all(references.map(async (value) => {
|
|
2354
|
+
const reference = record(value);
|
|
2355
|
+
if (typeof reference?.queue_id !== "string" || typeof reference.queue_consumer_id !== "string" || typeof reference.queue_name !== "string") return null;
|
|
2356
|
+
return {
|
|
2357
|
+
queueName: reference.queue_name,
|
|
2358
|
+
consumer: await readCloudflare(`${base}/queues/${encodeURIComponent(reference.queue_id)}/consumers/${encodeURIComponent(reference.queue_consumer_id)}`)
|
|
2359
|
+
};
|
|
2360
|
+
}));
|
|
2361
|
+
for (const entry of consumers) {
|
|
2362
|
+
if (!entry || matched.has(entry.queueName)) return false;
|
|
2363
|
+
const desiredQueueConfig = queues.find((queue) => queue?.queue === entry.queueName);
|
|
2364
|
+
if (!desiredQueueConfig || !matchesQueueConsumer(entry.consumer, desiredQueueConfig, workerName)) return false;
|
|
2365
|
+
matched.add(entry.queueName);
|
|
2366
|
+
}
|
|
2367
|
+
return matched.size === queues.length;
|
|
2368
|
+
} catch {
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
//#endregion
|
|
2373
|
+
//#region src/cli/platform-zero-trust-uninstall.ts
|
|
2374
|
+
/**
|
|
2375
|
+
* Uninstall turns off the platform API, and only that API can remove the
|
|
2376
|
+
* Cloudflare Access applications project Zero Trust created. The shared
|
|
2377
|
+
* project application covers every hostname under the application domain, so
|
|
2378
|
+
* a left-over one keeps asking for sign-in on hosts Void no longer serves.
|
|
2379
|
+
* Uninstall therefore reads the saved Zero Trust state straight from
|
|
2380
|
+
* PostgreSQL, which works whether or not the API still answers, and refuses
|
|
2381
|
+
* while anything remains to clean up or the state cannot be read.
|
|
2382
|
+
*/
|
|
2383
|
+
const ZERO_TRUST_SETTING = "zeroTrust.configuration";
|
|
2384
|
+
const ZERO_TRUST_LOCK = "hashtext('zero-trust:configuration')";
|
|
2385
|
+
const UNINSTALL_SETTING = "platform.uninstall";
|
|
2386
|
+
const LISTED_APPLICATIONS = 20;
|
|
2387
|
+
function isRecord(value) {
|
|
2388
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2389
|
+
}
|
|
2390
|
+
function optionalText(value, label) {
|
|
2391
|
+
if (value === null || value === void 0) return null;
|
|
2392
|
+
if (typeof value !== "string") throw new Error(`the saved ${label} is malformed`);
|
|
2393
|
+
return value;
|
|
2394
|
+
}
|
|
2395
|
+
function savedApplication(value, label) {
|
|
2396
|
+
if (value === null || value === void 0) return null;
|
|
2397
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.name !== "string") throw new Error(`the saved ${label} is malformed`);
|
|
2398
|
+
const destinations = Array.isArray(value.destinations) ? value.destinations.filter((entry) => typeof entry === "string") : [];
|
|
2399
|
+
return {
|
|
2400
|
+
id: value.id,
|
|
2401
|
+
name: value.name,
|
|
2402
|
+
destinations
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Interpret the saved configuration and project rows. Anything that does not
|
|
2407
|
+
* have the expected shape throws, so the caller refuses instead of guessing.
|
|
2408
|
+
*/
|
|
2409
|
+
function projectZeroTrustUninstallState(setting, projectRows) {
|
|
2410
|
+
const applications = [];
|
|
2411
|
+
let active = false;
|
|
2412
|
+
let destinations = [];
|
|
2413
|
+
if (setting !== null && setting !== void 0) {
|
|
2414
|
+
if (!isRecord(setting) || typeof setting.enabled !== "boolean") throw new Error("the saved Zero Trust configuration is malformed");
|
|
2415
|
+
const operation = setting.operation ?? null;
|
|
2416
|
+
if (operation !== null && !isRecord(operation)) throw new Error("the saved Zero Trust operation is malformed");
|
|
2417
|
+
active = setting.enabled || operation !== null;
|
|
2418
|
+
const wildcard = savedApplication(setting.wildcardApplication, "shared project application");
|
|
2419
|
+
const health = savedApplication(setting.healthApplication, "health check exception");
|
|
2420
|
+
const wildcardName = optionalText(setting.wildcardApplicationName, "application name");
|
|
2421
|
+
const healthName = optionalText(setting.healthApplicationName, "application name");
|
|
2422
|
+
if (wildcard || active && wildcardName) applications.push({
|
|
2423
|
+
role: "shared project application",
|
|
2424
|
+
id: wildcard?.id ?? null,
|
|
2425
|
+
name: wildcard?.name ?? wildcardName
|
|
2426
|
+
});
|
|
2427
|
+
if (health || active && healthName) applications.push({
|
|
2428
|
+
role: "health check exception",
|
|
2429
|
+
id: health?.id ?? null,
|
|
2430
|
+
name: health?.name ?? healthName
|
|
2431
|
+
});
|
|
2432
|
+
destinations = wildcard?.destinations ?? [];
|
|
2433
|
+
}
|
|
2434
|
+
const unfinishedDeletions = [];
|
|
2435
|
+
for (const row of projectRows) {
|
|
2436
|
+
const deleting = row.project_status === "deleting";
|
|
2437
|
+
const owned = applications.length;
|
|
2438
|
+
if (row.bypass_application_id || row.bypass_application_name) applications.push({
|
|
2439
|
+
role: `public exception for ${row.project_id}`,
|
|
2440
|
+
id: row.bypass_application_id,
|
|
2441
|
+
name: row.bypass_application_name,
|
|
2442
|
+
...deleting ? { deleting } : {}
|
|
2443
|
+
});
|
|
2444
|
+
if (row.protected_application_id || row.protected_application_name) applications.push({
|
|
2445
|
+
role: `custom domain application for ${row.project_id}`,
|
|
2446
|
+
id: row.protected_application_id,
|
|
2447
|
+
name: row.protected_application_name,
|
|
2448
|
+
...deleting ? { deleting } : {}
|
|
2449
|
+
});
|
|
2450
|
+
if (deleting && applications.length > owned) unfinishedDeletions.push(row.project_slug ?? row.project_id);
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
active,
|
|
2454
|
+
destinations,
|
|
2455
|
+
applications,
|
|
2456
|
+
unfinishedDeletions
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
/** Read the project Zero Trust state from the platform database. */
|
|
2460
|
+
async function readPlatformProjectZeroTrustStateConnection(client) {
|
|
2461
|
+
const present = (await client.query(`SELECT to_regclass('public.platform_settings') IS NOT NULL AS "settings",
|
|
2462
|
+
to_regclass('public.project_zero_trust') IS NOT NULL AS "projects"`)).rows[0];
|
|
2463
|
+
if (!present) throw new Error("the database did not report its Zero Trust tables");
|
|
2464
|
+
return projectZeroTrustUninstallState(present.settings ? (await client.query("SELECT \"value\" FROM \"platform_settings\" WHERE \"key\" = $1", [ZERO_TRUST_SETTING])).rows[0]?.value : void 0, present.projects ? (await client.query(`SELECT z."project_id", p."slug" AS "project_slug", p."status" AS "project_status",
|
|
2465
|
+
z."bypass_application_id", z."bypass_application_name",
|
|
2466
|
+
z."protected_application_id", z."protected_application_name"
|
|
2467
|
+
FROM "project_zero_trust" z
|
|
2468
|
+
LEFT JOIN "projects" p ON p."id" = z."project_id"
|
|
2469
|
+
WHERE z."bypass_application_id" IS NOT NULL
|
|
2470
|
+
OR z."bypass_application_name" IS NOT NULL
|
|
2471
|
+
OR z."protected_application_id" IS NOT NULL
|
|
2472
|
+
OR z."protected_application_name" IS NOT NULL
|
|
2473
|
+
ORDER BY z."project_id"`)).rows : []);
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Take the Zero Trust configuration lock exclusively on this session. Every
|
|
2477
|
+
* API path that records or creates a Void-managed Access application holds
|
|
2478
|
+
* the same lock, so none can commit until the returned release runs.
|
|
2479
|
+
*/
|
|
2480
|
+
async function lockPlatformProjectZeroTrustConnection(client) {
|
|
2481
|
+
await client.query(`SELECT pg_advisory_lock(${ZERO_TRUST_LOCK})`);
|
|
2482
|
+
let released = false;
|
|
2483
|
+
return async () => {
|
|
2484
|
+
if (released) return;
|
|
2485
|
+
released = true;
|
|
2486
|
+
await client.query(`SELECT pg_advisory_unlock(${ZERO_TRUST_LOCK})`);
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
async function hasSettingsTable(client) {
|
|
2490
|
+
return (await client.query(`SELECT to_regclass('public.platform_settings') IS NOT NULL AS "present"`)).rows[0]?.present === true;
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Record that uninstall passed its final check. Runs while the configuration
|
|
2494
|
+
* lock is held. An API request admitted before traffic was disabled may be
|
|
2495
|
+
* waiting on that lock; once it gets the lock it reads this record and
|
|
2496
|
+
* refuses. Without the settings table the API has no Zero Trust writers.
|
|
2497
|
+
*/
|
|
2498
|
+
async function fencePlatformProjectZeroTrustConnection(client) {
|
|
2499
|
+
if (!await hasSettingsTable(client)) return;
|
|
2500
|
+
await client.query(`INSERT INTO "platform_settings" ("key", "value") VALUES ($1, $2::jsonb)
|
|
2501
|
+
ON CONFLICT ("key") DO UPDATE SET "value" = EXCLUDED."value", "updated_at" = now()`, [UNINSTALL_SETTING, JSON.stringify({ startedAt: (/* @__PURE__ */ new Date()).toISOString() })]);
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Remove the uninstall record. Uninstall does this when it stops before
|
|
2505
|
+
* disabling traffic; installing or enabling does it before traffic opens.
|
|
2506
|
+
*/
|
|
2507
|
+
async function liftPlatformProjectZeroTrustFenceConnection(client) {
|
|
2508
|
+
if (!await hasSettingsTable(client)) return;
|
|
2509
|
+
await client.query("DELETE FROM \"platform_settings\" WHERE \"key\" = $1", [UNINSTALL_SETTING]);
|
|
2510
|
+
}
|
|
2511
|
+
function formatApplications(applications) {
|
|
2512
|
+
const lines = applications.slice(0, LISTED_APPLICATIONS).map((application) => ` ${application.role}: ${application.name ?? "(name not recorded)"}${application.id ? ` (${application.id})` : ""}`);
|
|
2513
|
+
if (applications.length > LISTED_APPLICATIONS) lines.push(` …and ${applications.length - LISTED_APPLICATIONS} more`);
|
|
2514
|
+
return lines.join("\n");
|
|
2515
|
+
}
|
|
2516
|
+
const DASHBOARD_HINT = "To lift sign-in right away, delete these applications in the Cloudflare dashboard under Zero Trust → Access → Applications.";
|
|
2517
|
+
function unfinishedDeletionLines(state) {
|
|
2518
|
+
if (!state.unfinishedDeletions.length) return [];
|
|
2519
|
+
return [`Deletion of ${state.unfinishedDeletions.length === 1 ? "project" : "projects"} ${state.unfinishedDeletions.join(", ")} stopped before it removed their Access applications. Rerun \`void project delete <name>\` for each, or delete it again from the admin dashboard. This removes their applications, including ones already deleted in the Cloudflare dashboard, and Void's records of them.`];
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Refuse to uninstall while project Zero Trust still owns Access applications,
|
|
2523
|
+
* or while its state cannot be read. Runs before uninstall changes anything.
|
|
2524
|
+
*/
|
|
2525
|
+
async function assertProjectZeroTrustAllowsUninstall(read) {
|
|
2526
|
+
let state;
|
|
2527
|
+
try {
|
|
2528
|
+
if (!read) throw new Error("no platform database connection is available");
|
|
2529
|
+
state = await read();
|
|
2530
|
+
} catch (error) {
|
|
2531
|
+
const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
|
|
2532
|
+
throw new Error([
|
|
2533
|
+
`platform: Refusing to uninstall because the project Zero Trust state could not be read from the platform database: ${reason}`,
|
|
2534
|
+
"Uninstall turns off the platform API, which Void needs to remove the Cloudflare Access applications that project Zero Trust created.",
|
|
2535
|
+
"Make the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry. If project Zero Trust was ever enabled, run `void platform zero-trust disable` first."
|
|
2536
|
+
].join("\n"));
|
|
2537
|
+
}
|
|
2538
|
+
if (state.active) {
|
|
2539
|
+
const covered = state.destinations.length ? ` on ${state.destinations.join(" and ")}` : " on every hostname under the application domain";
|
|
2540
|
+
throw new Error([
|
|
2541
|
+
"platform: Refusing to uninstall while project Zero Trust is enabled or still changing.",
|
|
2542
|
+
`Uninstall turns off the platform API, so Void could no longer remove its Cloudflare Access applications, and Access would keep asking for sign-in${covered}, including hostnames Void does not serve.`,
|
|
2543
|
+
"Run `void platform zero-trust disable`, rerun it until `void platform zero-trust status` reports Zero Trust disabled with no operation running, then retry the uninstall.",
|
|
2544
|
+
"If the platform API does not answer, bring it back with `void platform enable` or `void platform repair` first.",
|
|
2545
|
+
...state.applications.length ? ["Void-managed Access applications:", formatApplications(state.applications)] : [],
|
|
2546
|
+
...unfinishedDeletionLines(state),
|
|
2547
|
+
DASHBOARD_HINT
|
|
2548
|
+
].join("\n"));
|
|
2549
|
+
}
|
|
2550
|
+
if (state.applications.length) throw new Error([
|
|
2551
|
+
"platform: Refusing to uninstall while Void-managed Access applications from project Zero Trust remain.",
|
|
2552
|
+
"Zero Trust is disabled, but it could not remove the applications of some projects. Retry the uninstall once they are gone.",
|
|
2553
|
+
"Void-managed Access applications:",
|
|
2554
|
+
formatApplications(state.applications),
|
|
2555
|
+
...state.applications.some((application) => !application.deleting) ? [`Hourly maintenance retries ${state.unfinishedDeletions.length ? "the other applications" : "them"} while the platform is enabled, and clears Void's records of any you delete in the dashboard.`] : [],
|
|
2556
|
+
...unfinishedDeletionLines(state),
|
|
2557
|
+
DASHBOARD_HINT
|
|
2558
|
+
].join("\n"));
|
|
2559
|
+
}
|
|
2560
|
+
//#endregion
|
|
2219
2561
|
//#region src/cli/platform-installer.ts
|
|
2220
2562
|
const REMOTE_INSTALLATION_MANIFEST_KEY = PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY;
|
|
2221
2563
|
const PLATFORM_WORKER_SECRET_UPDATE_NAMES = [
|
|
@@ -2530,7 +2872,13 @@ async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory
|
|
|
2530
2872
|
}
|
|
2531
2873
|
async function recoverInterruptedPlatformWorkerCutover(manifest, interruptedCutover, workerActions, configs, previousConfigs, plan, dependencies, ids, zoneId, previousPublicEndpoints, hasEmailGatewayArtifact, runPinnedPlatformWrangler, now, writeRemoteInstallationCheckpoint, onManifestChange) {
|
|
2532
2874
|
if (interruptedCutover) {
|
|
2533
|
-
for (const key of [
|
|
2875
|
+
for (const key of [
|
|
2876
|
+
"dispatch",
|
|
2877
|
+
"tail",
|
|
2878
|
+
"emailGateway",
|
|
2879
|
+
"proxy",
|
|
2880
|
+
"api"
|
|
2881
|
+
].filter((key) => interruptedCutover.staged.has(key))) {
|
|
2534
2882
|
const snapshot = interruptedCutover.staged.get(key);
|
|
2535
2883
|
const preservedRoutes = dependencies.recoverWorkerCutover ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, snapshot.workerName);
|
|
2536
2884
|
const recoveryConfig = mergePlatformWorkerTrafficConfig({
|
|
@@ -2632,7 +2980,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2632
2980
|
if (plan.actions.some((action) => action.action === "inspect")) throw new Error("platform: AI Gateway access must be verified before provisioning. Provide AI Gateway: Edit on the runtime token, then retry.");
|
|
2633
2981
|
const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
2634
2982
|
const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl ?? resolvePinnedApiBaseUrl();
|
|
2635
|
-
const runPinnedPlatformWrangler = (
|
|
2983
|
+
const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl);
|
|
2984
|
+
const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true);
|
|
2636
2985
|
const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
|
|
2637
2986
|
const runtimeDirectory = resolvedRuntime.directory;
|
|
2638
2987
|
const runtime = resolvedRuntime.manifest;
|
|
@@ -2724,6 +3073,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2724
3073
|
writeInstallationManifest(manifest);
|
|
2725
3074
|
}
|
|
2726
3075
|
assertRequiredPlatformSecrets(plan, manifest, secrets, !!dependencies.prepareAuthentication);
|
|
3076
|
+
if (hyperdriveAction?.action === "create") {
|
|
3077
|
+
parsePostgresConnectionString(secrets.databaseUrl);
|
|
3078
|
+
assertVoidCreatedHyperdriveTrustsDatabase(secrets.databaseUrl);
|
|
3079
|
+
}
|
|
2727
3080
|
const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
|
|
2728
3081
|
writePendingInstallationSecrets(manifest, {
|
|
2729
3082
|
...pendingSecrets,
|
|
@@ -2846,6 +3199,12 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2846
3199
|
manifest = transitionInstallation(manifest, "provisioning", now());
|
|
2847
3200
|
writeInstallationManifest(manifest);
|
|
2848
3201
|
}
|
|
3202
|
+
if (runtime.deploymentExecution !== 1 && (manifest.deploymentExecution === 1 || dependencies.databaseRollbackSource?.manifest.deploymentExecution === 1)) {
|
|
3203
|
+
if (!releaseDatabaseLock.checkpoint) throw new Error("platform: The database lifecycle lock cannot fence deployment admission.");
|
|
3204
|
+
await releaseDatabaseLock.checkpoint(manifest);
|
|
3205
|
+
dependencies.onProgress?.("Verifying durable deployment operations have drained");
|
|
3206
|
+
await (dependencies.verifyDeploymentExecutionDrain ?? verifyPlatformDeploymentExecutionDrain)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce), manifest.id);
|
|
3207
|
+
}
|
|
2849
3208
|
if (dependencies.prepareAuthentication) {
|
|
2850
3209
|
dependencies.onProgress?.("Preparing platform authentication");
|
|
2851
3210
|
try {
|
|
@@ -2897,6 +3256,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2897
3256
|
client: dependencies.client,
|
|
2898
3257
|
manifest,
|
|
2899
3258
|
plan,
|
|
3259
|
+
dashboardUrl: platformDashboardUrl(plan.deployment),
|
|
2900
3260
|
protection,
|
|
2901
3261
|
checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent)
|
|
2902
3262
|
});
|
|
@@ -2964,11 +3324,6 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2964
3324
|
if (targetSandboxDrainProtocol >= 1 && !prepareSandboxDrain) throw new Error("platform: The PostgreSQL lifecycle lock cannot establish the sandbox-drain safety checkpoint.");
|
|
2965
3325
|
const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 3 ? 3 : targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
|
|
2966
3326
|
if (sandboxDrainPreparation && sandboxDrainPreparation.admissionProtocol < targetSandboxDrainProtocol) throw new Error("platform: The database did not arm the Sandbox admission protocol required by this runtime.");
|
|
2967
|
-
const sandboxDrainRoutingKvId = requireId(Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id])), "routing-kv");
|
|
2968
|
-
const writeSandboxDrainMirror = async (preparation) => {
|
|
2969
|
-
await dependencies.client.putKvValue(plan.account.id, sandboxDrainRoutingKvId, PLATFORM_SANDBOX_DRAIN_KEY, preparation.mirrorValue);
|
|
2970
|
-
};
|
|
2971
|
-
if (sandboxDrainPreparation) await writeSandboxDrainMirror(sandboxDrainPreparation);
|
|
2972
3327
|
const sandboxDrainState = sandboxDrainPreparation?.state ?? "complete";
|
|
2973
3328
|
if (targetSandboxDrainProtocol >= 1 && (manifest.safetyProtocols?.sandboxDrain ?? 0) < targetSandboxDrainProtocol) {
|
|
2974
3329
|
manifest = {
|
|
@@ -3027,6 +3382,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3027
3382
|
accountId: plan.account.id,
|
|
3028
3383
|
apiUrl: plan.apiUrl,
|
|
3029
3384
|
deployment: plan.deployment,
|
|
3385
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
3030
3386
|
healthProbeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
3031
3387
|
installationId: manifest.id,
|
|
3032
3388
|
ownershipNonce: manifest.ownershipNonce,
|
|
@@ -3075,7 +3431,59 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3075
3431
|
};
|
|
3076
3432
|
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
3077
3433
|
}
|
|
3078
|
-
|
|
3434
|
+
const parallelWorkerUploads = !dependencies.deployWorker && !requiresStagedCommit && !workerSecretUpdateIntent && !recoveringUnsafeTraffic && workerActions.length === workerKeys.length && workerActions.every((action) => action.action === "update");
|
|
3435
|
+
let workerManifestQueue = Promise.resolve();
|
|
3436
|
+
let workerManifestFailed = false;
|
|
3437
|
+
let workerManifestFailure;
|
|
3438
|
+
const checkpointWorkerManifest = (update, safetyRecovery = false) => {
|
|
3439
|
+
if (!parallelWorkerUploads) return update();
|
|
3440
|
+
const result = workerManifestQueue.then(async () => {
|
|
3441
|
+
if (workerManifestFailed && !safetyRecovery) throw workerManifestFailure;
|
|
3442
|
+
try {
|
|
3443
|
+
await update();
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
workerManifestFailed = true;
|
|
3446
|
+
workerManifestFailure = error;
|
|
3447
|
+
throw error;
|
|
3448
|
+
}
|
|
3449
|
+
});
|
|
3450
|
+
workerManifestQueue = result.then(() => void 0, () => void 0);
|
|
3451
|
+
return result;
|
|
3452
|
+
};
|
|
3453
|
+
let availableUploadSlots = 2;
|
|
3454
|
+
let uploadFailure;
|
|
3455
|
+
const uploadWaiters = [];
|
|
3456
|
+
const acquireUploadSlot = async () => {
|
|
3457
|
+
if (uploadFailure !== void 0) throw uploadFailure;
|
|
3458
|
+
if (availableUploadSlots === 0) await new Promise((resolve, reject) => uploadWaiters.push({
|
|
3459
|
+
resolve,
|
|
3460
|
+
reject
|
|
3461
|
+
}));
|
|
3462
|
+
else availableUploadSlots--;
|
|
3463
|
+
return (failure) => {
|
|
3464
|
+
if (failure !== void 0 && uploadFailure === void 0) {
|
|
3465
|
+
uploadFailure = failure;
|
|
3466
|
+
for (const waiter of uploadWaiters.splice(0)) waiter.reject(failure);
|
|
3467
|
+
}
|
|
3468
|
+
const next = uploadFailure === void 0 ? uploadWaiters.shift() : void 0;
|
|
3469
|
+
if (next) next.resolve();
|
|
3470
|
+
else availableUploadSlots++;
|
|
3471
|
+
};
|
|
3472
|
+
};
|
|
3473
|
+
const workerReady = (parallelWorkerUploads ? workerKeys : []).map(() => {
|
|
3474
|
+
let resolve;
|
|
3475
|
+
let reject;
|
|
3476
|
+
return {
|
|
3477
|
+
promise: new Promise((onResolve, onReject) => {
|
|
3478
|
+
resolve = onResolve;
|
|
3479
|
+
reject = onReject;
|
|
3480
|
+
}),
|
|
3481
|
+
resolve,
|
|
3482
|
+
reject
|
|
3483
|
+
};
|
|
3484
|
+
});
|
|
3485
|
+
const uploadBatch = Promise.all(workerReady.map((ready) => ready.promise)).then(() => ({ error: null }), (error) => ({ error }));
|
|
3486
|
+
const deployOneWorker = async (key, waitForPriorWorker, onReadyForActivation) => {
|
|
3079
3487
|
dependencies.onProgress?.(`Deploying the ${key} service`);
|
|
3080
3488
|
let workerStepStarted = performance.now();
|
|
3081
3489
|
const markWorkerStep = (step) => {
|
|
@@ -3084,12 +3492,14 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3084
3492
|
};
|
|
3085
3493
|
const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
|
|
3086
3494
|
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
manifest
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3495
|
+
await checkpointWorkerManifest(async () => {
|
|
3496
|
+
const intended = recordProvisionIntent(manifest, action, now());
|
|
3497
|
+
if (intended !== manifest) {
|
|
3498
|
+
manifest = intended;
|
|
3499
|
+
writeInstallationManifest(manifest);
|
|
3500
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3093
3503
|
await dependencies.revalidateBeforeWorkerDeploy(manifest, action);
|
|
3094
3504
|
markWorkerStep("revalidate");
|
|
3095
3505
|
const preservedRoutes = dependencies.deployWorker ? [] : action.action === "create" ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
|
|
@@ -3098,29 +3508,40 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3098
3508
|
const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
|
|
3099
3509
|
const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
|
|
3100
3510
|
apiBaseUrl: cloudflareApiBaseUrl,
|
|
3511
|
+
...parallelWorkerUploads ? {
|
|
3512
|
+
acquireUploadSlot,
|
|
3513
|
+
onReadyForActivation,
|
|
3514
|
+
waitForPriorWorker
|
|
3515
|
+
} : {},
|
|
3101
3516
|
...dependencies.client.request ? { readCloudflare: (path) => dependencies.client.request(path) } : {},
|
|
3102
3517
|
onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3518
|
+
await checkpointWorkerManifest(async () => {
|
|
3519
|
+
manifest = recordWorkerCutoverStage(manifest, workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
|
|
3520
|
+
writeInstallationManifest(manifest);
|
|
3521
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3522
|
+
});
|
|
3106
3523
|
},
|
|
3107
3524
|
onTrafficPublished: async () => {
|
|
3108
3525
|
if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
|
|
3109
3526
|
const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
manifest
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3527
|
+
await checkpointWorkerManifest(async () => {
|
|
3528
|
+
const next = recordPlatformWorkerTraffic(manifest, action.name, {
|
|
3529
|
+
customDomains,
|
|
3530
|
+
routes
|
|
3531
|
+
}, now());
|
|
3532
|
+
if (JSON.stringify(next.resources) !== JSON.stringify(manifest.resources)) {
|
|
3533
|
+
manifest = next;
|
|
3534
|
+
writeInstallationManifest(manifest);
|
|
3535
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3536
|
+
}
|
|
3537
|
+
});
|
|
3119
3538
|
},
|
|
3120
3539
|
onUnsafeTraffic: async () => {
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3540
|
+
await checkpointWorkerManifest(async () => {
|
|
3541
|
+
manifest = recordWorkerUnsafeTraffic(manifest, workerCutoverGeneration, key, action.name, now());
|
|
3542
|
+
writeInstallationManifest(manifest);
|
|
3543
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3544
|
+
}, true);
|
|
3124
3545
|
},
|
|
3125
3546
|
managedRoutes: preserveDisabled ? [] : managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
|
|
3126
3547
|
preservedRoutes,
|
|
@@ -3140,22 +3561,40 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3140
3561
|
retainNewWorker: async () => {
|
|
3141
3562
|
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
3142
3563
|
},
|
|
3143
|
-
runner: runPinnedPlatformWrangler,
|
|
3564
|
+
runner: parallelWorkerUploads ? runParallelPinnedPlatformWrangler : runPinnedPlatformWrangler,
|
|
3144
3565
|
workerExisted: action.action !== "create"
|
|
3145
3566
|
});
|
|
3146
3567
|
markWorkerStep("deploy");
|
|
3147
3568
|
if (rollback) workerRollbacks.push(rollback);
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3569
|
+
await checkpointWorkerManifest(async () => {
|
|
3570
|
+
const recorded = recordResource(manifest, action, action.name, "created", now());
|
|
3571
|
+
if (rollback || dependencies.deployWorker || JSON.stringify(recorded.resources) !== JSON.stringify(manifest.resources)) {
|
|
3572
|
+
manifest = recorded;
|
|
3573
|
+
writeInstallationManifest(manifest);
|
|
3574
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3575
|
+
}
|
|
3576
|
+
});
|
|
3154
3577
|
markWorkerStep("record");
|
|
3155
|
-
}
|
|
3578
|
+
};
|
|
3579
|
+
if (parallelWorkerUploads) {
|
|
3580
|
+
const tasks = [];
|
|
3581
|
+
for (const [index, key] of workerKeys.entries()) {
|
|
3582
|
+
const priorWorker = tasks.at(-1);
|
|
3583
|
+
const ready = workerReady[index];
|
|
3584
|
+
const task = deployOneWorker(key, priorWorker ? () => priorWorker : void 0, async () => {
|
|
3585
|
+
ready.resolve();
|
|
3586
|
+
const batch = await uploadBatch;
|
|
3587
|
+
if (batch.error !== null) throw batch.error;
|
|
3588
|
+
});
|
|
3589
|
+
task.then(() => ready.resolve(), (error) => ready.reject(error));
|
|
3590
|
+
tasks.push(task);
|
|
3591
|
+
}
|
|
3592
|
+
const firstFailure = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
|
|
3593
|
+
if (firstFailure) throw firstFailure.reason;
|
|
3594
|
+
} else for (const key of workerKeys) await deployOneWorker(key);
|
|
3156
3595
|
manifest = setInstallationEndpoints(manifest, {
|
|
3157
3596
|
apiUrl: plan.apiUrl,
|
|
3158
|
-
dashboardUrl:
|
|
3597
|
+
dashboardUrl: platformDashboardUrl(plan.deployment),
|
|
3159
3598
|
proxyUrl: plan.proxyUrl
|
|
3160
3599
|
}, now());
|
|
3161
3600
|
if (preserveDisabled) {
|
|
@@ -3166,6 +3605,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3166
3605
|
manifest = {
|
|
3167
3606
|
...manifest,
|
|
3168
3607
|
deployment: plan.deployment,
|
|
3608
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
3169
3609
|
platformVersion: version,
|
|
3170
3610
|
runtime: runtimeProvenance,
|
|
3171
3611
|
updatedAt: now(),
|
|
@@ -3264,9 +3704,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3264
3704
|
zoneNameServers: zone?.nameServers ?? []
|
|
3265
3705
|
};
|
|
3266
3706
|
}
|
|
3267
|
-
|
|
3268
|
-
if (completedDrain.state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
|
|
3269
|
-
await writeSandboxDrainMirror(completedDrain);
|
|
3707
|
+
if ((await prepareSandboxDrain()).state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
|
|
3270
3708
|
}
|
|
3271
3709
|
if (requiresStagedCommit) {
|
|
3272
3710
|
if (!authenticationCutoverCommitted) {
|
|
@@ -3338,6 +3776,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3338
3776
|
retainNewWorker: async () => {
|
|
3339
3777
|
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
3340
3778
|
},
|
|
3779
|
+
reuseMatchingVersion: false,
|
|
3341
3780
|
runner: runPinnedPlatformWrangler,
|
|
3342
3781
|
workerExisted: true
|
|
3343
3782
|
});
|
|
@@ -3361,6 +3800,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3361
3800
|
dependencies.onProgress?.("Preparing shared email routing");
|
|
3362
3801
|
emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce));
|
|
3363
3802
|
}
|
|
3803
|
+
await releaseDatabaseLock.liftProjectZeroTrustFence?.();
|
|
3364
3804
|
if (dependencies.client.deleteKvValue) await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
|
|
3365
3805
|
else if (wasDisabled || !previousPublicEndpoints) throw new Error("platform: Routing disablement removal is unavailable.");
|
|
3366
3806
|
if (requiresStagedCommit) {
|
|
@@ -3375,6 +3815,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3375
3815
|
manifest = {
|
|
3376
3816
|
...manifest,
|
|
3377
3817
|
deployment: plan.deployment,
|
|
3818
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
3378
3819
|
platformVersion: version,
|
|
3379
3820
|
runtime: runtimeProvenance,
|
|
3380
3821
|
safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
|
|
@@ -3747,6 +4188,14 @@ function assertRequiredPlatformSecrets(plan, manifest, secrets, authenticationWi
|
|
|
3747
4188
|
}
|
|
3748
4189
|
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.`);
|
|
3749
4190
|
}
|
|
4191
|
+
/**
|
|
4192
|
+
* A Hyperdrive that Void creates validates the server against public certificate authorities
|
|
4193
|
+
* only, so it cannot honor a private CA that the local database client would trust.
|
|
4194
|
+
*/
|
|
4195
|
+
function assertVoidCreatedHyperdriveTrustsDatabase(databaseUrl) {
|
|
4196
|
+
const sslrootcert = new URL(databaseUrl).searchParams.getAll("sslrootcert").at(-1);
|
|
4197
|
+
if (sslrootcert && sslrootcert !== "system") throw new Error("platform: A Hyperdrive created by Void trusts only public certificate authorities, so it cannot use the certificate authority in DATABASE_URL sslrootcert. If the database certificate comes from a public certificate authority, use sslrootcert=system. Otherwise create a separately managed Hyperdrive that trusts it, then select it with \"Use an existing Hyperdrive\" when you start a new installation, or set VOID_PLATFORM_HYPERDRIVE_ID, VOID_PLATFORM_HYPERDRIVE_ORIGIN_HOST, and VOID_PLATFORM_HYPERDRIVE_ORIGIN_USER.");
|
|
4198
|
+
}
|
|
3750
4199
|
/** Make pg's current strict TLS behavior explicit without rewriting saved connection details. */
|
|
3751
4200
|
function platformDatabaseClientUrl(databaseUrl) {
|
|
3752
4201
|
let url;
|
|
@@ -3755,7 +4204,13 @@ function platformDatabaseClientUrl(databaseUrl) {
|
|
|
3755
4204
|
} catch {
|
|
3756
4205
|
return databaseUrl;
|
|
3757
4206
|
}
|
|
3758
|
-
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:"
|
|
4207
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return databaseUrl;
|
|
4208
|
+
if (url.searchParams.getAll("sslrootcert").at(-1) === "system") {
|
|
4209
|
+
url.searchParams.delete("sslrootcert");
|
|
4210
|
+
url.searchParams.set("sslmode", "verify-full");
|
|
4211
|
+
return url.toString();
|
|
4212
|
+
}
|
|
4213
|
+
if (url.searchParams.getAll("sslmode").at(-1) !== "require" || url.searchParams.getAll("uselibpqcompat").at(-1) === "true") return databaseUrl;
|
|
3759
4214
|
url.searchParams.set("sslmode", "verify-full");
|
|
3760
4215
|
return url.toString();
|
|
3761
4216
|
}
|
|
@@ -3812,6 +4267,21 @@ async function readPlatformDatabaseLifecycleManifest(databaseUrl, installationId
|
|
|
3812
4267
|
await pool.end();
|
|
3813
4268
|
}
|
|
3814
4269
|
}
|
|
4270
|
+
/** Read project Zero Trust state for a plan, which holds no lifecycle lock. */
|
|
4271
|
+
async function readPlatformProjectZeroTrustState(databaseUrl, installationId, ownershipNonce) {
|
|
4272
|
+
if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
|
|
4273
|
+
const { Pool } = await import("pg");
|
|
4274
|
+
const pool = new Pool({
|
|
4275
|
+
connectionString: platformDatabaseClientUrl(databaseUrl),
|
|
4276
|
+
max: 1
|
|
4277
|
+
});
|
|
4278
|
+
try {
|
|
4279
|
+
await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
|
|
4280
|
+
return await readPlatformProjectZeroTrustStateConnection(pool);
|
|
4281
|
+
} finally {
|
|
4282
|
+
await pool.end();
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
3815
4285
|
async function inspectPlatformDatabaseStateConnection(client, database) {
|
|
3816
4286
|
const relation = await client.query(`SELECT to_regclass('"drizzle"."__drizzle_migrations"')::text AS "relation"`);
|
|
3817
4287
|
if (relation.rows.length === 0 || relation.rows[0]?.relation === null) return {
|
|
@@ -3939,6 +4409,10 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
|
|
|
3939
4409
|
protection: parsePlatformAccessProtection(settings.protection)
|
|
3940
4410
|
} : null;
|
|
3941
4411
|
};
|
|
4412
|
+
release.readProjectZeroTrust = () => readPlatformProjectZeroTrustStateConnection(client);
|
|
4413
|
+
release.lockProjectZeroTrust = () => lockPlatformProjectZeroTrustConnection(client);
|
|
4414
|
+
release.fenceProjectZeroTrust = () => fencePlatformProjectZeroTrustConnection(client);
|
|
4415
|
+
release.liftProjectZeroTrustFence = () => liftPlatformProjectZeroTrustFenceConnection(client);
|
|
3942
4416
|
release.requiredSafetyProtocols = await readRequiredPlatformSafetyProtocols(client);
|
|
3943
4417
|
release.checkpoint = async (manifest) => {
|
|
3944
4418
|
if (released) throw new Error("platform: Cannot checkpoint lifecycle state after releasing its lock.");
|
|
@@ -3980,14 +4454,6 @@ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionPr
|
|
|
3980
4454
|
return {
|
|
3981
4455
|
admissionProtocol: row.admission_protocol,
|
|
3982
4456
|
generation: row.generation,
|
|
3983
|
-
mirrorValue: JSON.stringify({
|
|
3984
|
-
version: 1,
|
|
3985
|
-
generation: row.generation,
|
|
3986
|
-
state: row.state,
|
|
3987
|
-
startedAt: row.started_at,
|
|
3988
|
-
updatedAt: row.updated_at,
|
|
3989
|
-
...row.completed_at ? { completedAt: row.completed_at } : {}
|
|
3990
|
-
}),
|
|
3991
4457
|
state: row.state === "complete" ? "complete" : "pending"
|
|
3992
4458
|
};
|
|
3993
4459
|
};
|
|
@@ -4154,11 +4620,12 @@ let cachedPlatformUploaderHash;
|
|
|
4154
4620
|
function platformWorkerUploaderHash() {
|
|
4155
4621
|
if (cachedPlatformUploaderHash !== void 0) return cachedPlatformUploaderHash;
|
|
4156
4622
|
try {
|
|
4157
|
-
const launcher =
|
|
4623
|
+
const launcher = resolveCloudflareOperationArgv(process.cwd())[1];
|
|
4158
4624
|
if (!launcher) return cachedPlatformUploaderHash = null;
|
|
4159
4625
|
const digest = createHash("sha256");
|
|
4160
4626
|
digest.update("platform-uploader:v1\0");
|
|
4161
4627
|
digest.update(process.version);
|
|
4628
|
+
digest.update(readFileSync(launcher));
|
|
4162
4629
|
digest.update(readFileSync(resolve(dirname(launcher), "..", "..", "package.json")));
|
|
4163
4630
|
const cfDirectory = dirname(fileURLToPath(import.meta.resolve("cf/package.json")));
|
|
4164
4631
|
digest.update(readFileSync(join(cfDirectory, "package.json")));
|
|
@@ -4268,32 +4735,34 @@ function snapshotPlatformWorkerUploadInputs(config, directory) {
|
|
|
4268
4735
|
}
|
|
4269
4736
|
function platformWorkerApiReadRunner(accountId, workerName, readCloudflare, fallback) {
|
|
4270
4737
|
const base = `/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}`;
|
|
4271
|
-
return async (
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
return {
|
|
4738
|
+
return async (operation, account, cwd, capture) => {
|
|
4739
|
+
try {
|
|
4740
|
+
if (operation.type === "deployments.status" || operation.type === "deployments.list") {
|
|
4741
|
+
const response = await readCloudflare(`${base}/deployments`);
|
|
4742
|
+
const deployments = response && typeof response === "object" && !Array.isArray(response) ? response.deployments : null;
|
|
4743
|
+
if (!Array.isArray(deployments)) throw new Error(`platform: Cloudflare returned malformed deployment history for ${workerName}.`);
|
|
4744
|
+
return {
|
|
4745
|
+
status: operation.type === "deployments.status" && deployments.length === 0 ? 1 : 0,
|
|
4746
|
+
stderr: "",
|
|
4747
|
+
stdout: "",
|
|
4748
|
+
data: operation.type === "deployments.status" ? deployments[0] : deployments
|
|
4749
|
+
};
|
|
4750
|
+
}
|
|
4751
|
+
if (operation.type === "versions.view") return {
|
|
4285
4752
|
status: 0,
|
|
4286
4753
|
stderr: "",
|
|
4287
|
-
stdout:
|
|
4754
|
+
stdout: "",
|
|
4755
|
+
data: await readCloudflare(`${base}/versions/${encodeURIComponent(operation.versionId)}`)
|
|
4288
4756
|
};
|
|
4289
|
-
}
|
|
4290
|
-
return fallback(
|
|
4757
|
+
} catch {}
|
|
4758
|
+
return fallback(operation, account, cwd, capture);
|
|
4291
4759
|
};
|
|
4292
4760
|
}
|
|
4293
4761
|
async function deployPlatformWorker(config, secrets, options) {
|
|
4294
4762
|
const directory = mkdtempSync(join(tmpdir(), "void-platform-deploy-"));
|
|
4295
4763
|
const workerName = String(config.name);
|
|
4296
4764
|
const accountId = String(config.account_id);
|
|
4765
|
+
const readRunner = options.readCloudflare ? platformWorkerApiReadRunner(accountId, workerName, options.readCloudflare, options.runner) : options.runner;
|
|
4297
4766
|
let activatedVersionId = null;
|
|
4298
4767
|
let activatedDeploymentId = null;
|
|
4299
4768
|
let uploadedVersionId = null;
|
|
@@ -4305,13 +4774,22 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4305
4774
|
let uploadConfig = withoutPlatformWorkerTrafficAttachments(nextTraffic.wranglerConfig);
|
|
4306
4775
|
const nextTriggers = readTriggerConfigFromObject(nextTraffic.wranglerConfig);
|
|
4307
4776
|
const contentHash = secrets === null ? platformWorkerUploadFingerprint(config, options.managedRoutes) : null;
|
|
4777
|
+
const reuseMatchingVersion = options.reuseMatchingVersion !== false;
|
|
4308
4778
|
let previousTriggers = readTriggerConfigFromObject(previousTraffic.wranglerConfig);
|
|
4309
4779
|
try {
|
|
4310
4780
|
const configPath = join(directory, "wrangler.json");
|
|
4311
4781
|
writeFileSync(configPath, `${JSON.stringify(uploadConfig, null, 2)}\n`, { mode: 384 });
|
|
4312
4782
|
const reconcileExistingVersion = async (versionId, triggerConfig, runner, deploymentId) => {
|
|
4313
4783
|
try {
|
|
4314
|
-
|
|
4784
|
+
if (deploymentId && options.readCloudflare ? await platformWorkerLiveTriggersMatch({
|
|
4785
|
+
accountId,
|
|
4786
|
+
workerName,
|
|
4787
|
+
desired: nextTriggers,
|
|
4788
|
+
readCloudflare: options.readCloudflare
|
|
4789
|
+
}) : false) {
|
|
4790
|
+
const beforeTraffic = await readPlatformWorkerDeployments(configPath, accountId, workerName, runner);
|
|
4791
|
+
if (beforeTraffic[0]?.id !== deploymentId || !deploymentRunsOnlyVersion(beforeTraffic[0], versionId)) throw new Error(`platform: Worker ${workerName} changed deployment lineage during trigger verification.`);
|
|
4792
|
+
} else assertPlatformTriggerStabilization(await stabilizeCloudflareVersionTriggers({
|
|
4315
4793
|
configPath,
|
|
4316
4794
|
knownTriggerConfigs: { [versionId]: nextTriggers },
|
|
4317
4795
|
manageRoutes: false,
|
|
@@ -4344,16 +4822,15 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4344
4822
|
throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} traffic reconciliation failed: ${errorMessage(error)}${failures.length > 0 ? ` Safety recovery also failed: ${failures.join("; ")}` : ""}`, { cause: error });
|
|
4345
4823
|
}
|
|
4346
4824
|
};
|
|
4347
|
-
if (options.workerExisted && contentHash && options.readCloudflare) {
|
|
4348
|
-
const directRunner = platformWorkerApiReadRunner(accountId, workerName, options.readCloudflare, options.runner);
|
|
4825
|
+
if (options.workerExisted && contentHash && reuseMatchingVersion && options.readCloudflare) {
|
|
4349
4826
|
let candidate;
|
|
4350
4827
|
let changedDuringRead = false;
|
|
4351
4828
|
try {
|
|
4352
|
-
const before = await readPlatformWorkerDeployments(configPath, accountId, workerName,
|
|
4829
|
+
const before = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4353
4830
|
const activeVersion = before[0]?.versions.length === 1 && before[0].versions[0]?.percentage === 100 ? before[0].versions[0].versionId : null;
|
|
4354
4831
|
if (!activeVersion) throw new Error(`platform: Existing Worker ${workerName} has no single active production version.`);
|
|
4355
|
-
const snapshot = await readPlatformWorkerVersionSnapshot(configPath, accountId, workerName, readPlatformWorkerOwnershipMarker(config), activeVersion,
|
|
4356
|
-
const after = await readPlatformWorkerDeployments(configPath, accountId, workerName,
|
|
4832
|
+
const snapshot = await readPlatformWorkerVersionSnapshot(configPath, accountId, workerName, readPlatformWorkerOwnershipMarker(config), activeVersion, readRunner);
|
|
4833
|
+
const after = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4357
4834
|
changedDuringRead = before[0]?.id !== after[0]?.id || !deploymentRunsOnlyVersion(after[0], snapshot.versionId);
|
|
4358
4835
|
if (!changedDuringRead && snapshot.contentHash === contentHash) candidate = {
|
|
4359
4836
|
deploymentId: after[0].id,
|
|
@@ -4363,16 +4840,24 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4363
4840
|
if (error instanceof Error && (error.message.includes("changed concurrently") || error.message.includes("does not belong to this installation"))) throw error;
|
|
4364
4841
|
}
|
|
4365
4842
|
if (changedDuringRead) throw new Error(`platform: Worker ${workerName} changed concurrently while its deployment lineage was inspected.`);
|
|
4366
|
-
if (candidate && platformWorkerUploadFingerprint(config, options.managedRoutes) === contentHash)
|
|
4843
|
+
if (candidate && platformWorkerUploadFingerprint(config, options.managedRoutes) === contentHash) {
|
|
4844
|
+
await options.onReadyForActivation?.();
|
|
4845
|
+
await options.waitForPriorWorker?.();
|
|
4846
|
+
return await reconcileExistingVersion(candidate.snapshot.versionId, candidate.snapshot.triggerConfig, readRunner, candidate.deploymentId);
|
|
4847
|
+
}
|
|
4367
4848
|
}
|
|
4368
4849
|
if (options.workerExisted) {
|
|
4369
|
-
const previous = await readActivePlatformWorkerSnapshot(configPath, accountId, workerName, readPlatformWorkerOwnershipMarker(config),
|
|
4850
|
+
const previous = await readActivePlatformWorkerSnapshot(configPath, accountId, workerName, readPlatformWorkerOwnershipMarker(config), readRunner);
|
|
4370
4851
|
previousVersionId = previous.versionId;
|
|
4371
|
-
const deployments = await readPlatformWorkerDeployments(configPath, accountId, workerName,
|
|
4852
|
+
const deployments = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4372
4853
|
if (!deploymentRunsOnlyVersion(deployments[0], previousVersionId)) throw new Error(`platform: Worker ${workerName} changed concurrently while its deployment lineage was inspected.`);
|
|
4373
4854
|
previousDeployment = deployments[0];
|
|
4374
4855
|
previousDeploymentId = previousDeployment.id;
|
|
4375
|
-
if (contentHash && previous.contentHash === contentHash && platformWorkerUploadFingerprint(config, options.managedRoutes) === contentHash)
|
|
4856
|
+
if (contentHash && reuseMatchingVersion && previous.contentHash === contentHash && platformWorkerUploadFingerprint(config, options.managedRoutes) === contentHash) {
|
|
4857
|
+
await options.onReadyForActivation?.();
|
|
4858
|
+
await options.waitForPriorWorker?.();
|
|
4859
|
+
return await reconcileExistingVersion(previousVersionId, previous.triggerConfig, readRunner);
|
|
4860
|
+
}
|
|
4376
4861
|
uploadConfig = withoutCompletedPlatformWorkerMigrations(uploadConfig, previous.migrationTag);
|
|
4377
4862
|
writeFileSync(configPath, `${JSON.stringify(uploadConfig, null, 2)}\n`, { mode: 384 });
|
|
4378
4863
|
previousTriggers = resolvePlatformPreviousTriggerConfig(withoutPlatformCustomDomainTriggers(previous.triggerConfig), previousTriggers, options.previousPublicEndpoints);
|
|
@@ -4388,44 +4873,50 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4388
4873
|
};
|
|
4389
4874
|
writeFileSync(configPath, `${JSON.stringify(uploadConfig, null, 2)}\n`, { mode: 384 });
|
|
4390
4875
|
}
|
|
4391
|
-
const
|
|
4392
|
-
|
|
4393
|
-
"--config",
|
|
4876
|
+
const operation = {
|
|
4877
|
+
type: options.workerExisted ? "versions.upload" : "worker.deploy",
|
|
4394
4878
|
configPath,
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
"--experimental-auto-create=false",
|
|
4398
|
-
...!options.workerExisted ? ["--autoconfig=false"] : [],
|
|
4399
|
-
"--message",
|
|
4400
|
-
encodeCloudflareVersionMessage([], nextTriggers, uploadedContentHash ?? void 0)
|
|
4401
|
-
];
|
|
4879
|
+
message: encodeCloudflareVersionMessage([], nextTriggers, uploadedContentHash ?? void 0)
|
|
4880
|
+
};
|
|
4402
4881
|
if (secrets) {
|
|
4403
4882
|
const secretsPath = join(directory, "secrets.json");
|
|
4404
4883
|
writeFileSync(secretsPath, JSON.stringify(secrets), { mode: 384 });
|
|
4405
|
-
|
|
4884
|
+
operation.secretsFilePath = secretsPath;
|
|
4406
4885
|
}
|
|
4407
|
-
const
|
|
4408
|
-
|
|
4886
|
+
const releaseUploadSlot = await options.acquireUploadSlot?.();
|
|
4887
|
+
let uploaded;
|
|
4888
|
+
let uploadError;
|
|
4889
|
+
try {
|
|
4890
|
+
uploaded = options.workerExisted ? await options.runner(operation, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(operation, accountId, directory, true, workerName, options.apiBaseUrl);
|
|
4891
|
+
assertCleanPlatformWrangler(uploaded, `${options.workerExisted ? "upload" : "bootstrap"} ${workerName}`);
|
|
4892
|
+
} catch (error) {
|
|
4893
|
+
uploadError = error;
|
|
4894
|
+
throw error;
|
|
4895
|
+
} finally {
|
|
4896
|
+
releaseUploadSlot?.(uploadError);
|
|
4897
|
+
}
|
|
4898
|
+
const uploadData = uploaded.data;
|
|
4899
|
+
uploadedVersionId = typeof uploadData?.versionId === "string" ? uploadData.versionId : null;
|
|
4409
4900
|
if (!options.workerExisted) activatedVersionId = uploadedVersionId;
|
|
4410
|
-
assertCleanPlatformWrangler(uploaded, `${options.workerExisted ? "upload" : "bootstrap"} ${workerName}`);
|
|
4411
4901
|
if (!uploadedVersionId) throw new Error(`platform: Wrangler uploaded ${workerName} without reporting its immutable version id.`);
|
|
4412
4902
|
await options.onStaged(previousVersionId, uploadedVersionId, previousTriggers);
|
|
4903
|
+
await options.onReadyForActivation?.();
|
|
4904
|
+
await options.waitForPriorWorker?.();
|
|
4413
4905
|
if (options.workerExisted) {
|
|
4414
|
-
const deploymentsBeforeActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName,
|
|
4906
|
+
const deploymentsBeforeActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4415
4907
|
if (deploymentsBeforeActivation[0]?.id !== previousDeploymentId || !deploymentRunsOnlyVersion(deploymentsBeforeActivation[0], previousVersionId)) throw new Error(`platform: Worker ${workerName} changed concurrently before activation; refusing to overwrite it.`);
|
|
4416
4908
|
activatedVersionId = uploadedVersionId;
|
|
4417
|
-
assertCleanPlatformWrangler(await options.runner(
|
|
4418
|
-
"versions",
|
|
4419
|
-
"deploy",
|
|
4420
|
-
`${uploadedVersionId}@100`,
|
|
4421
|
-
"--config",
|
|
4909
|
+
assertCleanPlatformWrangler(await options.runner({
|
|
4910
|
+
type: "versions.deploy",
|
|
4422
4911
|
configPath,
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4912
|
+
versions: [{
|
|
4913
|
+
versionId: uploadedVersionId,
|
|
4914
|
+
percentage: 100
|
|
4915
|
+
}],
|
|
4916
|
+
message: `Void platform activation ${uploadedVersionId}`
|
|
4917
|
+
}, accountId, directory, true), `activate ${workerName}`);
|
|
4918
|
+
}
|
|
4919
|
+
const deploymentsAfterActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4429
4920
|
const lineage = classifyPlatformWorkerDeploymentLineage(deploymentsAfterActivation, uploadedVersionId, previousDeploymentId);
|
|
4430
4921
|
if (lineage.kind === "concurrent-after") {
|
|
4431
4922
|
activatedVersionId = null;
|
|
@@ -4445,7 +4936,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4445
4936
|
expectedActiveDeploymentId: lineage.deploymentId,
|
|
4446
4937
|
expectedActiveVersionId: uploadedVersionId,
|
|
4447
4938
|
restoration: interveningDeployment,
|
|
4448
|
-
runner:
|
|
4939
|
+
runner: readRunner,
|
|
4449
4940
|
workerName
|
|
4450
4941
|
});
|
|
4451
4942
|
} catch (recoveryError) {
|
|
@@ -4471,7 +4962,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4471
4962
|
await options.removeCustomDomains();
|
|
4472
4963
|
return cleanPlatformCommandResult();
|
|
4473
4964
|
},
|
|
4474
|
-
runWrangler: platformWorkerCommandRunner(accountId, directory,
|
|
4965
|
+
runWrangler: platformWorkerCommandRunner(accountId, directory, readRunner),
|
|
4475
4966
|
targetTriggerConfig: nextTriggers,
|
|
4476
4967
|
targetVersionId: uploadedVersionId,
|
|
4477
4968
|
workerName
|
|
@@ -4495,7 +4986,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4495
4986
|
removeCustomDomains: options.removeCustomDomains,
|
|
4496
4987
|
publishCustomDomains: options.publishCustomDomains,
|
|
4497
4988
|
retainNewWorker: options.retainNewWorker,
|
|
4498
|
-
runner:
|
|
4989
|
+
runner: readRunner,
|
|
4499
4990
|
workerExisted: options.workerExisted,
|
|
4500
4991
|
workerName
|
|
4501
4992
|
});
|
|
@@ -4522,7 +5013,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4522
5013
|
removeCustomDomains: options.removeCustomDomains,
|
|
4523
5014
|
publishCustomDomains: options.publishCustomDomains,
|
|
4524
5015
|
retainNewWorker: options.retainNewWorker,
|
|
4525
|
-
runner:
|
|
5016
|
+
runner: readRunner,
|
|
4526
5017
|
workerExisted: options.workerExisted,
|
|
4527
5018
|
workerName
|
|
4528
5019
|
}) : null,
|
|
@@ -4617,27 +5108,24 @@ async function rollbackPlatformWorker(input) {
|
|
|
4617
5108
|
}
|
|
4618
5109
|
}
|
|
4619
5110
|
function platformWorkerCommandRunner(accountId, cwd, runner) {
|
|
4620
|
-
return (
|
|
5111
|
+
return (operation, options) => runner(operation, accountId, cwd, options.capture);
|
|
4621
5112
|
}
|
|
4622
|
-
async function runPlatformWrangler(
|
|
4623
|
-
return runPlatformWranglerAtApiBase(
|
|
5113
|
+
async function runPlatformWrangler(operation, accountId, cwd, capture) {
|
|
5114
|
+
return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
|
|
4624
5115
|
}
|
|
4625
5116
|
const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
|
|
4626
|
-
async function runPlatformWranglerAtApiBase(
|
|
5117
|
+
async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false) {
|
|
4627
5118
|
const startedAt = performance.now();
|
|
4628
5119
|
const result = await createDefaultRunWrangler(process.cwd(), {
|
|
4629
|
-
launcher:
|
|
5120
|
+
launcher: resolveCloudflareOperationArgv(process.cwd()),
|
|
5121
|
+
parallelWorkerUpload,
|
|
4630
5122
|
timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
|
|
4631
|
-
})(
|
|
5123
|
+
})(operation, {
|
|
4632
5124
|
accountId,
|
|
4633
5125
|
apiBaseUrl,
|
|
4634
|
-
captureJson: capture,
|
|
4635
5126
|
cwd
|
|
4636
5127
|
});
|
|
4637
|
-
if (process.env.VOID_PLATFORM_TIMINGS === "1") {
|
|
4638
|
-
const operation = args[0] === "versions" || args[0] === "deployments" ? `${args[0]} ${args[1] ?? ""}` : args[0] ?? "unknown";
|
|
4639
|
-
process.stderr.write(`[platform timing] wrangler ${operation} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
|
|
4640
|
-
}
|
|
5128
|
+
if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] Cloudflare ${operation.type} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
|
|
4641
5129
|
const stdout = result.stdout || "";
|
|
4642
5130
|
const stderr = result.stderr || "";
|
|
4643
5131
|
if (!capture && result.status !== 0) {
|
|
@@ -4645,7 +5133,7 @@ async function runPlatformWranglerAtApiBase(args, accountId, cwd, capture, apiBa
|
|
|
4645
5133
|
if (stderr) process.stderr.write(stderr);
|
|
4646
5134
|
}
|
|
4647
5135
|
return {
|
|
4648
|
-
|
|
5136
|
+
...result,
|
|
4649
5137
|
stderr,
|
|
4650
5138
|
stdout
|
|
4651
5139
|
};
|
|
@@ -4676,8 +5164,11 @@ async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId,
|
|
|
4676
5164
|
const upstreamBase = new URL(upstreamApiBaseUrl);
|
|
4677
5165
|
if (upstreamBase.protocol !== "http:" && upstreamBase.protocol !== "https:") throw new Error("platform: Cloudflare API base URL must use HTTP or HTTPS.");
|
|
4678
5166
|
const basePath = upstreamBase.pathname.replace(/\/+$/, "");
|
|
4679
|
-
const
|
|
5167
|
+
const accountPath = `${basePath}/accounts/${encodeURIComponent(accountId)}`;
|
|
5168
|
+
const expectedPath = `${accountPath}/workers/scripts/${encodeURIComponent(workerName)}`;
|
|
5169
|
+
const assetUploadPath = `${accountPath}/workers/assets/upload`;
|
|
4680
5170
|
const state = {
|
|
5171
|
+
assetSessionStarted: false,
|
|
4681
5172
|
attempted: false,
|
|
4682
5173
|
conflictStatus: null,
|
|
4683
5174
|
created: false,
|
|
@@ -4697,7 +5188,9 @@ async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId,
|
|
|
4697
5188
|
const isConditionalCreate = incoming.method === "PUT" && incomingUrl.pathname === expectedPath && supportedCreateQuery;
|
|
4698
5189
|
const isTopLevelWorkerUpload = incoming.method === "PUT" && new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/accounts/[^/]+/workers/scripts/[^/]+$`).test(incomingUrl.pathname);
|
|
4699
5190
|
const isReadOnly = incoming.method === "GET" || incoming.method === "HEAD" || incoming.method === "OPTIONS";
|
|
4700
|
-
|
|
5191
|
+
const isAssetUploadSession = incoming.method === "POST" && incomingUrl.pathname === `${expectedPath}/assets-upload-session` && incomingUrl.search === "";
|
|
5192
|
+
const isAssetUpload = incoming.method === "POST" && state.assetSessionStarted && (incomingUrl.pathname === assetUploadPath && incomingUrl.search === "?base64=true" || incomingUrl.search === "" && incomingUrl.pathname.startsWith(`${assetUploadPath}/`) && /^[0-9a-f]{32}$/.test(incomingUrl.pathname.slice(assetUploadPath.length + 1)));
|
|
5193
|
+
if (isTopLevelWorkerUpload && !isConditionalCreate || !isReadOnly && !isConditionalCreate && !isAssetUploadSession && !isAssetUpload && !state.created) {
|
|
4701
5194
|
state.rejectedMutation = `${incoming.method ?? "UNKNOWN"} ${incomingUrl.pathname}${incomingUrl.pathname === expectedPath && !supportedCreateQuery ? " (unsupported Worker create query)" : ""}`;
|
|
4702
5195
|
incoming.resume();
|
|
4703
5196
|
outgoing.writeHead(409, { "Content-Type": "application/json" });
|
|
@@ -4710,6 +5203,7 @@ async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId,
|
|
|
4710
5203
|
state.attempted = true;
|
|
4711
5204
|
headers["if-none-match"] = "*";
|
|
4712
5205
|
}
|
|
5206
|
+
if (isAssetUploadSession) state.assetSessionStarted = true;
|
|
4713
5207
|
const upstreamUrl = new URL(`${incomingUrl.pathname}${incomingUrl.search}`, upstreamBase.origin);
|
|
4714
5208
|
const request$2 = (upstreamUrl.protocol === "https:" ? request$1 : request)(upstreamUrl, {
|
|
4715
5209
|
headers,
|
|
@@ -4760,48 +5254,30 @@ async function withConditionalPlatformWorkerCreate(accountId, workerName, operat
|
|
|
4760
5254
|
if (proxy.state.conflictStatus !== null) throw new Error(`platform: Worker ${workerName} was created concurrently (HTTP ${proxy.state.conflictStatus}); no existing Worker was changed. Re-run to inspect the collision.`);
|
|
4761
5255
|
return result;
|
|
4762
5256
|
}
|
|
4763
|
-
async function runPlatformWranglerForConditionalCreate(
|
|
4764
|
-
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(
|
|
5257
|
+
async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl) {
|
|
5258
|
+
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl), upstreamApiBaseUrl);
|
|
4765
5259
|
}
|
|
4766
5260
|
async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
4767
|
-
const result = await runner(
|
|
4768
|
-
"deployments",
|
|
4769
|
-
"status",
|
|
4770
|
-
"--config",
|
|
5261
|
+
const result = await runner({
|
|
5262
|
+
type: "deployments.status",
|
|
4771
5263
|
configPath,
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
"--json"
|
|
4775
|
-
], accountId, process.cwd(), true);
|
|
5264
|
+
workerName
|
|
5265
|
+
}, accountId, process.cwd(), true);
|
|
4776
5266
|
if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
|
|
4777
|
-
|
|
4778
|
-
try {
|
|
4779
|
-
value = JSON.parse(result.stdout);
|
|
4780
|
-
} catch {
|
|
4781
|
-
throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
|
|
4782
|
-
}
|
|
5267
|
+
const value = result.data;
|
|
4783
5268
|
if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
|
|
4784
5269
|
const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
|
|
4785
5270
|
if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
|
|
4786
5271
|
return active[0].version_id;
|
|
4787
5272
|
}
|
|
4788
5273
|
async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
4789
|
-
const result = await runner(
|
|
4790
|
-
"deployments",
|
|
4791
|
-
"list",
|
|
4792
|
-
"--config",
|
|
5274
|
+
const result = await runner({
|
|
5275
|
+
type: "deployments.list",
|
|
4793
5276
|
configPath,
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
"--json"
|
|
4797
|
-
], accountId, process.cwd(), true);
|
|
5277
|
+
workerName
|
|
5278
|
+
}, accountId, process.cwd(), true);
|
|
4798
5279
|
if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
|
|
4799
|
-
|
|
4800
|
-
try {
|
|
4801
|
-
value = JSON.parse(result.stdout);
|
|
4802
|
-
} catch {
|
|
4803
|
-
throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
4804
|
-
}
|
|
5280
|
+
const value = result.data;
|
|
4805
5281
|
if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
4806
5282
|
return value.map((entry, index) => {
|
|
4807
5283
|
if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.created_on !== "string" || !Array.isArray(entry.versions)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
@@ -4836,16 +5312,15 @@ async function restorePlatformWorkerDeployment(input) {
|
|
|
4836
5312
|
if (restorationVersions.length === 0 || restorationVersions.length > 2 || restorationVersions.some((version, index) => version.percentage > 100 || restorationVersions.findIndex((entry) => entry.versionId === version.versionId) !== index) || Math.abs(totalPercentage - 100) > .001) throw new Error(`Worker ${input.workerName} deployment ${input.restoration.id} has an unsupported traffic distribution.`);
|
|
4837
5313
|
const before = await readPlatformWorkerDeployments(input.configPath, input.accountId, input.workerName, input.runner);
|
|
4838
5314
|
if (before[0]?.id !== input.expectedActiveDeploymentId || !deploymentRunsOnlyVersion(before[0], input.expectedActiveVersionId) || before[1]?.id !== input.restoration.id || !samePlatformWorkerTraffic(before[1], input.restoration)) throw new Error(`Worker ${input.workerName} changed concurrently before deployment restoration; refusing to overwrite the newer deployment.`);
|
|
4839
|
-
assertCleanPlatformWrangler(await input.runner(
|
|
4840
|
-
"versions",
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
`Void platform restoration ${input.restoration.id}
|
|
4847
|
-
|
|
4848
|
-
], input.accountId, input.directory, true), `restore deployment ${input.restoration.id}`);
|
|
5315
|
+
assertCleanPlatformWrangler(await input.runner({
|
|
5316
|
+
type: "versions.deploy",
|
|
5317
|
+
configPath: input.configPath,
|
|
5318
|
+
versions: restorationVersions.map((version) => ({
|
|
5319
|
+
versionId: version.versionId,
|
|
5320
|
+
percentage: version.percentage
|
|
5321
|
+
})),
|
|
5322
|
+
message: `Void platform restoration ${input.restoration.id}`
|
|
5323
|
+
}, input.accountId, input.directory, true), `restore deployment ${input.restoration.id}`);
|
|
4849
5324
|
const after = await readPlatformWorkerDeployments(input.configPath, input.accountId, input.workerName, input.runner);
|
|
4850
5325
|
if (!samePlatformWorkerTraffic(after[0], input.restoration) || after[1]?.id !== input.expectedActiveDeploymentId) throw new Error(`Worker ${input.workerName} deployment restoration was superseded or did not restore deployment ${input.restoration.id}.`);
|
|
4851
5326
|
}
|
|
@@ -4901,23 +5376,14 @@ async function readActivePlatformWorkerSnapshot(configPath, accountId, workerNam
|
|
|
4901
5376
|
});
|
|
4902
5377
|
}
|
|
4903
5378
|
async function readPlatformWorkerVersionSnapshot(configPath, accountId, workerName, expectedOwnershipMarker, versionId, runner, validateCurrentVersion) {
|
|
4904
|
-
const result = await runner(
|
|
4905
|
-
"versions",
|
|
4906
|
-
"view",
|
|
5379
|
+
const result = await runner({
|
|
5380
|
+
type: "versions.view",
|
|
4907
5381
|
versionId,
|
|
4908
|
-
"--config",
|
|
4909
5382
|
configPath,
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
"--json"
|
|
4913
|
-
], accountId, process.cwd(), true);
|
|
5383
|
+
workerName
|
|
5384
|
+
}, accountId, process.cwd(), true);
|
|
4914
5385
|
assertCleanPlatformWrangler(result, `read rollback metadata for ${workerName}`);
|
|
4915
|
-
|
|
4916
|
-
try {
|
|
4917
|
-
value = JSON.parse(result.stdout);
|
|
4918
|
-
} catch {
|
|
4919
|
-
throw new Error(`platform: Wrangler returned malformed version metadata for ${workerName}.`);
|
|
4920
|
-
}
|
|
5386
|
+
const value = result.data;
|
|
4921
5387
|
if (!value || typeof value !== "object") throw new Error(`platform: Wrangler returned malformed version metadata for ${workerName}.`);
|
|
4922
5388
|
const resources = value.resources;
|
|
4923
5389
|
const bindings = resources && typeof resources === "object" && !Array.isArray(resources) ? resources.bindings : null;
|
|
@@ -4943,11 +5409,8 @@ function withoutCompletedPlatformWorkerMigrations(config, activeMigrationTag) {
|
|
|
4943
5409
|
delete result.migrations;
|
|
4944
5410
|
return result;
|
|
4945
5411
|
}
|
|
4946
|
-
function parseUploadedPlatformWorkerVersion(output) {
|
|
4947
|
-
return /(?:Worker|Current) Version ID:\s*([0-9a-f]{8}-[0-9a-f-]{27})/i.exec(output)?.[1] ?? null;
|
|
4948
|
-
}
|
|
4949
5412
|
function assertCleanPlatformWrangler(result, operation) {
|
|
4950
|
-
if (result.status !== 0) throw new Error(`platform: Wrangler failed to ${operation} (${result.stderr.trim() || `exit ${result.status}`}).`);
|
|
5413
|
+
if (result.status !== 0 || result.signal || result.error) throw new Error(`platform: Wrangler failed to ${operation} (${result.stderr.trim() || `exit ${result.status}`}).`);
|
|
4951
5414
|
}
|
|
4952
5415
|
function cleanPlatformCommandResult() {
|
|
4953
5416
|
return {
|
|
@@ -5068,18 +5531,18 @@ function parseWorkerCutoverSnapshot(value) {
|
|
|
5068
5531
|
return null;
|
|
5069
5532
|
}
|
|
5070
5533
|
}
|
|
5071
|
-
function
|
|
5534
|
+
function workerCutoverGenerations(manifest) {
|
|
5072
5535
|
const generations = /* @__PURE__ */ new Map();
|
|
5073
5536
|
for (const step of manifest.steps) {
|
|
5074
5537
|
const resolution = /^worker-cutover:(\d+):(committed|restored)$/.exec(step.key);
|
|
5075
5538
|
if (resolution) {
|
|
5076
5539
|
const generation = Number(resolution[1]);
|
|
5077
5540
|
const entry = generations.get(generation) ?? {
|
|
5078
|
-
|
|
5541
|
+
resolution: null,
|
|
5079
5542
|
staged: /* @__PURE__ */ new Map(),
|
|
5080
5543
|
unsafeTraffic: /* @__PURE__ */ new Set()
|
|
5081
5544
|
};
|
|
5082
|
-
entry.
|
|
5545
|
+
entry.resolution = resolution[2];
|
|
5083
5546
|
generations.set(generation, entry);
|
|
5084
5547
|
continue;
|
|
5085
5548
|
}
|
|
@@ -5087,7 +5550,7 @@ function interruptedWorkerCutover(manifest) {
|
|
|
5087
5550
|
if (unsafeTraffic) {
|
|
5088
5551
|
const generation = Number(unsafeTraffic[1]);
|
|
5089
5552
|
const entry = generations.get(generation) ?? {
|
|
5090
|
-
|
|
5553
|
+
resolution: null,
|
|
5091
5554
|
staged: /* @__PURE__ */ new Map(),
|
|
5092
5555
|
unsafeTraffic: /* @__PURE__ */ new Set()
|
|
5093
5556
|
};
|
|
@@ -5101,14 +5564,17 @@ function interruptedWorkerCutover(manifest) {
|
|
|
5101
5564
|
const snapshot = parseWorkerCutoverSnapshot(step.resultHash);
|
|
5102
5565
|
if (!snapshot) throw new Error(`platform: Invalid Worker cutover checkpoint ${step.key}.`);
|
|
5103
5566
|
const entry = generations.get(generation) ?? {
|
|
5104
|
-
|
|
5567
|
+
resolution: null,
|
|
5105
5568
|
staged: /* @__PURE__ */ new Map(),
|
|
5106
5569
|
unsafeTraffic: /* @__PURE__ */ new Set()
|
|
5107
5570
|
};
|
|
5108
5571
|
entry.staged.set(staged[2], snapshot);
|
|
5109
5572
|
generations.set(generation, entry);
|
|
5110
5573
|
}
|
|
5111
|
-
|
|
5574
|
+
return generations;
|
|
5575
|
+
}
|
|
5576
|
+
function interruptedWorkerCutover(manifest) {
|
|
5577
|
+
const unresolved = [...workerCutoverGenerations(manifest).entries()].filter(([, entry]) => entry.resolution === null && entry.staged.size > 0).sort(([left], [right]) => right - left)[0];
|
|
5112
5578
|
return unresolved ? {
|
|
5113
5579
|
generation: unresolved[0],
|
|
5114
5580
|
staged: unresolved[1].staged,
|
|
@@ -5122,8 +5588,8 @@ async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs
|
|
|
5122
5588
|
"tail",
|
|
5123
5589
|
"dispatch"
|
|
5124
5590
|
]) {
|
|
5125
|
-
const cutover =
|
|
5126
|
-
if (cutover
|
|
5591
|
+
const cutover = workerCutoverGenerations(manifest).get(generation);
|
|
5592
|
+
if (!cutover || cutover.resolution === "restored" || cutover.staged.size !== workerKeys.length) throw new Error(`platform: Worker cutover generation ${generation} is missing a complete immutable-version checkpoint.`);
|
|
5127
5593
|
const directory = mkdtempSync(join(tmpdir(), "void-platform-cutover-verify-"));
|
|
5128
5594
|
try {
|
|
5129
5595
|
for (const key of workerKeys) {
|
|
@@ -5214,7 +5680,7 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
|
|
|
5214
5680
|
redirect: "manual",
|
|
5215
5681
|
signal: AbortSignal.timeout(3e4)
|
|
5216
5682
|
});
|
|
5217
|
-
const dispatchUrl = dispatchOrigin ?? `https
|
|
5683
|
+
const dispatchUrl = dispatchOrigin ?? `https://${PLATFORM_HEALTH_HOSTNAME_LABEL}.${applicationBaseDomain}`;
|
|
5218
5684
|
const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
|
|
5219
5685
|
request(`${apiUrl}/health`),
|
|
5220
5686
|
request(`${apiUrl}/.well-known/void-platform`),
|
|
@@ -5259,9 +5725,8 @@ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fe
|
|
|
5259
5725
|
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.");
|
|
5260
5726
|
return status.state === "complete";
|
|
5261
5727
|
}
|
|
5262
|
-
async function
|
|
5263
|
-
const response = await fetcher(`${apiUrl}/
|
|
5264
|
-
method: "POST",
|
|
5728
|
+
async function verifyPlatformDeploymentExecutionDrain(apiUrl, healthProbeToken, installationId, fetcher = fetch) {
|
|
5729
|
+
const response = await fetcher(`${apiUrl}/__void/platform/deployment-execution`, {
|
|
5265
5730
|
headers: {
|
|
5266
5731
|
"X-Void-Platform-Probe": healthProbeToken,
|
|
5267
5732
|
...cfAccessHeaders(process.env, apiUrl)
|
|
@@ -5269,18 +5734,104 @@ async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher =
|
|
|
5269
5734
|
signal: AbortSignal.timeout(3e4),
|
|
5270
5735
|
redirect: "manual"
|
|
5271
5736
|
});
|
|
5737
|
+
if (!response.ok) throw new Error(`platform: Deployment execution drain probe failed (HTTP ${response.status}). Keep the current runtime until its durable deployment operations have drained.`);
|
|
5272
5738
|
let value;
|
|
5273
5739
|
try {
|
|
5274
5740
|
value = await response.json();
|
|
5275
5741
|
} catch {
|
|
5276
|
-
throw new Error(
|
|
5742
|
+
throw new Error("platform: Deployment execution drain probe returned an invalid document.");
|
|
5277
5743
|
}
|
|
5278
|
-
const status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5279
|
-
if (
|
|
5280
|
-
|
|
5744
|
+
const status = value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5745
|
+
if (status?.protocolVersion !== 1 || status.installationId !== installationId || typeof status.admissionClosed !== "boolean" || !Number.isSafeInteger(status.pendingDeployments) || Number(status.pendingDeployments) < 0) throw new Error("platform: Deployment execution drain probe returned an invalid document.");
|
|
5746
|
+
if (!status.admissionClosed || status.pendingDeployments !== 0) throw new Error("platform: Durable deployment operations are still admitted or unfinished. Keep the current runtime and retry this command after they finish.");
|
|
5747
|
+
}
|
|
5748
|
+
async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
|
|
5749
|
+
const deadline = Date.now() + 3e5;
|
|
5750
|
+
let operationId = null;
|
|
5751
|
+
const timedOut = () => /* @__PURE__ */ new Error(`platform: Shared email bootstrap did not finish within five minutes at ${apiUrl}.${operationId ? ` Operation ${operationId} remains incomplete.` : ""} Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
|
|
5752
|
+
const waitForProgress = async () => {
|
|
5753
|
+
const remaining = deadline - Date.now();
|
|
5754
|
+
if (remaining <= 0) throw timedOut();
|
|
5755
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, remaining)));
|
|
5756
|
+
};
|
|
5757
|
+
let response;
|
|
5758
|
+
let value;
|
|
5759
|
+
let status;
|
|
5760
|
+
for (;;) {
|
|
5761
|
+
const remaining = deadline - Date.now();
|
|
5762
|
+
if (remaining <= 0) throw timedOut();
|
|
5763
|
+
const signal = AbortSignal.timeout(Math.min(12e4, remaining));
|
|
5764
|
+
try {
|
|
5765
|
+
response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
|
|
5766
|
+
method: "POST",
|
|
5767
|
+
headers: {
|
|
5768
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
5769
|
+
...cfAccessHeaders(process.env, apiUrl)
|
|
5770
|
+
},
|
|
5771
|
+
signal,
|
|
5772
|
+
redirect: "manual"
|
|
5773
|
+
});
|
|
5774
|
+
} catch (error) {
|
|
5775
|
+
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
5776
|
+
await waitForProgress();
|
|
5777
|
+
continue;
|
|
5778
|
+
}
|
|
5779
|
+
throw new Error(`platform: Shared email bootstrap request failed at ${apiUrl}. Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
|
|
5780
|
+
}
|
|
5781
|
+
try {
|
|
5782
|
+
value = await response.json();
|
|
5783
|
+
} catch (error) {
|
|
5784
|
+
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
5785
|
+
await waitForProgress();
|
|
5786
|
+
continue;
|
|
5787
|
+
}
|
|
5788
|
+
throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
|
|
5789
|
+
}
|
|
5790
|
+
status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5791
|
+
const observedOperationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
|
|
5792
|
+
if (status?.version === 1 && observedOperationId) {
|
|
5793
|
+
if (operationId && operationId !== observedOperationId) throw new Error("platform: Shared email bootstrap operation identity changed while waiting. Traffic remains disabled. Inspect the installation before retrying.");
|
|
5794
|
+
operationId = observedOperationId;
|
|
5795
|
+
}
|
|
5796
|
+
if ([
|
|
5797
|
+
200,
|
|
5798
|
+
202,
|
|
5799
|
+
503
|
|
5800
|
+
].includes(response.status) && status?.version === 1 && status.state === "incomplete" && observedOperationId && (status.operationStatus === "pending" || status.operationStatus === "running") && status.failureCode == null) {
|
|
5801
|
+
await waitForProgress();
|
|
5802
|
+
continue;
|
|
5803
|
+
}
|
|
5804
|
+
break;
|
|
5805
|
+
}
|
|
5806
|
+
if (!response.ok || status?.version !== 1 || status.state !== "ready" || operationId === null || status.operationId !== operationId || status.operationStatus != null && status.operationStatus !== "completed" || status.failureCode != null) {
|
|
5807
|
+
const code = (value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value) ? value : null;
|
|
5808
|
+
const operationStatus = code(status?.operationStatus);
|
|
5809
|
+
const operationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
|
|
5810
|
+
const details = operationId ? [`Operation ${operationId}: ${operationStatus ?? "incomplete"}.`] : operationStatus ? [`Operation status: ${operationStatus}.`] : [];
|
|
5811
|
+
const currentStep = code(status?.currentStep);
|
|
5812
|
+
const failureCode = code(status?.failureCode);
|
|
5813
|
+
if (currentStep) details.push(`Step: ${currentStep}.`);
|
|
5814
|
+
if (failureCode) details.push(`Failure: ${failureCode}.`);
|
|
5815
|
+
let domain = null;
|
|
5816
|
+
if (typeof status?.domain === "string") try {
|
|
5817
|
+
domain = normalizeApplicationDomain(status.domain);
|
|
5818
|
+
} catch {}
|
|
5819
|
+
const inboundStatus = code(status?.inboundStatus);
|
|
5820
|
+
if (domain) details.push(`Domain: ${domain}${inboundStatus ? ` (inbound: ${inboundStatus})` : ""}.`);
|
|
5821
|
+
const mxDetail = domain ? `MX records for ${domain} do not route mail to Cloudflare Email Routing` : null;
|
|
5822
|
+
const safeDetails = [
|
|
5823
|
+
...domain ? [mxDetail, `enabled explicit Cloudflare email rules conflict with ${domain}`] : [],
|
|
5824
|
+
"zone catch-all does not point to this ingress",
|
|
5825
|
+
"another enabled catch-all owns this zone"
|
|
5826
|
+
];
|
|
5827
|
+
if (typeof status?.failureDetail === "string" && safeDetails.includes(status.failureDetail)) {
|
|
5828
|
+
details.push(`${status.failureDetail}.`);
|
|
5829
|
+
if (currentStep === "inbound_probe" && status.failureDetail === mxDetail) details.push(`Enable Email Routing for ${domain} in the Cloudflare dashboard, then resume this installation.`);
|
|
5830
|
+
}
|
|
5831
|
+
const detail = details.length ? ` ${details.join(" ")}` : "";
|
|
5281
5832
|
throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
|
|
5282
5833
|
}
|
|
5283
|
-
return
|
|
5834
|
+
return operationId;
|
|
5284
5835
|
}
|
|
5285
5836
|
function createPlatformHealthProbeToken(ownershipNonce) {
|
|
5286
5837
|
return createHash("sha256").update("void-platform-health-probe:v1\0").update(ownershipNonce).digest("base64url");
|
|
@@ -5291,7 +5842,7 @@ function answer(value) {
|
|
|
5291
5842
|
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
5292
5843
|
return value;
|
|
5293
5844
|
}
|
|
5294
|
-
async function selectInstallationAuthentication(file, recorded) {
|
|
5845
|
+
async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
5295
5846
|
if (file) {
|
|
5296
5847
|
const input = JSON.parse(await readFile(file, "utf8"));
|
|
5297
5848
|
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.");
|
|
@@ -5310,9 +5861,12 @@ async function selectInstallationAuthentication(file, recorded) {
|
|
|
5310
5861
|
file: selected
|
|
5311
5862
|
};
|
|
5312
5863
|
}
|
|
5313
|
-
if (recorded)
|
|
5314
|
-
|
|
5315
|
-
|
|
5864
|
+
if (recorded) {
|
|
5865
|
+
if (choices.loginMethods && JSON.stringify(choices.loginMethods) !== JSON.stringify(recorded.providers) || choices.accessProtection !== void 0 && choices.accessProtection !== !!recorded.accessProtection) throw new Error("platform: Resume with the recorded login methods and protection settings.");
|
|
5866
|
+
return { authentication: recorded };
|
|
5867
|
+
}
|
|
5868
|
+
if (!process.stdin.isTTY && !choices.loginMethods && choices.accessProtection === void 0) return { authentication: { providers: ["github"] } };
|
|
5869
|
+
const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] : answer(await multiselect({
|
|
5316
5870
|
message: "Platform login methods:",
|
|
5317
5871
|
initialValues: ["github"],
|
|
5318
5872
|
required: true,
|
|
@@ -5334,11 +5888,11 @@ async function selectInstallationAuthentication(file, recorded) {
|
|
|
5334
5888
|
label: "Cloudflare Access login"
|
|
5335
5889
|
}
|
|
5336
5890
|
]
|
|
5337
|
-
}));
|
|
5338
|
-
const protection = answer(await confirm({
|
|
5891
|
+
})));
|
|
5892
|
+
const protection = choices.accessProtection ?? (!process.stdin.isTTY ? false : answer(await confirm({
|
|
5339
5893
|
message: "Also protect the platform UI and APIs with Cloudflare Access?",
|
|
5340
5894
|
initialValue: false
|
|
5341
|
-
}));
|
|
5895
|
+
})));
|
|
5342
5896
|
return { authentication: {
|
|
5343
5897
|
providers,
|
|
5344
5898
|
...protection || providers.length !== 1 || providers[0] !== "github" ? { configurable: true } : {},
|
|
@@ -5375,6 +5929,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
5375
5929
|
const accessEntry = entries.find((entry) => entry.configuration.kind === "cloudflare-access");
|
|
5376
5930
|
const prepared = access && (selection.authentication.accessProtection || accessEntry && (!selection.file || selection.file.cloudflareAccess)) ? await preparePlatformAccessSetup({
|
|
5377
5931
|
plan,
|
|
5932
|
+
...platformDashboardUrl(plan.deployment) ? { additionalOrigins: [platformDashboardUrl(plan.deployment)] } : {},
|
|
5378
5933
|
ownershipNonce: access.ownershipNonce,
|
|
5379
5934
|
protection: !!selection.authentication.accessProtection,
|
|
5380
5935
|
...accessEntry ? { login: {
|
|
@@ -5582,13 +6137,13 @@ function platformSetupFieldHelp(key) {
|
|
|
5582
6137
|
switch (key) {
|
|
5583
6138
|
case "databaseUrl": return {
|
|
5584
6139
|
placeholder: "postgresql://user:pw@…/database",
|
|
5585
|
-
description: "Stores platform users, projects, and deployments
|
|
6140
|
+
description: "Stores platform users, projects, and deployments. Use an empty hosted database, e.g. " + platformSetupLink({
|
|
5586
6141
|
title: "Neon",
|
|
5587
6142
|
url: "https://neon.com/"
|
|
5588
6143
|
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
5589
6144
|
};
|
|
5590
|
-
case "jwtSecret": return { description: "Signs platform login tokens. Run openssl rand -base64 32
|
|
5591
|
-
case "projectSecretKey": return { description: "Encrypts app secrets. Run openssl rand -base64 32 separately
|
|
6145
|
+
case "jwtSecret": return { description: "Signs platform login tokens. Run openssl rand -base64 32. Save the key in your password manager. " + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
6146
|
+
case "projectSecretKey": return { description: "Encrypts app secrets. Run openssl rand -base64 32 separately. Save this key for recovery. " + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
5592
6147
|
default: return;
|
|
5593
6148
|
}
|
|
5594
6149
|
}
|
|
@@ -5706,8 +6261,8 @@ function platformSetupPages(plan) {
|
|
|
5706
6261
|
type: "account"
|
|
5707
6262
|
}),
|
|
5708
6263
|
instructions: [
|
|
5709
|
-
"Select Object Read & Write
|
|
5710
|
-
"Keep Apply to all buckets in this account
|
|
6264
|
+
"Select Object Read & Write.",
|
|
6265
|
+
"Keep Apply to all buckets in this account selected.",
|
|
5711
6266
|
"Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
|
|
5712
6267
|
].join("\n")
|
|
5713
6268
|
}
|
|
@@ -5716,16 +6271,20 @@ function platformSetupPages(plan) {
|
|
|
5716
6271
|
function formatPlatformSetupPlan(plan) {
|
|
5717
6272
|
const pages = platformSetupPages(plan);
|
|
5718
6273
|
return [
|
|
5719
|
-
"Credentials needed
|
|
6274
|
+
"Credentials needed during installation:",
|
|
6275
|
+
"PostgreSQL database URL",
|
|
5720
6276
|
platformSetupLink(pages.runtime),
|
|
5721
|
-
pages.runtime.instructions,
|
|
5722
|
-
"",
|
|
5723
6277
|
platformSetupLink(pages.r2),
|
|
5724
|
-
pages.
|
|
6278
|
+
...plan.deployment.authentication.providers.includes("github") ? [platformSetupLink(pages.github)] : [],
|
|
6279
|
+
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => (provider === "google" ? "Google OAuth" : provider === "oidc" ? "OIDC" : "Cloudflare Access OIDC") + " client ID and secret"),
|
|
6280
|
+
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access application and installer credentials"] : [],
|
|
6281
|
+
platformSetupLink({
|
|
6282
|
+
title: "JWT signing key and project encryption key",
|
|
6283
|
+
url: "https://void.cloud/guide/platform/installation/credentials#signing-and-encryption-keys"
|
|
6284
|
+
}),
|
|
5725
6285
|
"",
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access protection: configure API and proxy origins and admitted installer/CI credentials."] : []
|
|
6286
|
+
"Login callback: " + plan.apiUrl + "/auth/callback",
|
|
6287
|
+
"Installation collects and validates missing credentials after you confirm the plan."
|
|
5729
6288
|
].join("\n");
|
|
5730
6289
|
}
|
|
5731
6290
|
function formatPlatformInstallCommand(options) {
|
|
@@ -5753,10 +6312,18 @@ function formatPlatformInstallCommand(options) {
|
|
|
5753
6312
|
flag("--application-domain", options.applicationDomain);
|
|
5754
6313
|
flag("--zone", options.zoneName);
|
|
5755
6314
|
if (options.dedicatedZone) parts.push("--dedicated-zone");
|
|
6315
|
+
else parts.push("--no-dedicated-zone");
|
|
5756
6316
|
}
|
|
5757
6317
|
flag("--control-plane-domain", options.controlPlaneDomain);
|
|
6318
|
+
if (!options.controlPlaneDomain) parts.push("--no-control-plane-domain");
|
|
5758
6319
|
}
|
|
6320
|
+
flag("--hyperdrive", options.hyperdrive ?? "");
|
|
5759
6321
|
flag("--auth-config", options.authConfig ?? "");
|
|
6322
|
+
if (!options.authConfig && !options.resumeId && options.authentication) {
|
|
6323
|
+
flag("--login-methods", options.authentication.providers.join(","));
|
|
6324
|
+
parts.push(options.authentication.accessProtection ? "--access-protection" : "--no-access-protection");
|
|
6325
|
+
}
|
|
6326
|
+
flag("--dashboard-url", options.dashboardUrl ?? "");
|
|
5760
6327
|
flag("--runtime", options.runtime);
|
|
5761
6328
|
if (options.yes) parts.push("--yes");
|
|
5762
6329
|
return parts.join(" ");
|
|
@@ -5816,14 +6383,14 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
|
5816
6383
|
}
|
|
5817
6384
|
async function runPlatformLifecycleCommand(args) {
|
|
5818
6385
|
if (args.subcommand === "domain") {
|
|
5819
|
-
const { runPlatformDomainCommand } = await import("./platform-domain-
|
|
6386
|
+
const { runPlatformDomainCommand } = await import("./platform-domain-BNkcz0OB.mjs");
|
|
5820
6387
|
await runPlatformDomainCommand(args);
|
|
5821
6388
|
return;
|
|
5822
6389
|
}
|
|
5823
6390
|
if (args.subcommand === "discover") {
|
|
5824
6391
|
const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
|
|
5825
6392
|
const accountId = await selectAccount(await readAccounts(client), args.account);
|
|
5826
|
-
const { recoverPlatformInstallations } = await import("./platform-recovery-
|
|
6393
|
+
const { recoverPlatformInstallations } = await import("./platform-recovery-D6KSpuFm.mjs");
|
|
5827
6394
|
const recovered = await recoverPlatformInstallations(client, accountId, void 0, args.selector);
|
|
5828
6395
|
if (recovered.length === 0) {
|
|
5829
6396
|
log.info(`No recoverable Void platform installations were found in ${accountId}.`);
|
|
@@ -5833,7 +6400,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
5833
6400
|
return;
|
|
5834
6401
|
}
|
|
5835
6402
|
if (args.subcommand !== "install") {
|
|
5836
|
-
const { runInstalledPlatformCommand } = await import("./platform-management-
|
|
6403
|
+
const { runInstalledPlatformCommand } = await import("./platform-management-CnyTdcWX.mjs");
|
|
5837
6404
|
await runInstalledPlatformCommand(args);
|
|
5838
6405
|
return;
|
|
5839
6406
|
}
|
|
@@ -5948,9 +6515,9 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
5948
6515
|
const applicationDomain = recorded?.application.baseDomain ?? (workersDev ? "" : normalizeApplicationDomain(args.applicationDomain || await promptText("Application base domain:", "example.app")));
|
|
5949
6516
|
const zoneName = recorded?.application.zoneName ?? (workersDev ? "" : normalizeApplicationDomain(args.zone || await promptText("Cloudflare zone:", applicationDomain)));
|
|
5950
6517
|
const displayName = recorded?.displayName ?? (args.displayName || await promptText("Platform display name:", `${installationSlug} Void`));
|
|
5951
|
-
const controlPlaneDomain = recorded ? recorded.controlPlane.apiHostname ?? "" : args.controlPlaneDomain || (process.stdin.isTTY ? await promptText("
|
|
6518
|
+
const controlPlaneDomain = recorded ? recorded.controlPlane.apiHostname ?? "" : args.controlPlaneDomain || (process.stdin.isTTY && !args.noControlPlaneDomain ? await promptText("Custom API hostname. Leave empty for workers.dev:", "", true) : "");
|
|
5952
6519
|
let dedicatedZone = recorded?.application.dedicatedZone ?? args.dedicatedZone;
|
|
5953
|
-
if (!workersDev && !recorded && !args.dedicatedZone && applicationDomain === zoneName && process.stdin.isTTY) {
|
|
6520
|
+
if (!workersDev && !recorded && !args.dedicatedZone && !args.dedicatedZoneSpecified && applicationDomain === zoneName && process.stdin.isTTY) {
|
|
5954
6521
|
const catchAll = await confirm({
|
|
5955
6522
|
message: `Dedicate all unmatched ${zoneName} traffic to Void apps?`,
|
|
5956
6523
|
initialValue: false
|
|
@@ -5992,11 +6559,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
5992
6559
|
host: externalHyperdriveHost,
|
|
5993
6560
|
user: externalHyperdriveUser
|
|
5994
6561
|
} : void 0;
|
|
5995
|
-
if (
|
|
6562
|
+
if (args.hyperdrive && externalHyperdrive && args.hyperdrive !== externalHyperdrive.id) throw new Error("platform: --hyperdrive conflicts with VOID_PLATFORM_HYPERDRIVE_ID.");
|
|
6563
|
+
if (!externalHyperdrive && args.hyperdrive !== "create" && (process.stdin.isTTY || args.hyperdrive) && (!existing || isPlatformSetupDraft(existing)) && !existing?.resources.some((resource) => resource.key === "hyperdrive")) {
|
|
5996
6564
|
const suggestedName = existing?.deployment.resources.hyperdrive ?? createPlatformResourceNames(installationSlug).hyperdrive;
|
|
5997
6565
|
const configurations = await client.listHyperdriveConfigs(accountId);
|
|
5998
6566
|
const suggestedNameAvailable = !configurations.some((resource) => resource.name === suggestedName);
|
|
5999
|
-
const choice = await select({
|
|
6567
|
+
const choice = args.hyperdrive ? "existing" : await select({
|
|
6000
6568
|
message: "How should Void set up Hyperdrive?",
|
|
6001
6569
|
options: [
|
|
6002
6570
|
...configurations.length > 0 ? [{
|
|
@@ -6016,12 +6584,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6016
6584
|
});
|
|
6017
6585
|
if (isCancel(choice)) throw new Error("platform: Cancelled.");
|
|
6018
6586
|
if (choice === "setup") {
|
|
6019
|
-
note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? `
|
|
6587
|
+
note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? ` such as ${suggestedName}` : ""}. Point it at the same dedicated PostgreSQL database you will give Void, use a runtime database user, and disable SQL result caching. If your organization manages Hyperdrive, ask its administrator to create it. Then rerun this command and select it from the list.\nSetup: https://developers.cloudflare.com/hyperdrive/get-started/`, "Set up Hyperdrive first");
|
|
6020
6588
|
outro("No Cloudflare or local installation state was changed.");
|
|
6021
6589
|
return;
|
|
6022
6590
|
}
|
|
6023
6591
|
if (choice === "existing") {
|
|
6024
|
-
const selectedId = await select({
|
|
6592
|
+
const selectedId = args.hyperdrive ?? await select({
|
|
6025
6593
|
message: "Which Hyperdrive should Void use?",
|
|
6026
6594
|
options: [...configurations].sort((a, b) => Number(b.name === suggestedName) - Number(a.name === suggestedName) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((resource) => ({
|
|
6027
6595
|
value: resource.id,
|
|
@@ -6036,7 +6604,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6036
6604
|
if (live.id !== selected.id || live.name !== selected.name || !live.origin.user) throw new Error(`platform: Could not verify the database user for existing Hyperdrive ${selected.name}.`);
|
|
6037
6605
|
note(`Database: ${live.origin.database}\nHost: ${live.origin.host}\nPort: ${live.origin.port}\nUser: ${live.origin.user}\nSQL result caching: ${live.cachingDisabled ? "disabled" : "enabled"}`, `Existing Hyperdrive ${live.name}`);
|
|
6038
6606
|
if (!live.cachingDisabled) throw new Error(`platform: Disable SQL result caching on existing Hyperdrive ${live.name} before installing.`);
|
|
6039
|
-
const confirmed = await confirm({
|
|
6607
|
+
const confirmed = args.hyperdrive ? true : await confirm({
|
|
6040
6608
|
message: `Use ${live.name} for this platform?`,
|
|
6041
6609
|
initialValue: true
|
|
6042
6610
|
});
|
|
@@ -6057,7 +6625,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6057
6625
|
progress?.stop("Read the saved installation.");
|
|
6058
6626
|
progress = void 0;
|
|
6059
6627
|
if (authoritative) {
|
|
6060
|
-
const { selectAuthoritativePlatformManifest } = await import("./platform-management-
|
|
6628
|
+
const { selectAuthoritativePlatformManifest } = await import("./platform-management-CnyTdcWX.mjs");
|
|
6061
6629
|
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
6062
6630
|
authoritativeProvisionIntents = true;
|
|
6063
6631
|
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
@@ -6065,12 +6633,13 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6065
6633
|
if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
|
|
6066
6634
|
}
|
|
6067
6635
|
}
|
|
6068
|
-
const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication);
|
|
6636
|
+
const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication, args);
|
|
6069
6637
|
const planInput = {
|
|
6070
6638
|
authentication: authenticationSelection.authentication,
|
|
6071
6639
|
accountId,
|
|
6072
6640
|
applicationDomain,
|
|
6073
6641
|
controlPlaneDomain,
|
|
6642
|
+
...args.dashboardUrl !== void 0 ? { dashboardUrl: args.dashboardUrl } : {},
|
|
6074
6643
|
dedicatedZone,
|
|
6075
6644
|
displayName,
|
|
6076
6645
|
...externalHyperdrive ? { externalHyperdrive } : {},
|
|
@@ -6084,13 +6653,15 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6084
6653
|
authoritativeProvisionIntents,
|
|
6085
6654
|
allowUnverifiedAiGateway: true,
|
|
6086
6655
|
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
6087
|
-
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3
|
|
6656
|
+
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3,
|
|
6657
|
+
deploymentExecution: runtime.manifest.deploymentExecution === 1
|
|
6088
6658
|
});
|
|
6089
6659
|
note(formatPlatformInstallPlan(plan), args.plan ? "Read-only installation plan" : "Installation plan");
|
|
6090
6660
|
note(formatPlatformRuntime(runtime), "Platform runtime");
|
|
6091
6661
|
if (args.plan) {
|
|
6092
6662
|
console.log(formatPlatformSetupPlan(plan));
|
|
6093
|
-
|
|
6663
|
+
outro("Plan complete. No installation changes were made.");
|
|
6664
|
+
console.log(`\n${import_picocolors$1.default.bold(import_picocolors$1.default.cyan(existing ? "Next step: run this command to resume" : "Next step: run this command to install"))}\n\n${import_picocolors$1.default.cyan(formatPlatformInstallCommand({
|
|
6094
6665
|
commandName: cliCommandName(),
|
|
6095
6666
|
accountId,
|
|
6096
6667
|
installationSlug,
|
|
@@ -6098,15 +6669,16 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6098
6669
|
applicationDomain,
|
|
6099
6670
|
zoneName,
|
|
6100
6671
|
controlPlaneDomain,
|
|
6672
|
+
...args.dashboardUrl !== void 0 ? { dashboardUrl: args.dashboardUrl } : {},
|
|
6101
6673
|
dedicatedZone,
|
|
6102
6674
|
workersDev,
|
|
6103
6675
|
authConfig: args.authConfig,
|
|
6676
|
+
authentication: authenticationSelection.authentication,
|
|
6677
|
+
hyperdrive: !existing || isPlatformSetupDraft(existing) ? externalHyperdrive?.id ?? "create" : void 0,
|
|
6104
6678
|
runtime: args.runtime,
|
|
6105
6679
|
...existing ? { resumeId: existing.id } : {},
|
|
6106
6680
|
yes: args.yes || process.stdin.isTTY !== true
|
|
6107
|
-
})}\n`);
|
|
6108
|
-
if (!existing && !args.authConfig && process.stdin.isTTY) console.log("Choose the same login methods when prompted.\n");
|
|
6109
|
-
outro("No Cloudflare or local installation state was changed.");
|
|
6681
|
+
}))}\n`);
|
|
6110
6682
|
return;
|
|
6111
6683
|
}
|
|
6112
6684
|
assertPlatformOperatorCredentialCanApplyPlan(operatorCredential, plan);
|
|
@@ -6282,11 +6854,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6282
6854
|
authoritativeProvisionIntents: true,
|
|
6283
6855
|
repairMissing: true,
|
|
6284
6856
|
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
6285
|
-
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3
|
|
6857
|
+
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3,
|
|
6858
|
+
deploymentExecution: runtime.manifest.deploymentExecution === 1
|
|
6286
6859
|
})).actions.filter((action) => action.kind === "worker"),
|
|
6287
6860
|
revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
|
|
6288
6861
|
verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
|
|
6289
|
-
const { verifyPlatformInstallationOwnership } = await import("./platform-management-
|
|
6862
|
+
const { verifyPlatformInstallationOwnership } = await import("./platform-management-CnyTdcWX.mjs");
|
|
6290
6863
|
const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
|
|
6291
6864
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
6292
6865
|
return action.action === "create" && previous && previous.name !== action.name;
|
|
@@ -6298,7 +6871,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6298
6871
|
});
|
|
6299
6872
|
}
|
|
6300
6873
|
});
|
|
6301
|
-
progress?.stop(result.waitingForDns || result.waitingForSandboxDrain ? "Installation paused
|
|
6874
|
+
progress?.stop(result.waitingForDns || result.waitingForSandboxDrain ? "Installation paused. Progress is saved." : "Platform installed.");
|
|
6302
6875
|
progress = void 0;
|
|
6303
6876
|
if (result.waitingForDns) {
|
|
6304
6877
|
note(result.zoneNameServers.length > 0 ? `Update the domain at your registrar to use:\n${result.zoneNameServers.join("\n")}\n\nThen rerun: void platform install --resume --name ${result.manifest.id}` : `Wait for ${zoneName} to become active, then rerun:\nvoid platform install --resume --name ${result.manifest.id}`, "Waiting for nameservers");
|
|
@@ -6306,13 +6879,13 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6306
6879
|
return;
|
|
6307
6880
|
}
|
|
6308
6881
|
if (result.waitingForSandboxDrain) {
|
|
6309
|
-
note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan
|
|
6882
|
+
note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan`. Follow any returned `nextCursor` with `--cursor '<value>'`. Then apply with `void platform system sandbox-drain --yes` and rerun until it reports DB-backed `complete: true`. Rerun this install command afterward to enable traffic.", "Waiting for managed Sandbox drain");
|
|
6310
6883
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
6311
6884
|
return;
|
|
6312
6885
|
}
|
|
6313
6886
|
log.success(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
|
|
6314
6887
|
log.info(`API URL: ${plan.apiUrl}`);
|
|
6315
|
-
if (result.emailOperationId) log.info(`Shared email routing ready
|
|
6888
|
+
if (result.emailOperationId) log.info(`Shared email routing ready. Operation: ${result.emailOperationId}.`);
|
|
6316
6889
|
const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
6317
6890
|
if (result.setupPending && completedAuthCode) {
|
|
6318
6891
|
log.info(`First administrator setup: ${plan.apiUrl}/setup`);
|
|
@@ -6352,7 +6925,8 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
|
|
|
6352
6925
|
verified = await createPlatformInstallPlan(client, input, existing, {
|
|
6353
6926
|
authoritativeProvisionIntents,
|
|
6354
6927
|
includeEmailGateway: preview.actions.some((entry) => entry.key === "email-gateway-worker"),
|
|
6355
|
-
enableSandbox: preview.deployment.capabilities.sandbox
|
|
6928
|
+
enableSandbox: preview.deployment.capabilities.sandbox,
|
|
6929
|
+
deploymentExecution: preview.actions.some((entry) => entry.key === "deployment-queue")
|
|
6356
6930
|
});
|
|
6357
6931
|
} catch (error) {
|
|
6358
6932
|
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 });
|
|
@@ -6426,4 +7000,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
|
|
|
6426
7000
|
return value;
|
|
6427
7001
|
}
|
|
6428
7002
|
//#endregion
|
|
6429
|
-
export {
|
|
7003
|
+
export { PlatformCloudflareClient as A, revalidatePlatformInstallAction as C, checkpointPlatformAccessResources as D, resolvePlatformCloudflareCredential as E, hyperdriveOriginFromConnectionString as M, hyperdriveOriginMatches as N, parsePlatformAccessSetupSpec as O, verifyPlatformRuntimeTokenPermissions as P, platformProvisionIntentHash as S, assertPlatformOperatorCredentialCanApplyPlan as T, recordPlatformWorkerTraffic as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, formatPlatformInstallPlan as b, createPlatformHealthProbeToken as c, loadPlatformRuntime as d, platformDatabaseClientUrl as f, assertProjectZeroTrustAllowsUninstall as g, resolveBootstrapGitHubId as h, verifyPlatformAiGatewayPlan as i, hyperdriveOriginConfigHash as j, preparePlatformAccessSetup as k, createPlatformInstallationSecrets as l, recordResource as m, resolvePlatformInstallDatabaseUrl as n, acquirePlatformDatabaseLifecycleLock as o, readPlatformProjectZeroTrustState as p, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLockConnection as s, assertNoninteractivePlatformMutationAcknowledged as t, installPlatform as u, trackedPlatformWorkerTraffic as v, platformDatabaseNameFromOwnerUrl as w, platformAdoptionIntentHash as x, createPlatformInstallPlan as y };
|