void 0.27.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{account-cmd-l-FM7Ioq.mjs → account-cmd-BTcUzv9L.mjs} +6 -6
- package/dist/{agents-B_pX_HmQ.mjs → agents-CuNtT0D_.mjs} +5 -2
- package/dist/{application-analysis-DKy02UY0.mjs → application-analysis-AB_YYWgH.mjs} +118 -135
- package/dist/{application-routing-DQyUh_5S.mjs → application-routing-L7V-TFhZ.mjs} +1 -2
- package/dist/atomic-write-Dq6R0BPN.mjs +18 -0
- package/dist/{auth-Dn2vw-SG.mjs → auth-B6iOCjKI.mjs} +23 -56
- package/dist/auth-Cbqi1iVX.mjs +1 -0
- package/dist/{auth-link-vz9Z2kmG.mjs → auth-link-DUJuvRx5.mjs} +11 -11
- package/dist/{auth-migrations-BwHgeEsD.mjs → auth-migrations-Cgs0k6lS.mjs} +28 -5
- package/dist/{auth-router-DvsCUVw4.mjs → auth-router-CIxiMG7i.mjs} +5 -5
- 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-D6we0VjO.mjs → build-cmd-BkIv25PA.mjs} +6 -6
- package/dist/{cache-CZRdzKx1.mjs → cache-Ctzfig-W.mjs} +6 -6
- package/dist/{cancel-deploy-CQ3NnMvf.mjs → cancel-deploy-CD9o-SsC.mjs} +5 -5
- package/dist/{cf-access-TWNzXltp.mjs → cf-access-BM9G0lhO.mjs} +4 -3
- package/dist/{cf-access-BcIBSKqG.mjs → cf-access-CWP4avfP.mjs} +3 -7
- package/dist/cf-build-output-BU3hqdHu.mjs +1 -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 +199 -312
- package/dist/cli/cloudflare-operation-process.mjs +41 -15
- package/dist/cli/env-schema-probe.d.mts +7 -44
- package/dist/cli/env-schema-probe.mjs +14 -80
- package/dist/client-B60PwCC4.mjs +1 -0
- package/dist/{client-DlDJuY0s.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-BIAF_BIY.mjs} +7 -6
- package/dist/{cloudflare-config-BmhBv3HV.mjs → cloudflare-config-DV60XFFK.mjs} +16 -28
- package/dist/{cloudflare-connect-2G3rL2rP.mjs → cloudflare-connect-WjwnVOJA.mjs} +4 -4
- package/dist/{cloudflare-operations-CwI1bChl.mjs → cloudflare-operations-B42EZeJy.mjs} +26 -34
- package/dist/cloudflare-operations-CrSUFRzJ.mjs +1 -0
- package/dist/{cloudflare-process-2-TfIEzB.mjs → cloudflare-process-CeDWJRhj.mjs} +12 -7
- package/dist/collect-Cae8A7Zr.mjs +1 -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-DPByhVck.mjs → config-D0GY5ItR.mjs} +1 -1
- package/dist/config-QrJWu0IX.mjs +1 -0
- package/dist/config-entry.d.mts +1 -1
- package/dist/{connect-BfPBHHm-.mjs → connect-CKtk-EZe.mjs} +9 -9
- package/dist/create-project-B-ewF_Af.mjs +1 -0
- package/dist/{create-project-BdXEFKal.mjs → create-project-yInGo72M.mjs} +8 -8
- package/dist/database-provider.mjs +1 -2
- package/dist/{db-Xs80hcaA.mjs → db-7I_gfdVq.mjs} +177 -268
- package/dist/db-types-Bb4WBWIs.mjs +49 -0
- package/dist/{delete-d2vuou3j.mjs → delete-Cy-M1u-r.mjs} +6 -6
- package/dist/deploy-9lgfhrXS.mjs +2 -0
- package/dist/deploy-BYYlnsiy.mjs +6164 -0
- package/dist/deploy-managed-static-Ca4TFdmx.mjs +1595 -0
- package/dist/deploy-transport-DWlWBe6M.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-ClzFSwns.mjs +1 -0
- package/dist/{discover-CkCYv9T_.mjs → discover-D2PGT63u.mjs} +2 -2
- package/dist/dispatch-DrTTk8Xg.mjs +431 -0
- package/dist/{dist-D3m88sjf.mjs → dist-BVdsO7Py.mjs} +5 -6
- package/dist/dist-CfgIkieS.mjs +1 -0
- package/dist/{dist-BVqXeR_h.mjs → dist-ipQi0Q8y.mjs} +2 -3
- package/dist/{domain-BsDUBAvt.mjs → domain-CZGxPv9u.mjs} +8 -23
- package/dist/{dotenv-BfU6eqFt.mjs → dotenv-CNazY3-w.mjs} +4 -16
- package/dist/edge.mjs +1 -2
- package/dist/{email-d_Aux1vM.mjs → email-BcjSyAL0.mjs} +4 -20
- package/dist/{email-_lGAHOhp.mjs → email-DzyNVuoU.mjs} +100 -192
- package/dist/email-cloudflare-DfG6UEMm.mjs +1096 -0
- package/dist/email-protocol-D9nTvhCp.mjs +82 -0
- package/dist/{entry-CMAkqBmU.mjs → entry-ehmBvs5w.mjs} +2 -2
- package/dist/{env-DMA3cXZt.mjs → env-0jrMQJI_.mjs} +6 -14
- package/dist/{env-BxocywBB.mjs → env-BJjULvL0.mjs} +2 -2
- package/dist/{env-C74QepDE.mjs → env-Nx3yf50r.mjs} +9 -9
- package/dist/{env-public-DR7ZM6d-.d.mts → env-public-CCdykygU.d.mts} +6 -23
- package/dist/{env-raw-jZpP5n47.mjs → env-raw-D74P4ebM.mjs} +15 -21
- package/dist/{env-types-CwXgeMgL.mjs → env-types-DS2_y4X8.mjs} +2 -2
- package/dist/env-validation-BT7z-qpQ.mjs +1 -0
- package/dist/env-validation-DYxjz_Wo.mjs +488 -0
- package/dist/{gen-DtkQhvQg.mjs → gen-CufHEf42.mjs} +132 -254
- package/dist/gen-sXyp5DFO.mjs +1 -0
- package/dist/generate-ClI89brV.mjs +1 -0
- package/dist/{generate-GDbL7sUr.mjs → generate-Dat0UTry.mjs} +38 -4
- package/dist/generated-wrangler-config-DeWj5F9Y.mjs +18 -0
- package/dist/{github-cmd-DaDdBbSS.mjs → github-cmd-FU8EjFyU.mjs} +28 -72
- 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-0bTe_lID.mjs → help-BT2hovNI.mjs} +26 -14
- package/dist/help-i2YGGnTH.mjs +1 -0
- package/dist/{inbound-PfQDLXc5.mjs → inbound-BuTfm11U.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 +714 -762
- package/dist/{info-PYWwkWyt.mjs → info-l249eUdH.mjs} +5 -5
- package/dist/{init-DaBGmaXC.mjs → init-D0A6M64Q.mjs} +229 -392
- package/dist/isr-config-fZaJJ8t7.mjs +165 -0
- package/dist/{link-C6fjafep.mjs → link-WMoJFf4i.mjs} +9 -9
- package/dist/{list-BTFBlqKl.mjs → list-CQ2EQEHL.mjs} +7 -18
- package/dist/{live-CgQuTpqD.mjs → live-BcHoTDFe.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-BxvQYEFi.mjs} +6 -6
- package/dist/{login-GKcNUmtI.mjs → login-C2T1oVG-.mjs} +12 -12
- package/dist/login-XD844Xs-.mjs +1 -0
- package/dist/{logs-BiMSULmc.mjs → logs-DnC1d7xB.mjs} +6 -6
- package/dist/{migrate-NLsjBbuM.mjs → migrate-BDIprOYd.mjs} +70 -22
- package/dist/migrate-BRwU-blY.mjs +1 -0
- 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/mysql-migration-sql-BIS5vbNk.mjs +241 -0
- package/dist/nitro.d.mts +30 -0
- package/dist/nitro.mjs +88 -0
- package/dist/{node-DJicPvPj.mjs → node-cPadMgUO.mjs} +9 -25
- package/dist/{operator-args-DOBhIaIg.mjs → operator-args-UnE8Y2eK.mjs} +27 -26
- package/dist/{operator-auth-BEZ_u0Cz.mjs → operator-auth-Cr2zCc-J.mjs} +2 -2
- package/dist/{operator-client-DbDv4vtE.mjs → operator-client-C-ROjk1I.mjs} +4 -4
- package/dist/operator-cmd-D6gnZsnK.mjs +1 -0
- package/dist/{operator-cmd-BT4hgavL.mjs → operator-cmd-DHieWtFp.mjs} +8 -8
- package/dist/{output-C50ag2r3.mjs → output-Ba6ZbrzY.mjs} +37 -6
- 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/head.mjs +1 -2
- 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/prefetch.mjs +1 -2
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +1 -1
- package/dist/pages/serialize.mjs +1 -2
- package/dist/plan-CKX_jWnD.mjs +1 -0
- package/dist/platform-args-B0HeTh0C.mjs +1 -0
- package/dist/{platform-args-DUeXY9Om.mjs → platform-args-CaCL_1Ch.mjs} +43 -60
- package/dist/{platform-auth-config-CkSx3usH.mjs → platform-auth-config-Z16-I8nT.mjs} +11 -12
- package/dist/{platform-auth-protection-BJimB_p9.mjs → platform-auth-protection-BXwZiJSF.mjs} +37 -27
- package/dist/{platform-auth-recovery-BU6izFAU.mjs → platform-auth-recovery-CXjfqyOv.mjs} +9 -10
- package/dist/{platform-cmd-BbY1dWiM.mjs → platform-cmd-DaAOiWTl.mjs} +16 -16
- package/dist/platform-cmd-PT4YwMLM.mjs +1 -0
- package/dist/{platform-discovery-DIYz8V63.mjs → platform-discovery-CHhNs8Nn.mjs} +1 -1
- package/dist/{platform-domain-CHVS6cV0.mjs → platform-domain-CxpvUeo0.mjs} +6 -6
- package/dist/{platform-lifecycle-DzCwqhFq.mjs → platform-lifecycle-DBiS0cJd.mjs} +1693 -2120
- package/dist/platform-lifecycle-u9pMOvxC.mjs +1 -0
- package/dist/{platform-management-CuuVDhyt.mjs → platform-management-DhEFdQAf.mjs} +696 -130
- package/dist/platform-management-DvBS83rC.mjs +1 -0
- package/dist/{platform-plans-config-CN-heS4q.mjs → platform-plans-config-BWn1S2Rv.mjs} +16 -12
- package/dist/{platform-recovery-CskSJgLh.mjs → platform-recovery-BUGOB4RC.mjs} +6 -9
- package/dist/{platform-registry-Cjv2Qf0r.mjs → platform-registry-B9mWX9LO.mjs} +13 -18
- package/dist/{platform-setup-BBxRxmFC.mjs → platform-setup-YvN9VQXV.mjs} +21 -28
- package/dist/{plugin-inference-BlQbOL0P.mjs → plugin-inference-DsARdQKE.mjs} +18 -33
- package/dist/prepare-CYT6tAzD.mjs +1 -0
- package/dist/{prepare-Do96654b.mjs → prepare-DEB5SeVJ.mjs} +12 -13
- package/dist/{prepare-Dq9hTIbo.mjs → prepare-PQCgsny0.mjs} +4 -4
- 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 +1 -2
- package/dist/{project-cmd-DaoRrn-8.mjs → project-cmd-DVatD8Ev.mjs} +20 -20
- package/dist/{project-paths-GgcMozYz.mjs → project-paths-DMvEeSau.mjs} +9 -14
- package/dist/{project-BAm2aB6Q.mjs → project-rqDnVsJN.mjs} +8 -4
- package/dist/project-slug-6cyz8E7k.mjs +1 -0
- package/dist/{project-team-Q_lCA7Io.mjs → project-team-DJrWEAG1.mjs} +7 -7
- package/dist/{project-token-BArgK9o1.mjs → project-token-CBgLBuL6.mjs} +6 -6
- package/dist/{project-tsconfig-CVJhNMxs.mjs → project-tsconfig-bveqhlYA.mjs} +3 -5
- package/dist/project-zdB3fs9G.mjs +1 -0
- package/dist/{project-zero-trust-KO9Hu2Pq.mjs → project-zero-trust-92gLtZ25.mjs} +6 -6
- 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-BnSSkiED.mjs} +119 -866
- package/dist/{requests-C7Oeoh5u.mjs → requests-7W1Pygt2.mjs} +5 -5
- package/dist/resolve-project-PnhzMtY6.mjs +1 -0
- package/dist/resolve-project-WpwkCB_c.mjs +48 -0
- package/dist/{rollback-DVAtrLAl.mjs → rollback-BcASVnwP.mjs} +8 -18
- package/dist/{route-types-CbtsIeFn.mjs → route-types-BZi26DVS.mjs} +9 -91
- package/dist/routing-assets-VkrKIbxE.mjs +139 -0
- package/dist/{runner-DMBKSrRk.mjs → runner-CabsQ-iM.mjs} +22 -34
- package/dist/runner-So9mYfcm.mjs +1 -0
- package/dist/runner-mysql-BhRaONkO.mjs +146 -0
- package/dist/runner-mysql-DWwE8jKi.mjs +1 -0
- package/dist/{runner-pg-DV8p_zjA.mjs → runner-pg-BRemq_GX.mjs} +3 -3
- package/dist/runner-pg-_Xncr3H5.mjs +1 -0
- package/dist/runtime/ai.mjs +23 -25
- package/dist/runtime/auth-client-react.mjs +1 -2
- package/dist/runtime/auth-client-solid.mjs +1 -2
- package/dist/runtime/auth-client-svelte.mjs +1 -2
- package/dist/runtime/auth-client-vue.mjs +1 -2
- package/dist/runtime/auth-client.mjs +1 -2
- package/dist/runtime/auth.mjs +1 -2
- package/dist/runtime/better-auth-mysql.d.mts +2 -5
- package/dist/runtime/better-auth-mysql.mjs +9 -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 +6 -2
- package/dist/runtime/durable.d.mts +18 -1
- 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 +1 -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 -2
- 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/limits.mjs +1 -2
- package/dist/runtime/live-client.d.mts +3 -2
- package/dist/runtime/live-server.d.mts +2 -1
- package/dist/runtime/live-server.mjs +1 -16
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/live.mjs +1 -2
- package/dist/runtime/log.d.mts +2 -1
- package/dist/runtime/migration-handler-mysql.d.mts +2 -2
- package/dist/runtime/migration-handler-mysql.mjs +39 -8
- 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 +1 -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/response.mjs +1 -2
- package/dist/runtime/route-url.mjs +1 -2
- package/dist/runtime/routing.d.mts +3 -5
- package/dist/runtime/routing.mjs +82 -218
- package/dist/runtime/sandbox-container.d.mts +2 -2
- package/dist/runtime/sandbox-container.mjs +1 -2
- package/dist/runtime/sandbox.d.mts +2 -2
- package/dist/runtime/sandbox.mjs +13 -6
- package/dist/runtime/seed.d.mts +2 -1
- package/dist/runtime/sse.mjs +1 -2
- package/dist/runtime/storage.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/validator.mjs +1 -2
- package/dist/runtime/ws-server.d.mts +4 -4
- 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-a9LRUtii.mjs} +13 -2
- 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-BvtB7IjY.mjs → secret-bg73f1Ge.mjs} +63 -81
- package/dist/{serialize-BPvnNQuA.mjs → serialize-BtQyaYoC.mjs} +10 -4
- package/dist/{skills-BgVMvQdy.mjs → skills-C_yRTg0g.mjs} +3 -3
- package/dist/{subcommand-prompt-BSlnD1fI.mjs → subcommand-prompt-Bm3ZlYgA.mjs} +39 -44
- package/dist/sveltekit.d.mts +2 -1
- package/dist/sveltekit.mjs +1 -1
- package/dist/validate-BQFdf0Pi.mjs +1 -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/wrapper-CwhpNWtH.mjs +663 -0
- package/dist/{ws-CChrrAU-.d.mts → ws-aKVYB-fA.d.mts} +1 -1
- package/dist/yarn-pnp-DLbEFMT0.mjs +92 -0
- package/docs/guide/ai.md +8 -2
- package/docs/guide/auth.md +2 -0
- package/docs/guide/database/mysql.md +14 -3
- package/docs/guide/database/postgresql.md +2 -0
- package/docs/guide/database.md +3 -5
- package/docs/guide/durable-state.md +32 -4
- package/docs/guide/platform/administration/access.md +4 -0
- package/docs/guide/platform/administration/connected-apps.md +55 -0
- package/docs/guide/platform/installation/credentials.md +21 -3
- package/docs/guide/platform/installation/domains.md +1 -1
- package/docs/guide/platform/installation/maintenance.md +38 -1
- package/docs/guide/platform/installation/prerequisites.md +6 -2
- package/docs/guide/platform/installation/setup.md +23 -1
- package/docs/guide/platform/installation/uninstall.md +6 -1
- package/docs/guide/quickstart.md +2 -0
- package/docs/guide/sandboxes.md +3 -1
- package/docs/guide/self-hosted-platform.md +1 -1
- package/docs/integrations/cloudflare.md +2 -2
- package/docs/integrations/frameworks/analog.md +3 -11
- package/docs/integrations/frameworks/nuxt.md +7 -10
- package/docs/integrations/nodejs-bun-deno.md +2 -0
- package/docs/reference/api/deploy.md +23 -0
- package/docs/reference/api/pages.md +2 -0
- package/docs/reference/api.md +3 -1
- package/docs/reference/cli/database.md +9 -3
- package/docs/reference/cli/platform-installation.md +19 -7
- package/docs/reference/cli/setup.md +2 -0
- package/docs/reference/config.md +2 -0
- package/package.json +39 -36
- package/sandbox.Dockerfile +1 -1
- package/skills/void/SKILL.md +2 -1
- package/dist/auth-LMJcNESe.mjs +0 -2
- package/dist/cf-build-output-H9a97vME.mjs +0 -2
- package/dist/cf-user-agent-lKblH3zU.mjs +0 -17
- package/dist/client-tso2OfcG.mjs +0 -2
- package/dist/cloudflare-operations-BlYLlowZ.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/create-project-qFDC45zg.mjs +0 -2
- package/dist/deploy-Bqe1PYBw.mjs +0 -10458
- package/dist/deploy-VsWsVQkw.mjs +0 -2
- package/dist/dev-inbox-CK-UuPob.mjs +0 -297
- package/dist/discover-EDUf6JJw.mjs +0 -2
- package/dist/dist-Bx1z6Eva.mjs +0 -2
- package/dist/dist-_UUlL6bg.mjs +0 -2
- package/dist/env-validation-CrVNjsPm.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-DfQYVfGY.mjs +0 -2
- package/dist/login-mx_ydhmp.mjs +0 -2
- package/dist/migrate-BHp07yJE.mjs +0 -2
- package/dist/operator-cmd-CwBB-1yb.mjs +0 -2
- package/dist/pages/head-client.d.mts +0 -13
- package/dist/pages/head-client.mjs +0 -2
- package/dist/plan-CH2G51je.mjs +0 -2
- package/dist/platform-args-D4E1PjVw.mjs +0 -2
- package/dist/platform-cmd-BbmJiq5O.mjs +0 -2
- package/dist/platform-lifecycle-ByAlpkhV.mjs +0 -2
- package/dist/platform-management-CBOT4BSM.mjs +0 -2
- package/dist/prepare-DRUV-g3X.mjs +0 -2
- package/dist/project-DedqCp59.mjs +0 -2
- package/dist/project-slug-DofjTFd-.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-9wjsSZx-.mjs +0 -2
- package/dist/runner-mysql-CEzKtmFb.mjs +0 -54
- 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,9 +1,597 @@
|
|
|
1
|
-
import { _ as removePlatformConnectionForInstallation, g as removeInstallationRecoverySecrets, m as readInstallationRecoverySecrets, o as listInstallationManifests, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest } from "./platform-registry-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { S as writePendingInstallationSecrets, _ as removePlatformConnectionForInstallation, g as removeInstallationRecoverySecrets, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, o as listInstallationManifests, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest } from "./platform-registry-B9mWX9LO.mjs";
|
|
2
|
+
import { t as ensurePlatformAccessToken } from "./cf-access-CWP4avfP.mjs";
|
|
3
|
+
import { A as confirm, H as outro, K as select, L as log, tt as isCancel } from "./dist-BVdsO7Py.mjs";
|
|
4
|
+
import { f as import_picocolors, l as formatUrl, m as printSuccess, p as note, r as createSpinner } from "./output-Ba6ZbrzY.mjs";
|
|
5
|
+
import { A as matchesRecordedR2Fingerprint, B as activatePlatformAccessCredentials, C as platformTrafficAttachmentWorker, D as formatPlatformInstallPlan, E as dnsRecordConfigHash, F as platformDatabaseNameFromOwnerUrl, G as PlatformCloudflareClient, I as turnOffInstalledProjectZeroTrust, J as hyperdriveOriginMatches, K as hyperdriveOriginConfigHash, L as assertPlatformOperatorCredentialCanApplyPlan, M as r2BucketConfigHash, N as resourceMarkerProblem, O as hasProvisionIntentStep, P as revalidatePlatformInstallAction, R as resolvePlatformCloudflareCredential, T as createPlatformInstallPlan, W as PlatformCloudflareApiError, Y as verifyPlatformRuntimeTokenPermissions, _ as readPlatformDatabase, a as authenticationSetupRequestHash, b as createPlatformInstallationSecrets, c as REMOTE_INSTALLATION_MANIFEST_KEY, g as formatZeroTrustApplications, h as assertProjectZeroTrustAllowsUninstall, j as platformAdoptionIntentHash, k as legacyResourceConfigHash, l as installPlatform, m as readPlatformProjectZeroTrustState, n as resolvePlatformInstallDatabaseUrl, o as readLoginReplacementFile, p as readPlatformProjectSlugs, q as hyperdriveOriginFromConnectionString, s as replacePendingInstallationAuthentication, t as assertNoninteractivePlatformMutationAcknowledged, u as acquirePlatformDatabaseLifecycleLock, v as loadPlatformRuntime, x as resolveBootstrapGitHubId, y as createPlatformHealthProbeToken, z as verifyPlatformCloudflareCredential } from "./platform-lifecycle-DBiS0cJd.mjs";
|
|
6
|
+
import { PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_R2_OWNERSHIP_MARKER_KEY, parsePlatformInstallationManifest, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, transitionInstallation } from "@void/platform";
|
|
7
|
+
import { setTimeout } from "node:timers/promises";
|
|
8
|
+
//#region src/cli/platform-teardown.ts
|
|
9
|
+
/**
|
|
10
|
+
* `void platform uninstall --purge-resources` deletes every Cloudflare resource the
|
|
11
|
+
* installation created, in an order Cloudflare accepts:
|
|
12
|
+
*
|
|
13
|
+
* traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
|
|
14
|
+
* -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
|
|
15
|
+
* -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
|
|
16
|
+
*
|
|
17
|
+
* Each resource is proven to belong to this installation right before its
|
|
18
|
+
* DELETE, and Cloudflare refuses a resource that something outside the
|
|
19
|
+
* installation still uses. The only forced DELETE is for a platform Worker
|
|
20
|
+
* that Cloudflare refused while every Worker it names as a dependent is one
|
|
21
|
+
* this installation created: the API and proxy Workers bind to each other, so
|
|
22
|
+
* neither can go first without it. A route, custom domain, DNS
|
|
23
|
+
* record, or Access resource that no longer matches what was recorded is kept
|
|
24
|
+
* and listed with the reason instead of stopping the run. Every finished
|
|
25
|
+
* deletion is checkpointed, so rerunning the command continues where it
|
|
26
|
+
* stopped. The routing KV namespace holds that checkpoint, so uninstall
|
|
27
|
+
* deletes it last.
|
|
28
|
+
*
|
|
29
|
+
* An uninstall without --purge-resources records what it leaves as retained. If it
|
|
30
|
+
* stopped before it finished, --purge-resources considers those resources again.
|
|
31
|
+
*/
|
|
32
|
+
const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
|
|
33
|
+
const WORKER_ORDER = [
|
|
34
|
+
"email-gateway-worker",
|
|
35
|
+
"proxy-worker",
|
|
36
|
+
"dispatch-worker",
|
|
37
|
+
"api-worker"
|
|
38
|
+
];
|
|
39
|
+
const PERMISSIONS = {
|
|
40
|
+
access: "Account → Access: Apps and Policies → Edit",
|
|
41
|
+
aiGateway: "Account → AI Gateway → Edit",
|
|
42
|
+
dns: "Zone → DNS → Edit",
|
|
43
|
+
emailRouting: "Zone → Email Routing Rules → Edit",
|
|
44
|
+
hyperdrive: "Account → Hyperdrive → Edit",
|
|
45
|
+
kv: "Account → Workers KV Storage → Edit",
|
|
46
|
+
queues: "Account → Queues → Edit",
|
|
47
|
+
r2: "Account → Workers R2 Storage → Edit",
|
|
48
|
+
routes: "Zone → Workers Routes → Edit",
|
|
49
|
+
serviceTokens: "Account → Access: Service Tokens → Edit",
|
|
50
|
+
workers: "Account → Workers Scripts → Edit"
|
|
51
|
+
};
|
|
52
|
+
function isPlatformTailWorker(manifest, resource) {
|
|
53
|
+
return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
|
|
54
|
+
}
|
|
55
|
+
function purgeResourcesResumeCommand(manifest) {
|
|
56
|
+
return `void platform uninstall ${manifest.id} --purge-resources`;
|
|
57
|
+
}
|
|
58
|
+
function accessStepKey(access) {
|
|
59
|
+
return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
|
|
60
|
+
}
|
|
61
|
+
/** Access resources that --purge-resources already deleted. */
|
|
62
|
+
function isPurgedAccessResource(manifest, access) {
|
|
63
|
+
return manifest.steps.some((step) => step.key === accessStepKey(access));
|
|
64
|
+
}
|
|
65
|
+
function hasStep(manifest, key) {
|
|
66
|
+
return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
|
|
67
|
+
}
|
|
68
|
+
function keptStepKey(key) {
|
|
69
|
+
return `kept:${key}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Why --purge-resources kept a resource it was going to delete, for example because
|
|
73
|
+
* a route now serves another Worker; null when it did not keep it. Access
|
|
74
|
+
* resources use the key `access:<key>`.
|
|
75
|
+
*/
|
|
76
|
+
function purgeResourcesKeptReason(manifest, key) {
|
|
77
|
+
return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
|
|
78
|
+
}
|
|
79
|
+
/** An uninstall that started has not finished, so what it kept can be reconsidered. */
|
|
80
|
+
function isReopenable(manifest) {
|
|
81
|
+
return manifest.status !== "uninstalled";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A resource the installation created that --purge-resources may still delete: one
|
|
85
|
+
* that is ready, or one that an uninstall which stopped before it finished
|
|
86
|
+
* recorded as retained. Resources --purge-resources itself kept, and earlier
|
|
87
|
+
* versions of a resource kept when it was replaced, are never reconsidered.
|
|
88
|
+
*/
|
|
89
|
+
function isPurgeResourcesCandidate(manifest, resource) {
|
|
90
|
+
return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
|
|
91
|
+
}
|
|
92
|
+
/** The zone that routes and DNS records live in, if it can still be used. */
|
|
93
|
+
function usableZone(manifest) {
|
|
94
|
+
return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A recorded resource whose live configuration no longer matches the
|
|
98
|
+
* installation's record. --purge-resources keeps such a DNS record and continues.
|
|
99
|
+
*/
|
|
100
|
+
var PlatformResourceChangedError = class extends Error {
|
|
101
|
+
resourceKey;
|
|
102
|
+
reason;
|
|
103
|
+
constructor(resourceKey, reason, message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.resourceKey = resourceKey;
|
|
106
|
+
this.reason = reason;
|
|
107
|
+
this.name = "PlatformResourceChangedError";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/** The zone whose Email Routing rules may send mail to the gateway Worker. */
|
|
111
|
+
function emailRoutingZone(manifest) {
|
|
112
|
+
return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
|
|
113
|
+
}
|
|
114
|
+
/** Everything --purge-resources will delete, in deletion order. */
|
|
115
|
+
function purgeResourcesTargets(manifest) {
|
|
116
|
+
const resources = manifest.resources;
|
|
117
|
+
const routing = resources.find((resource) => resource.key === "routing-kv");
|
|
118
|
+
const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
|
|
119
|
+
if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
|
|
120
|
+
const zone = usableZone(manifest);
|
|
121
|
+
const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
|
|
122
|
+
const targets = [];
|
|
123
|
+
for (const resource of resources) {
|
|
124
|
+
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;
|
|
125
|
+
const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
|
|
126
|
+
const worker = workers.find((entry) => entry.name === name);
|
|
127
|
+
if (worker) targets.push({
|
|
128
|
+
phase: "traffic",
|
|
129
|
+
resource,
|
|
130
|
+
worker
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
|
|
134
|
+
phase: "dns",
|
|
135
|
+
resource
|
|
136
|
+
});
|
|
137
|
+
const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
|
|
138
|
+
for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
|
|
139
|
+
phase: "access",
|
|
140
|
+
access: entry
|
|
141
|
+
});
|
|
142
|
+
const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
|
|
143
|
+
if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
|
|
144
|
+
phase: "email",
|
|
145
|
+
resource: gateway
|
|
146
|
+
});
|
|
147
|
+
const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
|
|
148
|
+
for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
|
|
149
|
+
phase: "queue-consumers",
|
|
150
|
+
resource
|
|
151
|
+
});
|
|
152
|
+
const rank = (resource) => {
|
|
153
|
+
const index = WORKER_ORDER.indexOf(resource.key);
|
|
154
|
+
return index < 0 ? WORKER_ORDER.length : index;
|
|
155
|
+
};
|
|
156
|
+
for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
|
|
157
|
+
phase: "workers",
|
|
158
|
+
resource
|
|
159
|
+
});
|
|
160
|
+
for (const resource of queues) targets.push({
|
|
161
|
+
phase: "queues",
|
|
162
|
+
resource
|
|
163
|
+
});
|
|
164
|
+
for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
|
|
165
|
+
phase: "dispatch",
|
|
166
|
+
resource
|
|
167
|
+
});
|
|
168
|
+
for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
|
|
169
|
+
phase: "tail",
|
|
170
|
+
resource
|
|
171
|
+
});
|
|
172
|
+
for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
|
|
173
|
+
phase: "data",
|
|
174
|
+
resource
|
|
175
|
+
});
|
|
176
|
+
for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
|
|
177
|
+
phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
|
|
178
|
+
resource
|
|
179
|
+
});
|
|
180
|
+
if (candidate(routing)) targets.push({
|
|
181
|
+
phase: "routing",
|
|
182
|
+
resource: routing
|
|
183
|
+
});
|
|
184
|
+
return targets;
|
|
185
|
+
}
|
|
186
|
+
/** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
|
|
187
|
+
function purgeResourcesHandlesEmailRouting(manifest) {
|
|
188
|
+
return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
|
|
189
|
+
}
|
|
190
|
+
/** Manifest resources that --purge-resources leaves in the account. */
|
|
191
|
+
function purgeResourcesKeptResources(manifest) {
|
|
192
|
+
const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
|
|
193
|
+
return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
|
|
194
|
+
}
|
|
195
|
+
function resourceLabel(resource) {
|
|
196
|
+
return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
|
|
197
|
+
}
|
|
198
|
+
/** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
|
|
199
|
+
function describePurgeResourcesPlan(manifest) {
|
|
200
|
+
const targets = purgeResourcesTargets(manifest);
|
|
201
|
+
const lines = [];
|
|
202
|
+
let step = 0;
|
|
203
|
+
let previous = null;
|
|
204
|
+
const consumerQueues = [];
|
|
205
|
+
for (const target of targets) {
|
|
206
|
+
if (target.phase === "email") {
|
|
207
|
+
step++;
|
|
208
|
+
previous = target.phase;
|
|
209
|
+
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)`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (target.phase !== previous) {
|
|
213
|
+
step++;
|
|
214
|
+
previous = target.phase;
|
|
215
|
+
}
|
|
216
|
+
const prefix = ` ${step}. `;
|
|
217
|
+
if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
|
|
218
|
+
else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
|
|
219
|
+
else if (target.phase === "queue-consumers") {
|
|
220
|
+
consumerQueues.push(target.resource.name);
|
|
221
|
+
if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
|
|
222
|
+
lines.push(` ${target.resource.name}`);
|
|
223
|
+
} else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
|
|
224
|
+
else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
|
|
225
|
+
else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
|
|
226
|
+
else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
|
|
227
|
+
else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
|
|
228
|
+
else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
|
|
229
|
+
else lines.push(`${prefix}${resourceLabel(target.resource)}`);
|
|
230
|
+
}
|
|
231
|
+
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."];
|
|
232
|
+
}
|
|
233
|
+
/** Lines naming what --purge-resources never removes. */
|
|
234
|
+
function purgeResourcesAlwaysKept() {
|
|
235
|
+
return [
|
|
236
|
+
" The external PostgreSQL database and its data.",
|
|
237
|
+
" Analytics Engine datasets; Cloudflare has no API to delete them.",
|
|
238
|
+
" Access policies and identity providers you chose.",
|
|
239
|
+
" API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
/** Token permissions --purge-resources needs for this manifest. */
|
|
243
|
+
function purgeResourcesPermissions(manifest) {
|
|
244
|
+
const targets = purgeResourcesTargets(manifest);
|
|
245
|
+
const needed = /* @__PURE__ */ new Set();
|
|
246
|
+
for (const target of targets) {
|
|
247
|
+
if (target.phase === "access") {
|
|
248
|
+
needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (target.phase === "email") {
|
|
252
|
+
needed.add(PERMISSIONS.emailRouting);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const { kind } = target.resource;
|
|
256
|
+
if (kind === "worker-route") needed.add(PERMISSIONS.routes);
|
|
257
|
+
else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
|
|
258
|
+
else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
|
|
259
|
+
else if (kind === "queue") needed.add(PERMISSIONS.queues);
|
|
260
|
+
else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
|
|
261
|
+
else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
|
|
262
|
+
else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
|
|
263
|
+
else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
|
|
264
|
+
}
|
|
265
|
+
return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
|
|
266
|
+
}
|
|
267
|
+
function deniedStatus(error) {
|
|
268
|
+
return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check resource reads before the first change. Browser login cannot change
|
|
272
|
+
* DNS or Access, so those need an API token. Read probes cannot prove Edit;
|
|
273
|
+
* the plan lists the required permissions.
|
|
274
|
+
*/
|
|
275
|
+
async function checkPurgeResourcesCredentials(client, manifest, credential) {
|
|
276
|
+
const targets = purgeResourcesTargets(manifest);
|
|
277
|
+
const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
|
|
278
|
+
const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
|
|
279
|
+
const has = (kind) => resources.some((resource) => resource.kind === kind);
|
|
280
|
+
const zone = manifest.resources.find((resource) => resource.key === "zone");
|
|
281
|
+
const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
|
|
282
|
+
const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
|
|
283
|
+
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}` : ""}`;
|
|
284
|
+
if (credential.source === "oauth") {
|
|
285
|
+
const browserMissing = [
|
|
286
|
+
...has("dns-record") ? [PERMISSIONS.dns] : [],
|
|
287
|
+
...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
|
|
288
|
+
...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
|
|
289
|
+
...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
|
|
290
|
+
];
|
|
291
|
+
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.`);
|
|
292
|
+
}
|
|
293
|
+
const probes = [];
|
|
294
|
+
if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
|
|
295
|
+
if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
|
|
296
|
+
if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
|
|
297
|
+
for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
|
|
298
|
+
else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
|
|
299
|
+
for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
|
|
300
|
+
if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
|
|
301
|
+
if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
|
|
302
|
+
if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
|
|
303
|
+
if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
|
|
304
|
+
if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
|
|
305
|
+
if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
|
|
306
|
+
const missing = /* @__PURE__ */ new Set();
|
|
307
|
+
for (const [permission, probe] of probes) try {
|
|
308
|
+
await probe();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (!deniedStatus(error)) throw error;
|
|
311
|
+
missing.add(permission);
|
|
312
|
+
}
|
|
313
|
+
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.`);
|
|
314
|
+
}
|
|
315
|
+
const LISTED_PROJECTS = 20;
|
|
316
|
+
/**
|
|
317
|
+
* Projects own Workers, storage, and domains that only the running platform
|
|
318
|
+
* can delete. --purge-resources deletes the platform, so it refuses before any
|
|
319
|
+
* change while a project remains.
|
|
320
|
+
*/
|
|
321
|
+
async function assertNoPlatformProjects(read) {
|
|
322
|
+
const slugs = await readPlatformDatabase(read, (reason) => `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.`);
|
|
323
|
+
if (slugs.length) throw new Error([
|
|
324
|
+
`platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
|
|
325
|
+
"--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
|
|
326
|
+
"Delete each project first with `void project delete <name>`, then retry:",
|
|
327
|
+
...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
|
|
328
|
+
...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
|
|
329
|
+
].join("\n"));
|
|
330
|
+
}
|
|
331
|
+
const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
|
|
332
|
+
/**
|
|
333
|
+
* AI Gateway ids are chosen by the caller and can be reused after a delete,
|
|
334
|
+
* and gateways carry no ownership marker. Only a creation time next to the
|
|
335
|
+
* installer's provision intent shows that this is the gateway it created.
|
|
336
|
+
*/
|
|
337
|
+
function createdDuringInstallation(manifest, resource, createdAt) {
|
|
338
|
+
const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
|
|
339
|
+
if (Number.isNaN(created)) return false;
|
|
340
|
+
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);
|
|
341
|
+
}
|
|
342
|
+
function ownershipRefusal(manifest, description, reason) {
|
|
343
|
+
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)}`);
|
|
344
|
+
}
|
|
345
|
+
function cloudflareRefusal(manifest, description, error, hint) {
|
|
346
|
+
if (!(error instanceof PlatformCloudflareApiError)) return error;
|
|
347
|
+
return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Delete every target in order. Returns the manifest with each deleted
|
|
351
|
+
* resource recorded; routing KV is left for the caller to delete last.
|
|
352
|
+
*/
|
|
353
|
+
async function runPurgeResourcesTeardown(context, initial) {
|
|
354
|
+
const { client } = context;
|
|
355
|
+
let next = initial;
|
|
356
|
+
const accountId = next.account.id;
|
|
357
|
+
const save = async (manifest) => {
|
|
358
|
+
next = manifest;
|
|
359
|
+
await context.checkpoint(next);
|
|
360
|
+
};
|
|
361
|
+
const markDeleted = async (key) => {
|
|
362
|
+
const resource = next.resources.find((entry) => entry.key === key);
|
|
363
|
+
await save(recordInstallationResource(next, {
|
|
364
|
+
...resource,
|
|
365
|
+
state: "deleted"
|
|
366
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
367
|
+
};
|
|
368
|
+
const recordStep = async (key, resultHash = "completed") => {
|
|
369
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
370
|
+
await save(recordInstallationStep(next, {
|
|
371
|
+
completedAt: now,
|
|
372
|
+
key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
|
|
373
|
+
resultHash
|
|
374
|
+
}, now));
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Live proof for these keys. A resource an earlier uninstall recorded as
|
|
378
|
+
* retained is proven like a ready one; only its recorded state differs.
|
|
379
|
+
*/
|
|
380
|
+
const verify = (keys) => context.verify({
|
|
381
|
+
...next,
|
|
382
|
+
resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
|
|
383
|
+
...resource,
|
|
384
|
+
state: "ready"
|
|
385
|
+
} : resource)
|
|
386
|
+
}, keys);
|
|
387
|
+
/**
|
|
388
|
+
* Prove the resource; false when it is already gone (and recorded).
|
|
389
|
+
*
|
|
390
|
+
* Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
|
|
391
|
+
* addressed by name, and a route or custom domain can be changed in place.
|
|
392
|
+
* Something replaced between this proof and the DELETE can still be removed.
|
|
393
|
+
* That one-request window is accepted because --purge-resources is opt-in and its
|
|
394
|
+
* plan lists every resource first. A forced Worker DELETE has the same
|
|
395
|
+
* window between reading its dependents and the DELETE.
|
|
396
|
+
*/
|
|
397
|
+
const prove = async (resource) => {
|
|
398
|
+
if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
|
|
399
|
+
await markDeleted(resource.key);
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
};
|
|
404
|
+
/** Keep a resource that no longer matches the record, and save why. */
|
|
405
|
+
const keep = async (key, reason) => {
|
|
406
|
+
const resource = next.resources.find((entry) => entry.key === key);
|
|
407
|
+
if (resource?.state === "ready") await save(recordInstallationResource(next, {
|
|
408
|
+
...resource,
|
|
409
|
+
state: "retained"
|
|
410
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
411
|
+
await recordStep(keptStepKey(key), reason);
|
|
412
|
+
};
|
|
413
|
+
const remove = async (resource, hint, zoneId) => {
|
|
414
|
+
if (!await prove(resource)) return;
|
|
415
|
+
try {
|
|
416
|
+
await client.deleteResource(accountId, resource, zoneId);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
|
|
419
|
+
}
|
|
420
|
+
await markDeleted(resource.key);
|
|
421
|
+
};
|
|
422
|
+
const zone = usableZone(next);
|
|
423
|
+
const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
|
|
424
|
+
/** The recorded Worker under this name that this run still deletes, if any. */
|
|
425
|
+
const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
|
|
426
|
+
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.";
|
|
427
|
+
/**
|
|
428
|
+
* Delete a platform Worker. When Cloudflare refuses because other Workers
|
|
429
|
+
* still bind to it, force the DELETE only if each of them is a Worker this
|
|
430
|
+
* run deletes too and still carries this installation's ownership marker.
|
|
431
|
+
*/
|
|
432
|
+
const removeWorker = async (resource) => {
|
|
433
|
+
if (!await prove(resource)) return;
|
|
434
|
+
try {
|
|
435
|
+
await client.deleteResource(accountId, resource);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (!(error instanceof PlatformCloudflareApiError)) throw error;
|
|
438
|
+
const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
|
|
439
|
+
const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
|
|
440
|
+
const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
|
|
441
|
+
const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
|
|
442
|
+
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);
|
|
443
|
+
try {
|
|
444
|
+
await client.forceDeleteWorker(accountId, resource.name);
|
|
445
|
+
} catch (forced) {
|
|
446
|
+
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
await markDeleted(resource.key);
|
|
450
|
+
};
|
|
451
|
+
for (const target of purgeResourcesTargets(initial)) {
|
|
452
|
+
if (target.phase === "access") {
|
|
453
|
+
const { access } = target;
|
|
454
|
+
const live = await client.getAccessResource(access.accountId, access.kind, access.id);
|
|
455
|
+
if (live) {
|
|
456
|
+
if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
|
|
457
|
+
await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
await client.deleteAccessResource(access.accountId, access.kind, access.id);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
await recordStep(`access:${access.key}`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const current = next.resources.find((resource) => resource.key === target.resource.key);
|
|
470
|
+
if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
|
|
471
|
+
switch (target.phase) {
|
|
472
|
+
case "traffic": {
|
|
473
|
+
const worker = next.resources.find((resource) => resource.key === target.worker.key);
|
|
474
|
+
if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
|
|
475
|
+
if (current.kind === "worker-route") {
|
|
476
|
+
const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
|
|
477
|
+
if (live && (live.pattern !== current.name || live.script !== worker.name)) {
|
|
478
|
+
await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (live) await client.deleteWorkerRoute(zone.id, current.id);
|
|
482
|
+
} else {
|
|
483
|
+
const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
|
|
484
|
+
if (live && (live.hostname !== current.name || live.service !== worker.name)) {
|
|
485
|
+
await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
if (live) await client.deleteWorkerDomain(accountId, current.id);
|
|
489
|
+
}
|
|
490
|
+
await markDeleted(current.key);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
case "dns":
|
|
494
|
+
try {
|
|
495
|
+
await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
|
|
498
|
+
await keep(current.key, error.reason);
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
case "email": {
|
|
502
|
+
if (!await prove(current)) break;
|
|
503
|
+
const zoneId = emailRoutingZone(next);
|
|
504
|
+
const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
|
|
505
|
+
try {
|
|
506
|
+
for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
|
|
507
|
+
if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
|
|
510
|
+
}
|
|
511
|
+
await recordStep("email-routing");
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
case "queue-consumers":
|
|
515
|
+
if (!await prove(current)) break;
|
|
516
|
+
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);
|
|
517
|
+
await recordStep(`consumers:${current.key}`);
|
|
518
|
+
break;
|
|
519
|
+
case "workers":
|
|
520
|
+
await removeWorker(current);
|
|
521
|
+
break;
|
|
522
|
+
case "tail":
|
|
523
|
+
await remove(current, workerHint);
|
|
524
|
+
break;
|
|
525
|
+
case "queues":
|
|
526
|
+
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.");
|
|
527
|
+
break;
|
|
528
|
+
case "dispatch": {
|
|
529
|
+
if (!await prove(current)) break;
|
|
530
|
+
const projects = await context.readProjects();
|
|
531
|
+
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)}`);
|
|
532
|
+
for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
|
|
533
|
+
await client.deleteDispatchNamespaceScript(accountId, current.name, script);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
await client.deleteResource(accountId, current);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
|
|
541
|
+
}
|
|
542
|
+
await markDeleted(current.key);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "ai-gateway": {
|
|
546
|
+
const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
|
|
547
|
+
if (live && !createdDuringInstallation(next, current, live.createdAt)) {
|
|
548
|
+
await keep(current.key, "Cloudflare does not show it was created during installation");
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "data":
|
|
555
|
+
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
556
|
+
break;
|
|
557
|
+
case "r2": await purgeBucket(current);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return next;
|
|
561
|
+
async function purgeBucket(bucket) {
|
|
562
|
+
const markerStep = `r2-marker:${bucket.key}`;
|
|
563
|
+
if (!hasStep(next, markerStep)) {
|
|
564
|
+
if (!await prove(bucket)) return;
|
|
565
|
+
await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
|
|
566
|
+
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");
|
|
567
|
+
});
|
|
568
|
+
if (!await prove(bucket)) return;
|
|
569
|
+
const generation = await context.bucketGeneration(next, bucket);
|
|
570
|
+
if (generation === null) {
|
|
571
|
+
await markDeleted(bucket.key);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
await recordStep(markerStep, generation);
|
|
575
|
+
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
576
|
+
} else {
|
|
577
|
+
const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
|
|
578
|
+
const generation = await context.bucketGeneration(next, bucket);
|
|
579
|
+
if (generation === null) {
|
|
580
|
+
await markDeleted(bucket.key);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
|
|
584
|
+
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
await client.deleteResource(accountId, bucket);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
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.");
|
|
590
|
+
}
|
|
591
|
+
await markDeleted(bucket.key);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
7
595
|
//#region src/cli/platform-management.ts
|
|
8
596
|
async function verifyPlatformInstallationOwnership(client, manifest, options = {}) {
|
|
9
597
|
const ready = manifest.resources.filter((resource) => resource.state === "ready" && isOwnershipVerifiableKind(resource.kind) && !options.retainedReplacementKeys?.has(resource.key));
|
|
@@ -64,8 +652,7 @@ async function runInstalledPlatformCommand(args) {
|
|
|
64
652
|
const rollbackSourceRuntime = args.subcommand === "rollback" ? loadPlatformRuntime(args.fromRuntime) : null;
|
|
65
653
|
let progress;
|
|
66
654
|
let reportResult;
|
|
67
|
-
const execute = async (
|
|
68
|
-
const manifest = currentManifest;
|
|
655
|
+
const execute = async (manifest, databaseLock, databaseCheckpointExists = false) => {
|
|
69
656
|
if (args.subcommand === "rollback") assertPlatformRollbackCompatibility(manifest, runtime, rollbackSourceRuntime);
|
|
70
657
|
const recovery = readInstallationRecoverySecrets(manifest);
|
|
71
658
|
const gatewayToken = process.env.VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN || recovery?.cloudflareApiToken;
|
|
@@ -79,6 +666,8 @@ async function runInstalledPlatformCommand(args) {
|
|
|
79
666
|
return;
|
|
80
667
|
}
|
|
81
668
|
if (args.subcommand === "uninstall") {
|
|
669
|
+
const apiUrl = manifest.endpoints.apiUrl;
|
|
670
|
+
const appAccessSetupToken = recovery?.appAccessSetupToken;
|
|
82
671
|
await uninstallPlatform(client, manifest, args, {
|
|
83
672
|
checkpoint: databaseLock?.checkpoint,
|
|
84
673
|
databaseUrl,
|
|
@@ -90,7 +679,19 @@ async function runInstalledPlatformCommand(args) {
|
|
|
90
679
|
credential: {
|
|
91
680
|
source: operatorCredential.source,
|
|
92
681
|
aiGatewayToken: !!gatewayToken
|
|
93
|
-
}
|
|
682
|
+
},
|
|
683
|
+
...apiUrl && appAccessSetupToken ? { turnOffProjectZeroTrust: async () => {
|
|
684
|
+
const saved = recovery?.accessCredentialsJson ? JSON.parse(recovery.accessCredentialsJson) : {};
|
|
685
|
+
if (saved && typeof saved === "object" && apiUrl in saved) activatePlatformAccessCredentials(JSON.stringify({ [apiUrl]: saved[apiUrl] }), [apiUrl]);
|
|
686
|
+
if (manifest.deployment.authentication.accessProtection) ensurePlatformAccessToken(apiUrl, false, true);
|
|
687
|
+
await turnOffInstalledProjectZeroTrust({
|
|
688
|
+
apiUrl,
|
|
689
|
+
installationId: manifest.id,
|
|
690
|
+
probeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
691
|
+
setupToken: appAccessSetupToken,
|
|
692
|
+
updateCommand: readPendingInstallationSecrets(manifest) ? `void platform install --resume --name ${commandArgument(manifest.id)}` : `void platform upgrade ${commandArgument(manifest.id)}`
|
|
693
|
+
});
|
|
694
|
+
} } : {}
|
|
94
695
|
});
|
|
95
696
|
return;
|
|
96
697
|
}
|
|
@@ -98,6 +699,13 @@ async function runInstalledPlatformCommand(args) {
|
|
|
98
699
|
log.info(`${manifest.deployment.displayName} is already enabled.`);
|
|
99
700
|
return;
|
|
100
701
|
}
|
|
702
|
+
const loginFile = args.subcommand === "repair" ? args.authConfig : void 0;
|
|
703
|
+
if (loginFile !== void 0) {
|
|
704
|
+
if (manifest.desiredState === "disabled") throw new Error(`platform: This platform is disabled. Run \`void platform enable ${commandArgument(manifest.id)}\` first, then repair to change the login settings.`);
|
|
705
|
+
if (!manifest.deployment.authentication.configurable) throw new Error("platform: This installation signs in with the GitHub OAuth App from its installation settings. Set VOID_PLATFORM_GITHUB_CLIENT_ID and VOID_PLATFORM_GITHUB_CLIENT_SECRET when running repair to replace it.");
|
|
706
|
+
if (!recovery?.authenticationSetupJson || !recovery.authSetupToken) throw new Error("platform: Changing the login settings needs the setup authority saved on the computer that installed the platform. Run this repair there.");
|
|
707
|
+
await readLoginReplacementFile(loginFile, manifest.deployment.authentication, recovery.authenticationSetupJson);
|
|
708
|
+
}
|
|
101
709
|
const deployment = manifest.deployment;
|
|
102
710
|
const planInput = {
|
|
103
711
|
accountId: manifest.account.id,
|
|
@@ -129,6 +737,7 @@ async function runInstalledPlatformCommand(args) {
|
|
|
129
737
|
].join("\n"), "Platform runtime");
|
|
130
738
|
log.info(manifest.desiredState === "disabled" && args.subcommand !== "enable" ? "Application traffic will stay paused because this installation is disabled." : "Application traffic may pause briefly during a safe cutover.");
|
|
131
739
|
if (args.plan) {
|
|
740
|
+
if (loginFile !== void 0) log.info(`Login settings will be replaced from ${loginFile}; a new setup code is printed at the end.`);
|
|
132
741
|
outro("No Cloudflare or local installation state was changed.");
|
|
133
742
|
return;
|
|
134
743
|
}
|
|
@@ -140,6 +749,21 @@ async function runInstalledPlatformCommand(args) {
|
|
|
140
749
|
databaseUrl
|
|
141
750
|
});
|
|
142
751
|
markPhase("ownership");
|
|
752
|
+
let replacedLogin;
|
|
753
|
+
if (args.subcommand === "repair" && (loginFile !== void 0 || process.stdin.isTTY && !args.yes) && manifest.desiredState !== "disabled" && manifest.deployment.authentication.configurable && recovery?.authenticationSetupJson && recovery.authSetupToken) {
|
|
754
|
+
const setup = await databaseLock?.readAdministratorSetup?.();
|
|
755
|
+
if (loginFile !== void 0) {
|
|
756
|
+
if (!setup) throw new Error("platform: The database lifecycle lock cannot read the administrator setup.");
|
|
757
|
+
if (setup.usersExist || setup.requestHash !== null && !setup.pending) throw new Error("platform: The first administrator has already signed in. Change login methods with `void platform config auth`.");
|
|
758
|
+
if (setup.requestHash === null) throw new Error(`platform: Administrator setup was never started. Resume the install with \`void platform install --resume --name ${commandArgument(manifest.id)}\`.`);
|
|
759
|
+
}
|
|
760
|
+
if (setup?.pending && !setup.usersExist && (loginFile !== void 0 || await promptForConfirmation("First administrator setup is not finished. Change the login settings before continuing?", false))) replacedLogin = await replacePendingInstallationAuthentication({
|
|
761
|
+
...loginFile !== void 0 ? { file: loginFile } : {},
|
|
762
|
+
authentication: manifest.deployment.authentication,
|
|
763
|
+
setupJson: recovery.authenticationSetupJson,
|
|
764
|
+
apiUrl: plan.apiUrl
|
|
765
|
+
});
|
|
766
|
+
}
|
|
143
767
|
const installation = args.subcommand === "enable" ? setInstallationDesiredState(manifest, "enabled", (/* @__PURE__ */ new Date()).toISOString()) : manifest;
|
|
144
768
|
const bootstrapGitHubLogin = process.env.VOID_PLATFORM_ADMIN_GITHUB_LOGIN || recovery?.bootstrapGitHubLogin || "";
|
|
145
769
|
const bootstrapGitHubId = process.env.VOID_PLATFORM_ADMIN_GITHUB_ID || recovery?.bootstrapGitHubId || (bootstrapGitHubLogin ? await resolveBootstrapGitHubId(bootstrapGitHubLogin) : "");
|
|
@@ -161,7 +785,8 @@ async function runInstalledPlatformCommand(args) {
|
|
|
161
785
|
projectSecretKeysJson: process.env.VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON || "",
|
|
162
786
|
projectSecretActiveKeyVersion: process.env.VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION || "",
|
|
163
787
|
r2AccessKeyId: process.env.VOID_PLATFORM_R2_ACCESS_KEY_ID || "",
|
|
164
|
-
r2SecretAccessKey: process.env.VOID_PLATFORM_R2_SECRET_ACCESS_KEY || ""
|
|
788
|
+
r2SecretAccessKey: process.env.VOID_PLATFORM_R2_SECRET_ACCESS_KEY || "",
|
|
789
|
+
...replacedLogin
|
|
165
790
|
});
|
|
166
791
|
const runtimeTokenToPreflight = resolvePlatformRuntimeTokenToPreflight(plan, secrets);
|
|
167
792
|
if (runtimeTokenToPreflight) await verifyPlatformRuntimeTokenPermissions(new PlatformCloudflareClient(runtimeTokenToPreflight), {
|
|
@@ -177,6 +802,17 @@ async function runInstalledPlatformCommand(args) {
|
|
|
177
802
|
} });
|
|
178
803
|
progress.start(args.subcommand === "upgrade" ? "Upgrading the platform" : args.subcommand === "rollback" ? "Rolling back the platform" : args.subcommand === "enable" ? "Enabling the platform" : "Repairing the platform");
|
|
179
804
|
if (installation !== manifest) writeInstallationManifest(installation);
|
|
805
|
+
const keepAcceptedLogin = async (error) => {
|
|
806
|
+
const after = replacedLogin ? await databaseLock?.readAdministratorSetup?.().catch(() => void 0) : void 0;
|
|
807
|
+
if (!after || !recovery?.authenticationSetupJson || after.requestHash !== authenticationSetupRequestHash(recovery.authenticationSetupJson) || !after.usersExist && after.pending) return error;
|
|
808
|
+
const pending = readPendingInstallationSecrets(installation);
|
|
809
|
+
if (pending) writePendingInstallationSecrets(installation, {
|
|
810
|
+
...pending,
|
|
811
|
+
authenticationSetupJson: recovery.authenticationSetupJson,
|
|
812
|
+
...recovery.authBootstrapCode ? { authBootstrapCode: recovery.authBootstrapCode } : {}
|
|
813
|
+
});
|
|
814
|
+
return new Error("platform: The first administrator signed in before the new login settings were applied. Nothing changed. Change login methods with `void platform config auth`.", { cause: error });
|
|
815
|
+
};
|
|
180
816
|
const result = await installPlatform(plan, secrets, installation, {
|
|
181
817
|
acquireDatabaseLifecycleLock: async () => Object.assign(async () => {}, {
|
|
182
818
|
checkpoint: databaseLock?.checkpoint,
|
|
@@ -207,6 +843,8 @@ async function runInstalledPlatformCommand(args) {
|
|
|
207
843
|
repairMissing: args.subcommand === "repair",
|
|
208
844
|
stableWorkerUpdate: args.subcommand === "upgrade"
|
|
209
845
|
})
|
|
846
|
+
}).catch(async (error) => {
|
|
847
|
+
throw await keepAcceptedLogin(error);
|
|
210
848
|
});
|
|
211
849
|
progress.message("Finalizing platform changes");
|
|
212
850
|
markPhase("installer complete");
|
|
@@ -233,8 +871,10 @@ async function runInstalledPlatformCommand(args) {
|
|
|
233
871
|
if (result.setupPending) {
|
|
234
872
|
const code = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
235
873
|
if (code) {
|
|
236
|
-
log.info(`First administrator setup: ${plan.apiUrl}/setup`);
|
|
874
|
+
log.info(`First administrator setup: ${formatUrl(`${plan.apiUrl}/setup`)}`);
|
|
237
875
|
console.log(`One-time administrator setup code: ${code}`);
|
|
876
|
+
log.info(`Login callback URL: ${formatUrl(`${plan.apiUrl}/auth/callback`)}`);
|
|
877
|
+
log.info(`If sign-in fails, run \`void platform repair ${commandArgument(manifest.id)}\` to change the login settings.`);
|
|
238
878
|
}
|
|
239
879
|
}
|
|
240
880
|
printSuccess(`${deployment.displayName}: runtime ${runtime.manifest.platformVersion} ${args.subcommand === "rollback" ? "restored" : "ready"}.`);
|
|
@@ -295,7 +935,7 @@ function assertPlatformRollbackCompatibility(installation, target, source) {
|
|
|
295
935
|
if (JSON.stringify(prefix) !== JSON.stringify(target.manifest.database.migrations)) throw new Error("platform: The rollback target database migrations are not an exact prefix of the installed runtime.");
|
|
296
936
|
if (!source.manifest.database.rollbackSafeFrom.some((entry) => entry.platformVersion === target.manifest.platformVersion && entry.schemaDigest === target.manifest.database.schemaDigest)) throw new Error(`platform: Installed runtime ${source.manifest.platformVersion} does not declare post-success rollback compatibility with ${target.manifest.platformVersion} at schema ${target.manifest.database.schemaDigest}.`);
|
|
297
937
|
if (installation.databaseSchemaDigest !== source.manifest.database.schemaDigest) throw new Error("platform: The authoritative installation checkpoint does not record the installed runtime database schema. Repair or roll forward before rollback.");
|
|
298
|
-
if (
|
|
938
|
+
if (target.manifest.safetyProtocols.sandboxDrain < installation.safetyProtocols.sandboxDrain) throw new Error("platform: The rollback target predates the installed sandbox-drain safety protocol.");
|
|
299
939
|
}
|
|
300
940
|
function resolvePlatformRuntimeTokenToPreflight(plan, secrets) {
|
|
301
941
|
const explicitUpdate = secrets.workerSecretUpdates?.CF_API_TOKEN;
|
|
@@ -319,18 +959,17 @@ async function resolveInstalledPlatformManifest(selector, dependencies = {}) {
|
|
|
319
959
|
return resolveInstallationManifest(String(picked));
|
|
320
960
|
}
|
|
321
961
|
async function disablePlatform(client, initialManifest, planOnly, dependencies = {}) {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
962
|
+
const manifest = await recoverDurablyMarkedProvisionIntents(client, initialManifest);
|
|
963
|
+
const saveCheckpoint = (state) => writeLifecycleCheckpoint(client, state, dependencies.checkpoint);
|
|
964
|
+
const saveRemoteManifest = (state) => writeRemoteManifest(client, state, dependencies.checkpoint);
|
|
325
965
|
note("Block all platform traffic and the platform API. Routes, custom domains, Workers, and data stay installed.", "Disable plan");
|
|
326
966
|
if (planOnly) {
|
|
327
967
|
outro("No Cloudflare or local installation state was changed.");
|
|
328
968
|
return;
|
|
329
969
|
}
|
|
330
|
-
const manifest = reconciled;
|
|
331
970
|
if (manifest !== initialManifest) writeInstallationManifest(manifest);
|
|
332
971
|
const alreadyDisabled = manifest.status === "disabled";
|
|
333
|
-
|
|
972
|
+
await verifyPlatformMutationOwnership(client, manifest, /* @__PURE__ */ new Set([
|
|
334
973
|
"routing-kv",
|
|
335
974
|
"zone",
|
|
336
975
|
...manifest.resources.filter((resource) => resource.kind === "worker").map((resource) => resource.key)
|
|
@@ -342,30 +981,27 @@ async function disablePlatform(client, initialManifest, planOnly, dependencies =
|
|
|
342
981
|
writeInstallationManifest(next);
|
|
343
982
|
}
|
|
344
983
|
try {
|
|
345
|
-
await
|
|
346
|
-
const zone = next.resources.find((resource) => resource.key === "zone");
|
|
984
|
+
await saveRemoteManifest(next);
|
|
347
985
|
const routing = requireResource(next, "routing-kv");
|
|
348
986
|
const trafficGeneration = currentTrafficGeneration(next);
|
|
349
987
|
await client.putKvValue(next.account.id, routing.id, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
350
|
-
const trackedTraffic = trackedPlatformWorkerTraffic(next);
|
|
351
|
-
await client.removeRoutes(next.account.id, !zone || missing.has(zone.key) ? null : zone.id, trackedTraffic);
|
|
352
988
|
next = markTrafficAttachmentsRetained(next);
|
|
353
989
|
if (!hasLifecycleStep(next, `disable:${trafficGeneration}:routes`)) next = recordLifecycleStep(next, `disable:${trafficGeneration}:routes`);
|
|
354
|
-
await
|
|
990
|
+
await saveCheckpoint(next);
|
|
355
991
|
next = transitionInstallation(next, "disabled", (/* @__PURE__ */ new Date()).toISOString());
|
|
356
|
-
await
|
|
992
|
+
await saveCheckpoint(next);
|
|
357
993
|
printSuccess(`${next.deployment.displayName} is disabled. Its data was not changed.`);
|
|
358
994
|
} catch (error) {
|
|
359
995
|
next = transitionInstallation(next, "failed", (/* @__PURE__ */ new Date()).toISOString());
|
|
360
996
|
writeInstallationManifest(next);
|
|
361
|
-
await
|
|
997
|
+
await saveRemoteManifest(next).catch(() => {});
|
|
362
998
|
throw error;
|
|
363
999
|
}
|
|
364
1000
|
}
|
|
365
1001
|
async function uninstallPlatform(client, initialManifest, args, dependencies = {}) {
|
|
366
1002
|
const manifest = await recoverDurablyMarkedProvisionIntents(client, initialManifest);
|
|
367
|
-
const
|
|
368
|
-
const
|
|
1003
|
+
const saveCheckpoint = (state) => writeLifecycleCheckpoint(client, state, dependencies.checkpoint);
|
|
1004
|
+
const saveRemoteManifest = (state) => writeRemoteManifest(client, state, dependencies.checkpoint);
|
|
369
1005
|
const retainData = !args.purgeData;
|
|
370
1006
|
const purgeResources = args.purgeResources;
|
|
371
1007
|
const routingKv = manifest.resources.find((resource) => resource.key === "routing-kv");
|
|
@@ -375,17 +1011,16 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
375
1011
|
const interactive = dependencies.interactive ?? process.stdin.isTTY;
|
|
376
1012
|
const confirmAction = dependencies.confirm ?? promptForConfirmation;
|
|
377
1013
|
if (createdZone && args.deleteZone === true) throw new Error(`platform: Refusing to delete Cloudflare zone ${JSON.stringify(createdZone.name)} automatically. A zone can contain unrelated DNS, settings, and products that the installer cannot exhaustively prove ownership of. Uninstall with --keep-zone, inspect the retained zone in Cloudflare, and delete it manually if appropriate.`);
|
|
378
|
-
await assertProjectZeroTrustAllowsUninstall(dependencies.readProjectZeroTrust);
|
|
1014
|
+
const zeroTrust = await assertProjectZeroTrustAllowsUninstall(dependencies.readProjectZeroTrust, { canTurnOff: !!dependencies.turnOffProjectZeroTrust });
|
|
379
1015
|
if (purgeResources) await assertNoPlatformProjects(dependencies.readProjects);
|
|
380
|
-
|
|
381
|
-
if (args.plan || interactive && !args.yes) note(purgeResources ? purgeResourcesPlanText(manifest) : [
|
|
1016
|
+
if (args.plan || interactive && !args.yes) note([...zeroTrust ? zeroTrust.turnOff.length ? ["Turn off project Zero Trust; no platform user exists yet. Delete its Access applications:", formatZeroTrustApplications(zeroTrust.turnOff)] : ["Turn off project Zero Trust; no platform user exists yet."] : [], purgeResources ? purgeResourcesPlanText(manifest) : [
|
|
382
1017
|
"Disable platform traffic.",
|
|
383
1018
|
retainData ? "Keep Workers and data." : "Delete eligible data; keep Workers, R2, and AI Gateway.",
|
|
384
1019
|
"Delete unused Queues.",
|
|
385
1020
|
...describeKeptResources(manifest, plannedKeptResources(manifest, retainData, hasRoutingCheckpoint), "Keep for manual cleanup:", "Keep Access resources:")
|
|
386
|
-
].join("\n"), import_picocolors.default.bold("Uninstall plan"));
|
|
387
|
-
if (
|
|
388
|
-
const credentialCheck = checkPurgeResourcesCredentials(
|
|
1021
|
+
].join("\n")].join("\n"), import_picocolors.default.bold("Uninstall plan"));
|
|
1022
|
+
if (purgeResources) {
|
|
1023
|
+
const credentialCheck = checkPurgeResourcesCredentials(client, manifest, dependencies.credential ?? {
|
|
389
1024
|
source: "environment",
|
|
390
1025
|
aiGatewayToken: false
|
|
391
1026
|
});
|
|
@@ -399,7 +1034,6 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
399
1034
|
return;
|
|
400
1035
|
}
|
|
401
1036
|
if (manifest !== initialManifest) writeInstallationManifest(manifest);
|
|
402
|
-
if (!interactive && !args.yes) throw new Error("platform: Non-interactive `void platform uninstall` requires --yes to acknowledge Cloudflare changes. Run with --plan first to inspect them.");
|
|
403
1037
|
if (interactive && !args.yes) {
|
|
404
1038
|
if (!await confirmAction(purgeResources ? "Permanently delete every Cloudflare resource this installation created?" : args.purgeData ? "Permanently purge installer-owned platform data?" : "Uninstall this platform?", false)) {
|
|
405
1039
|
outro("No changes made.");
|
|
@@ -433,6 +1067,10 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
433
1067
|
}
|
|
434
1068
|
const { fenceProjectZeroTrust, liftProjectZeroTrustFence, lockProjectZeroTrust } = dependencies;
|
|
435
1069
|
if (!lockProjectZeroTrust || !fenceProjectZeroTrust || !liftProjectZeroTrustFence) throw new Error("platform: Refusing to uninstall because no platform database connection is available to hold off project Zero Trust changes.");
|
|
1070
|
+
if (zeroTrust) {
|
|
1071
|
+
log.step("Turning off project Zero Trust");
|
|
1072
|
+
await dependencies.turnOffProjectZeroTrust();
|
|
1073
|
+
}
|
|
436
1074
|
const releaseProjectZeroTrust = await lockProjectZeroTrust();
|
|
437
1075
|
let fenced = false;
|
|
438
1076
|
let disablingTraffic = false;
|
|
@@ -455,7 +1093,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
455
1093
|
throw error;
|
|
456
1094
|
}
|
|
457
1095
|
try {
|
|
458
|
-
await
|
|
1096
|
+
await saveRemoteManifest(next);
|
|
459
1097
|
const routing = hasRoutingCheckpoint ? requireResource(next, "routing-kv") : void 0;
|
|
460
1098
|
disablingTraffic = true;
|
|
461
1099
|
if (routing) await client.putKvValue(next.account.id, routing.id, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
@@ -463,7 +1101,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
463
1101
|
for (const key of missingResources) {
|
|
464
1102
|
const resource = requireResource(next, key);
|
|
465
1103
|
next = updateResourceState(next, key, resource.ownership === "created" ? "deleted" : "retained");
|
|
466
|
-
await
|
|
1104
|
+
await saveCheckpoint(next);
|
|
467
1105
|
}
|
|
468
1106
|
const workers = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.state === "ready").map((resource) => resource.name));
|
|
469
1107
|
const zone = next.resources.find((resource) => resource.key === "zone");
|
|
@@ -473,18 +1111,16 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
473
1111
|
"zone",
|
|
474
1112
|
worker.key
|
|
475
1113
|
]), { databaseUrl: dependencies.databaseUrl })).has(worker.key)) {
|
|
476
|
-
const trackedTraffic = trackedPlatformWorkerTraffic(next, /* @__PURE__ */ new Set([worker.name]));
|
|
477
|
-
await client.removeRoutes(next.account.id, zone.id, trackedTraffic);
|
|
478
1114
|
next = markTrafficAttachmentsRetained(next);
|
|
479
1115
|
next = recordLifecycleStep(next, `uninstall:routes:${worker.key}`);
|
|
480
|
-
await
|
|
1116
|
+
await saveCheckpoint(next);
|
|
481
1117
|
}
|
|
482
1118
|
}
|
|
483
|
-
if (
|
|
484
|
-
client
|
|
1119
|
+
if (purgeResources && hasRoutingCheckpoint) next = await runPurgeResourcesTeardown({
|
|
1120
|
+
client,
|
|
485
1121
|
checkpoint: async (checkpointManifest) => {
|
|
486
1122
|
next = checkpointManifest;
|
|
487
|
-
await
|
|
1123
|
+
await saveCheckpoint(checkpointManifest);
|
|
488
1124
|
},
|
|
489
1125
|
verify: (checkpointManifest, keys) => verifyPlatformMutationOwnership(client, checkpointManifest, keys, {
|
|
490
1126
|
allowDeletedRoutingCheckpoint: true,
|
|
@@ -518,18 +1154,18 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
518
1154
|
if (resource.state !== "ready") continue;
|
|
519
1155
|
if (!hasRoutingCheckpoint && resource.kind !== "zone") {
|
|
520
1156
|
next = updateResourceState(next, resource.key, "retained");
|
|
521
|
-
await
|
|
1157
|
+
await saveCheckpoint(next);
|
|
522
1158
|
continue;
|
|
523
1159
|
}
|
|
524
1160
|
if (!shouldDeleteResource(resource, retainData)) {
|
|
525
1161
|
const removed = resource.kind === "ai-gateway" && resource.ownership === "created" && !hasNamedResource(await client.listAiGateways(next.account.id), resource);
|
|
526
1162
|
next = updateResourceState(next, resource.key, removed ? "deleted" : "retained");
|
|
527
|
-
await
|
|
1163
|
+
await saveCheckpoint(next);
|
|
528
1164
|
continue;
|
|
529
1165
|
}
|
|
530
1166
|
if (resource.kind === "dns-record" && !zone || resource.kind === "queue" && queuesInUse) {
|
|
531
1167
|
next = updateResourceState(next, resource.key, "retained");
|
|
532
|
-
await
|
|
1168
|
+
await saveCheckpoint(next);
|
|
533
1169
|
continue;
|
|
534
1170
|
}
|
|
535
1171
|
if ((await verifyPlatformMutationOwnership(client, next, /* @__PURE__ */ new Set(["routing-kv", resource.key]), {
|
|
@@ -537,15 +1173,15 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
537
1173
|
databaseUrl: dependencies.databaseUrl
|
|
538
1174
|
})).has(resource.key)) {
|
|
539
1175
|
next = updateResourceState(next, resource.key, resource.ownership === "created" ? "deleted" : "retained");
|
|
540
|
-
await
|
|
1176
|
+
await saveCheckpoint(next);
|
|
541
1177
|
continue;
|
|
542
1178
|
}
|
|
543
1179
|
await client.deleteResource(next.account.id, resource, zone?.id);
|
|
544
1180
|
next = updateResourceState(next, resource.key, "deleted");
|
|
545
|
-
await
|
|
1181
|
+
await saveCheckpoint(next);
|
|
546
1182
|
}
|
|
547
1183
|
const completed = transitionInstallation(next, "uninstalled", (/* @__PURE__ */ new Date()).toISOString());
|
|
548
|
-
await
|
|
1184
|
+
await saveRemoteManifest(completed);
|
|
549
1185
|
next = completed;
|
|
550
1186
|
writeInstallationManifest(completed);
|
|
551
1187
|
if (!retainData) removeInstallationRecoverySecrets(next);
|
|
@@ -557,7 +1193,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
557
1193
|
await stopProjectZeroTrustHold().catch(() => {});
|
|
558
1194
|
next = transitionInstallation(next, "failed", (/* @__PURE__ */ new Date()).toISOString());
|
|
559
1195
|
writeInstallationManifest(next);
|
|
560
|
-
await
|
|
1196
|
+
await saveRemoteManifest(next).catch(() => {});
|
|
561
1197
|
throw error;
|
|
562
1198
|
}
|
|
563
1199
|
}
|
|
@@ -593,7 +1229,7 @@ async function recoverDurablyMarkedProvisionIntents(client, manifest) {
|
|
|
593
1229
|
"r2-bucket",
|
|
594
1230
|
manifest.deployment.resources.assetsBucket
|
|
595
1231
|
]
|
|
596
|
-
].filter(([key, kind, name]) => !manifest.resources.some((resource) => resource.key === key) &&
|
|
1232
|
+
].filter(([key, kind, name]) => !manifest.resources.some((resource) => resource.key === key) && hasProvisionIntentStep(manifest, key, kind, name));
|
|
597
1233
|
if (pending.length === 0) return manifest;
|
|
598
1234
|
const [workers, buckets] = await Promise.all([client.listWorkerNames(manifest.account.id), client.listR2Buckets(manifest.account.id)]);
|
|
599
1235
|
let recovered = manifest;
|
|
@@ -604,10 +1240,7 @@ async function recoverDurablyMarkedProvisionIntents(client, manifest) {
|
|
|
604
1240
|
} : null : buckets.find((bucket) => bucket.name === name) ?? null;
|
|
605
1241
|
if (!live) continue;
|
|
606
1242
|
const resource = {
|
|
607
|
-
configHash:
|
|
608
|
-
kind,
|
|
609
|
-
name
|
|
610
|
-
})).digest("hex"),
|
|
1243
|
+
configHash: legacyResourceConfigHash(kind, name),
|
|
611
1244
|
id: live.id,
|
|
612
1245
|
key,
|
|
613
1246
|
kind,
|
|
@@ -667,28 +1300,6 @@ function purgeResourcesPlanText(manifest) {
|
|
|
667
1300
|
...permissions.length ? [`API token needs: ${permissions.join(", ")}. Browser login cannot delete DNS records or Access resources.`] : []
|
|
668
1301
|
].join("\n");
|
|
669
1302
|
}
|
|
670
|
-
function requirePurgeResourcesClient(client) {
|
|
671
|
-
if ([
|
|
672
|
-
"deleteAccessResource",
|
|
673
|
-
"deleteDispatchNamespaceScript",
|
|
674
|
-
"deleteEmailRoutingRule",
|
|
675
|
-
"deleteQueueConsumer",
|
|
676
|
-
"deleteR2Object",
|
|
677
|
-
"deleteWorkerDomain",
|
|
678
|
-
"deleteWorkerRoute",
|
|
679
|
-
"emptyR2Bucket",
|
|
680
|
-
"forceDeleteWorker",
|
|
681
|
-
"getAccessResource",
|
|
682
|
-
"getEmailRoutingCatchAll",
|
|
683
|
-
"listDispatchNamespaceScripts",
|
|
684
|
-
"listEmailRoutingRules",
|
|
685
|
-
"listQueueConsumers",
|
|
686
|
-
"listWorkerDependents",
|
|
687
|
-
"listWorkerRoutes",
|
|
688
|
-
"resetEmailRoutingCatchAll"
|
|
689
|
-
].some((method) => typeof client[method] !== "function")) throw new Error("platform: This Cloudflare client cannot run --purge-resources.");
|
|
690
|
-
return client;
|
|
691
|
-
}
|
|
692
1303
|
function describeKeptResources(manifest, resources, heading, accessHeading, emailRulesRemoved = false) {
|
|
693
1304
|
const sorted = [...resources].sort((left, right) => keptResourceRank(manifest, left) - keptResourceRank(manifest, right));
|
|
694
1305
|
const isTraffic = (resource) => KEPT_RESOURCE_ORDER.indexOf(resource.kind) < KEPT_RESOURCE_ORDER.indexOf("queue");
|
|
@@ -753,7 +1364,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
753
1364
|
if (remote.id !== manifest.id || remote.ownershipNonce !== manifest.ownershipNonce || remote.account.id !== manifest.account.id) throw ownershipError("the remote installation identity does not match this installation");
|
|
754
1365
|
if (targetResources.every((resource) => hasMatchingResourceProof(remote, resource))) break;
|
|
755
1366
|
}
|
|
756
|
-
if (attempt < 2) await
|
|
1367
|
+
if (attempt < 2) await setTimeout(50 * 2 ** attempt);
|
|
757
1368
|
}
|
|
758
1369
|
if (rawRemote === null) {
|
|
759
1370
|
if ((options.allowMissingRoutingCheckpoint || options.allowUninitializedRoutingCheckpoint) && routing.ownership === "created") {
|
|
@@ -800,10 +1411,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
800
1411
|
const live = await client.getHyperdriveConfig(accountId, resource.id);
|
|
801
1412
|
exists = live.id === resource.id && live.name === resource.name;
|
|
802
1413
|
if (exists) {
|
|
803
|
-
const legacyHash =
|
|
804
|
-
kind: resource.kind,
|
|
805
|
-
name: resource.name
|
|
806
|
-
})).digest("hex");
|
|
1414
|
+
const legacyHash = legacyResourceConfigHash(resource.kind, resource.name);
|
|
807
1415
|
const malformedLegacyHash = !/^[a-f0-9]{64}$/.test(resource.configHash);
|
|
808
1416
|
if (options.databaseUrl && resource.ownership !== "adopted") {
|
|
809
1417
|
const expected = hyperdriveOriginFromConnectionString(options.databaseUrl);
|
|
@@ -824,10 +1432,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
824
1432
|
exists = current !== void 0;
|
|
825
1433
|
if (current && resource.ownership === "created") {
|
|
826
1434
|
const liveHash = dnsRecordConfigHash(current.name, current.type, current.content, current.proxied);
|
|
827
|
-
const legacyHash =
|
|
828
|
-
kind: resource.kind,
|
|
829
|
-
name: resource.name
|
|
830
|
-
})).digest("hex");
|
|
1435
|
+
const legacyHash = legacyResourceConfigHash(resource.kind, resource.name);
|
|
831
1436
|
const expectedHash = dnsRecordConfigHash(resource.name, "AAAA", "100::", true);
|
|
832
1437
|
if (liveHash !== resource.configHash && !(resource.configHash === legacyHash && liveHash === expectedHash)) throw new PlatformResourceChangedError(resource.key, "its configuration changed after installation", ownershipError(`dns-record ${resource.name} changed configuration after installation`).message);
|
|
833
1438
|
}
|
|
@@ -839,12 +1444,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
839
1444
|
const live = r2Buckets.find((entry) => entry.id === resource.id && entry.name === resource.name);
|
|
840
1445
|
exists = live !== void 0;
|
|
841
1446
|
if (live) {
|
|
842
|
-
|
|
843
|
-
const legacyHash = createHash("sha256").update(JSON.stringify({
|
|
844
|
-
kind: resource.kind,
|
|
845
|
-
name: resource.name
|
|
846
|
-
})).digest("hex");
|
|
847
|
-
if (liveHash === null || resource.configHash !== liveHash && resource.configHash !== legacyHash) throw ownershipError(`r2-bucket ${resource.name} changed creation fingerprint`);
|
|
1447
|
+
if (!matchesRecordedR2Fingerprint(resource.configHash, live)) throw ownershipError(`r2-bucket ${resource.name} changed creation fingerprint`);
|
|
848
1448
|
verifyResourceMarker(await client.getR2Object(accountId, resource.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), manifest, resource);
|
|
849
1449
|
}
|
|
850
1450
|
} else throw ownershipError(`live ownership checks do not support ${resource.kind}`);
|
|
@@ -857,10 +1457,7 @@ function hasMatchingResourceProof(manifest, resource) {
|
|
|
857
1457
|
return proof?.id === resource.id && proof.kind === resource.kind && proof.name === resource.name && proof.ownership === resource.ownership;
|
|
858
1458
|
}
|
|
859
1459
|
function hasMatchingResourceIntent(manifest, resource) {
|
|
860
|
-
if (resource.ownership === "created")
|
|
861
|
-
const previousResourceId = manifest.resources.find((entry) => entry.key === resource.key)?.id ?? null;
|
|
862
|
-
return manifest.steps.some((step) => (step.key === `provision-intent:${resource.key}` || step.key.startsWith(`provision-intent:${resource.key}:`)) && step.resultHash === platformProvisionIntentHash(manifest, resource.key, resource.kind, resource.name, previousResourceId));
|
|
863
|
-
}
|
|
1460
|
+
if (resource.ownership === "created") return hasProvisionIntentStep(manifest, resource.key, resource.kind, resource.name);
|
|
864
1461
|
if (resource.ownership === "adopted") return manifest.steps.some((step) => step.key === `adoption-intent:${resource.key}` && step.resultHash === platformAdoptionIntentHash(manifest, resource.key, resource.kind, resource.id, resource.name));
|
|
865
1462
|
return false;
|
|
866
1463
|
}
|
|
@@ -877,40 +1474,9 @@ function isPlatformManifestMonotonicExtension(earlier, later) {
|
|
|
877
1474
|
function hasNamedResource(entries, resource) {
|
|
878
1475
|
return entries.some((entry) => entry.id === resource.id && entry.name === resource.name);
|
|
879
1476
|
}
|
|
880
|
-
function dnsRecordConfigHash(name, type, content, proxied) {
|
|
881
|
-
return createHash("sha256").update(JSON.stringify({
|
|
882
|
-
content,
|
|
883
|
-
kind: "dns-record",
|
|
884
|
-
name,
|
|
885
|
-
proxied,
|
|
886
|
-
type
|
|
887
|
-
})).digest("hex");
|
|
888
|
-
}
|
|
889
|
-
function r2BucketConfigHash(name, fingerprint) {
|
|
890
|
-
return createHash("sha256").update(JSON.stringify({
|
|
891
|
-
fingerprint,
|
|
892
|
-
kind: "r2-bucket",
|
|
893
|
-
name
|
|
894
|
-
})).digest("hex");
|
|
895
|
-
}
|
|
896
1477
|
function verifyResourceMarker(value, manifest, resource) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
try {
|
|
900
|
-
actual = parsePlatformResourceOwnershipMarker(JSON.parse(value));
|
|
901
|
-
} catch {
|
|
902
|
-
throw ownershipError(`${resource.kind} ${resource.name} has an invalid ownership marker`);
|
|
903
|
-
}
|
|
904
|
-
const expected = createPlatformResourceOwnershipMarker({
|
|
905
|
-
accountId: manifest.account.id,
|
|
906
|
-
installationId: manifest.id,
|
|
907
|
-
ownershipNonce: manifest.ownershipNonce,
|
|
908
|
-
resourceId: resource.id,
|
|
909
|
-
resourceKey: resource.key,
|
|
910
|
-
resourceKind: resource.kind,
|
|
911
|
-
resourceName: resource.name
|
|
912
|
-
});
|
|
913
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw ownershipError(`${resource.kind} ${resource.name} belongs to another installation`);
|
|
1478
|
+
const problem = resourceMarkerProblem(value, manifest, resource);
|
|
1479
|
+
if (problem) throw ownershipError(`${resource.kind} ${resource.name} ${problem}`);
|
|
914
1480
|
}
|
|
915
1481
|
function canResumeAfterRoutingCheckpointDeletion(manifest, targetKeys) {
|
|
916
1482
|
if (manifest.status !== "failed" && manifest.status !== "uninstalling") return false;
|
|
@@ -953,11 +1519,11 @@ function recordLifecycleStep(manifest, key) {
|
|
|
953
1519
|
resultHash: "completed"
|
|
954
1520
|
}, now);
|
|
955
1521
|
}
|
|
956
|
-
async function
|
|
1522
|
+
async function writeLifecycleCheckpoint(client, manifest, databaseCheckpoint) {
|
|
957
1523
|
writeInstallationManifest(manifest);
|
|
958
|
-
await
|
|
1524
|
+
await writeRemoteManifest(client, manifest, databaseCheckpoint);
|
|
959
1525
|
}
|
|
960
|
-
async function
|
|
1526
|
+
async function writeRemoteManifest(client, manifest, databaseCheckpoint) {
|
|
961
1527
|
await databaseCheckpoint?.(manifest);
|
|
962
1528
|
const routing = manifest.resources.find((resource) => resource.key === "routing-kv" && resource.state !== "deleted");
|
|
963
1529
|
if (routing) await client.putKvValue(manifest.account.id, routing.id, REMOTE_INSTALLATION_MANIFEST_KEY, JSON.stringify(manifest));
|