void 0.26.1 → 0.28.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 +3 -1
- package/dist/{account-cmd-CfUXh_nU.mjs → account-cmd-Ci00cncT.mjs} +5 -5
- package/dist/{agents-B_pX_HmQ.mjs → agents-CuNtT0D_.mjs} +5 -2
- package/dist/{application-analysis-DKy02UY0.mjs → application-analysis-D72ZQfMZ.mjs} +118 -135
- package/dist/{application-routing-DQyUh_5S.mjs → application-routing-Dy589Vgn.mjs} +1 -2
- package/dist/atomic-write-Dq6R0BPN.mjs +18 -0
- package/dist/{auth-B5_tZLxT.mjs → auth-B6iOCjKI.mjs} +23 -56
- package/dist/{auth-link-D5RmKnzh.mjs → auth-link-DWr6Afao.mjs} +10 -10
- package/dist/{auth-migrations-BwHgeEsD.mjs → auth-migrations-Cgs0k6lS.mjs} +28 -5
- package/dist/{auth-router-Bap-pvbn.mjs → auth-router-DP14wVkZ.mjs} +4 -4
- package/dist/auth-wn6F8dB8.mjs +2 -0
- package/dist/base64-DVrOndmI.mjs +16 -0
- package/dist/{better-auth-shared-CPvWD3nG.d.mts → better-auth-shared-B74NJIus.d.mts} +8 -13
- package/dist/{better-auth-shared-DPpj8zxc.mjs → better-auth-shared-CMXlZG7W.mjs} +56 -68
- package/dist/{build-cmd-B58c8iS8.mjs → build-cmd-Cr3FJxv0.mjs} +5 -5
- package/dist/{cache-Bo52trKF.mjs → cache-D0Tt28cn.mjs} +5 -5
- package/dist/{cancel-deploy-BZNz3Q26.mjs → cancel-deploy-CJKvk7AN.mjs} +4 -4
- package/dist/{cf-access-TWNzXltp.mjs → cf-access-BM9G0lhO.mjs} +4 -3
- package/dist/{cf-access-ngGqZpHZ.mjs → cf-access-CWP4avfP.mjs} +3 -7
- package/dist/cf-build-output-2u583yJK.mjs +2 -0
- package/dist/{cf-build-output-DjvB_872.mjs → cf-build-output-Bc8i3dzE.mjs} +2 -2
- package/dist/cf-user-agent-ChjQp2MU.mjs +33 -0
- package/dist/cli/cli.mjs +198 -311
- package/dist/cli/cloudflare-operation-process.mjs +46 -14
- package/dist/cli/env-schema-probe.d.mts +5 -43
- package/dist/cli/env-schema-probe.mjs +14 -80
- package/dist/client-C1BjRn3g.mjs +2 -0
- package/dist/{client-C2Prx_Bi.mjs → client-DM9Kcf4e.mjs} +161 -415
- package/dist/{cloudflare-auth-CuxfF6OA.mjs → cloudflare-auth-CqFBDo52.mjs} +12 -39
- package/dist/{cloudflare-cmd-lHqNlJGW.mjs → cloudflare-cmd-CTrn-lXJ.mjs} +6 -5
- package/dist/{cloudflare-config-BmhBv3HV.mjs → cloudflare-config-DV60XFFK.mjs} +16 -28
- package/dist/{cloudflare-connect-2G3rL2rP.mjs → cloudflare-connect-BosmCNlq.mjs} +3 -3
- package/dist/{cloudflare-operations-CwI1bChl.mjs → cloudflare-operations-BKdSfNBN.mjs} +26 -34
- package/dist/{cloudflare-operations-BlYLlowZ.mjs → cloudflare-operations-Ds-lX3Ts.mjs} +1 -1
- package/dist/{cloudflare-process-2-TfIEzB.mjs → cloudflare-process-CeDWJRhj.mjs} +12 -7
- package/dist/collect-NUlJMpLY.mjs +2 -0
- package/dist/{collect-DiTn9yr6.mjs → collect-xjffdCdk.mjs} +1 -1
- package/dist/{config-CSa15FvD.d.mts → config-7qoq2p9v.d.mts} +2 -2
- package/dist/{config-CLYAosnQ.mjs → config-BEm9vRvS.mjs} +11 -5
- package/dist/{config-DiowPT80.mjs → config-Bx_1SpnO.mjs} +87 -177
- package/dist/config-CNnrSMYm.mjs +2 -0
- package/dist/{config-DPByhVck.mjs → config-D0GY5ItR.mjs} +1 -1
- package/dist/config-entry.d.mts +1 -1
- package/dist/{connect-X0MUA1gk.mjs → connect-nukQSoEO.mjs} +8 -8
- package/dist/{create-project-DxiyW6iX.mjs → create-project-D4APeBwX.mjs} +7 -7
- package/dist/{create-project-iwkbqiBU.mjs → create-project-DL7YXpjj.mjs} +1 -1
- package/dist/{db-XnVMHzoV.mjs → db-27RztQLy.mjs} +173 -263
- package/dist/{delete-esYlMiSJ.mjs → delete-BbsZFZIw.mjs} +5 -5
- package/dist/deploy-B06EQq4t.mjs +6009 -0
- package/dist/deploy-Db2ALnQP.mjs +3 -0
- package/dist/deploy-managed-static-CGswbcay.mjs +2277 -0
- package/dist/deploy-transport-BiicfoQW.mjs +180 -0
- package/dist/deploy.d.mts +10 -0
- package/dist/deploy.mjs +82 -0
- package/dist/dev-inbox-Rd6j_8vM.mjs +217 -0
- package/dist/discover-CTUiSsfG.mjs +2 -0
- package/dist/{discover-CkCYv9T_.mjs → discover-D2PGT63u.mjs} +2 -2
- package/dist/{dist-BVqXeR_h.mjs → dist-ipQi0Q8y.mjs} +2 -3
- package/dist/{domain-CbbBWhWE.mjs → domain-Bfq9n03H.mjs} +7 -22
- package/dist/{dotenv-BfU6eqFt.mjs → dotenv-CNazY3-w.mjs} +4 -16
- package/dist/{email-d_Aux1vM.mjs → email-BcjSyAL0.mjs} +4 -20
- package/dist/{email-CpuT42g7.mjs → email-DIwaQU4P.mjs} +99 -191
- package/dist/email-cloudflare-CH053RxK.mjs +1096 -0
- package/dist/email-protocol-D9nTvhCp.mjs +82 -0
- package/dist/{entry-CMAkqBmU.mjs → entry-ehmBvs5w.mjs} +2 -2
- package/dist/{env-BxocywBB.mjs → env-BJjULvL0.mjs} +2 -2
- package/dist/{env-DMA3cXZt.mjs → env-BhdWMrNy.mjs} +2 -13
- package/dist/{env-w9jOyGZn.mjs → env-C6ipBLp3.mjs} +8 -8
- package/dist/{env-public-DR7ZM6d-.d.mts → env-public-CCdykygU.d.mts} +6 -23
- package/dist/{env-raw-jZpP5n47.mjs → env-raw-Cklzcde3.mjs} +3 -20
- package/dist/{env-types-CwXgeMgL.mjs → env-types-DS2_y4X8.mjs} +2 -2
- package/dist/{env-validation-CrVNjsPm.mjs → env-validation-CTn-HhG1.mjs} +1 -1
- package/dist/env-validation-cE0zcmzO.mjs +486 -0
- package/dist/{gen-DtkQhvQg.mjs → gen-CnN1Xidx.mjs} +131 -253
- package/dist/gen-F8pSZnhc.mjs +2 -0
- package/dist/{generate-GDbL7sUr.mjs → generate-Dat0UTry.mjs} +38 -4
- package/dist/generate-Rii9klHB.mjs +2 -0
- package/dist/generated-wrangler-config-DeWj5F9Y.mjs +18 -0
- package/dist/{github-cmd-D_FsOFxq.mjs → github-cmd-BdkU4Nhe.mjs} +27 -71
- package/dist/{handler-BHAn2fZJ.mjs → handler-BZ1t7-Rv.mjs} +2 -2
- package/dist/{handler-CNAwjRcW.d.mts → handler-_PqY82xe.d.mts} +1 -1
- package/dist/{headers-DlfR78Nu.mjs → headers-Di1FsD3e.mjs} +26 -81
- package/dist/{help-CVFBTmBx.mjs → help-Bug7pyK_.mjs} +18 -10
- package/dist/help-s9q7LTcu.mjs +2 -0
- package/dist/{inbound-PfQDLXc5.mjs → inbound-DmkTQeWp.mjs} +71 -222
- package/dist/{inbound-CH5Mksyy.d.mts → inbound-brj2-TM5.d.mts} +19 -58
- package/dist/index.d.mts +2 -18
- package/dist/index.mjs +200 -496
- package/dist/{info-PYWwkWyt.mjs → info-CujPwtMe.mjs} +5 -5
- package/dist/{init-Bw-fRKGp.mjs → init-ZWOJvc-Z.mjs} +179 -388
- package/dist/isr-config-DPjXM8J2.mjs +194 -0
- package/dist/{link-DlPG8iX3.mjs → link-UEc7bnF4.mjs} +8 -8
- package/dist/{list-2GN7_ueV.mjs → list-CuWeOnzk.mjs} +6 -17
- package/dist/{live-CgQuTpqD.mjs → live-BxLX-JqJ.mjs} +19 -17
- package/dist/{live-Du8jXwOW.d.mts → live-DJONVlQK.d.mts} +1 -1
- package/dist/load-user-env-DD5qveNU.mjs +49 -0
- package/dist/{local-d1-ga5O_idV.mjs → local-d1-BM3p74mm.mjs} +6 -6
- package/dist/{login-D-Q4xw3Q.mjs → login-CZ2Cqa-D.mjs} +11 -11
- package/dist/login-DhK7o1oJ.mjs +2 -0
- package/dist/{logs-rd8J-OFh.mjs → logs-TihRcLTk.mjs} +5 -5
- package/dist/migrate-BgT-hmu8.mjs +2 -0
- package/dist/{migrate-NLsjBbuM.mjs → migrate-uFwojusJ.mjs} +69 -21
- package/dist/migration-handler-Cg0Y4EPu.mjs +470 -0
- package/dist/{migration-handler-DM4clYj5.d.mts → migration-handler-CwlEQUoo.d.mts} +17 -26
- package/dist/{mime-DxCznlJq.mjs → mime-srOklVVv.mjs} +92 -395
- package/dist/{node-DJicPvPj.mjs → node-DoKbo1SZ.mjs} +8 -24
- package/dist/{operator-args-BNsW5gfp.mjs → operator-args-UnE8Y2eK.mjs} +27 -26
- package/dist/{operator-auth-AzVLbrr3.mjs → operator-auth-Cr2zCc-J.mjs} +2 -2
- package/dist/{operator-client-BQtzyG-g.mjs → operator-client-C-ROjk1I.mjs} +4 -4
- package/dist/{operator-cmd-B6f6Cg9Z.mjs → operator-cmd-C5lCctnc.mjs} +8 -8
- package/dist/operator-cmd-Cjk5DiRx.mjs +2 -0
- package/dist/{output-C50ag2r3.mjs → output-FNHKw5Ac.mjs} +36 -5
- package/dist/{package-json-DXfiT1jV.mjs → package-json-BjYqR-So.mjs} +1 -1
- package/dist/pages/client.d.mts +1 -1
- package/dist/pages/client.mjs +91 -5
- package/dist/pages/index.d.mts +1 -2
- package/dist/pages/index.mjs +6 -6
- package/dist/pages/islands-plugin.mjs +5 -83
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +1 -1
- package/dist/pages/serialize.mjs +1 -1
- package/dist/{platform-args-BoZkWQnJ.mjs → platform-args-C6Mb_I3a.mjs} +34 -58
- package/dist/platform-args-Cm7x-fot.mjs +2 -0
- package/dist/{platform-auth-config-n603OANg.mjs → platform-auth-config-CJmNkFT2.mjs} +10 -11
- package/dist/{platform-auth-protection-zxDr0hWW.mjs → platform-auth-protection-D5wvsI-Y.mjs} +36 -26
- package/dist/{platform-auth-recovery-tw9lgtgE.mjs → platform-auth-recovery-D_0I3n-1.mjs} +8 -9
- package/dist/{platform-cmd-C00YM14r.mjs → platform-cmd-Cuj4oUVo.mjs} +1 -1
- package/dist/{platform-cmd-B8-bCb7t.mjs → platform-cmd-DxxZO6e5.mjs} +17 -16
- package/dist/{platform-discovery-BC0HCKYt.mjs → platform-discovery-CHhNs8Nn.mjs} +1 -1
- package/dist/{platform-domain-p0kobLV8.mjs → platform-domain-zLXnW1G_.mjs} +5 -5
- package/dist/{platform-lifecycle-D_QEY32D.mjs → platform-lifecycle-DOW-wLfx.mjs} +1398 -2043
- package/dist/{platform-lifecycle-vFqtNPPa.mjs → platform-lifecycle-_gfnjUBr.mjs} +1 -1
- package/dist/{platform-management-oi-SvI3f.mjs → platform-management-CYrLvTGH.mjs} +1 -1
- package/dist/{platform-management-BdYblvvE.mjs → platform-management-t7HKy4-U.mjs} +632 -124
- package/dist/{platform-plans-config-CjiFv5J4.mjs → platform-plans-config-CNH6zbPX.mjs} +15 -11
- package/dist/{platform-recovery-PvNS9Nl2.mjs → platform-recovery-DeucI7A9.mjs} +6 -9
- package/dist/{platform-registry-P387v42A.mjs → platform-registry-B9mWX9LO.mjs} +18 -19
- package/dist/{platform-setup-DVe42Ws7.mjs → platform-setup-CTf6MMT9.mjs} +19 -26
- package/dist/{plugin-inference-BlQbOL0P.mjs → plugin-inference-DsARdQKE.mjs} +18 -33
- package/dist/{prepare-Dq9hTIbo.mjs → prepare-C4zfwks4.mjs} +3 -3
- package/dist/{prepare-Do96654b.mjs → prepare-CSH9dKTI.mjs} +11 -13
- package/dist/prepare-dr9uxQSV.mjs +2 -0
- package/dist/{prerender-render-Ct20jftc.mjs → prerender-render-Bfsumdkv.mjs} +8 -2
- package/dist/prerender-render.d.mts +2 -0
- package/dist/prerender-render.mjs +2 -2
- package/dist/project-DiiOBZPF.mjs +2 -0
- package/dist/{project-cmd-ti8_yTcA.mjs → project-cmd-Dq75AXzy.mjs} +19 -19
- package/dist/{project-paths-GgcMozYz.mjs → project-paths-DMvEeSau.mjs} +9 -14
- package/dist/{project-DQstYTxO.mjs → project-rqDnVsJN.mjs} +8 -4
- package/dist/{project-team-CDPE1Tsg.mjs → project-team-B3_QcStG.mjs} +6 -6
- package/dist/{project-token-CkZ1cN0Z.mjs → project-token-CvsRvv2I.mjs} +5 -5
- package/dist/{project-tsconfig-CVJhNMxs.mjs → project-tsconfig-bveqhlYA.mjs} +3 -5
- package/dist/{project-zero-trust-DdjLR61k.mjs → project-zero-trust-D1ICiwN_.mjs} +5 -5
- package/dist/{protocol-BPZaNqBo.d.mts → protocol-4H7V_3Os.d.mts} +1 -1
- package/dist/{protocol-CfaUwNs8.mjs → protocol-RNP0u7UY.mjs} +9 -3
- package/dist/{provision-BfaLiWbj.mjs → provision-DDh7ITVS.mjs} +118 -865
- package/dist/{requests-DFAqyYy8.mjs → requests-3CL3BuZh.mjs} +4 -4
- package/dist/resolve-project-B9Hr0D3n.mjs +2 -0
- package/dist/resolve-project-C_Cu7E8a.mjs +48 -0
- package/dist/{rollback-BKYXBduI.mjs → rollback-Ce1d_0J9.mjs} +7 -17
- package/dist/{route-types-CbtsIeFn.mjs → route-types-IrBuwJ7u.mjs} +35 -70
- package/dist/routing-assets-VkrKIbxE.mjs +139 -0
- package/dist/{runner-9wjsSZx-.mjs → runner-B5huK2eC.mjs} +1 -1
- package/dist/{runner-DMBKSrRk.mjs → runner-DcVDrzuR.mjs} +7 -28
- package/dist/{runner-mysql-CEzKtmFb.mjs → runner-mysql-CCtyI0p6.mjs} +4 -3
- package/dist/runner-mysql-CaJq-hd6.mjs +2 -0
- package/dist/runner-pg-BJhFZZG4.mjs +2 -0
- package/dist/{runner-pg-DV8p_zjA.mjs → runner-pg-CPd-CB_F.mjs} +1 -1
- package/dist/runtime/ai.mjs +9 -22
- package/dist/runtime/better-auth-mysql.d.mts +2 -5
- package/dist/runtime/better-auth-mysql.mjs +6 -31
- package/dist/runtime/better-auth-pg.d.mts +2 -5
- package/dist/runtime/better-auth-pg.mjs +6 -32
- package/dist/runtime/better-auth.d.mts +2 -4
- package/dist/runtime/better-auth.mjs +9 -19
- package/dist/runtime/db.mjs +1 -1
- package/dist/runtime/durable.d.mts +16 -0
- package/dist/runtime/durable.mjs +26 -2
- package/dist/runtime/email/testing.d.mts +1 -1
- package/dist/runtime/email/testing.mjs +9 -23
- package/dist/runtime/email-protocol.d.mts +12 -0
- package/dist/runtime/email-protocol.mjs +2 -70
- package/dist/runtime/email.d.mts +1 -1
- package/dist/runtime/email.mjs +79 -221
- package/dist/runtime/env-public.d.mts +1 -1
- package/dist/runtime/env-public.mjs +12 -38
- package/dist/runtime/env.d.mts +1 -12
- package/dist/runtime/env.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/handler.mjs +1 -1
- package/dist/runtime/isr.mjs +1 -1
- package/dist/runtime/kv.mjs +1 -1
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live-server.mjs +1 -16
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/live.mjs +1 -1
- package/dist/runtime/migration-handler-mysql.d.mts +2 -2
- package/dist/runtime/migration-handler-mysql.mjs +3 -2
- package/dist/runtime/migration-handler-pg.d.mts +2 -2
- package/dist/runtime/migration-handler-pg.mjs +1 -1
- package/dist/runtime/migration-handler.d.mts +2 -2
- package/dist/runtime/migration-handler.mjs +2 -470
- package/dist/runtime/queues.mjs +1 -1
- package/dist/runtime/remote/binding-handler.mjs +62 -62
- package/dist/runtime/remote/index.mjs +16 -77
- package/dist/runtime/routing.d.mts +1 -4
- package/dist/runtime/routing.mjs +82 -218
- package/dist/runtime/sandbox-container.d.mts +2 -2
- package/dist/runtime/sandbox-container.mjs +2 -2
- package/dist/runtime/sandbox.d.mts +2 -2
- package/dist/runtime/sandbox.mjs +13 -6
- package/dist/runtime/storage.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +2 -3
- package/dist/runtime/ws-server.mjs +6 -28
- package/dist/runtime/ws.d.mts +1 -1
- package/dist/{sandbox-container-Bo0eiFKz.d.mts → sandbox-container-ChQRum-j.d.mts} +3 -1
- package/dist/{sandbox-container-C6ItmVuN.mjs → sandbox-container-DBKIqDEK.mjs} +12 -1
- package/dist/{sandbox-C5LFoJMf.d.mts → sandbox-q3waMc2T.d.mts} +1 -1
- package/dist/{scan-C3uDzz6J.mjs → scan-C0BMdmEd.mjs} +84 -7
- package/dist/{secret-BAt2KMmO.mjs → secret-B5J8bBsI.mjs} +62 -80
- package/dist/{serialize-BPvnNQuA.mjs → serialize-BtQyaYoC.mjs} +10 -4
- package/dist/{skills-BgVMvQdy.mjs → skills-BJsan98C.mjs} +2 -2
- package/dist/{subcommand-prompt-BSlnD1fI.mjs → subcommand-prompt-gQ-NDA-u.mjs} +38 -43
- package/dist/sveltekit.mjs +1 -1
- package/dist/validate-C9xtFOYR.mjs +2 -0
- package/dist/{validate-C9qJArv7.mjs → validate-CheZGRE7.mjs} +4 -8
- package/dist/version-metadata-DIrqEHQo.mjs +73 -0
- package/dist/{wrangler-D9KTuqnv.mjs → wrangler-BYs07Rxy.mjs} +119 -378
- package/dist/{ws-CChrrAU-.d.mts → ws-aKVYB-fA.d.mts} +1 -1
- package/dist/yarn-pnp-DLbEFMT0.mjs +92 -0
- package/docs/guide/auth.md +2 -0
- package/docs/guide/database/postgresql.md +2 -0
- package/docs/guide/database.md +2 -0
- package/docs/guide/deployment.md +2 -2
- package/docs/guide/durable-state.md +24 -0
- package/docs/guide/platform/administration/access.md +5 -1
- package/docs/guide/platform/administration/connected-apps.md +55 -0
- package/docs/guide/platform/installation/credentials.md +3 -1
- package/docs/guide/platform/installation/first-deployment.md +1 -1
- package/docs/guide/project-collaboration.md +1 -1
- package/docs/guide/quickstart.md +2 -0
- package/docs/guide/sandboxes.md +2 -0
- package/docs/reference/api/deploy.md +23 -0
- package/docs/reference/api/pages.md +2 -0
- package/docs/reference/api.md +2 -1
- package/docs/reference/cli/auth.md +1 -1
- package/docs/reference/cli/database.md +9 -3
- package/docs/reference/cli/deploy.md +1 -1
- package/docs/reference/cli/platform-installation.md +1 -1
- package/docs/reference/cli/setup.md +2 -0
- package/package.json +14 -17
- package/skills/void/SKILL.md +4 -2
- package/dist/auth-BgX2qYSY.mjs +0 -2
- package/dist/cf-build-output-H9a97vME.mjs +0 -2
- package/dist/client-By4JjMw_.mjs +0 -2
- package/dist/collect-DLyFDiKW.mjs +0 -2
- package/dist/config-5r-YpMC-.mjs +0 -2
- package/dist/config-B_ggY-IO.mjs +0 -89
- package/dist/config-BtSFvXZx.mjs +0 -2
- package/dist/config-write-AHGesphx.mjs +0 -61
- package/dist/deploy-BsEvnlJl.mjs +0 -10455
- package/dist/deploy-C1d-YTfp.mjs +0 -2
- package/dist/dev-inbox-CK-UuPob.mjs +0 -297
- package/dist/discover-EDUf6JJw.mjs +0 -2
- package/dist/dist-_UUlL6bg.mjs +0 -2
- package/dist/env-validation-Dxdx_5wo.mjs +0 -951
- package/dist/gen-dA9hW7AV.mjs +0 -2
- package/dist/head-client-DzmJGN4C.mjs +0 -90
- package/dist/help-DzKUrQvk.mjs +0 -2
- package/dist/login-Dc-wuTDq.mjs +0 -2
- package/dist/migrate-BHp07yJE.mjs +0 -2
- package/dist/operator-cmd-Cwn50lOV.mjs +0 -2
- package/dist/pages/head-client.d.mts +0 -13
- package/dist/pages/head-client.mjs +0 -2
- package/dist/platform-args-DYgITk0D.mjs +0 -2
- package/dist/prepare-DRUV-g3X.mjs +0 -2
- package/dist/project-BiaPQ7ur.mjs +0 -2
- package/dist/provision-Wu9ZBk9L.mjs +0 -2
- package/dist/resolve-project-BIeYs9Cw.mjs +0 -2
- package/dist/resolve-project-CP8rZs_p.mjs +0 -28
- package/dist/runner-mysql-CkFqZ6CE.mjs +0 -2
- package/dist/runner-pg-BGYRFdRp.mjs +0 -2
- package/dist/runtime/env-helpers.d.mts +0 -2
- package/dist/runtime/env-helpers.mjs +0 -2
- package/dist/validate-BJr9LfXy.mjs +0 -2
- package/dist/yarn-pnp-CUX_iTPA.mjs +0 -188
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { a as join,
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import {
|
|
1
|
+
import { a as join, c as resolve, n as dirname } from "./dist-ipQi0Q8y.mjs";
|
|
2
|
+
import { t as writeFileAtomic } from "./atomic-write-Dq6R0BPN.mjs";
|
|
3
|
+
import { S as writePendingInstallationSecrets, a as latestPlatformWorkerCutoverGeneration, c as normalizePlatformApiUrl, d as platformRegistryDirectory, f as platformWorkerSecretUpdateIntentState, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, n as assertPlatformInstallationSlugAvailable, o as listInstallationManifests, p as promoteInstallationSecrets, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest, y as savePlatformConnection } from "./platform-registry-B9mWX9LO.mjs";
|
|
4
|
+
import { t as cfAccessHeaders } from "./cf-access-BM9G0lhO.mjs";
|
|
5
|
+
import { t as ensurePlatformAccessToken } from "./cf-access-CWP4avfP.mjs";
|
|
5
6
|
import { A as confirm, H as outro, K as select, L as log, U as password, X as text$1, tt as isCancel, z as multiselect } from "./dist-D3m88sjf.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { l as resolveCloudflareOperationArgv } from "./cloudflare-process-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { a as platformManagementSetupPage, c as platformSetupLink, i as platformInstallCommandArguments, l as platformSetupPages, n as formatPlatformSetupPlan, o as platformPromptAnswer, r as openPlatformSetupPage, s as platformSetupFieldHelp, t as formatPlatformInstallCommand } from "./platform-setup-
|
|
12
|
-
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync,
|
|
7
|
+
import { f as import_picocolors, l as formatUrl, m as printSuccess, n as cliCommandName, p as note, r as createSpinner, u as getErrorMessage } from "./output-FNHKw5Ac.mjs";
|
|
8
|
+
import { l as resolveCloudflareOperationArgv } from "./cloudflare-process-CeDWJRhj.mjs";
|
|
9
|
+
import { d as resolvePinnedApiBaseUrl, i as createDefaultRunWrangler } from "./provision-DDh7ITVS.mjs";
|
|
10
|
+
import { t as parsePlatformArgs } from "./platform-args-C6Mb_I3a.mjs";
|
|
11
|
+
import { o as runCloudflareAuthCommand } from "./cloudflare-auth-CqFBDo52.mjs";
|
|
12
|
+
import { a as platformManagementSetupPage, c as platformSetupLink, i as platformInstallCommandArguments, l as platformSetupPages, n as formatPlatformSetupPlan, o as platformPromptAnswer, r as openPlatformSetupPage, s as platformSetupFieldHelp, t as formatPlatformInstallCommand } from "./platform-setup-CTf6MMT9.mjs";
|
|
13
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { isRecord } from "@void/deploy-core";
|
|
15
|
-
import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY as REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAppAccessSetup, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep,
|
|
16
|
+
import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY as REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAppAccessSetup, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
16
17
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
17
18
|
import { tmpdir } from "node:os";
|
|
18
19
|
import { stripVTControlCharacters } from "node:util";
|
|
19
|
-
import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
20
|
+
import { cloudflareCommandDiagnostic, encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
20
21
|
import { createServer, request } from "node:http";
|
|
21
22
|
import { readFile } from "node:fs/promises";
|
|
22
23
|
import { PLATFORM_ADMIN_ASSETS_DIRECTORY, platformDatabaseSchemaDigest, resolvePlatformRuntime } from "@void/platform/installer";
|
|
@@ -201,7 +202,19 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
201
202
|
return null;
|
|
202
203
|
}
|
|
203
204
|
async listWorkerDomains(accountId) {
|
|
204
|
-
|
|
205
|
+
const result = [];
|
|
206
|
+
const perPage = 100;
|
|
207
|
+
for (let page = 1;; page++) {
|
|
208
|
+
const query = new URLSearchParams({
|
|
209
|
+
page: String(page),
|
|
210
|
+
per_page: String(perPage)
|
|
211
|
+
});
|
|
212
|
+
const response = await this.#requestEnvelope(`/accounts/${encodeURIComponent(accountId)}/workers/domains?${query}`);
|
|
213
|
+
result.push(...response.result);
|
|
214
|
+
const totalPages = response.resultInfo?.total_pages;
|
|
215
|
+
if (typeof totalPages === "number" && page >= totalPages || totalPages === void 0 && response.result.length < perPage) break;
|
|
216
|
+
}
|
|
217
|
+
return result.map((entry) => ({
|
|
205
218
|
hostname: requiredString(entry.hostname, "Worker custom domain hostname"),
|
|
206
219
|
id: requiredString(entry.id, "Worker custom domain id"),
|
|
207
220
|
service: requiredString(entry.service, "Worker custom domain service"),
|
|
@@ -241,26 +254,8 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
241
254
|
} catch (error) {
|
|
242
255
|
throw new PlatformWorkerCustomDomainsChangedError(`platform: Custom Domain publication for Worker ${JSON.stringify(workerName)} had an ambiguous outcome; retaining live domains for safe reconciliation.`, { cause: error });
|
|
243
256
|
}
|
|
244
|
-
const expected = [...origins].map((origin) => origin.hostname).sort();
|
|
245
257
|
const inventory = await this.listWorkerDomains(accountId);
|
|
246
|
-
|
|
247
|
-
if (!expected.every((hostname) => inventory.filter((domain) => domain.hostname === hostname && domain.service === workerName).length === 1) || expected.some((hostname) => !actual.includes(hostname))) throw new PlatformWorkerCustomDomainsChangedError(`platform: Worker ${JSON.stringify(workerName)} Custom Domains changed concurrently after publication; refusing to overwrite the newer traffic state.`);
|
|
248
|
-
}
|
|
249
|
-
async #listWorkerDomainRecords(accountId, service) {
|
|
250
|
-
const entries = [];
|
|
251
|
-
const perPage = 100;
|
|
252
|
-
for (let page = 1;; page++) {
|
|
253
|
-
const query = new URLSearchParams({
|
|
254
|
-
page: String(page),
|
|
255
|
-
per_page: String(perPage)
|
|
256
|
-
});
|
|
257
|
-
if (service) query.set("service", service);
|
|
258
|
-
const response = await this.#requestEnvelope(`/accounts/${encodeURIComponent(accountId)}/workers/domains?${query}`);
|
|
259
|
-
entries.push(...response.result);
|
|
260
|
-
const totalPages = response.resultInfo?.total_pages;
|
|
261
|
-
if (typeof totalPages === "number" && page >= totalPages || totalPages === void 0 && response.result.length < perPage) break;
|
|
262
|
-
}
|
|
263
|
-
return entries;
|
|
258
|
+
if (!origins.every(({ hostname }) => inventory.filter((domain) => domain.hostname === hostname && domain.service === workerName).length === 1)) throw new PlatformWorkerCustomDomainsChangedError(`platform: Worker ${JSON.stringify(workerName)} Custom Domains changed concurrently after publication; refusing to overwrite the newer traffic state.`);
|
|
264
259
|
}
|
|
265
260
|
async listWorkerRoutes(zoneId) {
|
|
266
261
|
return (await this.request(`/zones/${encodeURIComponent(zoneId)}/workers/routes`)).map((entry) => ({
|
|
@@ -586,9 +581,8 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
586
581
|
async deleteResource(accountId, resource, zoneId) {
|
|
587
582
|
const { kind } = resource;
|
|
588
583
|
if (kind === "ai-gateway" && this.#aiGatewayClient) return this.#aiGatewayClient.deleteResource(accountId, resource, zoneId);
|
|
589
|
-
const account = encodeURIComponent(accountId);
|
|
590
584
|
const encoded = encodeURIComponent(kind === "dispatch-namespace" || kind === "worker" ? resource.name : resource.id);
|
|
591
|
-
const path = kind
|
|
585
|
+
const path = deletionPath(kind, encodeURIComponent(accountId), encoded, zoneId);
|
|
592
586
|
if (!path) throw new Error(`platform: Unsupported Cloudflare deletion kind ${kind}.`);
|
|
593
587
|
await this.#deleteIfPresent(path);
|
|
594
588
|
}
|
|
@@ -718,9 +712,6 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
718
712
|
})
|
|
719
713
|
});
|
|
720
714
|
}
|
|
721
|
-
async removeRoutes(_accountId, _zoneId, tracked) {
|
|
722
|
-
if (tracked.routes.length > 0 || tracked.customDomains.length > 0) throw new Error("platform: Cloudflare does not support conditional Worker traffic deletion. Retaining routes and custom domains for explicit dashboard cleanup.");
|
|
723
|
-
}
|
|
724
715
|
};
|
|
725
716
|
function accessResourcePath(accountId, kind, id) {
|
|
726
717
|
return `/accounts/${encodeURIComponent(accountId)}/access/${kind === "application" ? "apps" : "service_tokens"}/${encodeURIComponent(id)}`;
|
|
@@ -734,6 +725,20 @@ function parseEmailRoutingTarget(rule) {
|
|
|
734
725
|
workerName: action?.type === "worker" && value.length === 1 && typeof value[0] === "string" ? value[0] : null
|
|
735
726
|
};
|
|
736
727
|
}
|
|
728
|
+
function deletionPath(kind, account, encoded, zoneId) {
|
|
729
|
+
switch (kind) {
|
|
730
|
+
case "kv-namespace": return `/accounts/${account}/storage/kv/namespaces/${encoded}`;
|
|
731
|
+
case "dns-record": return zoneId ? `/zones/${encodeURIComponent(zoneId)}/dns_records/${encoded}` : null;
|
|
732
|
+
case "ai-gateway": return `/accounts/${account}/ai-gateway/gateways/${encoded}`;
|
|
733
|
+
case "r2-bucket": return `/accounts/${account}/r2/buckets/${encoded}`;
|
|
734
|
+
case "queue": return `/accounts/${account}/queues/${encoded}`;
|
|
735
|
+
case "dispatch-namespace": return `/accounts/${account}/workers/dispatch/namespaces/${encoded}`;
|
|
736
|
+
case "hyperdrive": return `/accounts/${account}/hyperdrive/configs/${encoded}`;
|
|
737
|
+
case "worker": return `/accounts/${account}/workers/scripts/${encoded}`;
|
|
738
|
+
case "zone": return `/zones/${encoded}`;
|
|
739
|
+
default: return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
737
742
|
function cloudflareErrorDetails(envelope, status) {
|
|
738
743
|
return envelope.errors?.filter((error) => error.message).map((error) => error.code === void 0 ? error.message : `${error.message} (code ${error.code})`).join(", ") || `HTTP ${status}`;
|
|
739
744
|
}
|
|
@@ -872,6 +877,9 @@ function sameSet(left, right) {
|
|
|
872
877
|
function fingerprint(value) {
|
|
873
878
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
874
879
|
}
|
|
880
|
+
function platformAccessNonce(ownershipNonce) {
|
|
881
|
+
return createHash("sha256").update(`void-access:${ownershipNonce}`).digest("hex").slice(0, 32);
|
|
882
|
+
}
|
|
875
883
|
var PlatformAccessClient = class {
|
|
876
884
|
client;
|
|
877
885
|
accountId;
|
|
@@ -1015,8 +1023,6 @@ async function inspectPlatformAccess(client, input) {
|
|
|
1015
1023
|
};
|
|
1016
1024
|
}
|
|
1017
1025
|
async function provisionPlatformAccess(client, input, previous, save) {
|
|
1018
|
-
if (!input.login && !input.protection) return { resources: [] };
|
|
1019
|
-
if (client.accountId !== input.accountId) throw new Error("platform: Access account selection does not match its management client.");
|
|
1020
1026
|
const inspected = await inspectPlatformAccess(client, input);
|
|
1021
1027
|
const origins = [...new Set([
|
|
1022
1028
|
input.apiUrl,
|
|
@@ -1027,13 +1033,7 @@ async function provisionPlatformAccess(client, input, previous, save) {
|
|
|
1027
1033
|
if (url.origin !== value || url.protocol !== "https:") throw new Error("platform: Access requires exact HTTPS origins without credentials, paths, or query strings.");
|
|
1028
1034
|
return url.origin;
|
|
1029
1035
|
}))];
|
|
1030
|
-
const state =
|
|
1031
|
-
version: 1,
|
|
1032
|
-
accountId: input.accountId,
|
|
1033
|
-
installationId: input.installationId,
|
|
1034
|
-
nonce: randomBytes(16).toString("hex"),
|
|
1035
|
-
resources: {}
|
|
1036
|
-
};
|
|
1036
|
+
const state = structuredClone(previous);
|
|
1037
1037
|
if (state.version !== 1 || state.accountId !== input.accountId || state.installationId !== input.installationId || !/^[a-f0-9]{32}$/.test(state.nonce)) throw new Error("platform: Access recovery data belongs to a different installation.");
|
|
1038
1038
|
save(state);
|
|
1039
1039
|
const policies = input.policyIds.map((id, index) => ({
|
|
@@ -1569,7 +1569,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1569
1569
|
version: 1,
|
|
1570
1570
|
accountId,
|
|
1571
1571
|
installationId: input.plan.installationId,
|
|
1572
|
-
nonce:
|
|
1572
|
+
nonce: platformAccessNonce(input.ownershipNonce),
|
|
1573
1573
|
resources: {}
|
|
1574
1574
|
};
|
|
1575
1575
|
let loginClientSecret = spec.loginClientSecretEnv ? secretReference(spec.loginClientSecretEnv) : input.checkpoint.accessExistingLoginSecret;
|
|
@@ -1774,11 +1774,6 @@ async function selectInstallationAppAccess(authentication, recorded, choice) {
|
|
|
1774
1774
|
})));
|
|
1775
1775
|
if (selected === "public" && (config?.identityProviderIds?.length || config?.policyIds?.length)) throw new Error("platform: Public apps cannot select identity policies. Choose custom app access to require company login.");
|
|
1776
1776
|
if (selected === "platform" && !protectedPlatform) throw new Error("platform: Reusing the platform Access policy requires --access-protection. Choose --app-access custom to protect apps separately.");
|
|
1777
|
-
if (![
|
|
1778
|
-
"public",
|
|
1779
|
-
"platform",
|
|
1780
|
-
"custom"
|
|
1781
|
-
].includes(String(selected))) throw new Error("platform: Choose who can access deployed apps.");
|
|
1782
1777
|
return selected;
|
|
1783
1778
|
}
|
|
1784
1779
|
/** Collect the hosting-account policy once, with explicit consent for a durable management token. */
|
|
@@ -1800,7 +1795,6 @@ async function prepareInstallationAppAccess(input) {
|
|
|
1800
1795
|
}
|
|
1801
1796
|
}
|
|
1802
1797
|
const file = input.authentication.file?.appAccess;
|
|
1803
|
-
if (file?.tokenEnv && !/^[A-Z_][A-Z0-9_]*$/.test(file.tokenEnv)) throw new Error("platform: Invalid app access token environment reference.");
|
|
1804
1798
|
let token = explicitToken || (input.checkpoint.appAccessTokenRejected ? void 0 : input.checkpoint.appAccessToken);
|
|
1805
1799
|
if (input.checkpoint.appAccessTokenRejected) log.info("The previous app access token was rejected. Provide a replacement to continue.");
|
|
1806
1800
|
const setupSpec = input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : input.authentication.file?.cloudflareAccess;
|
|
@@ -2070,207 +2064,161 @@ async function verifyPlatformAuthenticationDatabase(client, accountId, resource,
|
|
|
2070
2064
|
if ((await read()).cachingDisabled !== true) throw new Error("platform: Could not verify uncached Hyperdrive reads. Authentication setup remains closed.");
|
|
2071
2065
|
}
|
|
2072
2066
|
//#endregion
|
|
2073
|
-
//#region src/cli/platform-installer-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
super(context?.accessProtected ? [
|
|
2077
|
-
"platform: Cloudflare Access blocked a protected platform health check (Default-Deny, error 1050).",
|
|
2078
|
-
`Blocked URL: ${url}`,
|
|
2079
|
-
"Keep employee Access protection and account-wide Default-Deny enabled.",
|
|
2080
|
-
"The reserved dispatch /health endpoint uses a path-only Access exception and still requires Void’s private probe token.",
|
|
2081
|
-
"Resume to verify the health exceptions and saved installer credentials.",
|
|
2082
|
-
...context.resumeCommand ? [`Resume: ${context.resumeCommand}`] : [],
|
|
2083
|
-
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
2084
|
-
].join("\n") : [
|
|
2085
|
-
"platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
|
|
2086
|
-
`Blocked URL: ${url}`,
|
|
2087
|
-
"",
|
|
2088
|
-
"To make this test platform publicly reachable:",
|
|
2089
|
-
"",
|
|
2090
|
-
`1. In ${context ? `account ${JSON.stringify(context.accountName)}` : "your Cloudflare account"}, open:`,
|
|
2091
|
-
" Zero Trust → Access controls → Applications → Create new application.",
|
|
2092
|
-
"2. Choose Self-hosted. When adding destinations, choose Worker and select:",
|
|
2093
|
-
` ${context ? context.workers.join(", ") + "." : "This installation's API, proxy, and dispatch Workers."}`,
|
|
2094
|
-
"3. Add a policy: Action → Bypass; Include → Everyone.",
|
|
2095
|
-
context?.resumeCommand ? `4. Save the policy and application, then resume:\n\n ${context.resumeCommand}` : "4. Save the policy and application, then rerun your platform command.",
|
|
2096
|
-
"",
|
|
2097
|
-
"This removes Cloudflare Access protection for these Workers.",
|
|
2098
|
-
"Void’s own authentication stays enabled. Keep account-wide Default-Deny on.",
|
|
2099
|
-
"",
|
|
2100
|
-
"For platforms that must remain Access-protected:",
|
|
2101
|
-
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
2102
|
-
].join("\n"));
|
|
2103
|
-
}
|
|
2104
|
-
};
|
|
2105
|
-
async function platformProbeFailure(label, url, response, context) {
|
|
2106
|
-
const message = `platform: ${label} failed at ${url} (HTTP ${response.status}).`;
|
|
2107
|
-
if (response.status === 403 && response.headers.get("server")?.toLowerCase() === "cloudflare" && response.headers.get("content-type")?.toLowerCase().includes("text/html")) {
|
|
2108
|
-
const body = await response.text().catch(() => "");
|
|
2109
|
-
if (body.includes("Default-Deny") && /errorCode:\s*1050\b/.test(body)) return new PlatformAccessBlockedError(url, context);
|
|
2110
|
-
}
|
|
2111
|
-
return new Error(message);
|
|
2067
|
+
//#region src/cli/platform-installer-process.ts
|
|
2068
|
+
function platformWorkerCommandRunner(accountId, cwd, runner) {
|
|
2069
|
+
return (operation, options) => runner(operation, accountId, cwd, options.capture);
|
|
2112
2070
|
}
|
|
2113
|
-
async function
|
|
2114
|
-
|
|
2115
|
-
headers: {
|
|
2116
|
-
"X-Void-Platform-Probe": healthProbeToken,
|
|
2117
|
-
...cfAccessHeaders(process.env, url)
|
|
2118
|
-
},
|
|
2119
|
-
redirect: "manual",
|
|
2120
|
-
signal: AbortSignal.timeout(3e4)
|
|
2121
|
-
});
|
|
2122
|
-
const dispatchUrl = dispatchOrigin ?? `https://${PLATFORM_HEALTH_HOSTNAME_LABEL}.${applicationBaseDomain}`;
|
|
2123
|
-
const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
|
|
2124
|
-
request(`${apiUrl}/health`),
|
|
2125
|
-
request(`${apiUrl}/.well-known/void-platform`),
|
|
2126
|
-
request(`${proxyUrl}/health`),
|
|
2127
|
-
request(`${dispatchUrl}/health`),
|
|
2128
|
-
context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
|
|
2129
|
-
]);
|
|
2130
|
-
if (!health.ok) throw await platformProbeFailure("Health check", `${apiUrl}/health`, health, context);
|
|
2131
|
-
if (!discovery.ok) throw await platformProbeFailure("Discovery check", `${apiUrl}/.well-known/void-platform`, discovery, context);
|
|
2132
|
-
const body = await discovery.json();
|
|
2133
|
-
if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
|
|
2134
|
-
if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
|
|
2135
|
-
if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
|
|
2136
|
-
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", `${proxyUrl}/health`, proxy, context);
|
|
2137
|
-
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", `${dispatchUrl}/health`, dispatch, context);
|
|
2138
|
-
if (emailGateway) {
|
|
2139
|
-
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", `${context.emailGatewayUrl}/health`, emailGateway, context);
|
|
2140
|
-
const identity = await emailGateway.json();
|
|
2141
|
-
if (identity?.installationId !== installationId || identity.platformVersion !== platformVersion || identity.emailProtocolVersion !== 1) throw new Error("platform: Deployed email gateway identity, version, or protocol does not match this installation.");
|
|
2142
|
-
}
|
|
2071
|
+
async function runPlatformWrangler(operation, accountId, cwd, capture) {
|
|
2072
|
+
return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
|
|
2143
2073
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2074
|
+
const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
|
|
2075
|
+
/** Opt-in maintainer diagnostics, enabled with VOID_PLATFORM_TIMINGS=1. */
|
|
2076
|
+
function writePlatformTiming(label, startedAt) {
|
|
2077
|
+
if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] ${label} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
|
|
2078
|
+
}
|
|
2079
|
+
async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false, apiToken) {
|
|
2080
|
+
const startedAt = performance.now();
|
|
2081
|
+
const result = await createDefaultRunWrangler(process.cwd(), {
|
|
2082
|
+
launcher: resolveCloudflareOperationArgv(),
|
|
2083
|
+
parallelWorkerUpload,
|
|
2084
|
+
timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
|
|
2085
|
+
})(operation, {
|
|
2086
|
+
accountId,
|
|
2087
|
+
apiBaseUrl,
|
|
2088
|
+
apiToken,
|
|
2089
|
+
cwd
|
|
2152
2090
|
});
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2091
|
+
writePlatformTiming(`Cloudflare ${operation.type}`, startedAt);
|
|
2092
|
+
const stdout = result.stdout || "";
|
|
2093
|
+
const stderr = result.stderr || "";
|
|
2094
|
+
if (!capture && result.status !== 0) {
|
|
2095
|
+
if (stdout) process.stdout.write(stdout);
|
|
2096
|
+
if (stderr) process.stderr.write(stderr);
|
|
2159
2097
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
return status.state === "complete";
|
|
2098
|
+
return {
|
|
2099
|
+
...result,
|
|
2100
|
+
stderr,
|
|
2101
|
+
stdout
|
|
2102
|
+
};
|
|
2166
2103
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2104
|
+
const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
2105
|
+
"connection",
|
|
2106
|
+
"keep-alive",
|
|
2107
|
+
"proxy-authenticate",
|
|
2108
|
+
"proxy-authorization",
|
|
2109
|
+
"te",
|
|
2110
|
+
"trailer",
|
|
2111
|
+
"transfer-encoding",
|
|
2112
|
+
"upgrade"
|
|
2113
|
+
]);
|
|
2114
|
+
function forwardHeaders(headers) {
|
|
2115
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => !HOP_BY_HOP_HEADERS.has(name.toLowerCase())));
|
|
2116
|
+
}
|
|
2117
|
+
function closeHttpServer(server) {
|
|
2118
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
2119
|
+
server.close((error) => {
|
|
2120
|
+
if (error) rejectPromise(error);
|
|
2121
|
+
else resolvePromise();
|
|
2122
|
+
});
|
|
2123
|
+
server.closeAllConnections();
|
|
2175
2124
|
});
|
|
2176
|
-
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.`);
|
|
2177
|
-
let value;
|
|
2178
|
-
try {
|
|
2179
|
-
value = await response.json();
|
|
2180
|
-
} catch {
|
|
2181
|
-
throw new Error("platform: Deployment execution drain probe returned an invalid document.");
|
|
2182
|
-
}
|
|
2183
|
-
const status = value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2184
|
-
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.");
|
|
2185
|
-
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.");
|
|
2186
2125
|
}
|
|
2187
|
-
async function
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
const
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2126
|
+
async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName) {
|
|
2127
|
+
const upstreamBase = new URL(upstreamApiBaseUrl);
|
|
2128
|
+
if (upstreamBase.protocol !== "http:" && upstreamBase.protocol !== "https:") throw new Error("platform: Cloudflare API base URL must use HTTP or HTTPS.");
|
|
2129
|
+
const basePath = upstreamBase.pathname.replace(/\/+$/, "");
|
|
2130
|
+
const accountPath = `${basePath}/accounts/${encodeURIComponent(accountId)}`;
|
|
2131
|
+
const expectedPath = `${accountPath}/workers/scripts/${encodeURIComponent(workerName)}`;
|
|
2132
|
+
const assetUploadPath = `${accountPath}/workers/assets/upload`;
|
|
2133
|
+
const state = {
|
|
2134
|
+
assetSessionStarted: false,
|
|
2135
|
+
attempted: false,
|
|
2136
|
+
conflictStatus: null,
|
|
2137
|
+
created: false,
|
|
2138
|
+
forwarded: false,
|
|
2139
|
+
rejectedMutation: null
|
|
2195
2140
|
};
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
try {
|
|
2204
|
-
response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
|
|
2205
|
-
method: "POST",
|
|
2206
|
-
headers: {
|
|
2207
|
-
"X-Void-Platform-Probe": healthProbeToken,
|
|
2208
|
-
...cfAccessHeaders(process.env, apiUrl)
|
|
2209
|
-
},
|
|
2210
|
-
signal,
|
|
2211
|
-
redirect: "manual"
|
|
2212
|
-
});
|
|
2213
|
-
} catch (error) {
|
|
2214
|
-
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
2215
|
-
await waitForProgress();
|
|
2216
|
-
continue;
|
|
2217
|
-
}
|
|
2218
|
-
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.`);
|
|
2219
|
-
}
|
|
2220
|
-
try {
|
|
2221
|
-
value = await response.json();
|
|
2222
|
-
} catch (error) {
|
|
2223
|
-
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
2224
|
-
await waitForProgress();
|
|
2225
|
-
continue;
|
|
2141
|
+
const server = createServer((incoming, outgoing) => {
|
|
2142
|
+
(async () => {
|
|
2143
|
+
const incomingUrl = new URL(incoming.url ?? "/", "http://127.0.0.1");
|
|
2144
|
+
if (incomingUrl.pathname !== basePath && !incomingUrl.pathname.startsWith(`${basePath}/`)) {
|
|
2145
|
+
outgoing.writeHead(502, { "Content-Type": "application/json" });
|
|
2146
|
+
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Invalid API proxy path\"}]}");
|
|
2147
|
+
return;
|
|
2226
2148
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
if (
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2149
|
+
const createQuery = [...incomingUrl.searchParams];
|
|
2150
|
+
const supportedCreateQuery = new Set(createQuery.map(([key]) => key)).size === createQuery.length && createQuery.every(([key, value]) => key === "excludeScript" && value === "true" || key === "bindings_inherit" && value === "strict");
|
|
2151
|
+
const isConditionalCreate = incoming.method === "PUT" && incomingUrl.pathname === expectedPath && supportedCreateQuery;
|
|
2152
|
+
const isTopLevelWorkerUpload = incoming.method === "PUT" && new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/accounts/[^/]+/workers/scripts/[^/]+$`).test(incomingUrl.pathname);
|
|
2153
|
+
const isReadOnly = incoming.method === "GET" || incoming.method === "HEAD" || incoming.method === "OPTIONS";
|
|
2154
|
+
const isAssetUploadSession = incoming.method === "POST" && incomingUrl.pathname === `${expectedPath}/assets-upload-session` && incomingUrl.search === "";
|
|
2155
|
+
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)));
|
|
2156
|
+
if (isTopLevelWorkerUpload && !isConditionalCreate || !isReadOnly && !isConditionalCreate && !isAssetUploadSession && !isAssetUpload && !state.created) {
|
|
2157
|
+
state.rejectedMutation = `${incoming.method ?? "UNKNOWN"} ${incomingUrl.pathname}${incomingUrl.pathname === expectedPath && !supportedCreateQuery ? " (unsupported Worker create query)" : ""}`;
|
|
2158
|
+
incoming.resume();
|
|
2159
|
+
outgoing.writeHead(409, { "Content-Type": "application/json" });
|
|
2160
|
+
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Mutation blocked until conditional Worker creation succeeds\"}]}");
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const headers = forwardHeaders(incoming.headers);
|
|
2164
|
+
delete headers.host;
|
|
2165
|
+
if (isConditionalCreate) {
|
|
2166
|
+
state.attempted = true;
|
|
2167
|
+
headers["if-none-match"] = "*";
|
|
2168
|
+
}
|
|
2169
|
+
if (isAssetUploadSession) state.assetSessionStarted = true;
|
|
2170
|
+
const upstreamUrl = new URL(`${incomingUrl.pathname}${incomingUrl.search}`, upstreamBase.origin);
|
|
2171
|
+
const request$2 = (upstreamUrl.protocol === "https:" ? request$1 : request)(upstreamUrl, {
|
|
2172
|
+
headers,
|
|
2173
|
+
method: incoming.method
|
|
2174
|
+
}, (response) => {
|
|
2175
|
+
if (isConditionalCreate) {
|
|
2176
|
+
state.forwarded = true;
|
|
2177
|
+
if (response.statusCode === 409 || response.statusCode === 412) state.conflictStatus = response.statusCode;
|
|
2178
|
+
state.created = response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300;
|
|
2179
|
+
}
|
|
2180
|
+
const responseHeaders = forwardHeaders(response.headers);
|
|
2181
|
+
outgoing.writeHead(response.statusCode ?? 502, responseHeaders);
|
|
2182
|
+
response.pipe(outgoing);
|
|
2183
|
+
});
|
|
2184
|
+
request$2.on("error", () => {
|
|
2185
|
+
if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
|
|
2186
|
+
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy request failed\"}]}");
|
|
2187
|
+
});
|
|
2188
|
+
incoming.pipe(request$2);
|
|
2189
|
+
})().catch(() => {
|
|
2190
|
+
if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
|
|
2191
|
+
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy failed\"}]}");
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
2195
|
+
server.once("error", rejectPromise);
|
|
2196
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2197
|
+
server.off("error", rejectPromise);
|
|
2198
|
+
resolvePromise();
|
|
2199
|
+
});
|
|
2200
|
+
});
|
|
2201
|
+
return {
|
|
2202
|
+
apiBaseUrl: `http://127.0.0.1:${server.address().port}${basePath}`,
|
|
2203
|
+
close: () => closeHttpServer(server),
|
|
2204
|
+
state
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
async function withConditionalPlatformWorkerCreate(accountId, workerName, operation, upstreamApiBaseUrl = resolvePinnedApiBaseUrl()) {
|
|
2208
|
+
const proxy = await startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName);
|
|
2209
|
+
let result;
|
|
2210
|
+
try {
|
|
2211
|
+
result = await operation(proxy.apiBaseUrl);
|
|
2212
|
+
} finally {
|
|
2213
|
+
await proxy.close();
|
|
2272
2214
|
}
|
|
2273
|
-
|
|
2215
|
+
if (proxy.state.rejectedMutation) throw new Error(`platform: Wrangler attempted unexpected mutation ${proxy.state.rejectedMutation} while conditionally creating new Worker ${workerName}; the mutation was blocked.`);
|
|
2216
|
+
if (!proxy.state.attempted || !proxy.state.forwarded) throw new Error(`platform: Wrangler did not use the guarded create endpoint for new Worker ${workerName}; refusing an unverified deployment.`);
|
|
2217
|
+
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.`);
|
|
2218
|
+
return result;
|
|
2219
|
+
}
|
|
2220
|
+
async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl, apiToken) {
|
|
2221
|
+
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl, false, apiToken), upstreamApiBaseUrl);
|
|
2274
2222
|
}
|
|
2275
2223
|
//#endregion
|
|
2276
2224
|
//#region src/cli/platform-plan.ts
|
|
@@ -2383,8 +2331,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2383
2331
|
}
|
|
2384
2332
|
throw error;
|
|
2385
2333
|
});
|
|
2386
|
-
const [
|
|
2387
|
-
Promise.resolve(accountSubdomain),
|
|
2334
|
+
const [aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, deploymentQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
|
|
2388
2335
|
gatewayInventory,
|
|
2389
2336
|
client.listKvNamespaces(account.id, names.routingKv),
|
|
2390
2337
|
client.listKvNamespaces(account.id, names.isrCacheKv),
|
|
@@ -2405,10 +2352,20 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2405
2352
|
const bucket = r2Buckets.find((resource) => resource.name === names.assetsBucket);
|
|
2406
2353
|
const recordedBucket = existing.resources.find((resource) => resource.key === "assets-bucket" && resource.kind === "r2-bucket" && resource.ownership === "created" && resource.state === "ready");
|
|
2407
2354
|
if (bucket && recordedBucket?.id === bucket.id && recordedBucket.name === bucket.name) {
|
|
2408
|
-
if (
|
|
2355
|
+
if (resourceMarkerProblem(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, {
|
|
2356
|
+
id: bucket.id,
|
|
2357
|
+
key: "assets-bucket",
|
|
2358
|
+
kind: "r2-bucket",
|
|
2359
|
+
name: bucket.name
|
|
2360
|
+
}) || !matchesRecordedR2Fingerprint(recordedBucket.configHash, bucket)) ambiguousResources.add("assets-bucket");
|
|
2409
2361
|
}
|
|
2410
2362
|
if (bucket && hasUnrecordedProvisionIntent(existing, "assets-bucket", "r2-bucket", bucket.name)) {
|
|
2411
|
-
if (
|
|
2363
|
+
if (!resourceMarkerProblem(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, {
|
|
2364
|
+
id: bucket.id,
|
|
2365
|
+
key: "assets-bucket",
|
|
2366
|
+
kind: "r2-bucket",
|
|
2367
|
+
name: bucket.name
|
|
2368
|
+
})) intentProofs.add("assets-bucket");
|
|
2412
2369
|
}
|
|
2413
2370
|
for (const [key, name] of [
|
|
2414
2371
|
["api-worker", names.apiWorker],
|
|
@@ -2417,8 +2374,10 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2417
2374
|
["tail-worker", names.tailWorker],
|
|
2418
2375
|
["dispatch-worker", names.dispatchWorker]
|
|
2419
2376
|
]) if (workers.includes(name) && hasUnrecordedProvisionIntent(existing, key, "worker", name)) {
|
|
2420
|
-
if (
|
|
2377
|
+
if (!resourceMarkerProblem((await client.getWorkerPlainTextBindings(account.id, name)).PLATFORM_OWNERSHIP_MARKER, existing, {
|
|
2421
2378
|
id: name,
|
|
2379
|
+
key,
|
|
2380
|
+
kind: "worker",
|
|
2422
2381
|
name
|
|
2423
2382
|
})) intentProofs.add(key);
|
|
2424
2383
|
}
|
|
@@ -2446,6 +2405,14 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2446
2405
|
if (records.length > 0 && (!matchingDomain || !ownedApiWorker)) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} already has a DNS record that installation would replace. Choose another hostname or remove the record explicitly.`);
|
|
2447
2406
|
}
|
|
2448
2407
|
const actions = [];
|
|
2408
|
+
const named = {
|
|
2409
|
+
actions,
|
|
2410
|
+
existing,
|
|
2411
|
+
options,
|
|
2412
|
+
remoteOwnership,
|
|
2413
|
+
intentProofs,
|
|
2414
|
+
ambiguousResources
|
|
2415
|
+
};
|
|
2449
2416
|
if (!testing) {
|
|
2450
2417
|
const recordedZone = existing?.resources.find((resource) => resource.key === "zone");
|
|
2451
2418
|
const remotelyOwnedZone = remoteOwnership?.resources.some((resource) => resource.key === "zone" && resource.id === zone?.id);
|
|
@@ -2469,15 +2436,60 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2469
2436
|
name: names.aiGateway,
|
|
2470
2437
|
reason: "AI Gateway access will be verified with the runtime token before provisioning (AI Gateway: Edit required)."
|
|
2471
2438
|
});
|
|
2472
|
-
else addNamedPlan(
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
addNamedPlan(
|
|
2479
|
-
|
|
2480
|
-
|
|
2439
|
+
else addNamedPlan(named, {
|
|
2440
|
+
key: "ai-gateway",
|
|
2441
|
+
kind: "ai-gateway",
|
|
2442
|
+
name: names.aiGateway,
|
|
2443
|
+
matches: aiGateways.filter((resource) => resource.id === names.aiGateway)
|
|
2444
|
+
});
|
|
2445
|
+
addNamedPlan(named, {
|
|
2446
|
+
key: "routing-kv",
|
|
2447
|
+
kind: "kv-namespace",
|
|
2448
|
+
name: names.routingKv,
|
|
2449
|
+
matches: routingKv
|
|
2450
|
+
});
|
|
2451
|
+
addNamedPlan(named, {
|
|
2452
|
+
key: "isr-cache-kv",
|
|
2453
|
+
kind: "kv-namespace",
|
|
2454
|
+
name: names.isrCacheKv,
|
|
2455
|
+
matches: isrKv
|
|
2456
|
+
});
|
|
2457
|
+
addNamedPlan(named, {
|
|
2458
|
+
key: "legacy-assets-kv",
|
|
2459
|
+
kind: "kv-namespace",
|
|
2460
|
+
name: names.legacyAssetsKv,
|
|
2461
|
+
matches: legacyKv
|
|
2462
|
+
});
|
|
2463
|
+
addNamedPlan(named, {
|
|
2464
|
+
key: "assets-bucket",
|
|
2465
|
+
kind: "r2-bucket",
|
|
2466
|
+
name: names.assetsBucket,
|
|
2467
|
+
matches: r2Buckets.filter((resource) => resource.name === names.assetsBucket)
|
|
2468
|
+
});
|
|
2469
|
+
addNamedPlan(named, {
|
|
2470
|
+
key: "usage-queue",
|
|
2471
|
+
kind: "queue",
|
|
2472
|
+
name: names.usageQueue,
|
|
2473
|
+
matches: usageQueues
|
|
2474
|
+
});
|
|
2475
|
+
addNamedPlan(named, {
|
|
2476
|
+
key: "cron-queue",
|
|
2477
|
+
kind: "queue",
|
|
2478
|
+
name: names.cronQueue,
|
|
2479
|
+
matches: cronQueues
|
|
2480
|
+
});
|
|
2481
|
+
if (options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue")) addNamedPlan(named, {
|
|
2482
|
+
key: "deployment-queue",
|
|
2483
|
+
kind: "queue",
|
|
2484
|
+
name: names.deploymentQueue,
|
|
2485
|
+
matches: deploymentQueues
|
|
2486
|
+
});
|
|
2487
|
+
addNamedPlan(named, {
|
|
2488
|
+
key: "dispatch-namespace",
|
|
2489
|
+
kind: "dispatch-namespace",
|
|
2490
|
+
name: names.dispatchNamespace,
|
|
2491
|
+
matches: dispatchNamespaces.filter((resource) => resource.name === names.dispatchNamespace)
|
|
2492
|
+
});
|
|
2481
2493
|
const recordedHyperdrive = existing?.resources.find((resource) => resource.key === "hyperdrive");
|
|
2482
2494
|
const externalHyperdriveId = input.externalHyperdrive?.id || (recordedHyperdrive?.ownership === "adopted" ? recordedHyperdrive.id : "");
|
|
2483
2495
|
if (externalHyperdriveId) {
|
|
@@ -2494,17 +2506,28 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2494
2506
|
name: remote.name,
|
|
2495
2507
|
reason: recordedHyperdrive ? "Reuse the recorded externally managed Hyperdrive." : "Use the externally managed Hyperdrive without taking ownership."
|
|
2496
2508
|
});
|
|
2497
|
-
} else addNamedPlan(
|
|
2509
|
+
} else addNamedPlan(named, {
|
|
2510
|
+
key: "hyperdrive",
|
|
2511
|
+
kind: "hyperdrive",
|
|
2512
|
+
name: names.hyperdrive,
|
|
2513
|
+
matches: hyperdrives.filter((resource) => resource.name === names.hyperdrive)
|
|
2514
|
+
});
|
|
2498
2515
|
for (const [key, name] of [
|
|
2499
2516
|
["api-worker", names.apiWorker],
|
|
2500
2517
|
["proxy-worker", names.proxyWorker],
|
|
2501
2518
|
...options?.includeEmailGateway === false ? [] : [["email-gateway-worker", names.emailGatewayWorker]],
|
|
2502
2519
|
["tail-worker", names.tailWorker],
|
|
2503
2520
|
["dispatch-worker", names.dispatchWorker]
|
|
2504
|
-
]) addNamedPlan(
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2521
|
+
]) addNamedPlan(named, {
|
|
2522
|
+
key,
|
|
2523
|
+
kind: "worker",
|
|
2524
|
+
name,
|
|
2525
|
+
matches: workers.includes(name) ? [{
|
|
2526
|
+
id: name,
|
|
2527
|
+
name
|
|
2528
|
+
}] : [],
|
|
2529
|
+
existingAction: "update"
|
|
2530
|
+
});
|
|
2508
2531
|
const resolvedResources = { ...deployment.resources };
|
|
2509
2532
|
for (const action of actions) {
|
|
2510
2533
|
const resourceKey = RESOURCE_NAME_KEY_BY_ACTION[action.key];
|
|
@@ -2514,8 +2537,8 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2514
2537
|
...deployment,
|
|
2515
2538
|
resources: resolvedResources
|
|
2516
2539
|
});
|
|
2517
|
-
const apiUrl = resolvedDeployment.controlPlane.apiHostname ? `https://${deployment.controlPlane.apiHostname}` : `https://${resolvedDeployment.resources.apiWorker}.${
|
|
2518
|
-
const proxyUrl =
|
|
2540
|
+
const apiUrl = resolvedDeployment.controlPlane.apiHostname ? `https://${deployment.controlPlane.apiHostname}` : `https://${resolvedDeployment.resources.apiWorker}.${accountSubdomain}.workers.dev`;
|
|
2541
|
+
const proxyUrl = `https://${resolvedDeployment.resources.proxyWorker}.${accountSubdomain}.workers.dev`;
|
|
2519
2542
|
return {
|
|
2520
2543
|
account,
|
|
2521
2544
|
actions,
|
|
@@ -2525,10 +2548,13 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2525
2548
|
...input.externalHyperdrive ? { externalHyperdrive: input.externalHyperdrive } : {},
|
|
2526
2549
|
installationId,
|
|
2527
2550
|
proxyUrl,
|
|
2528
|
-
workersSubdomain,
|
|
2551
|
+
workersSubdomain: accountSubdomain,
|
|
2529
2552
|
zone
|
|
2530
2553
|
};
|
|
2531
2554
|
}
|
|
2555
|
+
function platformWorkersDevUrl(plan, workerName) {
|
|
2556
|
+
return `https://${workerName}.${plan.workersSubdomain}.workers.dev`;
|
|
2557
|
+
}
|
|
2532
2558
|
async function revalidatePlatformInstallAction(client, original, manifest, action, options) {
|
|
2533
2559
|
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) => {
|
|
2534
2560
|
const resource = manifest.resources.find((candidate) => candidate.key === entry.key);
|
|
@@ -2536,8 +2562,10 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
|
|
|
2536
2562
|
})) {
|
|
2537
2563
|
const recorded = manifest.resources.find((resource) => resource.key === action.key);
|
|
2538
2564
|
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.`);
|
|
2539
|
-
if (
|
|
2565
|
+
if (resourceMarkerProblem((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, {
|
|
2540
2566
|
id: action.name,
|
|
2567
|
+
key: action.key,
|
|
2568
|
+
kind: "worker",
|
|
2541
2569
|
name: action.name
|
|
2542
2570
|
})) throw new Error(`platform: Cloudflare Worker ${JSON.stringify(action.name)} no longer has this installation's ownership marker. Review a new plan before retrying.`);
|
|
2543
2571
|
return;
|
|
@@ -2560,8 +2588,10 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
|
|
|
2560
2588
|
})).actions.find((entry) => entry.key === action.key);
|
|
2561
2589
|
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.`);
|
|
2562
2590
|
if (action.kind === "worker" && action.action === "update") {
|
|
2563
|
-
if (
|
|
2591
|
+
if (resourceMarkerProblem((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, {
|
|
2564
2592
|
id: action.name,
|
|
2593
|
+
key: action.key,
|
|
2594
|
+
kind: "worker",
|
|
2565
2595
|
name: action.name
|
|
2566
2596
|
})) throw new Error(`platform: Cloudflare Worker ${JSON.stringify(action.name)} no longer has this installation's ownership marker. Review a new plan before retrying.`);
|
|
2567
2597
|
}
|
|
@@ -2618,7 +2648,9 @@ function resolveAccount(accounts, selector) {
|
|
|
2618
2648
|
if (matches.length !== 1) throw new Error(matches.length === 0 ? `platform: Cloudflare account ${JSON.stringify(selector)} is not available to this token.` : "platform: Select a Cloudflare account with --account <id>.");
|
|
2619
2649
|
return matches[0];
|
|
2620
2650
|
}
|
|
2621
|
-
function addNamedPlan(
|
|
2651
|
+
function addNamedPlan(context, target) {
|
|
2652
|
+
const { actions, existing, options, remoteOwnership, intentProofs, ambiguousResources } = context;
|
|
2653
|
+
const { key, kind, name, matches, existingAction = "reuse" } = target;
|
|
2622
2654
|
if (matches.length > 1) throw new Error(`platform: More than one ${kind} named ${JSON.stringify(name)} exists.`);
|
|
2623
2655
|
const remote = matches[0];
|
|
2624
2656
|
const recorded = existing?.resources.find((resource) => resource.key === key);
|
|
@@ -2685,15 +2717,16 @@ function addNamedPlan(actions, existing, key, kind, name, matches, existingActio
|
|
|
2685
2717
|
});
|
|
2686
2718
|
}
|
|
2687
2719
|
function hasUnrecordedProvisionIntent(manifest, key, kind, name) {
|
|
2688
|
-
return !manifest.resources.some((resource) => resource.key === key && resource.state === "ready") &&
|
|
2720
|
+
return !manifest.resources.some((resource) => resource.key === key && resource.state === "ready") && hasProvisionIntentStep(manifest, key, kind, name);
|
|
2689
2721
|
}
|
|
2690
2722
|
function hasAmbiguousProvisionIntent(manifest, key, kind, remoteId, name) {
|
|
2691
2723
|
const recorded = manifest.resources.find((resource) => resource.key === key);
|
|
2692
2724
|
if (recorded?.id === remoteId && recorded.kind === kind && recorded.name === name) return false;
|
|
2693
|
-
return
|
|
2725
|
+
return hasProvisionIntentStep(manifest, key, kind, name, recorded?.id ?? null);
|
|
2694
2726
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2727
|
+
/** Whether the manifest records an intent to provision this resource. */
|
|
2728
|
+
function hasProvisionIntentStep(manifest, key, kind, name, previousResourceId = null) {
|
|
2729
|
+
return manifest.steps.some((step) => (step.key === `provision-intent:${key}` || step.key.startsWith(`provision-intent:${key}:`)) && step.resultHash === platformProvisionIntentHash(manifest, key, kind, name, previousResourceId));
|
|
2697
2730
|
}
|
|
2698
2731
|
function interruptedProvisionRetryName(manifest, key, kind, name) {
|
|
2699
2732
|
const suffix = `-retry-${createHash("sha256").update(JSON.stringify({
|
|
@@ -2704,36 +2737,53 @@ function interruptedProvisionRetryName(manifest, key, kind, name) {
|
|
|
2704
2737
|
})).digest("hex").slice(0, 32)}`;
|
|
2705
2738
|
return `${name.slice(0, 63 - suffix.length).replace(/-+$/, "")}${suffix}`;
|
|
2706
2739
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
name: resource.name
|
|
2740
|
+
/** Name-only config hash, recorded for resources without a live fingerprint. */
|
|
2741
|
+
function legacyResourceConfigHash(kind, name) {
|
|
2742
|
+
return createHash("sha256").update(JSON.stringify({
|
|
2743
|
+
kind,
|
|
2744
|
+
name
|
|
2713
2745
|
})).digest("hex");
|
|
2714
|
-
|
|
2746
|
+
}
|
|
2747
|
+
function r2BucketConfigHash(name, fingerprint) {
|
|
2748
|
+
return createHash("sha256").update(JSON.stringify({
|
|
2749
|
+
fingerprint,
|
|
2715
2750
|
kind: "r2-bucket",
|
|
2716
|
-
name
|
|
2751
|
+
name
|
|
2752
|
+
})).digest("hex");
|
|
2753
|
+
}
|
|
2754
|
+
function dnsRecordConfigHash(name, type, content, proxied) {
|
|
2755
|
+
return createHash("sha256").update(JSON.stringify({
|
|
2756
|
+
content,
|
|
2757
|
+
kind: "dns-record",
|
|
2758
|
+
name,
|
|
2759
|
+
proxied,
|
|
2760
|
+
type
|
|
2717
2761
|
})).digest("hex");
|
|
2718
|
-
return configHash === liveHash || configHash === legacyHash;
|
|
2719
2762
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2763
|
+
/** Whether a live R2 bucket still matches the creation fingerprint recorded for it. */
|
|
2764
|
+
function matchesRecordedR2Fingerprint(configHash, bucket) {
|
|
2765
|
+
if (!bucket.fingerprint) return false;
|
|
2766
|
+
return configHash === r2BucketConfigHash(bucket.name, bucket.fingerprint) || configHash === legacyResourceConfigHash("r2-bucket", bucket.name);
|
|
2767
|
+
}
|
|
2768
|
+
/** Why `value` is not this installation's ownership marker for the resource, or null when it is. */
|
|
2769
|
+
function resourceMarkerProblem(value, manifest, resource) {
|
|
2770
|
+
if (!value) return "has no ownership marker";
|
|
2771
|
+
let actual;
|
|
2722
2772
|
try {
|
|
2723
|
-
|
|
2724
|
-
const expected = createPlatformResourceOwnershipMarker({
|
|
2725
|
-
accountId: manifest.account.id,
|
|
2726
|
-
installationId: manifest.id,
|
|
2727
|
-
ownershipNonce: manifest.ownershipNonce,
|
|
2728
|
-
resourceId: resource.id,
|
|
2729
|
-
resourceKey: key,
|
|
2730
|
-
resourceKind: kind,
|
|
2731
|
-
resourceName: resource.name
|
|
2732
|
-
});
|
|
2733
|
-
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
2773
|
+
actual = parsePlatformResourceOwnershipMarker(JSON.parse(value));
|
|
2734
2774
|
} catch {
|
|
2735
|
-
return
|
|
2775
|
+
return "has an invalid ownership marker";
|
|
2736
2776
|
}
|
|
2777
|
+
const expected = createPlatformResourceOwnershipMarker({
|
|
2778
|
+
accountId: manifest.account.id,
|
|
2779
|
+
installationId: manifest.id,
|
|
2780
|
+
ownershipNonce: manifest.ownershipNonce,
|
|
2781
|
+
resourceId: resource.id,
|
|
2782
|
+
resourceKey: resource.key,
|
|
2783
|
+
resourceKind: resource.kind,
|
|
2784
|
+
resourceName: resource.name
|
|
2785
|
+
});
|
|
2786
|
+
return JSON.stringify(actual) === JSON.stringify(expected) ? null : "belongs to another installation";
|
|
2737
2787
|
}
|
|
2738
2788
|
function isLegacyUniqueProvisionAttempt(manifest, key, kind, name) {
|
|
2739
2789
|
const resourceSlug = resourceSlugForManifest(manifest);
|
|
@@ -2896,12 +2946,6 @@ function platformTrafficAttachmentWorker(deployment, resource) {
|
|
|
2896
2946
|
}
|
|
2897
2947
|
return null;
|
|
2898
2948
|
}
|
|
2899
|
-
function trackedPlatformWorkerTraffic(_manifest, _workers) {
|
|
2900
|
-
return {
|
|
2901
|
-
customDomains: [],
|
|
2902
|
-
routes: []
|
|
2903
|
-
};
|
|
2904
|
-
}
|
|
2905
2949
|
function recordPlatformWorkerTraffic(manifest, workerName, inventory, updatedAt) {
|
|
2906
2950
|
const desired = platformManagedWorkerTraffic(manifest.deployment, /* @__PURE__ */ new Set([workerName]));
|
|
2907
2951
|
let next = manifest;
|
|
@@ -3009,17 +3053,17 @@ function saveInstalledPlatformConnection(plan, manifest) {
|
|
|
3009
3053
|
async function writeRemoteInstallationCheckpointWithoutDatabase(client, plan, manifest, databaseLock) {
|
|
3010
3054
|
const startedAt = performance.now();
|
|
3011
3055
|
await databaseLock?.checkpoint?.(manifest);
|
|
3012
|
-
|
|
3056
|
+
writePlatformTiming("database checkpoint", startedAt);
|
|
3013
3057
|
const routing = manifest.resources.find((resource) => resource.key === "routing-kv" && resource.kind === "kv-namespace" && resource.ownership === "created" && resource.state === "ready");
|
|
3014
3058
|
if (!routing) return;
|
|
3015
3059
|
const kvStartedAt = performance.now();
|
|
3016
3060
|
await client.putKvValue(plan.account.id, routing.id, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, JSON.stringify(manifest));
|
|
3017
|
-
|
|
3061
|
+
writePlatformTiming("remote checkpoint", kvStartedAt);
|
|
3018
3062
|
}
|
|
3019
3063
|
function recordProvisionIntent(manifest, action, now) {
|
|
3020
3064
|
const previousResourceId = manifest.resources.find((resource) => resource.key === action.key)?.id ?? null;
|
|
3065
|
+
if (hasProvisionIntentStep(manifest, action.key, action.kind, action.name, previousResourceId)) return manifest;
|
|
3021
3066
|
const resultHash = platformProvisionIntentHash(manifest, action.key, action.kind, action.name, previousResourceId);
|
|
3022
|
-
if (manifest.steps.find((step) => (step.key === `provision-intent:${action.key}` || step.key.startsWith(`provision-intent:${action.key}:`)) && step.resultHash === resultHash)) return manifest;
|
|
3023
3067
|
return recordInstallationStep(manifest, {
|
|
3024
3068
|
completedAt: now,
|
|
3025
3069
|
key: `provision-intent:${action.key}:${resultHash.slice(0, 16)}`,
|
|
@@ -3035,7 +3079,7 @@ function recordAdoptionIntent(manifest, action, id, now) {
|
|
|
3035
3079
|
}
|
|
3036
3080
|
function hasProvisionIntent(manifest, action) {
|
|
3037
3081
|
const previousResourceId = manifest.resources.find((resource) => resource.key === action.key)?.id ?? null;
|
|
3038
|
-
return
|
|
3082
|
+
return hasProvisionIntentStep(manifest, action.key, action.kind, action.name, previousResourceId);
|
|
3039
3083
|
}
|
|
3040
3084
|
async function createResource(client, plan, action, databaseUrl, zoneId) {
|
|
3041
3085
|
switch (action.kind) {
|
|
@@ -3052,10 +3096,7 @@ async function createResource(client, plan, action, databaseUrl, zoneId) {
|
|
|
3052
3096
|
function recordResource(manifest, action, id, ownership, now, databaseUrl, resourceFingerprint) {
|
|
3053
3097
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
3054
3098
|
return recordInstallationResource(manifest, {
|
|
3055
|
-
configHash: action.kind === "dns-record" ? dnsRecordConfigHash(action.name, "AAAA", "100::", true) : action.kind === "hyperdrive" && resourceFingerprint ? resourceFingerprint : action.kind === "hyperdrive" && databaseUrl ? hyperdriveOriginConfigHash(hyperdriveOriginFromConnectionString(databaseUrl)) : action.kind === "r2-bucket" && resourceFingerprint ? r2BucketConfigHash(action.name, resourceFingerprint) :
|
|
3056
|
-
kind: action.kind,
|
|
3057
|
-
name: action.name
|
|
3058
|
-
})),
|
|
3099
|
+
configHash: action.kind === "dns-record" ? dnsRecordConfigHash(action.name, "AAAA", "100::", true) : action.kind === "hyperdrive" && resourceFingerprint ? resourceFingerprint : action.kind === "hyperdrive" && databaseUrl ? hyperdriveOriginConfigHash(hyperdriveOriginFromConnectionString(databaseUrl)) : action.kind === "r2-bucket" && resourceFingerprint ? r2BucketConfigHash(action.name, resourceFingerprint) : legacyResourceConfigHash(action.kind, action.name),
|
|
3059
3100
|
id,
|
|
3060
3101
|
key: action.key,
|
|
3061
3102
|
kind: action.kind,
|
|
@@ -3064,13 +3105,6 @@ function recordResource(manifest, action, id, ownership, now, databaseUrl, resou
|
|
|
3064
3105
|
state: "ready"
|
|
3065
3106
|
}, now);
|
|
3066
3107
|
}
|
|
3067
|
-
function r2BucketConfigHash(name, fingerprint) {
|
|
3068
|
-
return hash(JSON.stringify({
|
|
3069
|
-
fingerprint,
|
|
3070
|
-
kind: "r2-bucket",
|
|
3071
|
-
name
|
|
3072
|
-
}));
|
|
3073
|
-
}
|
|
3074
3108
|
function retainReplacedInstallationResource(manifest, action, now) {
|
|
3075
3109
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
3076
3110
|
if (action.action !== "create" || !previous || previous.state !== "ready" || previous.name === action.name) return manifest;
|
|
@@ -3098,7 +3132,6 @@ function validateReplannedWorkerActions(original, manifest, actions) {
|
|
|
3098
3132
|
return actions;
|
|
3099
3133
|
}
|
|
3100
3134
|
async function inventoryUnmanagedPlatformWorkerTraffic(client, plan, workerName) {
|
|
3101
|
-
if (!client.listWorkerDomains || !client.listWorkerServiceRoutes) throw new Error(`platform: Cloudflare traffic inventory is required before updating Worker ${JSON.stringify(workerName)}.`);
|
|
3102
3135
|
const [routes, domains] = await Promise.all([client.listWorkerServiceRoutes(plan.account.id, workerName), client.listWorkerDomains(plan.account.id)]);
|
|
3103
3136
|
const managed = platformManagedWorkerTraffic(plan.deployment, /* @__PURE__ */ new Set([workerName]));
|
|
3104
3137
|
const managedRoutes = new Set(managed.routes.map((route) => route.pattern));
|
|
@@ -3176,10 +3209,6 @@ function requiredBoolean(value, field) {
|
|
|
3176
3209
|
if (typeof value !== "boolean") throw new Error(`platform: ${field} must be a boolean.`);
|
|
3177
3210
|
return value;
|
|
3178
3211
|
}
|
|
3179
|
-
async function publishPlatformWorkerCustomDomains(client, accountId, workerName, origins) {
|
|
3180
|
-
if (!client.publishWorkerCustomDomainsSafely) throw new Error("platform: Safe Cloudflare Custom Domain publication is unavailable.");
|
|
3181
|
-
await client.publishWorkerCustomDomainsSafely(accountId, workerName, origins);
|
|
3182
|
-
}
|
|
3183
3212
|
function uniqueTriggerRoutes(routes) {
|
|
3184
3213
|
const seen = /* @__PURE__ */ new Set();
|
|
3185
3214
|
return routes.filter((route) => {
|
|
@@ -3189,15 +3218,6 @@ function uniqueTriggerRoutes(routes) {
|
|
|
3189
3218
|
return true;
|
|
3190
3219
|
});
|
|
3191
3220
|
}
|
|
3192
|
-
function dnsRecordConfigHash(name, type, content, proxied) {
|
|
3193
|
-
return hash(JSON.stringify({
|
|
3194
|
-
content,
|
|
3195
|
-
kind: "dns-record",
|
|
3196
|
-
name,
|
|
3197
|
-
proxied,
|
|
3198
|
-
type
|
|
3199
|
-
}));
|
|
3200
|
-
}
|
|
3201
3221
|
function hash(value) {
|
|
3202
3222
|
return createHash("sha256").update(value).digest("hex");
|
|
3203
3223
|
}
|
|
@@ -3257,7 +3277,7 @@ function createPlatformInstallationSecrets(input) {
|
|
|
3257
3277
|
if (modernAuthentication && !existingInstallation && (authSetupToken.length < 32 || !/^[a-f0-9]{64}$/.test(authBootstrapCode))) throw new Error("platform: Valid setup authority and an administrator setup code are required.");
|
|
3258
3278
|
if (authenticationSetupJson) {
|
|
3259
3279
|
const setup = parsePlatformAuthenticationSetup(JSON.parse(authenticationSetupJson));
|
|
3260
|
-
if (!existingInstallation &&
|
|
3280
|
+
if (!existingInstallation && createHash("sha256").update(authBootstrapCode).digest("hex") !== setup.bootstrapCredentialHash) throw new Error("platform: Valid installation setup authority and a matching administrator setup code are required.");
|
|
3261
3281
|
}
|
|
3262
3282
|
const jwtSecret = restoredIdentity(input.jwtSecret || "", "jwtSecret");
|
|
3263
3283
|
const resolvedJwtSecret = input.existingInstallation ? jwtSecret : required(jwtSecret, "VOID_PLATFORM_JWT_SECRET");
|
|
@@ -3282,6 +3302,11 @@ function createPlatformInstallationSecrets(input) {
|
|
|
3282
3302
|
});
|
|
3283
3303
|
const pendingWorkerSecretUpdateIntent = parseWorkerSecretUpdateIntent(checkpoint.workerSecretUpdatesJson);
|
|
3284
3304
|
const emailSigningSecretUpdateAuthorized = emailSigningSecretInScope && !!emailSigningSecret && (emailEnabled && (!existingEmailEnabled || !checkpoint.emailSigningSecret) || !!input.emailSigningSecret);
|
|
3305
|
+
const githubOptional = existingInstallation || modernAuthentication;
|
|
3306
|
+
const resolve = (key, requiredName, optional) => {
|
|
3307
|
+
const value = configured(input[key], key);
|
|
3308
|
+
return optional ? value : required(value, requiredName);
|
|
3309
|
+
};
|
|
3285
3310
|
const explicitWorkerSecretUpdates = withoutEmptySecrets({
|
|
3286
3311
|
CF_API_TOKEN: input.cloudflareApiToken,
|
|
3287
3312
|
...emailSigningSecretUpdateAuthorized ? { EMAIL_SIGNING_SECRET: emailSigningSecret } : {},
|
|
@@ -3306,20 +3331,20 @@ function createPlatformInstallationSecrets(input) {
|
|
|
3306
3331
|
...checkpoint.accessCredentialsJson ? { accessCredentialsJson: checkpoint.accessCredentialsJson } : {},
|
|
3307
3332
|
workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
|
|
3308
3333
|
pendingWorkerSecretUpdateIntent: input.updateExistingWorkers ? pendingWorkerSecretUpdateIntent : void 0,
|
|
3309
|
-
bootstrapGitHubId:
|
|
3310
|
-
bootstrapGitHubLogin:
|
|
3311
|
-
cloudflareApiToken:
|
|
3334
|
+
bootstrapGitHubId: resolve("bootstrapGitHubId", "resolved administrator GitHub account id", githubOptional),
|
|
3335
|
+
bootstrapGitHubLogin: resolve("bootstrapGitHubLogin", "VOID_PLATFORM_ADMIN_GITHUB_LOGIN", githubOptional),
|
|
3336
|
+
cloudflareApiToken: resolve("cloudflareApiToken", "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN", existingInstallation),
|
|
3312
3337
|
databaseUrl: required(input.databaseUrl || checkpoint.databaseUrl || "", "DATABASE_URL"),
|
|
3313
3338
|
emailEnabled,
|
|
3314
3339
|
emailSigningSecret,
|
|
3315
|
-
githubClientId:
|
|
3316
|
-
githubClientSecret:
|
|
3340
|
+
githubClientId: resolve("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID", githubOptional),
|
|
3341
|
+
githubClientSecret: resolve("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET", githubOptional),
|
|
3317
3342
|
jwtSecret: resolvedJwtSecret,
|
|
3318
3343
|
projectSecretActiveKeyVersion: projectSecretKeyring.activeVersion,
|
|
3319
3344
|
projectSecretKey: projectSecretKeyring.v1Key,
|
|
3320
3345
|
projectSecretKeysJson: projectSecretKeyring.keysJson,
|
|
3321
|
-
r2AccessKeyId:
|
|
3322
|
-
r2SecretAccessKey:
|
|
3346
|
+
r2AccessKeyId: resolve("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID", existingInstallation),
|
|
3347
|
+
r2SecretAccessKey: resolve("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY", existingInstallation)
|
|
3323
3348
|
};
|
|
3324
3349
|
}
|
|
3325
3350
|
function resolveProjectSecretKeyring(input) {
|
|
@@ -3461,14 +3486,7 @@ function platformWorkerSecretPayload(worker, secrets, accountId, zoneId, existin
|
|
|
3461
3486
|
}
|
|
3462
3487
|
return proxyWorkerSecrets(secrets, accountId);
|
|
3463
3488
|
}
|
|
3464
|
-
const allowed = worker === "api" ? [
|
|
3465
|
-
"CF_API_TOKEN",
|
|
3466
|
-
"EMAIL_SIGNING_SECRET",
|
|
3467
|
-
"GITHUB_CLIENT_ID",
|
|
3468
|
-
"GITHUB_CLIENT_SECRET",
|
|
3469
|
-
"R2_ACCESS_KEY_ID",
|
|
3470
|
-
"R2_SECRET_ACCESS_KEY"
|
|
3471
|
-
] : worker === "proxy" ? ["CF_API_TOKEN", "EMAIL_SIGNING_SECRET"] : ["EMAIL_SIGNING_SECRET"];
|
|
3489
|
+
const allowed = worker === "api" ? PLATFORM_WORKER_SECRET_UPDATE_NAMES : worker === "proxy" ? ["CF_API_TOKEN", "EMAIL_SIGNING_SECRET"] : ["EMAIL_SIGNING_SECRET"];
|
|
3472
3490
|
const updates = Object.fromEntries(Object.entries(secrets.workerSecretUpdates ?? {}).filter(([name, value]) => allowed.includes(name) && typeof value === "string" && value !== ""));
|
|
3473
3491
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
3474
3492
|
}
|
|
@@ -3542,220 +3560,64 @@ function createPlatformHealthProbeToken(ownershipNonce) {
|
|
|
3542
3560
|
//#endregion
|
|
3543
3561
|
//#region src/cli/platform-installer-errors.ts
|
|
3544
3562
|
function assertCleanPlatformWrangler(result, operation) {
|
|
3545
|
-
if (result.status !== 0 || result.signal || result.error)
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
};
|
|
3563
|
+
if (result.status !== 0 || result.signal || result.error) {
|
|
3564
|
+
const diagnostic = cloudflareCommandDiagnostic({
|
|
3565
|
+
...result,
|
|
3566
|
+
stderr: result.stderr.trim() || result.errors?.map((error) => error.message).join("\n") || ""
|
|
3567
|
+
});
|
|
3568
|
+
throw new Error(`platform: Wrangler failed to ${operation} (${diagnostic || `exit ${result.status}`}).`);
|
|
3569
|
+
}
|
|
3553
3570
|
}
|
|
3554
3571
|
function assertPlatformTriggerStabilization(result, workerName, targetVersionId, operation) {
|
|
3555
3572
|
if (!result.ok) throw new Error(`platform: Failed to ${operation} triggers for ${workerName} (${result.reason}).`);
|
|
3556
3573
|
if (!result.targetOwnsTraffic) throw new Error(`platform: Worker ${workerName} was superseded by ${result.activeVersionId} while publishing ${targetVersionId}; its active trigger state was repaired.`);
|
|
3557
3574
|
}
|
|
3558
|
-
function errorMessage(error) {
|
|
3559
|
-
return error instanceof Error ? error.message : String(error);
|
|
3560
|
-
}
|
|
3561
3575
|
//#endregion
|
|
3562
|
-
//#region src/cli/platform-installer-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3576
|
+
//#region src/cli/platform-installer-inspection.ts
|
|
3577
|
+
var PlatformWorkerOwnershipError = class extends Error {};
|
|
3578
|
+
async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
3579
|
+
const result = await runner({
|
|
3580
|
+
type: "deployments.status",
|
|
3581
|
+
configPath,
|
|
3582
|
+
workerName
|
|
3583
|
+
}, accountId, process.cwd(), true);
|
|
3584
|
+
if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
|
|
3585
|
+
const value = result.data;
|
|
3586
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
|
|
3587
|
+
const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
|
|
3588
|
+
if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
|
|
3589
|
+
return active[0].version_id;
|
|
3568
3590
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
"proxy-authorization",
|
|
3600
|
-
"te",
|
|
3601
|
-
"trailer",
|
|
3602
|
-
"transfer-encoding",
|
|
3603
|
-
"upgrade"
|
|
3604
|
-
]);
|
|
3605
|
-
function forwardHeaders(headers) {
|
|
3606
|
-
return Object.fromEntries(Object.entries(headers).filter(([name]) => !HOP_BY_HOP_HEADERS.has(name.toLowerCase())));
|
|
3607
|
-
}
|
|
3608
|
-
function closeHttpServer(server) {
|
|
3609
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
3610
|
-
server.close((error) => {
|
|
3611
|
-
if (error) rejectPromise(error);
|
|
3612
|
-
else resolvePromise();
|
|
3613
|
-
});
|
|
3614
|
-
server.closeAllConnections();
|
|
3615
|
-
});
|
|
3616
|
-
}
|
|
3617
|
-
async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName) {
|
|
3618
|
-
const upstreamBase = new URL(upstreamApiBaseUrl);
|
|
3619
|
-
if (upstreamBase.protocol !== "http:" && upstreamBase.protocol !== "https:") throw new Error("platform: Cloudflare API base URL must use HTTP or HTTPS.");
|
|
3620
|
-
const basePath = upstreamBase.pathname.replace(/\/+$/, "");
|
|
3621
|
-
const accountPath = `${basePath}/accounts/${encodeURIComponent(accountId)}`;
|
|
3622
|
-
const expectedPath = `${accountPath}/workers/scripts/${encodeURIComponent(workerName)}`;
|
|
3623
|
-
const assetUploadPath = `${accountPath}/workers/assets/upload`;
|
|
3624
|
-
const state = {
|
|
3625
|
-
assetSessionStarted: false,
|
|
3626
|
-
attempted: false,
|
|
3627
|
-
conflictStatus: null,
|
|
3628
|
-
created: false,
|
|
3629
|
-
forwarded: false,
|
|
3630
|
-
rejectedMutation: null
|
|
3631
|
-
};
|
|
3632
|
-
const server = createServer((incoming, outgoing) => {
|
|
3633
|
-
(async () => {
|
|
3634
|
-
const incomingUrl = new URL(incoming.url ?? "/", "http://127.0.0.1");
|
|
3635
|
-
if (incomingUrl.pathname !== basePath && !incomingUrl.pathname.startsWith(`${basePath}/`)) {
|
|
3636
|
-
outgoing.writeHead(502, { "Content-Type": "application/json" });
|
|
3637
|
-
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Invalid API proxy path\"}]}");
|
|
3638
|
-
return;
|
|
3639
|
-
}
|
|
3640
|
-
const createQuery = [...incomingUrl.searchParams];
|
|
3641
|
-
const supportedCreateQuery = new Set(createQuery.map(([key]) => key)).size === createQuery.length && createQuery.every(([key, value]) => key === "excludeScript" && value === "true" || key === "bindings_inherit" && value === "strict");
|
|
3642
|
-
const isConditionalCreate = incoming.method === "PUT" && incomingUrl.pathname === expectedPath && supportedCreateQuery;
|
|
3643
|
-
const isTopLevelWorkerUpload = incoming.method === "PUT" && new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/accounts/[^/]+/workers/scripts/[^/]+$`).test(incomingUrl.pathname);
|
|
3644
|
-
const isReadOnly = incoming.method === "GET" || incoming.method === "HEAD" || incoming.method === "OPTIONS";
|
|
3645
|
-
const isAssetUploadSession = incoming.method === "POST" && incomingUrl.pathname === `${expectedPath}/assets-upload-session` && incomingUrl.search === "";
|
|
3646
|
-
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)));
|
|
3647
|
-
if (isTopLevelWorkerUpload && !isConditionalCreate || !isReadOnly && !isConditionalCreate && !isAssetUploadSession && !isAssetUpload && !state.created) {
|
|
3648
|
-
state.rejectedMutation = `${incoming.method ?? "UNKNOWN"} ${incomingUrl.pathname}${incomingUrl.pathname === expectedPath && !supportedCreateQuery ? " (unsupported Worker create query)" : ""}`;
|
|
3649
|
-
incoming.resume();
|
|
3650
|
-
outgoing.writeHead(409, { "Content-Type": "application/json" });
|
|
3651
|
-
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Mutation blocked until conditional Worker creation succeeds\"}]}");
|
|
3652
|
-
return;
|
|
3653
|
-
}
|
|
3654
|
-
const headers = forwardHeaders(incoming.headers);
|
|
3655
|
-
delete headers.host;
|
|
3656
|
-
if (isConditionalCreate) {
|
|
3657
|
-
state.attempted = true;
|
|
3658
|
-
headers["if-none-match"] = "*";
|
|
3659
|
-
}
|
|
3660
|
-
if (isAssetUploadSession) state.assetSessionStarted = true;
|
|
3661
|
-
const upstreamUrl = new URL(`${incomingUrl.pathname}${incomingUrl.search}`, upstreamBase.origin);
|
|
3662
|
-
const request$2 = (upstreamUrl.protocol === "https:" ? request$1 : request)(upstreamUrl, {
|
|
3663
|
-
headers,
|
|
3664
|
-
method: incoming.method
|
|
3665
|
-
}, (response) => {
|
|
3666
|
-
if (isConditionalCreate) {
|
|
3667
|
-
state.forwarded = true;
|
|
3668
|
-
if (response.statusCode === 409 || response.statusCode === 412) state.conflictStatus = response.statusCode;
|
|
3669
|
-
state.created = response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300;
|
|
3670
|
-
}
|
|
3671
|
-
const responseHeaders = forwardHeaders(response.headers);
|
|
3672
|
-
outgoing.writeHead(response.statusCode ?? 502, responseHeaders);
|
|
3673
|
-
response.pipe(outgoing);
|
|
3674
|
-
});
|
|
3675
|
-
request$2.on("error", () => {
|
|
3676
|
-
if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
|
|
3677
|
-
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy request failed\"}]}");
|
|
3678
|
-
});
|
|
3679
|
-
incoming.pipe(request$2);
|
|
3680
|
-
})().catch(() => {
|
|
3681
|
-
if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
|
|
3682
|
-
outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy failed\"}]}");
|
|
3683
|
-
});
|
|
3684
|
-
});
|
|
3685
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
3686
|
-
server.once("error", rejectPromise);
|
|
3687
|
-
server.listen(0, "127.0.0.1", () => {
|
|
3688
|
-
server.off("error", rejectPromise);
|
|
3689
|
-
resolvePromise();
|
|
3690
|
-
});
|
|
3691
|
-
});
|
|
3692
|
-
return {
|
|
3693
|
-
apiBaseUrl: `http://127.0.0.1:${server.address().port}${basePath}`,
|
|
3694
|
-
close: () => closeHttpServer(server),
|
|
3695
|
-
state
|
|
3696
|
-
};
|
|
3697
|
-
}
|
|
3698
|
-
async function withConditionalPlatformWorkerCreate(accountId, workerName, operation, upstreamApiBaseUrl = resolvePinnedApiBaseUrl()) {
|
|
3699
|
-
const proxy = await startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName);
|
|
3700
|
-
let result;
|
|
3701
|
-
try {
|
|
3702
|
-
result = await operation(proxy.apiBaseUrl);
|
|
3703
|
-
} finally {
|
|
3704
|
-
await proxy.close();
|
|
3705
|
-
}
|
|
3706
|
-
if (proxy.state.rejectedMutation) throw new Error(`platform: Wrangler attempted unexpected mutation ${proxy.state.rejectedMutation} while conditionally creating new Worker ${workerName}; the mutation was blocked.`);
|
|
3707
|
-
if (!proxy.state.attempted || !proxy.state.forwarded) throw new Error(`platform: Wrangler did not use the guarded create endpoint for new Worker ${workerName}; refusing an unverified deployment.`);
|
|
3708
|
-
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.`);
|
|
3709
|
-
return result;
|
|
3710
|
-
}
|
|
3711
|
-
async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl, apiToken) {
|
|
3712
|
-
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl, false, apiToken), upstreamApiBaseUrl);
|
|
3713
|
-
}
|
|
3714
|
-
//#endregion
|
|
3715
|
-
//#region src/cli/platform-installer-inspection.ts
|
|
3716
|
-
async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
3717
|
-
const result = await runner({
|
|
3718
|
-
type: "deployments.status",
|
|
3719
|
-
configPath,
|
|
3720
|
-
workerName
|
|
3721
|
-
}, accountId, process.cwd(), true);
|
|
3722
|
-
if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
|
|
3723
|
-
const value = result.data;
|
|
3724
|
-
if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
|
|
3725
|
-
const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
|
|
3726
|
-
if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
|
|
3727
|
-
return active[0].version_id;
|
|
3728
|
-
}
|
|
3729
|
-
async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
3730
|
-
const result = await runner({
|
|
3731
|
-
type: "deployments.list",
|
|
3732
|
-
configPath,
|
|
3733
|
-
workerName
|
|
3734
|
-
}, accountId, process.cwd(), true);
|
|
3735
|
-
if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
|
|
3736
|
-
const value = result.data;
|
|
3737
|
-
if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
3738
|
-
return value.map((entry, index) => {
|
|
3739
|
-
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}.`);
|
|
3740
|
-
const createdAt = Date.parse(entry.created_on);
|
|
3741
|
-
if (!Number.isFinite(createdAt)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
3742
|
-
const versions = entry.versions.map((version) => {
|
|
3743
|
-
if (!version || typeof version !== "object" || typeof version.version_id !== "string" || !Number.isFinite(Number(version.percentage))) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
3744
|
-
return {
|
|
3745
|
-
percentage: Number(version.percentage),
|
|
3746
|
-
versionId: version.version_id
|
|
3747
|
-
};
|
|
3748
|
-
});
|
|
3749
|
-
return {
|
|
3750
|
-
createdAt,
|
|
3751
|
-
id: entry.id,
|
|
3752
|
-
index,
|
|
3753
|
-
versions
|
|
3754
|
-
};
|
|
3755
|
-
}).sort((left, right) => right.createdAt - left.createdAt || left.index - right.index).map(({ id, versions }) => ({
|
|
3756
|
-
id,
|
|
3757
|
-
versions
|
|
3758
|
-
}));
|
|
3591
|
+
async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
3592
|
+
const result = await runner({
|
|
3593
|
+
type: "deployments.list",
|
|
3594
|
+
configPath,
|
|
3595
|
+
workerName
|
|
3596
|
+
}, accountId, process.cwd(), true);
|
|
3597
|
+
if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
|
|
3598
|
+
const value = result.data;
|
|
3599
|
+
if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
3600
|
+
return value.map((entry, index) => {
|
|
3601
|
+
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}.`);
|
|
3602
|
+
const createdAt = Date.parse(entry.created_on);
|
|
3603
|
+
if (!Number.isFinite(createdAt)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
3604
|
+
const versions = entry.versions.map((version) => {
|
|
3605
|
+
if (!version || typeof version !== "object" || typeof version.version_id !== "string" || !Number.isFinite(Number(version.percentage))) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
3606
|
+
return {
|
|
3607
|
+
percentage: Number(version.percentage),
|
|
3608
|
+
versionId: version.version_id
|
|
3609
|
+
};
|
|
3610
|
+
});
|
|
3611
|
+
return {
|
|
3612
|
+
createdAt,
|
|
3613
|
+
id: entry.id,
|
|
3614
|
+
index,
|
|
3615
|
+
versions
|
|
3616
|
+
};
|
|
3617
|
+
}).sort((left, right) => right.createdAt - left.createdAt || left.index - right.index).map(({ id, versions }) => ({
|
|
3618
|
+
id,
|
|
3619
|
+
versions
|
|
3620
|
+
}));
|
|
3759
3621
|
}
|
|
3760
3622
|
function samePlatformWorkerTraffic(left, right) {
|
|
3761
3623
|
if (!left) return false;
|
|
@@ -3777,7 +3639,6 @@ function classifyPlatformWorkerDeploymentLineage(deployments, installedVersionId
|
|
|
3777
3639
|
};
|
|
3778
3640
|
}
|
|
3779
3641
|
async function inspectNewPlatformWorker(client, accountId, manifest, resourceKey, workerName) {
|
|
3780
|
-
if (!client.listWorkerNames || !client.getWorkerPlainTextBindings) throw new Error("platform: Cloudflare Worker ownership inspection is unavailable.");
|
|
3781
3642
|
if (!(await client.listWorkerNames(accountId)).includes(workerName)) return "missing";
|
|
3782
3643
|
const bindings = await client.getWorkerPlainTextBindings(accountId, workerName);
|
|
3783
3644
|
let marker;
|
|
@@ -3826,7 +3687,7 @@ async function readPlatformWorkerVersionSnapshot(configPath, accountId, workerNa
|
|
|
3826
3687
|
const resources = value.resources;
|
|
3827
3688
|
const bindings = resources && typeof resources === "object" && !Array.isArray(resources) ? resources.bindings : null;
|
|
3828
3689
|
const ownershipBindings = Array.isArray(bindings) ? bindings.filter((binding) => binding && typeof binding === "object" && binding.name === "PLATFORM_OWNERSHIP_MARKER" && binding.type === "plain_text" && typeof binding.text === "string") : [];
|
|
3829
|
-
if (ownershipBindings.length !== 1 || ownershipBindings[0].text !== expectedOwnershipMarker) throw new
|
|
3690
|
+
if (ownershipBindings.length !== 1 || ownershipBindings[0].text !== expectedOwnershipMarker) throw new PlatformWorkerOwnershipError(`platform: Active Worker version ${versionId} for ${workerName} does not belong to this installation.`);
|
|
3830
3691
|
await validateCurrentVersion?.();
|
|
3831
3692
|
const annotations = value.annotations;
|
|
3832
3693
|
const message = annotations && typeof annotations === "object" && typeof annotations["workers/message"] === "string" ? annotations["workers/message"] ?? null : null;
|
|
@@ -4033,7 +3894,7 @@ function resolvePlatformPreviousTriggerConfig(versionSnapshot, configuredSnapsho
|
|
|
4033
3894
|
//#endregion
|
|
4034
3895
|
//#region src/cli/platform-trigger-proof.ts
|
|
4035
3896
|
function record(value) {
|
|
4036
|
-
return
|
|
3897
|
+
return isRecord(value) ? value : null;
|
|
4037
3898
|
}
|
|
4038
3899
|
function sameStrings(actual, expected) {
|
|
4039
3900
|
const sortedExpected = [...expected].sort();
|
|
@@ -4122,7 +3983,23 @@ async function platformWorkerLiveTriggersMatch(options) {
|
|
|
4122
3983
|
}
|
|
4123
3984
|
//#endregion
|
|
4124
3985
|
//#region src/cli/platform-installer-workers.ts
|
|
4125
|
-
|
|
3986
|
+
function platformWorkerResourceKey(key) {
|
|
3987
|
+
return key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`;
|
|
3988
|
+
}
|
|
3989
|
+
/** Callbacks shared by a platform Worker deploy and its interrupted-cutover rollback. */
|
|
3990
|
+
function platformWorkerTrafficCallbacks(input) {
|
|
3991
|
+
const { client, accountId, ids, key, workerName, readManifest } = input;
|
|
3992
|
+
return {
|
|
3993
|
+
inspectNewWorker: () => inspectNewPlatformWorker(client, accountId, readManifest(), platformWorkerResourceKey(key), workerName),
|
|
3994
|
+
publishCustomDomains: (name, origins) => client.publishWorkerCustomDomainsSafely(accountId, name, origins),
|
|
3995
|
+
retainNewWorker: async () => {
|
|
3996
|
+
await client.putKvValue(accountId, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
3997
|
+
}
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
async function recoverInterruptedPlatformWorkerCutover(input) {
|
|
4001
|
+
const { interruptedCutover, configs, previousConfigs, plan, dependencies, ids, previousPublicEndpoints, hasEmailGatewayArtifact, runPinnedPlatformWrangler, now, writeRemoteInstallationCheckpoint, onManifestChange } = input;
|
|
4002
|
+
let { manifest, workerActions } = input;
|
|
4126
4003
|
if (interruptedCutover) {
|
|
4127
4004
|
for (const key of [
|
|
4128
4005
|
"dispatch",
|
|
@@ -4163,15 +4040,14 @@ async function recoverInterruptedPlatformWorkerCutover(manifest, interruptedCuto
|
|
|
4163
4040
|
previousTriggers: mergePreservedTriggerRoutes(withoutPlatformCustomDomainTriggers(snapshot.previousTriggers), preservedRoutes),
|
|
4164
4041
|
previousVersionId: snapshot.previousVersionId,
|
|
4165
4042
|
preserveCustomDomains: interruptedCutover.unsafeTraffic.has(key),
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
},
|
|
4043
|
+
...platformWorkerTrafficCallbacks({
|
|
4044
|
+
client: dependencies.client,
|
|
4045
|
+
accountId: plan.account.id,
|
|
4046
|
+
ids,
|
|
4047
|
+
key,
|
|
4048
|
+
workerName: snapshot.workerName,
|
|
4049
|
+
readManifest: () => manifest
|
|
4050
|
+
}),
|
|
4175
4051
|
runner: runPinnedPlatformWrangler,
|
|
4176
4052
|
workerExisted: snapshot.previousVersionId !== null,
|
|
4177
4053
|
workerName: snapshot.workerName
|
|
@@ -4180,15 +4056,10 @@ async function recoverInterruptedPlatformWorkerCutover(manifest, interruptedCuto
|
|
|
4180
4056
|
}
|
|
4181
4057
|
manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "restored", now());
|
|
4182
4058
|
manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, previousPublicEndpoints && interruptedCutover.unsafeTraffic.size === 0, now());
|
|
4183
|
-
let retainedNewWorker = false;
|
|
4184
|
-
for (const [, snapshot] of interruptedCutover.staged) {
|
|
4185
|
-
if (snapshot.previousVersionId !== null) continue;
|
|
4186
|
-
retainedNewWorker = true;
|
|
4187
|
-
}
|
|
4188
4059
|
onManifestChange(manifest);
|
|
4189
4060
|
writeInstallationManifest(manifest);
|
|
4190
4061
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
4191
|
-
if (
|
|
4062
|
+
if ([...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null)) {
|
|
4192
4063
|
if (!dependencies.replanWorkers) throw new Error("platform: Recovering retained Workers requires a fresh Cloudflare installation plan.");
|
|
4193
4064
|
workerActions = validateReplannedWorkerActions(plan, manifest, await dependencies.replanWorkers(manifest)).filter((action) => hasEmailGatewayArtifact || action.key !== "email-gateway-worker");
|
|
4194
4065
|
}
|
|
@@ -4202,7 +4073,7 @@ let cachedPlatformUploaderHash;
|
|
|
4202
4073
|
function platformWorkerUploaderHash() {
|
|
4203
4074
|
if (cachedPlatformUploaderHash !== void 0) return cachedPlatformUploaderHash;
|
|
4204
4075
|
try {
|
|
4205
|
-
const launcher = resolveCloudflareOperationArgv(
|
|
4076
|
+
const launcher = resolveCloudflareOperationArgv()[1];
|
|
4206
4077
|
if (!launcher) return cachedPlatformUploaderHash = null;
|
|
4207
4078
|
const digest = createHash("sha256");
|
|
4208
4079
|
digest.update("platform-uploader:v1\0");
|
|
@@ -4376,10 +4247,6 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4376
4247
|
knownTriggerConfigs: { [versionId]: nextTriggers },
|
|
4377
4248
|
manageRoutes: false,
|
|
4378
4249
|
previousTriggerConfig: triggerConfig,
|
|
4379
|
-
publishEmptyCustomDomains: async () => {
|
|
4380
|
-
await options.removeCustomDomains();
|
|
4381
|
-
return cleanPlatformCommandResult();
|
|
4382
|
-
},
|
|
4383
4250
|
runWrangler: platformWorkerCommandRunner(accountId, directory, runner),
|
|
4384
4251
|
targetTriggerConfig: nextTriggers,
|
|
4385
4252
|
targetVersionId: versionId,
|
|
@@ -4401,7 +4268,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4401
4268
|
rollback: null,
|
|
4402
4269
|
unsafeTraffic: true
|
|
4403
4270
|
});
|
|
4404
|
-
throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} traffic reconciliation failed: ${
|
|
4271
|
+
throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} traffic reconciliation failed: ${getErrorMessage(error)}${failures.length > 0 ? ` Safety recovery also failed: ${failures.join("; ")}` : ""}`, { cause: error });
|
|
4405
4272
|
}
|
|
4406
4273
|
};
|
|
4407
4274
|
if (options.workerExisted && contentHash && reuseMatchingVersion && options.readCloudflare) {
|
|
@@ -4419,7 +4286,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4419
4286
|
snapshot
|
|
4420
4287
|
};
|
|
4421
4288
|
} catch (error) {
|
|
4422
|
-
if (error instanceof
|
|
4289
|
+
if (error instanceof PlatformWorkerOwnershipError) throw error;
|
|
4423
4290
|
}
|
|
4424
4291
|
if (changedDuringRead) throw new Error(`platform: Worker ${workerName} changed concurrently while its deployment lineage was inspected.`);
|
|
4425
4292
|
if (candidate && platformWorkerUploadFingerprint(config, options.managedRoutes) === contentHash) {
|
|
@@ -4528,7 +4395,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4528
4395
|
rollback: null,
|
|
4529
4396
|
unsafeTraffic: true
|
|
4530
4397
|
});
|
|
4531
|
-
throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} changed concurrently during activation, and restoring intervening deployment ${interveningDeployment.id} failed: ${
|
|
4398
|
+
throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} changed concurrently during activation, and restoring intervening deployment ${interveningDeployment.id} failed: ${getErrorMessage(recoveryError)}${recoveryFailures.length > 0 ? ` Safety recovery also failed: ${recoveryFailures.join("; ")}` : ""}`, { cause: recoveryError });
|
|
4532
4399
|
}
|
|
4533
4400
|
throw new Error(`platform: Worker ${workerName} changed concurrently during activation. The intervening deployment was restored without deleting deployment history.`);
|
|
4534
4401
|
}
|
|
@@ -4540,10 +4407,6 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4540
4407
|
},
|
|
4541
4408
|
manageRoutes: false,
|
|
4542
4409
|
previousTriggerConfig: previousTriggers,
|
|
4543
|
-
publishEmptyCustomDomains: async () => {
|
|
4544
|
-
await options.removeCustomDomains();
|
|
4545
|
-
return cleanPlatformCommandResult();
|
|
4546
|
-
},
|
|
4547
4410
|
runWrangler: platformWorkerCommandRunner(accountId, directory, readRunner),
|
|
4548
4411
|
targetTriggerConfig: nextTriggers,
|
|
4549
4412
|
targetVersionId: uploadedVersionId,
|
|
@@ -4565,7 +4428,6 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4565
4428
|
previousVersionId,
|
|
4566
4429
|
preserveCustomDomains: false,
|
|
4567
4430
|
inspectNewWorker: options.inspectNewWorker,
|
|
4568
|
-
removeCustomDomains: options.removeCustomDomains,
|
|
4569
4431
|
publishCustomDomains: options.publishCustomDomains,
|
|
4570
4432
|
retainNewWorker: options.retainNewWorker,
|
|
4571
4433
|
runner: readRunner,
|
|
@@ -4584,7 +4446,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4584
4446
|
activatedDeploymentId,
|
|
4585
4447
|
activatedVersionId: rollbackVersionId,
|
|
4586
4448
|
config,
|
|
4587
|
-
nextTriggers
|
|
4449
|
+
nextTriggers,
|
|
4588
4450
|
previousConfig: options.previousConfig,
|
|
4589
4451
|
previousCustomDomains: previousTraffic.customDomains,
|
|
4590
4452
|
previousDeployment,
|
|
@@ -4592,7 +4454,6 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4592
4454
|
previousVersionId,
|
|
4593
4455
|
preserveCustomDomains: unsafeTraffic,
|
|
4594
4456
|
inspectNewWorker: options.inspectNewWorker,
|
|
4595
|
-
removeCustomDomains: options.removeCustomDomains,
|
|
4596
4457
|
publishCustomDomains: options.publishCustomDomains,
|
|
4597
4458
|
retainNewWorker: options.retainNewWorker,
|
|
4598
4459
|
runner: readRunner,
|
|
@@ -4601,8 +4462,8 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4601
4462
|
}) : null,
|
|
4602
4463
|
unsafeTraffic
|
|
4603
4464
|
});
|
|
4604
|
-
if (recoveryFailures.length > 0) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${
|
|
4605
|
-
if (unsafeTraffic) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${
|
|
4465
|
+
if (recoveryFailures.length > 0) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${getErrorMessage(error)} Automatic recovery for ${workerName} was incomplete: ${recoveryFailures.join("; ")}`, { cause: error });
|
|
4466
|
+
if (unsafeTraffic) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${getErrorMessage(error)} The routing gate remains closed and live Custom Domains were retained for safe reconciliation on retry.`, { cause: error });
|
|
4606
4467
|
throw error;
|
|
4607
4468
|
} finally {
|
|
4608
4469
|
rmSync(directory, {
|
|
@@ -4617,7 +4478,7 @@ async function recoverFailedPlatformWorkerDeployment(input) {
|
|
|
4617
4478
|
try {
|
|
4618
4479
|
await operation();
|
|
4619
4480
|
} catch (error) {
|
|
4620
|
-
failures.push(`${label}: ${
|
|
4481
|
+
failures.push(`${label}: ${getErrorMessage(error)}`);
|
|
4621
4482
|
}
|
|
4622
4483
|
};
|
|
4623
4484
|
if (input.unsafeTraffic) {
|
|
@@ -4639,7 +4500,6 @@ async function rollbackPlatformWorker(input) {
|
|
|
4639
4500
|
if (currentVersionId !== input.activatedVersionId) throw new Error(`Worker ${input.workerName} changed concurrently from installer version ${input.activatedVersionId} to ${currentVersionId ?? "no active version"}; refusing to delete it.`);
|
|
4640
4501
|
if (await input.inspectNewWorker() === "missing") return;
|
|
4641
4502
|
await input.retainNewWorker();
|
|
4642
|
-
if (!input.preserveCustomDomains) await input.removeCustomDomains();
|
|
4643
4503
|
return;
|
|
4644
4504
|
}
|
|
4645
4505
|
const currentVersionId = await readActivePlatformWorkerVersion(configPath, input.accountId, input.workerName, input.runner);
|
|
@@ -4672,10 +4532,6 @@ async function rollbackPlatformWorker(input) {
|
|
|
4672
4532
|
},
|
|
4673
4533
|
manageRoutes: false,
|
|
4674
4534
|
previousTriggerConfig: input.nextTriggers,
|
|
4675
|
-
publishEmptyCustomDomains: async () => {
|
|
4676
|
-
await input.removeCustomDomains();
|
|
4677
|
-
return cleanPlatformCommandResult();
|
|
4678
|
-
},
|
|
4679
4535
|
runWrangler: platformWorkerCommandRunner(input.accountId, directory, input.runner),
|
|
4680
4536
|
targetTriggerConfig: input.previousTriggers,
|
|
4681
4537
|
targetVersionId: input.previousVersionId,
|
|
@@ -4708,149 +4564,509 @@ async function restorePlatformWorkerDeployment(input) {
|
|
|
4708
4564
|
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}.`);
|
|
4709
4565
|
}
|
|
4710
4566
|
//#endregion
|
|
4711
|
-
//#region src/cli/platform-install-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
await
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
for (const key of workerKeys) {
|
|
4735
|
-
dependencies.onProgress?.(`Enabling the ${key} service`);
|
|
4736
|
-
const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
|
|
4737
|
-
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
4738
|
-
const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
|
|
4739
|
-
const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
|
|
4740
|
-
const previousWorkerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
|
|
4741
|
-
const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
|
|
4742
|
-
apiBaseUrl: cloudflareApiBaseUrl,
|
|
4743
|
-
...dependencies.client.request ? { readCloudflare: (path) => dependencies.client.request(path) } : {},
|
|
4744
|
-
onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
|
|
4745
|
-
input.manifest = recordWorkerCutoverStage(input.manifest, input.workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
|
|
4746
|
-
writeInstallationManifest(input.manifest);
|
|
4747
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
4748
|
-
},
|
|
4749
|
-
onTrafficPublished: async () => {
|
|
4750
|
-
if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
|
|
4751
|
-
const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
|
|
4752
|
-
const next = recordPlatformWorkerTraffic(input.manifest, action.name, {
|
|
4753
|
-
customDomains,
|
|
4754
|
-
routes
|
|
4755
|
-
}, now());
|
|
4756
|
-
if (JSON.stringify(next.resources) !== JSON.stringify(input.manifest.resources)) {
|
|
4757
|
-
input.manifest = next;
|
|
4758
|
-
writeInstallationManifest(input.manifest);
|
|
4759
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
4760
|
-
}
|
|
4761
|
-
},
|
|
4762
|
-
onUnsafeTraffic: async () => {
|
|
4763
|
-
input.manifest = recordWorkerUnsafeTraffic(input.manifest, input.workerCutoverGeneration, key, action.name, now());
|
|
4764
|
-
writeInstallationManifest(input.manifest);
|
|
4765
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
4766
|
-
},
|
|
4767
|
-
managedRoutes: managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
|
|
4768
|
-
preservedRoutes,
|
|
4769
|
-
publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
|
|
4770
|
-
publishRoutes: async (workerName, patterns) => {
|
|
4771
|
-
if (!dependencies.client.publishWorkerRoutesSafely) throw new Error("platform: Safe Cloudflare Worker route publication is unavailable.");
|
|
4772
|
-
if (zoneId) await dependencies.client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
|
|
4773
|
-
else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
|
|
4774
|
-
},
|
|
4775
|
-
previousConfig: previousWorkerConfig,
|
|
4776
|
-
previousPublicEndpoints: true,
|
|
4777
|
-
inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, input.manifest, action.key, action.name),
|
|
4778
|
-
removeCustomDomains: async () => {
|
|
4779
|
-
if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
|
|
4780
|
-
await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(input.manifest, /* @__PURE__ */ new Set([action.name])));
|
|
4781
|
-
},
|
|
4782
|
-
retainNewWorker: async () => {
|
|
4783
|
-
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
4784
|
-
},
|
|
4785
|
-
reuseMatchingVersion: false,
|
|
4786
|
-
runner: runPinnedPlatformWrangler,
|
|
4787
|
-
workerExisted: true
|
|
4567
|
+
//#region src/cli/platform-install-workers.ts
|
|
4568
|
+
function platformWorkerAction(workerActions, key) {
|
|
4569
|
+
return workerActions.find((entry) => entry.key === platformWorkerResourceKey(key));
|
|
4570
|
+
}
|
|
4571
|
+
/** Deploy callbacks shared by the upload and enable phases for one platform Worker. */
|
|
4572
|
+
function platformWorkerDeployCallbacks(input) {
|
|
4573
|
+
const { client, plan, zoneId, ids, key, action, generation, now, readManifest, writeManifest, checkpoint } = input;
|
|
4574
|
+
return {
|
|
4575
|
+
readCloudflare: (path) => client.request(path),
|
|
4576
|
+
onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
|
|
4577
|
+
await checkpoint(async () => {
|
|
4578
|
+
await writeManifest(recordWorkerCutoverStage(readManifest(), generation, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now()));
|
|
4579
|
+
});
|
|
4580
|
+
},
|
|
4581
|
+
onTrafficPublished: async () => {
|
|
4582
|
+
const [customDomains, routes] = await Promise.all([client.listWorkerDomains(plan.account.id), zoneId ? client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
|
|
4583
|
+
await checkpoint(async () => {
|
|
4584
|
+
const manifest = readManifest();
|
|
4585
|
+
const next = recordPlatformWorkerTraffic(manifest, action.name, {
|
|
4586
|
+
customDomains,
|
|
4587
|
+
routes
|
|
4588
|
+
}, now());
|
|
4589
|
+
if (JSON.stringify(next.resources) !== JSON.stringify(manifest.resources)) await writeManifest(next);
|
|
4788
4590
|
});
|
|
4789
|
-
|
|
4591
|
+
},
|
|
4592
|
+
onUnsafeTraffic: async () => {
|
|
4593
|
+
await checkpoint(async () => {
|
|
4594
|
+
await writeManifest(recordWorkerUnsafeTraffic(readManifest(), generation, key, action.name, now()));
|
|
4595
|
+
}, true);
|
|
4596
|
+
},
|
|
4597
|
+
...platformWorkerTrafficCallbacks({
|
|
4598
|
+
client,
|
|
4599
|
+
accountId: plan.account.id,
|
|
4600
|
+
ids,
|
|
4601
|
+
key,
|
|
4602
|
+
workerName: action.name,
|
|
4603
|
+
readManifest
|
|
4604
|
+
}),
|
|
4605
|
+
publishRoutes: async (workerName, patterns) => {
|
|
4606
|
+
if (zoneId) await client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
|
|
4607
|
+
else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
|
|
4790
4608
|
}
|
|
4791
|
-
}
|
|
4792
|
-
const verifyCurrentWorkerCutover = async () => {
|
|
4793
|
-
if (!requiresStagedCommit) return;
|
|
4794
|
-
if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(input.manifest, input.workerCutoverGeneration);
|
|
4795
|
-
else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(input.manifest, input.workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler, workerKeys);
|
|
4796
|
-
};
|
|
4797
|
-
await verifyCurrentWorkerCutover();
|
|
4798
|
-
let emailOperationId;
|
|
4799
|
-
if (shouldBootstrapSharedEmail) {
|
|
4800
|
-
input.workerDeploymentCommitted = true;
|
|
4801
|
-
input.manifest = recordWorkerCutoverResolution(input.manifest, input.workerCutoverGeneration, "committed", now());
|
|
4802
|
-
if (input.workerSecretUpdateIntent) input.manifest = recordWorkerSecretUpdateState(input.manifest, input.workerCutoverGeneration, "committed", input.workerSecretUpdateIntent.id, now());
|
|
4803
|
-
writeInstallationManifest(input.manifest);
|
|
4804
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
4805
|
-
input.workerRollbacks = [];
|
|
4806
|
-
dependencies.onProgress?.("Preparing shared email routing");
|
|
4807
|
-
emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(input.manifest.ownershipNonce));
|
|
4808
|
-
}
|
|
4809
|
-
await releaseDatabaseLock.liftProjectZeroTrustFence?.();
|
|
4810
|
-
if (dependencies.client.deleteKvValue) await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
|
|
4811
|
-
else if (wasDisabled || !previousPublicEndpoints) throw new Error("platform: Routing disablement removal is unavailable.");
|
|
4812
|
-
if (requiresStagedCommit) {
|
|
4813
|
-
dependencies.onProgress?.("Verifying the enabled platform");
|
|
4814
|
-
await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, input.manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(input.manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ? `https://${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev` : void 0, healthCheckContext);
|
|
4815
|
-
await verifyCurrentWorkerCutover();
|
|
4816
|
-
}
|
|
4817
|
-
input.workerDeploymentCommitted = true;
|
|
4818
|
-
input.manifest = recordWorkerCutoverResolution(input.manifest, input.workerCutoverGeneration, "committed", now());
|
|
4819
|
-
input.manifest = recordWorkerTrafficState(input.manifest, input.workerCutoverGeneration, true, now());
|
|
4820
|
-
if (input.workerSecretUpdateIntent) input.manifest = recordWorkerSecretUpdateState(input.manifest, input.workerCutoverGeneration, "committed", input.workerSecretUpdateIntent.id, now());
|
|
4821
|
-
input.manifest = {
|
|
4822
|
-
...input.manifest,
|
|
4823
|
-
deployment: plan.deployment,
|
|
4824
|
-
deploymentExecution: runtime.deploymentExecution,
|
|
4825
|
-
platformVersion: version,
|
|
4826
|
-
runtime: runtimeProvenance,
|
|
4827
|
-
safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
|
|
4828
|
-
updatedAt: now(),
|
|
4829
|
-
usageProtocolVersion: targetUsageProtocolVersion
|
|
4830
4609
|
};
|
|
4831
|
-
input.manifest = transitionInstallation(input.manifest, "installed", now());
|
|
4832
|
-
return { emailOperationId };
|
|
4833
|
-
}
|
|
4834
|
-
//#endregion
|
|
4835
|
-
//#region src/cli/platform-access-health.ts
|
|
4836
|
-
/** These reserved endpoints require Void's probe token and cannot serve application traffic. */
|
|
4837
|
-
function platformAccessHealthTargets(plan) {
|
|
4838
|
-
const destination = `${plan.deployment.application.zoneName ? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${plan.deployment.application.baseDomain}` : `${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev`}/health`;
|
|
4839
|
-
return [{
|
|
4840
|
-
key: `health-dispatch-${createHash("sha256").update(destination).digest("hex").slice(0, 12)}`,
|
|
4841
|
-
destination
|
|
4842
|
-
}];
|
|
4843
|
-
}
|
|
4844
|
-
async function verifyHealthException(access, app, destination) {
|
|
4845
|
-
const destinations = platformAccessPublicDestinations(app);
|
|
4846
|
-
if (typeof app.id !== "string" || !app.id || app.type !== "self_hosted" || app.domain !== destination || !destinations.length || destinations.some((value) => value !== destination) || app.destinations !== void 0 && (!Array.isArray(app.destinations) || app.destinations.some((value) => !value || value.type !== "public" || value.uri !== destination)) || app.target_criteria != null && (!Array.isArray(app.target_criteria) || app.target_criteria.length > 0)) throw new Error(`platform: The Access health exception must cover only ${destination}.`);
|
|
4847
|
-
const policies = await access.list(`apps/${encodeURIComponent(app.id)}/policies`);
|
|
4848
|
-
const policy = policies[0];
|
|
4849
|
-
if (policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== "[{\"everyone\":{}}]" || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) throw new Error(`platform: The Access policy for ${destination} does not match the reserved health-check exception.`);
|
|
4850
4610
|
}
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
const
|
|
4611
|
+
async function uploadInstallationWorkers(input) {
|
|
4612
|
+
const { state, dependencies, requiresStagedCommit, recoveringUnsafeTraffic, workerActions, workerKeys, workerDeploymentSecrets, zoneId, now, writeRemoteInstallationCheckpoint, configs, previousConfigs, cloudflareApiBaseUrl, preserveDisabled, managedRoutes, previousPublicEndpoints, ids, runParallelPinnedPlatformWrangler, runPinnedPlatformWrangler } = input;
|
|
4613
|
+
const { plan, workerSecretUpdateIntent, workerCutoverGeneration, workerRollbacks } = state;
|
|
4614
|
+
const parallelWorkerUploads = !dependencies.deployWorker && !requiresStagedCommit && !workerSecretUpdateIntent && !recoveringUnsafeTraffic && workerActions.length === workerKeys.length && workerActions.every((action) => action.action === "update");
|
|
4615
|
+
let workerManifestQueue = Promise.resolve();
|
|
4616
|
+
let workerManifestFailed = false;
|
|
4617
|
+
let workerManifestFailure;
|
|
4618
|
+
const checkpointWorkerManifest = (update, safetyRecovery = false) => {
|
|
4619
|
+
if (!parallelWorkerUploads) return update();
|
|
4620
|
+
const result = workerManifestQueue.then(async () => {
|
|
4621
|
+
if (workerManifestFailed && !safetyRecovery) throw workerManifestFailure;
|
|
4622
|
+
try {
|
|
4623
|
+
await update();
|
|
4624
|
+
} catch (error) {
|
|
4625
|
+
workerManifestFailed = true;
|
|
4626
|
+
workerManifestFailure = error;
|
|
4627
|
+
throw error;
|
|
4628
|
+
}
|
|
4629
|
+
});
|
|
4630
|
+
workerManifestQueue = result.then(() => void 0, () => void 0);
|
|
4631
|
+
return result;
|
|
4632
|
+
};
|
|
4633
|
+
const writeManifest = async (next) => {
|
|
4634
|
+
state.manifest = next;
|
|
4635
|
+
writeInstallationManifest(next);
|
|
4636
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, next);
|
|
4637
|
+
};
|
|
4638
|
+
let availableUploadSlots = 2;
|
|
4639
|
+
let uploadFailure;
|
|
4640
|
+
const uploadWaiters = [];
|
|
4641
|
+
const acquireUploadSlot = async () => {
|
|
4642
|
+
if (uploadFailure !== void 0) throw uploadFailure;
|
|
4643
|
+
if (availableUploadSlots === 0) await new Promise((resolve, reject) => uploadWaiters.push({
|
|
4644
|
+
resolve,
|
|
4645
|
+
reject
|
|
4646
|
+
}));
|
|
4647
|
+
else availableUploadSlots--;
|
|
4648
|
+
return (failure) => {
|
|
4649
|
+
if (failure !== void 0 && uploadFailure === void 0) {
|
|
4650
|
+
uploadFailure = failure;
|
|
4651
|
+
for (const waiter of uploadWaiters.splice(0)) waiter.reject(failure);
|
|
4652
|
+
}
|
|
4653
|
+
const next = uploadFailure === void 0 ? uploadWaiters.shift() : void 0;
|
|
4654
|
+
if (next) next.resolve();
|
|
4655
|
+
else availableUploadSlots++;
|
|
4656
|
+
};
|
|
4657
|
+
};
|
|
4658
|
+
const workerReady = (parallelWorkerUploads ? workerKeys : []).map(() => {
|
|
4659
|
+
let resolve;
|
|
4660
|
+
let reject;
|
|
4661
|
+
return {
|
|
4662
|
+
promise: new Promise((onResolve, onReject) => {
|
|
4663
|
+
resolve = onResolve;
|
|
4664
|
+
reject = onReject;
|
|
4665
|
+
}),
|
|
4666
|
+
resolve,
|
|
4667
|
+
reject
|
|
4668
|
+
};
|
|
4669
|
+
});
|
|
4670
|
+
const uploadBatch = Promise.all(workerReady.map((ready) => ready.promise)).then(() => ({ error: null }), (error) => ({ error }));
|
|
4671
|
+
const deployOneWorker = async (key, waitForPriorWorker, onReadyForActivation) => {
|
|
4672
|
+
dependencies.onProgress?.(`Deploying the ${key} service`);
|
|
4673
|
+
let workerStepStarted = performance.now();
|
|
4674
|
+
const markWorkerStep = (step) => {
|
|
4675
|
+
writePlatformTiming(`worker ${key} ${step}`, workerStepStarted);
|
|
4676
|
+
workerStepStarted = performance.now();
|
|
4677
|
+
};
|
|
4678
|
+
const action = platformWorkerAction(workerActions, key);
|
|
4679
|
+
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
4680
|
+
await checkpointWorkerManifest(async () => {
|
|
4681
|
+
const intended = recordProvisionIntent(state.manifest, action, now());
|
|
4682
|
+
if (intended !== state.manifest) await writeManifest(intended);
|
|
4683
|
+
});
|
|
4684
|
+
await dependencies.revalidateBeforeWorkerDeploy(state.manifest, action);
|
|
4685
|
+
markWorkerStep("revalidate");
|
|
4686
|
+
const preservedRoutes = dependencies.deployWorker ? [] : action.action === "create" ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
|
|
4687
|
+
markWorkerStep("traffic inventory");
|
|
4688
|
+
const workerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
|
|
4689
|
+
const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
|
|
4690
|
+
const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
|
|
4691
|
+
...platformWorkerDeployCallbacks({
|
|
4692
|
+
client: dependencies.client,
|
|
4693
|
+
plan,
|
|
4694
|
+
zoneId,
|
|
4695
|
+
ids,
|
|
4696
|
+
key,
|
|
4697
|
+
action,
|
|
4698
|
+
generation: workerCutoverGeneration,
|
|
4699
|
+
now,
|
|
4700
|
+
readManifest: () => state.manifest,
|
|
4701
|
+
writeManifest,
|
|
4702
|
+
checkpoint: checkpointWorkerManifest
|
|
4703
|
+
}),
|
|
4704
|
+
apiBaseUrl: cloudflareApiBaseUrl,
|
|
4705
|
+
operatorApiToken: dependencies.operatorApiToken,
|
|
4706
|
+
...parallelWorkerUploads ? {
|
|
4707
|
+
acquireUploadSlot,
|
|
4708
|
+
onReadyForActivation,
|
|
4709
|
+
waitForPriorWorker
|
|
4710
|
+
} : {},
|
|
4711
|
+
managedRoutes: preserveDisabled ? [] : managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
|
|
4712
|
+
preservedRoutes,
|
|
4713
|
+
previousConfig: previousWorkerConfig,
|
|
4714
|
+
previousPublicEndpoints,
|
|
4715
|
+
runner: parallelWorkerUploads ? runParallelPinnedPlatformWrangler : runPinnedPlatformWrangler,
|
|
4716
|
+
workerExisted: action.action !== "create"
|
|
4717
|
+
});
|
|
4718
|
+
markWorkerStep("deploy");
|
|
4719
|
+
if (rollback) workerRollbacks.push(rollback);
|
|
4720
|
+
await checkpointWorkerManifest(async () => {
|
|
4721
|
+
const recorded = recordResource(state.manifest, action, action.name, "created", now());
|
|
4722
|
+
if (rollback || dependencies.deployWorker || JSON.stringify(recorded.resources) !== JSON.stringify(state.manifest.resources)) await writeManifest(recorded);
|
|
4723
|
+
});
|
|
4724
|
+
markWorkerStep("record");
|
|
4725
|
+
};
|
|
4726
|
+
if (parallelWorkerUploads) {
|
|
4727
|
+
const tasks = [];
|
|
4728
|
+
for (const [index, key] of workerKeys.entries()) {
|
|
4729
|
+
const priorWorker = tasks.at(-1);
|
|
4730
|
+
const ready = workerReady[index];
|
|
4731
|
+
const task = deployOneWorker(key, priorWorker ? () => priorWorker : void 0, async () => {
|
|
4732
|
+
ready.resolve();
|
|
4733
|
+
const batch = await uploadBatch;
|
|
4734
|
+
if (batch.error !== null) throw batch.error;
|
|
4735
|
+
});
|
|
4736
|
+
task.then(() => ready.resolve(), (error) => ready.reject(error));
|
|
4737
|
+
tasks.push(task);
|
|
4738
|
+
}
|
|
4739
|
+
const firstFailure = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
|
|
4740
|
+
if (firstFailure) throw firstFailure.reason;
|
|
4741
|
+
} else for (const key of workerKeys) await deployOneWorker(key);
|
|
4742
|
+
}
|
|
4743
|
+
//#endregion
|
|
4744
|
+
//#region src/cli/platform-installer-health.ts
|
|
4745
|
+
var PlatformAccessBlockedError = class extends Error {
|
|
4746
|
+
constructor(url, context) {
|
|
4747
|
+
super(context?.accessProtected ? [
|
|
4748
|
+
"platform: Cloudflare Access blocked a protected platform health check (Default-Deny, error 1050).",
|
|
4749
|
+
`Blocked URL: ${url}`,
|
|
4750
|
+
"Keep employee Access protection and account-wide Default-Deny enabled.",
|
|
4751
|
+
"The reserved dispatch /health endpoint uses a path-only Access exception and still requires Void’s private probe token.",
|
|
4752
|
+
"Resume to verify the health exceptions and saved installer credentials.",
|
|
4753
|
+
...context.resumeCommand ? [`Resume: ${context.resumeCommand}`] : [],
|
|
4754
|
+
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
4755
|
+
].join("\n") : [
|
|
4756
|
+
"platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
|
|
4757
|
+
`Blocked URL: ${url}`,
|
|
4758
|
+
"",
|
|
4759
|
+
"To make this test platform publicly reachable:",
|
|
4760
|
+
"",
|
|
4761
|
+
`1. In ${context ? `account ${JSON.stringify(context.accountName)}` : "your Cloudflare account"}, open:`,
|
|
4762
|
+
" Zero Trust → Access controls → Applications → Create new application.",
|
|
4763
|
+
"2. Choose Self-hosted. When adding destinations, choose Worker and select:",
|
|
4764
|
+
` ${context ? context.workers.join(", ") + "." : "This installation's API, proxy, and dispatch Workers."}`,
|
|
4765
|
+
"3. Add a policy: Action → Bypass; Include → Everyone.",
|
|
4766
|
+
context?.resumeCommand ? `4. Save the policy and application, then resume:\n\n ${context.resumeCommand}` : "4. Save the policy and application, then rerun your platform command.",
|
|
4767
|
+
"",
|
|
4768
|
+
"This removes Cloudflare Access protection for these Workers.",
|
|
4769
|
+
"Void’s own authentication stays enabled. Keep account-wide Default-Deny on.",
|
|
4770
|
+
"",
|
|
4771
|
+
"For platforms that must remain Access-protected:",
|
|
4772
|
+
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
4773
|
+
].join("\n"));
|
|
4774
|
+
}
|
|
4775
|
+
};
|
|
4776
|
+
async function platformProbeFailure(label, url, response, context) {
|
|
4777
|
+
const message = `platform: ${label} failed at ${url} (HTTP ${response.status}).`;
|
|
4778
|
+
if (response.status === 403 && response.headers.get("server")?.toLowerCase() === "cloudflare" && response.headers.get("content-type")?.toLowerCase().includes("text/html")) {
|
|
4779
|
+
const body = await response.text().catch(() => "");
|
|
4780
|
+
if (body.includes("Default-Deny") && /errorCode:\s*1050\b/.test(body)) return new PlatformAccessBlockedError(url, context);
|
|
4781
|
+
}
|
|
4782
|
+
return new Error(message);
|
|
4783
|
+
}
|
|
4784
|
+
async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicationBaseDomain, platformVersion, usageProtocolVersion, healthProbeToken, fetcher = fetch, dispatchOrigin, context) {
|
|
4785
|
+
const request = (url) => fetcher(url, {
|
|
4786
|
+
headers: {
|
|
4787
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
4788
|
+
...cfAccessHeaders(process.env, url)
|
|
4789
|
+
},
|
|
4790
|
+
redirect: "manual",
|
|
4791
|
+
signal: AbortSignal.timeout(3e4)
|
|
4792
|
+
});
|
|
4793
|
+
const dispatchUrl = dispatchOrigin ?? `https://${PLATFORM_HEALTH_HOSTNAME_LABEL}.${applicationBaseDomain}`;
|
|
4794
|
+
const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
|
|
4795
|
+
request(`${apiUrl}/health`),
|
|
4796
|
+
request(`${apiUrl}/.well-known/void-platform`),
|
|
4797
|
+
request(`${proxyUrl}/health`),
|
|
4798
|
+
request(`${dispatchUrl}/health`),
|
|
4799
|
+
context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
|
|
4800
|
+
]);
|
|
4801
|
+
if (!health.ok) throw await platformProbeFailure("Health check", `${apiUrl}/health`, health, context);
|
|
4802
|
+
if (!discovery.ok) throw await platformProbeFailure("Discovery check", `${apiUrl}/.well-known/void-platform`, discovery, context);
|
|
4803
|
+
const body = await discovery.json();
|
|
4804
|
+
if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
|
|
4805
|
+
if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
|
|
4806
|
+
if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
|
|
4807
|
+
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", `${proxyUrl}/health`, proxy, context);
|
|
4808
|
+
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", `${dispatchUrl}/health`, dispatch, context);
|
|
4809
|
+
if (emailGateway) {
|
|
4810
|
+
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", `${context.emailGatewayUrl}/health`, emailGateway, context);
|
|
4811
|
+
const identity = await emailGateway.json();
|
|
4812
|
+
if (identity?.installationId !== installationId || identity.platformVersion !== platformVersion || identity.emailProtocolVersion !== 1) throw new Error("platform: Deployed email gateway identity, version, or protocol does not match this installation.");
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fetch) {
|
|
4816
|
+
const response = await fetcher(`${apiUrl}/internal/platform/sandbox-drain`, {
|
|
4817
|
+
headers: {
|
|
4818
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
4819
|
+
...cfAccessHeaders(process.env, apiUrl)
|
|
4820
|
+
},
|
|
4821
|
+
signal: AbortSignal.timeout(3e4),
|
|
4822
|
+
redirect: "manual"
|
|
4823
|
+
});
|
|
4824
|
+
if (!response.ok) throw new Error(`platform: Sandbox drain probe failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
|
|
4825
|
+
let value;
|
|
4826
|
+
try {
|
|
4827
|
+
value = await response.json();
|
|
4828
|
+
} catch {
|
|
4829
|
+
throw new Error("platform: Sandbox drain probe did not return its DB-backed protocol v1 document. Traffic remains disabled.");
|
|
4830
|
+
}
|
|
4831
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
|
|
4832
|
+
const status = value;
|
|
4833
|
+
const validState = status.state === "required" || status.state === "draining" || status.state === "complete";
|
|
4834
|
+
const validPhase = status.phase === null || status.phase === "deployments" || status.phase === "resources";
|
|
4835
|
+
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.");
|
|
4836
|
+
return status.state === "complete";
|
|
4837
|
+
}
|
|
4838
|
+
async function verifyPlatformDeploymentExecutionDrain(apiUrl, healthProbeToken, installationId, fetcher = fetch) {
|
|
4839
|
+
const response = await fetcher(`${apiUrl}/__void/platform/deployment-execution`, {
|
|
4840
|
+
headers: {
|
|
4841
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
4842
|
+
...cfAccessHeaders(process.env, apiUrl)
|
|
4843
|
+
},
|
|
4844
|
+
signal: AbortSignal.timeout(3e4),
|
|
4845
|
+
redirect: "manual"
|
|
4846
|
+
});
|
|
4847
|
+
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.`);
|
|
4848
|
+
let value;
|
|
4849
|
+
try {
|
|
4850
|
+
value = await response.json();
|
|
4851
|
+
} catch {
|
|
4852
|
+
throw new Error("platform: Deployment execution drain probe returned an invalid document.");
|
|
4853
|
+
}
|
|
4854
|
+
const status = value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4855
|
+
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.");
|
|
4856
|
+
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.");
|
|
4857
|
+
}
|
|
4858
|
+
async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
|
|
4859
|
+
const deadline = Date.now() + 3e5;
|
|
4860
|
+
let operationId = null;
|
|
4861
|
+
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.`);
|
|
4862
|
+
const waitForProgress = async () => {
|
|
4863
|
+
const remaining = deadline - Date.now();
|
|
4864
|
+
if (remaining <= 0) throw timedOut();
|
|
4865
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, remaining)));
|
|
4866
|
+
};
|
|
4867
|
+
let response;
|
|
4868
|
+
let value;
|
|
4869
|
+
let status;
|
|
4870
|
+
for (;;) {
|
|
4871
|
+
const remaining = deadline - Date.now();
|
|
4872
|
+
if (remaining <= 0) throw timedOut();
|
|
4873
|
+
const signal = AbortSignal.timeout(Math.min(12e4, remaining));
|
|
4874
|
+
try {
|
|
4875
|
+
response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
|
|
4876
|
+
method: "POST",
|
|
4877
|
+
headers: {
|
|
4878
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
4879
|
+
...cfAccessHeaders(process.env, apiUrl)
|
|
4880
|
+
},
|
|
4881
|
+
signal,
|
|
4882
|
+
redirect: "manual"
|
|
4883
|
+
});
|
|
4884
|
+
} catch (error) {
|
|
4885
|
+
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
4886
|
+
await waitForProgress();
|
|
4887
|
+
continue;
|
|
4888
|
+
}
|
|
4889
|
+
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.`);
|
|
4890
|
+
}
|
|
4891
|
+
try {
|
|
4892
|
+
value = await response.json();
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
4895
|
+
await waitForProgress();
|
|
4896
|
+
continue;
|
|
4897
|
+
}
|
|
4898
|
+
throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
|
|
4899
|
+
}
|
|
4900
|
+
status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4901
|
+
const observedOperationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
|
|
4902
|
+
if (status?.version === 1 && observedOperationId) {
|
|
4903
|
+
if (operationId && operationId !== observedOperationId) throw new Error("platform: Shared email bootstrap operation identity changed while waiting. Traffic remains disabled. Inspect the installation before retrying.");
|
|
4904
|
+
operationId = observedOperationId;
|
|
4905
|
+
}
|
|
4906
|
+
if ([
|
|
4907
|
+
200,
|
|
4908
|
+
202,
|
|
4909
|
+
503
|
|
4910
|
+
].includes(response.status) && status?.version === 1 && status.state === "incomplete" && observedOperationId && (status.operationStatus === "pending" || status.operationStatus === "running") && status.failureCode == null) {
|
|
4911
|
+
await waitForProgress();
|
|
4912
|
+
continue;
|
|
4913
|
+
}
|
|
4914
|
+
break;
|
|
4915
|
+
}
|
|
4916
|
+
if (!response.ok || status?.version !== 1 || status.state !== "ready" || operationId === null || status.operationId !== operationId || status.operationStatus != null && status.operationStatus !== "completed" || status.failureCode != null) {
|
|
4917
|
+
const code = (value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value) ? value : null;
|
|
4918
|
+
const operationStatus = code(status?.operationStatus);
|
|
4919
|
+
const operationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
|
|
4920
|
+
const details = operationId ? [`Operation ${operationId}: ${operationStatus ?? "incomplete"}.`] : operationStatus ? [`Operation status: ${operationStatus}.`] : [];
|
|
4921
|
+
const currentStep = code(status?.currentStep);
|
|
4922
|
+
const failureCode = code(status?.failureCode);
|
|
4923
|
+
if (currentStep) details.push(`Step: ${currentStep}.`);
|
|
4924
|
+
if (failureCode) details.push(`Failure: ${failureCode}.`);
|
|
4925
|
+
let domain = null;
|
|
4926
|
+
if (typeof status?.domain === "string") try {
|
|
4927
|
+
domain = normalizeApplicationDomain(status.domain);
|
|
4928
|
+
} catch {}
|
|
4929
|
+
const inboundStatus = code(status?.inboundStatus);
|
|
4930
|
+
if (domain) details.push(`Domain: ${domain}${inboundStatus ? ` (inbound: ${inboundStatus})` : ""}.`);
|
|
4931
|
+
const mxDetail = domain ? `MX records for ${domain} do not route mail to Cloudflare Email Routing` : null;
|
|
4932
|
+
const safeDetails = [
|
|
4933
|
+
...domain ? [mxDetail, `enabled explicit Cloudflare email rules conflict with ${domain}`] : [],
|
|
4934
|
+
"zone catch-all does not point to this ingress",
|
|
4935
|
+
"another enabled catch-all owns this zone"
|
|
4936
|
+
];
|
|
4937
|
+
if (typeof status?.failureDetail === "string" && safeDetails.includes(status.failureDetail)) {
|
|
4938
|
+
details.push(`${status.failureDetail}.`);
|
|
4939
|
+
if (currentStep === "inbound_probe" && status.failureDetail === mxDetail) details.push(`Enable Email Routing for ${domain} in the Cloudflare dashboard, then resume this installation.`);
|
|
4940
|
+
}
|
|
4941
|
+
const detail = details.length ? ` ${details.join(" ")}` : "";
|
|
4942
|
+
throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
|
|
4943
|
+
}
|
|
4944
|
+
return operationId;
|
|
4945
|
+
}
|
|
4946
|
+
//#endregion
|
|
4947
|
+
//#region src/cli/platform-install-activation.ts
|
|
4948
|
+
async function enableInstallationServices(input) {
|
|
4949
|
+
const { state, checkpointOperation, requiresStagedCommit, authenticationCutoverCommitted, now, writeRemoteInstallationCheckpoint, dependencies, workerKeys, workerActions, workerDeploymentSecrets, zoneId, enabledConfigs, configs, cloudflareApiBaseUrl, managedRoutes, ids, runPinnedPlatformWrangler, shouldBootstrapSharedEmail, releaseDatabaseLock, version, targetUsageProtocolVersion, healthCheckContext, runtime, runtimeProvenance } = input;
|
|
4950
|
+
const { plan, secrets } = state;
|
|
4951
|
+
await checkpointOperation("enabling");
|
|
4952
|
+
if (requiresStagedCommit) {
|
|
4953
|
+
if (!authenticationCutoverCommitted) {
|
|
4954
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
|
|
4955
|
+
state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
|
|
4956
|
+
writeInstallationManifest(state.manifest);
|
|
4957
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
4958
|
+
state.workerRollbacks = [];
|
|
4959
|
+
}
|
|
4960
|
+
state.workerCutoverGeneration = nextWorkerCutoverGeneration(state.manifest);
|
|
4961
|
+
if (state.workerSecretUpdateIntent) {
|
|
4962
|
+
state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "pending", state.workerSecretUpdateIntent.id, now());
|
|
4963
|
+
writeInstallationManifest(state.manifest);
|
|
4964
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
4965
|
+
state.workerSecretUpdateIntent = {
|
|
4966
|
+
...state.workerSecretUpdateIntent,
|
|
4967
|
+
generation: state.workerCutoverGeneration
|
|
4968
|
+
};
|
|
4969
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(secrets, state.workerSecretUpdateIntent));
|
|
4970
|
+
}
|
|
4971
|
+
const writeManifest = async (next) => {
|
|
4972
|
+
state.manifest = next;
|
|
4973
|
+
writeInstallationManifest(next);
|
|
4974
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, next);
|
|
4975
|
+
};
|
|
4976
|
+
for (const key of workerKeys) {
|
|
4977
|
+
dependencies.onProgress?.(`Enabling the ${key} service`);
|
|
4978
|
+
const action = platformWorkerAction(workerActions, key);
|
|
4979
|
+
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
4980
|
+
const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
|
|
4981
|
+
const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
|
|
4982
|
+
const previousWorkerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
|
|
4983
|
+
const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
|
|
4984
|
+
...platformWorkerDeployCallbacks({
|
|
4985
|
+
client: dependencies.client,
|
|
4986
|
+
plan,
|
|
4987
|
+
zoneId,
|
|
4988
|
+
ids,
|
|
4989
|
+
key,
|
|
4990
|
+
action,
|
|
4991
|
+
generation: state.workerCutoverGeneration,
|
|
4992
|
+
now,
|
|
4993
|
+
readManifest: () => state.manifest,
|
|
4994
|
+
writeManifest,
|
|
4995
|
+
checkpoint: (update) => update()
|
|
4996
|
+
}),
|
|
4997
|
+
apiBaseUrl: cloudflareApiBaseUrl,
|
|
4998
|
+
managedRoutes: managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
|
|
4999
|
+
preservedRoutes,
|
|
5000
|
+
previousConfig: previousWorkerConfig,
|
|
5001
|
+
previousPublicEndpoints: true,
|
|
5002
|
+
reuseMatchingVersion: false,
|
|
5003
|
+
runner: runPinnedPlatformWrangler,
|
|
5004
|
+
workerExisted: true
|
|
5005
|
+
});
|
|
5006
|
+
if (rollback) state.workerRollbacks.push(rollback);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
const verifyCurrentWorkerCutover = async () => {
|
|
5010
|
+
if (!requiresStagedCommit) return;
|
|
5011
|
+
if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(state.manifest, state.workerCutoverGeneration);
|
|
5012
|
+
else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(state.manifest, state.workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler, workerKeys);
|
|
5013
|
+
};
|
|
5014
|
+
await verifyCurrentWorkerCutover();
|
|
5015
|
+
let emailOperationId;
|
|
5016
|
+
if (shouldBootstrapSharedEmail) {
|
|
5017
|
+
state.workerDeploymentCommitted = true;
|
|
5018
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
|
|
5019
|
+
if (state.workerSecretUpdateIntent) state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "committed", state.workerSecretUpdateIntent.id, now());
|
|
5020
|
+
writeInstallationManifest(state.manifest);
|
|
5021
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
5022
|
+
state.workerRollbacks = [];
|
|
5023
|
+
dependencies.onProgress?.("Preparing shared email routing");
|
|
5024
|
+
emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(state.manifest.ownershipNonce));
|
|
5025
|
+
}
|
|
5026
|
+
await releaseDatabaseLock.liftProjectZeroTrustFence?.();
|
|
5027
|
+
await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
|
|
5028
|
+
if (requiresStagedCommit) {
|
|
5029
|
+
dependencies.onProgress?.("Verifying the enabled platform");
|
|
5030
|
+
await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, state.manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(state.manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ? platformWorkersDevUrl(plan, plan.deployment.resources.dispatchWorker) : void 0, healthCheckContext);
|
|
5031
|
+
await verifyCurrentWorkerCutover();
|
|
5032
|
+
}
|
|
5033
|
+
state.workerDeploymentCommitted = true;
|
|
5034
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
|
|
5035
|
+
state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, true, now());
|
|
5036
|
+
if (state.workerSecretUpdateIntent) state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "committed", state.workerSecretUpdateIntent.id, now());
|
|
5037
|
+
state.manifest = {
|
|
5038
|
+
...state.manifest,
|
|
5039
|
+
deployment: plan.deployment,
|
|
5040
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
5041
|
+
platformVersion: version,
|
|
5042
|
+
runtime: runtimeProvenance,
|
|
5043
|
+
safetyProtocols: runtime.safetyProtocols,
|
|
5044
|
+
updatedAt: now(),
|
|
5045
|
+
usageProtocolVersion: targetUsageProtocolVersion
|
|
5046
|
+
};
|
|
5047
|
+
state.manifest = transitionInstallation(state.manifest, "installed", now());
|
|
5048
|
+
return { emailOperationId };
|
|
5049
|
+
}
|
|
5050
|
+
//#endregion
|
|
5051
|
+
//#region src/cli/platform-access-health.ts
|
|
5052
|
+
/** These reserved endpoints require Void's probe token and cannot serve application traffic. */
|
|
5053
|
+
function platformAccessHealthTargets(plan) {
|
|
5054
|
+
const destination = `${plan.deployment.application.zoneName ? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${plan.deployment.application.baseDomain}` : `${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev`}/health`;
|
|
5055
|
+
return [{
|
|
5056
|
+
key: `health-dispatch-${createHash("sha256").update(destination).digest("hex").slice(0, 12)}`,
|
|
5057
|
+
destination
|
|
5058
|
+
}];
|
|
5059
|
+
}
|
|
5060
|
+
async function verifyHealthException(access, app, destination) {
|
|
5061
|
+
const destinations = platformAccessPublicDestinations(app);
|
|
5062
|
+
if (typeof app.id !== "string" || !app.id || app.type !== "self_hosted" || app.domain !== destination || !destinations.length || destinations.some((value) => value !== destination) || app.destinations !== void 0 && (!Array.isArray(app.destinations) || app.destinations.some((value) => !value || value.type !== "public" || value.uri !== destination)) || app.target_criteria != null && (!Array.isArray(app.target_criteria) || app.target_criteria.length > 0)) throw new Error(`platform: The Access health exception must cover only ${destination}.`);
|
|
5063
|
+
const policies = await access.list(`apps/${encodeURIComponent(app.id)}/policies`);
|
|
5064
|
+
const policy = policies[0];
|
|
5065
|
+
if (policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== "[{\"everyone\":{}}]" || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) throw new Error(`platform: The Access policy for ${destination} does not match the reserved health-check exception.`);
|
|
5066
|
+
}
|
|
5067
|
+
/** Keep the same path-only exception used by project Zero Trust available during bootstrap. */
|
|
5068
|
+
async function ensurePlatformAccessHealthChecks(input) {
|
|
5069
|
+
const appAccess = !input.manifest.steps.some((step) => step.key === "app-access-initialized:v1") && input.checkpoint.appAccessSetupJson ? parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson)) : null;
|
|
4854
5070
|
if (appAccess && appAccess.accountId !== input.plan.account.id) throw new Error("platform: App access recovery belongs to another Cloudflare account.");
|
|
4855
5071
|
if (!input.plan.deployment.authentication.accessProtection && !appAccess?.managed) return;
|
|
4856
5072
|
const spec = appAccess?.managed ? {
|
|
@@ -4949,7 +5165,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
4949
5165
|
platformAccessResourceInventory(encoded, input.manifest.id);
|
|
4950
5166
|
const state = JSON.parse(encoded);
|
|
4951
5167
|
const spec = parsePlatformAccessSetupSpec(JSON.parse(specEncoded));
|
|
4952
|
-
if (state.accountId !== (spec.accountId ?? input.manifest.account.id) || state.nonce !==
|
|
5168
|
+
if (state.accountId !== (spec.accountId ?? input.manifest.account.id) || state.nonce !== platformAccessNonce(input.manifest.ownershipNonce)) throw new Error("platform: The saved Access setup does not match this installation and identity account.");
|
|
4953
5169
|
const application = state.resources.protection;
|
|
4954
5170
|
const service = state.resources["service-token"];
|
|
4955
5171
|
if (!application?.id || application.pending || !service?.id || service.pending || spec.protectionApplicationId && spec.protectionApplicationId !== application.id) throw new Error("platform: Complete or restore the Access application and service-token checkpoint before maintenance.");
|
|
@@ -4984,7 +5200,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
4984
5200
|
}
|
|
4985
5201
|
const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
|
|
4986
5202
|
if (existingWorkers.length) {
|
|
4987
|
-
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-
|
|
5203
|
+
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-D5wvsI-Y.mjs");
|
|
4988
5204
|
await verifyProtectionWorkerOrigins({
|
|
4989
5205
|
request: input.client.request.bind(input.client),
|
|
4990
5206
|
listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
|
|
@@ -4994,213 +5210,200 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
4994
5210
|
}
|
|
4995
5211
|
//#endregion
|
|
4996
5212
|
//#region src/cli/platform-install-preparation.ts
|
|
5213
|
+
function withAuthentication(deployment, authentication) {
|
|
5214
|
+
return createPlatformDeploymentManifest({
|
|
5215
|
+
...deployment,
|
|
5216
|
+
authentication,
|
|
5217
|
+
capabilities: {
|
|
5218
|
+
...deployment.capabilities,
|
|
5219
|
+
githubOAuth: authentication.providers.includes("github"),
|
|
5220
|
+
googleOAuth: authentication.providers.includes("google")
|
|
5221
|
+
}
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
4997
5224
|
async function prepareInstallationAuthentication(input) {
|
|
4998
|
-
const { releaseDatabaseLock, runtime, authenticationProtocolTransition, now, dependencies
|
|
5225
|
+
const { state, releaseDatabaseLock, runtime, authenticationProtocolTransition, now, dependencies } = input;
|
|
5226
|
+
const { workerSecretUpdateIntent } = state;
|
|
4999
5227
|
const liveAuthentication = await releaseDatabaseLock.readAuthenticationState?.();
|
|
5000
5228
|
if (liveAuthentication) {
|
|
5001
|
-
|
|
5229
|
+
state.authenticationEnforcementActive = liveAuthentication.sessionsRequired;
|
|
5002
5230
|
if (liveAuthentication.sessionsRequired && runtime.authenticationProtocolVersion !== 1) throw new Error("platform: This runtime cannot enforce the active authentication configuration. Use an authentication-compatible runtime.");
|
|
5003
|
-
const deployment = mergePlatformLiveAuthenticationDeployment(
|
|
5004
|
-
|
|
5005
|
-
...
|
|
5231
|
+
const deployment = mergePlatformLiveAuthenticationDeployment(state.plan.deployment, liveAuthentication);
|
|
5232
|
+
state.plan = {
|
|
5233
|
+
...state.plan,
|
|
5006
5234
|
deployment
|
|
5007
5235
|
};
|
|
5008
|
-
if (!authenticationProtocolTransition)
|
|
5009
|
-
...
|
|
5010
|
-
deployment
|
|
5011
|
-
};
|
|
5012
|
-
}
|
|
5013
|
-
if (!liveAuthentication && input.manifest.deployment.authentication.configurable && !input.plan.deployment.authentication.configurable) {
|
|
5014
|
-
const authentication = input.manifest.deployment.authentication;
|
|
5015
|
-
const providers = authentication.providers;
|
|
5016
|
-
input.plan = {
|
|
5017
|
-
...input.plan,
|
|
5018
|
-
deployment: createPlatformDeploymentManifest({
|
|
5019
|
-
...input.plan.deployment,
|
|
5020
|
-
authentication,
|
|
5021
|
-
capabilities: {
|
|
5022
|
-
...input.plan.deployment.capabilities,
|
|
5023
|
-
githubOAuth: providers.includes("github"),
|
|
5024
|
-
googleOAuth: providers.includes("google")
|
|
5025
|
-
}
|
|
5026
|
-
})
|
|
5027
|
-
};
|
|
5028
|
-
}
|
|
5029
|
-
if (authenticationProtocolTransition && !input.plan.deployment.authentication.configurable) {
|
|
5030
|
-
const providers = input.plan.deployment.authentication.providers;
|
|
5031
|
-
const deployment = createPlatformDeploymentManifest({
|
|
5032
|
-
...input.plan.deployment,
|
|
5033
|
-
authentication: {
|
|
5034
|
-
providers,
|
|
5035
|
-
configurable: true
|
|
5036
|
-
},
|
|
5037
|
-
capabilities: {
|
|
5038
|
-
...input.plan.deployment.capabilities,
|
|
5039
|
-
githubOAuth: providers.includes("github"),
|
|
5040
|
-
googleOAuth: providers.includes("google")
|
|
5041
|
-
}
|
|
5042
|
-
});
|
|
5043
|
-
input.plan = {
|
|
5044
|
-
...input.plan,
|
|
5236
|
+
if (!authenticationProtocolTransition) state.manifest = {
|
|
5237
|
+
...state.manifest,
|
|
5045
5238
|
deployment
|
|
5046
5239
|
};
|
|
5047
5240
|
}
|
|
5241
|
+
if (!liveAuthentication && state.manifest.deployment.authentication.configurable && !state.plan.deployment.authentication.configurable) state.plan = {
|
|
5242
|
+
...state.plan,
|
|
5243
|
+
deployment: withAuthentication(state.plan.deployment, state.manifest.deployment.authentication)
|
|
5244
|
+
};
|
|
5245
|
+
if (authenticationProtocolTransition && !state.plan.deployment.authentication.configurable) state.plan = {
|
|
5246
|
+
...state.plan,
|
|
5247
|
+
deployment: withAuthentication(state.plan.deployment, {
|
|
5248
|
+
providers: state.plan.deployment.authentication.providers,
|
|
5249
|
+
configurable: true
|
|
5250
|
+
})
|
|
5251
|
+
};
|
|
5048
5252
|
const activateAccessCredentials = () => {
|
|
5049
|
-
if (!
|
|
5050
|
-
const credentials = JSON.parse(
|
|
5253
|
+
if (!state.secrets.accessCredentialsJson) return;
|
|
5254
|
+
const credentials = JSON.parse(state.secrets.accessCredentialsJson);
|
|
5051
5255
|
if (!credentials || typeof credentials !== "object" || Array.isArray(credentials)) throw new Error("platform: Invalid saved Access credentials.");
|
|
5052
|
-
const origins = [
|
|
5256
|
+
const origins = [state.plan.apiUrl, state.plan.proxyUrl];
|
|
5053
5257
|
activatePlatformAccessCredentials(JSON.stringify(Object.fromEntries(Object.entries(credentials).filter(([origin]) => origins.includes(origin)))), origins);
|
|
5054
5258
|
};
|
|
5055
5259
|
activateAccessCredentials();
|
|
5056
|
-
|
|
5057
|
-
writeInstallationManifest(
|
|
5058
|
-
await releaseDatabaseLock.checkpoint?.(
|
|
5260
|
+
state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
|
|
5261
|
+
writeInstallationManifest(state.manifest);
|
|
5262
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
5059
5263
|
dependencies.onProgress?.("Checking and creating Cloudflare resources");
|
|
5060
|
-
if (!authenticationProtocolTransition && JSON.stringify(
|
|
5061
|
-
|
|
5062
|
-
...
|
|
5063
|
-
deployment:
|
|
5264
|
+
if (!authenticationProtocolTransition && JSON.stringify(state.manifest.deployment) !== JSON.stringify(state.plan.deployment)) {
|
|
5265
|
+
state.manifest = {
|
|
5266
|
+
...state.manifest,
|
|
5267
|
+
deployment: state.plan.deployment,
|
|
5064
5268
|
updatedAt: now()
|
|
5065
5269
|
};
|
|
5066
|
-
writeInstallationManifest(
|
|
5067
|
-
await releaseDatabaseLock.checkpoint?.(
|
|
5270
|
+
writeInstallationManifest(state.manifest);
|
|
5271
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
5068
5272
|
}
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
writeInstallationManifest(input.manifest);
|
|
5273
|
+
if (state.manifest.status !== "provisioning") {
|
|
5274
|
+
state.manifest = transitionInstallation(state.manifest, "provisioning", now());
|
|
5275
|
+
writeInstallationManifest(state.manifest);
|
|
5073
5276
|
}
|
|
5074
|
-
if (runtime.deploymentExecution !== 1 && (
|
|
5277
|
+
if (runtime.deploymentExecution !== 1 && (state.manifest.deploymentExecution === 1 || dependencies.databaseRollbackSource?.manifest.deploymentExecution === 1)) {
|
|
5075
5278
|
if (!releaseDatabaseLock.checkpoint) throw new Error("platform: The database lifecycle lock cannot fence deployment admission.");
|
|
5076
|
-
await releaseDatabaseLock.checkpoint(
|
|
5279
|
+
await releaseDatabaseLock.checkpoint(state.manifest);
|
|
5077
5280
|
dependencies.onProgress?.("Verifying durable deployment operations have drained");
|
|
5078
|
-
await (dependencies.verifyDeploymentExecutionDrain ?? verifyPlatformDeploymentExecutionDrain)(
|
|
5281
|
+
await (dependencies.verifyDeploymentExecutionDrain ?? verifyPlatformDeploymentExecutionDrain)(state.plan.apiUrl, createPlatformHealthProbeToken(state.manifest.ownershipNonce), state.manifest.id);
|
|
5079
5282
|
}
|
|
5080
5283
|
if (dependencies.prepareAuthentication) {
|
|
5081
5284
|
dependencies.onProgress?.("Preparing platform authentication");
|
|
5082
5285
|
try {
|
|
5083
|
-
|
|
5084
|
-
...
|
|
5085
|
-
...await dependencies.prepareAuthentication(
|
|
5286
|
+
state.secrets = {
|
|
5287
|
+
...state.secrets,
|
|
5288
|
+
...await dependencies.prepareAuthentication(state.manifest, state.secrets)
|
|
5086
5289
|
};
|
|
5087
5290
|
} catch (error) {
|
|
5088
|
-
const latest = readPendingInstallationSecrets(
|
|
5089
|
-
|
|
5090
|
-
...
|
|
5291
|
+
const latest = readPendingInstallationSecrets(state.manifest) ?? {};
|
|
5292
|
+
state.secrets = {
|
|
5293
|
+
...state.secrets,
|
|
5091
5294
|
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
5092
|
-
authenticationSetupJson: latest.authenticationSetupJson ||
|
|
5093
|
-
authSetupToken: latest.authSetupToken ||
|
|
5094
|
-
authBootstrapCode: latest.authBootstrapCode ||
|
|
5095
|
-
accessProvisioningJson: latest.accessProvisioningJson ||
|
|
5096
|
-
accessCredentialsJson: latest.accessCredentialsJson ||
|
|
5295
|
+
authenticationSetupJson: latest.authenticationSetupJson || state.secrets.authenticationSetupJson,
|
|
5296
|
+
authSetupToken: latest.authSetupToken || state.secrets.authSetupToken,
|
|
5297
|
+
authBootstrapCode: latest.authBootstrapCode || state.secrets.authBootstrapCode,
|
|
5298
|
+
accessProvisioningJson: latest.accessProvisioningJson || state.secrets.accessProvisioningJson,
|
|
5299
|
+
accessCredentialsJson: latest.accessCredentialsJson || state.secrets.accessCredentialsJson
|
|
5097
5300
|
};
|
|
5098
|
-
|
|
5099
|
-
writeInstallationManifest(
|
|
5100
|
-
await releaseDatabaseLock.checkpoint?.(
|
|
5301
|
+
state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
|
|
5302
|
+
writeInstallationManifest(state.manifest);
|
|
5303
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
5101
5304
|
throw error;
|
|
5102
5305
|
}
|
|
5103
|
-
const latest = readPendingInstallationSecrets(
|
|
5104
|
-
|
|
5105
|
-
...
|
|
5306
|
+
const latest = readPendingInstallationSecrets(state.manifest) ?? {};
|
|
5307
|
+
state.secrets = {
|
|
5308
|
+
...state.secrets,
|
|
5106
5309
|
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name)))
|
|
5107
5310
|
};
|
|
5108
|
-
|
|
5109
|
-
writeInstallationManifest(
|
|
5110
|
-
await releaseDatabaseLock.checkpoint?.(
|
|
5311
|
+
state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
|
|
5312
|
+
writeInstallationManifest(state.manifest);
|
|
5313
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
5111
5314
|
activateAccessCredentials();
|
|
5112
|
-
assertRequiredPlatformSecrets(
|
|
5113
|
-
if (
|
|
5114
|
-
writePendingInstallationSecrets(
|
|
5115
|
-
...secretCheckpoint(
|
|
5315
|
+
assertRequiredPlatformSecrets(state.plan, state.manifest, state.secrets);
|
|
5316
|
+
if (state.secrets.authenticationSetupJson && createHash("sha256").update(state.secrets.authBootstrapCode ?? "").digest("hex") !== parsePlatformAuthenticationSetup(JSON.parse(state.secrets.authenticationSetupJson)).bootstrapCredentialHash) throw new Error("platform: Prepared authentication does not match its administrator setup code.");
|
|
5317
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
5318
|
+
...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
|
|
5116
5319
|
databaseClaimState: "claimed"
|
|
5117
5320
|
});
|
|
5118
5321
|
}
|
|
5119
|
-
const appAccessSetupRequired = !!
|
|
5322
|
+
const appAccessSetupRequired = !!state.plan.deployment.appAccess && !state.manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
5120
5323
|
if (appAccessSetupRequired) {
|
|
5121
5324
|
if (dependencies.prepareAppAccess) try {
|
|
5122
|
-
const appAccessSetupJson = await dependencies.prepareAppAccess(
|
|
5123
|
-
const latest = readPendingInstallationSecrets(
|
|
5124
|
-
|
|
5125
|
-
...
|
|
5325
|
+
const appAccessSetupJson = await dependencies.prepareAppAccess(state.manifest, state.secrets);
|
|
5326
|
+
const latest = readPendingInstallationSecrets(state.manifest) ?? {};
|
|
5327
|
+
state.secrets = {
|
|
5328
|
+
...state.secrets,
|
|
5126
5329
|
appAccessSetupJson,
|
|
5127
5330
|
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
|
|
5128
5331
|
};
|
|
5129
|
-
writePendingInstallationSecrets(
|
|
5130
|
-
...secretCheckpoint(
|
|
5332
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
5333
|
+
...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
|
|
5131
5334
|
databaseClaimState: "claimed"
|
|
5132
5335
|
});
|
|
5133
5336
|
} catch (error) {
|
|
5134
|
-
const latest = readPendingInstallationSecrets(
|
|
5135
|
-
|
|
5136
|
-
...
|
|
5137
|
-
appAccessSetupJson: latest.appAccessSetupJson ||
|
|
5337
|
+
const latest = readPendingInstallationSecrets(state.manifest) ?? {};
|
|
5338
|
+
state.secrets = {
|
|
5339
|
+
...state.secrets,
|
|
5340
|
+
appAccessSetupJson: latest.appAccessSetupJson || state.secrets.appAccessSetupJson,
|
|
5138
5341
|
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
|
|
5139
5342
|
};
|
|
5140
5343
|
throw error;
|
|
5141
5344
|
}
|
|
5142
|
-
if (!
|
|
5143
|
-
if (!
|
|
5144
|
-
if (
|
|
5145
|
-
|
|
5146
|
-
...
|
|
5345
|
+
if (!state.secrets.appAccessSetupJson) throw new Error("platform: Restore the encrypted app access setup checkpoint before resuming.");
|
|
5346
|
+
if (!state.secrets.appAccessSetupToken) {
|
|
5347
|
+
if (state.manifest.resources.some((resource) => resource.key === "api-worker" && resource.state === "ready")) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
|
|
5348
|
+
state.secrets = {
|
|
5349
|
+
...state.secrets,
|
|
5147
5350
|
appAccessSetupToken: randomBytes(32).toString("hex")
|
|
5148
5351
|
};
|
|
5149
|
-
writePendingInstallationSecrets(
|
|
5150
|
-
...secretCheckpoint(
|
|
5352
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
5353
|
+
...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
|
|
5151
5354
|
databaseClaimState: "claimed"
|
|
5152
5355
|
});
|
|
5153
5356
|
}
|
|
5154
5357
|
}
|
|
5155
|
-
if (
|
|
5156
|
-
const protection = liveAuthentication ? liveAuthentication.protection :
|
|
5358
|
+
if (state.plan.deployment.authentication.accessProtection) {
|
|
5359
|
+
const protection = liveAuthentication ? liveAuthentication.protection : state.secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(state.secrets.authenticationSetupJson)).protection : null;
|
|
5157
5360
|
if (!protection) throw new Error("platform: The active Access protection configuration is unavailable. Restore its database and encrypted setup checkpoint before maintenance.");
|
|
5158
5361
|
dependencies.onProgress?.("Verifying current Access policies and protected origins");
|
|
5159
5362
|
await (dependencies.verifyAccessProtection ?? verifyInstalledPlatformAccessProtection)({
|
|
5160
5363
|
client: dependencies.client,
|
|
5161
|
-
manifest:
|
|
5162
|
-
plan:
|
|
5163
|
-
dashboardUrl: platformDashboardUrl(
|
|
5364
|
+
manifest: state.manifest,
|
|
5365
|
+
plan: state.plan,
|
|
5366
|
+
dashboardUrl: platformDashboardUrl(state.plan.deployment),
|
|
5164
5367
|
protection,
|
|
5165
|
-
checkpoint: secretCheckpoint(
|
|
5368
|
+
checkpoint: secretCheckpoint(state.secrets, workerSecretUpdateIntent)
|
|
5166
5369
|
});
|
|
5167
5370
|
}
|
|
5168
|
-
if (
|
|
5371
|
+
if (state.plan.deployment.authentication.accessProtection || appAccessSetupRequired && state.secrets.appAccessSetupJson && JSON.parse(state.secrets.appAccessSetupJson).managed === true) {
|
|
5169
5372
|
dependencies.onProgress?.("Preparing Access for authenticated platform health checks");
|
|
5170
5373
|
try {
|
|
5171
5374
|
await (dependencies.prepareHealthAccess ?? ensurePlatformAccessHealthChecks)({
|
|
5172
5375
|
client: dependencies.client,
|
|
5173
|
-
manifest:
|
|
5174
|
-
plan:
|
|
5175
|
-
checkpoint: secretCheckpoint(
|
|
5376
|
+
manifest: state.manifest,
|
|
5377
|
+
plan: state.plan,
|
|
5378
|
+
checkpoint: secretCheckpoint(state.secrets, workerSecretUpdateIntent),
|
|
5176
5379
|
save: (accessHealthProvisioningJson) => {
|
|
5177
|
-
|
|
5178
|
-
...
|
|
5380
|
+
state.secrets = {
|
|
5381
|
+
...state.secrets,
|
|
5179
5382
|
accessHealthProvisioningJson
|
|
5180
5383
|
};
|
|
5181
|
-
|
|
5182
|
-
writePendingInstallationSecrets(
|
|
5183
|
-
...secretCheckpoint(
|
|
5384
|
+
state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
|
|
5385
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
5386
|
+
...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
|
|
5184
5387
|
databaseClaimState: "claimed"
|
|
5185
5388
|
});
|
|
5186
|
-
writeInstallationManifest(
|
|
5389
|
+
writeInstallationManifest(state.manifest);
|
|
5187
5390
|
}
|
|
5188
5391
|
});
|
|
5189
5392
|
} catch (error) {
|
|
5190
|
-
if (appAccessSetupRequired &&
|
|
5191
|
-
|
|
5192
|
-
...
|
|
5393
|
+
if (appAccessSetupRequired && state.secrets.appAccessSetupJson && JSON.parse(state.secrets.appAccessSetupJson).managed && error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) {
|
|
5394
|
+
state.secrets = {
|
|
5395
|
+
...state.secrets,
|
|
5193
5396
|
authenticationPreparation: {
|
|
5194
|
-
...
|
|
5397
|
+
...state.secrets.authenticationPreparation,
|
|
5195
5398
|
appAccessTokenRejected: "true"
|
|
5196
5399
|
}
|
|
5197
5400
|
};
|
|
5198
|
-
writePendingInstallationSecrets(
|
|
5401
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, workerSecretUpdateIntent));
|
|
5199
5402
|
throw new PlatformAppAccessCredentialError("platform: App access token rejected while preparing health checks. Resume installation to provide a replacement.");
|
|
5200
5403
|
}
|
|
5201
5404
|
throw error;
|
|
5202
5405
|
} finally {
|
|
5203
|
-
await releaseDatabaseLock.checkpoint?.(
|
|
5406
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
5204
5407
|
}
|
|
5205
5408
|
}
|
|
5206
5409
|
}
|
|
@@ -5218,7 +5421,7 @@ function verifyRuntimeArtifacts(runtimeDirectory, manifest) {
|
|
|
5218
5421
|
try {
|
|
5219
5422
|
contents = readFileSync(path);
|
|
5220
5423
|
} catch (error) {
|
|
5221
|
-
throw new Error(`platform: Runtime artifact is missing: ${
|
|
5424
|
+
throw new Error(`platform: Runtime artifact is missing: ${getErrorMessage(error)}`);
|
|
5222
5425
|
}
|
|
5223
5426
|
if (contents.byteLength !== artifact.size || hash(contents) !== artifact.sha256) throw new Error(`platform: Runtime artifact failed integrity verification: ${path}.`);
|
|
5224
5427
|
}
|
|
@@ -5230,7 +5433,7 @@ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
|
|
|
5230
5433
|
try {
|
|
5231
5434
|
journal = readFileSync(journalPath);
|
|
5232
5435
|
} catch (error) {
|
|
5233
|
-
throw new Error(`platform: Runtime database migration journal is missing: ${
|
|
5436
|
+
throw new Error(`platform: Runtime database migration journal is missing: ${getErrorMessage(error)}`);
|
|
5234
5437
|
}
|
|
5235
5438
|
if (hash(journal) !== database.journalSha256) throw new Error(`platform: Runtime database migration journal failed integrity verification: ${journalPath}.`);
|
|
5236
5439
|
for (const migration of database.migrations) {
|
|
@@ -5239,611 +5442,12 @@ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
|
|
|
5239
5442
|
try {
|
|
5240
5443
|
contents = readFileSync(path);
|
|
5241
5444
|
} catch (error) {
|
|
5242
|
-
throw new Error(`platform: Runtime database migration is missing: ${
|
|
5445
|
+
throw new Error(`platform: Runtime database migration is missing: ${getErrorMessage(error)}`);
|
|
5243
5446
|
}
|
|
5244
5447
|
if (hash(contents) !== migration.sha256) throw new Error(`platform: Runtime database migration failed integrity verification: ${path}.`);
|
|
5245
5448
|
}
|
|
5246
5449
|
}
|
|
5247
5450
|
//#endregion
|
|
5248
|
-
//#region src/cli/platform-teardown.ts
|
|
5249
|
-
/**
|
|
5250
|
-
* `void platform uninstall --purge-resources` deletes every Cloudflare resource the
|
|
5251
|
-
* installation created, in an order Cloudflare accepts:
|
|
5252
|
-
*
|
|
5253
|
-
* traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
|
|
5254
|
-
* -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
|
|
5255
|
-
* -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
|
|
5256
|
-
*
|
|
5257
|
-
* Each resource is proven to belong to this installation right before its
|
|
5258
|
-
* DELETE, and Cloudflare refuses a resource that something outside the
|
|
5259
|
-
* installation still uses. The only forced DELETE is for a platform Worker
|
|
5260
|
-
* that Cloudflare refused while every Worker it names as a dependent is one
|
|
5261
|
-
* this installation created: the API and proxy Workers bind to each other, so
|
|
5262
|
-
* neither can go first without it. A route, custom domain, DNS
|
|
5263
|
-
* record, or Access resource that no longer matches what was recorded is kept
|
|
5264
|
-
* and listed with the reason instead of stopping the run. Every finished
|
|
5265
|
-
* deletion is checkpointed, so rerunning the command continues where it
|
|
5266
|
-
* stopped. The routing KV namespace holds that checkpoint, so uninstall
|
|
5267
|
-
* deletes it last.
|
|
5268
|
-
*
|
|
5269
|
-
* An uninstall without --purge-resources records what it leaves as retained. If it
|
|
5270
|
-
* stopped before it finished, --purge-resources considers those resources again.
|
|
5271
|
-
*/
|
|
5272
|
-
const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
|
|
5273
|
-
const WORKER_ORDER = [
|
|
5274
|
-
"email-gateway-worker",
|
|
5275
|
-
"proxy-worker",
|
|
5276
|
-
"dispatch-worker",
|
|
5277
|
-
"api-worker"
|
|
5278
|
-
];
|
|
5279
|
-
const PERMISSIONS = {
|
|
5280
|
-
access: "Account → Access: Apps and Policies → Edit",
|
|
5281
|
-
aiGateway: "Account → AI Gateway → Edit",
|
|
5282
|
-
dns: "Zone → DNS → Edit",
|
|
5283
|
-
emailRouting: "Zone → Email Routing Rules → Edit",
|
|
5284
|
-
hyperdrive: "Account → Hyperdrive → Edit",
|
|
5285
|
-
kv: "Account → Workers KV Storage → Edit",
|
|
5286
|
-
queues: "Account → Queues → Edit",
|
|
5287
|
-
r2: "Account → Workers R2 Storage → Edit",
|
|
5288
|
-
routes: "Zone → Workers Routes → Edit",
|
|
5289
|
-
serviceTokens: "Account → Access: Service Tokens → Edit",
|
|
5290
|
-
workers: "Account → Workers Scripts → Edit"
|
|
5291
|
-
};
|
|
5292
|
-
function isPlatformTailWorker(manifest, resource) {
|
|
5293
|
-
return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
|
|
5294
|
-
}
|
|
5295
|
-
function purgeResourcesResumeCommand(manifest) {
|
|
5296
|
-
return `void platform uninstall ${manifest.id} --purge-resources`;
|
|
5297
|
-
}
|
|
5298
|
-
function accessStepKey(access) {
|
|
5299
|
-
return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
|
|
5300
|
-
}
|
|
5301
|
-
/** Access resources that --purge-resources already deleted. */
|
|
5302
|
-
function isPurgedAccessResource(manifest, access) {
|
|
5303
|
-
return manifest.steps.some((step) => step.key === accessStepKey(access));
|
|
5304
|
-
}
|
|
5305
|
-
function hasStep(manifest, key) {
|
|
5306
|
-
return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
|
|
5307
|
-
}
|
|
5308
|
-
function keptStepKey(key) {
|
|
5309
|
-
return `kept:${key}`;
|
|
5310
|
-
}
|
|
5311
|
-
/**
|
|
5312
|
-
* Why --purge-resources kept a resource it was going to delete, for example because
|
|
5313
|
-
* a route now serves another Worker; null when it did not keep it. Access
|
|
5314
|
-
* resources use the key `access:<key>`.
|
|
5315
|
-
*/
|
|
5316
|
-
function purgeResourcesKeptReason(manifest, key) {
|
|
5317
|
-
return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
|
|
5318
|
-
}
|
|
5319
|
-
/** An uninstall that started has not finished, so what it kept can be reconsidered. */
|
|
5320
|
-
function isReopenable(manifest) {
|
|
5321
|
-
return manifest.status !== "uninstalled";
|
|
5322
|
-
}
|
|
5323
|
-
/**
|
|
5324
|
-
* A resource the installation created that --purge-resources may still delete: one
|
|
5325
|
-
* that is ready, or one that an uninstall which stopped before it finished
|
|
5326
|
-
* recorded as retained. Resources --purge-resources itself kept, and earlier
|
|
5327
|
-
* versions of a resource kept when it was replaced, are never reconsidered.
|
|
5328
|
-
*/
|
|
5329
|
-
function isPurgeResourcesCandidate(manifest, resource) {
|
|
5330
|
-
return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
|
|
5331
|
-
}
|
|
5332
|
-
/** The zone that routes and DNS records live in, if it can still be used. */
|
|
5333
|
-
function usableZone(manifest) {
|
|
5334
|
-
return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
|
|
5335
|
-
}
|
|
5336
|
-
/**
|
|
5337
|
-
* A recorded resource whose live configuration no longer matches the
|
|
5338
|
-
* installation's record. --purge-resources keeps such a DNS record and continues.
|
|
5339
|
-
*/
|
|
5340
|
-
var PlatformResourceChangedError = class extends Error {
|
|
5341
|
-
resourceKey;
|
|
5342
|
-
reason;
|
|
5343
|
-
constructor(resourceKey, reason, message) {
|
|
5344
|
-
super(message);
|
|
5345
|
-
this.resourceKey = resourceKey;
|
|
5346
|
-
this.reason = reason;
|
|
5347
|
-
this.name = "PlatformResourceChangedError";
|
|
5348
|
-
}
|
|
5349
|
-
};
|
|
5350
|
-
/** The zone whose Email Routing rules may send mail to the gateway Worker. */
|
|
5351
|
-
function emailRoutingZone(manifest) {
|
|
5352
|
-
return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
|
|
5353
|
-
}
|
|
5354
|
-
/** Everything --purge-resources will delete, in deletion order. */
|
|
5355
|
-
function purgeResourcesTargets(manifest) {
|
|
5356
|
-
const resources = manifest.resources;
|
|
5357
|
-
const routing = resources.find((resource) => resource.key === "routing-kv");
|
|
5358
|
-
const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
|
|
5359
|
-
if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
|
|
5360
|
-
const zone = usableZone(manifest);
|
|
5361
|
-
const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
|
|
5362
|
-
const targets = [];
|
|
5363
|
-
for (const resource of resources) {
|
|
5364
|
-
if (resource.kind !== "worker-route" && resource.kind !== "worker-custom-domain" || resource.state !== "ready" && resource.state !== "retained" || resource.kind === "worker-route" && !zone || purgeResourcesKeptReason(manifest, resource.key) !== null) continue;
|
|
5365
|
-
const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
|
|
5366
|
-
const worker = workers.find((entry) => entry.name === name);
|
|
5367
|
-
if (worker) targets.push({
|
|
5368
|
-
phase: "traffic",
|
|
5369
|
-
resource,
|
|
5370
|
-
worker
|
|
5371
|
-
});
|
|
5372
|
-
}
|
|
5373
|
-
for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
|
|
5374
|
-
phase: "dns",
|
|
5375
|
-
resource
|
|
5376
|
-
});
|
|
5377
|
-
const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
|
|
5378
|
-
for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
|
|
5379
|
-
phase: "access",
|
|
5380
|
-
access: entry
|
|
5381
|
-
});
|
|
5382
|
-
const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
|
|
5383
|
-
if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
|
|
5384
|
-
phase: "email",
|
|
5385
|
-
resource: gateway
|
|
5386
|
-
});
|
|
5387
|
-
const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
|
|
5388
|
-
for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
|
|
5389
|
-
phase: "queue-consumers",
|
|
5390
|
-
resource
|
|
5391
|
-
});
|
|
5392
|
-
const rank = (resource) => {
|
|
5393
|
-
const index = WORKER_ORDER.indexOf(resource.key);
|
|
5394
|
-
return index < 0 ? WORKER_ORDER.length : index;
|
|
5395
|
-
};
|
|
5396
|
-
for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
|
|
5397
|
-
phase: "workers",
|
|
5398
|
-
resource
|
|
5399
|
-
});
|
|
5400
|
-
for (const resource of queues) targets.push({
|
|
5401
|
-
phase: "queues",
|
|
5402
|
-
resource
|
|
5403
|
-
});
|
|
5404
|
-
for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
|
|
5405
|
-
phase: "dispatch",
|
|
5406
|
-
resource
|
|
5407
|
-
});
|
|
5408
|
-
for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
|
|
5409
|
-
phase: "tail",
|
|
5410
|
-
resource
|
|
5411
|
-
});
|
|
5412
|
-
for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
|
|
5413
|
-
phase: "data",
|
|
5414
|
-
resource
|
|
5415
|
-
});
|
|
5416
|
-
for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
|
|
5417
|
-
phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
|
|
5418
|
-
resource
|
|
5419
|
-
});
|
|
5420
|
-
if (candidate(routing)) targets.push({
|
|
5421
|
-
phase: "routing",
|
|
5422
|
-
resource: routing
|
|
5423
|
-
});
|
|
5424
|
-
return targets;
|
|
5425
|
-
}
|
|
5426
|
-
/** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
|
|
5427
|
-
function purgeResourcesHandlesEmailRouting(manifest) {
|
|
5428
|
-
return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
|
|
5429
|
-
}
|
|
5430
|
-
/** Manifest resources that --purge-resources leaves in the account. */
|
|
5431
|
-
function purgeResourcesKeptResources(manifest) {
|
|
5432
|
-
const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
|
|
5433
|
-
return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
|
|
5434
|
-
}
|
|
5435
|
-
function resourceLabel(resource) {
|
|
5436
|
-
return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
|
|
5437
|
-
}
|
|
5438
|
-
/** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
|
|
5439
|
-
function describePurgeResourcesPlan(manifest) {
|
|
5440
|
-
const targets = purgeResourcesTargets(manifest);
|
|
5441
|
-
const lines = [];
|
|
5442
|
-
let step = 0;
|
|
5443
|
-
let previous = null;
|
|
5444
|
-
const consumerQueues = [];
|
|
5445
|
-
for (const target of targets) {
|
|
5446
|
-
if (target.phase === "email") {
|
|
5447
|
-
step++;
|
|
5448
|
-
previous = target.phase;
|
|
5449
|
-
lines.push(` ${step}. Email Routing rules in zone ${emailRoutingZone(manifest)} that send mail to ${target.resource.name}, and its catch-all rule if it does (reset to drop mail, disabled)`);
|
|
5450
|
-
continue;
|
|
5451
|
-
}
|
|
5452
|
-
if (target.phase !== previous) {
|
|
5453
|
-
step++;
|
|
5454
|
-
previous = target.phase;
|
|
5455
|
-
}
|
|
5456
|
-
const prefix = ` ${step}. `;
|
|
5457
|
-
if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
|
|
5458
|
-
else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
|
|
5459
|
-
else if (target.phase === "queue-consumers") {
|
|
5460
|
-
consumerQueues.push(target.resource.name);
|
|
5461
|
-
if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
|
|
5462
|
-
lines.push(` ${target.resource.name}`);
|
|
5463
|
-
} else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
|
|
5464
|
-
else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
|
|
5465
|
-
else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
|
|
5466
|
-
else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
|
|
5467
|
-
else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
|
|
5468
|
-
else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
|
|
5469
|
-
else lines.push(`${prefix}${resourceLabel(target.resource)}`);
|
|
5470
|
-
}
|
|
5471
|
-
return lines.length ? ["Delete every resource this installation created (--purge-resources), in this order:", ...lines] : ["--purge-resources finds no resource it can prove this installation created."];
|
|
5472
|
-
}
|
|
5473
|
-
/** Lines naming what --purge-resources never removes. */
|
|
5474
|
-
function purgeResourcesAlwaysKept() {
|
|
5475
|
-
return [
|
|
5476
|
-
" The external PostgreSQL database and its data.",
|
|
5477
|
-
" Analytics Engine datasets; Cloudflare has no API to delete them.",
|
|
5478
|
-
" Access policies and identity providers you chose.",
|
|
5479
|
-
" API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
|
|
5480
|
-
];
|
|
5481
|
-
}
|
|
5482
|
-
/** Token permissions --purge-resources needs for this manifest. */
|
|
5483
|
-
function purgeResourcesPermissions(manifest) {
|
|
5484
|
-
const targets = purgeResourcesTargets(manifest);
|
|
5485
|
-
const needed = /* @__PURE__ */ new Set();
|
|
5486
|
-
for (const target of targets) {
|
|
5487
|
-
if (target.phase === "access") {
|
|
5488
|
-
needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
|
|
5489
|
-
continue;
|
|
5490
|
-
}
|
|
5491
|
-
if (target.phase === "email") {
|
|
5492
|
-
needed.add(PERMISSIONS.emailRouting);
|
|
5493
|
-
continue;
|
|
5494
|
-
}
|
|
5495
|
-
const { kind } = target.resource;
|
|
5496
|
-
if (kind === "worker-route") needed.add(PERMISSIONS.routes);
|
|
5497
|
-
else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
|
|
5498
|
-
else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
|
|
5499
|
-
else if (kind === "queue") needed.add(PERMISSIONS.queues);
|
|
5500
|
-
else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
|
|
5501
|
-
else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
|
|
5502
|
-
else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
|
|
5503
|
-
else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
|
|
5504
|
-
}
|
|
5505
|
-
return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
|
|
5506
|
-
}
|
|
5507
|
-
function deniedStatus(error) {
|
|
5508
|
-
return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
|
|
5509
|
-
}
|
|
5510
|
-
/**
|
|
5511
|
-
* Check resource reads before the first change. Browser login cannot change
|
|
5512
|
-
* DNS or Access, so those need an API token. Read probes cannot prove Edit;
|
|
5513
|
-
* the plan lists the required permissions.
|
|
5514
|
-
*/
|
|
5515
|
-
async function checkPurgeResourcesCredentials(client, manifest, credential) {
|
|
5516
|
-
const targets = purgeResourcesTargets(manifest);
|
|
5517
|
-
const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
|
|
5518
|
-
const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
|
|
5519
|
-
const has = (kind) => resources.some((resource) => resource.kind === kind);
|
|
5520
|
-
const zone = manifest.resources.find((resource) => resource.key === "zone");
|
|
5521
|
-
const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
|
|
5522
|
-
const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
|
|
5523
|
-
const scope = `${accounts.length === 1 ? "account" : "accounts"} ${accounts.join(" and ")}${has("dns-record") || has("worker-route") ? ` and zone ${zone?.name ?? ""}` : ""}${emailZone ? ` and Email Routing zone ${emailZone}` : ""}`;
|
|
5524
|
-
if (credential.source === "oauth") {
|
|
5525
|
-
const browserMissing = [
|
|
5526
|
-
...has("dns-record") ? [PERMISSIONS.dns] : [],
|
|
5527
|
-
...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
|
|
5528
|
-
...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
|
|
5529
|
-
...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
|
|
5530
|
-
];
|
|
5531
|
-
if (browserMissing.length) throw new Error(`platform: Cloudflare browser login cannot delete every resource --purge-resources plans. Set CLOUDFLARE_API_TOKEN to a token with ${purgeResourcesPermissions(manifest).join(", ")}, scoped to ${scope}, then rerun. Browser login is missing: ${browserMissing.join(", ")}. No Cloudflare resource was changed.`);
|
|
5532
|
-
}
|
|
5533
|
-
const probes = [];
|
|
5534
|
-
if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
|
|
5535
|
-
if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
|
|
5536
|
-
if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
|
|
5537
|
-
for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
|
|
5538
|
-
else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
|
|
5539
|
-
for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
|
|
5540
|
-
if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
|
|
5541
|
-
if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
|
|
5542
|
-
if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
|
|
5543
|
-
if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
|
|
5544
|
-
if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
|
|
5545
|
-
if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
|
|
5546
|
-
const missing = /* @__PURE__ */ new Set();
|
|
5547
|
-
for (const [permission, probe] of probes) try {
|
|
5548
|
-
await probe();
|
|
5549
|
-
} catch (error) {
|
|
5550
|
-
if (!deniedStatus(error)) throw error;
|
|
5551
|
-
missing.add(permission);
|
|
5552
|
-
}
|
|
5553
|
-
if (missing.size) throw new Error(`platform: The Cloudflare credential cannot read every resource --purge-resources would delete. Add ${[...missing].join(", ")} for ${scope}, then rerun. No Cloudflare resource was changed.`);
|
|
5554
|
-
}
|
|
5555
|
-
const LISTED_PROJECTS = 20;
|
|
5556
|
-
/** Slugs of every project row, including projects whose deletion stopped partway. */
|
|
5557
|
-
async function readPlatformProjectSlugsConnection(client) {
|
|
5558
|
-
if ((await client.query(`SELECT to_regclass('public.projects') IS NOT NULL AS "present"`)).rows[0]?.present !== true) return [];
|
|
5559
|
-
return (await client.query("SELECT \"slug\" FROM \"projects\" ORDER BY \"slug\"")).rows.map((row) => row.slug);
|
|
5560
|
-
}
|
|
5561
|
-
/**
|
|
5562
|
-
* Projects own Workers, storage, and domains that only the running platform
|
|
5563
|
-
* can delete. --purge-resources deletes the platform, so it refuses before any
|
|
5564
|
-
* change while a project remains.
|
|
5565
|
-
*/
|
|
5566
|
-
async function assertNoPlatformProjects(read) {
|
|
5567
|
-
let slugs;
|
|
5568
|
-
try {
|
|
5569
|
-
if (!read) throw new Error("no platform database connection is available");
|
|
5570
|
-
slugs = await read();
|
|
5571
|
-
} catch (error) {
|
|
5572
|
-
const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
|
|
5573
|
-
throw new Error(`platform: Refusing --purge-resources because the project list could not be read from the platform database: ${reason}\nMake the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry.`);
|
|
5574
|
-
}
|
|
5575
|
-
if (slugs.length) throw new Error([
|
|
5576
|
-
`platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
|
|
5577
|
-
"--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
|
|
5578
|
-
"Delete each project first with `void project delete <name>`, then retry:",
|
|
5579
|
-
...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
|
|
5580
|
-
...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
|
|
5581
|
-
].join("\n"));
|
|
5582
|
-
}
|
|
5583
|
-
const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
|
|
5584
|
-
/**
|
|
5585
|
-
* AI Gateway ids are chosen by the caller and can be reused after a delete,
|
|
5586
|
-
* and gateways carry no ownership marker. Only a creation time next to the
|
|
5587
|
-
* installer's provision intent shows that this is the gateway it created.
|
|
5588
|
-
*/
|
|
5589
|
-
function createdDuringInstallation(manifest, resource, createdAt) {
|
|
5590
|
-
const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
|
|
5591
|
-
if (Number.isNaN(created)) return false;
|
|
5592
|
-
return manifest.steps.some((step) => (step.key === `provision-intent:${resource.key}` || step.key.startsWith(`provision-intent:${resource.key}:`)) && Math.abs(created - Date.parse(step.completedAt)) <= AI_GATEWAY_CREATION_WINDOW_MS);
|
|
5593
|
-
}
|
|
5594
|
-
function ownershipRefusal(manifest, description, reason) {
|
|
5595
|
-
return /* @__PURE__ */ new Error(`platform: Refusing to delete ${description} because ${reason}. It may no longer belong to this installation. Delete or fix it in the Cloudflare dashboard if it is yours, then rerun: ${purgeResourcesResumeCommand(manifest)}`);
|
|
5596
|
-
}
|
|
5597
|
-
function cloudflareRefusal(manifest, description, error, hint) {
|
|
5598
|
-
if (!(error instanceof PlatformCloudflareApiError)) return error;
|
|
5599
|
-
return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
|
|
5600
|
-
}
|
|
5601
|
-
/**
|
|
5602
|
-
* Delete every target in order. Returns the manifest with each deleted
|
|
5603
|
-
* resource recorded; routing KV is left for the caller to delete last.
|
|
5604
|
-
*/
|
|
5605
|
-
async function runPurgeResourcesTeardown(context, initial) {
|
|
5606
|
-
const { client } = context;
|
|
5607
|
-
let next = initial;
|
|
5608
|
-
const accountId = next.account.id;
|
|
5609
|
-
const save = async (manifest) => {
|
|
5610
|
-
next = manifest;
|
|
5611
|
-
await context.checkpoint(next);
|
|
5612
|
-
};
|
|
5613
|
-
const markDeleted = async (key) => {
|
|
5614
|
-
const resource = next.resources.find((entry) => entry.key === key);
|
|
5615
|
-
await save(recordInstallationResource(next, {
|
|
5616
|
-
...resource,
|
|
5617
|
-
state: "deleted"
|
|
5618
|
-
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
5619
|
-
};
|
|
5620
|
-
const recordStep = async (key, resultHash = "completed") => {
|
|
5621
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5622
|
-
await save(recordInstallationStep(next, {
|
|
5623
|
-
completedAt: now,
|
|
5624
|
-
key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
|
|
5625
|
-
resultHash
|
|
5626
|
-
}, now));
|
|
5627
|
-
};
|
|
5628
|
-
/**
|
|
5629
|
-
* Live proof for these keys. A resource an earlier uninstall recorded as
|
|
5630
|
-
* retained is proven like a ready one; only its recorded state differs.
|
|
5631
|
-
*/
|
|
5632
|
-
const verify = (keys) => context.verify({
|
|
5633
|
-
...next,
|
|
5634
|
-
resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
|
|
5635
|
-
...resource,
|
|
5636
|
-
state: "ready"
|
|
5637
|
-
} : resource)
|
|
5638
|
-
}, keys);
|
|
5639
|
-
/**
|
|
5640
|
-
* Prove the resource; false when it is already gone (and recorded).
|
|
5641
|
-
*
|
|
5642
|
-
* Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
|
|
5643
|
-
* addressed by name, and a route or custom domain can be changed in place.
|
|
5644
|
-
* Something replaced between this proof and the DELETE can still be removed.
|
|
5645
|
-
* That one-request window is accepted because --purge-resources is opt-in and its
|
|
5646
|
-
* plan lists every resource first. A forced Worker DELETE has the same
|
|
5647
|
-
* window between reading its dependents and the DELETE.
|
|
5648
|
-
*/
|
|
5649
|
-
const prove = async (resource) => {
|
|
5650
|
-
if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
|
|
5651
|
-
await markDeleted(resource.key);
|
|
5652
|
-
return false;
|
|
5653
|
-
}
|
|
5654
|
-
return true;
|
|
5655
|
-
};
|
|
5656
|
-
/** Keep a resource that no longer matches the record, and save why. */
|
|
5657
|
-
const keep = async (key, reason) => {
|
|
5658
|
-
const resource = next.resources.find((entry) => entry.key === key);
|
|
5659
|
-
if (resource?.state === "ready") await save(recordInstallationResource(next, {
|
|
5660
|
-
...resource,
|
|
5661
|
-
state: "retained"
|
|
5662
|
-
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
5663
|
-
await recordStep(keptStepKey(key), reason);
|
|
5664
|
-
};
|
|
5665
|
-
const remove = async (resource, hint, zoneId) => {
|
|
5666
|
-
if (!await prove(resource)) return;
|
|
5667
|
-
try {
|
|
5668
|
-
await client.deleteResource(accountId, resource, zoneId);
|
|
5669
|
-
} catch (error) {
|
|
5670
|
-
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
|
|
5671
|
-
}
|
|
5672
|
-
await markDeleted(resource.key);
|
|
5673
|
-
};
|
|
5674
|
-
const zone = usableZone(next);
|
|
5675
|
-
const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
|
|
5676
|
-
/** The recorded Worker under this name that this run still deletes, if any. */
|
|
5677
|
-
const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
|
|
5678
|
-
const workerHint = "A Worker this installation did not create may still use it, for example by sending it logs as a tail Worker or through a service binding. Remove that link in the Cloudflare dashboard.";
|
|
5679
|
-
/**
|
|
5680
|
-
* Delete a platform Worker. When Cloudflare refuses because other Workers
|
|
5681
|
-
* still bind to it, force the DELETE only if each of them is a Worker this
|
|
5682
|
-
* run deletes too and still carries this installation's ownership marker.
|
|
5683
|
-
*/
|
|
5684
|
-
const removeWorker = async (resource) => {
|
|
5685
|
-
if (!await prove(resource)) return;
|
|
5686
|
-
try {
|
|
5687
|
-
await client.deleteResource(accountId, resource);
|
|
5688
|
-
} catch (error) {
|
|
5689
|
-
if (!(error instanceof PlatformCloudflareApiError)) throw error;
|
|
5690
|
-
const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
|
|
5691
|
-
const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
|
|
5692
|
-
const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
|
|
5693
|
-
const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
|
|
5694
|
-
if (!dependents || dependents.pagesFunction || dependents.workers.length === 0 || outside.length > 0 || !missing || keys.some((key) => missing.has(key))) throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, outside.length ? `Workers outside this installation still use it: ${outside.join(", ")}. Remove that link in the Cloudflare dashboard.` : workerHint);
|
|
5695
|
-
try {
|
|
5696
|
-
await client.forceDeleteWorker(accountId, resource.name);
|
|
5697
|
-
} catch (forced) {
|
|
5698
|
-
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
|
|
5699
|
-
}
|
|
5700
|
-
}
|
|
5701
|
-
await markDeleted(resource.key);
|
|
5702
|
-
};
|
|
5703
|
-
for (const target of purgeResourcesTargets(initial)) {
|
|
5704
|
-
if (target.phase === "access") {
|
|
5705
|
-
const { access } = target;
|
|
5706
|
-
const live = await client.getAccessResource(access.accountId, access.kind, access.id);
|
|
5707
|
-
if (live) {
|
|
5708
|
-
if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
|
|
5709
|
-
await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
|
|
5710
|
-
continue;
|
|
5711
|
-
}
|
|
5712
|
-
try {
|
|
5713
|
-
await client.deleteAccessResource(access.accountId, access.kind, access.id);
|
|
5714
|
-
} catch (error) {
|
|
5715
|
-
throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
|
|
5716
|
-
}
|
|
5717
|
-
}
|
|
5718
|
-
await recordStep(`access:${access.key}`);
|
|
5719
|
-
continue;
|
|
5720
|
-
}
|
|
5721
|
-
const current = next.resources.find((resource) => resource.key === target.resource.key);
|
|
5722
|
-
if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
|
|
5723
|
-
switch (target.phase) {
|
|
5724
|
-
case "traffic": {
|
|
5725
|
-
const worker = next.resources.find((resource) => resource.key === target.worker.key);
|
|
5726
|
-
if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
|
|
5727
|
-
if (current.kind === "worker-route") {
|
|
5728
|
-
const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
|
|
5729
|
-
if (live && (live.pattern !== current.name || live.script !== worker.name)) {
|
|
5730
|
-
await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
|
|
5731
|
-
break;
|
|
5732
|
-
}
|
|
5733
|
-
if (live) await client.deleteWorkerRoute(zone.id, current.id);
|
|
5734
|
-
} else {
|
|
5735
|
-
const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
|
|
5736
|
-
if (live && (live.hostname !== current.name || live.service !== worker.name)) {
|
|
5737
|
-
await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
|
|
5738
|
-
break;
|
|
5739
|
-
}
|
|
5740
|
-
if (live) await client.deleteWorkerDomain(accountId, current.id);
|
|
5741
|
-
}
|
|
5742
|
-
await markDeleted(current.key);
|
|
5743
|
-
break;
|
|
5744
|
-
}
|
|
5745
|
-
case "dns":
|
|
5746
|
-
try {
|
|
5747
|
-
await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
|
|
5748
|
-
} catch (error) {
|
|
5749
|
-
if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
|
|
5750
|
-
await keep(current.key, error.reason);
|
|
5751
|
-
}
|
|
5752
|
-
break;
|
|
5753
|
-
case "email": {
|
|
5754
|
-
if (!await prove(current)) break;
|
|
5755
|
-
const zoneId = emailRoutingZone(next);
|
|
5756
|
-
const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
|
|
5757
|
-
try {
|
|
5758
|
-
for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
|
|
5759
|
-
if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
|
|
5760
|
-
} catch (error) {
|
|
5761
|
-
throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
|
|
5762
|
-
}
|
|
5763
|
-
await recordStep("email-routing");
|
|
5764
|
-
break;
|
|
5765
|
-
}
|
|
5766
|
-
case "queue-consumers":
|
|
5767
|
-
if (!await prove(current)) break;
|
|
5768
|
-
for (const consumer of await client.listQueueConsumers(accountId, current.id)) if (consumer.script && createdWorkerNames.has(consumer.script)) await client.deleteQueueConsumer(accountId, current.id, consumer.id);
|
|
5769
|
-
await recordStep(`consumers:${current.key}`);
|
|
5770
|
-
break;
|
|
5771
|
-
case "workers":
|
|
5772
|
-
await removeWorker(current);
|
|
5773
|
-
break;
|
|
5774
|
-
case "tail":
|
|
5775
|
-
await remove(current, workerHint);
|
|
5776
|
-
break;
|
|
5777
|
-
case "queues":
|
|
5778
|
-
await remove(current, "A Worker this installation did not create may still use the Queue as a producer or consumer. Remove it from that Worker in the Cloudflare dashboard.");
|
|
5779
|
-
break;
|
|
5780
|
-
case "dispatch": {
|
|
5781
|
-
if (!await prove(current)) break;
|
|
5782
|
-
const projects = await context.readProjects();
|
|
5783
|
-
if (projects.length) throw new Error(`platform: Stopped before deleting the application Workers in dispatch namespace ${current.name} because ${projects.length === 1 ? "a project was" : "projects were"} created while this uninstall ran: ${projects.join(", ")}. Nothing in the namespace was deleted. --purge-resources refuses while the platform database lists a project. Remove each one from the "projects" table once you have dealt with its Cloudflare resources, then rerun: ${purgeResourcesResumeCommand(next)}`);
|
|
5784
|
-
for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
|
|
5785
|
-
await client.deleteDispatchNamespaceScript(accountId, current.name, script);
|
|
5786
|
-
} catch (error) {
|
|
5787
|
-
throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
|
|
5788
|
-
}
|
|
5789
|
-
try {
|
|
5790
|
-
await client.deleteResource(accountId, current);
|
|
5791
|
-
} catch (error) {
|
|
5792
|
-
throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
|
|
5793
|
-
}
|
|
5794
|
-
await markDeleted(current.key);
|
|
5795
|
-
break;
|
|
5796
|
-
}
|
|
5797
|
-
case "ai-gateway": {
|
|
5798
|
-
const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
|
|
5799
|
-
if (live && !createdDuringInstallation(next, current, live.createdAt)) {
|
|
5800
|
-
await keep(current.key, "Cloudflare does not show it was created during installation");
|
|
5801
|
-
break;
|
|
5802
|
-
}
|
|
5803
|
-
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
5804
|
-
break;
|
|
5805
|
-
}
|
|
5806
|
-
case "data":
|
|
5807
|
-
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
5808
|
-
break;
|
|
5809
|
-
case "r2": await purgeBucket(current);
|
|
5810
|
-
}
|
|
5811
|
-
}
|
|
5812
|
-
return next;
|
|
5813
|
-
async function purgeBucket(bucket) {
|
|
5814
|
-
const markerStep = `r2-marker:${bucket.key}`;
|
|
5815
|
-
if (!hasStep(next, markerStep)) {
|
|
5816
|
-
if (!await prove(bucket)) return;
|
|
5817
|
-
await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
|
|
5818
|
-
if ((await verify(/* @__PURE__ */ new Set(["routing-kv", bucket.key]))).has(bucket.key)) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "it disappeared while it was being emptied");
|
|
5819
|
-
});
|
|
5820
|
-
if (!await prove(bucket)) return;
|
|
5821
|
-
const generation = await context.bucketGeneration(next, bucket);
|
|
5822
|
-
if (generation === null) {
|
|
5823
|
-
await markDeleted(bucket.key);
|
|
5824
|
-
return;
|
|
5825
|
-
}
|
|
5826
|
-
await recordStep(markerStep, generation);
|
|
5827
|
-
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
5828
|
-
} else {
|
|
5829
|
-
const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
|
|
5830
|
-
const generation = await context.bucketGeneration(next, bucket);
|
|
5831
|
-
if (generation === null) {
|
|
5832
|
-
await markDeleted(bucket.key);
|
|
5833
|
-
return;
|
|
5834
|
-
}
|
|
5835
|
-
if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
|
|
5836
|
-
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
5837
|
-
}
|
|
5838
|
-
try {
|
|
5839
|
-
await client.deleteResource(accountId, bucket);
|
|
5840
|
-
} catch (error) {
|
|
5841
|
-
throw cloudflareRefusal(next, `r2-bucket ${bucket.name}`, error, "Cloudflare deletes only empty buckets. If objects were added after it was emptied, delete them in the Cloudflare dashboard.");
|
|
5842
|
-
}
|
|
5843
|
-
await markDeleted(bucket.key);
|
|
5844
|
-
}
|
|
5845
|
-
}
|
|
5846
|
-
//#endregion
|
|
5847
5451
|
//#region src/cli/platform-zero-trust-uninstall.ts
|
|
5848
5452
|
/**
|
|
5849
5453
|
* Uninstall turns off the platform API, and only that API can remove the
|
|
@@ -5858,6 +5462,16 @@ const ZERO_TRUST_SETTING = "zeroTrust.configuration";
|
|
|
5858
5462
|
const ZERO_TRUST_LOCK = "hashtext('zero-trust:configuration')";
|
|
5859
5463
|
const UNINSTALL_SETTING = "platform.uninstall";
|
|
5860
5464
|
const LISTED_APPLICATIONS = 20;
|
|
5465
|
+
/** Runs an uninstall precheck read; any failure becomes the refusal `refuse` builds. */
|
|
5466
|
+
async function readPlatformDatabase(read, refuse) {
|
|
5467
|
+
try {
|
|
5468
|
+
if (!read) throw new Error("no platform database connection is available");
|
|
5469
|
+
return await read();
|
|
5470
|
+
} catch (error) {
|
|
5471
|
+
const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
|
|
5472
|
+
throw new Error(refuse(reason));
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5861
5475
|
function optionalText(value, label) {
|
|
5862
5476
|
if (value === null || value === void 0) return null;
|
|
5863
5477
|
if (typeof value !== "string") throw new Error(`the saved ${label} is malformed`);
|
|
@@ -5995,18 +5609,11 @@ function unfinishedDeletionLines(state) {
|
|
|
5995
5609
|
* or while its state cannot be read. Runs before uninstall changes anything.
|
|
5996
5610
|
*/
|
|
5997
5611
|
async function assertProjectZeroTrustAllowsUninstall(read) {
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
|
|
6004
|
-
throw new Error([
|
|
6005
|
-
`platform: Refusing to uninstall because the project Zero Trust state could not be read from the platform database: ${reason}`,
|
|
6006
|
-
"Uninstall turns off the platform API, which Void needs to remove the Cloudflare Access applications that project Zero Trust created.",
|
|
6007
|
-
"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."
|
|
6008
|
-
].join("\n"));
|
|
6009
|
-
}
|
|
5612
|
+
const state = await readPlatformDatabase(read, (reason) => [
|
|
5613
|
+
`platform: Refusing to uninstall because the project Zero Trust state could not be read from the platform database: ${reason}`,
|
|
5614
|
+
"Uninstall turns off the platform API, which Void needs to remove the Cloudflare Access applications that project Zero Trust created.",
|
|
5615
|
+
"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."
|
|
5616
|
+
].join("\n"));
|
|
6010
5617
|
if (state.active) {
|
|
6011
5618
|
const covered = state.destinations.length ? ` on ${state.destinations.join(" and ")}` : " on every hostname under the application domain";
|
|
6012
5619
|
throw new Error([
|
|
@@ -6031,7 +5638,7 @@ async function assertProjectZeroTrustAllowsUninstall(read) {
|
|
|
6031
5638
|
}
|
|
6032
5639
|
//#endregion
|
|
6033
5640
|
//#region src/cli/platform-installer-database.ts
|
|
6034
|
-
async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan,
|
|
5641
|
+
async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan, dependencies, releaseDatabaseLock, now, writeRemoteInstallationCheckpoint, onManifestChange) {
|
|
6035
5642
|
dependencies.onProgress?.("Checking the platform database schema");
|
|
6036
5643
|
const rollbackDatabase = dependencies.databaseRollbackSource;
|
|
6037
5644
|
const migrationStep = `database-schema:${runtime.database.schemaDigest}`;
|
|
@@ -6049,14 +5656,9 @@ async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory
|
|
|
6049
5656
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
6050
5657
|
} else if (!manifest.steps.some((step) => step.key === migrationStep)) {
|
|
6051
5658
|
dependencies.onProgress?.("Applying platform database migrations");
|
|
6052
|
-
const
|
|
6053
|
-
if (
|
|
6054
|
-
|
|
6055
|
-
} else {
|
|
6056
|
-
const prepareDatabase = dependencies.prepareDatabase ?? releaseDatabaseLock.prepareDatabase;
|
|
6057
|
-
if (!prepareDatabase) throw new Error("platform: The PostgreSQL lifecycle lock cannot verify or migrate this database.");
|
|
6058
|
-
await prepareDatabase(runtimeDirectory, runtime.database, sourcePlatformVersion, hadPriorWorkers);
|
|
6059
|
-
}
|
|
5659
|
+
const prepareDatabase = dependencies.prepareDatabase ?? releaseDatabaseLock.prepareDatabase;
|
|
5660
|
+
if (!prepareDatabase) throw new Error("platform: The PostgreSQL lifecycle lock cannot verify or migrate this database.");
|
|
5661
|
+
await prepareDatabase(runtimeDirectory, runtime.database, sourcePlatformVersion, hadPriorWorkers);
|
|
6060
5662
|
manifest = recordInstallationStep(manifest, {
|
|
6061
5663
|
completedAt: now(),
|
|
6062
5664
|
key: migrationStep,
|
|
@@ -6123,20 +5725,28 @@ function platformDatabaseClientUrl(databaseUrl) {
|
|
|
6123
5725
|
url.searchParams.set("sslmode", "verify-full");
|
|
6124
5726
|
return url.toString();
|
|
6125
5727
|
}
|
|
6126
|
-
async function
|
|
5728
|
+
async function withPlatformDatabasePool(databaseUrl, use) {
|
|
6127
5729
|
const { Pool } = await import("pg");
|
|
6128
5730
|
const pool = new Pool({
|
|
6129
5731
|
connectionString: platformDatabaseClientUrl(databaseUrl),
|
|
6130
5732
|
max: 1
|
|
6131
5733
|
});
|
|
6132
|
-
const client = await pool.connect();
|
|
6133
5734
|
try {
|
|
6134
|
-
await
|
|
5735
|
+
return await use(pool);
|
|
6135
5736
|
} finally {
|
|
6136
|
-
client.release();
|
|
6137
5737
|
await pool.end();
|
|
6138
5738
|
}
|
|
6139
5739
|
}
|
|
5740
|
+
function claimPlatformDatabase(databaseUrl, installationId, ownershipNonce) {
|
|
5741
|
+
return withPlatformDatabasePool(databaseUrl, async (pool) => {
|
|
5742
|
+
const client = await pool.connect();
|
|
5743
|
+
try {
|
|
5744
|
+
await claimPlatformDatabaseConnection(client, installationId, ownershipNonce);
|
|
5745
|
+
} finally {
|
|
5746
|
+
client.release();
|
|
5747
|
+
}
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
6140
5750
|
async function acquirePlatformDatabaseLifecycleLock(databaseUrl, installationId, ownershipNonce) {
|
|
6141
5751
|
const { Pool } = await import("pg");
|
|
6142
5752
|
const pool = new Pool({
|
|
@@ -6162,49 +5772,35 @@ async function acquirePlatformDatabaseLifecycleLock(databaseUrl, installationId,
|
|
|
6162
5772
|
}
|
|
6163
5773
|
}
|
|
6164
5774
|
async function readPlatformDatabaseLifecycleManifest(databaseUrl, installationId, ownershipNonce) {
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
5775
|
+
return withPlatformDatabasePool(databaseUrl, async (pool) => {
|
|
5776
|
+
try {
|
|
5777
|
+
return await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
|
|
5778
|
+
} catch (error) {
|
|
5779
|
+
if (error instanceof Error && error.message.startsWith("platform:")) throw error;
|
|
5780
|
+
throw new Error(`platform: Could not read the PostgreSQL lifecycle checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
5781
|
+
}
|
|
6169
5782
|
});
|
|
6170
|
-
try {
|
|
6171
|
-
return await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
|
|
6172
|
-
} catch (error) {
|
|
6173
|
-
if (error instanceof Error && error.message.startsWith("platform:")) throw error;
|
|
6174
|
-
throw new Error(`platform: Could not read the PostgreSQL lifecycle checkpoint: ${error instanceof Error ? error.message : String(error)}`);
|
|
6175
|
-
} finally {
|
|
6176
|
-
await pool.end();
|
|
6177
|
-
}
|
|
6178
5783
|
}
|
|
6179
|
-
/**
|
|
6180
|
-
async function
|
|
5784
|
+
/** Plans hold no lifecycle lock, so verify the installation claim before reading. */
|
|
5785
|
+
async function readClaimedPlatformDatabase(databaseUrl, installationId, ownershipNonce, read) {
|
|
6181
5786
|
if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
|
|
6182
|
-
|
|
6183
|
-
const pool = new Pool({
|
|
6184
|
-
connectionString: platformDatabaseClientUrl(databaseUrl),
|
|
6185
|
-
max: 1
|
|
6186
|
-
});
|
|
6187
|
-
try {
|
|
5787
|
+
return withPlatformDatabasePool(databaseUrl, async (pool) => {
|
|
6188
5788
|
await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
|
|
6189
|
-
return
|
|
6190
|
-
} finally {
|
|
6191
|
-
await pool.end();
|
|
6192
|
-
}
|
|
6193
|
-
}
|
|
6194
|
-
/** Read project slugs for a --purge-resources plan, which holds no lifecycle lock. */
|
|
6195
|
-
async function readPlatformProjectSlugs(databaseUrl, installationId, ownershipNonce) {
|
|
6196
|
-
if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
|
|
6197
|
-
const { Pool } = await import("pg");
|
|
6198
|
-
const pool = new Pool({
|
|
6199
|
-
connectionString: platformDatabaseClientUrl(databaseUrl),
|
|
6200
|
-
max: 1
|
|
5789
|
+
return read(pool);
|
|
6201
5790
|
});
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
5791
|
+
}
|
|
5792
|
+
/** Read project Zero Trust state for a plan. */
|
|
5793
|
+
function readPlatformProjectZeroTrustState(databaseUrl, installationId, ownershipNonce) {
|
|
5794
|
+
return readClaimedPlatformDatabase(databaseUrl, installationId, ownershipNonce, readPlatformProjectZeroTrustStateConnection);
|
|
5795
|
+
}
|
|
5796
|
+
/** Read project slugs for a --purge-resources plan. */
|
|
5797
|
+
function readPlatformProjectSlugs(databaseUrl, installationId, ownershipNonce) {
|
|
5798
|
+
return readClaimedPlatformDatabase(databaseUrl, installationId, ownershipNonce, readPlatformProjectSlugsConnection);
|
|
5799
|
+
}
|
|
5800
|
+
/** Slugs of every project row, including projects whose deletion stopped partway. */
|
|
5801
|
+
async function readPlatformProjectSlugsConnection(client) {
|
|
5802
|
+
if ((await client.query(`SELECT to_regclass('public.projects') IS NOT NULL AS "present"`)).rows[0]?.present !== true) return [];
|
|
5803
|
+
return (await client.query("SELECT \"slug\" FROM \"projects\" ORDER BY \"slug\"")).rows.map((row) => row.slug);
|
|
6208
5804
|
}
|
|
6209
5805
|
async function inspectPlatformDatabaseStateConnection(client, database) {
|
|
6210
5806
|
const relation = await client.query(`SELECT to_regclass('"drizzle"."__drizzle_migrations"')::text AS "relation"`);
|
|
@@ -6217,14 +5813,12 @@ async function inspectPlatformDatabaseStateConnection(client, database) {
|
|
|
6217
5813
|
ORDER BY "created_at", "id"`);
|
|
6218
5814
|
if (result.rows.length > database.migrations.length) throw new Error("platform: PostgreSQL contains migrations newer than this Void Platform package.");
|
|
6219
5815
|
let previousTimestamp = -1;
|
|
6220
|
-
const seenTimestamps = /* @__PURE__ */ new Set();
|
|
6221
5816
|
for (const [position, row] of result.rows.entries()) {
|
|
6222
5817
|
const timestampMillis = databaseInteger(row.created_at);
|
|
6223
|
-
if (timestampMillis === null || timestampMillis <= previousTimestamp
|
|
5818
|
+
if (timestampMillis === null || timestampMillis <= previousTimestamp) throw new Error("platform: PostgreSQL migration history is duplicated or not strictly ordered.");
|
|
6224
5819
|
const expected = database.migrations[position];
|
|
6225
5820
|
if (!expected || timestampMillis !== expected.timestampMillis || row.hash !== expected.sha256) throw new Error("platform: PostgreSQL migration history is not an exact prefix of this Void Platform package. Refusing to apply edited, forked, or out-of-order migrations.");
|
|
6226
5821
|
previousTimestamp = timestampMillis;
|
|
6227
|
-
seenTimestamps.add(timestampMillis);
|
|
6228
5822
|
}
|
|
6229
5823
|
const applied = database.migrations.slice(0, result.rows.length);
|
|
6230
5824
|
return {
|
|
@@ -6359,15 +5953,21 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
|
|
|
6359
5953
|
release.verifyDatabase = (runtimeDirectory, database) => verifyPlatformDatabaseConnection(client, runtimeDirectory, database);
|
|
6360
5954
|
return release;
|
|
6361
5955
|
}
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
5956
|
+
/** Null when the sandbox-drain checkpoint table does not exist. */
|
|
5957
|
+
async function readSandboxDrainTable(client) {
|
|
5958
|
+
if (!(await client.query(`SELECT to_regclass('"sandbox_drain_checkpoints"')::text AS "relation"`)).rows[0]?.relation) return null;
|
|
5959
|
+
return { hasAdmissionColumn: (await client.query(`SELECT EXISTS(
|
|
6365
5960
|
SELECT 1
|
|
6366
5961
|
FROM information_schema.columns
|
|
6367
5962
|
WHERE table_schema = 'public'
|
|
6368
5963
|
AND table_name = 'sandbox_drain_checkpoints'
|
|
6369
5964
|
AND column_name = 'admission_protocol'
|
|
6370
|
-
) AS "present"`)).rows[0]?.present === true;
|
|
5965
|
+
) AS "present"`)).rows[0]?.present === true };
|
|
5966
|
+
}
|
|
5967
|
+
async function preparePlatformSandboxDrainConnection(client, requiredAdmissionProtocol = 0) {
|
|
5968
|
+
const table = await readSandboxDrainTable(client);
|
|
5969
|
+
if (!table) throw new Error("platform: Runtime declares sandbox-drain safety but its database checkpoint table is missing.");
|
|
5970
|
+
const { hasAdmissionColumn } = table;
|
|
6371
5971
|
if (requiredAdmissionProtocol >= 2 && !hasAdmissionColumn) throw new Error("platform: The runtime database does not include the sandbox deployment admission barrier.");
|
|
6372
5972
|
const readCheckpoint = () => client.query(`SELECT ${hasAdmissionColumn ? "\"admission_protocol\"" : "0 AS \"admission_protocol\""}, "generation", "state", "phase", "cursor", "started_at", "updated_at", "completed_at"
|
|
6373
5973
|
FROM "sandbox_drain_checkpoints"
|
|
@@ -6471,14 +6071,9 @@ async function verifyPlatformDatabaseConnection(client, runtimeDirectory, databa
|
|
|
6471
6071
|
if (state.pendingMigrations.length !== 0 || state.schemaDigest !== database.schemaDigest) throw new Error("platform: PostgreSQL is not at the exact forward-only schema recorded by the installed runtime.");
|
|
6472
6072
|
}
|
|
6473
6073
|
async function readRequiredPlatformSafetyProtocols(client) {
|
|
6474
|
-
|
|
6475
|
-
if (
|
|
6476
|
-
|
|
6477
|
-
FROM information_schema.columns
|
|
6478
|
-
WHERE table_schema = 'public'
|
|
6479
|
-
AND table_name = 'sandbox_drain_checkpoints'
|
|
6480
|
-
AND column_name = 'admission_protocol'
|
|
6481
|
-
) AS "present"`)).rows[0]?.present === true) {
|
|
6074
|
+
const table = await readSandboxDrainTable(client);
|
|
6075
|
+
if (!table) return { sandboxDrain: 0 };
|
|
6076
|
+
if (table.hasAdmissionColumn) {
|
|
6482
6077
|
const admission = await client.query(`SELECT EXISTS(
|
|
6483
6078
|
SELECT 1
|
|
6484
6079
|
FROM "sandbox_drain_checkpoints"
|
|
@@ -6498,7 +6093,7 @@ async function readRequiredPlatformSafetyProtocols(client) {
|
|
|
6498
6093
|
) AS "required"`)).rows[0]?.required === true ? 1 : 0 };
|
|
6499
6094
|
}
|
|
6500
6095
|
function assertPlatformRuntimeSafety(runtime, installed, database = { sandboxDrain: 0 }) {
|
|
6501
|
-
const requiredSandboxDrain = Math.max(installed
|
|
6096
|
+
const requiredSandboxDrain = Math.max(installed.sandboxDrain, database.sandboxDrain);
|
|
6502
6097
|
if ((runtime.safetyProtocols?.sandboxDrain ?? 0) < requiredSandboxDrain) throw new Error(`platform: Void Platform ${runtime.platformVersion} predates the required sandbox-drain safety protocol. Refusing to upload or activate a runtime that could reopen sandbox creation after drain.`);
|
|
6503
6098
|
}
|
|
6504
6099
|
async function claimPlatformDatabaseConnection(client, installationId, ownershipNonce) {
|
|
@@ -6544,48 +6139,49 @@ async function claimPlatformDatabaseConnection(client, installationId, ownership
|
|
|
6544
6139
|
//#endregion
|
|
6545
6140
|
//#region src/cli/platform-install-provisioning.ts
|
|
6546
6141
|
async function provisionInstallationServices(input) {
|
|
6547
|
-
const {
|
|
6142
|
+
const { state, now, releaseDatabaseLock, dependencies, writeRemoteInstallationCheckpoint, checkpointOperation, externalHyperdriveOriginHash, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers } = input;
|
|
6143
|
+
const { plan, secrets } = state;
|
|
6548
6144
|
const routingAction = plan.actions.find((action) => action.key === "routing-kv");
|
|
6549
6145
|
if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
|
|
6550
6146
|
if (routingAction.action === "reuse") {
|
|
6551
|
-
if (!
|
|
6552
|
-
|
|
6553
|
-
writeInstallationManifest(
|
|
6147
|
+
if (!state.manifest.resources.some((resource) => resource.key === routingAction.key) && routingAction.id) {
|
|
6148
|
+
state.manifest = recordResource(state.manifest, routingAction, routingAction.id, "created", now());
|
|
6149
|
+
writeInstallationManifest(state.manifest);
|
|
6554
6150
|
}
|
|
6555
6151
|
} else {
|
|
6556
|
-
|
|
6557
|
-
writeInstallationManifest(
|
|
6558
|
-
await releaseDatabaseLock.checkpoint?.(
|
|
6152
|
+
state.manifest = recordProvisionIntent(state.manifest, routingAction, now());
|
|
6153
|
+
writeInstallationManifest(state.manifest);
|
|
6154
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
6559
6155
|
const routing = await createResource(dependencies.client, plan, routingAction, secrets.databaseUrl, "");
|
|
6560
|
-
|
|
6561
|
-
writeInstallationManifest(
|
|
6156
|
+
state.manifest = recordResource(state.manifest, routingAction, routing.id, "created", now());
|
|
6157
|
+
writeInstallationManifest(state.manifest);
|
|
6562
6158
|
}
|
|
6563
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan,
|
|
6159
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
6564
6160
|
const zoneAction = plan.actions.find((action) => action.key === "zone");
|
|
6565
6161
|
let zone = plan.zone;
|
|
6566
6162
|
if (zoneAction) {
|
|
6567
6163
|
if (zoneAction.action === "create") {
|
|
6568
|
-
|
|
6569
|
-
writeInstallationManifest(
|
|
6570
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan,
|
|
6164
|
+
state.manifest = recordProvisionIntent(state.manifest, zoneAction, now());
|
|
6165
|
+
writeInstallationManifest(state.manifest);
|
|
6166
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
6571
6167
|
zone = await dependencies.client.createZone(plan.account.id, zoneAction.name);
|
|
6572
6168
|
}
|
|
6573
6169
|
if (!zone) throw new Error("platform: Cloudflare did not return the platform zone.");
|
|
6574
6170
|
if (zoneAction.action === "adopt") {
|
|
6575
|
-
|
|
6576
|
-
writeInstallationManifest(
|
|
6577
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan,
|
|
6171
|
+
state.manifest = recordAdoptionIntent(state.manifest, zoneAction, zone.id, now());
|
|
6172
|
+
writeInstallationManifest(state.manifest);
|
|
6173
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
6578
6174
|
}
|
|
6579
|
-
|
|
6580
|
-
writeInstallationManifest(
|
|
6581
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan,
|
|
6175
|
+
state.manifest = recordResource(state.manifest, zoneAction, zone.id, zoneAction.action === "adopt" ? "adopted" : zoneAction.action === "create" || hasProvisionIntent(state.manifest, zoneAction) ? "created" : void 0, now());
|
|
6176
|
+
writeInstallationManifest(state.manifest);
|
|
6177
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
6582
6178
|
if (zone.status !== "active") {
|
|
6583
|
-
|
|
6179
|
+
state.manifest = transitionInstallation(state.manifest, "waiting-for-dns", now());
|
|
6584
6180
|
await checkpointOperation("waiting-for-dns");
|
|
6585
|
-
writeInstallationManifest(
|
|
6586
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan,
|
|
6181
|
+
writeInstallationManifest(state.manifest);
|
|
6182
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
6587
6183
|
return {
|
|
6588
|
-
manifest:
|
|
6184
|
+
manifest: state.manifest,
|
|
6589
6185
|
waitingForDns: true,
|
|
6590
6186
|
waitingForSandboxDrain: false,
|
|
6591
6187
|
zoneNameServers: zone.nameServers
|
|
@@ -6593,47 +6189,47 @@ async function provisionInstallationServices(input) {
|
|
|
6593
6189
|
}
|
|
6594
6190
|
}
|
|
6595
6191
|
const zoneId = zone?.id ?? "";
|
|
6596
|
-
|
|
6597
|
-
|
|
6192
|
+
state.manifest = await reconcilePlatformInstallationResources(state.manifest, plan, secrets, zoneId, externalHyperdriveOriginHash, dependencies, now, writeRemoteInstallationCheckpoint, (next) => {
|
|
6193
|
+
state.manifest = next;
|
|
6598
6194
|
});
|
|
6599
6195
|
dependencies.onProgress?.("Verifying uncached authentication database reads");
|
|
6600
|
-
const authenticationDatabase =
|
|
6196
|
+
const authenticationDatabase = state.manifest.resources.find((resource) => resource.key === "hyperdrive");
|
|
6601
6197
|
if (!authenticationDatabase) throw new Error("platform: The authentication database resource is missing.");
|
|
6602
6198
|
await verifyPlatformAuthenticationDatabase(dependencies.client, plan.account.id, authenticationDatabase, secrets.databaseUrl);
|
|
6603
|
-
|
|
6604
|
-
|
|
6199
|
+
state.manifest = await preparePlatformDatabaseSchema(state.manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan, dependencies, releaseDatabaseLock, now, writeRemoteInstallationCheckpoint, (next) => {
|
|
6200
|
+
state.manifest = next;
|
|
6605
6201
|
});
|
|
6606
6202
|
dependencies.onProgress?.("Checking platform safety");
|
|
6607
|
-
const targetSandboxDrainProtocol = runtime.safetyProtocols
|
|
6203
|
+
const targetSandboxDrainProtocol = runtime.safetyProtocols.sandboxDrain;
|
|
6608
6204
|
const prepareSandboxDrain = dependencies.prepareSandboxDrain ?? releaseDatabaseLock.prepareSandboxDrain;
|
|
6609
6205
|
if (targetSandboxDrainProtocol >= 1 && !prepareSandboxDrain) throw new Error("platform: The PostgreSQL lifecycle lock cannot establish the sandbox-drain safety checkpoint.");
|
|
6610
6206
|
const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 3 ? 3 : targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
|
|
6611
6207
|
if (sandboxDrainPreparation && sandboxDrainPreparation.admissionProtocol < targetSandboxDrainProtocol) throw new Error("platform: The database did not arm the Sandbox admission protocol required by this runtime.");
|
|
6612
6208
|
const sandboxDrainState = sandboxDrainPreparation?.state ?? "complete";
|
|
6613
|
-
if (targetSandboxDrainProtocol >= 1 && (
|
|
6614
|
-
|
|
6615
|
-
...
|
|
6209
|
+
if (targetSandboxDrainProtocol >= 1 && (state.manifest.safetyProtocols?.sandboxDrain ?? 0) < targetSandboxDrainProtocol) {
|
|
6210
|
+
state.manifest = {
|
|
6211
|
+
...state.manifest,
|
|
6616
6212
|
safetyProtocols: { sandboxDrain: targetSandboxDrainProtocol },
|
|
6617
6213
|
updatedAt: now()
|
|
6618
6214
|
};
|
|
6619
|
-
writeInstallationManifest(
|
|
6620
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan,
|
|
6215
|
+
writeInstallationManifest(state.manifest);
|
|
6216
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
|
|
6621
6217
|
}
|
|
6622
|
-
|
|
6218
|
+
state.manifest = transitionInstallation(state.manifest, "deploying", now());
|
|
6623
6219
|
await checkpointOperation("deploying");
|
|
6624
|
-
writeInstallationManifest(
|
|
6625
|
-
const ids = Object.fromEntries(
|
|
6220
|
+
writeInstallationManifest(state.manifest);
|
|
6221
|
+
const ids = Object.fromEntries(state.manifest.resources.map((resource) => [resource.key, resource.id]));
|
|
6626
6222
|
const bootstrapStep = "bootstrap-access:v1";
|
|
6627
|
-
if (!
|
|
6223
|
+
if (!state.manifest.steps.some((step) => step.key === bootstrapStep)) {
|
|
6628
6224
|
const routingKv = requireId(ids, "routing-kv");
|
|
6629
6225
|
await dependencies.client.putKvValue(plan.account.id, routingKv, "signup:enabled", "true");
|
|
6630
6226
|
await dependencies.client.putKvValue(plan.account.id, routingKv, "signup:allowlist", JSON.stringify([]));
|
|
6631
|
-
|
|
6227
|
+
state.manifest = recordInstallationStep(state.manifest, {
|
|
6632
6228
|
completedAt: now(),
|
|
6633
6229
|
key: bootstrapStep,
|
|
6634
6230
|
resultHash: hash(secrets.authenticationSetupJson ?? secrets.bootstrapGitHubId)
|
|
6635
6231
|
}, now());
|
|
6636
|
-
writeInstallationManifest(
|
|
6232
|
+
writeInstallationManifest(state.manifest);
|
|
6637
6233
|
}
|
|
6638
6234
|
const entrypoints = Object.fromEntries(Object.entries(runtime.artifacts).map(([key, artifact]) => [key, join(runtimeDirectory, artifact.path)]));
|
|
6639
6235
|
return {
|
|
@@ -6647,182 +6243,7 @@ async function provisionInstallationServices(input) {
|
|
|
6647
6243
|
};
|
|
6648
6244
|
}
|
|
6649
6245
|
//#endregion
|
|
6650
|
-
//#region src/cli/platform-install-workers.ts
|
|
6651
|
-
async function uploadInstallationWorkers(input) {
|
|
6652
|
-
const { dependencies, requiresStagedCommit, workerSecretUpdateIntent, recoveringUnsafeTraffic, workerActions, workerKeys, workerDeploymentSecrets, plan, zoneId, now, writeRemoteInstallationCheckpoint, configs, previousConfigs, cloudflareApiBaseUrl, workerCutoverGeneration, preserveDisabled, managedRoutes, previousPublicEndpoints, ids, runParallelPinnedPlatformWrangler, runPinnedPlatformWrangler, workerRollbacks } = input;
|
|
6653
|
-
const parallelWorkerUploads = !dependencies.deployWorker && !requiresStagedCommit && !workerSecretUpdateIntent && !recoveringUnsafeTraffic && workerActions.length === workerKeys.length && workerActions.every((action) => action.action === "update");
|
|
6654
|
-
let workerManifestQueue = Promise.resolve();
|
|
6655
|
-
let workerManifestFailed = false;
|
|
6656
|
-
let workerManifestFailure;
|
|
6657
|
-
const checkpointWorkerManifest = (update, safetyRecovery = false) => {
|
|
6658
|
-
if (!parallelWorkerUploads) return update();
|
|
6659
|
-
const result = workerManifestQueue.then(async () => {
|
|
6660
|
-
if (workerManifestFailed && !safetyRecovery) throw workerManifestFailure;
|
|
6661
|
-
try {
|
|
6662
|
-
await update();
|
|
6663
|
-
} catch (error) {
|
|
6664
|
-
workerManifestFailed = true;
|
|
6665
|
-
workerManifestFailure = error;
|
|
6666
|
-
throw error;
|
|
6667
|
-
}
|
|
6668
|
-
});
|
|
6669
|
-
workerManifestQueue = result.then(() => void 0, () => void 0);
|
|
6670
|
-
return result;
|
|
6671
|
-
};
|
|
6672
|
-
let availableUploadSlots = 2;
|
|
6673
|
-
let uploadFailure;
|
|
6674
|
-
const uploadWaiters = [];
|
|
6675
|
-
const acquireUploadSlot = async () => {
|
|
6676
|
-
if (uploadFailure !== void 0) throw uploadFailure;
|
|
6677
|
-
if (availableUploadSlots === 0) await new Promise((resolve, reject) => uploadWaiters.push({
|
|
6678
|
-
resolve,
|
|
6679
|
-
reject
|
|
6680
|
-
}));
|
|
6681
|
-
else availableUploadSlots--;
|
|
6682
|
-
return (failure) => {
|
|
6683
|
-
if (failure !== void 0 && uploadFailure === void 0) {
|
|
6684
|
-
uploadFailure = failure;
|
|
6685
|
-
for (const waiter of uploadWaiters.splice(0)) waiter.reject(failure);
|
|
6686
|
-
}
|
|
6687
|
-
const next = uploadFailure === void 0 ? uploadWaiters.shift() : void 0;
|
|
6688
|
-
if (next) next.resolve();
|
|
6689
|
-
else availableUploadSlots++;
|
|
6690
|
-
};
|
|
6691
|
-
};
|
|
6692
|
-
const workerReady = (parallelWorkerUploads ? workerKeys : []).map(() => {
|
|
6693
|
-
let resolve;
|
|
6694
|
-
let reject;
|
|
6695
|
-
return {
|
|
6696
|
-
promise: new Promise((onResolve, onReject) => {
|
|
6697
|
-
resolve = onResolve;
|
|
6698
|
-
reject = onReject;
|
|
6699
|
-
}),
|
|
6700
|
-
resolve,
|
|
6701
|
-
reject
|
|
6702
|
-
};
|
|
6703
|
-
});
|
|
6704
|
-
const uploadBatch = Promise.all(workerReady.map((ready) => ready.promise)).then(() => ({ error: null }), (error) => ({ error }));
|
|
6705
|
-
const deployOneWorker = async (key, waitForPriorWorker, onReadyForActivation) => {
|
|
6706
|
-
dependencies.onProgress?.(`Deploying the ${key} service`);
|
|
6707
|
-
let workerStepStarted = performance.now();
|
|
6708
|
-
const markWorkerStep = (step) => {
|
|
6709
|
-
if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] worker ${key} ${step} ${(performance.now() - workerStepStarted).toFixed(0)} ms\n`);
|
|
6710
|
-
workerStepStarted = performance.now();
|
|
6711
|
-
};
|
|
6712
|
-
const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
|
|
6713
|
-
const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
|
|
6714
|
-
await checkpointWorkerManifest(async () => {
|
|
6715
|
-
const intended = recordProvisionIntent(input.manifest, action, now());
|
|
6716
|
-
if (intended !== input.manifest) {
|
|
6717
|
-
input.manifest = intended;
|
|
6718
|
-
writeInstallationManifest(input.manifest);
|
|
6719
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
6720
|
-
}
|
|
6721
|
-
});
|
|
6722
|
-
await dependencies.revalidateBeforeWorkerDeploy(input.manifest, action);
|
|
6723
|
-
markWorkerStep("revalidate");
|
|
6724
|
-
const preservedRoutes = dependencies.deployWorker ? [] : action.action === "create" ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
|
|
6725
|
-
markWorkerStep("traffic inventory");
|
|
6726
|
-
const workerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
|
|
6727
|
-
const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
|
|
6728
|
-
const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
|
|
6729
|
-
apiBaseUrl: cloudflareApiBaseUrl,
|
|
6730
|
-
operatorApiToken: dependencies.operatorApiToken,
|
|
6731
|
-
...parallelWorkerUploads ? {
|
|
6732
|
-
acquireUploadSlot,
|
|
6733
|
-
onReadyForActivation,
|
|
6734
|
-
waitForPriorWorker
|
|
6735
|
-
} : {},
|
|
6736
|
-
...dependencies.client.request ? { readCloudflare: (path) => dependencies.client.request(path) } : {},
|
|
6737
|
-
onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
|
|
6738
|
-
await checkpointWorkerManifest(async () => {
|
|
6739
|
-
input.manifest = recordWorkerCutoverStage(input.manifest, workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
|
|
6740
|
-
writeInstallationManifest(input.manifest);
|
|
6741
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
6742
|
-
});
|
|
6743
|
-
},
|
|
6744
|
-
onTrafficPublished: async () => {
|
|
6745
|
-
if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
|
|
6746
|
-
const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
|
|
6747
|
-
await checkpointWorkerManifest(async () => {
|
|
6748
|
-
const next = recordPlatformWorkerTraffic(input.manifest, action.name, {
|
|
6749
|
-
customDomains,
|
|
6750
|
-
routes
|
|
6751
|
-
}, now());
|
|
6752
|
-
if (JSON.stringify(next.resources) !== JSON.stringify(input.manifest.resources)) {
|
|
6753
|
-
input.manifest = next;
|
|
6754
|
-
writeInstallationManifest(input.manifest);
|
|
6755
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
6756
|
-
}
|
|
6757
|
-
});
|
|
6758
|
-
},
|
|
6759
|
-
onUnsafeTraffic: async () => {
|
|
6760
|
-
await checkpointWorkerManifest(async () => {
|
|
6761
|
-
input.manifest = recordWorkerUnsafeTraffic(input.manifest, workerCutoverGeneration, key, action.name, now());
|
|
6762
|
-
writeInstallationManifest(input.manifest);
|
|
6763
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
6764
|
-
}, true);
|
|
6765
|
-
},
|
|
6766
|
-
managedRoutes: preserveDisabled ? [] : managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
|
|
6767
|
-
preservedRoutes,
|
|
6768
|
-
publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
|
|
6769
|
-
publishRoutes: async (workerName, patterns) => {
|
|
6770
|
-
if (!dependencies.client.publishWorkerRoutesSafely) throw new Error("platform: Safe Cloudflare Worker route publication is unavailable.");
|
|
6771
|
-
if (zoneId) await dependencies.client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
|
|
6772
|
-
else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
|
|
6773
|
-
},
|
|
6774
|
-
previousConfig: previousWorkerConfig,
|
|
6775
|
-
previousPublicEndpoints,
|
|
6776
|
-
inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, input.manifest, action.key, action.name),
|
|
6777
|
-
removeCustomDomains: async () => {
|
|
6778
|
-
if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
|
|
6779
|
-
await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(input.manifest, /* @__PURE__ */ new Set([action.name])));
|
|
6780
|
-
},
|
|
6781
|
-
retainNewWorker: async () => {
|
|
6782
|
-
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
6783
|
-
},
|
|
6784
|
-
runner: parallelWorkerUploads ? runParallelPinnedPlatformWrangler : runPinnedPlatformWrangler,
|
|
6785
|
-
workerExisted: action.action !== "create"
|
|
6786
|
-
});
|
|
6787
|
-
markWorkerStep("deploy");
|
|
6788
|
-
if (rollback) workerRollbacks.push(rollback);
|
|
6789
|
-
await checkpointWorkerManifest(async () => {
|
|
6790
|
-
const recorded = recordResource(input.manifest, action, action.name, "created", now());
|
|
6791
|
-
if (rollback || dependencies.deployWorker || JSON.stringify(recorded.resources) !== JSON.stringify(input.manifest.resources)) {
|
|
6792
|
-
input.manifest = recorded;
|
|
6793
|
-
writeInstallationManifest(input.manifest);
|
|
6794
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
|
|
6795
|
-
}
|
|
6796
|
-
});
|
|
6797
|
-
markWorkerStep("record");
|
|
6798
|
-
};
|
|
6799
|
-
if (parallelWorkerUploads) {
|
|
6800
|
-
const tasks = [];
|
|
6801
|
-
for (const [index, key] of workerKeys.entries()) {
|
|
6802
|
-
const priorWorker = tasks.at(-1);
|
|
6803
|
-
const ready = workerReady[index];
|
|
6804
|
-
const task = deployOneWorker(key, priorWorker ? () => priorWorker : void 0, async () => {
|
|
6805
|
-
ready.resolve();
|
|
6806
|
-
const batch = await uploadBatch;
|
|
6807
|
-
if (batch.error !== null) throw batch.error;
|
|
6808
|
-
});
|
|
6809
|
-
task.then(() => ready.resolve(), (error) => ready.reject(error));
|
|
6810
|
-
tasks.push(task);
|
|
6811
|
-
}
|
|
6812
|
-
const firstFailure = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
|
|
6813
|
-
if (firstFailure) throw firstFailure.reason;
|
|
6814
|
-
} else for (const key of workerKeys) await deployOneWorker(key);
|
|
6815
|
-
}
|
|
6816
|
-
//#endregion
|
|
6817
6246
|
//#region src/cli/platform-secret-update-authority.ts
|
|
6818
|
-
const COMPLETE_WORKER_SECRET_UPDATE_NAMES = [
|
|
6819
|
-
"CF_API_TOKEN",
|
|
6820
|
-
"GITHUB_CLIENT_ID",
|
|
6821
|
-
"GITHUB_CLIENT_SECRET",
|
|
6822
|
-
"R2_ACCESS_KEY_ID",
|
|
6823
|
-
"R2_SECRET_ACCESS_KEY"
|
|
6824
|
-
];
|
|
6825
|
-
const EMAIL_WORKER_SECRET_UPDATE_NAME = "EMAIL_SIGNING_SECRET";
|
|
6826
6247
|
/**
|
|
6827
6248
|
* Prevents a host without the encrypted values for an interrupted credential
|
|
6828
6249
|
* update from carrying an opaque, possibly partial update into another cutover.
|
|
@@ -6860,33 +6281,34 @@ function readLatestWorkerGeneration(manifest) {
|
|
|
6860
6281
|
}));
|
|
6861
6282
|
}
|
|
6862
6283
|
function hasCompleteWorkerSecretReplacement(updates, requiresEmailSigningSecret) {
|
|
6863
|
-
return
|
|
6284
|
+
return PLATFORM_WORKER_SECRET_UPDATE_NAMES.every((name) => name === "EMAIL_SIGNING_SECRET" && !requiresEmailSigningSecret || typeof updates[name] === "string" && updates[name] !== "");
|
|
6864
6285
|
}
|
|
6865
6286
|
//#endregion
|
|
6866
6287
|
//#region src/cli/platform-installer.ts
|
|
6867
|
-
async function recoverFailedPlatformInstallation(error,
|
|
6288
|
+
async function recoverFailedPlatformInstallation(error, state) {
|
|
6289
|
+
const { plan, dependencies, workerTrafficGuarded, workerDeploymentCommitted, workerRollbacks, authenticationEnforcementActive, authenticationProtocolTransition, workerCutoverGeneration, previousPublicEndpoints, operationStarted, now, writeRemoteInstallationCheckpoint } = state;
|
|
6290
|
+
let { manifest } = state;
|
|
6868
6291
|
let failure = error;
|
|
6869
6292
|
const currentWorkerRecoveryIncomplete = error instanceof PlatformWorkerRecoveryIncompleteError;
|
|
6870
6293
|
if (workerTrafficGuarded) try {
|
|
6871
6294
|
await dependencies.client.putKvValue(plan.account.id, requireId(Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id])), "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
6872
6295
|
} catch (guardError) {
|
|
6873
|
-
failure = new Error(`platform: ${
|
|
6296
|
+
failure = new Error(`platform: ${getErrorMessage(error)} The installation also failed to retain its routing safety gate: ${getErrorMessage(guardError)}`, { cause: error });
|
|
6874
6297
|
}
|
|
6875
6298
|
if (!workerDeploymentCommitted && workerRollbacks.length > 0 && !(authenticationEnforcementActive && authenticationProtocolTransition)) {
|
|
6876
6299
|
const rollbackFailures = [];
|
|
6877
6300
|
for (const rollback of workerRollbacks.reverse()) try {
|
|
6878
6301
|
await rollback();
|
|
6879
6302
|
} catch (rollbackError) {
|
|
6880
|
-
rollbackFailures.push(
|
|
6303
|
+
rollbackFailures.push(getErrorMessage(rollbackError));
|
|
6881
6304
|
}
|
|
6882
|
-
|
|
6883
|
-
if (rollbackFailures.length > 0 || currentWorkerRecoveryIncomplete) failure = new Error(rollbackFailures.length > 0 ? `platform: ${errorMessage(error)} Automatic Worker rollback was incomplete: ${rollbackFailures.join("; ")} Inspect the installation before retrying.` : `platform: ${errorMessage(error)} The current Worker rollback was incomplete. The cutover remains unresolved and will be recovered on retry.`, { cause: error });
|
|
6305
|
+
if (rollbackFailures.length > 0 || currentWorkerRecoveryIncomplete) failure = new Error(rollbackFailures.length > 0 ? `platform: ${getErrorMessage(error)} Automatic Worker rollback was incomplete: ${rollbackFailures.join("; ")} Inspect the installation before retrying.` : `platform: ${getErrorMessage(error)} The current Worker rollback was incomplete. The cutover remains unresolved and will be recovered on retry.`, { cause: error });
|
|
6884
6306
|
else if (workerCutoverGeneration > 0) {
|
|
6885
6307
|
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "restored", now());
|
|
6886
6308
|
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, previousPublicEndpoints, now());
|
|
6887
6309
|
writeInstallationManifest(manifest);
|
|
6888
6310
|
}
|
|
6889
|
-
}
|
|
6311
|
+
}
|
|
6890
6312
|
if (manifest.status !== "uninstalled" && (manifest.status !== "failed" || operationStarted)) try {
|
|
6891
6313
|
const updatedAt = now();
|
|
6892
6314
|
manifest = transitionInstallation(manifest, "failed", updatedAt);
|
|
@@ -6906,9 +6328,10 @@ async function recoverFailedPlatformInstallation(error, manifest, plan, dependen
|
|
|
6906
6328
|
async function installPlatform(plan, secrets, existing, dependencies) {
|
|
6907
6329
|
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.");
|
|
6908
6330
|
const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
6909
|
-
const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl
|
|
6910
|
-
const
|
|
6911
|
-
const
|
|
6331
|
+
const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl;
|
|
6332
|
+
const pinnedPlatformWrangler = (parallelWorkerUpload) => (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, parallelWorkerUpload, dependencies.operatorApiToken);
|
|
6333
|
+
const runPinnedPlatformWrangler = pinnedPlatformWrangler(false);
|
|
6334
|
+
const runParallelPinnedPlatformWrangler = pinnedPlatformWrangler(true);
|
|
6912
6335
|
const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
|
|
6913
6336
|
const runtimeDirectory = resolvedRuntime.directory;
|
|
6914
6337
|
const runtime = resolvedRuntime.manifest;
|
|
@@ -6959,18 +6382,13 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
6959
6382
|
const targetUsageProtocolVersion = runtime.usageProtocolVersion;
|
|
6960
6383
|
const sourcePlatformVersion = existing?.platformVersion ?? null;
|
|
6961
6384
|
const hadPriorWorkers = existing !== null && (existing.resources.some((resource) => resource.kind === "worker" && resource.state === "ready") || plan.actions.some((action) => action.kind === "worker" && action.action !== "create"));
|
|
6962
|
-
const wasDisabled = existing?.status === "disabled";
|
|
6963
6385
|
const preserveDisabled = existing?.desiredState === "disabled";
|
|
6964
6386
|
const shouldBootstrapSharedEmail = !preserveDisabled && plan.deployment.capabilities.email && runtime.emailProtocolVersion === 1 && !!plan.deployment.emailSharedZoneId;
|
|
6965
6387
|
const previousPublicEndpoints = stablePublicEndpointState(existing);
|
|
6966
6388
|
const authenticationProtocolTransition = hadPriorWorkers && existing?.deployment.authentication.configurable !== true && runtime.authenticationProtocolVersion === 1;
|
|
6967
|
-
let workerCutoverGeneration = 0;
|
|
6968
|
-
let workerRollbacks = [];
|
|
6969
|
-
let workerDeploymentCommitted = false;
|
|
6970
6389
|
let workerTrafficGuarded = false;
|
|
6971
|
-
let
|
|
6972
|
-
|
|
6973
|
-
if (!manifest) {
|
|
6390
|
+
let manifest;
|
|
6391
|
+
if (!existing) {
|
|
6974
6392
|
const createdAt = now();
|
|
6975
6393
|
manifest = createPlatformInstallationManifest({
|
|
6976
6394
|
account: plan.account,
|
|
@@ -6982,11 +6400,12 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
6982
6400
|
});
|
|
6983
6401
|
manifest = transitionInstallation(manifest, "planned", now());
|
|
6984
6402
|
writeInstallationManifest(manifest);
|
|
6985
|
-
} else if (
|
|
6403
|
+
} else if (existing.status === "uninstalled") throw new Error(`platform: Installation ${existing.id} has already been uninstalled.`);
|
|
6404
|
+
else manifest = existing;
|
|
6986
6405
|
const explicitWorkerSecretUpdates = sanitizeWorkerSecretUpdates(secrets.workerSecretUpdates);
|
|
6987
6406
|
const hasExplicitWorkerSecretUpdates = Object.keys(explicitWorkerSecretUpdates).length > 0;
|
|
6988
6407
|
const resumedWorkerSecretUpdateIntent = secrets.pendingWorkerSecretUpdateIntent && platformWorkerSecretUpdateIntentState(manifest, secrets.pendingWorkerSecretUpdateIntent) === "active" ? secrets.pendingWorkerSecretUpdateIntent : null;
|
|
6989
|
-
|
|
6408
|
+
const workerSecretUpdateIntent = hasExplicitWorkerSecretUpdates ? {
|
|
6990
6409
|
baselineGeneration: latestPlatformWorkerCutoverGeneration(manifest),
|
|
6991
6410
|
generation: null,
|
|
6992
6411
|
id: randomUUID(),
|
|
@@ -6996,10 +6415,6 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
6996
6415
|
...explicitWorkerSecretUpdates
|
|
6997
6416
|
}
|
|
6998
6417
|
} : resumedWorkerSecretUpdateIntent;
|
|
6999
|
-
if (preserveDisabled && manifest.desiredState !== "disabled") {
|
|
7000
|
-
manifest = setInstallationDesiredState(manifest, "disabled", now());
|
|
7001
|
-
writeInstallationManifest(manifest);
|
|
7002
|
-
}
|
|
7003
6418
|
assertRequiredPlatformSecrets(plan, manifest, secrets, !!dependencies.prepareAuthentication);
|
|
7004
6419
|
if (hyperdriveAction?.action === "create") {
|
|
7005
6420
|
parsePostgresConnectionString(secrets.databaseUrl);
|
|
@@ -7009,30 +6424,40 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7009
6424
|
...secrets,
|
|
7010
6425
|
externalHyperdriveJson: JSON.stringify(plan.externalHyperdrive)
|
|
7011
6426
|
};
|
|
7012
|
-
const
|
|
7013
|
-
|
|
6427
|
+
const state = {
|
|
6428
|
+
manifest,
|
|
6429
|
+
plan,
|
|
6430
|
+
secrets,
|
|
6431
|
+
workerSecretUpdateIntent,
|
|
6432
|
+
workerCutoverGeneration: 0,
|
|
6433
|
+
workerRollbacks: [],
|
|
6434
|
+
workerDeploymentCommitted: false,
|
|
6435
|
+
authenticationEnforcementActive: false
|
|
6436
|
+
};
|
|
6437
|
+
const pendingSecrets = secretCheckpoint(state.secrets, state.workerSecretUpdateIntent);
|
|
6438
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
7014
6439
|
...pendingSecrets,
|
|
7015
6440
|
databaseClaimState: "claiming"
|
|
7016
6441
|
});
|
|
7017
6442
|
dependencies.onProgress?.("Preparing the platform database");
|
|
7018
6443
|
try {
|
|
7019
|
-
await (dependencies.claimDatabase ?? claimPlatformDatabase)(secrets.databaseUrl, manifest.id, manifest.ownershipNonce);
|
|
6444
|
+
await (dependencies.claimDatabase ?? claimPlatformDatabase)(state.secrets.databaseUrl, state.manifest.id, state.manifest.ownershipNonce);
|
|
7020
6445
|
} catch (error) {
|
|
7021
|
-
writePendingInstallationSecrets(manifest, {
|
|
6446
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
7022
6447
|
...pendingSecrets,
|
|
7023
6448
|
databaseClaimState: "failed"
|
|
7024
6449
|
});
|
|
7025
6450
|
throw error;
|
|
7026
6451
|
}
|
|
7027
|
-
writePendingInstallationSecrets(manifest, {
|
|
6452
|
+
writePendingInstallationSecrets(state.manifest, {
|
|
7028
6453
|
...pendingSecrets,
|
|
7029
6454
|
databaseClaimState: "claimed"
|
|
7030
6455
|
});
|
|
7031
6456
|
dependencies.onProgress?.("Acquiring the database lock");
|
|
7032
|
-
const releaseDatabaseLock = await dependencies.acquireDatabaseLifecycleLock(secrets.databaseUrl, manifest.id, manifest.ownershipNonce);
|
|
6457
|
+
const releaseDatabaseLock = await dependencies.acquireDatabaseLifecycleLock(state.secrets.databaseUrl, state.manifest.id, state.manifest.ownershipNonce);
|
|
7033
6458
|
const writeRemoteInstallationCheckpoint = (client, checkpointPlan, checkpointManifest) => writeRemoteInstallationCheckpointWithoutDatabase(client, checkpointPlan, checkpointManifest, releaseDatabaseLock);
|
|
7034
6459
|
const authoritative = releaseDatabaseLock.authoritativeManifest;
|
|
7035
|
-
if (authoritative && JSON.stringify(authoritative) !== JSON.stringify(manifest)) {
|
|
6460
|
+
if (authoritative && JSON.stringify(authoritative) !== JSON.stringify(state.manifest)) {
|
|
7036
6461
|
writeInstallationManifest(authoritative);
|
|
7037
6462
|
await releaseDatabaseLock();
|
|
7038
6463
|
throw new Error("platform: This installation changed on another host. Local state was refreshed; rerun the command to review a current plan.");
|
|
@@ -7040,41 +6465,40 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7040
6465
|
try {
|
|
7041
6466
|
assertPlatformWorkerSecretUpdateAuthority({
|
|
7042
6467
|
explicitUpdates: explicitWorkerSecretUpdates,
|
|
7043
|
-
localIntent: secrets.pendingWorkerSecretUpdateIntent,
|
|
7044
|
-
manifest,
|
|
7045
|
-
requiresEmailSigningSecret: manifest.deployment.capabilities.email || plan.deployment.capabilities.email
|
|
6468
|
+
localIntent: state.secrets.pendingWorkerSecretUpdateIntent,
|
|
6469
|
+
manifest: state.manifest,
|
|
6470
|
+
requiresEmailSigningSecret: state.manifest.deployment.capabilities.email || state.plan.deployment.capabilities.email
|
|
7046
6471
|
});
|
|
7047
6472
|
} catch (error) {
|
|
7048
6473
|
await releaseDatabaseLock();
|
|
7049
6474
|
throw error;
|
|
7050
6475
|
}
|
|
7051
|
-
if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
|
|
7052
6476
|
const checkpointOperation = async (phase) => {
|
|
7053
|
-
if (!manifest
|
|
6477
|
+
if (!state.manifest.operation) return;
|
|
7054
6478
|
const updatedAt = now();
|
|
7055
|
-
manifest = {
|
|
7056
|
-
...manifest,
|
|
6479
|
+
state.manifest = {
|
|
6480
|
+
...state.manifest,
|
|
7057
6481
|
operation: {
|
|
7058
|
-
...manifest.operation,
|
|
6482
|
+
...state.manifest.operation,
|
|
7059
6483
|
phase,
|
|
7060
6484
|
updatedAt
|
|
7061
6485
|
},
|
|
7062
6486
|
updatedAt
|
|
7063
6487
|
};
|
|
7064
|
-
writeInstallationManifest(manifest);
|
|
7065
|
-
await releaseDatabaseLock.checkpoint?.(manifest);
|
|
6488
|
+
writeInstallationManifest(state.manifest);
|
|
6489
|
+
await releaseDatabaseLock.checkpoint?.(state.manifest);
|
|
7066
6490
|
};
|
|
7067
6491
|
let operationStarted = false;
|
|
7068
6492
|
try {
|
|
7069
|
-
assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
|
|
7070
|
-
if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
|
|
6493
|
+
assertPlatformRuntimeSafety(runtime, state.manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
|
|
6494
|
+
if (existing) await dependencies.verifyExistingOwnership?.(state.manifest, authoritative !== null && authoritative !== void 0);
|
|
7071
6495
|
const action = dependencies.operation ?? (existing ? "upgrade" : "install");
|
|
7072
|
-
const previousOperation = manifest.operation;
|
|
6496
|
+
const previousOperation = state.manifest.operation;
|
|
7073
6497
|
const resuming = previousOperation?.action === action && previousOperation.toVersion === version && previousOperation.runtimeManifestSha256 === resolvedRuntime.manifestSha256 && previousOperation.phase !== "complete";
|
|
7074
6498
|
const startedAt = now();
|
|
7075
6499
|
operationStarted = true;
|
|
7076
|
-
manifest = {
|
|
7077
|
-
...manifest,
|
|
6500
|
+
state.manifest = {
|
|
6501
|
+
...state.manifest,
|
|
7078
6502
|
operation: {
|
|
7079
6503
|
id: resuming ? previousOperation.id : randomUUID(),
|
|
7080
6504
|
action,
|
|
@@ -7089,49 +6513,18 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7089
6513
|
};
|
|
7090
6514
|
await checkpointOperation("preparing");
|
|
7091
6515
|
await prepareInstallationAuthentication({
|
|
6516
|
+
state,
|
|
7092
6517
|
releaseDatabaseLock,
|
|
7093
|
-
get authenticationEnforcementActive() {
|
|
7094
|
-
return authenticationEnforcementActive;
|
|
7095
|
-
},
|
|
7096
|
-
set authenticationEnforcementActive(value) {
|
|
7097
|
-
authenticationEnforcementActive = value;
|
|
7098
|
-
},
|
|
7099
6518
|
runtime,
|
|
7100
|
-
get plan() {
|
|
7101
|
-
return plan;
|
|
7102
|
-
},
|
|
7103
|
-
set plan(value) {
|
|
7104
|
-
plan = value;
|
|
7105
|
-
},
|
|
7106
6519
|
authenticationProtocolTransition,
|
|
7107
|
-
get manifest() {
|
|
7108
|
-
return manifest;
|
|
7109
|
-
},
|
|
7110
|
-
set manifest(value) {
|
|
7111
|
-
manifest = value;
|
|
7112
|
-
},
|
|
7113
|
-
get secrets() {
|
|
7114
|
-
return secrets;
|
|
7115
|
-
},
|
|
7116
|
-
set secrets(value) {
|
|
7117
|
-
secrets = value;
|
|
7118
|
-
},
|
|
7119
6520
|
now,
|
|
7120
|
-
dependencies
|
|
7121
|
-
workerSecretUpdateIntent
|
|
6521
|
+
dependencies
|
|
7122
6522
|
});
|
|
7123
|
-
const
|
|
7124
|
-
|
|
7125
|
-
get manifest() {
|
|
7126
|
-
return manifest;
|
|
7127
|
-
},
|
|
7128
|
-
set manifest(value) {
|
|
7129
|
-
manifest = value;
|
|
7130
|
-
},
|
|
6523
|
+
const provisioned = await provisionInstallationServices({
|
|
6524
|
+
state,
|
|
7131
6525
|
now,
|
|
7132
6526
|
releaseDatabaseLock,
|
|
7133
6527
|
dependencies,
|
|
7134
|
-
secrets,
|
|
7135
6528
|
writeRemoteInstallationCheckpoint,
|
|
7136
6529
|
checkpointOperation,
|
|
7137
6530
|
externalHyperdriveOriginHash,
|
|
@@ -7140,55 +6533,49 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7140
6533
|
sourcePlatformVersion,
|
|
7141
6534
|
hadPriorWorkers
|
|
7142
6535
|
});
|
|
7143
|
-
if ("waitingForDns" in
|
|
7144
|
-
const zone =
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
let interruptedCutover = interruptedWorkerCutover(manifest);
|
|
7152
|
-
if (interruptedCutover && authenticationProtocolTransition && authenticationEnforcementActive) {
|
|
7153
|
-
manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "committed", now());
|
|
7154
|
-
manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, false, now());
|
|
7155
|
-
writeInstallationManifest(manifest);
|
|
7156
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
6536
|
+
if ("waitingForDns" in provisioned) return provisioned;
|
|
6537
|
+
const { zone, zoneId, targetSandboxDrainProtocol, prepareSandboxDrain, sandboxDrainState, ids, entrypoints } = provisioned;
|
|
6538
|
+
let interruptedCutover = interruptedWorkerCutover(state.manifest);
|
|
6539
|
+
if (interruptedCutover && authenticationProtocolTransition && state.authenticationEnforcementActive) {
|
|
6540
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, interruptedCutover.generation, "committed", now());
|
|
6541
|
+
state.manifest = recordWorkerTrafficState(state.manifest, interruptedCutover.generation, false, now());
|
|
6542
|
+
writeInstallationManifest(state.manifest);
|
|
6543
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
7157
6544
|
interruptedCutover = null;
|
|
7158
6545
|
}
|
|
7159
6546
|
const recoveringUnsafeTraffic = (interruptedCutover?.unsafeTraffic.size ?? 0) > 0;
|
|
7160
|
-
const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
6547
|
+
const createsWorker = state.plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
7161
6548
|
const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
|
|
7162
6549
|
const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
|
|
7163
|
-
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain || !!plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
6550
|
+
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || state.manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain || !!state.plan.deployment.appAccess && !state.manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
7164
6551
|
if (requiresTrafficGuard) {
|
|
7165
|
-
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
6552
|
+
await dependencies.client.putKvValue(state.plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
7166
6553
|
workerTrafficGuarded = true;
|
|
7167
6554
|
dependencies.onProgress?.("Application traffic paused for a safe platform cutover");
|
|
7168
6555
|
}
|
|
7169
6556
|
if (!preserveDisabled) {
|
|
7170
|
-
const trafficGeneration = nextTrafficGeneration(manifest);
|
|
7171
|
-
manifest = recordInstallationStep(manifest, {
|
|
6557
|
+
const trafficGeneration = nextTrafficGeneration(state.manifest);
|
|
6558
|
+
state.manifest = recordInstallationStep(state.manifest, {
|
|
7172
6559
|
completedAt: now(),
|
|
7173
6560
|
key: `traffic-enabled:${trafficGeneration}`,
|
|
7174
6561
|
resultHash: String(trafficGeneration)
|
|
7175
6562
|
}, now());
|
|
7176
|
-
writeInstallationManifest(manifest);
|
|
7177
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
6563
|
+
writeInstallationManifest(state.manifest);
|
|
6564
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
7178
6565
|
}
|
|
7179
6566
|
const configInput = {
|
|
7180
6567
|
authenticationDatabaseUncached: true,
|
|
7181
6568
|
proxyHealthBinding: true,
|
|
7182
|
-
accountId: plan.account.id,
|
|
7183
|
-
apiUrl: plan.apiUrl,
|
|
7184
|
-
deployment: plan.deployment,
|
|
6569
|
+
accountId: state.plan.account.id,
|
|
6570
|
+
apiUrl: state.plan.apiUrl,
|
|
6571
|
+
deployment: state.plan.deployment,
|
|
7185
6572
|
deploymentExecution: runtime.deploymentExecution,
|
|
7186
|
-
healthProbeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
7187
|
-
installationId: manifest.id,
|
|
7188
|
-
ownershipNonce: manifest.ownershipNonce,
|
|
6573
|
+
healthProbeToken: createPlatformHealthProbeToken(state.manifest.ownershipNonce),
|
|
6574
|
+
installationId: state.manifest.id,
|
|
6575
|
+
ownershipNonce: state.manifest.ownershipNonce,
|
|
7189
6576
|
platformVersion: version,
|
|
7190
|
-
proxyUrl: plan.proxyUrl,
|
|
7191
|
-
emailGatewayUrl:
|
|
6577
|
+
proxyUrl: state.plan.proxyUrl,
|
|
6578
|
+
emailGatewayUrl: platformWorkersDevUrl(state.plan, state.plan.deployment.resources.emailGatewayWorker),
|
|
7192
6579
|
resources: {
|
|
7193
6580
|
hyperdrive: requireId(ids, "hyperdrive"),
|
|
7194
6581
|
isrCacheKv: requireId(ids, "isr-cache-kv"),
|
|
@@ -7200,8 +6587,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7200
6587
|
...runtime.assets ? { runtimeApiAssetsDirectory: join(runtimeDirectory, PLATFORM_ADMIN_ASSETS_DIRECTORY) } : {}
|
|
7201
6588
|
};
|
|
7202
6589
|
const workerDeploymentSecrets = {
|
|
7203
|
-
...secrets,
|
|
7204
|
-
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
6590
|
+
...state.secrets,
|
|
6591
|
+
workerSecretUpdates: state.workerSecretUpdateIntent?.updates ?? {}
|
|
7205
6592
|
};
|
|
7206
6593
|
const requiresStagedCommit = !preserveDisabled && requiresTrafficGuard;
|
|
7207
6594
|
const enabledConfigs = createPlatformWorkerConfigs({
|
|
@@ -7219,80 +6606,86 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7219
6606
|
publicEndpoints: previousPublicEndpoints,
|
|
7220
6607
|
trafficState: previousPublicEndpoints ? "enabled" : "staging"
|
|
7221
6608
|
});
|
|
7222
|
-
const managedRoutes = platformManagedWorkerTraffic(plan.deployment).routes;
|
|
7223
|
-
let workerActions = plan.actions.filter((action) => action.kind === "worker" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
7224
|
-
({manifest, workerActions} = await recoverInterruptedPlatformWorkerCutover(
|
|
7225
|
-
manifest
|
|
6609
|
+
const managedRoutes = platformManagedWorkerTraffic(state.plan.deployment).routes;
|
|
6610
|
+
let workerActions = state.plan.actions.filter((action) => action.kind === "worker" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
6611
|
+
({manifest: state.manifest, workerActions} = await recoverInterruptedPlatformWorkerCutover({
|
|
6612
|
+
manifest: state.manifest,
|
|
6613
|
+
interruptedCutover,
|
|
6614
|
+
workerActions,
|
|
6615
|
+
configs,
|
|
6616
|
+
previousConfigs,
|
|
6617
|
+
plan: state.plan,
|
|
6618
|
+
dependencies,
|
|
6619
|
+
ids,
|
|
6620
|
+
previousPublicEndpoints,
|
|
6621
|
+
hasEmailGatewayArtifact,
|
|
6622
|
+
runPinnedPlatformWrangler,
|
|
6623
|
+
now,
|
|
6624
|
+
writeRemoteInstallationCheckpoint,
|
|
6625
|
+
onManifestChange: (next) => {
|
|
6626
|
+
state.manifest = next;
|
|
6627
|
+
}
|
|
7226
6628
|
}));
|
|
7227
|
-
workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
|
|
7228
|
-
if (workerSecretUpdateIntent) {
|
|
7229
|
-
manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "pending", workerSecretUpdateIntent.id, now());
|
|
7230
|
-
writeInstallationManifest(manifest);
|
|
7231
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
7232
|
-
workerSecretUpdateIntent = {
|
|
7233
|
-
...workerSecretUpdateIntent,
|
|
7234
|
-
generation: workerCutoverGeneration
|
|
6629
|
+
state.workerCutoverGeneration = nextWorkerCutoverGeneration(state.manifest);
|
|
6630
|
+
if (state.workerSecretUpdateIntent) {
|
|
6631
|
+
state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "pending", state.workerSecretUpdateIntent.id, now());
|
|
6632
|
+
writeInstallationManifest(state.manifest);
|
|
6633
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
6634
|
+
state.workerSecretUpdateIntent = {
|
|
6635
|
+
...state.workerSecretUpdateIntent,
|
|
6636
|
+
generation: state.workerCutoverGeneration
|
|
7235
6637
|
};
|
|
7236
|
-
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
6638
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, state.workerSecretUpdateIntent));
|
|
7237
6639
|
}
|
|
7238
6640
|
await uploadInstallationWorkers({
|
|
6641
|
+
state,
|
|
7239
6642
|
dependencies,
|
|
7240
6643
|
requiresStagedCommit,
|
|
7241
|
-
workerSecretUpdateIntent,
|
|
7242
6644
|
recoveringUnsafeTraffic,
|
|
7243
6645
|
workerActions,
|
|
7244
6646
|
workerKeys,
|
|
7245
6647
|
workerDeploymentSecrets,
|
|
7246
|
-
plan,
|
|
7247
6648
|
zoneId,
|
|
7248
|
-
get manifest() {
|
|
7249
|
-
return manifest;
|
|
7250
|
-
},
|
|
7251
|
-
set manifest(value) {
|
|
7252
|
-
manifest = value;
|
|
7253
|
-
},
|
|
7254
6649
|
now,
|
|
7255
6650
|
writeRemoteInstallationCheckpoint,
|
|
7256
6651
|
configs,
|
|
7257
6652
|
previousConfigs,
|
|
7258
6653
|
cloudflareApiBaseUrl,
|
|
7259
|
-
workerCutoverGeneration,
|
|
7260
6654
|
preserveDisabled,
|
|
7261
6655
|
managedRoutes,
|
|
7262
6656
|
previousPublicEndpoints,
|
|
7263
6657
|
ids,
|
|
7264
6658
|
runParallelPinnedPlatformWrangler,
|
|
7265
|
-
runPinnedPlatformWrangler
|
|
7266
|
-
workerRollbacks
|
|
6659
|
+
runPinnedPlatformWrangler
|
|
7267
6660
|
});
|
|
7268
|
-
manifest = setInstallationEndpoints(manifest, {
|
|
7269
|
-
apiUrl: plan.apiUrl,
|
|
7270
|
-
dashboardUrl: platformDashboardUrl(plan.deployment),
|
|
7271
|
-
proxyUrl: plan.proxyUrl
|
|
6661
|
+
state.manifest = setInstallationEndpoints(state.manifest, {
|
|
6662
|
+
apiUrl: state.plan.apiUrl,
|
|
6663
|
+
dashboardUrl: platformDashboardUrl(state.plan.deployment),
|
|
6664
|
+
proxyUrl: state.plan.proxyUrl
|
|
7272
6665
|
}, now());
|
|
7273
6666
|
if (preserveDisabled) {
|
|
7274
|
-
workerDeploymentCommitted = true;
|
|
7275
|
-
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
|
|
7276
|
-
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
|
|
7277
|
-
if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
|
|
7278
|
-
manifest = {
|
|
7279
|
-
...manifest,
|
|
7280
|
-
deployment: plan.deployment,
|
|
6667
|
+
state.workerDeploymentCommitted = true;
|
|
6668
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
|
|
6669
|
+
state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
|
|
6670
|
+
if (state.workerSecretUpdateIntent) state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "committed", state.workerSecretUpdateIntent.id, now());
|
|
6671
|
+
state.manifest = {
|
|
6672
|
+
...state.manifest,
|
|
6673
|
+
deployment: state.plan.deployment,
|
|
7281
6674
|
deploymentExecution: runtime.deploymentExecution,
|
|
7282
6675
|
platformVersion: version,
|
|
7283
6676
|
runtime: runtimeProvenance,
|
|
7284
6677
|
updatedAt: now(),
|
|
7285
6678
|
usageProtocolVersion: targetUsageProtocolVersion
|
|
7286
6679
|
};
|
|
7287
|
-
manifest = transitionInstallation(manifest, "disabled", now());
|
|
6680
|
+
state.manifest = transitionInstallation(state.manifest, "disabled", now());
|
|
7288
6681
|
await checkpointOperation("complete");
|
|
7289
|
-
writeInstallationManifest(manifest);
|
|
7290
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
7291
|
-
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets));
|
|
7292
|
-
promoteInstallationSecrets(manifest);
|
|
7293
|
-
saveInstalledPlatformConnection(plan, manifest);
|
|
6682
|
+
writeInstallationManifest(state.manifest);
|
|
6683
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
6684
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets));
|
|
6685
|
+
promoteInstallationSecrets(state.manifest);
|
|
6686
|
+
saveInstalledPlatformConnection(state.plan, state.manifest);
|
|
7294
6687
|
return {
|
|
7295
|
-
manifest,
|
|
6688
|
+
manifest: state.manifest,
|
|
7296
6689
|
waitingForDns: false,
|
|
7297
6690
|
waitingForSandboxDrain: false,
|
|
7298
6691
|
zoneNameServers: zone?.nameServers ?? []
|
|
@@ -7300,113 +6693,113 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7300
6693
|
}
|
|
7301
6694
|
let authenticationCutoverCommitted = false;
|
|
7302
6695
|
if (authenticationProtocolTransition && requiresStagedCommit) {
|
|
7303
|
-
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
|
|
7304
|
-
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
|
|
7305
|
-
writeInstallationManifest(manifest);
|
|
7306
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
7307
|
-
workerRollbacks = [];
|
|
6696
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
|
|
6697
|
+
state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
|
|
6698
|
+
writeInstallationManifest(state.manifest);
|
|
6699
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
6700
|
+
state.workerRollbacks = [];
|
|
7308
6701
|
authenticationCutoverCommitted = true;
|
|
7309
6702
|
}
|
|
7310
|
-
let authenticationStep = secrets.authenticationSetupJson ? "authentication-initialized:v1:" + hash(secrets.authenticationSetupJson) : "";
|
|
6703
|
+
let authenticationStep = state.secrets.authenticationSetupJson ? "authentication-initialized:v1:" + hash(state.secrets.authenticationSetupJson) : "";
|
|
7311
6704
|
let setupPending = false;
|
|
7312
|
-
manifest = transitionInstallation(manifest, "verifying", now());
|
|
6705
|
+
state.manifest = transitionInstallation(state.manifest, "verifying", now());
|
|
7313
6706
|
await checkpointOperation("verifying");
|
|
7314
|
-
writeInstallationManifest(manifest);
|
|
7315
|
-
if (plan.deployment.authentication.configurable && authenticationStep) {
|
|
7316
|
-
if (!secrets.authenticationSetupJson || !secrets.authSetupToken) throw new Error("platform: Restore the encrypted authentication setup checkpoint before resuming.");
|
|
6707
|
+
writeInstallationManifest(state.manifest);
|
|
6708
|
+
if (state.plan.deployment.authentication.configurable && authenticationStep) {
|
|
6709
|
+
if (!state.secrets.authenticationSetupJson || !state.secrets.authSetupToken) throw new Error("platform: Restore the encrypted authentication setup checkpoint before resuming.");
|
|
7317
6710
|
dependencies.onProgress?.("Initializing platform authentication");
|
|
7318
6711
|
if ((await resumeInstalledPlatformAuthentication({
|
|
7319
|
-
apiUrl: plan.apiUrl,
|
|
7320
|
-
installationId: manifest.id,
|
|
7321
|
-
setupToken: secrets.authSetupToken,
|
|
7322
|
-
setupJson: secrets.authenticationSetupJson,
|
|
6712
|
+
apiUrl: state.plan.apiUrl,
|
|
6713
|
+
installationId: state.manifest.id,
|
|
6714
|
+
setupToken: state.secrets.authSetupToken,
|
|
6715
|
+
setupJson: state.secrets.authenticationSetupJson,
|
|
7323
6716
|
checkpoint: (replacement) => {
|
|
7324
|
-
secrets = {
|
|
7325
|
-
...secrets,
|
|
6717
|
+
state.secrets = {
|
|
6718
|
+
...state.secrets,
|
|
7326
6719
|
...replacement
|
|
7327
6720
|
};
|
|
7328
6721
|
authenticationStep = "authentication-initialized:v1:" + hash(replacement.authenticationSetupJson);
|
|
7329
|
-
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
6722
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, state.workerSecretUpdateIntent));
|
|
7330
6723
|
}
|
|
7331
6724
|
}, dependencies.initializeAuthentication ?? initializeInstalledPlatformAuthentication)).setupPending) {
|
|
7332
6725
|
setupPending = true;
|
|
7333
|
-
dependencies.onProgress?.(`Administrator setup pending: ${plan.apiUrl}/setup`);
|
|
6726
|
+
dependencies.onProgress?.(`Administrator setup pending: ${state.plan.apiUrl}/setup`);
|
|
7334
6727
|
}
|
|
7335
|
-
manifest = recordInstallationStep(manifest, {
|
|
6728
|
+
state.manifest = recordInstallationStep(state.manifest, {
|
|
7336
6729
|
key: authenticationStep,
|
|
7337
6730
|
completedAt: now(),
|
|
7338
|
-
resultHash: hash(secrets.authenticationSetupJson)
|
|
6731
|
+
resultHash: hash(state.secrets.authenticationSetupJson)
|
|
7339
6732
|
}, now());
|
|
7340
|
-
writeInstallationManifest(manifest);
|
|
7341
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
6733
|
+
writeInstallationManifest(state.manifest);
|
|
6734
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
7342
6735
|
}
|
|
7343
|
-
if (plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1")) {
|
|
6736
|
+
if (state.plan.deployment.appAccess && !state.manifest.steps.some((step) => step.key === "app-access-initialized:v1")) {
|
|
7344
6737
|
dependencies.onProgress?.("Configuring and verifying deployed-app access");
|
|
7345
6738
|
try {
|
|
7346
6739
|
await (dependencies.initializeAppAccess ?? initializeInstalledAppAccess)({
|
|
7347
|
-
apiUrl: plan.apiUrl,
|
|
7348
|
-
installationId: manifest.id,
|
|
7349
|
-
probeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
7350
|
-
setupJson: secrets.appAccessSetupJson,
|
|
7351
|
-
setupToken: secrets.appAccessSetupToken
|
|
6740
|
+
apiUrl: state.plan.apiUrl,
|
|
6741
|
+
installationId: state.manifest.id,
|
|
6742
|
+
probeToken: createPlatformHealthProbeToken(state.manifest.ownershipNonce),
|
|
6743
|
+
setupJson: state.secrets.appAccessSetupJson,
|
|
6744
|
+
setupToken: state.secrets.appAccessSetupToken
|
|
7352
6745
|
});
|
|
7353
6746
|
} catch (error) {
|
|
7354
6747
|
if (error instanceof PlatformAppAccessCredentialError) {
|
|
7355
|
-
secrets = {
|
|
7356
|
-
...secrets,
|
|
6748
|
+
state.secrets = {
|
|
6749
|
+
...state.secrets,
|
|
7357
6750
|
authenticationPreparation: {
|
|
7358
|
-
...secrets.authenticationPreparation,
|
|
6751
|
+
...state.secrets.authenticationPreparation,
|
|
7359
6752
|
appAccessTokenRejected: "true"
|
|
7360
6753
|
}
|
|
7361
6754
|
};
|
|
7362
|
-
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
6755
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, state.workerSecretUpdateIntent));
|
|
7363
6756
|
}
|
|
7364
6757
|
throw error;
|
|
7365
6758
|
}
|
|
7366
|
-
manifest = recordInstallationStep(manifest, {
|
|
6759
|
+
state.manifest = recordInstallationStep(state.manifest, {
|
|
7367
6760
|
key: "app-access-initialized:v1",
|
|
7368
6761
|
completedAt: now(),
|
|
7369
|
-
resultHash: hash(secrets.appAccessSetupJson)
|
|
6762
|
+
resultHash: hash(state.secrets.appAccessSetupJson)
|
|
7370
6763
|
}, now());
|
|
7371
|
-
writeInstallationManifest(manifest);
|
|
7372
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
6764
|
+
writeInstallationManifest(state.manifest);
|
|
6765
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
7373
6766
|
}
|
|
7374
6767
|
dependencies.onProgress?.("Verifying platform health");
|
|
7375
|
-
const emailGatewayUrl = hasEmailGatewayArtifact && plan.deployment.capabilities.email ?
|
|
7376
|
-
if (plan.deployment.authentication.accessProtection) {
|
|
7377
|
-
ensurePlatformAccessToken(plan.apiUrl, false, true);
|
|
7378
|
-
ensurePlatformAccessToken(plan.proxyUrl, false, true);
|
|
6768
|
+
const emailGatewayUrl = hasEmailGatewayArtifact && state.plan.deployment.capabilities.email ? platformWorkersDevUrl(state.plan, state.plan.deployment.resources.emailGatewayWorker) : void 0;
|
|
6769
|
+
if (state.plan.deployment.authentication.accessProtection) {
|
|
6770
|
+
ensurePlatformAccessToken(state.plan.apiUrl, false, true);
|
|
6771
|
+
ensurePlatformAccessToken(state.plan.proxyUrl, false, true);
|
|
7379
6772
|
}
|
|
7380
6773
|
const healthCheckContext = {
|
|
7381
|
-
accountName: plan.account.name,
|
|
7382
|
-
...plan.deployment.authentication.accessProtection ? { accessProtected: true } : {},
|
|
6774
|
+
accountName: state.plan.account.name,
|
|
6775
|
+
...state.plan.deployment.authentication.accessProtection ? { accessProtected: true } : {},
|
|
7383
6776
|
workers: [
|
|
7384
|
-
plan.deployment.resources.apiWorker,
|
|
7385
|
-
plan.deployment.resources.proxyWorker,
|
|
7386
|
-
...emailGatewayUrl ? [plan.deployment.resources.emailGatewayWorker] : [],
|
|
7387
|
-
plan.deployment.resources.dispatchWorker
|
|
6777
|
+
state.plan.deployment.resources.apiWorker,
|
|
6778
|
+
state.plan.deployment.resources.proxyWorker,
|
|
6779
|
+
...emailGatewayUrl ? [state.plan.deployment.resources.emailGatewayWorker] : [],
|
|
6780
|
+
state.plan.deployment.resources.dispatchWorker
|
|
7388
6781
|
],
|
|
7389
6782
|
resumeCommand: dependencies.healthCheckResumeCommand,
|
|
7390
6783
|
...emailGatewayUrl ? { emailGatewayUrl } : {}
|
|
7391
6784
|
};
|
|
7392
|
-
await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ?
|
|
6785
|
+
await (dependencies.verifyHealth ?? verifyPlatformHealth)(state.plan.apiUrl, state.manifest.id, state.plan.proxyUrl, state.plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(state.manifest.ownershipNonce), void 0, !state.plan.deployment.application.zoneName ? platformWorkersDevUrl(state.plan, state.plan.deployment.resources.dispatchWorker) : void 0, healthCheckContext);
|
|
7393
6786
|
if (requiresSandboxDrain) {
|
|
7394
|
-
if (!await (dependencies.verifySandboxDrain ?? verifyPlatformSandboxDrain)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce))) {
|
|
7395
|
-
manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
|
|
7396
|
-
manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
|
|
7397
|
-
manifest = recordInstallationStep(manifest, {
|
|
6787
|
+
if (!await (dependencies.verifySandboxDrain ?? verifyPlatformSandboxDrain)(state.plan.apiUrl, createPlatformHealthProbeToken(state.manifest.ownershipNonce))) {
|
|
6788
|
+
state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
|
|
6789
|
+
state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
|
|
6790
|
+
state.manifest = recordInstallationStep(state.manifest, {
|
|
7398
6791
|
completedAt: now(),
|
|
7399
6792
|
key: "sandbox-drain:maintenance-runtime:v1",
|
|
7400
6793
|
resultHash: resolvedRuntime.manifestSha256
|
|
7401
6794
|
}, now());
|
|
7402
6795
|
await checkpointOperation("waiting-for-sandbox-drain");
|
|
7403
|
-
writeInstallationManifest(manifest);
|
|
7404
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
7405
|
-
workerRollbacks = [];
|
|
7406
|
-
workerDeploymentCommitted = true;
|
|
7407
|
-
saveInstalledPlatformConnection(plan, manifest);
|
|
6796
|
+
writeInstallationManifest(state.manifest);
|
|
6797
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
6798
|
+
state.workerRollbacks = [];
|
|
6799
|
+
state.workerDeploymentCommitted = true;
|
|
6800
|
+
saveInstalledPlatformConnection(state.plan, state.manifest);
|
|
7408
6801
|
return {
|
|
7409
|
-
manifest,
|
|
6802
|
+
manifest: state.manifest,
|
|
7410
6803
|
waitingForDns: false,
|
|
7411
6804
|
waitingForSandboxDrain: true,
|
|
7412
6805
|
zoneNameServers: zone?.nameServers ?? []
|
|
@@ -7415,38 +6808,13 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7415
6808
|
if ((await prepareSandboxDrain()).state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
|
|
7416
6809
|
}
|
|
7417
6810
|
const emailOperationId = (await enableInstallationServices({
|
|
6811
|
+
state,
|
|
7418
6812
|
checkpointOperation,
|
|
7419
6813
|
requiresStagedCommit,
|
|
7420
6814
|
authenticationCutoverCommitted,
|
|
7421
|
-
get manifest() {
|
|
7422
|
-
return manifest;
|
|
7423
|
-
},
|
|
7424
|
-
set manifest(value) {
|
|
7425
|
-
manifest = value;
|
|
7426
|
-
},
|
|
7427
|
-
get workerCutoverGeneration() {
|
|
7428
|
-
return workerCutoverGeneration;
|
|
7429
|
-
},
|
|
7430
|
-
set workerCutoverGeneration(value) {
|
|
7431
|
-
workerCutoverGeneration = value;
|
|
7432
|
-
},
|
|
7433
6815
|
now,
|
|
7434
6816
|
writeRemoteInstallationCheckpoint,
|
|
7435
6817
|
dependencies,
|
|
7436
|
-
plan,
|
|
7437
|
-
get workerRollbacks() {
|
|
7438
|
-
return workerRollbacks;
|
|
7439
|
-
},
|
|
7440
|
-
set workerRollbacks(value) {
|
|
7441
|
-
workerRollbacks = value;
|
|
7442
|
-
},
|
|
7443
|
-
get workerSecretUpdateIntent() {
|
|
7444
|
-
return workerSecretUpdateIntent;
|
|
7445
|
-
},
|
|
7446
|
-
set workerSecretUpdateIntent(value) {
|
|
7447
|
-
workerSecretUpdateIntent = value;
|
|
7448
|
-
},
|
|
7449
|
-
secrets,
|
|
7450
6818
|
workerKeys,
|
|
7451
6819
|
workerActions,
|
|
7452
6820
|
workerDeploymentSecrets,
|
|
@@ -7458,15 +6826,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7458
6826
|
ids,
|
|
7459
6827
|
runPinnedPlatformWrangler,
|
|
7460
6828
|
shouldBootstrapSharedEmail,
|
|
7461
|
-
get workerDeploymentCommitted() {
|
|
7462
|
-
return workerDeploymentCommitted;
|
|
7463
|
-
},
|
|
7464
|
-
set workerDeploymentCommitted(value) {
|
|
7465
|
-
workerDeploymentCommitted = value;
|
|
7466
|
-
},
|
|
7467
6829
|
releaseDatabaseLock,
|
|
7468
|
-
wasDisabled,
|
|
7469
|
-
previousPublicEndpoints,
|
|
7470
6830
|
version,
|
|
7471
6831
|
targetUsageProtocolVersion,
|
|
7472
6832
|
healthCheckContext,
|
|
@@ -7474,14 +6834,14 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7474
6834
|
runtimeProvenance
|
|
7475
6835
|
})).emailOperationId;
|
|
7476
6836
|
await checkpointOperation("complete");
|
|
7477
|
-
writeInstallationManifest(manifest);
|
|
7478
|
-
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
6837
|
+
writeInstallationManifest(state.manifest);
|
|
6838
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
|
|
7479
6839
|
workerTrafficGuarded = false;
|
|
7480
|
-
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets));
|
|
7481
|
-
promoteInstallationSecrets(manifest);
|
|
7482
|
-
saveInstalledPlatformConnection(plan, manifest);
|
|
6840
|
+
writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets));
|
|
6841
|
+
promoteInstallationSecrets(state.manifest);
|
|
6842
|
+
saveInstalledPlatformConnection(state.plan, state.manifest);
|
|
7483
6843
|
return {
|
|
7484
|
-
manifest,
|
|
6844
|
+
manifest: state.manifest,
|
|
7485
6845
|
...emailOperationId ? { emailOperationId } : {},
|
|
7486
6846
|
setupPending,
|
|
7487
6847
|
waitingForDns: false,
|
|
@@ -7489,13 +6849,25 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
7489
6849
|
zoneNameServers: zone?.nameServers ?? []
|
|
7490
6850
|
};
|
|
7491
6851
|
} catch (error) {
|
|
7492
|
-
throw await recoverFailedPlatformInstallation(error,
|
|
6852
|
+
throw await recoverFailedPlatformInstallation(error, {
|
|
6853
|
+
...state,
|
|
6854
|
+
dependencies,
|
|
6855
|
+
workerTrafficGuarded,
|
|
6856
|
+
authenticationProtocolTransition,
|
|
6857
|
+
previousPublicEndpoints,
|
|
6858
|
+
operationStarted,
|
|
6859
|
+
now,
|
|
6860
|
+
writeRemoteInstallationCheckpoint
|
|
6861
|
+
});
|
|
7493
6862
|
} finally {
|
|
7494
6863
|
await releaseDatabaseLock();
|
|
7495
6864
|
}
|
|
7496
6865
|
}
|
|
7497
6866
|
//#endregion
|
|
7498
6867
|
//#region src/cli/platform-install-auth.ts
|
|
6868
|
+
function defaultConnectionId(kind) {
|
|
6869
|
+
return kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind;
|
|
6870
|
+
}
|
|
7499
6871
|
function platformManagementAccessForAuthentication(selection, accountId, checkpoint) {
|
|
7500
6872
|
if (!selection.authentication.accessProtection && (!selection.authentication.providers.includes("cloudflare-access") || selection.file && !selection.file.cloudflareAccess)) return;
|
|
7501
6873
|
const spec = selection.file?.cloudflareAccess ?? (checkpoint?.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(checkpoint.accessSetupSpecJson)) : void 0);
|
|
@@ -7528,7 +6900,6 @@ async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
|
7528
6900
|
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.");
|
|
7529
6901
|
return { authentication: recorded };
|
|
7530
6902
|
}
|
|
7531
|
-
if (!process.stdin.isTTY && !choices.loginMethods && choices.accessProtection === void 0) return { authentication: { providers: ["github"] } };
|
|
7532
6903
|
const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] : platformPromptAnswer(await multiselect({
|
|
7533
6904
|
message: "Platform login methods (select all that apply):",
|
|
7534
6905
|
initialValues: [],
|
|
@@ -7583,7 +6954,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
7583
6954
|
const entries = selection.file?.connections ?? selection.authentication.providers.map((kind) => ({
|
|
7584
6955
|
configuration: {
|
|
7585
6956
|
kind,
|
|
7586
|
-
id: kind
|
|
6957
|
+
id: defaultConnectionId(kind),
|
|
7587
6958
|
label: kind === "github" ? "GitHub" : kind === "google" ? "Google" : kind === "oidc" ? "Company SSO" : "Cloudflare Access"
|
|
7588
6959
|
},
|
|
7589
6960
|
clientSecretEnv: void 0
|
|
@@ -7606,11 +6977,11 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
7606
6977
|
save,
|
|
7607
6978
|
yes: access.yes
|
|
7608
6979
|
}) : void 0;
|
|
7609
|
-
console.log(`Login callback URL: ${plan.apiUrl}/auth/callback`);
|
|
6980
|
+
console.log(`Login callback URL: ${formatUrl(`${plan.apiUrl}/auth/callback`)}`);
|
|
7610
6981
|
for (const entry of entries) {
|
|
7611
6982
|
const config = { ...entry.configuration };
|
|
7612
6983
|
const kind = config.kind;
|
|
7613
|
-
if (!config.id) config.id = kind
|
|
6984
|
+
if (!config.id) config.id = defaultConnectionId(kind);
|
|
7614
6985
|
if (prepared?.login && prepared.login.configuration.id === config.id) {
|
|
7615
6986
|
const connection = prepared.login;
|
|
7616
6987
|
save({ [`authenticationConnection:${connection.configuration.id}`]: JSON.stringify(connection) });
|
|
@@ -7769,17 +7140,7 @@ function savePlatformInstallChoices(arguments_, accountName, directory = platfor
|
|
|
7769
7140
|
recursive: true,
|
|
7770
7141
|
mode: 448
|
|
7771
7142
|
});
|
|
7772
|
-
|
|
7773
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
7774
|
-
try {
|
|
7775
|
-
writeFileSync(temporary, JSON.stringify(record, null, 2) + "\n", {
|
|
7776
|
-
mode: 384,
|
|
7777
|
-
flag: "wx"
|
|
7778
|
-
});
|
|
7779
|
-
renameSync(temporary, path);
|
|
7780
|
-
} finally {
|
|
7781
|
-
rmSync(temporary, { force: true });
|
|
7782
|
-
}
|
|
7143
|
+
writeFileAtomic(join(directory, `plan-${plan.id}.json`), JSON.stringify(record, null, 2) + "\n", 384);
|
|
7783
7144
|
return plan;
|
|
7784
7145
|
}
|
|
7785
7146
|
function listSavedPlatformInstallPlans(directory = platformRegistryDirectory()) {
|
|
@@ -7854,12 +7215,6 @@ function completePlatformSetupDraft(manifest) {
|
|
|
7854
7215
|
return completed;
|
|
7855
7216
|
}
|
|
7856
7217
|
//#endregion
|
|
7857
|
-
//#region src/cli/prompt-password.ts
|
|
7858
|
-
/** A visual example for an empty secret field; never a default value or unmasked input. */
|
|
7859
|
-
function passwordWithPlaceholder(options) {
|
|
7860
|
-
return password(options);
|
|
7861
|
-
}
|
|
7862
|
-
//#endregion
|
|
7863
7218
|
//#region src/cli/platform-lifecycle.ts
|
|
7864
7219
|
function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
7865
7220
|
const explicit = process.env.VOID_PLATFORM_DATABASE_URL || process.env.DATABASE_URL || "";
|
|
@@ -7876,14 +7231,14 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
|
7876
7231
|
}
|
|
7877
7232
|
async function runPlatformLifecycleCommand(commandArgs) {
|
|
7878
7233
|
if (commandArgs.subcommand === "domain") {
|
|
7879
|
-
const { runPlatformDomainCommand } = await import("./platform-domain-
|
|
7234
|
+
const { runPlatformDomainCommand } = await import("./platform-domain-zLXnW1G_.mjs");
|
|
7880
7235
|
await runPlatformDomainCommand(commandArgs);
|
|
7881
7236
|
return;
|
|
7882
7237
|
}
|
|
7883
7238
|
if (commandArgs.subcommand === "discover") {
|
|
7884
7239
|
const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
|
|
7885
7240
|
const accountId = await selectAccount(await readAccounts(client), commandArgs.account);
|
|
7886
|
-
const { recoverPlatformInstallations } = await import("./platform-recovery-
|
|
7241
|
+
const { recoverPlatformInstallations } = await import("./platform-recovery-DeucI7A9.mjs");
|
|
7887
7242
|
const recovered = await recoverPlatformInstallations(client, accountId, void 0, commandArgs.selector);
|
|
7888
7243
|
if (recovered.length === 0) {
|
|
7889
7244
|
log.info(`No recoverable Void platform installations were found in ${accountId}.`);
|
|
@@ -7894,7 +7249,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
7894
7249
|
return;
|
|
7895
7250
|
}
|
|
7896
7251
|
if (commandArgs.subcommand !== "install") {
|
|
7897
|
-
const { runInstalledPlatformCommand } = await import("./platform-management-
|
|
7252
|
+
const { runInstalledPlatformCommand } = await import("./platform-management-CYrLvTGH.mjs");
|
|
7898
7253
|
await runInstalledPlatformCommand(commandArgs);
|
|
7899
7254
|
return;
|
|
7900
7255
|
}
|
|
@@ -8067,7 +7422,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
8067
7422
|
progress?.stop("Read the saved installation.");
|
|
8068
7423
|
progress = void 0;
|
|
8069
7424
|
if (authoritative) {
|
|
8070
|
-
const { selectAuthoritativePlatformManifest } = await import("./platform-management-
|
|
7425
|
+
const { selectAuthoritativePlatformManifest } = await import("./platform-management-CYrLvTGH.mjs");
|
|
8071
7426
|
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
8072
7427
|
authoritativeProvisionIntents = true;
|
|
8073
7428
|
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
@@ -8123,7 +7478,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
8123
7478
|
});
|
|
8124
7479
|
if (isCancel(choice)) throw new Error("platform: Cancelled.");
|
|
8125
7480
|
if (choice === "setup") {
|
|
8126
|
-
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
|
|
7481
|
+
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: ${formatUrl("https://developers.cloudflare.com/hyperdrive/get-started/")}`, import_picocolors.default.bold("Set up Hyperdrive first"));
|
|
8127
7482
|
outro("No Cloudflare or local installation state was changed.");
|
|
8128
7483
|
return;
|
|
8129
7484
|
}
|
|
@@ -8472,7 +7827,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
8472
7827
|
})).actions.filter((action) => action.kind === "worker"),
|
|
8473
7828
|
revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
|
|
8474
7829
|
verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
|
|
8475
|
-
const { verifyPlatformInstallationOwnership } = await import("./platform-management-
|
|
7830
|
+
const { verifyPlatformInstallationOwnership } = await import("./platform-management-CYrLvTGH.mjs");
|
|
8476
7831
|
const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
|
|
8477
7832
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
8478
7833
|
return action.action === "create" && previous && previous.name !== action.name;
|
|
@@ -8496,11 +7851,11 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
8496
7851
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
8497
7852
|
return;
|
|
8498
7853
|
}
|
|
8499
|
-
log.info(`API URL: ${plan.apiUrl}`);
|
|
7854
|
+
log.info(`API URL: ${formatUrl(plan.apiUrl)}`);
|
|
8500
7855
|
if (result.emailOperationId) log.info(`Shared email routing ready. Operation: ${result.emailOperationId}.`);
|
|
8501
7856
|
const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
8502
7857
|
if (result.setupPending && completedAuthCode) {
|
|
8503
|
-
log.info(`First administrator setup: ${plan.apiUrl}/setup`);
|
|
7858
|
+
log.info(`First administrator setup: ${formatUrl(`${plan.apiUrl}/setup`)}`);
|
|
8504
7859
|
console.log(`One-time administrator setup code: ${completedAuthCode}`);
|
|
8505
7860
|
log.info("Confirm your identity in the browser, then test and enable any additional login methods in Settings.");
|
|
8506
7861
|
}
|
|
@@ -8542,7 +7897,7 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
|
|
|
8542
7897
|
deploymentExecution: preview.actions.some((entry) => entry.key === "deployment-queue")
|
|
8543
7898
|
});
|
|
8544
7899
|
} catch (error) {
|
|
8545
|
-
if (
|
|
7900
|
+
if (isPlatformTokenAuthorizationError(error, "/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 });
|
|
8546
7901
|
throw error;
|
|
8547
7902
|
}
|
|
8548
7903
|
assertPlatformInstallPlanUnchanged(preview, verified, "AI Gateway access");
|
|
@@ -8613,10 +7968,10 @@ async function promptSecret(message, masked = true, validate, placeholder) {
|
|
|
8613
7968
|
message,
|
|
8614
7969
|
validate: (input) => input ? validate?.(input) : "A value is required."
|
|
8615
7970
|
};
|
|
8616
|
-
const value = masked ?
|
|
7971
|
+
const value = masked ? await password({
|
|
8617
7972
|
...options,
|
|
8618
7973
|
placeholder
|
|
8619
|
-
}) : await
|
|
7974
|
+
}) : await text$1({
|
|
8620
7975
|
...options,
|
|
8621
7976
|
placeholder
|
|
8622
7977
|
});
|
|
@@ -8624,4 +7979,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
|
|
|
8624
7979
|
return value;
|
|
8625
7980
|
}
|
|
8626
7981
|
//#endregion
|
|
8627
|
-
export {
|
|
7982
|
+
export { revalidatePlatformInstallAction as A, hyperdriveOriginConfigHash as B, formatPlatformInstallPlan as C, platformAdoptionIntentHash as D, matchesRecordedR2Fingerprint as E, checkpointPlatformAccessResources as F, hyperdriveOriginMatches as H, parsePlatformAccessSetupSpec as I, preparePlatformAccessSetup as L, assertPlatformOperatorCredentialCanApplyPlan as M, resolvePlatformCloudflareCredential as N, r2BucketConfigHash as O, verifyPlatformCloudflareCredential as P, PlatformCloudflareApiError as R, dnsRecordConfigHash as S, legacyResourceConfigHash as T, verifyPlatformRuntimeTokenPermissions as U, hyperdriveOriginFromConnectionString as V, resolveBootstrapGitHubId as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, recordPlatformWorkerTraffic as b, acquirePlatformDatabaseLifecycleLockConnection as c, readPlatformProjectZeroTrustState as d, assertProjectZeroTrustAllowsUninstall as f, createPlatformInstallationSecrets as g, createPlatformHealthProbeToken as h, verifyPlatformAiGatewayPlan as i, platformDatabaseNameFromOwnerUrl as j, resourceMarkerProblem as k, platformDatabaseClientUrl as l, loadPlatformRuntime as m, resolvePlatformInstallDatabaseUrl as n, installPlatform as o, readPlatformDatabase as p, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLock as s, assertNoninteractivePlatformMutationAcknowledged as t, readPlatformProjectSlugs as u, recordResource as v, hasProvisionIntentStep as w, createPlatformInstallPlan as x, platformTrafficAttachmentWorker as y, PlatformCloudflareClient as z };
|