void 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{account-cmd-l-FM7Ioq.mjs → account-cmd-Ci00cncT.mjs} +5 -5
- package/dist/{agents-B_pX_HmQ.mjs → agents-CuNtT0D_.mjs} +5 -2
- package/dist/{application-analysis-DKy02UY0.mjs → application-analysis-D72ZQfMZ.mjs} +118 -135
- package/dist/{application-routing-DQyUh_5S.mjs → application-routing-Dy589Vgn.mjs} +1 -2
- package/dist/atomic-write-Dq6R0BPN.mjs +18 -0
- package/dist/{auth-Dn2vw-SG.mjs → auth-B6iOCjKI.mjs} +23 -56
- package/dist/{auth-link-vz9Z2kmG.mjs → auth-link-DWr6Afao.mjs} +10 -10
- package/dist/{auth-migrations-BwHgeEsD.mjs → auth-migrations-Cgs0k6lS.mjs} +28 -5
- package/dist/{auth-router-DvsCUVw4.mjs → auth-router-DP14wVkZ.mjs} +4 -4
- package/dist/auth-wn6F8dB8.mjs +2 -0
- package/dist/base64-DVrOndmI.mjs +16 -0
- package/dist/{better-auth-shared-CPvWD3nG.d.mts → better-auth-shared-B74NJIus.d.mts} +8 -13
- package/dist/{better-auth-shared-DPpj8zxc.mjs → better-auth-shared-CMXlZG7W.mjs} +56 -68
- package/dist/{build-cmd-D6we0VjO.mjs → build-cmd-Cr3FJxv0.mjs} +5 -5
- package/dist/{cache-CZRdzKx1.mjs → cache-D0Tt28cn.mjs} +5 -5
- package/dist/{cancel-deploy-CQ3NnMvf.mjs → cancel-deploy-CJKvk7AN.mjs} +4 -4
- 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-2u583yJK.mjs +2 -0
- package/dist/{cf-build-output-DjvB_872.mjs → cf-build-output-Bc8i3dzE.mjs} +2 -2
- package/dist/cf-user-agent-ChjQp2MU.mjs +33 -0
- package/dist/cli/cli.mjs +198 -311
- package/dist/cli/cloudflare-operation-process.mjs +41 -15
- package/dist/cli/env-schema-probe.d.mts +5 -43
- package/dist/cli/env-schema-probe.mjs +14 -80
- package/dist/client-C1BjRn3g.mjs +2 -0
- package/dist/{client-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-CTrn-lXJ.mjs} +6 -5
- package/dist/{cloudflare-config-BmhBv3HV.mjs → cloudflare-config-DV60XFFK.mjs} +16 -28
- package/dist/{cloudflare-connect-2G3rL2rP.mjs → cloudflare-connect-BosmCNlq.mjs} +3 -3
- package/dist/{cloudflare-operations-CwI1bChl.mjs → cloudflare-operations-BKdSfNBN.mjs} +26 -34
- package/dist/{cloudflare-operations-BlYLlowZ.mjs → cloudflare-operations-Ds-lX3Ts.mjs} +1 -1
- package/dist/{cloudflare-process-2-TfIEzB.mjs → cloudflare-process-CeDWJRhj.mjs} +12 -7
- package/dist/collect-NUlJMpLY.mjs +2 -0
- package/dist/{collect-DiTn9yr6.mjs → collect-xjffdCdk.mjs} +1 -1
- package/dist/{config-CSa15FvD.d.mts → config-7qoq2p9v.d.mts} +2 -2
- package/dist/{config-CLYAosnQ.mjs → config-BEm9vRvS.mjs} +11 -5
- package/dist/{config-DiowPT80.mjs → config-Bx_1SpnO.mjs} +87 -177
- package/dist/config-CNnrSMYm.mjs +2 -0
- package/dist/{config-DPByhVck.mjs → config-D0GY5ItR.mjs} +1 -1
- package/dist/config-entry.d.mts +1 -1
- package/dist/{connect-BfPBHHm-.mjs → connect-nukQSoEO.mjs} +8 -8
- package/dist/{create-project-BdXEFKal.mjs → create-project-D4APeBwX.mjs} +7 -7
- package/dist/{create-project-qFDC45zg.mjs → create-project-DL7YXpjj.mjs} +1 -1
- package/dist/{db-Xs80hcaA.mjs → db-27RztQLy.mjs} +173 -263
- package/dist/{delete-d2vuou3j.mjs → delete-BbsZFZIw.mjs} +5 -5
- package/dist/deploy-B06EQq4t.mjs +6009 -0
- package/dist/deploy-Db2ALnQP.mjs +3 -0
- package/dist/deploy-managed-static-CGswbcay.mjs +2277 -0
- package/dist/deploy-transport-BiicfoQW.mjs +180 -0
- package/dist/deploy.d.mts +10 -0
- package/dist/deploy.mjs +82 -0
- package/dist/dev-inbox-Rd6j_8vM.mjs +217 -0
- package/dist/discover-CTUiSsfG.mjs +2 -0
- package/dist/{discover-CkCYv9T_.mjs → discover-D2PGT63u.mjs} +2 -2
- package/dist/{dist-BVqXeR_h.mjs → dist-ipQi0Q8y.mjs} +2 -3
- package/dist/{domain-BsDUBAvt.mjs → domain-Bfq9n03H.mjs} +7 -22
- package/dist/{dotenv-BfU6eqFt.mjs → dotenv-CNazY3-w.mjs} +4 -16
- package/dist/{email-d_Aux1vM.mjs → email-BcjSyAL0.mjs} +4 -20
- package/dist/{email-_lGAHOhp.mjs → email-DIwaQU4P.mjs} +99 -191
- package/dist/email-cloudflare-CH053RxK.mjs +1096 -0
- package/dist/email-protocol-D9nTvhCp.mjs +82 -0
- package/dist/{entry-CMAkqBmU.mjs → entry-ehmBvs5w.mjs} +2 -2
- package/dist/{env-BxocywBB.mjs → env-BJjULvL0.mjs} +2 -2
- package/dist/{env-DMA3cXZt.mjs → env-BhdWMrNy.mjs} +2 -13
- package/dist/{env-C74QepDE.mjs → env-C6ipBLp3.mjs} +8 -8
- package/dist/{env-public-DR7ZM6d-.d.mts → env-public-CCdykygU.d.mts} +6 -23
- package/dist/{env-raw-jZpP5n47.mjs → env-raw-Cklzcde3.mjs} +3 -20
- package/dist/{env-types-CwXgeMgL.mjs → env-types-DS2_y4X8.mjs} +2 -2
- package/dist/{env-validation-CrVNjsPm.mjs → env-validation-CTn-HhG1.mjs} +1 -1
- package/dist/env-validation-cE0zcmzO.mjs +486 -0
- package/dist/{gen-DtkQhvQg.mjs → gen-CnN1Xidx.mjs} +131 -253
- package/dist/gen-F8pSZnhc.mjs +2 -0
- package/dist/{generate-GDbL7sUr.mjs → generate-Dat0UTry.mjs} +38 -4
- package/dist/generate-Rii9klHB.mjs +2 -0
- package/dist/generated-wrangler-config-DeWj5F9Y.mjs +18 -0
- package/dist/{github-cmd-DaDdBbSS.mjs → github-cmd-BdkU4Nhe.mjs} +27 -71
- package/dist/{handler-BHAn2fZJ.mjs → handler-BZ1t7-Rv.mjs} +2 -2
- package/dist/{handler-CNAwjRcW.d.mts → handler-_PqY82xe.d.mts} +1 -1
- package/dist/{headers-DlfR78Nu.mjs → headers-Di1FsD3e.mjs} +26 -81
- package/dist/{help-0bTe_lID.mjs → help-Bug7pyK_.mjs} +18 -10
- package/dist/help-s9q7LTcu.mjs +2 -0
- package/dist/{inbound-PfQDLXc5.mjs → inbound-DmkTQeWp.mjs} +71 -222
- package/dist/{inbound-CH5Mksyy.d.mts → inbound-brj2-TM5.d.mts} +19 -58
- package/dist/index.d.mts +2 -18
- package/dist/index.mjs +200 -496
- package/dist/{info-PYWwkWyt.mjs → info-CujPwtMe.mjs} +5 -5
- package/dist/{init-DaBGmaXC.mjs → init-ZWOJvc-Z.mjs} +179 -388
- package/dist/isr-config-DPjXM8J2.mjs +194 -0
- package/dist/{link-C6fjafep.mjs → link-UEc7bnF4.mjs} +8 -8
- package/dist/{list-BTFBlqKl.mjs → list-CuWeOnzk.mjs} +6 -17
- package/dist/{live-CgQuTpqD.mjs → live-BxLX-JqJ.mjs} +19 -17
- package/dist/{live-Du8jXwOW.d.mts → live-DJONVlQK.d.mts} +1 -1
- package/dist/load-user-env-DD5qveNU.mjs +49 -0
- package/dist/{local-d1-ga5O_idV.mjs → local-d1-BM3p74mm.mjs} +6 -6
- package/dist/{login-GKcNUmtI.mjs → login-CZ2Cqa-D.mjs} +11 -11
- package/dist/login-DhK7o1oJ.mjs +2 -0
- package/dist/{logs-BiMSULmc.mjs → logs-TihRcLTk.mjs} +5 -5
- package/dist/migrate-BgT-hmu8.mjs +2 -0
- package/dist/{migrate-NLsjBbuM.mjs → migrate-uFwojusJ.mjs} +69 -21
- package/dist/migration-handler-Cg0Y4EPu.mjs +470 -0
- package/dist/{migration-handler-DM4clYj5.d.mts → migration-handler-CwlEQUoo.d.mts} +17 -26
- package/dist/{mime-DxCznlJq.mjs → mime-srOklVVv.mjs} +92 -395
- package/dist/{node-DJicPvPj.mjs → node-DoKbo1SZ.mjs} +8 -24
- package/dist/{operator-args-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-BT4hgavL.mjs → operator-cmd-C5lCctnc.mjs} +8 -8
- package/dist/operator-cmd-Cjk5DiRx.mjs +2 -0
- package/dist/{output-C50ag2r3.mjs → output-FNHKw5Ac.mjs} +36 -5
- package/dist/{package-json-DXfiT1jV.mjs → package-json-BjYqR-So.mjs} +1 -1
- package/dist/pages/client.d.mts +1 -1
- package/dist/pages/client.mjs +91 -5
- package/dist/pages/index.d.mts +1 -2
- package/dist/pages/index.mjs +6 -6
- package/dist/pages/islands-plugin.mjs +5 -83
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +1 -1
- package/dist/pages/serialize.mjs +1 -1
- package/dist/{platform-args-DUeXY9Om.mjs → platform-args-C6Mb_I3a.mjs} +34 -58
- package/dist/platform-args-Cm7x-fot.mjs +2 -0
- package/dist/{platform-auth-config-CkSx3usH.mjs → platform-auth-config-CJmNkFT2.mjs} +10 -11
- package/dist/{platform-auth-protection-BJimB_p9.mjs → platform-auth-protection-D5wvsI-Y.mjs} +36 -26
- package/dist/{platform-auth-recovery-BU6izFAU.mjs → platform-auth-recovery-D_0I3n-1.mjs} +8 -9
- package/dist/{platform-cmd-BbmJiq5O.mjs → platform-cmd-Cuj4oUVo.mjs} +1 -1
- package/dist/{platform-cmd-BbY1dWiM.mjs → platform-cmd-DxxZO6e5.mjs} +15 -15
- package/dist/{platform-discovery-DIYz8V63.mjs → platform-discovery-CHhNs8Nn.mjs} +1 -1
- package/dist/{platform-domain-CHVS6cV0.mjs → platform-domain-zLXnW1G_.mjs} +5 -5
- package/dist/{platform-lifecycle-DzCwqhFq.mjs → platform-lifecycle-DOW-wLfx.mjs} +1398 -2043
- package/dist/{platform-lifecycle-ByAlpkhV.mjs → platform-lifecycle-_gfnjUBr.mjs} +1 -1
- package/dist/{platform-management-CBOT4BSM.mjs → platform-management-CYrLvTGH.mjs} +1 -1
- package/dist/{platform-management-CuuVDhyt.mjs → platform-management-t7HKy4-U.mjs} +632 -124
- package/dist/{platform-plans-config-CN-heS4q.mjs → platform-plans-config-CNH6zbPX.mjs} +15 -11
- package/dist/{platform-recovery-CskSJgLh.mjs → platform-recovery-DeucI7A9.mjs} +6 -9
- package/dist/{platform-registry-Cjv2Qf0r.mjs → platform-registry-B9mWX9LO.mjs} +13 -18
- package/dist/{platform-setup-BBxRxmFC.mjs → platform-setup-CTf6MMT9.mjs} +19 -26
- package/dist/{plugin-inference-BlQbOL0P.mjs → plugin-inference-DsARdQKE.mjs} +18 -33
- package/dist/{prepare-Dq9hTIbo.mjs → prepare-C4zfwks4.mjs} +3 -3
- package/dist/{prepare-Do96654b.mjs → prepare-CSH9dKTI.mjs} +11 -13
- package/dist/prepare-dr9uxQSV.mjs +2 -0
- package/dist/{prerender-render-Ct20jftc.mjs → prerender-render-Bfsumdkv.mjs} +8 -2
- package/dist/prerender-render.d.mts +2 -0
- package/dist/prerender-render.mjs +2 -2
- package/dist/project-DiiOBZPF.mjs +2 -0
- package/dist/{project-cmd-DaoRrn-8.mjs → project-cmd-Dq75AXzy.mjs} +19 -19
- 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-team-Q_lCA7Io.mjs → project-team-B3_QcStG.mjs} +6 -6
- package/dist/{project-token-BArgK9o1.mjs → project-token-CvsRvv2I.mjs} +5 -5
- package/dist/{project-tsconfig-CVJhNMxs.mjs → project-tsconfig-bveqhlYA.mjs} +3 -5
- package/dist/{project-zero-trust-KO9Hu2Pq.mjs → project-zero-trust-D1ICiwN_.mjs} +5 -5
- package/dist/{protocol-BPZaNqBo.d.mts → protocol-4H7V_3Os.d.mts} +1 -1
- package/dist/{protocol-CfaUwNs8.mjs → protocol-RNP0u7UY.mjs} +9 -3
- package/dist/{provision-BfaLiWbj.mjs → provision-DDh7ITVS.mjs} +118 -865
- package/dist/{requests-C7Oeoh5u.mjs → requests-3CL3BuZh.mjs} +4 -4
- package/dist/resolve-project-B9Hr0D3n.mjs +2 -0
- package/dist/resolve-project-C_Cu7E8a.mjs +48 -0
- package/dist/{rollback-DVAtrLAl.mjs → rollback-Ce1d_0J9.mjs} +7 -17
- package/dist/{route-types-CbtsIeFn.mjs → route-types-IrBuwJ7u.mjs} +35 -70
- package/dist/routing-assets-VkrKIbxE.mjs +139 -0
- package/dist/{runner-9wjsSZx-.mjs → runner-B5huK2eC.mjs} +1 -1
- package/dist/{runner-DMBKSrRk.mjs → runner-DcVDrzuR.mjs} +7 -28
- package/dist/{runner-mysql-CEzKtmFb.mjs → runner-mysql-CCtyI0p6.mjs} +4 -3
- package/dist/runner-mysql-CaJq-hd6.mjs +2 -0
- package/dist/runner-pg-BJhFZZG4.mjs +2 -0
- package/dist/{runner-pg-DV8p_zjA.mjs → runner-pg-CPd-CB_F.mjs} +1 -1
- package/dist/runtime/ai.mjs +9 -22
- package/dist/runtime/better-auth-mysql.d.mts +2 -5
- package/dist/runtime/better-auth-mysql.mjs +6 -31
- package/dist/runtime/better-auth-pg.d.mts +2 -5
- package/dist/runtime/better-auth-pg.mjs +6 -32
- package/dist/runtime/better-auth.d.mts +2 -4
- package/dist/runtime/better-auth.mjs +9 -19
- package/dist/runtime/db.mjs +1 -1
- package/dist/runtime/durable.d.mts +16 -0
- package/dist/runtime/durable.mjs +26 -2
- package/dist/runtime/email/testing.d.mts +1 -1
- package/dist/runtime/email/testing.mjs +9 -23
- package/dist/runtime/email-protocol.d.mts +12 -0
- package/dist/runtime/email-protocol.mjs +2 -70
- package/dist/runtime/email.d.mts +1 -1
- package/dist/runtime/email.mjs +79 -221
- package/dist/runtime/env-public.d.mts +1 -1
- package/dist/runtime/env-public.mjs +12 -38
- package/dist/runtime/env.d.mts +1 -12
- package/dist/runtime/env.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/handler.mjs +1 -1
- package/dist/runtime/isr.mjs +1 -1
- package/dist/runtime/kv.mjs +1 -1
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live-server.mjs +1 -16
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/live.mjs +1 -1
- package/dist/runtime/migration-handler-mysql.d.mts +2 -2
- package/dist/runtime/migration-handler-mysql.mjs +3 -2
- package/dist/runtime/migration-handler-pg.d.mts +2 -2
- package/dist/runtime/migration-handler-pg.mjs +1 -1
- package/dist/runtime/migration-handler.d.mts +2 -2
- package/dist/runtime/migration-handler.mjs +2 -470
- package/dist/runtime/queues.mjs +1 -1
- package/dist/runtime/remote/binding-handler.mjs +62 -62
- package/dist/runtime/remote/index.mjs +16 -77
- package/dist/runtime/routing.d.mts +1 -4
- package/dist/runtime/routing.mjs +82 -218
- package/dist/runtime/sandbox-container.d.mts +2 -2
- package/dist/runtime/sandbox-container.mjs +2 -2
- package/dist/runtime/sandbox.d.mts +2 -2
- package/dist/runtime/sandbox.mjs +13 -6
- package/dist/runtime/storage.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +2 -3
- package/dist/runtime/ws-server.mjs +6 -28
- package/dist/runtime/ws.d.mts +1 -1
- package/dist/{sandbox-container-Bo0eiFKz.d.mts → sandbox-container-ChQRum-j.d.mts} +3 -1
- package/dist/{sandbox-container-C6ItmVuN.mjs → sandbox-container-DBKIqDEK.mjs} +12 -1
- package/dist/{sandbox-C5LFoJMf.d.mts → sandbox-q3waMc2T.d.mts} +1 -1
- package/dist/{scan-C3uDzz6J.mjs → scan-C0BMdmEd.mjs} +84 -7
- package/dist/{secret-BvtB7IjY.mjs → secret-B5J8bBsI.mjs} +62 -80
- package/dist/{serialize-BPvnNQuA.mjs → serialize-BtQyaYoC.mjs} +10 -4
- package/dist/{skills-BgVMvQdy.mjs → skills-BJsan98C.mjs} +2 -2
- package/dist/{subcommand-prompt-BSlnD1fI.mjs → subcommand-prompt-gQ-NDA-u.mjs} +38 -43
- package/dist/sveltekit.mjs +1 -1
- package/dist/validate-C9xtFOYR.mjs +2 -0
- package/dist/{validate-C9qJArv7.mjs → validate-CheZGRE7.mjs} +4 -8
- package/dist/version-metadata-DIrqEHQo.mjs +73 -0
- package/dist/{wrangler-D9KTuqnv.mjs → wrangler-BYs07Rxy.mjs} +119 -378
- package/dist/{ws-CChrrAU-.d.mts → ws-aKVYB-fA.d.mts} +1 -1
- package/dist/yarn-pnp-DLbEFMT0.mjs +92 -0
- package/docs/guide/auth.md +2 -0
- package/docs/guide/database/postgresql.md +2 -0
- package/docs/guide/database.md +2 -0
- package/docs/guide/durable-state.md +24 -0
- 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 +3 -1
- package/docs/guide/quickstart.md +2 -0
- package/docs/guide/sandboxes.md +2 -0
- package/docs/reference/api/deploy.md +23 -0
- package/docs/reference/api/pages.md +2 -0
- package/docs/reference/api.md +2 -1
- package/docs/reference/cli/database.md +9 -3
- package/docs/reference/cli/platform-installation.md +1 -1
- package/docs/reference/cli/setup.md +2 -0
- package/package.json +13 -16
- package/skills/void/SKILL.md +1 -0
- 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/collect-DLyFDiKW.mjs +0 -2
- package/dist/config-5r-YpMC-.mjs +0 -2
- package/dist/config-B_ggY-IO.mjs +0 -89
- package/dist/config-BtSFvXZx.mjs +0 -2
- package/dist/config-write-AHGesphx.mjs +0 -61
- package/dist/deploy-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-_UUlL6bg.mjs +0 -2
- package/dist/env-validation-Dxdx_5wo.mjs +0 -951
- package/dist/gen-dA9hW7AV.mjs +0 -2
- package/dist/head-client-DzmJGN4C.mjs +0 -90
- package/dist/help-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/platform-args-D4E1PjVw.mjs +0 -2
- package/dist/prepare-DRUV-g3X.mjs +0 -2
- package/dist/project-DedqCp59.mjs +0 -2
- package/dist/provision-Wu9ZBk9L.mjs +0 -2
- package/dist/resolve-project-BIeYs9Cw.mjs +0 -2
- package/dist/resolve-project-CP8rZs_p.mjs +0 -28
- package/dist/runner-mysql-CkFqZ6CE.mjs +0 -2
- package/dist/runner-pg-BGYRFdRp.mjs +0 -2
- package/dist/runtime/env-helpers.d.mts +0 -2
- package/dist/runtime/env-helpers.mjs +0 -2
- package/dist/validate-BJr9LfXy.mjs +0 -2
- package/dist/yarn-pnp-CUX_iTPA.mjs +0 -188
|
@@ -1,9 +1,596 @@
|
|
|
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-
|
|
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-B9mWX9LO.mjs";
|
|
2
2
|
import { A as confirm, H as outro, K as select, L as log, tt as isCancel } from "./dist-D3m88sjf.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { A as
|
|
5
|
-
import { PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_R2_OWNERSHIP_MARKER_KEY,
|
|
6
|
-
import {
|
|
3
|
+
import { f as import_picocolors, l as formatUrl, m as printSuccess, p as note, r as createSpinner } from "./output-FNHKw5Ac.mjs";
|
|
4
|
+
import { A as revalidatePlatformInstallAction, B as hyperdriveOriginConfigHash, C as formatPlatformInstallPlan, D as platformAdoptionIntentHash, E as matchesRecordedR2Fingerprint, H as hyperdriveOriginMatches, M as assertPlatformOperatorCredentialCanApplyPlan, N as resolvePlatformCloudflareCredential, O as r2BucketConfigHash, P as verifyPlatformCloudflareCredential, R as PlatformCloudflareApiError, S as dnsRecordConfigHash, T as legacyResourceConfigHash, U as verifyPlatformRuntimeTokenPermissions, V as hyperdriveOriginFromConnectionString, _ as resolveBootstrapGitHubId, a as REMOTE_INSTALLATION_MANIFEST_KEY, d as readPlatformProjectZeroTrustState, f as assertProjectZeroTrustAllowsUninstall, g as createPlatformInstallationSecrets, j as platformDatabaseNameFromOwnerUrl, k as resourceMarkerProblem, m as loadPlatformRuntime, n as resolvePlatformInstallDatabaseUrl, o as installPlatform, p as readPlatformDatabase, s as acquirePlatformDatabaseLifecycleLock, t as assertNoninteractivePlatformMutationAcknowledged, u as readPlatformProjectSlugs, w as hasProvisionIntentStep, x as createPlatformInstallPlan, y as platformTrafficAttachmentWorker, z as PlatformCloudflareClient } from "./platform-lifecycle-DOW-wLfx.mjs";
|
|
5
|
+
import { PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_R2_OWNERSHIP_MARKER_KEY, parsePlatformInstallationManifest, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, transitionInstallation } from "@void/platform";
|
|
6
|
+
import { setTimeout } from "node:timers/promises";
|
|
7
|
+
//#region src/cli/platform-teardown.ts
|
|
8
|
+
/**
|
|
9
|
+
* `void platform uninstall --purge-resources` deletes every Cloudflare resource the
|
|
10
|
+
* installation created, in an order Cloudflare accepts:
|
|
11
|
+
*
|
|
12
|
+
* traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
|
|
13
|
+
* -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
|
|
14
|
+
* -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
|
|
15
|
+
*
|
|
16
|
+
* Each resource is proven to belong to this installation right before its
|
|
17
|
+
* DELETE, and Cloudflare refuses a resource that something outside the
|
|
18
|
+
* installation still uses. The only forced DELETE is for a platform Worker
|
|
19
|
+
* that Cloudflare refused while every Worker it names as a dependent is one
|
|
20
|
+
* this installation created: the API and proxy Workers bind to each other, so
|
|
21
|
+
* neither can go first without it. A route, custom domain, DNS
|
|
22
|
+
* record, or Access resource that no longer matches what was recorded is kept
|
|
23
|
+
* and listed with the reason instead of stopping the run. Every finished
|
|
24
|
+
* deletion is checkpointed, so rerunning the command continues where it
|
|
25
|
+
* stopped. The routing KV namespace holds that checkpoint, so uninstall
|
|
26
|
+
* deletes it last.
|
|
27
|
+
*
|
|
28
|
+
* An uninstall without --purge-resources records what it leaves as retained. If it
|
|
29
|
+
* stopped before it finished, --purge-resources considers those resources again.
|
|
30
|
+
*/
|
|
31
|
+
const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
|
|
32
|
+
const WORKER_ORDER = [
|
|
33
|
+
"email-gateway-worker",
|
|
34
|
+
"proxy-worker",
|
|
35
|
+
"dispatch-worker",
|
|
36
|
+
"api-worker"
|
|
37
|
+
];
|
|
38
|
+
const PERMISSIONS = {
|
|
39
|
+
access: "Account → Access: Apps and Policies → Edit",
|
|
40
|
+
aiGateway: "Account → AI Gateway → Edit",
|
|
41
|
+
dns: "Zone → DNS → Edit",
|
|
42
|
+
emailRouting: "Zone → Email Routing Rules → Edit",
|
|
43
|
+
hyperdrive: "Account → Hyperdrive → Edit",
|
|
44
|
+
kv: "Account → Workers KV Storage → Edit",
|
|
45
|
+
queues: "Account → Queues → Edit",
|
|
46
|
+
r2: "Account → Workers R2 Storage → Edit",
|
|
47
|
+
routes: "Zone → Workers Routes → Edit",
|
|
48
|
+
serviceTokens: "Account → Access: Service Tokens → Edit",
|
|
49
|
+
workers: "Account → Workers Scripts → Edit"
|
|
50
|
+
};
|
|
51
|
+
function isPlatformTailWorker(manifest, resource) {
|
|
52
|
+
return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
|
|
53
|
+
}
|
|
54
|
+
function purgeResourcesResumeCommand(manifest) {
|
|
55
|
+
return `void platform uninstall ${manifest.id} --purge-resources`;
|
|
56
|
+
}
|
|
57
|
+
function accessStepKey(access) {
|
|
58
|
+
return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
|
|
59
|
+
}
|
|
60
|
+
/** Access resources that --purge-resources already deleted. */
|
|
61
|
+
function isPurgedAccessResource(manifest, access) {
|
|
62
|
+
return manifest.steps.some((step) => step.key === accessStepKey(access));
|
|
63
|
+
}
|
|
64
|
+
function hasStep(manifest, key) {
|
|
65
|
+
return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
|
|
66
|
+
}
|
|
67
|
+
function keptStepKey(key) {
|
|
68
|
+
return `kept:${key}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Why --purge-resources kept a resource it was going to delete, for example because
|
|
72
|
+
* a route now serves another Worker; null when it did not keep it. Access
|
|
73
|
+
* resources use the key `access:<key>`.
|
|
74
|
+
*/
|
|
75
|
+
function purgeResourcesKeptReason(manifest, key) {
|
|
76
|
+
return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
|
|
77
|
+
}
|
|
78
|
+
/** An uninstall that started has not finished, so what it kept can be reconsidered. */
|
|
79
|
+
function isReopenable(manifest) {
|
|
80
|
+
return manifest.status !== "uninstalled";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A resource the installation created that --purge-resources may still delete: one
|
|
84
|
+
* that is ready, or one that an uninstall which stopped before it finished
|
|
85
|
+
* recorded as retained. Resources --purge-resources itself kept, and earlier
|
|
86
|
+
* versions of a resource kept when it was replaced, are never reconsidered.
|
|
87
|
+
*/
|
|
88
|
+
function isPurgeResourcesCandidate(manifest, resource) {
|
|
89
|
+
return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
|
|
90
|
+
}
|
|
91
|
+
/** The zone that routes and DNS records live in, if it can still be used. */
|
|
92
|
+
function usableZone(manifest) {
|
|
93
|
+
return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A recorded resource whose live configuration no longer matches the
|
|
97
|
+
* installation's record. --purge-resources keeps such a DNS record and continues.
|
|
98
|
+
*/
|
|
99
|
+
var PlatformResourceChangedError = class extends Error {
|
|
100
|
+
resourceKey;
|
|
101
|
+
reason;
|
|
102
|
+
constructor(resourceKey, reason, message) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.resourceKey = resourceKey;
|
|
105
|
+
this.reason = reason;
|
|
106
|
+
this.name = "PlatformResourceChangedError";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/** The zone whose Email Routing rules may send mail to the gateway Worker. */
|
|
110
|
+
function emailRoutingZone(manifest) {
|
|
111
|
+
return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
|
|
112
|
+
}
|
|
113
|
+
/** Everything --purge-resources will delete, in deletion order. */
|
|
114
|
+
function purgeResourcesTargets(manifest) {
|
|
115
|
+
const resources = manifest.resources;
|
|
116
|
+
const routing = resources.find((resource) => resource.key === "routing-kv");
|
|
117
|
+
const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
|
|
118
|
+
if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
|
|
119
|
+
const zone = usableZone(manifest);
|
|
120
|
+
const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
|
|
121
|
+
const targets = [];
|
|
122
|
+
for (const resource of resources) {
|
|
123
|
+
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;
|
|
124
|
+
const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
|
|
125
|
+
const worker = workers.find((entry) => entry.name === name);
|
|
126
|
+
if (worker) targets.push({
|
|
127
|
+
phase: "traffic",
|
|
128
|
+
resource,
|
|
129
|
+
worker
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
|
|
133
|
+
phase: "dns",
|
|
134
|
+
resource
|
|
135
|
+
});
|
|
136
|
+
const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
|
|
137
|
+
for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
|
|
138
|
+
phase: "access",
|
|
139
|
+
access: entry
|
|
140
|
+
});
|
|
141
|
+
const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
|
|
142
|
+
if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
|
|
143
|
+
phase: "email",
|
|
144
|
+
resource: gateway
|
|
145
|
+
});
|
|
146
|
+
const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
|
|
147
|
+
for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
|
|
148
|
+
phase: "queue-consumers",
|
|
149
|
+
resource
|
|
150
|
+
});
|
|
151
|
+
const rank = (resource) => {
|
|
152
|
+
const index = WORKER_ORDER.indexOf(resource.key);
|
|
153
|
+
return index < 0 ? WORKER_ORDER.length : index;
|
|
154
|
+
};
|
|
155
|
+
for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
|
|
156
|
+
phase: "workers",
|
|
157
|
+
resource
|
|
158
|
+
});
|
|
159
|
+
for (const resource of queues) targets.push({
|
|
160
|
+
phase: "queues",
|
|
161
|
+
resource
|
|
162
|
+
});
|
|
163
|
+
for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
|
|
164
|
+
phase: "dispatch",
|
|
165
|
+
resource
|
|
166
|
+
});
|
|
167
|
+
for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
|
|
168
|
+
phase: "tail",
|
|
169
|
+
resource
|
|
170
|
+
});
|
|
171
|
+
for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
|
|
172
|
+
phase: "data",
|
|
173
|
+
resource
|
|
174
|
+
});
|
|
175
|
+
for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
|
|
176
|
+
phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
|
|
177
|
+
resource
|
|
178
|
+
});
|
|
179
|
+
if (candidate(routing)) targets.push({
|
|
180
|
+
phase: "routing",
|
|
181
|
+
resource: routing
|
|
182
|
+
});
|
|
183
|
+
return targets;
|
|
184
|
+
}
|
|
185
|
+
/** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
|
|
186
|
+
function purgeResourcesHandlesEmailRouting(manifest) {
|
|
187
|
+
return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
|
|
188
|
+
}
|
|
189
|
+
/** Manifest resources that --purge-resources leaves in the account. */
|
|
190
|
+
function purgeResourcesKeptResources(manifest) {
|
|
191
|
+
const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
|
|
192
|
+
return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
|
|
193
|
+
}
|
|
194
|
+
function resourceLabel(resource) {
|
|
195
|
+
return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
|
|
196
|
+
}
|
|
197
|
+
/** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
|
|
198
|
+
function describePurgeResourcesPlan(manifest) {
|
|
199
|
+
const targets = purgeResourcesTargets(manifest);
|
|
200
|
+
const lines = [];
|
|
201
|
+
let step = 0;
|
|
202
|
+
let previous = null;
|
|
203
|
+
const consumerQueues = [];
|
|
204
|
+
for (const target of targets) {
|
|
205
|
+
if (target.phase === "email") {
|
|
206
|
+
step++;
|
|
207
|
+
previous = target.phase;
|
|
208
|
+
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)`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (target.phase !== previous) {
|
|
212
|
+
step++;
|
|
213
|
+
previous = target.phase;
|
|
214
|
+
}
|
|
215
|
+
const prefix = ` ${step}. `;
|
|
216
|
+
if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
|
|
217
|
+
else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
|
|
218
|
+
else if (target.phase === "queue-consumers") {
|
|
219
|
+
consumerQueues.push(target.resource.name);
|
|
220
|
+
if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
|
|
221
|
+
lines.push(` ${target.resource.name}`);
|
|
222
|
+
} else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
|
|
223
|
+
else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
|
|
224
|
+
else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
|
|
225
|
+
else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
|
|
226
|
+
else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
|
|
227
|
+
else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
|
|
228
|
+
else lines.push(`${prefix}${resourceLabel(target.resource)}`);
|
|
229
|
+
}
|
|
230
|
+
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."];
|
|
231
|
+
}
|
|
232
|
+
/** Lines naming what --purge-resources never removes. */
|
|
233
|
+
function purgeResourcesAlwaysKept() {
|
|
234
|
+
return [
|
|
235
|
+
" The external PostgreSQL database and its data.",
|
|
236
|
+
" Analytics Engine datasets; Cloudflare has no API to delete them.",
|
|
237
|
+
" Access policies and identity providers you chose.",
|
|
238
|
+
" API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
/** Token permissions --purge-resources needs for this manifest. */
|
|
242
|
+
function purgeResourcesPermissions(manifest) {
|
|
243
|
+
const targets = purgeResourcesTargets(manifest);
|
|
244
|
+
const needed = /* @__PURE__ */ new Set();
|
|
245
|
+
for (const target of targets) {
|
|
246
|
+
if (target.phase === "access") {
|
|
247
|
+
needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (target.phase === "email") {
|
|
251
|
+
needed.add(PERMISSIONS.emailRouting);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const { kind } = target.resource;
|
|
255
|
+
if (kind === "worker-route") needed.add(PERMISSIONS.routes);
|
|
256
|
+
else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
|
|
257
|
+
else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
|
|
258
|
+
else if (kind === "queue") needed.add(PERMISSIONS.queues);
|
|
259
|
+
else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
|
|
260
|
+
else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
|
|
261
|
+
else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
|
|
262
|
+
else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
|
|
263
|
+
}
|
|
264
|
+
return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
|
|
265
|
+
}
|
|
266
|
+
function deniedStatus(error) {
|
|
267
|
+
return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check resource reads before the first change. Browser login cannot change
|
|
271
|
+
* DNS or Access, so those need an API token. Read probes cannot prove Edit;
|
|
272
|
+
* the plan lists the required permissions.
|
|
273
|
+
*/
|
|
274
|
+
async function checkPurgeResourcesCredentials(client, manifest, credential) {
|
|
275
|
+
const targets = purgeResourcesTargets(manifest);
|
|
276
|
+
const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
|
|
277
|
+
const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
|
|
278
|
+
const has = (kind) => resources.some((resource) => resource.kind === kind);
|
|
279
|
+
const zone = manifest.resources.find((resource) => resource.key === "zone");
|
|
280
|
+
const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
|
|
281
|
+
const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
|
|
282
|
+
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}` : ""}`;
|
|
283
|
+
if (credential.source === "oauth") {
|
|
284
|
+
const browserMissing = [
|
|
285
|
+
...has("dns-record") ? [PERMISSIONS.dns] : [],
|
|
286
|
+
...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
|
|
287
|
+
...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
|
|
288
|
+
...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
|
|
289
|
+
];
|
|
290
|
+
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.`);
|
|
291
|
+
}
|
|
292
|
+
const probes = [];
|
|
293
|
+
if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
|
|
294
|
+
if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
|
|
295
|
+
if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
|
|
296
|
+
for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
|
|
297
|
+
else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
|
|
298
|
+
for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
|
|
299
|
+
if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
|
|
300
|
+
if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
|
|
301
|
+
if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
|
|
302
|
+
if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
|
|
303
|
+
if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
|
|
304
|
+
if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
|
|
305
|
+
const missing = /* @__PURE__ */ new Set();
|
|
306
|
+
for (const [permission, probe] of probes) try {
|
|
307
|
+
await probe();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (!deniedStatus(error)) throw error;
|
|
310
|
+
missing.add(permission);
|
|
311
|
+
}
|
|
312
|
+
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.`);
|
|
313
|
+
}
|
|
314
|
+
const LISTED_PROJECTS = 20;
|
|
315
|
+
/**
|
|
316
|
+
* Projects own Workers, storage, and domains that only the running platform
|
|
317
|
+
* can delete. --purge-resources deletes the platform, so it refuses before any
|
|
318
|
+
* change while a project remains.
|
|
319
|
+
*/
|
|
320
|
+
async function assertNoPlatformProjects(read) {
|
|
321
|
+
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.`);
|
|
322
|
+
if (slugs.length) throw new Error([
|
|
323
|
+
`platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
|
|
324
|
+
"--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
|
|
325
|
+
"Delete each project first with `void project delete <name>`, then retry:",
|
|
326
|
+
...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
|
|
327
|
+
...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
|
|
328
|
+
].join("\n"));
|
|
329
|
+
}
|
|
330
|
+
const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
|
|
331
|
+
/**
|
|
332
|
+
* AI Gateway ids are chosen by the caller and can be reused after a delete,
|
|
333
|
+
* and gateways carry no ownership marker. Only a creation time next to the
|
|
334
|
+
* installer's provision intent shows that this is the gateway it created.
|
|
335
|
+
*/
|
|
336
|
+
function createdDuringInstallation(manifest, resource, createdAt) {
|
|
337
|
+
const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
|
|
338
|
+
if (Number.isNaN(created)) return false;
|
|
339
|
+
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);
|
|
340
|
+
}
|
|
341
|
+
function ownershipRefusal(manifest, description, reason) {
|
|
342
|
+
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)}`);
|
|
343
|
+
}
|
|
344
|
+
function cloudflareRefusal(manifest, description, error, hint) {
|
|
345
|
+
if (!(error instanceof PlatformCloudflareApiError)) return error;
|
|
346
|
+
return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Delete every target in order. Returns the manifest with each deleted
|
|
350
|
+
* resource recorded; routing KV is left for the caller to delete last.
|
|
351
|
+
*/
|
|
352
|
+
async function runPurgeResourcesTeardown(context, initial) {
|
|
353
|
+
const { client } = context;
|
|
354
|
+
let next = initial;
|
|
355
|
+
const accountId = next.account.id;
|
|
356
|
+
const save = async (manifest) => {
|
|
357
|
+
next = manifest;
|
|
358
|
+
await context.checkpoint(next);
|
|
359
|
+
};
|
|
360
|
+
const markDeleted = async (key) => {
|
|
361
|
+
const resource = next.resources.find((entry) => entry.key === key);
|
|
362
|
+
await save(recordInstallationResource(next, {
|
|
363
|
+
...resource,
|
|
364
|
+
state: "deleted"
|
|
365
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
366
|
+
};
|
|
367
|
+
const recordStep = async (key, resultHash = "completed") => {
|
|
368
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
369
|
+
await save(recordInstallationStep(next, {
|
|
370
|
+
completedAt: now,
|
|
371
|
+
key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
|
|
372
|
+
resultHash
|
|
373
|
+
}, now));
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Live proof for these keys. A resource an earlier uninstall recorded as
|
|
377
|
+
* retained is proven like a ready one; only its recorded state differs.
|
|
378
|
+
*/
|
|
379
|
+
const verify = (keys) => context.verify({
|
|
380
|
+
...next,
|
|
381
|
+
resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
|
|
382
|
+
...resource,
|
|
383
|
+
state: "ready"
|
|
384
|
+
} : resource)
|
|
385
|
+
}, keys);
|
|
386
|
+
/**
|
|
387
|
+
* Prove the resource; false when it is already gone (and recorded).
|
|
388
|
+
*
|
|
389
|
+
* Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
|
|
390
|
+
* addressed by name, and a route or custom domain can be changed in place.
|
|
391
|
+
* Something replaced between this proof and the DELETE can still be removed.
|
|
392
|
+
* That one-request window is accepted because --purge-resources is opt-in and its
|
|
393
|
+
* plan lists every resource first. A forced Worker DELETE has the same
|
|
394
|
+
* window between reading its dependents and the DELETE.
|
|
395
|
+
*/
|
|
396
|
+
const prove = async (resource) => {
|
|
397
|
+
if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
|
|
398
|
+
await markDeleted(resource.key);
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
};
|
|
403
|
+
/** Keep a resource that no longer matches the record, and save why. */
|
|
404
|
+
const keep = async (key, reason) => {
|
|
405
|
+
const resource = next.resources.find((entry) => entry.key === key);
|
|
406
|
+
if (resource?.state === "ready") await save(recordInstallationResource(next, {
|
|
407
|
+
...resource,
|
|
408
|
+
state: "retained"
|
|
409
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
410
|
+
await recordStep(keptStepKey(key), reason);
|
|
411
|
+
};
|
|
412
|
+
const remove = async (resource, hint, zoneId) => {
|
|
413
|
+
if (!await prove(resource)) return;
|
|
414
|
+
try {
|
|
415
|
+
await client.deleteResource(accountId, resource, zoneId);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
|
|
418
|
+
}
|
|
419
|
+
await markDeleted(resource.key);
|
|
420
|
+
};
|
|
421
|
+
const zone = usableZone(next);
|
|
422
|
+
const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
|
|
423
|
+
/** The recorded Worker under this name that this run still deletes, if any. */
|
|
424
|
+
const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
|
|
425
|
+
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.";
|
|
426
|
+
/**
|
|
427
|
+
* Delete a platform Worker. When Cloudflare refuses because other Workers
|
|
428
|
+
* still bind to it, force the DELETE only if each of them is a Worker this
|
|
429
|
+
* run deletes too and still carries this installation's ownership marker.
|
|
430
|
+
*/
|
|
431
|
+
const removeWorker = async (resource) => {
|
|
432
|
+
if (!await prove(resource)) return;
|
|
433
|
+
try {
|
|
434
|
+
await client.deleteResource(accountId, resource);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (!(error instanceof PlatformCloudflareApiError)) throw error;
|
|
437
|
+
const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
|
|
438
|
+
const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
|
|
439
|
+
const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
|
|
440
|
+
const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
|
|
441
|
+
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);
|
|
442
|
+
try {
|
|
443
|
+
await client.forceDeleteWorker(accountId, resource.name);
|
|
444
|
+
} catch (forced) {
|
|
445
|
+
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
await markDeleted(resource.key);
|
|
449
|
+
};
|
|
450
|
+
for (const target of purgeResourcesTargets(initial)) {
|
|
451
|
+
if (target.phase === "access") {
|
|
452
|
+
const { access } = target;
|
|
453
|
+
const live = await client.getAccessResource(access.accountId, access.kind, access.id);
|
|
454
|
+
if (live) {
|
|
455
|
+
if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
|
|
456
|
+
await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
await client.deleteAccessResource(access.accountId, access.kind, access.id);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await recordStep(`access:${access.key}`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const current = next.resources.find((resource) => resource.key === target.resource.key);
|
|
469
|
+
if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
|
|
470
|
+
switch (target.phase) {
|
|
471
|
+
case "traffic": {
|
|
472
|
+
const worker = next.resources.find((resource) => resource.key === target.worker.key);
|
|
473
|
+
if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
|
|
474
|
+
if (current.kind === "worker-route") {
|
|
475
|
+
const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
|
|
476
|
+
if (live && (live.pattern !== current.name || live.script !== worker.name)) {
|
|
477
|
+
await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
if (live) await client.deleteWorkerRoute(zone.id, current.id);
|
|
481
|
+
} else {
|
|
482
|
+
const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
|
|
483
|
+
if (live && (live.hostname !== current.name || live.service !== worker.name)) {
|
|
484
|
+
await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (live) await client.deleteWorkerDomain(accountId, current.id);
|
|
488
|
+
}
|
|
489
|
+
await markDeleted(current.key);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case "dns":
|
|
493
|
+
try {
|
|
494
|
+
await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
|
|
497
|
+
await keep(current.key, error.reason);
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
case "email": {
|
|
501
|
+
if (!await prove(current)) break;
|
|
502
|
+
const zoneId = emailRoutingZone(next);
|
|
503
|
+
const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
|
|
504
|
+
try {
|
|
505
|
+
for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
|
|
506
|
+
if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
|
|
509
|
+
}
|
|
510
|
+
await recordStep("email-routing");
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "queue-consumers":
|
|
514
|
+
if (!await prove(current)) break;
|
|
515
|
+
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);
|
|
516
|
+
await recordStep(`consumers:${current.key}`);
|
|
517
|
+
break;
|
|
518
|
+
case "workers":
|
|
519
|
+
await removeWorker(current);
|
|
520
|
+
break;
|
|
521
|
+
case "tail":
|
|
522
|
+
await remove(current, workerHint);
|
|
523
|
+
break;
|
|
524
|
+
case "queues":
|
|
525
|
+
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.");
|
|
526
|
+
break;
|
|
527
|
+
case "dispatch": {
|
|
528
|
+
if (!await prove(current)) break;
|
|
529
|
+
const projects = await context.readProjects();
|
|
530
|
+
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)}`);
|
|
531
|
+
for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
|
|
532
|
+
await client.deleteDispatchNamespaceScript(accountId, current.name, script);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
await client.deleteResource(accountId, current);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
|
|
540
|
+
}
|
|
541
|
+
await markDeleted(current.key);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case "ai-gateway": {
|
|
545
|
+
const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
|
|
546
|
+
if (live && !createdDuringInstallation(next, current, live.createdAt)) {
|
|
547
|
+
await keep(current.key, "Cloudflare does not show it was created during installation");
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case "data":
|
|
554
|
+
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
555
|
+
break;
|
|
556
|
+
case "r2": await purgeBucket(current);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return next;
|
|
560
|
+
async function purgeBucket(bucket) {
|
|
561
|
+
const markerStep = `r2-marker:${bucket.key}`;
|
|
562
|
+
if (!hasStep(next, markerStep)) {
|
|
563
|
+
if (!await prove(bucket)) return;
|
|
564
|
+
await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
|
|
565
|
+
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");
|
|
566
|
+
});
|
|
567
|
+
if (!await prove(bucket)) return;
|
|
568
|
+
const generation = await context.bucketGeneration(next, bucket);
|
|
569
|
+
if (generation === null) {
|
|
570
|
+
await markDeleted(bucket.key);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
await recordStep(markerStep, generation);
|
|
574
|
+
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
575
|
+
} else {
|
|
576
|
+
const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
|
|
577
|
+
const generation = await context.bucketGeneration(next, bucket);
|
|
578
|
+
if (generation === null) {
|
|
579
|
+
await markDeleted(bucket.key);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
|
|
583
|
+
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
await client.deleteResource(accountId, bucket);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
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.");
|
|
589
|
+
}
|
|
590
|
+
await markDeleted(bucket.key);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
7
594
|
//#region src/cli/platform-management.ts
|
|
8
595
|
async function verifyPlatformInstallationOwnership(client, manifest, options = {}) {
|
|
9
596
|
const ready = manifest.resources.filter((resource) => resource.state === "ready" && isOwnershipVerifiableKind(resource.kind) && !options.retainedReplacementKeys?.has(resource.key));
|
|
@@ -64,8 +651,7 @@ async function runInstalledPlatformCommand(args) {
|
|
|
64
651
|
const rollbackSourceRuntime = args.subcommand === "rollback" ? loadPlatformRuntime(args.fromRuntime) : null;
|
|
65
652
|
let progress;
|
|
66
653
|
let reportResult;
|
|
67
|
-
const execute = async (
|
|
68
|
-
const manifest = currentManifest;
|
|
654
|
+
const execute = async (manifest, databaseLock, databaseCheckpointExists = false) => {
|
|
69
655
|
if (args.subcommand === "rollback") assertPlatformRollbackCompatibility(manifest, runtime, rollbackSourceRuntime);
|
|
70
656
|
const recovery = readInstallationRecoverySecrets(manifest);
|
|
71
657
|
const gatewayToken = process.env.VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN || recovery?.cloudflareApiToken;
|
|
@@ -233,7 +819,7 @@ async function runInstalledPlatformCommand(args) {
|
|
|
233
819
|
if (result.setupPending) {
|
|
234
820
|
const code = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
235
821
|
if (code) {
|
|
236
|
-
log.info(`First administrator setup: ${plan.apiUrl}/setup`);
|
|
822
|
+
log.info(`First administrator setup: ${formatUrl(`${plan.apiUrl}/setup`)}`);
|
|
237
823
|
console.log(`One-time administrator setup code: ${code}`);
|
|
238
824
|
}
|
|
239
825
|
}
|
|
@@ -295,7 +881,7 @@ function assertPlatformRollbackCompatibility(installation, target, source) {
|
|
|
295
881
|
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
882
|
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
883
|
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 (
|
|
884
|
+
if (target.manifest.safetyProtocols.sandboxDrain < installation.safetyProtocols.sandboxDrain) throw new Error("platform: The rollback target predates the installed sandbox-drain safety protocol.");
|
|
299
885
|
}
|
|
300
886
|
function resolvePlatformRuntimeTokenToPreflight(plan, secrets) {
|
|
301
887
|
const explicitUpdate = secrets.workerSecretUpdates?.CF_API_TOKEN;
|
|
@@ -319,18 +905,17 @@ async function resolveInstalledPlatformManifest(selector, dependencies = {}) {
|
|
|
319
905
|
return resolveInstallationManifest(String(picked));
|
|
320
906
|
}
|
|
321
907
|
async function disablePlatform(client, initialManifest, planOnly, dependencies = {}) {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
908
|
+
const manifest = await recoverDurablyMarkedProvisionIntents(client, initialManifest);
|
|
909
|
+
const saveCheckpoint = (state) => writeLifecycleCheckpoint(client, state, dependencies.checkpoint);
|
|
910
|
+
const saveRemoteManifest = (state) => writeRemoteManifest(client, state, dependencies.checkpoint);
|
|
325
911
|
note("Block all platform traffic and the platform API. Routes, custom domains, Workers, and data stay installed.", "Disable plan");
|
|
326
912
|
if (planOnly) {
|
|
327
913
|
outro("No Cloudflare or local installation state was changed.");
|
|
328
914
|
return;
|
|
329
915
|
}
|
|
330
|
-
const manifest = reconciled;
|
|
331
916
|
if (manifest !== initialManifest) writeInstallationManifest(manifest);
|
|
332
917
|
const alreadyDisabled = manifest.status === "disabled";
|
|
333
|
-
|
|
918
|
+
await verifyPlatformMutationOwnership(client, manifest, /* @__PURE__ */ new Set([
|
|
334
919
|
"routing-kv",
|
|
335
920
|
"zone",
|
|
336
921
|
...manifest.resources.filter((resource) => resource.kind === "worker").map((resource) => resource.key)
|
|
@@ -342,30 +927,27 @@ async function disablePlatform(client, initialManifest, planOnly, dependencies =
|
|
|
342
927
|
writeInstallationManifest(next);
|
|
343
928
|
}
|
|
344
929
|
try {
|
|
345
|
-
await
|
|
346
|
-
const zone = next.resources.find((resource) => resource.key === "zone");
|
|
930
|
+
await saveRemoteManifest(next);
|
|
347
931
|
const routing = requireResource(next, "routing-kv");
|
|
348
932
|
const trafficGeneration = currentTrafficGeneration(next);
|
|
349
933
|
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
934
|
next = markTrafficAttachmentsRetained(next);
|
|
353
935
|
if (!hasLifecycleStep(next, `disable:${trafficGeneration}:routes`)) next = recordLifecycleStep(next, `disable:${trafficGeneration}:routes`);
|
|
354
|
-
await
|
|
936
|
+
await saveCheckpoint(next);
|
|
355
937
|
next = transitionInstallation(next, "disabled", (/* @__PURE__ */ new Date()).toISOString());
|
|
356
|
-
await
|
|
938
|
+
await saveCheckpoint(next);
|
|
357
939
|
printSuccess(`${next.deployment.displayName} is disabled. Its data was not changed.`);
|
|
358
940
|
} catch (error) {
|
|
359
941
|
next = transitionInstallation(next, "failed", (/* @__PURE__ */ new Date()).toISOString());
|
|
360
942
|
writeInstallationManifest(next);
|
|
361
|
-
await
|
|
943
|
+
await saveRemoteManifest(next).catch(() => {});
|
|
362
944
|
throw error;
|
|
363
945
|
}
|
|
364
946
|
}
|
|
365
947
|
async function uninstallPlatform(client, initialManifest, args, dependencies = {}) {
|
|
366
948
|
const manifest = await recoverDurablyMarkedProvisionIntents(client, initialManifest);
|
|
367
|
-
const
|
|
368
|
-
const
|
|
949
|
+
const saveCheckpoint = (state) => writeLifecycleCheckpoint(client, state, dependencies.checkpoint);
|
|
950
|
+
const saveRemoteManifest = (state) => writeRemoteManifest(client, state, dependencies.checkpoint);
|
|
369
951
|
const retainData = !args.purgeData;
|
|
370
952
|
const purgeResources = args.purgeResources;
|
|
371
953
|
const routingKv = manifest.resources.find((resource) => resource.key === "routing-kv");
|
|
@@ -377,15 +959,14 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
377
959
|
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
960
|
await assertProjectZeroTrustAllowsUninstall(dependencies.readProjectZeroTrust);
|
|
379
961
|
if (purgeResources) await assertNoPlatformProjects(dependencies.readProjects);
|
|
380
|
-
const purgeResourcesClient = purgeResources ? requirePurgeResourcesClient(client) : null;
|
|
381
962
|
if (args.plan || interactive && !args.yes) note(purgeResources ? purgeResourcesPlanText(manifest) : [
|
|
382
963
|
"Disable platform traffic.",
|
|
383
964
|
retainData ? "Keep Workers and data." : "Delete eligible data; keep Workers, R2, and AI Gateway.",
|
|
384
965
|
"Delete unused Queues.",
|
|
385
966
|
...describeKeptResources(manifest, plannedKeptResources(manifest, retainData, hasRoutingCheckpoint), "Keep for manual cleanup:", "Keep Access resources:")
|
|
386
967
|
].join("\n"), import_picocolors.default.bold("Uninstall plan"));
|
|
387
|
-
if (
|
|
388
|
-
const credentialCheck = checkPurgeResourcesCredentials(
|
|
968
|
+
if (purgeResources) {
|
|
969
|
+
const credentialCheck = checkPurgeResourcesCredentials(client, manifest, dependencies.credential ?? {
|
|
389
970
|
source: "environment",
|
|
390
971
|
aiGatewayToken: false
|
|
391
972
|
});
|
|
@@ -399,7 +980,6 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
399
980
|
return;
|
|
400
981
|
}
|
|
401
982
|
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
983
|
if (interactive && !args.yes) {
|
|
404
984
|
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
985
|
outro("No changes made.");
|
|
@@ -455,7 +1035,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
455
1035
|
throw error;
|
|
456
1036
|
}
|
|
457
1037
|
try {
|
|
458
|
-
await
|
|
1038
|
+
await saveRemoteManifest(next);
|
|
459
1039
|
const routing = hasRoutingCheckpoint ? requireResource(next, "routing-kv") : void 0;
|
|
460
1040
|
disablingTraffic = true;
|
|
461
1041
|
if (routing) await client.putKvValue(next.account.id, routing.id, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
@@ -463,7 +1043,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
463
1043
|
for (const key of missingResources) {
|
|
464
1044
|
const resource = requireResource(next, key);
|
|
465
1045
|
next = updateResourceState(next, key, resource.ownership === "created" ? "deleted" : "retained");
|
|
466
|
-
await
|
|
1046
|
+
await saveCheckpoint(next);
|
|
467
1047
|
}
|
|
468
1048
|
const workers = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.state === "ready").map((resource) => resource.name));
|
|
469
1049
|
const zone = next.resources.find((resource) => resource.key === "zone");
|
|
@@ -473,18 +1053,16 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
473
1053
|
"zone",
|
|
474
1054
|
worker.key
|
|
475
1055
|
]), { 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
1056
|
next = markTrafficAttachmentsRetained(next);
|
|
479
1057
|
next = recordLifecycleStep(next, `uninstall:routes:${worker.key}`);
|
|
480
|
-
await
|
|
1058
|
+
await saveCheckpoint(next);
|
|
481
1059
|
}
|
|
482
1060
|
}
|
|
483
|
-
if (
|
|
484
|
-
client
|
|
1061
|
+
if (purgeResources && hasRoutingCheckpoint) next = await runPurgeResourcesTeardown({
|
|
1062
|
+
client,
|
|
485
1063
|
checkpoint: async (checkpointManifest) => {
|
|
486
1064
|
next = checkpointManifest;
|
|
487
|
-
await
|
|
1065
|
+
await saveCheckpoint(checkpointManifest);
|
|
488
1066
|
},
|
|
489
1067
|
verify: (checkpointManifest, keys) => verifyPlatformMutationOwnership(client, checkpointManifest, keys, {
|
|
490
1068
|
allowDeletedRoutingCheckpoint: true,
|
|
@@ -518,18 +1096,18 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
518
1096
|
if (resource.state !== "ready") continue;
|
|
519
1097
|
if (!hasRoutingCheckpoint && resource.kind !== "zone") {
|
|
520
1098
|
next = updateResourceState(next, resource.key, "retained");
|
|
521
|
-
await
|
|
1099
|
+
await saveCheckpoint(next);
|
|
522
1100
|
continue;
|
|
523
1101
|
}
|
|
524
1102
|
if (!shouldDeleteResource(resource, retainData)) {
|
|
525
1103
|
const removed = resource.kind === "ai-gateway" && resource.ownership === "created" && !hasNamedResource(await client.listAiGateways(next.account.id), resource);
|
|
526
1104
|
next = updateResourceState(next, resource.key, removed ? "deleted" : "retained");
|
|
527
|
-
await
|
|
1105
|
+
await saveCheckpoint(next);
|
|
528
1106
|
continue;
|
|
529
1107
|
}
|
|
530
1108
|
if (resource.kind === "dns-record" && !zone || resource.kind === "queue" && queuesInUse) {
|
|
531
1109
|
next = updateResourceState(next, resource.key, "retained");
|
|
532
|
-
await
|
|
1110
|
+
await saveCheckpoint(next);
|
|
533
1111
|
continue;
|
|
534
1112
|
}
|
|
535
1113
|
if ((await verifyPlatformMutationOwnership(client, next, /* @__PURE__ */ new Set(["routing-kv", resource.key]), {
|
|
@@ -537,15 +1115,15 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
537
1115
|
databaseUrl: dependencies.databaseUrl
|
|
538
1116
|
})).has(resource.key)) {
|
|
539
1117
|
next = updateResourceState(next, resource.key, resource.ownership === "created" ? "deleted" : "retained");
|
|
540
|
-
await
|
|
1118
|
+
await saveCheckpoint(next);
|
|
541
1119
|
continue;
|
|
542
1120
|
}
|
|
543
1121
|
await client.deleteResource(next.account.id, resource, zone?.id);
|
|
544
1122
|
next = updateResourceState(next, resource.key, "deleted");
|
|
545
|
-
await
|
|
1123
|
+
await saveCheckpoint(next);
|
|
546
1124
|
}
|
|
547
1125
|
const completed = transitionInstallation(next, "uninstalled", (/* @__PURE__ */ new Date()).toISOString());
|
|
548
|
-
await
|
|
1126
|
+
await saveRemoteManifest(completed);
|
|
549
1127
|
next = completed;
|
|
550
1128
|
writeInstallationManifest(completed);
|
|
551
1129
|
if (!retainData) removeInstallationRecoverySecrets(next);
|
|
@@ -557,7 +1135,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
|
|
|
557
1135
|
await stopProjectZeroTrustHold().catch(() => {});
|
|
558
1136
|
next = transitionInstallation(next, "failed", (/* @__PURE__ */ new Date()).toISOString());
|
|
559
1137
|
writeInstallationManifest(next);
|
|
560
|
-
await
|
|
1138
|
+
await saveRemoteManifest(next).catch(() => {});
|
|
561
1139
|
throw error;
|
|
562
1140
|
}
|
|
563
1141
|
}
|
|
@@ -593,7 +1171,7 @@ async function recoverDurablyMarkedProvisionIntents(client, manifest) {
|
|
|
593
1171
|
"r2-bucket",
|
|
594
1172
|
manifest.deployment.resources.assetsBucket
|
|
595
1173
|
]
|
|
596
|
-
].filter(([key, kind, name]) => !manifest.resources.some((resource) => resource.key === key) &&
|
|
1174
|
+
].filter(([key, kind, name]) => !manifest.resources.some((resource) => resource.key === key) && hasProvisionIntentStep(manifest, key, kind, name));
|
|
597
1175
|
if (pending.length === 0) return manifest;
|
|
598
1176
|
const [workers, buckets] = await Promise.all([client.listWorkerNames(manifest.account.id), client.listR2Buckets(manifest.account.id)]);
|
|
599
1177
|
let recovered = manifest;
|
|
@@ -604,10 +1182,7 @@ async function recoverDurablyMarkedProvisionIntents(client, manifest) {
|
|
|
604
1182
|
} : null : buckets.find((bucket) => bucket.name === name) ?? null;
|
|
605
1183
|
if (!live) continue;
|
|
606
1184
|
const resource = {
|
|
607
|
-
configHash:
|
|
608
|
-
kind,
|
|
609
|
-
name
|
|
610
|
-
})).digest("hex"),
|
|
1185
|
+
configHash: legacyResourceConfigHash(kind, name),
|
|
611
1186
|
id: live.id,
|
|
612
1187
|
key,
|
|
613
1188
|
kind,
|
|
@@ -667,28 +1242,6 @@ function purgeResourcesPlanText(manifest) {
|
|
|
667
1242
|
...permissions.length ? [`API token needs: ${permissions.join(", ")}. Browser login cannot delete DNS records or Access resources.`] : []
|
|
668
1243
|
].join("\n");
|
|
669
1244
|
}
|
|
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
1245
|
function describeKeptResources(manifest, resources, heading, accessHeading, emailRulesRemoved = false) {
|
|
693
1246
|
const sorted = [...resources].sort((left, right) => keptResourceRank(manifest, left) - keptResourceRank(manifest, right));
|
|
694
1247
|
const isTraffic = (resource) => KEPT_RESOURCE_ORDER.indexOf(resource.kind) < KEPT_RESOURCE_ORDER.indexOf("queue");
|
|
@@ -753,7 +1306,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
753
1306
|
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
1307
|
if (targetResources.every((resource) => hasMatchingResourceProof(remote, resource))) break;
|
|
755
1308
|
}
|
|
756
|
-
if (attempt < 2) await
|
|
1309
|
+
if (attempt < 2) await setTimeout(50 * 2 ** attempt);
|
|
757
1310
|
}
|
|
758
1311
|
if (rawRemote === null) {
|
|
759
1312
|
if ((options.allowMissingRoutingCheckpoint || options.allowUninitializedRoutingCheckpoint) && routing.ownership === "created") {
|
|
@@ -800,10 +1353,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
800
1353
|
const live = await client.getHyperdriveConfig(accountId, resource.id);
|
|
801
1354
|
exists = live.id === resource.id && live.name === resource.name;
|
|
802
1355
|
if (exists) {
|
|
803
|
-
const legacyHash =
|
|
804
|
-
kind: resource.kind,
|
|
805
|
-
name: resource.name
|
|
806
|
-
})).digest("hex");
|
|
1356
|
+
const legacyHash = legacyResourceConfigHash(resource.kind, resource.name);
|
|
807
1357
|
const malformedLegacyHash = !/^[a-f0-9]{64}$/.test(resource.configHash);
|
|
808
1358
|
if (options.databaseUrl && resource.ownership !== "adopted") {
|
|
809
1359
|
const expected = hyperdriveOriginFromConnectionString(options.databaseUrl);
|
|
@@ -824,10 +1374,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
824
1374
|
exists = current !== void 0;
|
|
825
1375
|
if (current && resource.ownership === "created") {
|
|
826
1376
|
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");
|
|
1377
|
+
const legacyHash = legacyResourceConfigHash(resource.kind, resource.name);
|
|
831
1378
|
const expectedHash = dnsRecordConfigHash(resource.name, "AAAA", "100::", true);
|
|
832
1379
|
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
1380
|
}
|
|
@@ -839,12 +1386,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
|
|
|
839
1386
|
const live = r2Buckets.find((entry) => entry.id === resource.id && entry.name === resource.name);
|
|
840
1387
|
exists = live !== void 0;
|
|
841
1388
|
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`);
|
|
1389
|
+
if (!matchesRecordedR2Fingerprint(resource.configHash, live)) throw ownershipError(`r2-bucket ${resource.name} changed creation fingerprint`);
|
|
848
1390
|
verifyResourceMarker(await client.getR2Object(accountId, resource.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), manifest, resource);
|
|
849
1391
|
}
|
|
850
1392
|
} else throw ownershipError(`live ownership checks do not support ${resource.kind}`);
|
|
@@ -857,10 +1399,7 @@ function hasMatchingResourceProof(manifest, resource) {
|
|
|
857
1399
|
return proof?.id === resource.id && proof.kind === resource.kind && proof.name === resource.name && proof.ownership === resource.ownership;
|
|
858
1400
|
}
|
|
859
1401
|
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
|
-
}
|
|
1402
|
+
if (resource.ownership === "created") return hasProvisionIntentStep(manifest, resource.key, resource.kind, resource.name);
|
|
864
1403
|
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
1404
|
return false;
|
|
866
1405
|
}
|
|
@@ -877,40 +1416,9 @@ function isPlatformManifestMonotonicExtension(earlier, later) {
|
|
|
877
1416
|
function hasNamedResource(entries, resource) {
|
|
878
1417
|
return entries.some((entry) => entry.id === resource.id && entry.name === resource.name);
|
|
879
1418
|
}
|
|
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
1419
|
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`);
|
|
1420
|
+
const problem = resourceMarkerProblem(value, manifest, resource);
|
|
1421
|
+
if (problem) throw ownershipError(`${resource.kind} ${resource.name} ${problem}`);
|
|
914
1422
|
}
|
|
915
1423
|
function canResumeAfterRoutingCheckpointDeletion(manifest, targetKeys) {
|
|
916
1424
|
if (manifest.status !== "failed" && manifest.status !== "uninstalling") return false;
|
|
@@ -953,11 +1461,11 @@ function recordLifecycleStep(manifest, key) {
|
|
|
953
1461
|
resultHash: "completed"
|
|
954
1462
|
}, now);
|
|
955
1463
|
}
|
|
956
|
-
async function
|
|
1464
|
+
async function writeLifecycleCheckpoint(client, manifest, databaseCheckpoint) {
|
|
957
1465
|
writeInstallationManifest(manifest);
|
|
958
|
-
await
|
|
1466
|
+
await writeRemoteManifest(client, manifest, databaseCheckpoint);
|
|
959
1467
|
}
|
|
960
|
-
async function
|
|
1468
|
+
async function writeRemoteManifest(client, manifest, databaseCheckpoint) {
|
|
961
1469
|
await databaseCheckpoint?.(manifest);
|
|
962
1470
|
const routing = manifest.resources.find((resource) => resource.key === "routing-kv" && resource.state !== "deleted");
|
|
963
1471
|
if (routing) await client.putKvValue(manifest.account.id, routing.id, REMOTE_INSTALLATION_MANIFEST_KEY, JSON.stringify(manifest));
|