void 0.25.0 → 0.26.1
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/AGENTS_TEMPLATE.md +1 -3
- package/README.md +19 -9
- package/dist/{account-cmd-DcR1ywt8.mjs → account-cmd-CfUXh_nU.mjs} +8 -7
- package/dist/{application-analysis-BVsVqc11.mjs → application-analysis-DKy02UY0.mjs} +21 -32
- package/dist/{application-routing-B7QEjqSR.mjs → application-routing-DQyUh_5S.mjs} +1 -1
- package/dist/{auth-7wuZ5ob8.mjs → auth-B5_tZLxT.mjs} +9 -9
- package/dist/{auth-CdgZ9uag.mjs → auth-BgX2qYSY.mjs} +1 -1
- package/dist/{auth-link-Z6E5DYvM.mjs → auth-link-D5RmKnzh.mjs} +5 -5
- package/dist/{auth-migrations-9vif1uj8.mjs → auth-migrations-BwHgeEsD.mjs} +8 -18
- package/dist/{auth-router-BdM4avI8.mjs → auth-router-Bap-pvbn.mjs} +6 -19
- package/dist/{better-auth-shared-BVkjZ3vM.d.mts → better-auth-shared-CPvWD3nG.d.mts} +0 -4
- package/dist/{better-auth-shared-hy6RPh9W.mjs → better-auth-shared-DPpj8zxc.mjs} +94 -16
- package/dist/{build-cmd-CEE7D-hM.mjs → build-cmd-B58c8iS8.mjs} +8 -7
- package/dist/{cache-TcZnBLtH.mjs → cache-Bo52trKF.mjs} +8 -8
- package/dist/{cancel-deploy-CYoZmcmR.mjs → cancel-deploy-BZNz3Q26.mjs} +6 -5
- package/dist/{cf-access-AHKBzmuu.mjs → cf-access-ngGqZpHZ.mjs} +11 -12
- package/dist/{cf-build-output-U8o1Gl6X.mjs → cf-build-output-DjvB_872.mjs} +62 -85
- package/dist/cf-build-output-H9a97vME.mjs +2 -0
- package/dist/cli/cli.mjs +48 -83
- package/dist/cli/cloudflare-operation-process.mjs +71 -77
- package/dist/cli/env-schema-probe.d.mts +1 -1
- package/dist/cli/env-schema-probe.mjs +5 -6
- package/dist/client-By4JjMw_.mjs +2 -0
- package/dist/{client-RsPo8wsP.mjs → client-C2Prx_Bi.mjs} +30 -8
- package/dist/{cloudflare-auth-Qdc7tw8F.mjs → cloudflare-auth-CuxfF6OA.mjs} +9 -17
- package/dist/{cloudflare-cmd-bhwCxAN6.mjs → cloudflare-cmd-lHqNlJGW.mjs} +6 -5
- package/dist/{cloudflare-config-Bktvwtpf.mjs → cloudflare-config-BmhBv3HV.mjs} +1 -1
- package/dist/{cloudflare-connect-CuhRgk3k.mjs → cloudflare-connect-2G3rL2rP.mjs} +4 -4
- package/dist/{cloudflare-operations-BhbK6yTq.mjs → cloudflare-operations-BlYLlowZ.mjs} +1 -1
- package/dist/{cloudflare-operations-CjaV4lZi.mjs → cloudflare-operations-CwI1bChl.mjs} +41 -70
- package/dist/{cloudflare-process-B-wekeR6.mjs → cloudflare-process-2-TfIEzB.mjs} +20 -22
- package/dist/{config-CyQ-wVd7.mjs → config-5r-YpMC-.mjs} +1 -1
- package/dist/{config-C_XRIPx2.mjs → config-B_ggY-IO.mjs} +3 -3
- package/dist/config-BtSFvXZx.mjs +2 -0
- package/dist/config-CLYAosnQ.mjs +78 -0
- package/dist/{config-BMHb8RCj.d.mts → config-CSa15FvD.d.mts} +1 -1
- package/dist/{config-Br_JZD6u.mjs → config-DiowPT80.mjs} +24 -30
- package/dist/config-entry.d.mts +1 -1
- package/dist/{config-write-B1f88wJA.mjs → config-write-AHGesphx.mjs} +8 -7
- package/dist/{connect-DCvUmPXY.mjs → connect-X0MUA1gk.mjs} +12 -11
- package/dist/{create-project-eAPgYq35.mjs → create-project-DxiyW6iX.mjs} +7 -7
- package/dist/{create-project-C734I-Mu.mjs → create-project-iwkbqiBU.mjs} +1 -1
- package/dist/{db-C07GEdKF.mjs → db-XnVMHzoV.mjs} +101 -133
- package/dist/{delete-pKMVsrpJ.mjs → delete-esYlMiSJ.mjs} +8 -8
- package/dist/{deploy-DfsAe-T0.mjs → deploy-BsEvnlJl.mjs} +2377 -2512
- package/dist/deploy-C1d-YTfp.mjs +2 -0
- package/dist/{dev-inbox-D64ZKUod.mjs → dev-inbox-CK-UuPob.mjs} +7 -16
- package/dist/{discover-BcPPiF1o.mjs → discover-CkCYv9T_.mjs} +1 -2
- package/dist/discover-EDUf6JJw.mjs +2 -0
- package/dist/{prompts-ZAuJlyCj.mjs → dist-Bx1z6Eva.mjs} +1 -1
- package/dist/{dist-AoCzRTJE.mjs → dist-Cegxq8gs.mjs} +8 -8
- package/dist/{prompts-6OSVA7DJ.mjs → dist-D3m88sjf.mjs} +10 -53
- package/dist/{domain-D3uA9ize.mjs → domain-CbbBWhWE.mjs} +29 -30
- package/dist/{dotenv-VQxupEUv.mjs → dotenv-BfU6eqFt.mjs} +20 -32
- package/dist/{drizzle-Beb2Am5S.mjs → drizzle-CF84FOVM.mjs} +16 -13
- package/dist/{email-z3o2cwjH.mjs → email-CpuT42g7.mjs} +24 -19
- package/dist/{email--tEtMoj_.mjs → email-d_Aux1vM.mjs} +1 -1
- package/dist/{entry-CF5wvH4w.mjs → entry-CMAkqBmU.mjs} +6 -6
- package/dist/{env-_v349JBd.mjs → env-BxocywBB.mjs} +25 -6
- package/dist/{env-D5liuQYR.mjs → env-DMA3cXZt.mjs} +1 -1
- package/dist/{env-public-BX_r8HR6.d.mts → env-public-DR7ZM6d-.d.mts} +1 -1
- package/dist/{env-raw-Cx8ElDdj.mjs → env-raw-jZpP5n47.mjs} +1 -2
- package/dist/{env-types-D5kds_OZ.mjs → env-types-CwXgeMgL.mjs} +1 -1
- package/dist/{env-validation-cZBvGWaP.mjs → env-validation-CrVNjsPm.mjs} +1 -1
- package/dist/{env-validation-vBSQ8gw5.mjs → env-validation-Dxdx_5wo.mjs} +17 -21
- package/dist/{env-3Q8-_2IH.mjs → env-w9jOyGZn.mjs} +10 -9
- package/dist/{fetch-_SeGZao9.d.mts → fetch-BPCYVi3H.d.mts} +20 -1
- package/dist/{fetch-BIZJh7vR.mjs → fetch-sAcwjzLO.mjs} +1 -1
- package/dist/{fetch-stream-IvCYKyQL.mjs → fetch-stream-PGgz4uGz.mjs} +26 -24
- package/dist/{fetch-stream-Bjf0hoZb.d.mts → fetch-stream-obvZ7fvO.d.mts} +1 -16
- package/dist/{gen-NjwoG9FK.mjs → gen-DtkQhvQg.mjs} +40 -57
- package/dist/gen-dA9hW7AV.mjs +2 -0
- package/dist/{github-cmd-DquUq-b1.mjs → github-cmd-D_FsOFxq.mjs} +65 -164
- package/dist/{handler-DghKr6dU.mjs → handler-BHAn2fZJ.mjs} +1 -0
- package/dist/{handler-CZ4nAylQ.d.mts → handler-CNAwjRcW.d.mts} +5 -12
- package/dist/{head-Do8P4puT.d.mts → head-C8K_iGe6.d.mts} +1 -1
- package/dist/{head-D_QRR5Yd.mjs → head-D2VF4ApL.mjs} +29 -51
- package/dist/{headers-2rKda9Y6.mjs → headers-DlfR78Nu.mjs} +3 -3
- package/dist/{help-DRZXC4fj.mjs → help-CVFBTmBx.mjs} +23 -38
- package/dist/help-DzKUrQvk.mjs +2 -0
- package/dist/{inbound-fRkqcXaC.mjs → inbound-PfQDLXc5.mjs} +23 -24
- package/dist/index.d.mts +19 -2
- package/dist/index.mjs +632 -532
- package/dist/{info-BmA3_IFZ.mjs → info-PYWwkWyt.mjs} +4 -4
- package/dist/{init-DT7yuuyl.mjs → init-Bw-fRKGp.mjs} +161 -197
- package/dist/{limits-Bq5LG8Id.d.mts → limits-CFsDKQvU.d.mts} +1 -10
- package/dist/{limits-Cjuk2VPm.mjs → limits-D0vD1M9A.mjs} +1 -22
- package/dist/{link-CXOlqh2F.mjs → link-DlPG8iX3.mjs} +10 -10
- package/dist/{list-eEWVJEPn.mjs → list-2GN7_ueV.mjs} +11 -9
- package/dist/{live-CB1y5IuC.mjs → live-CgQuTpqD.mjs} +15 -16
- package/dist/{live-Chw1eIMv.d.mts → live-Du8jXwOW.d.mts} +4 -2
- package/dist/{local-d1-Df4XpwhW.mjs → local-d1-ga5O_idV.mjs} +2 -2
- package/dist/{login-pWQHYVu2.mjs → login-D-Q4xw3Q.mjs} +7 -7
- package/dist/login-Dc-wuTDq.mjs +2 -0
- package/dist/{logs-DnxVX7TR.mjs → logs-rd8J-OFh.mjs} +28 -11
- package/dist/migrate-BHp07yJE.mjs +2 -0
- package/dist/{migrate-CV1on-yJ.mjs → migrate-NLsjBbuM.mjs} +34 -20
- package/dist/{mime-D5Nmdzf7.mjs → mime-DxCznlJq.mjs} +45 -95
- package/dist/{node-BkyRWRx8.mjs → node-DJicPvPj.mjs} +5 -5
- package/dist/{operator-args-CAVcmwUt.mjs → operator-args-BNsW5gfp.mjs} +10 -1
- package/dist/{operator-auth-CkmTE3rt.mjs → operator-auth-AzVLbrr3.mjs} +4 -8
- package/dist/{operator-client-LmwjxT6q.mjs → operator-client-BQtzyG-g.mjs} +3 -3
- package/dist/{operator-cmd-d-IEKgNk.mjs → operator-cmd-B6f6Cg9Z.mjs} +45 -14
- package/dist/operator-cmd-Cwn50lOV.mjs +2 -0
- package/dist/{output-BBsUwdyT.mjs → output-C50ag2r3.mjs} +44 -2
- package/dist/pages/client.d.mts +19 -51
- package/dist/pages/client.mjs +204 -225
- package/dist/pages/head-client.d.mts +1 -1
- package/dist/pages/head.d.mts +1 -1
- package/dist/pages/head.mjs +1 -1
- package/dist/pages/index.d.mts +2 -2
- package/dist/pages/index.mjs +6 -12
- package/dist/pages/islands-plugin.d.mts +1 -1
- package/dist/pages/islands-plugin.mjs +21 -25
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +2 -2
- package/dist/{plan-NPpwZ_kc.mjs → plan-BhLO2p4w.mjs} +4 -3
- package/dist/plan-CH2G51je.mjs +2 -0
- package/dist/{platform-args-Bzf8JPc2.mjs → platform-args-BoZkWQnJ.mjs} +4 -2
- package/dist/platform-args-DYgITk0D.mjs +2 -0
- package/dist/{platform-auth-config-0V0lA3pF.mjs → platform-auth-config-n603OANg.mjs} +45 -40
- package/dist/{platform-auth-protection-DFeJ-JkK.mjs → platform-auth-protection-zxDr0hWW.mjs} +9 -8
- package/dist/{platform-auth-recovery-ICy4muq6.mjs → platform-auth-recovery-tw9lgtgE.mjs} +8 -7
- package/dist/{platform-cmd-D7vSWMvV.mjs → platform-cmd-B8-bCb7t.mjs} +48 -23
- package/dist/{platform-cmd-BPySFOew.mjs → platform-cmd-C00YM14r.mjs} +1 -1
- package/dist/{platform-discovery-DDDUvnqp.mjs → platform-discovery-BC0HCKYt.mjs} +18 -4
- package/dist/{platform-domain-CUdY064e.mjs → platform-domain-p0kobLV8.mjs} +6 -5
- package/dist/{platform-lifecycle-Dm09JNkn.mjs → platform-lifecycle-D_QEY32D.mjs} +906 -389
- package/dist/{platform-lifecycle-BaUdTRPU.mjs → platform-lifecycle-vFqtNPPa.mjs} +1 -1
- package/dist/{platform-management-BSEc_ZeG.mjs → platform-management-BdYblvvE.mjs} +175 -32
- package/dist/{platform-management-Bp6aoH2C.mjs → platform-management-oi-SvI3f.mjs} +1 -1
- package/dist/{platform-plans-config-B0pOVQyW.mjs → platform-plans-config-CjiFv5J4.mjs} +5 -4
- package/dist/{platform-recovery-WOZ95Uze.mjs → platform-recovery-PvNS9Nl2.mjs} +3 -3
- package/dist/{platform-registry-KfNEF84O.mjs → platform-registry-P387v42A.mjs} +17 -10
- package/dist/platform-setup-DVe42Ws7.mjs +299 -0
- package/dist/{plugin-inference-CXWnn79A.mjs → plugin-inference-BlQbOL0P.mjs} +43 -42
- package/dist/prepare-DRUV-g3X.mjs +2 -0
- package/dist/{prepare-DnaKBi--.mjs → prepare-Do96654b.mjs} +8 -8
- package/dist/{prepare-39EvOhI6.mjs → prepare-Dq9hTIbo.mjs} +4 -3
- package/dist/{prerender-render-D9qbLs2f.mjs → prerender-render-Ct20jftc.mjs} +17 -3
- package/dist/prerender-render.mjs +1 -1
- package/dist/{project-IOjBU8R6.mjs → project-BiaPQ7ur.mjs} +1 -1
- package/dist/{project-BkbfDs0V.mjs → project-DQstYTxO.mjs} +1 -1
- package/dist/{project-cmd-Bs5A392E.mjs → project-cmd-ti8_yTcA.mjs} +19 -19
- package/dist/{project-team-ohAWWaew.mjs → project-team-CDPE1Tsg.mjs} +15 -18
- package/dist/{project-token-CP01Jna6.mjs → project-token-CkZ1cN0Z.mjs} +14 -17
- package/dist/{project-tsconfig-B8GeM0RM.mjs → project-tsconfig-CVJhNMxs.mjs} +2 -1
- package/dist/{project-zero-trust-BzkVgx35.mjs → project-zero-trust-DdjLR61k.mjs} +12 -9
- package/dist/{protocol-CjF_iI9X.d.mts → protocol-BPZaNqBo.d.mts} +4 -2
- package/dist/{protocol-U7bfjHmA.mjs → protocol-CfaUwNs8.mjs} +46 -49
- package/dist/{providers-CJlNS3kT.mjs → providers-BXuhmuub.mjs} +1 -12
- package/dist/{provision-Btj1jppf.mjs → provision-BfaLiWbj.mjs} +51 -42
- package/dist/{provision-BEv0k-qz.mjs → provision-Wu9ZBk9L.mjs} +2 -3
- package/dist/{requests-B3PBuGlX.mjs → requests-DFAqyYy8.mjs} +5 -5
- package/dist/resolve-project-BIeYs9Cw.mjs +2 -0
- package/dist/{resolve-project-BG5ORGID.mjs → resolve-project-CP8rZs_p.mjs} +6 -1
- package/dist/{rollback-DDub9GsO.mjs → rollback-BKYXBduI.mjs} +30 -37
- package/dist/{route-types-Clm4WD0B.mjs → route-types-CbtsIeFn.mjs} +1 -1
- package/dist/route-url-BgPja1GS.d.mts +5 -0
- package/dist/route-url-BjGv_KKY.mjs +28 -0
- package/dist/runner-9wjsSZx-.mjs +2 -0
- package/dist/{runner-Ol0TNjk6.mjs → runner-DMBKSrRk.mjs} +2 -2
- package/dist/{runner-mysql-DmWchSWJ.mjs → runner-mysql-CEzKtmFb.mjs} +8 -8
- package/dist/{runner-mysql-CEG5JozK.mjs → runner-mysql-CkFqZ6CE.mjs} +1 -1
- package/dist/{runner-pg-Bh4AgZdj.mjs → runner-pg-BGYRFdRp.mjs} +1 -1
- package/dist/runtime/ai.d.mts +11 -12
- package/dist/runtime/ai.mjs +47 -113
- package/dist/runtime/better-auth-mysql.d.mts +1 -1
- package/dist/runtime/better-auth-mysql.mjs +4 -9
- package/dist/runtime/better-auth-pg.d.mts +1 -1
- package/dist/runtime/better-auth-pg.mjs +5 -10
- package/dist/runtime/better-auth.d.mts +1 -1
- package/dist/runtime/better-auth.mjs +3 -3
- package/dist/runtime/client-react.d.mts +2 -3
- package/dist/runtime/client-react.mjs +2 -2
- package/dist/runtime/client-solid.d.mts +2 -3
- package/dist/runtime/client-solid.mjs +2 -2
- package/dist/runtime/client-svelte.d.mts +2 -3
- package/dist/runtime/client-svelte.mjs +2 -2
- package/dist/runtime/client-vue.d.mts +2 -3
- package/dist/runtime/client-vue.mjs +2 -2
- package/dist/runtime/client.d.mts +2 -3
- package/dist/runtime/client.mjs +2 -2
- package/dist/runtime/db.mjs +1 -1
- package/dist/runtime/durable.mjs +3 -10
- package/dist/runtime/email/testing.mjs +5 -5
- package/dist/runtime/email.d.mts +2 -1
- package/dist/runtime/email.mjs +11 -27
- package/dist/runtime/env-public.d.mts +1 -1
- package/dist/runtime/env-public.mjs +15 -20
- package/dist/runtime/env.mjs +1 -1
- package/dist/runtime/fetch-stream.d.mts +2 -2
- package/dist/runtime/fetch-stream.mjs +1 -1
- package/dist/runtime/fetch.d.mts +2 -3
- package/dist/runtime/fetch.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/handler.mjs +2 -2
- package/dist/runtime/isr.mjs +1 -1
- package/dist/runtime/kv.mjs +10 -11
- package/dist/runtime/limits.d.mts +2 -2
- package/dist/runtime/limits.mjs +2 -2
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live-client.mjs +12 -22
- package/dist/runtime/live-server.mjs +27 -35
- package/dist/runtime/live.d.mts +2 -2
- package/dist/runtime/live.mjs +2 -2
- package/dist/runtime/migration-handler.mjs +35 -86
- package/dist/runtime/queues.mjs +1 -1
- package/dist/runtime/remote/binding-handler.d.mts +2 -2
- package/dist/runtime/remote/binding-handler.mjs +8 -9
- package/dist/runtime/remote/index.mjs +27 -77
- package/dist/runtime/route-url.d.mts +2 -4
- package/dist/runtime/route-url.mjs +1 -1
- package/dist/runtime/routing.d.mts +1 -1
- package/dist/runtime/routing.mjs +15 -26
- package/dist/runtime/sandbox.d.mts +1 -1
- package/dist/runtime/sandbox.mjs +14 -24
- package/dist/runtime/sse.mjs +1 -1
- package/dist/runtime/storage.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +2 -2
- package/dist/runtime/ws-server.mjs +30 -61
- package/dist/runtime/ws.d.mts +1 -1
- package/dist/runtime/ws.mjs +1 -1
- package/dist/{sandbox-XZAqzFlG.d.mts → sandbox-C5LFoJMf.d.mts} +7 -7
- package/dist/{scan-C7okrLyM.mjs → scan-C3uDzz6J.mjs} +21 -18
- package/dist/{secret-3c72lY1U.mjs → secret-BAt2KMmO.mjs} +15 -15
- package/dist/{skills-BPQUGClu.mjs → skills-BgVMvQdy.mjs} +2 -2
- package/dist/{sse-CQNaDFFV.mjs → sse-CMvCgjNk.mjs} +1 -1
- package/dist/{subcommand-prompt-Dhom39bd.mjs → subcommand-prompt-BSlnD1fI.mjs} +2 -2
- package/dist/sveltekit.d.mts +0 -2
- package/dist/terminal-log-text-D9ACPOuG.mjs +32 -0
- package/dist/validate-BJr9LfXy.mjs +2 -0
- package/dist/{validate-ctOrgiS3.mjs → validate-C9qJArv7.mjs} +9 -28
- package/dist/{defer-YsYDUoii.mjs → validation-error-Dg9Recn1.mjs} +11 -11
- package/dist/{wrangler-DhUerFkW.mjs → wrangler-D9KTuqnv.mjs} +89 -194
- package/dist/{ws-CL1w7GXU.d.mts → ws-CChrrAU-.d.mts} +1 -1
- package/dist/{yarn-pnp-BV4AzNul.mjs → yarn-pnp-CUX_iTPA.mjs} +19 -27
- package/{skills/void/docs → docs}/guide/ai.md +69 -64
- package/{skills/void/docs → docs}/guide/app-types.md +1 -1
- package/{skills/void/docs → docs}/guide/auth.md +37 -1
- package/{skills/void/docs → docs}/guide/database/postgresql.md +2 -2
- package/{skills/void/docs → docs}/guide/database.md +23 -8
- package/{skills/void/docs → docs}/guide/edge/revalidation.md +1 -1
- package/{skills/void/docs → docs}/guide/edge/rewrites.md +2 -4
- package/{skills/void/docs → docs}/guide/email/receiving.md +2 -2
- package/{skills/void/docs → docs}/guide/email/sending.md +0 -2
- package/{skills/void/docs → docs}/guide/env-vars.md +4 -0
- package/{skills/void/docs → docs}/guide/jobs.md +2 -2
- package/{skills/void/docs → docs}/guide/live.md +12 -5
- package/{skills/void/docs → docs}/guide/pages-routing/actions-and-forms.md +21 -9
- package/{skills/void/docs → docs}/guide/pages-routing/loaders.md +7 -3
- package/{skills/void/docs → docs}/guide/pages-routing/markdown.md +15 -23
- package/{skills/void/docs → docs}/guide/pages-routing/overview.md +6 -4
- package/{skills/void/docs → docs}/guide/platform/administration/access.md +2 -10
- package/{skills/void/docs → docs}/guide/platform/administration/operations.md +3 -12
- package/{skills/void/docs → docs}/guide/platform/administration/projects.md +1 -1
- package/{skills/void/docs → docs}/guide/platform/development/local.md +4 -2
- package/{skills/void/docs → docs}/guide/platform/development/runtime.md +1 -1
- package/{skills/void/docs → docs}/guide/platform/development/schema-ci.md +1 -1
- package/{skills/void/docs → docs}/guide/platform/installation/credentials.md +2 -2
- package/{skills/void/docs → docs}/guide/platform/installation/domains.md +3 -1
- package/{skills/void/docs → docs}/guide/platform/installation/first-deployment.md +8 -2
- package/{skills/void/docs → docs}/guide/platform/installation/maintenance.md +7 -1
- package/{skills/void/docs → docs}/guide/platform/installation/setup.md +1 -1
- package/docs/guide/platform/installation/uninstall.md +102 -0
- package/{skills/void/docs → docs}/guide/project-collaboration.md +6 -2
- package/{skills/void/docs → docs}/guide/queues.md +0 -2
- package/{skills/void/docs → docs}/guide/remote-dev.md +5 -5
- package/{skills/void/docs → docs}/guide/sandboxes.md +41 -22
- package/{skills/void/docs → docs}/guide/sse.md +0 -2
- package/{skills/void/docs → docs}/guide/storage.md +7 -2
- package/{skills/void/docs → docs}/guide/websockets.md +4 -0
- package/docs/integrations/agents.md +37 -0
- package/{skills/void/docs → docs}/integrations/frameworks/astro.md +3 -3
- package/{skills/void/docs → docs}/integrations/frameworks/nuxt.md +6 -2
- package/{skills/void/docs → docs}/integrations/hono.md +15 -7
- package/{skills/void/docs → docs}/integrations/nodejs-bun-deno.md +1 -0
- package/{skills/void/docs → docs}/reference/api/auth.md +1 -1
- package/{skills/void/docs → docs}/reference/api/handlers.md +4 -2
- package/{skills/void/docs → docs}/reference/api/pages.md +4 -0
- package/{skills/void/docs → docs}/reference/api/resources.md +11 -14
- package/{skills/void/docs → docs}/reference/api.md +35 -34
- package/{skills/void/docs → docs}/reference/cli/auth.md +3 -6
- package/{skills/void/docs → docs}/reference/cli/database.md +3 -4
- package/{skills/void/docs → docs}/reference/cli/generate.md +1 -1
- package/{skills/void/docs → docs}/reference/cli/github.md +3 -12
- package/{skills/void/docs → docs}/reference/cli/platform-config.md +3 -3
- package/{skills/void/docs → docs}/reference/cli/platform-email.md +2 -3
- package/{skills/void/docs → docs}/reference/cli/platform-installation.md +15 -13
- package/{skills/void/docs → docs}/reference/cli/platform-operations.md +3 -0
- package/{skills/void/docs → docs}/reference/cli/platform-users.md +2 -2
- package/{skills/void/docs → docs}/reference/cli/platform.md +4 -2
- package/{skills/void/docs → docs}/reference/cli/project.md +5 -4
- package/{skills/void/docs → docs}/reference/cli/secrets.md +3 -3
- package/{skills/void/docs → docs}/reference/cli/setup.md +9 -11
- package/{skills/void/docs → docs}/reference/cli.md +2 -2
- package/{skills/void/docs → docs}/reference/config.md +13 -13
- package/{skills/void/docs → docs}/reference/resource-inference.md +2 -16
- package/package.json +34 -30
- package/schema.json +1 -1
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +4 -1
- package/skills/void/SKILL.md +11 -5
- package/dist/cf-build-output-ChY6-XWP.mjs +0 -2
- package/dist/client-B4Me-9HE.mjs +0 -2
- package/dist/config-0acf1oys.mjs +0 -2
- package/dist/config-D8MOgiQV.mjs +0 -32
- package/dist/deploy-BDu1vhSH.mjs +0 -2
- package/dist/discover-AMCBXivQ.mjs +0 -2
- package/dist/fetch-error-Sp1R4mZv.d.mts +0 -20
- package/dist/gen-ById9r2j.mjs +0 -2
- package/dist/help-DPGBwomV.mjs +0 -2
- package/dist/login-C98-sw79.mjs +0 -2
- package/dist/migrate-CEu2X5DB.mjs +0 -2
- package/dist/plan-D1Q5rf-r.mjs +0 -2
- package/dist/platform-args-Cq6EjQro.mjs +0 -2
- package/dist/prepare-CnjcTV2s.mjs +0 -2
- package/dist/resolve-project-DtbidV4y.mjs +0 -2
- package/dist/route-url-CG7U-cRN.mjs +0 -15
- package/dist/runner-CXA9Fh8h.mjs +0 -2
- package/dist/validate-Dq_L3s0S.mjs +0 -2
- package/skills/void/docs/guide/platform/installation/uninstall.md +0 -69
- package/skills/void/docs/integrations/agents.md +0 -26
- package/dist/{runner-pg-D4OPlil-.mjs → runner-pg-DV8p_zjA.mjs} +4 -4
- package/{skills/void/docs → docs}/guide/database/d1.md +2 -2
- package/{skills/void/docs → docs}/guide/database/mysql.md +0 -0
- package/{skills/void/docs → docs}/guide/deployment.md +0 -0
- package/{skills/void/docs → docs}/guide/durable-state.md +0 -0
- package/{skills/void/docs → docs}/guide/edge/headers.md +0 -0
- package/{skills/void/docs → docs}/guide/edge/prerendering.md +0 -0
- package/{skills/void/docs → docs}/guide/edge/redirects.md +0 -0
- package/{skills/void/docs → docs}/guide/edge/static-assets.md +0 -0
- package/{skills/void/docs → docs}/guide/email/domains.md +0 -0
- package/{skills/void/docs → docs}/guide/email.md +0 -0
- package/{skills/void/docs → docs}/guide/index.md +0 -0
- package/{skills/void/docs → docs}/guide/kv.md +0 -0
- package/{skills/void/docs → docs}/guide/pages-routing/head.md +0 -0
- package/{skills/void/docs → docs}/guide/pages-routing/islands.md +2 -2
- package/{skills/void/docs → docs}/guide/pages-routing/layouts.md +0 -0
- package/{skills/void/docs → docs}/guide/pages-routing/view-transitions.md +0 -0
- package/{skills/void/docs → docs}/guide/platform/administration/email.md +0 -0
- package/{skills/void/docs → docs}/guide/platform/administration/plans.md +0 -0
- package/{skills/void/docs → docs}/guide/platform/administration/zero-trust.md +0 -0
- package/{skills/void/docs → docs}/guide/platform/installation/ci.md +0 -0
- package/{skills/void/docs → docs}/guide/platform/installation/prerequisites.md +0 -0
- package/{skills/void/docs → docs}/guide/platform-administration.md +0 -0
- package/{skills/void/docs → docs}/guide/platform-development.md +0 -0
- package/{skills/void/docs → docs}/guide/quickstart.md +0 -0
- package/{skills/void/docs → docs}/guide/self-hosted-platform.md +0 -0
- package/{skills/void/docs → docs}/guide/server-routing.md +3 -3
- package/{skills/void/docs → docs}/guide/ssg.md +0 -0
- package/{skills/void/docs → docs}/guide/ssr.md +0 -0
- package/{skills/void/docs → docs}/guide/type-safety.md +0 -0
- package/{skills/void/docs → docs}/guide/typed-fetch.md +0 -0
- package/{skills/void/docs → docs}/index.md +0 -0
- package/{skills/void/docs → docs}/integrations/cloudflare.md +0 -0
- package/{skills/void/docs → docs}/integrations/frameworks/analog.md +3 -3
- package/{skills/void/docs → docs}/integrations/frameworks/overview.md +0 -0
- package/{skills/void/docs → docs}/integrations/frameworks/react-router.md +3 -3
- package/{skills/void/docs → docs}/integrations/frameworks/sveltekit.md +1 -1
- package/{skills/void/docs → docs}/integrations/frameworks/tanstack-start.md +2 -2
- package/{skills/void/docs → docs}/reference/api/client.md +0 -0
- package/{skills/void/docs → docs}/reference/api/database.md +2 -2
- /package/{skills/void/docs → docs}/reference/api/durable.md +0 -0
- /package/{skills/void/docs → docs}/reference/api/env.md +0 -0
- /package/{skills/void/docs → docs}/reference/api/plugin.md +0 -0
- /package/{skills/void/docs → docs}/reference/api/rewrites.md +0 -0
- /package/{skills/void/docs → docs}/reference/api/types.md +0 -0
- /package/{skills/void/docs → docs}/reference/cli/deploy.md +0 -0
- /package/{skills/void/docs → docs}/reference/cli/domains.md +0 -0
- /package/{skills/void/docs → docs}/reference/cli/email.md +0 -0
- /package/{skills/void/docs → docs}/reference/cli/platform-zero-trust.md +0 -0
- /package/{skills/void/docs → docs}/reference/structure.md +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { a as join, n as dirname, u as resolve } from "./dist-BVqXeR_h.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { S as writePendingInstallationSecrets, a as latestPlatformWorkerCutoverGeneration, c as normalizePlatformApiUrl, d as platformRegistryDirectory, f as platformWorkerSecretUpdateIntentState, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, n as assertPlatformInstallationSlugAvailable, o as listInstallationManifests, p as promoteInstallationSecrets, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest, y as savePlatformConnection } from "./platform-registry-P387v42A.mjs";
|
|
3
3
|
import { t as cfAccessHeaders } from "./cf-access-TWNzXltp.mjs";
|
|
4
|
-
import { n as ensurePlatformAccessToken } from "./cf-access-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { o as
|
|
10
|
-
import {
|
|
11
|
-
import { t as
|
|
4
|
+
import { n as ensurePlatformAccessToken } from "./cf-access-ngGqZpHZ.mjs";
|
|
5
|
+
import { A as confirm, H as outro, K as select, L as log, U as password, X as text$1, tt as isCancel, z as multiselect } from "./dist-D3m88sjf.mjs";
|
|
6
|
+
import { d as printSuccess, l as import_picocolors, n as cliCommandName, r as createSpinner, u as note } from "./output-C50ag2r3.mjs";
|
|
7
|
+
import { l as resolveCloudflareOperationArgv } from "./cloudflare-process-2-TfIEzB.mjs";
|
|
8
|
+
import { o as runCloudflareAuthCommand } from "./cloudflare-auth-CuxfF6OA.mjs";
|
|
9
|
+
import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-BfaLiWbj.mjs";
|
|
10
|
+
import { t as parsePlatformArgs } from "./platform-args-BoZkWQnJ.mjs";
|
|
11
|
+
import { a as platformManagementSetupPage, c as platformSetupLink, i as platformInstallCommandArguments, l as platformSetupPages, n as formatPlatformSetupPlan, o as platformPromptAnswer, r as openPlatformSetupPage, s as platformSetupFieldHelp, t as formatPlatformInstallCommand } from "./platform-setup-DVe42Ws7.mjs";
|
|
12
12
|
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { isRecord } from "@void/deploy-core";
|
|
@@ -16,11 +16,10 @@ import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFI
|
|
|
16
16
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { stripVTControlCharacters } from "node:util";
|
|
19
|
-
import { execFile } from "node:child_process";
|
|
20
19
|
import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
20
|
+
import { createServer, request } from "node:http";
|
|
21
21
|
import { readFile } from "node:fs/promises";
|
|
22
22
|
import { PLATFORM_ADMIN_ASSETS_DIRECTORY, platformDatabaseSchemaDigest, resolvePlatformRuntime } from "@void/platform/installer";
|
|
23
|
-
import { createServer, request } from "node:http";
|
|
24
23
|
import { request as request$1 } from "node:https";
|
|
25
24
|
//#region src/cli/platform-cloudflare.ts
|
|
26
25
|
var PlatformCloudflareApiError = class extends Error {
|
|
@@ -116,7 +115,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
116
115
|
} catch {
|
|
117
116
|
throw new PlatformCloudflareApiError(`Cloudflare returned a non-JSON response (HTTP ${response.status}).`, response.status, path.split("?")[0]);
|
|
118
117
|
}
|
|
119
|
-
if (!response.ok || envelope.success !== true || envelope.result === void 0) throw new PlatformCloudflareApiError(`Cloudflare API error: ${envelope
|
|
118
|
+
if (!response.ok || envelope.success !== true || envelope.result === void 0) throw new PlatformCloudflareApiError(`Cloudflare API error: ${cloudflareErrorDetails(envelope, response.status)} (HTTP ${response.status}; ${init?.method ?? "GET"} ${path.split("?")[0]}).`, response.status, path.split("?")[0]);
|
|
120
119
|
return {
|
|
121
120
|
result: envelope.result,
|
|
122
121
|
resultInfo: envelope.result_info
|
|
@@ -157,6 +156,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
157
156
|
if (result.length < 50) break;
|
|
158
157
|
}
|
|
159
158
|
return entries.map((entry) => ({
|
|
159
|
+
...withCreatedAt(entry.created_at),
|
|
160
160
|
id: requiredString(entry.id, "AI Gateway id"),
|
|
161
161
|
name: requiredString(entry.id, "AI Gateway id")
|
|
162
162
|
}));
|
|
@@ -548,7 +548,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
548
548
|
throw error;
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
|
-
async emptyR2Bucket(accountId, bucketName, preservedKeys = /* @__PURE__ */ new Set()) {
|
|
551
|
+
async emptyR2Bucket(accountId, bucketName, preservedKeys = /* @__PURE__ */ new Set(), beforeDelete) {
|
|
552
552
|
let cursor = "";
|
|
553
553
|
const observedCursors = /* @__PURE__ */ new Set();
|
|
554
554
|
for (;;) {
|
|
@@ -565,6 +565,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
565
565
|
const envelope = await response.json();
|
|
566
566
|
if (!response.ok || envelope.success !== true || !Array.isArray(envelope.result)) throw new PlatformCloudflareApiError(`Cloudflare API error listing R2 objects: ${envelope.errors?.map((error) => error.message).filter(Boolean).join(", ") || `HTTP ${response.status}`}`, response.status);
|
|
567
567
|
const keys = envelope.result.map((object) => requiredString(object.key, "R2 object key")).filter((key) => !preservedKeys.has(key));
|
|
568
|
+
if (keys.length > 0) await beforeDelete?.();
|
|
568
569
|
for (let offset = 0; offset < keys.length; offset += 1e3) await this.deleteR2Objects(accountId, bucketName, keys.slice(offset, offset + 1e3));
|
|
569
570
|
const nextCursor = envelope.result_info?.cursor ?? "";
|
|
570
571
|
if (!nextCursor) return;
|
|
@@ -582,40 +583,173 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
582
583
|
const deletedKeys = new Set(deleted.map((entry) => requiredString(entry.key, "deleted R2 object key")));
|
|
583
584
|
if (keys.find((key) => !deletedKeys.has(key)) !== void 0) throw new Error("platform: Cloudflare did not confirm every requested R2 object deletion.");
|
|
584
585
|
}
|
|
585
|
-
async deleteResource(accountId,
|
|
586
|
+
async deleteResource(accountId, resource, zoneId) {
|
|
587
|
+
const { kind } = resource;
|
|
588
|
+
if (kind === "ai-gateway" && this.#aiGatewayClient) return this.#aiGatewayClient.deleteResource(accountId, resource, zoneId);
|
|
586
589
|
const account = encodeURIComponent(accountId);
|
|
587
|
-
const encoded = encodeURIComponent(id);
|
|
590
|
+
const encoded = encodeURIComponent(kind === "dispatch-namespace" || kind === "worker" ? resource.name : resource.id);
|
|
588
591
|
const path = kind === "kv-namespace" ? `/accounts/${account}/storage/kv/namespaces/${encoded}` : kind === "dns-record" ? zoneId ? `/zones/${encodeURIComponent(zoneId)}/dns_records/${encoded}` : null : kind === "ai-gateway" ? `/accounts/${account}/ai-gateway/gateways/${encoded}` : kind === "r2-bucket" ? `/accounts/${account}/r2/buckets/${encoded}` : kind === "queue" ? `/accounts/${account}/queues/${encoded}` : kind === "dispatch-namespace" ? `/accounts/${account}/workers/dispatch/namespaces/${encoded}` : kind === "hyperdrive" ? `/accounts/${account}/hyperdrive/configs/${encoded}` : kind === "worker" ? `/accounts/${account}/workers/scripts/${encoded}` : kind === "zone" ? `/zones/${encoded}` : null;
|
|
589
592
|
if (!path) throw new Error(`platform: Unsupported Cloudflare deletion kind ${kind}.`);
|
|
593
|
+
await this.#deleteIfPresent(path);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* DELETE one resource by the key Cloudflare addresses it with. Only
|
|
597
|
+
* `forceDeleteWorker` sends `force`; otherwise Cloudflare refuses a
|
|
598
|
+
* resource another Worker still uses. A 404 means it is already gone.
|
|
599
|
+
*/
|
|
600
|
+
async #deleteIfPresent(path) {
|
|
601
|
+
const response = await this.#fetcher(`${this.apiBaseUrl}${path}`, {
|
|
602
|
+
headers: {
|
|
603
|
+
Accept: "application/json",
|
|
604
|
+
Authorization: `Bearer ${this.#token}`
|
|
605
|
+
},
|
|
606
|
+
method: "DELETE",
|
|
607
|
+
redirect: "manual",
|
|
608
|
+
signal: AbortSignal.timeout(3e4)
|
|
609
|
+
});
|
|
610
|
+
if (response.status === 404) return;
|
|
611
|
+
let envelope = {};
|
|
590
612
|
try {
|
|
591
|
-
await
|
|
613
|
+
envelope = await response.json();
|
|
614
|
+
} catch {
|
|
615
|
+
envelope = { success: response.ok };
|
|
616
|
+
}
|
|
617
|
+
if (!response.ok || envelope.success !== true) throw new PlatformCloudflareApiError(`Cloudflare API error: ${cloudflareErrorDetails(envelope, response.status)} (HTTP ${response.status}; DELETE ${path}).`, response.status, path);
|
|
618
|
+
}
|
|
619
|
+
async deleteWorkerRoute(zoneId, routeId) {
|
|
620
|
+
await this.#deleteIfPresent(`/zones/${encodeURIComponent(zoneId)}/workers/routes/${encodeURIComponent(routeId)}`);
|
|
621
|
+
}
|
|
622
|
+
async deleteWorkerDomain(accountId, domainId) {
|
|
623
|
+
await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/workers/domains/${encodeURIComponent(domainId)}`);
|
|
624
|
+
}
|
|
625
|
+
async listQueueConsumers(accountId, queueId) {
|
|
626
|
+
return (await this.request(`/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(queueId)}/consumers`)).map((entry) => ({
|
|
627
|
+
id: requiredString(entry.consumer_id, "Queue consumer id"),
|
|
628
|
+
script: entry.type === "http_pull" ? null : requiredString(entry.script_name ?? entry.script ?? entry.service, "Queue consumer Worker name")
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
async listWorkerDependents(accountId, workerName) {
|
|
632
|
+
const account = encodeURIComponent(accountId);
|
|
633
|
+
const script = encodeURIComponent(workerName);
|
|
634
|
+
const references = await this.request(`/accounts/${account}/workers/scripts/${script}/references`);
|
|
635
|
+
const tails = await this.request(`/accounts/${account}/workers/tails/by-consumer/${script}`);
|
|
636
|
+
const name = (reference) => requiredString(reference?.script ?? reference?.service, "Dependent Worker name");
|
|
637
|
+
const workers = [
|
|
638
|
+
...(references.services?.incoming ?? []).map(name),
|
|
639
|
+
...(references.durable_objects ?? []).map(name).filter((entry) => entry !== workerName),
|
|
640
|
+
...(references.dispatch_outbounds ?? []).map(name),
|
|
641
|
+
...tails.map((tail) => name(tail.producer))
|
|
642
|
+
];
|
|
643
|
+
return {
|
|
644
|
+
pagesFunction: references.services?.pages_function === true,
|
|
645
|
+
workers: [...new Set(workers)]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* DELETE a Worker even though other Workers still bind to it. Cloudflare
|
|
650
|
+
* leaves those bindings pointing at nothing, so callers first check that
|
|
651
|
+
* every dependent is going away too.
|
|
652
|
+
*/
|
|
653
|
+
async forceDeleteWorker(accountId, workerName) {
|
|
654
|
+
await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}?force=true`);
|
|
655
|
+
}
|
|
656
|
+
async deleteQueueConsumer(accountId, queueId, consumerId) {
|
|
657
|
+
await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(queueId)}/consumers/${encodeURIComponent(consumerId)}`);
|
|
658
|
+
}
|
|
659
|
+
async listDispatchNamespaceScripts(accountId, namespace) {
|
|
660
|
+
return (await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/dispatch/namespaces/${encodeURIComponent(namespace)}/scripts`)).map((entry) => requiredString(entry.id, "Dispatch namespace script name"));
|
|
661
|
+
}
|
|
662
|
+
async deleteDispatchNamespaceScript(accountId, namespace, scriptName) {
|
|
663
|
+
await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/workers/dispatch/namespaces/${encodeURIComponent(namespace)}/scripts/${encodeURIComponent(scriptName)}`);
|
|
664
|
+
}
|
|
665
|
+
/** Read one Access application or service token; null when it is gone. */
|
|
666
|
+
async getAccessResource(accountId, kind, id) {
|
|
667
|
+
try {
|
|
668
|
+
const result = await this.request(accessResourcePath(accountId, kind, id));
|
|
669
|
+
return {
|
|
670
|
+
id: requiredString(result.id, "Access resource id"),
|
|
671
|
+
name: requiredString(result.name, "Access resource name")
|
|
672
|
+
};
|
|
592
673
|
} catch (error) {
|
|
593
|
-
if (error instanceof PlatformCloudflareApiError && error.status === 404) return;
|
|
674
|
+
if (error instanceof PlatformCloudflareApiError && error.status === 404) return null;
|
|
594
675
|
throw error;
|
|
595
676
|
}
|
|
596
677
|
}
|
|
597
|
-
async
|
|
598
|
-
|
|
678
|
+
async deleteAccessResource(accountId, kind, id) {
|
|
679
|
+
await this.#deleteIfPresent(accessResourcePath(accountId, kind, id));
|
|
599
680
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
headers: { "Cloudflare-Workers-Script-Api-Date": "2025-08-01" },
|
|
608
|
-
method: "POST"
|
|
681
|
+
/** Email Routing rules in a zone, without the catch-all, which has its own endpoint. */
|
|
682
|
+
async listEmailRoutingRules(zoneId) {
|
|
683
|
+
const entries = [];
|
|
684
|
+
for (let page = 1;; page++) {
|
|
685
|
+
const query = new URLSearchParams({
|
|
686
|
+
page: String(page),
|
|
687
|
+
per_page: "50"
|
|
609
688
|
});
|
|
689
|
+
const result = await this.request(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules?${query}`);
|
|
690
|
+
entries.push(...result);
|
|
691
|
+
if (result.length < 50) break;
|
|
692
|
+
}
|
|
693
|
+
return entries.filter((entry) => !(Array.isArray(entry.matchers) ? entry.matchers : []).some((matcher) => matcher?.type === "all")).map((entry) => ({
|
|
694
|
+
id: requiredString(entry.id ?? entry.tag, "Email Routing rule id"),
|
|
695
|
+
...parseEmailRoutingTarget(entry)
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
async deleteEmailRoutingRule(zoneId, ruleId) {
|
|
699
|
+
await this.#deleteIfPresent(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules/${encodeURIComponent(ruleId)}`);
|
|
700
|
+
}
|
|
701
|
+
/** The zone's Email Routing catch-all rule; null when the zone has none. */
|
|
702
|
+
async getEmailRoutingCatchAll(zoneId) {
|
|
703
|
+
try {
|
|
704
|
+
return parseEmailRoutingTarget(await this.request(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules/catch_all`));
|
|
610
705
|
} catch (error) {
|
|
611
|
-
if (
|
|
706
|
+
if (error instanceof PlatformCloudflareApiError && error.status === 404) return null;
|
|
612
707
|
throw error;
|
|
613
708
|
}
|
|
614
709
|
}
|
|
710
|
+
/** Point the catch-all back at its disabled default, which drops mail. */
|
|
711
|
+
async resetEmailRoutingCatchAll(zoneId) {
|
|
712
|
+
await this.request(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules/catch_all`, {
|
|
713
|
+
method: "PUT",
|
|
714
|
+
body: JSON.stringify({
|
|
715
|
+
matchers: [{ type: "all" }],
|
|
716
|
+
actions: [{ type: "drop" }],
|
|
717
|
+
enabled: false
|
|
718
|
+
})
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async removeRoutes(_accountId, _zoneId, tracked) {
|
|
722
|
+
if (tracked.routes.length > 0 || tracked.customDomains.length > 0) throw new Error("platform: Cloudflare does not support conditional Worker traffic deletion. Retaining routes and custom domains for explicit dashboard cleanup.");
|
|
723
|
+
}
|
|
615
724
|
};
|
|
725
|
+
function accessResourcePath(accountId, kind, id) {
|
|
726
|
+
return `/accounts/${encodeURIComponent(accountId)}/access/${kind === "application" ? "apps" : "service_tokens"}/${encodeURIComponent(id)}`;
|
|
727
|
+
}
|
|
728
|
+
function parseEmailRoutingTarget(rule) {
|
|
729
|
+
const actions = Array.isArray(rule.actions) ? rule.actions : [];
|
|
730
|
+
const action = actions.length === 1 ? actions[0] : null;
|
|
731
|
+
const value = Array.isArray(action?.value) ? action.value : [];
|
|
732
|
+
return {
|
|
733
|
+
enabled: rule.enabled === true,
|
|
734
|
+
workerName: action?.type === "worker" && value.length === 1 && typeof value[0] === "string" ? value[0] : null
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function cloudflareErrorDetails(envelope, status) {
|
|
738
|
+
return envelope.errors?.filter((error) => error.message).map((error) => error.code === void 0 ? error.message : `${error.message} (code ${error.code})`).join(", ") || `HTTP ${status}`;
|
|
739
|
+
}
|
|
616
740
|
function encodeR2ObjectKey(key) {
|
|
617
741
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
618
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* An AI Gateway creation time as ISO 8601 UTC. Cloudflare reports it without a
|
|
745
|
+
* zone ("2026-10-05 18:29:14"), which JavaScript would read as local time.
|
|
746
|
+
*/
|
|
747
|
+
function withCreatedAt(value) {
|
|
748
|
+
if (typeof value !== "string") return {};
|
|
749
|
+
const zoneless = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(value);
|
|
750
|
+
const time = Date.parse(zoneless ? `${value.replace(" ", "T")}Z` : value);
|
|
751
|
+
return Number.isNaN(time) ? {} : { createdAt: new Date(time).toISOString() };
|
|
752
|
+
}
|
|
619
753
|
function requiredString(value, label) {
|
|
620
754
|
if (typeof value !== "string" || value === "") throw new Error(`platform: ${label} is missing from the Cloudflare response.`);
|
|
621
755
|
return value;
|
|
@@ -847,6 +981,18 @@ function platformAccessOverlapsOrigin(destination, origin) {
|
|
|
847
981
|
const pattern = "^" + destination.split("/")[0].split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$";
|
|
848
982
|
return new RegExp(pattern, "i").test(new URL(origin).hostname);
|
|
849
983
|
}
|
|
984
|
+
/** Whether an application path-covers, or exactly covers, any protected platform origin. */
|
|
985
|
+
function platformAccessOverlapsOrigins(application, origins) {
|
|
986
|
+
return platformAccessPublicDestinations(application).some((destination) => origins.some((origin) => platformAccessOverlapsOrigin(destination, origin)) && (destination.includes("/") || origins.some((origin) => destination === new URL(origin).hostname)));
|
|
987
|
+
}
|
|
988
|
+
/** A definitive 4xx means the create did not happen, so a retry may not find a hidden duplicate. */
|
|
989
|
+
function platformAccessCreateRejected(error) {
|
|
990
|
+
return error instanceof PlatformCloudflareApiError && error.status >= 400 && error.status < 500 && ![
|
|
991
|
+
408,
|
|
992
|
+
409,
|
|
993
|
+
429
|
|
994
|
+
].includes(error.status);
|
|
995
|
+
}
|
|
850
996
|
async function inspectPlatformAccess(client, input) {
|
|
851
997
|
const authDomain = text(object(await client.get("organizations"), "Access organization").auth_domain, "Access team domain");
|
|
852
998
|
const teamDomain = authDomain.startsWith("https://") ? authDomain : `https://${authDomain}`;
|
|
@@ -881,7 +1027,6 @@ async function provisionPlatformAccess(client, input, previous, save) {
|
|
|
881
1027
|
if (url.origin !== value || url.protocol !== "https:") throw new Error("platform: Access requires exact HTTPS origins without credentials, paths, or query strings.");
|
|
882
1028
|
return url.origin;
|
|
883
1029
|
}))];
|
|
884
|
-
for (const origin of origins) if (!origin.startsWith("https://")) throw new Error("platform: Access protection requires HTTPS platform origins.");
|
|
885
1030
|
const state = previous ? structuredClone(previous) : {
|
|
886
1031
|
version: 1,
|
|
887
1032
|
accountId: input.accountId,
|
|
@@ -913,7 +1058,7 @@ async function provisionPlatformAccess(client, input, previous, save) {
|
|
|
913
1058
|
}
|
|
914
1059
|
if (input.protection) for (const app of inventory) {
|
|
915
1060
|
if (app.id === input.protection.applicationId || app.id === state.resources.protection?.id || app.name === state.resources.protection?.name) continue;
|
|
916
|
-
if (
|
|
1061
|
+
if (platformAccessOverlapsOrigins(app, origins)) throw new Error("platform: An existing Access application overlaps a platform endpoint. Connect or reconcile it explicitly before provisioning protection.");
|
|
917
1062
|
}
|
|
918
1063
|
const output = { resources: [] };
|
|
919
1064
|
const application = async (key, expected, existingId) => {
|
|
@@ -968,11 +1113,7 @@ async function provisionPlatformAccess(client, input, previous, save) {
|
|
|
968
1113
|
save(state);
|
|
969
1114
|
return object(await client.get(`apps/${encodeURIComponent(checkpoint.id)}`), "Access application");
|
|
970
1115
|
} catch (error) {
|
|
971
|
-
if (error
|
|
972
|
-
408,
|
|
973
|
-
409,
|
|
974
|
-
429
|
|
975
|
-
].includes(error.status)) {
|
|
1116
|
+
if (platformAccessCreateRejected(error)) {
|
|
976
1117
|
checkpoint.pending = false;
|
|
977
1118
|
save(state);
|
|
978
1119
|
}
|
|
@@ -1127,282 +1268,6 @@ async function verifyPlatformAccessApplication(client, input, app, type, machine
|
|
|
1127
1268
|
if (machineId && !currentPolicies.some((policy) => policy.decision === "non_identity")) throw new Error("platform: The platform service token is not admitted by Access.");
|
|
1128
1269
|
}
|
|
1129
1270
|
//#endregion
|
|
1130
|
-
//#region src/cli/platform-setup.ts
|
|
1131
|
-
function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
|
|
1132
|
-
return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + import_picocolors.default.underline(page.title) + "\x1B]8;;\x1B\\" : page.title + ": " + import_picocolors.default.underline(page.url);
|
|
1133
|
-
}
|
|
1134
|
-
function platformSetupFieldHelp(key) {
|
|
1135
|
-
const docs = (page, section, title) => platformSetupLink({
|
|
1136
|
-
title,
|
|
1137
|
-
url: `https://void.cloud/guide/platform/installation/${page}#${section}`
|
|
1138
|
-
});
|
|
1139
|
-
switch (key) {
|
|
1140
|
-
case "databaseUrl": return {
|
|
1141
|
-
placeholder: "postgresql://user:pw@…/database",
|
|
1142
|
-
description: "Provide a PostgreSQL database URL for platform users, projects, and deployments. Use an empty hosted database, e.g. " + platformSetupLink({
|
|
1143
|
-
title: "Neon",
|
|
1144
|
-
url: "https://neon.com/"
|
|
1145
|
-
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
1146
|
-
};
|
|
1147
|
-
case "jwtSecret": return { description: `Create a JWT signing key for platform login tokens. Run ${import_picocolors.default.blue("openssl rand -base64 32")}. Save the key in your password manager. ` + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
1148
|
-
case "projectSecretKey": return { description: `Generate a project encryption key for app secrets. Run ${import_picocolors.default.blue("openssl rand -base64 32")} separately. Save this key for recovery. ` + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
1149
|
-
default: return;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
function supportsSetupLinks() {
|
|
1153
|
-
return !!process.stdout.isTTY && ([
|
|
1154
|
-
"ghostty",
|
|
1155
|
-
"iTerm.app",
|
|
1156
|
-
"WezTerm",
|
|
1157
|
-
"vscode",
|
|
1158
|
-
"WarpTerminal"
|
|
1159
|
-
].includes(process.env.TERM_PROGRAM ?? "") || !!process.env.WT_SESSION || !!process.env.VTE_VERSION || !!process.env.KONSOLE_VERSION || process.env.TERM === "xterm-kitty");
|
|
1160
|
-
}
|
|
1161
|
-
/** Dashboard keys use permission-catalog labels, which can differ from product names. */
|
|
1162
|
-
function platformAccountTokenPermissions(includeAiGateway) {
|
|
1163
|
-
return [
|
|
1164
|
-
{
|
|
1165
|
-
key: "account_settings",
|
|
1166
|
-
type: "read"
|
|
1167
|
-
},
|
|
1168
|
-
{
|
|
1169
|
-
key: "workers_scripts",
|
|
1170
|
-
type: "edit"
|
|
1171
|
-
},
|
|
1172
|
-
{
|
|
1173
|
-
key: "workers_kv_storage",
|
|
1174
|
-
type: "edit"
|
|
1175
|
-
},
|
|
1176
|
-
{
|
|
1177
|
-
key: "workers_r2",
|
|
1178
|
-
type: "edit"
|
|
1179
|
-
},
|
|
1180
|
-
{
|
|
1181
|
-
key: "d1",
|
|
1182
|
-
type: "edit"
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
key: "queues",
|
|
1186
|
-
type: "edit"
|
|
1187
|
-
},
|
|
1188
|
-
{
|
|
1189
|
-
key: "account_analytics",
|
|
1190
|
-
type: "read"
|
|
1191
|
-
},
|
|
1192
|
-
{
|
|
1193
|
-
key: "workers_tail",
|
|
1194
|
-
type: "read"
|
|
1195
|
-
},
|
|
1196
|
-
{
|
|
1197
|
-
key: "query_cache",
|
|
1198
|
-
type: "edit"
|
|
1199
|
-
},
|
|
1200
|
-
...includeAiGateway ? [{
|
|
1201
|
-
key: "aig",
|
|
1202
|
-
type: "edit"
|
|
1203
|
-
}] : []
|
|
1204
|
-
];
|
|
1205
|
-
}
|
|
1206
|
-
function platformManagementSetupPage(plan, options = {}) {
|
|
1207
|
-
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1208
|
-
const zoneName = plan.deployment.application.zoneName;
|
|
1209
|
-
const apiHostname = plan.deployment.controlPlane.apiHostname;
|
|
1210
|
-
const access = options.access ?? (plan.deployment.authentication.accessProtection || plan.deployment.authentication.providers.includes("cloudflare-access") ? {
|
|
1211
|
-
protection: !!plan.deployment.authentication.accessProtection,
|
|
1212
|
-
readOnly: false
|
|
1213
|
-
} : void 0);
|
|
1214
|
-
const accessLevel = access && access.readOnly ? "read" : "edit";
|
|
1215
|
-
const appAccess = !!plan.deployment.appAccess;
|
|
1216
|
-
const appAccessLevel = appAccess ? "edit" : accessLevel;
|
|
1217
|
-
const permissions = [
|
|
1218
|
-
...platformAccountTokenPermissions(true),
|
|
1219
|
-
...zoneName || apiHostname || createsZone ? [
|
|
1220
|
-
{
|
|
1221
|
-
key: "zone",
|
|
1222
|
-
type: createsZone ? "edit" : "read"
|
|
1223
|
-
},
|
|
1224
|
-
{
|
|
1225
|
-
key: "dns",
|
|
1226
|
-
type: "edit"
|
|
1227
|
-
},
|
|
1228
|
-
{
|
|
1229
|
-
key: "workers_routes",
|
|
1230
|
-
type: "edit"
|
|
1231
|
-
}
|
|
1232
|
-
] : [],
|
|
1233
|
-
...access || appAccess ? [
|
|
1234
|
-
{
|
|
1235
|
-
key: "access",
|
|
1236
|
-
type: appAccessLevel
|
|
1237
|
-
},
|
|
1238
|
-
{
|
|
1239
|
-
key: "access_acct",
|
|
1240
|
-
type: "read"
|
|
1241
|
-
},
|
|
1242
|
-
...access && access.protection ? [{
|
|
1243
|
-
key: "access_service_token",
|
|
1244
|
-
type: accessLevel
|
|
1245
|
-
}] : []
|
|
1246
|
-
] : [],
|
|
1247
|
-
...zoneName && plan.deployment.application.baseDomain !== zoneName ? [{
|
|
1248
|
-
key: "ssl_and_certificates",
|
|
1249
|
-
type: "read"
|
|
1250
|
-
}] : []
|
|
1251
|
-
];
|
|
1252
|
-
return {
|
|
1253
|
-
title: "Cloudflare management API token",
|
|
1254
|
-
linkInTitle: true,
|
|
1255
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1256
|
-
to: "/" + plan.account.id + "/api-tokens",
|
|
1257
|
-
name: plan.deployment.displayName + " Management",
|
|
1258
|
-
permissionGroupKeys: JSON.stringify(permissions)
|
|
1259
|
-
}),
|
|
1260
|
-
instructions: [
|
|
1261
|
-
"This token lets the Void CLI install your platform. Void will now open a pre-configured token creation site. Please review the permissions and then paste the token here.",
|
|
1262
|
-
`Scope it to ${plan.account.name}${createsZone ? "; allow zone creation." : zoneName ? ` and zone ${zoneName}.` : "."}`,
|
|
1263
|
-
...apiHostname && apiHostname !== zoneName && !apiHostname.endsWith(`.${zoneName}`) ? [`Also include the zone for ${apiHostname}.`] : []
|
|
1264
|
-
].join("\n")
|
|
1265
|
-
};
|
|
1266
|
-
}
|
|
1267
|
-
function platformSetupPages(plan) {
|
|
1268
|
-
const permissions = [
|
|
1269
|
-
...platformAccountTokenPermissions(plan.actions.some((action) => action.kind === "ai-gateway" && action.action === "inspect")),
|
|
1270
|
-
...plan.deployment.application.zoneName ? [{
|
|
1271
|
-
key: "cache",
|
|
1272
|
-
type: "purge"
|
|
1273
|
-
}] : [],
|
|
1274
|
-
...plan.deployment.capabilities.customDomains ? [{
|
|
1275
|
-
key: "ssl_and_certificates",
|
|
1276
|
-
type: "edit"
|
|
1277
|
-
}] : [],
|
|
1278
|
-
...plan.deployment.capabilities.email ? [
|
|
1279
|
-
{
|
|
1280
|
-
key: "zone",
|
|
1281
|
-
type: "read"
|
|
1282
|
-
},
|
|
1283
|
-
{
|
|
1284
|
-
key: "zone_settings",
|
|
1285
|
-
type: "edit"
|
|
1286
|
-
},
|
|
1287
|
-
{
|
|
1288
|
-
key: "email_routing_address",
|
|
1289
|
-
type: "edit"
|
|
1290
|
-
},
|
|
1291
|
-
{
|
|
1292
|
-
key: "email_routing_rule",
|
|
1293
|
-
type: "edit"
|
|
1294
|
-
},
|
|
1295
|
-
{
|
|
1296
|
-
key: "email_sending",
|
|
1297
|
-
type: "edit"
|
|
1298
|
-
}
|
|
1299
|
-
] : []
|
|
1300
|
-
];
|
|
1301
|
-
return {
|
|
1302
|
-
runtime: {
|
|
1303
|
-
title: "Cloudflare runtime token",
|
|
1304
|
-
linkInTitle: true,
|
|
1305
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1306
|
-
to: "/" + plan.account.id + "/api-tokens",
|
|
1307
|
-
name: plan.deployment.displayName + " Runtime",
|
|
1308
|
-
permissionGroupKeys: JSON.stringify(permissions)
|
|
1309
|
-
}),
|
|
1310
|
-
instructions: "Create a Cloudflare runtime token that Void can use to manage the platform. Please review the permissions and then paste the token here."
|
|
1311
|
-
},
|
|
1312
|
-
github: {
|
|
1313
|
-
title: "GitHub OAuth App",
|
|
1314
|
-
url: "https://github.com/settings/applications/new",
|
|
1315
|
-
instructions: "GitHub Auth will be used by the users of your Void Platform instance. Register a GitHub app and generate a client secret.\nHomepage: " + plan.apiUrl + "\nRedirect URI: " + plan.apiUrl + "/auth/callback"
|
|
1316
|
-
},
|
|
1317
|
-
r2: {
|
|
1318
|
-
title: "R2 upload token",
|
|
1319
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1320
|
-
to: "/" + plan.account.id + "/r2/api-tokens/create",
|
|
1321
|
-
type: "account"
|
|
1322
|
-
}),
|
|
1323
|
-
instructions: [
|
|
1324
|
-
"Select Object Read & Write.",
|
|
1325
|
-
"Keep Apply to all buckets in this account selected.",
|
|
1326
|
-
"Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
|
|
1327
|
-
].join("\n")
|
|
1328
|
-
}
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
function formatPlatformSetupPlan(plan, options = {}) {
|
|
1332
|
-
const writesDns = plan.actions.some((action) => action.kind === "zone" && action.action === "create" || action.kind === "dns-record" && action.action !== "reuse");
|
|
1333
|
-
return [
|
|
1334
|
-
"The installation will guide you through setting up the following credentials:",
|
|
1335
|
-
"",
|
|
1336
|
-
...[
|
|
1337
|
-
...options.managementToken ? [options.managementAccess ? `Cloudflare management API token for ${writesDns ? "DNS changes and " : ""}Access setup` : "Cloudflare management API token for DNS changes"] : [],
|
|
1338
|
-
"PostgreSQL database URL",
|
|
1339
|
-
"Cloudflare runtime token",
|
|
1340
|
-
"R2 upload token",
|
|
1341
|
-
...plan.deployment.authentication.providers.includes("github") ? ["GitHub OAuth App client ID and secret"] : [],
|
|
1342
|
-
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => (provider === "google" ? "Google OAuth" : provider === "oidc" ? "OIDC" : "Cloudflare Access OIDC") + " client ID and secret"),
|
|
1343
|
-
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access application and installer credentials"] : [],
|
|
1344
|
-
...plan.deployment.appAccess ? ["Cloudflare Access check and app-management token when needed (stored encrypted)"] : [],
|
|
1345
|
-
"JWT signing key and project encryption key"
|
|
1346
|
-
].map((credential) => `• ${credential}`)
|
|
1347
|
-
].join("\n");
|
|
1348
|
-
}
|
|
1349
|
-
function platformInstallCommandArguments(options) {
|
|
1350
|
-
const parts = [
|
|
1351
|
-
options.commandName,
|
|
1352
|
-
"platform",
|
|
1353
|
-
"install"
|
|
1354
|
-
];
|
|
1355
|
-
const flag = (name, value) => {
|
|
1356
|
-
if (value) parts.push(name, value);
|
|
1357
|
-
};
|
|
1358
|
-
if (options.resumeId) {
|
|
1359
|
-
parts.push("--resume");
|
|
1360
|
-
flag("--name", options.resumeId);
|
|
1361
|
-
} else {
|
|
1362
|
-
flag("--name", options.installationSlug);
|
|
1363
|
-
flag("--account", options.accountId);
|
|
1364
|
-
flag("--display-name", options.displayName);
|
|
1365
|
-
if (options.workersDev) parts.push("--workers-dev");
|
|
1366
|
-
else {
|
|
1367
|
-
flag("--application-domain", options.applicationDomain);
|
|
1368
|
-
flag("--zone", options.zoneName);
|
|
1369
|
-
if (options.dedicatedZone) parts.push("--dedicated-zone");
|
|
1370
|
-
else parts.push("--no-dedicated-zone");
|
|
1371
|
-
}
|
|
1372
|
-
flag("--control-plane-domain", options.controlPlaneDomain);
|
|
1373
|
-
if (!options.controlPlaneDomain) parts.push("--no-control-plane-domain");
|
|
1374
|
-
}
|
|
1375
|
-
flag("--hyperdrive", options.hyperdrive ?? "");
|
|
1376
|
-
flag("--auth-config", options.authConfig ?? "");
|
|
1377
|
-
if (!options.resumeId && options.appAccess) flag("--app-access", options.appAccess);
|
|
1378
|
-
if (!options.authConfig && !options.resumeId && options.authentication) {
|
|
1379
|
-
flag("--login-methods", options.authentication.providers.join(","));
|
|
1380
|
-
parts.push(options.authentication.accessProtection ? "--access-protection" : "--no-access-protection");
|
|
1381
|
-
}
|
|
1382
|
-
flag("--dashboard-url", options.dashboardUrl ?? "");
|
|
1383
|
-
flag("--runtime", options.runtime);
|
|
1384
|
-
if (options.yes) parts.push("--yes");
|
|
1385
|
-
return parts;
|
|
1386
|
-
}
|
|
1387
|
-
function formatPlatformInstallCommand(options) {
|
|
1388
|
-
return platformInstallCommandArguments(options).map((value) => {
|
|
1389
|
-
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
1390
|
-
return process.platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
1391
|
-
}).join(" ");
|
|
1392
|
-
}
|
|
1393
|
-
function openPlatformSetupPage(page, dependencies = {}) {
|
|
1394
|
-
log.info((page.linkInTitle ? platformSetupLink(page) : page.title) + "\n" + page.instructions);
|
|
1395
|
-
if (!page.linkInTitle) console.log(platformSetupLink({
|
|
1396
|
-
...page,
|
|
1397
|
-
title: "Open " + page.title
|
|
1398
|
-
}));
|
|
1399
|
-
if (dependencies.interactive ?? process.stdin.isTTY === true) try {
|
|
1400
|
-
(dependencies.open ?? ((url) => openBrowser(url, execFile)))(page.url);
|
|
1401
|
-
} catch {
|
|
1402
|
-
log.info("Could not open the browser. Use the link above.");
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
//#endregion
|
|
1406
1271
|
//#region src/cli/platform-access-setup.ts
|
|
1407
1272
|
function accessSetupGuide() {
|
|
1408
1273
|
return platformSetupLink({
|
|
@@ -1433,11 +1298,11 @@ async function selectCompanyAccessPolicies(client, initialPolicies, accountId, m
|
|
|
1433
1298
|
let choices;
|
|
1434
1299
|
if (options.length === 0) {
|
|
1435
1300
|
log.info("No reusable Allow or Deny policies were found in this account. Create an Allow policy for your company, or refresh after saving one.");
|
|
1436
|
-
choices = [
|
|
1301
|
+
choices = [platformPromptAnswer(await select({
|
|
1437
1302
|
message: "Company Access policies:",
|
|
1438
1303
|
options: actions
|
|
1439
1304
|
}))];
|
|
1440
|
-
} else choices =
|
|
1305
|
+
} else choices = platformPromptAnswer(await multiselect({
|
|
1441
1306
|
message: "Company Access policies (select all that apply):",
|
|
1442
1307
|
options: [...options, ...actions],
|
|
1443
1308
|
initialValues: selectedIds,
|
|
@@ -1456,7 +1321,7 @@ async function selectCompanyAccessPolicies(client, initialPolicies, accountId, m
|
|
|
1456
1321
|
title: "Open Cloudflare account",
|
|
1457
1322
|
url: `https://dash.cloudflare.com/${encodeURIComponent(accountId)}`
|
|
1458
1323
|
}));
|
|
1459
|
-
if (!
|
|
1324
|
+
if (!platformPromptAnswer(await confirm({
|
|
1460
1325
|
message: "Have you saved the new policy?",
|
|
1461
1326
|
initialValue: true
|
|
1462
1327
|
}))) continue;
|
|
@@ -1537,10 +1402,6 @@ function parsePlatformAccessSetupSpec(input) {
|
|
|
1537
1402
|
}
|
|
1538
1403
|
return value;
|
|
1539
1404
|
}
|
|
1540
|
-
function answer$2(value) {
|
|
1541
|
-
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
1542
|
-
return value;
|
|
1543
|
-
}
|
|
1544
1405
|
function secretReference(name) {
|
|
1545
1406
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !process.env[name]) throw new Error("platform: Supply the referenced Access credential in the environment.");
|
|
1546
1407
|
return process.env[name];
|
|
@@ -1579,7 +1440,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1579
1440
|
if (input.protection) throw new Error("platform: Protection requires cloudflareAccess create or existing application verification in --auth-config.");
|
|
1580
1441
|
return;
|
|
1581
1442
|
}
|
|
1582
|
-
const mode =
|
|
1443
|
+
const mode = platformPromptAnswer(await select({
|
|
1583
1444
|
message: "Cloudflare Access setup:",
|
|
1584
1445
|
options: [
|
|
1585
1446
|
{
|
|
@@ -1602,17 +1463,17 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1602
1463
|
spec = { mode };
|
|
1603
1464
|
if (mode === "existing") {
|
|
1604
1465
|
log.info("Enter the account that owns your company’s Access configuration. This may differ from the account hosting Void.");
|
|
1605
|
-
spec.accountId =
|
|
1466
|
+
spec.accountId = platformPromptAnswer(await text$1({
|
|
1606
1467
|
message: "Cloudflare identity account ID:",
|
|
1607
1468
|
initialValue: input.plan.account.id
|
|
1608
1469
|
}));
|
|
1609
1470
|
if (input.login) {
|
|
1610
1471
|
log.info("Enter the ID of the Access application that provides company sign-in to Void.");
|
|
1611
|
-
spec.loginApplicationId =
|
|
1472
|
+
spec.loginApplicationId = platformPromptAnswer(await text$1({ message: "Existing Access OIDC application ID:" }));
|
|
1612
1473
|
}
|
|
1613
1474
|
if (input.protection) {
|
|
1614
1475
|
log.info("Enter the ID of the Access application that protects the platform’s UI and APIs.");
|
|
1615
|
-
spec.protectionApplicationId =
|
|
1476
|
+
spec.protectionApplicationId = platformPromptAnswer(await text$1({ message: "Existing Access protection application ID:" }));
|
|
1616
1477
|
}
|
|
1617
1478
|
}
|
|
1618
1479
|
}
|
|
@@ -1632,7 +1493,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1632
1493
|
if (!(error instanceof PlatformCloudflareApiError) || ![401, 403].includes(error.status) || !process.stdin.isTTY || credential) throw error;
|
|
1633
1494
|
log.info(`Your current token lacks the required Access permissions for account ${accountId}. Create a token for that account using the permissions in the setup guide.`);
|
|
1634
1495
|
console.log(accessSetupGuide());
|
|
1635
|
-
const token =
|
|
1496
|
+
const token = platformPromptAnswer(await password({ message: "Access setup API token:" }));
|
|
1636
1497
|
input.save({ accessManagementToken: token });
|
|
1637
1498
|
management = new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl });
|
|
1638
1499
|
client = new PlatformAccessClient(management, accountId);
|
|
@@ -1641,7 +1502,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1641
1502
|
policies = await client.list("policies");
|
|
1642
1503
|
}
|
|
1643
1504
|
if (!spec.identityProviderIds && process.stdin.isTTY) log.info("Choose how employees sign in—for example, your company’s Okta or Google Workspace connection.");
|
|
1644
|
-
const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ?
|
|
1505
|
+
const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ? platformPromptAnswer(await multiselect({
|
|
1645
1506
|
message: "Company identity providers:",
|
|
1646
1507
|
required: true,
|
|
1647
1508
|
options: providers.filter((provider) => typeof provider.id === "string").map(platformAccessIdentityProvider).map((provider) => ({
|
|
@@ -1676,9 +1537,9 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1676
1537
|
else if (process.stdin.isTTY) {
|
|
1677
1538
|
log.info("This lets the installer reach the protected platform during setup. Use a service token allowed by the existing Access application.");
|
|
1678
1539
|
serviceToken = {
|
|
1679
|
-
id:
|
|
1680
|
-
clientId:
|
|
1681
|
-
clientSecret:
|
|
1540
|
+
id: platformPromptAnswer(await text$1({ message: "Existing admitted Access service token ID:" })),
|
|
1541
|
+
clientId: platformPromptAnswer(await password({ message: "Access service token client ID:" })),
|
|
1542
|
+
clientSecret: platformPromptAnswer(await password({ message: "Access service token client secret:" }))
|
|
1682
1543
|
};
|
|
1683
1544
|
input.save({ accessExistingServiceTokenJson: JSON.stringify(serviceToken) });
|
|
1684
1545
|
}
|
|
@@ -1699,7 +1560,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1699
1560
|
};
|
|
1700
1561
|
console.log(JSON.stringify({ accessPlan: reviewed }, null, 2));
|
|
1701
1562
|
if (!input.yes) {
|
|
1702
|
-
if (!process.stdin.isTTY || !
|
|
1563
|
+
if (!process.stdin.isTTY || !platformPromptAnswer(await confirm({
|
|
1703
1564
|
message: "Apply this Access configuration?",
|
|
1704
1565
|
initialValue: false
|
|
1705
1566
|
}))) throw new Error("platform: Access setup changes require confirmation or --yes.");
|
|
@@ -1714,7 +1575,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1714
1575
|
let loginClientSecret = spec.loginClientSecretEnv ? secretReference(spec.loginClientSecretEnv) : input.checkpoint.accessExistingLoginSecret;
|
|
1715
1576
|
if (input.login && spec.mode === "existing" && !loginClientSecret && process.stdin.isTTY) {
|
|
1716
1577
|
log.info("Enter the client secret from the existing Access login application’s OIDC settings.");
|
|
1717
|
-
loginClientSecret =
|
|
1578
|
+
loginClientSecret = platformPromptAnswer(await password({ message: "Existing Access OIDC client secret:" }));
|
|
1718
1579
|
input.save({ accessExistingLoginSecret: loginClientSecret });
|
|
1719
1580
|
}
|
|
1720
1581
|
const result = await provisionPlatformAccess(client, {
|
|
@@ -1746,6 +1607,28 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1746
1607
|
}
|
|
1747
1608
|
//#endregion
|
|
1748
1609
|
//#region src/cli/platform-auth.ts
|
|
1610
|
+
/** Use the selected credential for read-only checks before taking lifecycle locks. */
|
|
1611
|
+
async function verifyPlatformCloudflareCredential(client, input) {
|
|
1612
|
+
const source = input.source === "saved" ? "saved management token" : input.source === "environment" ? "environment API token" : "Cloudflare browser login";
|
|
1613
|
+
const retry = input.source === "oauth" ? "Run `void cloudflare login`, or set CLOUDFLARE_API_TOKEN for this account, then retry." : "Set CLOUDFLARE_API_TOKEN for this account, then retry.";
|
|
1614
|
+
const check = async (label, permission, action, allowMissing = false) => {
|
|
1615
|
+
try {
|
|
1616
|
+
await action();
|
|
1617
|
+
} catch (cause) {
|
|
1618
|
+
const status = cause instanceof PlatformCloudflareApiError ? cause.status : null;
|
|
1619
|
+
if (status === 404 && allowMissing) return;
|
|
1620
|
+
const detail = status === 401 || status === 403 ? `Check that the credential is valid, scoped to this account, and includes ${permission}. ${retry}` : status === 404 ? "Check the bucket name and account scope before retrying." : status === 429 ? "Cloudflare rate-limited this check. Retry shortly." : "Retry the command after Cloudflare connectivity is restored.";
|
|
1621
|
+
throw new Error(`platform: ${label} is unavailable${status === null ? "" : ` (HTTP ${status})`} using the ${source}. ${detail}`, { cause });
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
let accounts = [];
|
|
1625
|
+
await check("Cloudflare account access", "Account → Account Settings → Read", async () => {
|
|
1626
|
+
accounts = await client.listAccounts();
|
|
1627
|
+
});
|
|
1628
|
+
if (!accounts.some((account) => account.id === input.account.id)) throw new Error(`platform: The ${source} cannot access Cloudflare account ${JSON.stringify(input.account.name)} (${input.account.id}). ${retry}`);
|
|
1629
|
+
await check("Cloudflare Workers access", "Account → Workers Scripts → Edit", () => client.listWorkerNames(input.account.id));
|
|
1630
|
+
for (const name of new Set(input.r2Buckets)) await check(`R2 bucket ${JSON.stringify(name)}`, "Account → Workers R2 Storage → Edit", () => client.request(`/accounts/${encodeURIComponent(input.account.id)}/r2/buckets/${encodeURIComponent(name)}`), input.allowMissingR2Buckets);
|
|
1631
|
+
}
|
|
1749
1632
|
/** Check Access inventory permissions before the installer starts provisioning. */
|
|
1750
1633
|
async function verifyPlatformManagementAccess(client, accountId, access) {
|
|
1751
1634
|
const base = `/accounts/${encodeURIComponent(accountId)}/access`;
|
|
@@ -1759,8 +1642,8 @@ async function verifyPlatformManagementAccess(client, accountId, access) {
|
|
|
1759
1642
|
}
|
|
1760
1643
|
function assertPlatformOperatorCredentialCanApplyPlan(credential, plan, access) {
|
|
1761
1644
|
if (!platformOperatorNeedsManagementToken(credential, plan, access)) return;
|
|
1762
|
-
const createsZone = plan
|
|
1763
|
-
const writesDns = plan
|
|
1645
|
+
const createsZone = planCreatesZone(plan);
|
|
1646
|
+
const writesDns = planWritesDns(plan);
|
|
1764
1647
|
const permissions = [
|
|
1765
1648
|
...createsZone ? ["Account → Zone → Edit"] : [],
|
|
1766
1649
|
...writesDns ? ["Zone → DNS → Edit"] : [],
|
|
@@ -1773,7 +1656,13 @@ function assertPlatformOperatorCredentialCanApplyPlan(credential, plan, access)
|
|
|
1773
1656
|
throw new Error(`platform: Cloudflare browser login cannot authorize the planned ${access ? "Access setup" : "zone or DNS changes"}. Set CLOUDFLARE_API_TOKEN with ${permissions.join(" and ")}, scoped to the selected account${createsZone || writesDns ? " and application zone" : ""}, then rerun the command.`);
|
|
1774
1657
|
}
|
|
1775
1658
|
function platformOperatorNeedsManagementToken(credential, plan, access) {
|
|
1776
|
-
return credential.source === "oauth" && (!!access || plan
|
|
1659
|
+
return credential.source === "oauth" && (!!access || planCreatesZone(plan) || planWritesDns(plan));
|
|
1660
|
+
}
|
|
1661
|
+
function planCreatesZone(plan) {
|
|
1662
|
+
return plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1663
|
+
}
|
|
1664
|
+
function planWritesDns(plan) {
|
|
1665
|
+
return plan.actions.some((action) => action.kind === "dns-record" && action.action !== "reuse");
|
|
1777
1666
|
}
|
|
1778
1667
|
async function resolvePlatformCloudflareCredential(dependencies = {}) {
|
|
1779
1668
|
const { credential } = await resolvePlatformCloudflareCredentialWithInput(async () => void 0, dependencies);
|
|
@@ -1837,26 +1726,18 @@ async function readExistingCredential(dependencies, runWrangler) {
|
|
|
1837
1726
|
if (!(dependencies.interactive ?? process.stdin.isTTY === true)) throw new Error("platform: Cloudflare authentication is required. Run `void cloudflare login` interactively, or set CLOUDFLARE_API_TOKEN in CI.");
|
|
1838
1727
|
return await readWranglerOAuthCredential(runWrangler);
|
|
1839
1728
|
}
|
|
1840
|
-
async function readWranglerOAuthCredential(runWrangler
|
|
1729
|
+
async function readWranglerOAuthCredential(runWrangler) {
|
|
1841
1730
|
const result = await runWrangler({ type: "auth.token" }, true);
|
|
1842
1731
|
if (result.status !== 0 || result.signal || result.error) return null;
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
};
|
|
1850
|
-
} catch {
|
|
1851
|
-
return null;
|
|
1852
|
-
}
|
|
1732
|
+
const parsed = result.data;
|
|
1733
|
+
if (!parsed || parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
1734
|
+
return {
|
|
1735
|
+
source: "oauth",
|
|
1736
|
+
token: parsed.token.trim()
|
|
1737
|
+
};
|
|
1853
1738
|
}
|
|
1854
1739
|
//#endregion
|
|
1855
1740
|
//#region src/cli/platform-app-access.ts
|
|
1856
|
-
function answer$1(value) {
|
|
1857
|
-
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
1858
|
-
return value;
|
|
1859
|
-
}
|
|
1860
1741
|
async function selectInstallationAppAccess(authentication, recorded, choice) {
|
|
1861
1742
|
const config = authentication.file?.appAccess;
|
|
1862
1743
|
if (config && (typeof config !== "object" || Array.isArray(config) || "apiToken" in config || config.tokenEnv !== void 0 && (typeof config.tokenEnv !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(config.tokenEnv)))) throw new Error("platform: App access configuration must use tokenEnv; plaintext tokens are not accepted.");
|
|
@@ -1872,7 +1753,7 @@ async function selectInstallationAppAccess(authentication, recorded, choice) {
|
|
|
1872
1753
|
return recorded.appAccess;
|
|
1873
1754
|
}
|
|
1874
1755
|
const protectedPlatform = !!authentication.authentication.accessProtection;
|
|
1875
|
-
const selected = supplied ?? (!process.stdin.isTTY ? protectedPlatform ? "platform" : "public" :
|
|
1756
|
+
const selected = supplied ?? (!process.stdin.isTTY ? protectedPlatform ? "platform" : "public" : platformPromptAnswer(await select({
|
|
1876
1757
|
message: "Who should be able to access deployed apps?",
|
|
1877
1758
|
initialValue: protectedPlatform ? "platform" : "public",
|
|
1878
1759
|
options: [
|
|
@@ -1943,7 +1824,7 @@ async function prepareInstallationAppAccess(input) {
|
|
|
1943
1824
|
const collectToken = async () => {
|
|
1944
1825
|
if (!process.stdin.isTTY) throw new Error("platform: App access requires VOID_PLATFORM_APP_ACCESS_TOKEN or appAccess.tokenEnv in --auth-config, scoped to the hosting account.");
|
|
1945
1826
|
tokenPage();
|
|
1946
|
-
const value = String(
|
|
1827
|
+
const value = String(platformPromptAnswer(await password({ message: "App access API token (stored encrypted when app rules are needed):" }))).trim();
|
|
1947
1828
|
if (!value || /\s/.test(value)) throw new Error("platform: An app access token without whitespace is required.");
|
|
1948
1829
|
input.save({
|
|
1949
1830
|
appAccessToken: value,
|
|
@@ -1987,7 +1868,7 @@ async function prepareInstallationAppAccess(input) {
|
|
|
1987
1868
|
if (!organization) throw new Error(`platform: Enable Zero Trust in account ${input.plan.account.name}, then resume to configure app access.`);
|
|
1988
1869
|
if (!token) {
|
|
1989
1870
|
log.info("Void needs an Access-management token stored encrypted to manage access for future apps and custom domains.");
|
|
1990
|
-
if (reusable && process.stdin.isTTY &&
|
|
1871
|
+
if (reusable && process.stdin.isTTY && platformPromptAnswer(await confirm({
|
|
1991
1872
|
message: "Reuse the setup token for ongoing app access management?",
|
|
1992
1873
|
initialValue: true
|
|
1993
1874
|
}))) {
|
|
@@ -2007,7 +1888,7 @@ async function prepareInstallationAppAccess(input) {
|
|
|
2007
1888
|
if (!process.stdin.isTTY) throw new Error("platform: Set appAccess.identityProviderIds in --auth-config for the hosting account.");
|
|
2008
1889
|
const providers = (await access.list("identity_providers")).map(platformAccessIdentityProvider);
|
|
2009
1890
|
if (!providers.length) throw new Error(`platform: Add an identity provider in Zero Trust for ${input.plan.account.name}, then resume.`);
|
|
2010
|
-
identityProviderIds =
|
|
1891
|
+
identityProviderIds = platformPromptAnswer(await multiselect({
|
|
2011
1892
|
message: "App identity providers:",
|
|
2012
1893
|
required: true,
|
|
2013
1894
|
options: providers.map((p) => ({
|
|
@@ -2516,7 +2397,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2516
2397
|
client.listHyperdriveConfigs(account.id),
|
|
2517
2398
|
client.listWorkerNames(account.id)
|
|
2518
2399
|
]);
|
|
2519
|
-
const dnsRecords = zone ? await client
|
|
2400
|
+
const dnsRecords = zone ? await listZoneDnsRecords(client, zone, wildcardHostname) : [];
|
|
2520
2401
|
const remoteOwnership = existing ? await readRemoteOwnership(client, account.id, routingKv, existing) : null;
|
|
2521
2402
|
const intentProofs = /* @__PURE__ */ new Set();
|
|
2522
2403
|
const ambiguousResources = /* @__PURE__ */ new Set();
|
|
@@ -2560,7 +2441,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
2560
2441
|
const matchingDomain = workerDomains.find((domain) => domain.hostname === apiHostname);
|
|
2561
2442
|
const expectedWorker = deployment.resources.apiWorker;
|
|
2562
2443
|
if (matchingDomain && matchingDomain.service !== expectedWorker) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} is already assigned to Worker ${JSON.stringify(matchingDomain.service)}. Choose another hostname.`);
|
|
2563
|
-
const records = controlPlaneZone ? await client
|
|
2444
|
+
const records = controlPlaneZone ? await listZoneDnsRecords(client, controlPlaneZone, apiHostname) : [];
|
|
2564
2445
|
const ownedApiWorker = intentProofs.has("api-worker") || existing?.resources.some((resource) => resource.key === "api-worker" && resource.kind === "worker" && resource.name === expectedWorker && resource.state === "ready");
|
|
2565
2446
|
if (records.length > 0 && (!matchingDomain || !ownedApiWorker)) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} already has a DNS record that installation would replace. Choose another hostname or remove the record explicitly.`);
|
|
2566
2447
|
}
|
|
@@ -2926,6 +2807,14 @@ function platformAdoptionIntentHash(manifest, key, kind, id, name) {
|
|
|
2926
2807
|
ownershipNonce: manifest.ownershipNonce
|
|
2927
2808
|
})).digest("hex");
|
|
2928
2809
|
}
|
|
2810
|
+
async function listZoneDnsRecords(client, zone, hostname) {
|
|
2811
|
+
try {
|
|
2812
|
+
return await client.listDnsRecords(zone.id, hostname);
|
|
2813
|
+
} catch (error) {
|
|
2814
|
+
if (error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403)) throw new Error(`platform: Cloudflare denied reading DNS records for ${zone.name} (HTTP ${error.status}). Add Zone → DNS → Edit for this zone to the management token, then rerun.`, { cause: error });
|
|
2815
|
+
throw error;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2929
2818
|
async function readRemoteOwnership(client, accountId, routingNamespaces, existing) {
|
|
2930
2819
|
for (const namespace of routingNamespaces) {
|
|
2931
2820
|
const value = await client.getKvValue(accountId, namespace.id, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY);
|
|
@@ -2991,6 +2880,22 @@ function trafficConfigHash(kind, value) {
|
|
|
2991
2880
|
value
|
|
2992
2881
|
})).digest("hex");
|
|
2993
2882
|
}
|
|
2883
|
+
/**
|
|
2884
|
+
* The installer Worker that a recorded route or custom domain serves, when the
|
|
2885
|
+
* record matches exactly what the installer publishes for this deployment.
|
|
2886
|
+
*/
|
|
2887
|
+
function platformTrafficAttachmentWorker(deployment, resource) {
|
|
2888
|
+
const managed = platformManagedWorkerTraffic(deployment);
|
|
2889
|
+
if (resource.kind === "worker-route") {
|
|
2890
|
+
const target = managed.routes.find((route) => route.pattern === resource.name);
|
|
2891
|
+
return target && trafficConfigHash("worker-route", target) === resource.configHash ? target.script : null;
|
|
2892
|
+
}
|
|
2893
|
+
if (resource.kind === "worker-custom-domain") {
|
|
2894
|
+
const target = managed.customDomains.find((domain) => domain.hostname === resource.name);
|
|
2895
|
+
return target && trafficConfigHash("worker-custom-domain", target) === resource.configHash ? target.service : null;
|
|
2896
|
+
}
|
|
2897
|
+
return null;
|
|
2898
|
+
}
|
|
2994
2899
|
function trackedPlatformWorkerTraffic(_manifest, _workers) {
|
|
2995
2900
|
return {
|
|
2996
2901
|
customDomains: [],
|
|
@@ -3662,7 +3567,7 @@ async function runPlatformWrangler(operation, accountId, cwd, capture) {
|
|
|
3662
3567
|
return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
|
|
3663
3568
|
}
|
|
3664
3569
|
const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
|
|
3665
|
-
async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false) {
|
|
3570
|
+
async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false, apiToken) {
|
|
3666
3571
|
const startedAt = performance.now();
|
|
3667
3572
|
const result = await createDefaultRunWrangler(process.cwd(), {
|
|
3668
3573
|
launcher: resolveCloudflareOperationArgv(process.cwd()),
|
|
@@ -3671,6 +3576,7 @@ async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture,
|
|
|
3671
3576
|
})(operation, {
|
|
3672
3577
|
accountId,
|
|
3673
3578
|
apiBaseUrl,
|
|
3579
|
+
apiToken,
|
|
3674
3580
|
cwd
|
|
3675
3581
|
});
|
|
3676
3582
|
if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] Cloudflare ${operation.type} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
|
|
@@ -3802,8 +3708,8 @@ async function withConditionalPlatformWorkerCreate(accountId, workerName, operat
|
|
|
3802
3708
|
if (proxy.state.conflictStatus !== null) throw new Error(`platform: Worker ${workerName} was created concurrently (HTTP ${proxy.state.conflictStatus}); no existing Worker was changed. Re-run to inspect the collision.`);
|
|
3803
3709
|
return result;
|
|
3804
3710
|
}
|
|
3805
|
-
async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl) {
|
|
3806
|
-
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl), upstreamApiBaseUrl);
|
|
3711
|
+
async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl, apiToken) {
|
|
3712
|
+
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl, false, apiToken), upstreamApiBaseUrl);
|
|
3807
3713
|
}
|
|
3808
3714
|
//#endregion
|
|
3809
3715
|
//#region src/cli/platform-installer-inspection.ts
|
|
@@ -4563,7 +4469,7 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4563
4469
|
let uploaded;
|
|
4564
4470
|
let uploadError;
|
|
4565
4471
|
try {
|
|
4566
|
-
uploaded = options.workerExisted ? await options.runner(operation, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(operation, accountId, directory, true, workerName, options.apiBaseUrl);
|
|
4472
|
+
uploaded = options.workerExisted ? await options.runner(operation, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(operation, accountId, directory, true, workerName, options.apiBaseUrl, options.operatorApiToken);
|
|
4567
4473
|
assertCleanPlatformWrangler(uploaded, `${options.workerExisted ? "upload" : "bootstrap"} ${workerName}`);
|
|
4568
4474
|
} catch (error) {
|
|
4569
4475
|
uploadError = error;
|
|
@@ -5017,11 +4923,7 @@ async function ensurePlatformAccessHealthChecks(input) {
|
|
|
5017
4923
|
recorded.id = created.id;
|
|
5018
4924
|
save();
|
|
5019
4925
|
} catch (error) {
|
|
5020
|
-
if (error
|
|
5021
|
-
408,
|
|
5022
|
-
409,
|
|
5023
|
-
429
|
|
5024
|
-
].includes(error.status)) {
|
|
4926
|
+
if (platformAccessCreateRejected(error)) {
|
|
5025
4927
|
recorded.pending = false;
|
|
5026
4928
|
save();
|
|
5027
4929
|
}
|
|
@@ -5071,7 +4973,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
5071
4973
|
].map((value) => new URL(value).origin).some((origin) => !origins.includes(origin)) || protection.applications.some((entry) => entry.audience !== app.aud || !platformAccessPublicDestinations(app).includes(new URL(entry.origin).hostname))) throw new Error("platform: Access audiences and complete origin coverage must match the active platform protection. Restore the current Access checkpoint or repair its applications.");
|
|
5072
4974
|
for (const other of await access.list("apps")) {
|
|
5073
4975
|
if (other.id === app.id) continue;
|
|
5074
|
-
if (
|
|
4976
|
+
if (platformAccessOverlapsOrigins(other, origins)) throw new Error("platform: Another Access application overlaps a protected platform origin. Reconcile it before maintenance.");
|
|
5075
4977
|
}
|
|
5076
4978
|
const liveToken = (await access.list("service_tokens")).find((entry) => entry.id === service.id);
|
|
5077
4979
|
const expiry = liveToken?.expires_at;
|
|
@@ -5082,7 +4984,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
5082
4984
|
}
|
|
5083
4985
|
const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
|
|
5084
4986
|
if (existingWorkers.length) {
|
|
5085
|
-
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-
|
|
4987
|
+
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-zxDr0hWW.mjs");
|
|
5086
4988
|
await verifyProtectionWorkerOrigins({
|
|
5087
4989
|
request: input.client.request.bind(input.client),
|
|
5088
4990
|
listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
|
|
@@ -5343,6 +5245,605 @@ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
|
|
|
5343
5245
|
}
|
|
5344
5246
|
}
|
|
5345
5247
|
//#endregion
|
|
5248
|
+
//#region src/cli/platform-teardown.ts
|
|
5249
|
+
/**
|
|
5250
|
+
* `void platform uninstall --purge-resources` deletes every Cloudflare resource the
|
|
5251
|
+
* installation created, in an order Cloudflare accepts:
|
|
5252
|
+
*
|
|
5253
|
+
* traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
|
|
5254
|
+
* -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
|
|
5255
|
+
* -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
|
|
5256
|
+
*
|
|
5257
|
+
* Each resource is proven to belong to this installation right before its
|
|
5258
|
+
* DELETE, and Cloudflare refuses a resource that something outside the
|
|
5259
|
+
* installation still uses. The only forced DELETE is for a platform Worker
|
|
5260
|
+
* that Cloudflare refused while every Worker it names as a dependent is one
|
|
5261
|
+
* this installation created: the API and proxy Workers bind to each other, so
|
|
5262
|
+
* neither can go first without it. A route, custom domain, DNS
|
|
5263
|
+
* record, or Access resource that no longer matches what was recorded is kept
|
|
5264
|
+
* and listed with the reason instead of stopping the run. Every finished
|
|
5265
|
+
* deletion is checkpointed, so rerunning the command continues where it
|
|
5266
|
+
* stopped. The routing KV namespace holds that checkpoint, so uninstall
|
|
5267
|
+
* deletes it last.
|
|
5268
|
+
*
|
|
5269
|
+
* An uninstall without --purge-resources records what it leaves as retained. If it
|
|
5270
|
+
* stopped before it finished, --purge-resources considers those resources again.
|
|
5271
|
+
*/
|
|
5272
|
+
const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
|
|
5273
|
+
const WORKER_ORDER = [
|
|
5274
|
+
"email-gateway-worker",
|
|
5275
|
+
"proxy-worker",
|
|
5276
|
+
"dispatch-worker",
|
|
5277
|
+
"api-worker"
|
|
5278
|
+
];
|
|
5279
|
+
const PERMISSIONS = {
|
|
5280
|
+
access: "Account → Access: Apps and Policies → Edit",
|
|
5281
|
+
aiGateway: "Account → AI Gateway → Edit",
|
|
5282
|
+
dns: "Zone → DNS → Edit",
|
|
5283
|
+
emailRouting: "Zone → Email Routing Rules → Edit",
|
|
5284
|
+
hyperdrive: "Account → Hyperdrive → Edit",
|
|
5285
|
+
kv: "Account → Workers KV Storage → Edit",
|
|
5286
|
+
queues: "Account → Queues → Edit",
|
|
5287
|
+
r2: "Account → Workers R2 Storage → Edit",
|
|
5288
|
+
routes: "Zone → Workers Routes → Edit",
|
|
5289
|
+
serviceTokens: "Account → Access: Service Tokens → Edit",
|
|
5290
|
+
workers: "Account → Workers Scripts → Edit"
|
|
5291
|
+
};
|
|
5292
|
+
function isPlatformTailWorker(manifest, resource) {
|
|
5293
|
+
return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
|
|
5294
|
+
}
|
|
5295
|
+
function purgeResourcesResumeCommand(manifest) {
|
|
5296
|
+
return `void platform uninstall ${manifest.id} --purge-resources`;
|
|
5297
|
+
}
|
|
5298
|
+
function accessStepKey(access) {
|
|
5299
|
+
return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
|
|
5300
|
+
}
|
|
5301
|
+
/** Access resources that --purge-resources already deleted. */
|
|
5302
|
+
function isPurgedAccessResource(manifest, access) {
|
|
5303
|
+
return manifest.steps.some((step) => step.key === accessStepKey(access));
|
|
5304
|
+
}
|
|
5305
|
+
function hasStep(manifest, key) {
|
|
5306
|
+
return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
|
|
5307
|
+
}
|
|
5308
|
+
function keptStepKey(key) {
|
|
5309
|
+
return `kept:${key}`;
|
|
5310
|
+
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Why --purge-resources kept a resource it was going to delete, for example because
|
|
5313
|
+
* a route now serves another Worker; null when it did not keep it. Access
|
|
5314
|
+
* resources use the key `access:<key>`.
|
|
5315
|
+
*/
|
|
5316
|
+
function purgeResourcesKeptReason(manifest, key) {
|
|
5317
|
+
return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
|
|
5318
|
+
}
|
|
5319
|
+
/** An uninstall that started has not finished, so what it kept can be reconsidered. */
|
|
5320
|
+
function isReopenable(manifest) {
|
|
5321
|
+
return manifest.status !== "uninstalled";
|
|
5322
|
+
}
|
|
5323
|
+
/**
|
|
5324
|
+
* A resource the installation created that --purge-resources may still delete: one
|
|
5325
|
+
* that is ready, or one that an uninstall which stopped before it finished
|
|
5326
|
+
* recorded as retained. Resources --purge-resources itself kept, and earlier
|
|
5327
|
+
* versions of a resource kept when it was replaced, are never reconsidered.
|
|
5328
|
+
*/
|
|
5329
|
+
function isPurgeResourcesCandidate(manifest, resource) {
|
|
5330
|
+
return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
|
|
5331
|
+
}
|
|
5332
|
+
/** The zone that routes and DNS records live in, if it can still be used. */
|
|
5333
|
+
function usableZone(manifest) {
|
|
5334
|
+
return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
|
|
5335
|
+
}
|
|
5336
|
+
/**
|
|
5337
|
+
* A recorded resource whose live configuration no longer matches the
|
|
5338
|
+
* installation's record. --purge-resources keeps such a DNS record and continues.
|
|
5339
|
+
*/
|
|
5340
|
+
var PlatformResourceChangedError = class extends Error {
|
|
5341
|
+
resourceKey;
|
|
5342
|
+
reason;
|
|
5343
|
+
constructor(resourceKey, reason, message) {
|
|
5344
|
+
super(message);
|
|
5345
|
+
this.resourceKey = resourceKey;
|
|
5346
|
+
this.reason = reason;
|
|
5347
|
+
this.name = "PlatformResourceChangedError";
|
|
5348
|
+
}
|
|
5349
|
+
};
|
|
5350
|
+
/** The zone whose Email Routing rules may send mail to the gateway Worker. */
|
|
5351
|
+
function emailRoutingZone(manifest) {
|
|
5352
|
+
return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
|
|
5353
|
+
}
|
|
5354
|
+
/** Everything --purge-resources will delete, in deletion order. */
|
|
5355
|
+
function purgeResourcesTargets(manifest) {
|
|
5356
|
+
const resources = manifest.resources;
|
|
5357
|
+
const routing = resources.find((resource) => resource.key === "routing-kv");
|
|
5358
|
+
const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
|
|
5359
|
+
if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
|
|
5360
|
+
const zone = usableZone(manifest);
|
|
5361
|
+
const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
|
|
5362
|
+
const targets = [];
|
|
5363
|
+
for (const resource of resources) {
|
|
5364
|
+
if (resource.kind !== "worker-route" && resource.kind !== "worker-custom-domain" || resource.state !== "ready" && resource.state !== "retained" || resource.kind === "worker-route" && !zone || purgeResourcesKeptReason(manifest, resource.key) !== null) continue;
|
|
5365
|
+
const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
|
|
5366
|
+
const worker = workers.find((entry) => entry.name === name);
|
|
5367
|
+
if (worker) targets.push({
|
|
5368
|
+
phase: "traffic",
|
|
5369
|
+
resource,
|
|
5370
|
+
worker
|
|
5371
|
+
});
|
|
5372
|
+
}
|
|
5373
|
+
for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
|
|
5374
|
+
phase: "dns",
|
|
5375
|
+
resource
|
|
5376
|
+
});
|
|
5377
|
+
const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
|
|
5378
|
+
for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
|
|
5379
|
+
phase: "access",
|
|
5380
|
+
access: entry
|
|
5381
|
+
});
|
|
5382
|
+
const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
|
|
5383
|
+
if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
|
|
5384
|
+
phase: "email",
|
|
5385
|
+
resource: gateway
|
|
5386
|
+
});
|
|
5387
|
+
const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
|
|
5388
|
+
for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
|
|
5389
|
+
phase: "queue-consumers",
|
|
5390
|
+
resource
|
|
5391
|
+
});
|
|
5392
|
+
const rank = (resource) => {
|
|
5393
|
+
const index = WORKER_ORDER.indexOf(resource.key);
|
|
5394
|
+
return index < 0 ? WORKER_ORDER.length : index;
|
|
5395
|
+
};
|
|
5396
|
+
for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
|
|
5397
|
+
phase: "workers",
|
|
5398
|
+
resource
|
|
5399
|
+
});
|
|
5400
|
+
for (const resource of queues) targets.push({
|
|
5401
|
+
phase: "queues",
|
|
5402
|
+
resource
|
|
5403
|
+
});
|
|
5404
|
+
for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
|
|
5405
|
+
phase: "dispatch",
|
|
5406
|
+
resource
|
|
5407
|
+
});
|
|
5408
|
+
for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
|
|
5409
|
+
phase: "tail",
|
|
5410
|
+
resource
|
|
5411
|
+
});
|
|
5412
|
+
for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
|
|
5413
|
+
phase: "data",
|
|
5414
|
+
resource
|
|
5415
|
+
});
|
|
5416
|
+
for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
|
|
5417
|
+
phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
|
|
5418
|
+
resource
|
|
5419
|
+
});
|
|
5420
|
+
if (candidate(routing)) targets.push({
|
|
5421
|
+
phase: "routing",
|
|
5422
|
+
resource: routing
|
|
5423
|
+
});
|
|
5424
|
+
return targets;
|
|
5425
|
+
}
|
|
5426
|
+
/** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
|
|
5427
|
+
function purgeResourcesHandlesEmailRouting(manifest) {
|
|
5428
|
+
return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
|
|
5429
|
+
}
|
|
5430
|
+
/** Manifest resources that --purge-resources leaves in the account. */
|
|
5431
|
+
function purgeResourcesKeptResources(manifest) {
|
|
5432
|
+
const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
|
|
5433
|
+
return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
|
|
5434
|
+
}
|
|
5435
|
+
function resourceLabel(resource) {
|
|
5436
|
+
return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
|
|
5437
|
+
}
|
|
5438
|
+
/** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
|
|
5439
|
+
function describePurgeResourcesPlan(manifest) {
|
|
5440
|
+
const targets = purgeResourcesTargets(manifest);
|
|
5441
|
+
const lines = [];
|
|
5442
|
+
let step = 0;
|
|
5443
|
+
let previous = null;
|
|
5444
|
+
const consumerQueues = [];
|
|
5445
|
+
for (const target of targets) {
|
|
5446
|
+
if (target.phase === "email") {
|
|
5447
|
+
step++;
|
|
5448
|
+
previous = target.phase;
|
|
5449
|
+
lines.push(` ${step}. Email Routing rules in zone ${emailRoutingZone(manifest)} that send mail to ${target.resource.name}, and its catch-all rule if it does (reset to drop mail, disabled)`);
|
|
5450
|
+
continue;
|
|
5451
|
+
}
|
|
5452
|
+
if (target.phase !== previous) {
|
|
5453
|
+
step++;
|
|
5454
|
+
previous = target.phase;
|
|
5455
|
+
}
|
|
5456
|
+
const prefix = ` ${step}. `;
|
|
5457
|
+
if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
|
|
5458
|
+
else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
|
|
5459
|
+
else if (target.phase === "queue-consumers") {
|
|
5460
|
+
consumerQueues.push(target.resource.name);
|
|
5461
|
+
if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
|
|
5462
|
+
lines.push(` ${target.resource.name}`);
|
|
5463
|
+
} else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
|
|
5464
|
+
else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
|
|
5465
|
+
else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
|
|
5466
|
+
else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
|
|
5467
|
+
else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
|
|
5468
|
+
else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
|
|
5469
|
+
else lines.push(`${prefix}${resourceLabel(target.resource)}`);
|
|
5470
|
+
}
|
|
5471
|
+
return lines.length ? ["Delete every resource this installation created (--purge-resources), in this order:", ...lines] : ["--purge-resources finds no resource it can prove this installation created."];
|
|
5472
|
+
}
|
|
5473
|
+
/** Lines naming what --purge-resources never removes. */
|
|
5474
|
+
function purgeResourcesAlwaysKept() {
|
|
5475
|
+
return [
|
|
5476
|
+
" The external PostgreSQL database and its data.",
|
|
5477
|
+
" Analytics Engine datasets; Cloudflare has no API to delete them.",
|
|
5478
|
+
" Access policies and identity providers you chose.",
|
|
5479
|
+
" API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
|
|
5480
|
+
];
|
|
5481
|
+
}
|
|
5482
|
+
/** Token permissions --purge-resources needs for this manifest. */
|
|
5483
|
+
function purgeResourcesPermissions(manifest) {
|
|
5484
|
+
const targets = purgeResourcesTargets(manifest);
|
|
5485
|
+
const needed = /* @__PURE__ */ new Set();
|
|
5486
|
+
for (const target of targets) {
|
|
5487
|
+
if (target.phase === "access") {
|
|
5488
|
+
needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
|
|
5489
|
+
continue;
|
|
5490
|
+
}
|
|
5491
|
+
if (target.phase === "email") {
|
|
5492
|
+
needed.add(PERMISSIONS.emailRouting);
|
|
5493
|
+
continue;
|
|
5494
|
+
}
|
|
5495
|
+
const { kind } = target.resource;
|
|
5496
|
+
if (kind === "worker-route") needed.add(PERMISSIONS.routes);
|
|
5497
|
+
else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
|
|
5498
|
+
else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
|
|
5499
|
+
else if (kind === "queue") needed.add(PERMISSIONS.queues);
|
|
5500
|
+
else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
|
|
5501
|
+
else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
|
|
5502
|
+
else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
|
|
5503
|
+
else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
|
|
5504
|
+
}
|
|
5505
|
+
return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
|
|
5506
|
+
}
|
|
5507
|
+
function deniedStatus(error) {
|
|
5508
|
+
return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
|
|
5509
|
+
}
|
|
5510
|
+
/**
|
|
5511
|
+
* Check resource reads before the first change. Browser login cannot change
|
|
5512
|
+
* DNS or Access, so those need an API token. Read probes cannot prove Edit;
|
|
5513
|
+
* the plan lists the required permissions.
|
|
5514
|
+
*/
|
|
5515
|
+
async function checkPurgeResourcesCredentials(client, manifest, credential) {
|
|
5516
|
+
const targets = purgeResourcesTargets(manifest);
|
|
5517
|
+
const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
|
|
5518
|
+
const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
|
|
5519
|
+
const has = (kind) => resources.some((resource) => resource.kind === kind);
|
|
5520
|
+
const zone = manifest.resources.find((resource) => resource.key === "zone");
|
|
5521
|
+
const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
|
|
5522
|
+
const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
|
|
5523
|
+
const scope = `${accounts.length === 1 ? "account" : "accounts"} ${accounts.join(" and ")}${has("dns-record") || has("worker-route") ? ` and zone ${zone?.name ?? ""}` : ""}${emailZone ? ` and Email Routing zone ${emailZone}` : ""}`;
|
|
5524
|
+
if (credential.source === "oauth") {
|
|
5525
|
+
const browserMissing = [
|
|
5526
|
+
...has("dns-record") ? [PERMISSIONS.dns] : [],
|
|
5527
|
+
...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
|
|
5528
|
+
...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
|
|
5529
|
+
...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
|
|
5530
|
+
];
|
|
5531
|
+
if (browserMissing.length) throw new Error(`platform: Cloudflare browser login cannot delete every resource --purge-resources plans. Set CLOUDFLARE_API_TOKEN to a token with ${purgeResourcesPermissions(manifest).join(", ")}, scoped to ${scope}, then rerun. Browser login is missing: ${browserMissing.join(", ")}. No Cloudflare resource was changed.`);
|
|
5532
|
+
}
|
|
5533
|
+
const probes = [];
|
|
5534
|
+
if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
|
|
5535
|
+
if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
|
|
5536
|
+
if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
|
|
5537
|
+
for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
|
|
5538
|
+
else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
|
|
5539
|
+
for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
|
|
5540
|
+
if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
|
|
5541
|
+
if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
|
|
5542
|
+
if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
|
|
5543
|
+
if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
|
|
5544
|
+
if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
|
|
5545
|
+
if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
|
|
5546
|
+
const missing = /* @__PURE__ */ new Set();
|
|
5547
|
+
for (const [permission, probe] of probes) try {
|
|
5548
|
+
await probe();
|
|
5549
|
+
} catch (error) {
|
|
5550
|
+
if (!deniedStatus(error)) throw error;
|
|
5551
|
+
missing.add(permission);
|
|
5552
|
+
}
|
|
5553
|
+
if (missing.size) throw new Error(`platform: The Cloudflare credential cannot read every resource --purge-resources would delete. Add ${[...missing].join(", ")} for ${scope}, then rerun. No Cloudflare resource was changed.`);
|
|
5554
|
+
}
|
|
5555
|
+
const LISTED_PROJECTS = 20;
|
|
5556
|
+
/** Slugs of every project row, including projects whose deletion stopped partway. */
|
|
5557
|
+
async function readPlatformProjectSlugsConnection(client) {
|
|
5558
|
+
if ((await client.query(`SELECT to_regclass('public.projects') IS NOT NULL AS "present"`)).rows[0]?.present !== true) return [];
|
|
5559
|
+
return (await client.query("SELECT \"slug\" FROM \"projects\" ORDER BY \"slug\"")).rows.map((row) => row.slug);
|
|
5560
|
+
}
|
|
5561
|
+
/**
|
|
5562
|
+
* Projects own Workers, storage, and domains that only the running platform
|
|
5563
|
+
* can delete. --purge-resources deletes the platform, so it refuses before any
|
|
5564
|
+
* change while a project remains.
|
|
5565
|
+
*/
|
|
5566
|
+
async function assertNoPlatformProjects(read) {
|
|
5567
|
+
let slugs;
|
|
5568
|
+
try {
|
|
5569
|
+
if (!read) throw new Error("no platform database connection is available");
|
|
5570
|
+
slugs = await read();
|
|
5571
|
+
} catch (error) {
|
|
5572
|
+
const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
|
|
5573
|
+
throw new Error(`platform: Refusing --purge-resources because the project list could not be read from the platform database: ${reason}\nMake the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry.`);
|
|
5574
|
+
}
|
|
5575
|
+
if (slugs.length) throw new Error([
|
|
5576
|
+
`platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
|
|
5577
|
+
"--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
|
|
5578
|
+
"Delete each project first with `void project delete <name>`, then retry:",
|
|
5579
|
+
...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
|
|
5580
|
+
...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
|
|
5581
|
+
].join("\n"));
|
|
5582
|
+
}
|
|
5583
|
+
const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
|
|
5584
|
+
/**
|
|
5585
|
+
* AI Gateway ids are chosen by the caller and can be reused after a delete,
|
|
5586
|
+
* and gateways carry no ownership marker. Only a creation time next to the
|
|
5587
|
+
* installer's provision intent shows that this is the gateway it created.
|
|
5588
|
+
*/
|
|
5589
|
+
function createdDuringInstallation(manifest, resource, createdAt) {
|
|
5590
|
+
const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
|
|
5591
|
+
if (Number.isNaN(created)) return false;
|
|
5592
|
+
return manifest.steps.some((step) => (step.key === `provision-intent:${resource.key}` || step.key.startsWith(`provision-intent:${resource.key}:`)) && Math.abs(created - Date.parse(step.completedAt)) <= AI_GATEWAY_CREATION_WINDOW_MS);
|
|
5593
|
+
}
|
|
5594
|
+
function ownershipRefusal(manifest, description, reason) {
|
|
5595
|
+
return /* @__PURE__ */ new Error(`platform: Refusing to delete ${description} because ${reason}. It may no longer belong to this installation. Delete or fix it in the Cloudflare dashboard if it is yours, then rerun: ${purgeResourcesResumeCommand(manifest)}`);
|
|
5596
|
+
}
|
|
5597
|
+
function cloudflareRefusal(manifest, description, error, hint) {
|
|
5598
|
+
if (!(error instanceof PlatformCloudflareApiError)) return error;
|
|
5599
|
+
return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
|
|
5600
|
+
}
|
|
5601
|
+
/**
|
|
5602
|
+
* Delete every target in order. Returns the manifest with each deleted
|
|
5603
|
+
* resource recorded; routing KV is left for the caller to delete last.
|
|
5604
|
+
*/
|
|
5605
|
+
async function runPurgeResourcesTeardown(context, initial) {
|
|
5606
|
+
const { client } = context;
|
|
5607
|
+
let next = initial;
|
|
5608
|
+
const accountId = next.account.id;
|
|
5609
|
+
const save = async (manifest) => {
|
|
5610
|
+
next = manifest;
|
|
5611
|
+
await context.checkpoint(next);
|
|
5612
|
+
};
|
|
5613
|
+
const markDeleted = async (key) => {
|
|
5614
|
+
const resource = next.resources.find((entry) => entry.key === key);
|
|
5615
|
+
await save(recordInstallationResource(next, {
|
|
5616
|
+
...resource,
|
|
5617
|
+
state: "deleted"
|
|
5618
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
5619
|
+
};
|
|
5620
|
+
const recordStep = async (key, resultHash = "completed") => {
|
|
5621
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5622
|
+
await save(recordInstallationStep(next, {
|
|
5623
|
+
completedAt: now,
|
|
5624
|
+
key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
|
|
5625
|
+
resultHash
|
|
5626
|
+
}, now));
|
|
5627
|
+
};
|
|
5628
|
+
/**
|
|
5629
|
+
* Live proof for these keys. A resource an earlier uninstall recorded as
|
|
5630
|
+
* retained is proven like a ready one; only its recorded state differs.
|
|
5631
|
+
*/
|
|
5632
|
+
const verify = (keys) => context.verify({
|
|
5633
|
+
...next,
|
|
5634
|
+
resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
|
|
5635
|
+
...resource,
|
|
5636
|
+
state: "ready"
|
|
5637
|
+
} : resource)
|
|
5638
|
+
}, keys);
|
|
5639
|
+
/**
|
|
5640
|
+
* Prove the resource; false when it is already gone (and recorded).
|
|
5641
|
+
*
|
|
5642
|
+
* Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
|
|
5643
|
+
* addressed by name, and a route or custom domain can be changed in place.
|
|
5644
|
+
* Something replaced between this proof and the DELETE can still be removed.
|
|
5645
|
+
* That one-request window is accepted because --purge-resources is opt-in and its
|
|
5646
|
+
* plan lists every resource first. A forced Worker DELETE has the same
|
|
5647
|
+
* window between reading its dependents and the DELETE.
|
|
5648
|
+
*/
|
|
5649
|
+
const prove = async (resource) => {
|
|
5650
|
+
if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
|
|
5651
|
+
await markDeleted(resource.key);
|
|
5652
|
+
return false;
|
|
5653
|
+
}
|
|
5654
|
+
return true;
|
|
5655
|
+
};
|
|
5656
|
+
/** Keep a resource that no longer matches the record, and save why. */
|
|
5657
|
+
const keep = async (key, reason) => {
|
|
5658
|
+
const resource = next.resources.find((entry) => entry.key === key);
|
|
5659
|
+
if (resource?.state === "ready") await save(recordInstallationResource(next, {
|
|
5660
|
+
...resource,
|
|
5661
|
+
state: "retained"
|
|
5662
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
5663
|
+
await recordStep(keptStepKey(key), reason);
|
|
5664
|
+
};
|
|
5665
|
+
const remove = async (resource, hint, zoneId) => {
|
|
5666
|
+
if (!await prove(resource)) return;
|
|
5667
|
+
try {
|
|
5668
|
+
await client.deleteResource(accountId, resource, zoneId);
|
|
5669
|
+
} catch (error) {
|
|
5670
|
+
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
|
|
5671
|
+
}
|
|
5672
|
+
await markDeleted(resource.key);
|
|
5673
|
+
};
|
|
5674
|
+
const zone = usableZone(next);
|
|
5675
|
+
const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
|
|
5676
|
+
/** The recorded Worker under this name that this run still deletes, if any. */
|
|
5677
|
+
const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
|
|
5678
|
+
const workerHint = "A Worker this installation did not create may still use it, for example by sending it logs as a tail Worker or through a service binding. Remove that link in the Cloudflare dashboard.";
|
|
5679
|
+
/**
|
|
5680
|
+
* Delete a platform Worker. When Cloudflare refuses because other Workers
|
|
5681
|
+
* still bind to it, force the DELETE only if each of them is a Worker this
|
|
5682
|
+
* run deletes too and still carries this installation's ownership marker.
|
|
5683
|
+
*/
|
|
5684
|
+
const removeWorker = async (resource) => {
|
|
5685
|
+
if (!await prove(resource)) return;
|
|
5686
|
+
try {
|
|
5687
|
+
await client.deleteResource(accountId, resource);
|
|
5688
|
+
} catch (error) {
|
|
5689
|
+
if (!(error instanceof PlatformCloudflareApiError)) throw error;
|
|
5690
|
+
const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
|
|
5691
|
+
const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
|
|
5692
|
+
const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
|
|
5693
|
+
const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
|
|
5694
|
+
if (!dependents || dependents.pagesFunction || dependents.workers.length === 0 || outside.length > 0 || !missing || keys.some((key) => missing.has(key))) throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, outside.length ? `Workers outside this installation still use it: ${outside.join(", ")}. Remove that link in the Cloudflare dashboard.` : workerHint);
|
|
5695
|
+
try {
|
|
5696
|
+
await client.forceDeleteWorker(accountId, resource.name);
|
|
5697
|
+
} catch (forced) {
|
|
5698
|
+
throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
await markDeleted(resource.key);
|
|
5702
|
+
};
|
|
5703
|
+
for (const target of purgeResourcesTargets(initial)) {
|
|
5704
|
+
if (target.phase === "access") {
|
|
5705
|
+
const { access } = target;
|
|
5706
|
+
const live = await client.getAccessResource(access.accountId, access.kind, access.id);
|
|
5707
|
+
if (live) {
|
|
5708
|
+
if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
|
|
5709
|
+
await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
|
|
5710
|
+
continue;
|
|
5711
|
+
}
|
|
5712
|
+
try {
|
|
5713
|
+
await client.deleteAccessResource(access.accountId, access.kind, access.id);
|
|
5714
|
+
} catch (error) {
|
|
5715
|
+
throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
await recordStep(`access:${access.key}`);
|
|
5719
|
+
continue;
|
|
5720
|
+
}
|
|
5721
|
+
const current = next.resources.find((resource) => resource.key === target.resource.key);
|
|
5722
|
+
if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
|
|
5723
|
+
switch (target.phase) {
|
|
5724
|
+
case "traffic": {
|
|
5725
|
+
const worker = next.resources.find((resource) => resource.key === target.worker.key);
|
|
5726
|
+
if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
|
|
5727
|
+
if (current.kind === "worker-route") {
|
|
5728
|
+
const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
|
|
5729
|
+
if (live && (live.pattern !== current.name || live.script !== worker.name)) {
|
|
5730
|
+
await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
|
|
5731
|
+
break;
|
|
5732
|
+
}
|
|
5733
|
+
if (live) await client.deleteWorkerRoute(zone.id, current.id);
|
|
5734
|
+
} else {
|
|
5735
|
+
const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
|
|
5736
|
+
if (live && (live.hostname !== current.name || live.service !== worker.name)) {
|
|
5737
|
+
await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
|
|
5738
|
+
break;
|
|
5739
|
+
}
|
|
5740
|
+
if (live) await client.deleteWorkerDomain(accountId, current.id);
|
|
5741
|
+
}
|
|
5742
|
+
await markDeleted(current.key);
|
|
5743
|
+
break;
|
|
5744
|
+
}
|
|
5745
|
+
case "dns":
|
|
5746
|
+
try {
|
|
5747
|
+
await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
|
|
5748
|
+
} catch (error) {
|
|
5749
|
+
if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
|
|
5750
|
+
await keep(current.key, error.reason);
|
|
5751
|
+
}
|
|
5752
|
+
break;
|
|
5753
|
+
case "email": {
|
|
5754
|
+
if (!await prove(current)) break;
|
|
5755
|
+
const zoneId = emailRoutingZone(next);
|
|
5756
|
+
const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
|
|
5757
|
+
try {
|
|
5758
|
+
for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
|
|
5759
|
+
if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
|
|
5760
|
+
} catch (error) {
|
|
5761
|
+
throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
|
|
5762
|
+
}
|
|
5763
|
+
await recordStep("email-routing");
|
|
5764
|
+
break;
|
|
5765
|
+
}
|
|
5766
|
+
case "queue-consumers":
|
|
5767
|
+
if (!await prove(current)) break;
|
|
5768
|
+
for (const consumer of await client.listQueueConsumers(accountId, current.id)) if (consumer.script && createdWorkerNames.has(consumer.script)) await client.deleteQueueConsumer(accountId, current.id, consumer.id);
|
|
5769
|
+
await recordStep(`consumers:${current.key}`);
|
|
5770
|
+
break;
|
|
5771
|
+
case "workers":
|
|
5772
|
+
await removeWorker(current);
|
|
5773
|
+
break;
|
|
5774
|
+
case "tail":
|
|
5775
|
+
await remove(current, workerHint);
|
|
5776
|
+
break;
|
|
5777
|
+
case "queues":
|
|
5778
|
+
await remove(current, "A Worker this installation did not create may still use the Queue as a producer or consumer. Remove it from that Worker in the Cloudflare dashboard.");
|
|
5779
|
+
break;
|
|
5780
|
+
case "dispatch": {
|
|
5781
|
+
if (!await prove(current)) break;
|
|
5782
|
+
const projects = await context.readProjects();
|
|
5783
|
+
if (projects.length) throw new Error(`platform: Stopped before deleting the application Workers in dispatch namespace ${current.name} because ${projects.length === 1 ? "a project was" : "projects were"} created while this uninstall ran: ${projects.join(", ")}. Nothing in the namespace was deleted. --purge-resources refuses while the platform database lists a project. Remove each one from the "projects" table once you have dealt with its Cloudflare resources, then rerun: ${purgeResourcesResumeCommand(next)}`);
|
|
5784
|
+
for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
|
|
5785
|
+
await client.deleteDispatchNamespaceScript(accountId, current.name, script);
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
|
|
5788
|
+
}
|
|
5789
|
+
try {
|
|
5790
|
+
await client.deleteResource(accountId, current);
|
|
5791
|
+
} catch (error) {
|
|
5792
|
+
throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
|
|
5793
|
+
}
|
|
5794
|
+
await markDeleted(current.key);
|
|
5795
|
+
break;
|
|
5796
|
+
}
|
|
5797
|
+
case "ai-gateway": {
|
|
5798
|
+
const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
|
|
5799
|
+
if (live && !createdDuringInstallation(next, current, live.createdAt)) {
|
|
5800
|
+
await keep(current.key, "Cloudflare does not show it was created during installation");
|
|
5801
|
+
break;
|
|
5802
|
+
}
|
|
5803
|
+
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
5804
|
+
break;
|
|
5805
|
+
}
|
|
5806
|
+
case "data":
|
|
5807
|
+
await remove(current, "Check that nothing outside this installation still uses it.");
|
|
5808
|
+
break;
|
|
5809
|
+
case "r2": await purgeBucket(current);
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
return next;
|
|
5813
|
+
async function purgeBucket(bucket) {
|
|
5814
|
+
const markerStep = `r2-marker:${bucket.key}`;
|
|
5815
|
+
if (!hasStep(next, markerStep)) {
|
|
5816
|
+
if (!await prove(bucket)) return;
|
|
5817
|
+
await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
|
|
5818
|
+
if ((await verify(/* @__PURE__ */ new Set(["routing-kv", bucket.key]))).has(bucket.key)) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "it disappeared while it was being emptied");
|
|
5819
|
+
});
|
|
5820
|
+
if (!await prove(bucket)) return;
|
|
5821
|
+
const generation = await context.bucketGeneration(next, bucket);
|
|
5822
|
+
if (generation === null) {
|
|
5823
|
+
await markDeleted(bucket.key);
|
|
5824
|
+
return;
|
|
5825
|
+
}
|
|
5826
|
+
await recordStep(markerStep, generation);
|
|
5827
|
+
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
5828
|
+
} else {
|
|
5829
|
+
const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
|
|
5830
|
+
const generation = await context.bucketGeneration(next, bucket);
|
|
5831
|
+
if (generation === null) {
|
|
5832
|
+
await markDeleted(bucket.key);
|
|
5833
|
+
return;
|
|
5834
|
+
}
|
|
5835
|
+
if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
|
|
5836
|
+
await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
|
|
5837
|
+
}
|
|
5838
|
+
try {
|
|
5839
|
+
await client.deleteResource(accountId, bucket);
|
|
5840
|
+
} catch (error) {
|
|
5841
|
+
throw cloudflareRefusal(next, `r2-bucket ${bucket.name}`, error, "Cloudflare deletes only empty buckets. If objects were added after it was emptied, delete them in the Cloudflare dashboard.");
|
|
5842
|
+
}
|
|
5843
|
+
await markDeleted(bucket.key);
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
//#endregion
|
|
5346
5847
|
//#region src/cli/platform-zero-trust-uninstall.ts
|
|
5347
5848
|
/**
|
|
5348
5849
|
* Uninstall turns off the platform API, and only that API can remove the
|
|
@@ -5444,8 +5945,9 @@ async function readPlatformProjectZeroTrustStateConnection(client) {
|
|
|
5444
5945
|
}
|
|
5445
5946
|
/**
|
|
5446
5947
|
* Take the Zero Trust configuration lock exclusively on this session. Every
|
|
5447
|
-
* API path that records or creates a Void-managed Access application
|
|
5448
|
-
* the same lock, so none can commit until the
|
|
5948
|
+
* API path that records or creates a Void-managed Access application or
|
|
5949
|
+
* creates a project holds the same lock, so none can commit until the
|
|
5950
|
+
* returned release runs.
|
|
5449
5951
|
*/
|
|
5450
5952
|
async function lockPlatformProjectZeroTrustConnection(client) {
|
|
5451
5953
|
await client.query(`SELECT pg_advisory_lock(${ZERO_TRUST_LOCK})`);
|
|
@@ -5689,6 +6191,21 @@ async function readPlatformProjectZeroTrustState(databaseUrl, installationId, ow
|
|
|
5689
6191
|
await pool.end();
|
|
5690
6192
|
}
|
|
5691
6193
|
}
|
|
6194
|
+
/** Read project slugs for a --purge-resources plan, which holds no lifecycle lock. */
|
|
6195
|
+
async function readPlatformProjectSlugs(databaseUrl, installationId, ownershipNonce) {
|
|
6196
|
+
if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
|
|
6197
|
+
const { Pool } = await import("pg");
|
|
6198
|
+
const pool = new Pool({
|
|
6199
|
+
connectionString: platformDatabaseClientUrl(databaseUrl),
|
|
6200
|
+
max: 1
|
|
6201
|
+
});
|
|
6202
|
+
try {
|
|
6203
|
+
await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
|
|
6204
|
+
return await readPlatformProjectSlugsConnection(pool);
|
|
6205
|
+
} finally {
|
|
6206
|
+
await pool.end();
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
5692
6209
|
async function inspectPlatformDatabaseStateConnection(client, database) {
|
|
5693
6210
|
const relation = await client.query(`SELECT to_regclass('"drizzle"."__drizzle_migrations"')::text AS "relation"`);
|
|
5694
6211
|
if (relation.rows.length === 0 || relation.rows[0]?.relation === null) return {
|
|
@@ -5817,6 +6334,7 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
|
|
|
5817
6334
|
} : null;
|
|
5818
6335
|
};
|
|
5819
6336
|
release.readProjectZeroTrust = () => readPlatformProjectZeroTrustStateConnection(client);
|
|
6337
|
+
release.readProjects = () => readPlatformProjectSlugsConnection(client);
|
|
5820
6338
|
release.lockProjectZeroTrust = () => lockPlatformProjectZeroTrustConnection(client);
|
|
5821
6339
|
release.fenceProjectZeroTrust = () => fencePlatformProjectZeroTrustConnection(client);
|
|
5822
6340
|
release.liftProjectZeroTrustFence = () => liftPlatformProjectZeroTrustFenceConnection(client);
|
|
@@ -6005,7 +6523,7 @@ async function claimPlatformDatabaseConnection(client, installationId, ownership
|
|
|
6005
6523
|
if (!claim) {
|
|
6006
6524
|
const contents = await client.query(`SELECT "schemaname", "tablename"
|
|
6007
6525
|
FROM "pg_catalog"."pg_tables"
|
|
6008
|
-
WHERE "schemaname" = ANY (current_schemas(false))
|
|
6526
|
+
WHERE ("schemaname" = ANY (current_schemas(false)) OR "schemaname" = 'drizzle')
|
|
6009
6527
|
AND "tablename" <> '__void_platform_installation'
|
|
6010
6528
|
ORDER BY "schemaname", "tablename"
|
|
6011
6529
|
LIMIT 1`);
|
|
@@ -6209,6 +6727,7 @@ async function uploadInstallationWorkers(input) {
|
|
|
6209
6727
|
const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
|
|
6210
6728
|
const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
|
|
6211
6729
|
apiBaseUrl: cloudflareApiBaseUrl,
|
|
6730
|
+
operatorApiToken: dependencies.operatorApiToken,
|
|
6212
6731
|
...parallelWorkerUploads ? {
|
|
6213
6732
|
acquireUploadSlot,
|
|
6214
6733
|
onReadyForActivation,
|
|
@@ -6388,8 +6907,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
6388
6907
|
if (plan.actions.some((action) => action.action === "inspect")) throw new Error("platform: AI Gateway access must be verified before provisioning. Provide AI Gateway: Edit on the runtime token, then retry.");
|
|
6389
6908
|
const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
6390
6909
|
const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl ?? resolvePinnedApiBaseUrl();
|
|
6391
|
-
const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl);
|
|
6392
|
-
const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true);
|
|
6910
|
+
const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, false, dependencies.operatorApiToken);
|
|
6911
|
+
const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true, dependencies.operatorApiToken);
|
|
6393
6912
|
const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
|
|
6394
6913
|
const runtimeDirectory = resolvedRuntime.directory;
|
|
6395
6914
|
const runtime = resolvedRuntime.manifest;
|
|
@@ -6986,10 +7505,6 @@ function platformManagementAccessForAuthentication(selection, accountId, checkpo
|
|
|
6986
7505
|
readOnly: spec?.mode === "existing"
|
|
6987
7506
|
};
|
|
6988
7507
|
}
|
|
6989
|
-
function answer(value) {
|
|
6990
|
-
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
6991
|
-
return value;
|
|
6992
|
-
}
|
|
6993
7508
|
async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
6994
7509
|
if (file) {
|
|
6995
7510
|
const input = JSON.parse(await readFile(file, "utf8"));
|
|
@@ -7014,7 +7529,7 @@ async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
|
7014
7529
|
return { authentication: recorded };
|
|
7015
7530
|
}
|
|
7016
7531
|
if (!process.stdin.isTTY && !choices.loginMethods && choices.accessProtection === void 0) return { authentication: { providers: ["github"] } };
|
|
7017
|
-
const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] :
|
|
7532
|
+
const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] : platformPromptAnswer(await multiselect({
|
|
7018
7533
|
message: "Platform login methods (select all that apply):",
|
|
7019
7534
|
initialValues: [],
|
|
7020
7535
|
required: true,
|
|
@@ -7037,7 +7552,7 @@ async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
|
7037
7552
|
}
|
|
7038
7553
|
]
|
|
7039
7554
|
})));
|
|
7040
|
-
const protection = choices.accessProtection ?? (!process.stdin.isTTY ? false :
|
|
7555
|
+
const protection = choices.accessProtection ?? (!process.stdin.isTTY ? false : platformPromptAnswer(await confirm({
|
|
7041
7556
|
message: "Also protect the platform UI and APIs with Cloudflare Access?",
|
|
7042
7557
|
initialValue: false
|
|
7043
7558
|
})));
|
|
@@ -7060,7 +7575,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
7060
7575
|
}
|
|
7061
7576
|
const prompt = async (message, initialValue = "", masked = false) => {
|
|
7062
7577
|
if (!process.stdin.isTTY) throw new Error(`platform: ${message} Supply it through --auth-config and environment references.`);
|
|
7063
|
-
return masked ?
|
|
7578
|
+
return masked ? platformPromptAnswer(await password({ message })) : platformPromptAnswer(await text$1({
|
|
7064
7579
|
message,
|
|
7065
7580
|
initialValue
|
|
7066
7581
|
}));
|
|
@@ -7127,7 +7642,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
7127
7642
|
save({ [partialKey]: JSON.stringify(connection) });
|
|
7128
7643
|
connections.push(connection);
|
|
7129
7644
|
}
|
|
7130
|
-
const administratorConnectionId = selection.file?.administratorConnectionId ?? (connections.length === 1 ? connections[0].configuration.id : process.stdin.isTTY ?
|
|
7645
|
+
const administratorConnectionId = selection.file?.administratorConnectionId ?? (connections.length === 1 ? connections[0].configuration.id : process.stdin.isTTY ? platformPromptAnswer(await select({
|
|
7131
7646
|
message: "Login method for the first administrator:",
|
|
7132
7647
|
options: connections.map((entry) => ({
|
|
7133
7648
|
value: entry.configuration.id,
|
|
@@ -7158,7 +7673,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
7158
7673
|
} : { mode: "restricted" });
|
|
7159
7674
|
if (!selection.file && !protection && process.stdin.isTTY) {
|
|
7160
7675
|
const eligible = connections.filter((entry) => entry.configuration.kind !== "github" && (entry.configuration.kind !== "google" || entry.configuration.allowedDomains.length > 0));
|
|
7161
|
-
const mode =
|
|
7676
|
+
const mode = platformPromptAnswer(await select({
|
|
7162
7677
|
message: "Who can create an account?",
|
|
7163
7678
|
initialValue: eligible.length ? "company" : "restricted",
|
|
7164
7679
|
options: [
|
|
@@ -7176,7 +7691,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
7176
7691
|
}
|
|
7177
7692
|
]
|
|
7178
7693
|
}));
|
|
7179
|
-
const ids = mode === "company" ?
|
|
7694
|
+
const ids = mode === "company" ? platformPromptAnswer(await multiselect({
|
|
7180
7695
|
message: "Company login methods that qualify new users:",
|
|
7181
7696
|
required: true,
|
|
7182
7697
|
initialValues: eligible.map((entry) => entry.configuration.id),
|
|
@@ -7361,24 +7876,25 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
|
7361
7876
|
}
|
|
7362
7877
|
async function runPlatformLifecycleCommand(commandArgs) {
|
|
7363
7878
|
if (commandArgs.subcommand === "domain") {
|
|
7364
|
-
const { runPlatformDomainCommand } = await import("./platform-domain-
|
|
7879
|
+
const { runPlatformDomainCommand } = await import("./platform-domain-p0kobLV8.mjs");
|
|
7365
7880
|
await runPlatformDomainCommand(commandArgs);
|
|
7366
7881
|
return;
|
|
7367
7882
|
}
|
|
7368
7883
|
if (commandArgs.subcommand === "discover") {
|
|
7369
7884
|
const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
|
|
7370
7885
|
const accountId = await selectAccount(await readAccounts(client), commandArgs.account);
|
|
7371
|
-
const { recoverPlatformInstallations } = await import("./platform-recovery-
|
|
7886
|
+
const { recoverPlatformInstallations } = await import("./platform-recovery-PvNS9Nl2.mjs");
|
|
7372
7887
|
const recovered = await recoverPlatformInstallations(client, accountId, void 0, commandArgs.selector);
|
|
7373
7888
|
if (recovered.length === 0) {
|
|
7374
7889
|
log.info(`No recoverable Void platform installations were found in ${accountId}.`);
|
|
7375
7890
|
return;
|
|
7376
7891
|
}
|
|
7377
|
-
for (const manifest of recovered) log.
|
|
7892
|
+
for (const manifest of recovered) log.step(`Recovered ${manifest.deployment.displayName} (${manifest.id}) at ${manifest.endpoints.apiUrl}.`);
|
|
7893
|
+
printSuccess(`Recovered ${recovered.length} platform installation${recovered.length === 1 ? "" : "s"}.`);
|
|
7378
7894
|
return;
|
|
7379
7895
|
}
|
|
7380
7896
|
if (commandArgs.subcommand !== "install") {
|
|
7381
|
-
const { runInstalledPlatformCommand } = await import("./platform-management-
|
|
7897
|
+
const { runInstalledPlatformCommand } = await import("./platform-management-oi-SvI3f.mjs");
|
|
7382
7898
|
await runInstalledPlatformCommand(commandArgs);
|
|
7383
7899
|
return;
|
|
7384
7900
|
}
|
|
@@ -7551,7 +8067,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
7551
8067
|
progress?.stop("Read the saved installation.");
|
|
7552
8068
|
progress = void 0;
|
|
7553
8069
|
if (authoritative) {
|
|
7554
|
-
const { selectAuthoritativePlatformManifest } = await import("./platform-management-
|
|
8070
|
+
const { selectAuthoritativePlatformManifest } = await import("./platform-management-oi-SvI3f.mjs");
|
|
7555
8071
|
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
7556
8072
|
authoritativeProvisionIntents = true;
|
|
7557
8073
|
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
@@ -7914,6 +8430,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
7914
8430
|
const result = await installPlatform(plan, secrets, existing, {
|
|
7915
8431
|
acquireDatabaseLifecycleLock: acquirePlatformDatabaseLifecycleLock,
|
|
7916
8432
|
client,
|
|
8433
|
+
operatorApiToken: managementApiToken || operatorCredential.token,
|
|
7917
8434
|
runtime,
|
|
7918
8435
|
healthCheckResumeCommand: `${cliCommandName()} platform install --resume --name ${existing.id}`,
|
|
7919
8436
|
onProgress: (message) => progress?.message(message),
|
|
@@ -7955,7 +8472,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
7955
8472
|
})).actions.filter((action) => action.kind === "worker"),
|
|
7956
8473
|
revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
|
|
7957
8474
|
verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
|
|
7958
|
-
const { verifyPlatformInstallationOwnership } = await import("./platform-management-
|
|
8475
|
+
const { verifyPlatformInstallationOwnership } = await import("./platform-management-oi-SvI3f.mjs");
|
|
7959
8476
|
const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
|
|
7960
8477
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
7961
8478
|
return action.action === "create" && previous && previous.name !== action.name;
|
|
@@ -7979,7 +8496,6 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
7979
8496
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
7980
8497
|
return;
|
|
7981
8498
|
}
|
|
7982
|
-
log.success(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
|
|
7983
8499
|
log.info(`API URL: ${plan.apiUrl}`);
|
|
7984
8500
|
if (result.emailOperationId) log.info(`Shared email routing ready. Operation: ${result.emailOperationId}.`);
|
|
7985
8501
|
const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
@@ -7989,7 +8505,8 @@ async function runPlatformLifecycleCommand(commandArgs) {
|
|
|
7989
8505
|
log.info("Confirm your identity in the browser, then test and enable any additional login methods in Settings.");
|
|
7990
8506
|
}
|
|
7991
8507
|
if (!plan.deployment.application.zoneName) log.info("Apps will use workers.dev addresses. When your domain is ready: void platform domain set <domain>");
|
|
7992
|
-
|
|
8508
|
+
log.info("Run `void account login`, then deploy an app with `void deploy --platform void`.");
|
|
8509
|
+
printSuccess(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
|
|
7993
8510
|
} catch (error) {
|
|
7994
8511
|
progress?.error("Platform installation failed.");
|
|
7995
8512
|
progress = void 0;
|
|
@@ -8107,4 +8624,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
|
|
|
8107
8624
|
return value;
|
|
8108
8625
|
}
|
|
8109
8626
|
//#endregion
|
|
8110
|
-
export {
|
|
8627
|
+
export { resolveBootstrapGitHubId as A, assertPlatformOperatorCredentialCanApplyPlan as B, purgeResourcesKeptReason as C, loadPlatformRuntime as D, runPurgeResourcesTeardown as E, formatPlatformInstallPlan as F, preparePlatformAccessSetup as G, verifyPlatformCloudflareCredential as H, platformAdoptionIntentHash as I, hyperdriveOriginFromConnectionString as J, PlatformCloudflareClient as K, platformProvisionIntentHash as L, recordPlatformWorkerTraffic as M, trackedPlatformWorkerTraffic as N, createPlatformHealthProbeToken as O, createPlatformInstallPlan as P, revalidatePlatformInstallAction as R, purgeResourcesHandlesEmailRouting as S, purgeResourcesPermissions as T, checkpointPlatformAccessResources as U, resolvePlatformCloudflareCredential as V, parsePlatformAccessSetupSpec as W, verifyPlatformRuntimeTokenPermissions as X, hyperdriveOriginMatches as Y, describePurgeResourcesPlan as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, isPurgedAccessResource as b, acquirePlatformDatabaseLifecycleLockConnection as c, readPlatformProjectZeroTrustState as d, assertProjectZeroTrustAllowsUninstall as f, checkPurgeResourcesCredentials as g, assertNoPlatformProjects as h, verifyPlatformAiGatewayPlan as i, recordResource as j, createPlatformInstallationSecrets as k, platformDatabaseClientUrl as l, PlatformResourceChangedError as m, resolvePlatformInstallDatabaseUrl as n, installPlatform as o, PURGE_RESOURCES_STEP_PREFIX as p, hyperdriveOriginConfigHash as q, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLock as s, assertNoninteractivePlatformMutationAcknowledged as t, readPlatformProjectSlugs as u, isPlatformTailWorker as v, purgeResourcesKeptResources as w, purgeResourcesAlwaysKept as x, isPurgeResourcesCandidate as y, platformDatabaseNameFromOwnerUrl as z };
|