void 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_TEMPLATE.md +1 -1
- package/README.md +5 -1
- package/dist/{account-cmd-CjyxcGkM.mjs → account-cmd-vJhh5lrb.mjs} +5 -5
- package/dist/{scan-ClYmX3sa.mjs → application-analysis-BVsVqc11.mjs} +135 -18
- package/dist/application-routing-B7QEjqSR.mjs +27 -0
- package/dist/{auth-DLNN0D3Z.mjs → auth-CQkSDS50.mjs} +26 -96
- package/dist/auth-DX3JURcX.mjs +2 -0
- package/dist/{auth-link-BLO4ptzo.mjs → auth-link-D75t9wO2.mjs} +5 -5
- package/dist/{auth-router-ZUg9MF_U.mjs → auth-router-ClfhL1J7.mjs} +7 -7
- package/dist/{better-auth-shared-rsBGBvWJ.mjs → better-auth-shared-hy6RPh9W.mjs} +13 -2
- package/dist/{build-cmd-BxR5FROK.mjs → build-cmd-zMi1iFnS.mjs} +21 -9
- package/dist/{cache-D0sWhgKI.mjs → cache-ZtQmyxVV.mjs} +5 -5
- package/dist/{cancel-deploy-D4NUqFbi.mjs → cancel-deploy-Box0kkEV.mjs} +5 -5
- package/dist/{cf-access-P0iZnu0q.mjs → cf-access-BGe5sWXF.mjs} +4 -48
- package/dist/{cf-access-DRsQRe6k.mjs → cf-access-TWNzXltp.mjs} +3 -11
- package/dist/{cf-build-output-BJ6yGEIS.mjs → cf-build-output-BPqOT964.mjs} +2 -1
- package/dist/cf-build-output-_0HNWysu.mjs +2 -0
- package/dist/cli/cli.mjs +89 -461
- package/dist/cli/{cf-compat.mjs → cloudflare-operation-process.mjs} +454 -424
- package/dist/cli/env-schema-probe.d.mts +2 -1
- package/dist/cli/env-schema-probe.mjs +4 -4
- package/dist/client-B9UBI03V.mjs +2 -0
- package/dist/{client-BTZ3XkrB.mjs → client-DKAsFA4X.mjs} +161 -24
- package/dist/{cloudflare-auth-DQkqoMYa.mjs → cloudflare-auth-Qdc7tw8F.mjs} +41 -38
- package/dist/{cloudflare-cmd-BKlfGHAy.mjs → cloudflare-cmd-CmY8dGkd.mjs} +6 -6
- package/dist/cloudflare-config-Bktvwtpf.mjs +182 -0
- package/dist/{cloudflare-connect-Ctmrw579.mjs → cloudflare-connect-IiR3ZieC.mjs} +4 -4
- package/dist/{cloudflare-operations-BT6OWFBk.mjs → cloudflare-operations-BCdXSsmo.mjs} +1 -1
- package/dist/{cloudflare-operations-B9tzgjmf.mjs → cloudflare-operations-DzH-A966.mjs} +116 -172
- package/dist/{preset-UHj9ARyP.mjs → cloudflare-process-B-wekeR6.mjs} +93 -6
- package/dist/{config-VavjpDnp.d.mts → config-BMHb8RCj.d.mts} +1 -0
- package/dist/config-C_XRIPx2.mjs +89 -0
- package/dist/config-entry.d.mts +1 -1
- package/dist/{connect-Dg-WkW-C.mjs → connect-B-usTnlF.mjs} +9 -9
- package/dist/{create-project-l9J7pbtm.mjs → create-project-CYOOAYcn.mjs} +12 -12
- package/dist/{create-project-DdoqwFLF.mjs → create-project-FWbf5Olo.mjs} +1 -1
- package/dist/{db-gp2sCXyN.mjs → db-Ck2eZ2iB.mjs} +26 -26
- package/dist/{delete-TTedbH6B.mjs → delete-sK56qtzZ.mjs} +5 -5
- package/dist/{deploy-CH-o2CaZ.mjs → deploy-gCVaiJbS.mjs} +984 -616
- package/dist/{deploy-B19L2Bra.mjs → deploy-irP5q0-x.mjs} +1 -1
- package/dist/{domain-JRF59P_r.mjs → domain-DFfQE8jg.mjs} +6 -6
- package/dist/{email-Bo6G9LOZ.mjs → email-CdnhUHvJ.mjs} +15 -30
- package/dist/{env-BYOrWQnv.mjs → env-DmGdHi5i.mjs} +8 -8
- package/dist/{env-public-BxU_0yTL.d.mts → env-public-BX_r8HR6.d.mts} +1 -1
- package/dist/{env-types-C2_bIk_b.mjs → env-types-D5kds_OZ.mjs} +1 -1
- package/dist/{env-validation-BB4GkLxn.mjs → env-validation-C_8tPdhs.mjs} +20 -73
- package/dist/{env-validation-BXge7uyK.mjs → env-validation-zZOCcQVZ.mjs} +1 -1
- package/dist/{fetch-CXDChK7B.mjs → fetch-BIZJh7vR.mjs} +2 -2
- package/dist/{fetch-stream-AOByI7Ki.mjs → fetch-stream-IvCYKyQL.mjs} +24 -4
- package/dist/{gen-sCtlCOdA.mjs → gen-CshAkukR.mjs} +4 -4
- package/dist/gen-DvQTWzDU.mjs +2 -0
- package/dist/{github-cmd-0-7PexDq.mjs → github-cmd-DftuEvIt.mjs} +13 -13
- package/dist/{handler-HEcZsaij.d.mts → handler-CZ4nAylQ.d.mts} +1 -1
- package/dist/{headers-DWi2IXWx.mjs → headers-2rKda9Y6.mjs} +1 -1
- package/dist/{help-GKtwl07I.mjs → help-Dotbf6iz.mjs} +236 -750
- package/dist/help-mabuIV98.mjs +2 -0
- package/dist/{inbound-aVHEUhKo.mjs → inbound-fRkqcXaC.mjs} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +299 -1517
- package/dist/info-B97bTX9N.mjs +113 -0
- package/dist/{init-FZx3Elvz.mjs → init-BTQEZZGJ.mjs} +61 -27
- package/dist/limits-Bq5LG8Id.d.mts +27 -0
- package/dist/limits-Cjuk2VPm.mjs +68 -0
- package/dist/{link-BrQfb_CU.mjs → link-B46sYH0K.mjs} +6 -6
- package/dist/{list-D44jmIAM.mjs → list-BnPrIsU_.mjs} +6 -6
- package/dist/{live-CKJlvlNp.d.mts → live-Chw1eIMv.d.mts} +1 -1
- package/dist/{local-d1-Bg9OzEEO.mjs → local-d1-2CMnpuW_.mjs} +2 -2
- package/dist/log-vqY3pqwI.mjs +29 -0
- package/dist/{login-CAsAsQ-_.mjs → login-BuiMyUrx.mjs} +8 -8
- package/dist/login-DIy9Sb-U.mjs +2 -0
- package/dist/{logs-BjKvFnVM.mjs → logs-CZG9d-ic.mjs} +10 -11
- package/dist/migrate-BCiJ8w0f.mjs +2 -0
- package/dist/{migrate-BPITvDJN.mjs → migrate-q4_njtaW.mjs} +4 -3
- package/dist/{node-Dt7z256D.mjs → node-BkyRWRx8.mjs} +1 -1
- package/dist/operator-args-B3BTmSoi.mjs +697 -0
- package/dist/{operator-auth-8KnOVXVd.mjs → operator-auth-7erYc6Lt.mjs} +2 -2
- package/dist/{operator-client-DUwumHpQ.mjs → operator-client-CpHjTd0t.mjs} +3 -3
- package/dist/{operator-cmd-BK5CCiT9.mjs → operator-cmd-DGqPJUD3.mjs} +70 -24
- package/dist/{output-Dm_A4Tbv.mjs → output-CbM85T4k.mjs} +18 -10
- package/dist/pages/client.d.mts +34 -2
- package/dist/pages/client.mjs +59 -3
- package/dist/pages/index.d.mts +1 -1
- package/dist/pages/index.mjs +2 -2
- package/dist/pages/islands-plugin.mjs +1 -1
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +2 -308
- package/dist/{parse-filename-DioPHiR9.mjs → parse-filename-CUbj-1MP.mjs} +35 -1
- package/dist/plan-D1Q5rf-r.mjs +2 -0
- package/dist/plan-NPpwZ_kc.mjs +58 -0
- package/dist/platform-args-3k8X9-H_.mjs +514 -0
- package/dist/platform-args-DdnM6WIf.mjs +2 -0
- package/dist/{platform-auth-config-BlN8xTdD.mjs → platform-auth-config-DDrxUy4m.mjs} +6 -6
- package/dist/{platform-auth-protection-7d5aV2Jg.mjs → platform-auth-protection-BLadnnct.mjs} +8 -8
- package/dist/{platform-auth-recovery-Dxij8ZbR.mjs → platform-auth-recovery-CmCzX2Fl.mjs} +6 -6
- package/dist/{platform-cmd-B3hwKrFK.mjs → platform-cmd-DMDEc9uS.mjs} +29 -8
- package/dist/{platform-cmd-E0FuL212.mjs → platform-cmd-pMTRhvYs.mjs} +1 -1
- package/dist/{platform-domain-D6Xcy9ZX.mjs → platform-domain-Di6EYrQl.mjs} +5 -5
- package/dist/{platform-lifecycle-k0E0xoxx.mjs → platform-lifecycle-BBoNzojY.mjs} +1541 -572
- package/dist/{platform-lifecycle-DMh_qrry.mjs → platform-lifecycle-BFdSi9Qt.mjs} +1 -1
- package/dist/{platform-management-Brz_BDiT.mjs → platform-management-CWOQOWXm.mjs} +10 -7
- package/dist/{platform-management-DOtss0BN.mjs → platform-management-DG47jys8.mjs} +1 -1
- package/dist/platform-plans-config-Vl9BP3H4.mjs +359 -0
- package/dist/{platform-recovery-BvtcXON5.mjs → platform-recovery-BJnQJEtQ.mjs} +4 -4
- package/dist/{platform-registry-QBa4DK32.mjs → platform-registry-DkPPXfp_.mjs} +2 -2
- package/dist/{plugin-inference-BMfKRSqE.mjs → plugin-inference-CXWnn79A.mjs} +174 -64
- package/dist/{prepare-C-6YZyyg.mjs → prepare-CBLT3CHH.mjs} +3 -3
- package/dist/prepare-CnjcTV2s.mjs +2 -0
- package/dist/{prepare-CRhJbVrG.mjs → prepare-DnaKBi--.mjs} +5 -21
- package/dist/{project-C0Fqu05O.mjs → project-DKG5Ra8p.mjs} +15 -82
- package/dist/project-DsblYifa.mjs +2 -0
- package/dist/{project-cmd-BZLCqOqw.mjs → project-cmd-DxnXstAq.mjs} +19 -19
- package/dist/{project-team-BvgM0WoX.mjs → project-team-BmHcaALJ.mjs} +5 -5
- package/dist/{project-token-l5DqK6Az.mjs → project-token-GionZlBI.mjs} +5 -5
- package/dist/{project-zero-trust-v6Mvpp8y.mjs → project-zero-trust-JLCjVQA7.mjs} +5 -5
- package/dist/prompts-2TGE6oXx.mjs +2 -0
- package/dist/prompts-BNUoTiaf.mjs +2044 -0
- package/dist/{protocol-BBa6cstI.d.mts → protocol-CjF_iI9X.d.mts} +2 -2
- package/dist/protocol-U7bfjHmA.mjs +331 -0
- package/dist/{provision-Fr9pRqce.mjs → provision-CVku1JFU.mjs} +1 -1
- package/dist/{provision-C4IGqkBf.mjs → provision-CaoxO_U7.mjs} +90 -199
- package/dist/{requests-DN-BbNiM.mjs → requests-BOjIVAeF.mjs} +5 -5
- package/dist/resource-name-C7LVpcRm.mjs +11 -0
- package/dist/{rollback-D7rzSaZM.mjs → rollback-CwR3pLj3.mjs} +6 -6
- package/dist/route-url-CG7U-cRN.mjs +15 -0
- package/dist/{runner-B8wXwWlo.mjs → runner-CXA9Fh8h.mjs} +1 -1
- package/dist/{runner-p-dMs2UN.mjs → runner-Ol0TNjk6.mjs} +2 -2
- package/dist/runtime/ai.d.mts +12 -8
- package/dist/runtime/ai.mjs +87 -22
- package/dist/runtime/better-auth-mysql.mjs +1 -1
- package/dist/runtime/better-auth-pg.mjs +1 -1
- package/dist/runtime/better-auth.mjs +1 -1
- package/dist/runtime/client-react.mjs +2 -2
- package/dist/runtime/client-solid.mjs +2 -2
- package/dist/runtime/client-svelte.mjs +2 -2
- package/dist/runtime/client-vue.mjs +2 -2
- package/dist/runtime/client.mjs +2 -2
- package/dist/runtime/durable.d.mts +3 -1
- package/dist/runtime/durable.mjs +4 -1
- package/dist/runtime/email/testing.mjs +2 -2
- package/dist/runtime/email.mjs +1 -1
- package/dist/runtime/env-public.d.mts +1 -1
- package/dist/runtime/fetch-stream.mjs +1 -1
- package/dist/runtime/fetch.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/kv.mjs +0 -1
- package/dist/runtime/limits.d.mts +2 -0
- package/dist/runtime/limits.mjs +2 -0
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live-server.mjs +25 -16
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/migration-handler.mjs +62 -42
- package/dist/runtime/remote/index.mjs +2 -2
- package/dist/runtime/route-url.d.mts +4 -0
- package/dist/runtime/route-url.mjs +2 -0
- package/dist/runtime/routing.d.mts +180 -0
- package/dist/runtime/routing.mjs +1082 -0
- package/dist/runtime/sandbox-container.d.mts +1 -1
- package/dist/runtime/sandbox-container.mjs +1 -1
- package/dist/runtime/sandbox.d.mts +3 -3
- package/dist/runtime/sandbox.mjs +75 -45
- package/dist/runtime/sse.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +4 -2
- package/dist/runtime/ws-server.mjs +27 -2
- package/dist/runtime/ws.d.mts +2 -2
- package/dist/runtime/ws.mjs +8 -6
- package/dist/{sandbox-qpNBT8a3.d.mts → sandbox-XZAqzFlG.d.mts} +11 -8
- package/dist/{sandbox-container-DdNEBfCc.d.mts → sandbox-container-Bo0eiFKz.d.mts} +2 -1
- package/dist/{sandbox-container-4fdqLnyb.mjs → sandbox-container-C6ItmVuN.mjs} +4 -2
- package/dist/{scan-4tfN-PSn.mjs → scan-C7okrLyM.mjs} +4 -35
- package/dist/{secret-BOtOl_cb.mjs → secret-ByWJWBUG.mjs} +7 -7
- package/dist/{skills-D1II1Juz.mjs → skills-C1PlclPt.mjs} +1 -1
- package/dist/{sse-BaC1jXko.mjs → sse-CQNaDFFV.mjs} +6 -3
- package/dist/{subcommand-prompt-CY1C4fvl.mjs → subcommand-prompt-klFCfcor.mjs} +40 -32
- package/dist/validate-Dq_L3s0S.mjs +2 -0
- package/dist/{validate-CIUwFpjB.mjs → validate-ctOrgiS3.mjs} +2 -1
- package/dist/{wrangler-DQF1vKyf.mjs → wrangler-7K-bW_DL.mjs} +8 -239
- package/dist/{ws-BoY7vQML.d.mts → ws-CL1w7GXU.d.mts} +13 -2
- package/package.json +16 -10
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +34 -157
- package/skills/void/SKILL.md +55 -135
- package/skills/void/docs/guide/ai.md +94 -84
- package/skills/void/docs/guide/app-types.md +3 -32
- package/skills/void/docs/guide/auth.md +12 -116
- package/skills/void/docs/guide/database/d1.md +9 -54
- package/skills/void/docs/guide/database/mysql.md +1 -1
- package/skills/void/docs/guide/database/postgresql.md +5 -26
- package/skills/void/docs/guide/database.md +23 -75
- package/skills/void/docs/guide/deployment.md +48 -112
- package/skills/void/docs/guide/durable-state.md +43 -18
- package/skills/void/docs/guide/edge/headers.md +4 -48
- package/skills/void/docs/guide/edge/prerendering.md +5 -20
- package/skills/void/docs/guide/edge/redirects.md +11 -64
- package/skills/void/docs/guide/edge/revalidation.md +5 -18
- package/skills/void/docs/guide/edge/rewrites.md +56 -284
- package/skills/void/docs/guide/edge/static-assets.md +22 -72
- package/skills/void/docs/guide/email/domains.md +116 -0
- package/skills/void/docs/guide/email/receiving.md +139 -0
- package/skills/void/docs/guide/email/sending.md +231 -0
- package/skills/void/docs/guide/email.md +13 -619
- package/skills/void/docs/guide/env-migration.md +11 -11
- package/skills/void/docs/guide/env-vars.md +9 -29
- package/skills/void/docs/guide/index.md +0 -15
- package/skills/void/docs/guide/jobs.md +3 -18
- package/skills/void/docs/guide/kv.md +5 -11
- package/skills/void/docs/guide/live.md +5 -56
- package/skills/void/docs/guide/pages-routing/actions-and-forms.md +78 -125
- package/skills/void/docs/guide/pages-routing/head.md +10 -10
- package/skills/void/docs/guide/pages-routing/islands.md +6 -36
- package/skills/void/docs/guide/pages-routing/layouts.md +6 -128
- package/skills/void/docs/guide/pages-routing/loaders.md +3 -19
- package/skills/void/docs/guide/pages-routing/markdown.md +13 -171
- package/skills/void/docs/guide/pages-routing/overview.md +7 -17
- package/skills/void/docs/guide/pages-routing/view-transitions.md +1 -1
- package/skills/void/docs/guide/platform/administration/access.md +1 -4
- package/skills/void/docs/guide/platform/administration/email.md +35 -8
- package/skills/void/docs/guide/platform/administration/operations.md +26 -4
- package/skills/void/docs/guide/platform/administration/plans.md +126 -0
- package/skills/void/docs/guide/platform/administration/projects.md +5 -2
- package/skills/void/docs/guide/platform/administration/zero-trust.md +49 -160
- package/skills/void/docs/guide/platform/development/runtime.md +3 -13
- package/skills/void/docs/guide/platform/development/schema-ci.md +0 -58
- package/skills/void/docs/guide/platform/installation/ci.md +1 -1
- package/skills/void/docs/guide/platform/installation/credentials.md +15 -6
- package/skills/void/docs/guide/platform/installation/domains.md +35 -2
- package/skills/void/docs/guide/platform/installation/first-deployment.md +4 -2
- package/skills/void/docs/guide/platform/installation/maintenance.md +15 -1
- package/skills/void/docs/guide/platform/installation/prerequisites.md +24 -18
- package/skills/void/docs/guide/platform/installation/setup.md +88 -14
- package/skills/void/docs/guide/platform-administration.md +1 -0
- package/skills/void/docs/guide/queues.md +7 -9
- package/skills/void/docs/guide/quickstart.md +38 -37
- package/skills/void/docs/guide/remote-dev.md +5 -10
- package/skills/void/docs/guide/sandboxes.md +29 -21
- package/skills/void/docs/guide/server-routing.md +9 -72
- package/skills/void/docs/guide/sse.md +4 -18
- package/skills/void/docs/guide/ssg.md +3 -15
- package/skills/void/docs/guide/ssr.md +14 -62
- package/skills/void/docs/guide/storage.md +9 -4
- package/skills/void/docs/guide/type-safety.md +3 -14
- package/skills/void/docs/guide/typed-fetch.md +3 -7
- package/skills/void/docs/guide/websockets.md +68 -40
- package/skills/void/docs/integrations/agents.md +3 -3
- package/skills/void/docs/integrations/cloudflare.md +85 -316
- package/skills/void/docs/integrations/frameworks/analog.md +5 -64
- package/skills/void/docs/integrations/frameworks/astro.md +4 -73
- package/skills/void/docs/integrations/frameworks/nuxt.md +5 -62
- package/skills/void/docs/integrations/frameworks/overview.md +11 -54
- package/skills/void/docs/integrations/frameworks/react-router.md +5 -60
- package/skills/void/docs/integrations/frameworks/sveltekit.md +6 -65
- package/skills/void/docs/integrations/frameworks/tanstack-start.md +4 -62
- package/skills/void/docs/integrations/nodejs-bun-deno.md +5 -69
- package/skills/void/docs/reference/api/auth.md +156 -0
- package/skills/void/docs/reference/api/client.md +87 -0
- package/skills/void/docs/reference/api/database.md +95 -0
- package/skills/void/docs/reference/api/durable.md +46 -0
- package/skills/void/docs/reference/api/env.md +50 -0
- package/skills/void/docs/reference/api/handlers.md +254 -0
- package/skills/void/docs/reference/api/pages.md +241 -0
- package/skills/void/docs/reference/api/plugin.md +39 -0
- package/skills/void/docs/reference/api/resources.md +109 -0
- package/skills/void/docs/reference/api/rewrites.md +76 -0
- package/skills/void/docs/reference/api/types.md +92 -0
- package/skills/void/docs/reference/api.md +56 -1218
- package/skills/void/docs/reference/cli/auth.md +89 -0
- package/skills/void/docs/reference/cli/database.md +128 -0
- package/skills/void/docs/reference/cli/deploy.md +105 -0
- package/skills/void/docs/reference/cli/domains.md +41 -0
- package/skills/void/docs/reference/cli/email.md +129 -0
- package/skills/void/docs/reference/cli/generate.md +116 -0
- package/skills/void/docs/reference/cli/github.md +189 -0
- package/skills/void/docs/reference/cli/platform-config.md +92 -0
- package/skills/void/docs/reference/cli/platform-email.md +81 -0
- package/skills/void/docs/reference/cli/platform-installation.md +142 -0
- package/skills/void/docs/reference/cli/platform-operations.md +90 -0
- package/skills/void/docs/reference/cli/platform-users.md +89 -0
- package/skills/void/docs/reference/cli/platform-zero-trust.md +59 -0
- package/skills/void/docs/reference/cli/platform.md +70 -0
- package/skills/void/docs/reference/cli/project.md +214 -0
- package/skills/void/docs/reference/cli/secrets.md +76 -0
- package/skills/void/docs/reference/cli/setup.md +70 -0
- package/skills/void/docs/reference/cli.md +32 -1682
- package/skills/void/docs/reference/config.md +12 -18
- package/skills/void/docs/reference/resource-inference.md +3 -58
- package/skills/void/docs/reference/structure.md +14 -41
- package/dist/auth-CZuiVsFh.mjs +0 -2
- package/dist/canonical-json-DuDiiUsQ.mjs +0 -13
- package/dist/client-Czz8o5jP.mjs +0 -2
- package/dist/dist-BuDuKZJv.mjs +0 -2
- package/dist/dist-CTBk70IR.mjs +0 -1310
- package/dist/gen-CNJ62MM7.mjs +0 -2
- package/dist/help-CmZzxUba.mjs +0 -2
- package/dist/log-ChfPKsVd.mjs +0 -26
- package/dist/login-CGcRKEoi.mjs +0 -2
- package/dist/migrate-CYfbKkXh.mjs +0 -2
- package/dist/plan-BEZ8VJW0.mjs +0 -256
- package/dist/plan-DpuOr14e.mjs +0 -2
- package/dist/prepare-Bm3iq-u4.mjs +0 -2
- package/dist/project-BuUwfj6f.mjs +0 -2
- package/dist/validate-EKmJWxmy.mjs +0 -2
- /package/dist/cli/{cf-compat.d.mts → cloudflare-operation-process.d.mts} +0 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { n as __toESM } from "./rolldown-runtime-DXIUcv95.mjs";
|
|
2
1
|
import { a as join, n as dirname, u as resolve } from "./dist-BVqXeR_h.mjs";
|
|
3
|
-
import { t as
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { o as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { copyFileSync, cpSync, existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { t as cfAccessHeaders } from "./cf-access-TWNzXltp.mjs";
|
|
3
|
+
import { _ as savePlatformConnection, a as listInstallationManifests, b as writePendingInstallationSecrets, d as promoteInstallationSecrets, f as readInstallationRecoverySecrets, g as resolveInstallationManifest, i as latestPlatformWorkerCutoverGeneration, l as platformRegistryDirectory, n as assertPlatformInstallationSlugAvailable, p as readPendingInstallationSecrets, s as normalizePlatformApiUrl, t as acquirePlatformLifecycleLock, u as platformWorkerSecretUpdateIntentState, y as writeInstallationManifest } from "./platform-registry-DkPPXfp_.mjs";
|
|
4
|
+
import { n as ensurePlatformAccessToken } from "./cf-access-BGe5sWXF.mjs";
|
|
5
|
+
import { u as openBrowser } from "./auth-CQkSDS50.mjs";
|
|
6
|
+
import { A as confirm, B as note, H as outro, K as select, L as log, N as intro, U as password, X as text$1, tt as isCancel, z as multiselect } from "./prompts-BNUoTiaf.mjs";
|
|
7
|
+
import { c as resolveCloudflareOperationArgv } from "./cloudflare-process-B-wekeR6.mjs";
|
|
8
|
+
import { o as runCloudflareAuthCommand } from "./cloudflare-auth-Qdc7tw8F.mjs";
|
|
9
|
+
import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-CaoxO_U7.mjs";
|
|
10
|
+
import { c as import_picocolors, i as createSpinner, n as cliCommandName, r as cliTitle } from "./output-CbM85T4k.mjs";
|
|
11
|
+
import { t as parsePlatformArgs } from "./platform-args-3k8X9-H_.mjs";
|
|
12
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
13
|
import { fileURLToPath } from "node:url";
|
|
15
14
|
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
17
16
|
import { execFile } from "node:child_process";
|
|
18
|
-
import { PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY,
|
|
17
|
+
import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAppAccessSetup, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
|
|
19
18
|
import { stripVTControlCharacters } from "node:util";
|
|
20
19
|
import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
|
|
21
20
|
import { readFile } from "node:fs/promises";
|
|
@@ -142,7 +141,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
|
|
|
142
141
|
} catch (error) {
|
|
143
142
|
const status = error instanceof PlatformCloudflareApiError ? error.status : null;
|
|
144
143
|
const reason = status === null ? "the request failed" : `HTTP ${status}`;
|
|
145
|
-
const guidance = status === 401 || status === 403 ? "Check your Cloudflare login or API token permissions for this account, then rerun the command." : status === 404 ? `
|
|
144
|
+
const guidance = status === 401 || status === 403 ? "Check your Cloudflare login or API token permissions for this account, then rerun the command." : status === 404 ? `Open ${setupUrl} once to provision your account for Workers automatically, then retry the install.` : "Rerun the command. If the dashboard already shows a subdomain, no Workers onboarding change is needed.";
|
|
146
145
|
throw new Error(`platform: Could not read the workers.dev subdomain for account ${accountId} (${reason}). ${guidance}`, { cause: error });
|
|
147
146
|
}
|
|
148
147
|
if (typeof result?.subdomain !== "string" || result.subdomain.trim() === "") throw new Error(`platform: This Cloudflare account does not have a workers.dev subdomain. Open ${setupUrl}, finish Workers onboarding, choose a subdomain, and rerun the command.`);
|
|
@@ -680,11 +679,12 @@ function hyperdriveOriginConfigHash(origin) {
|
|
|
680
679
|
host: origin.host.toLowerCase(),
|
|
681
680
|
port: origin.port,
|
|
682
681
|
scheme: "postgresql",
|
|
683
|
-
user: origin.user
|
|
682
|
+
user: origin.user,
|
|
683
|
+
...origin.accessClientId ? { accessClientId: origin.accessClientId } : {}
|
|
684
684
|
})).digest("hex");
|
|
685
685
|
}
|
|
686
686
|
function hyperdriveOriginMatches(live, expected) {
|
|
687
|
-
return live.database === expected.database && live.host.toLowerCase() === expected.host.toLowerCase() && live.port === expected.port && live.scheme === expected.scheme && (live.user === null || live.user === expected.user);
|
|
687
|
+
return live.database === expected.database && live.host.toLowerCase() === expected.host.toLowerCase() && live.port === expected.port && live.accessClientId === expected.accessClientId && live.scheme === expected.scheme && (live.user === null || live.user === expected.user);
|
|
688
688
|
}
|
|
689
689
|
function parseHyperdriveConfig(value) {
|
|
690
690
|
const origin = value.origin;
|
|
@@ -692,13 +692,15 @@ function parseHyperdriveConfig(value) {
|
|
|
692
692
|
const record = origin;
|
|
693
693
|
const scheme = requiredString(record.scheme, "Hyperdrive origin scheme").toLowerCase();
|
|
694
694
|
if (scheme !== "postgres" && scheme !== "postgresql") throw new Error(`platform: Unsupported Hyperdrive origin scheme ${JSON.stringify(scheme)}.`);
|
|
695
|
-
const
|
|
696
|
-
|
|
695
|
+
const accessClientId = typeof record.access_client_id === "string" && record.access_client_id ? record.access_client_id : void 0;
|
|
696
|
+
const port = accessClientId && record.port == null ? null : typeof record.port === "number" || typeof record.port === "string" ? Number(record.port) : NaN;
|
|
697
|
+
if (port !== null && (!Number.isInteger(port) || port <= 0 || port > 65535)) throw new Error("platform: Cloudflare Hyperdrive config has an invalid origin port.");
|
|
697
698
|
return {
|
|
698
699
|
id: requiredString(value.id, "Hyperdrive id"),
|
|
699
700
|
name: requiredString(value.name, "Hyperdrive name"),
|
|
700
701
|
cachingDisabled: !!value.caching && typeof value.caching === "object" && !Array.isArray(value.caching) && value.caching.disabled === true,
|
|
701
702
|
origin: {
|
|
703
|
+
...accessClientId ? { accessClientId } : {},
|
|
702
704
|
database: requiredString(record.database, "Hyperdrive origin database"),
|
|
703
705
|
host: requiredString(record.host, "Hyperdrive origin host").toLowerCase(),
|
|
704
706
|
port,
|
|
@@ -717,6 +719,14 @@ function text(value, name) {
|
|
|
717
719
|
if (typeof value !== "string" || !value || value.length > 8192) throw new Error(`platform: Cloudflare Access did not return ${name}.`);
|
|
718
720
|
return value;
|
|
719
721
|
}
|
|
722
|
+
function platformAccessIdentityProvider(provider) {
|
|
723
|
+
const id = text(provider.id, "identity provider ID");
|
|
724
|
+
const name = provider.name;
|
|
725
|
+
return {
|
|
726
|
+
id,
|
|
727
|
+
name: name == null || typeof name === "string" && !name.trim() ? id : text(name, "identity provider name")
|
|
728
|
+
};
|
|
729
|
+
}
|
|
720
730
|
function ids(value) {
|
|
721
731
|
if (!Array.isArray(value)) return [];
|
|
722
732
|
return value.map((entry) => typeof entry === "string" ? entry : text(object(entry, "policy").id, "policy ID"));
|
|
@@ -846,10 +856,7 @@ async function inspectPlatformAccess(client, input) {
|
|
|
846
856
|
audience: "validation"
|
|
847
857
|
}]
|
|
848
858
|
});
|
|
849
|
-
const identityProviders = (await client.list("identity_providers")).map(
|
|
850
|
-
id: text(provider.id, "identity provider ID"),
|
|
851
|
-
name: text(provider.name ?? provider.id, "identity provider name")
|
|
852
|
-
}));
|
|
859
|
+
const identityProviders = (await client.list("identity_providers")).map(platformAccessIdentityProvider);
|
|
853
860
|
if (!input.identityProviderIds.length || !input.policyIds.length || input.identityProviderIds.length > 16 || input.policyIds.length > 16 || new Set(input.identityProviderIds).size !== input.identityProviderIds.length || new Set(input.policyIds).size !== input.policyIds.length) throw new Error("platform: Select explicit identity providers and company policies for Access.");
|
|
854
861
|
if (input.identityProviderIds.some((id) => !identityProviders.some((provider) => provider.id === id))) throw new Error("platform: A selected Access identity provider is unavailable in this account.");
|
|
855
862
|
const policies = await Promise.all(input.policyIds.map(async (id) => object(await client.get(`policies/${encodeURIComponent(id)}`), "Access policy")));
|
|
@@ -1119,7 +1126,343 @@ async function verifyPlatformAccessApplication(client, input, app, type, machine
|
|
|
1119
1126
|
if (machineId && !currentPolicies.some((policy) => policy.decision === "non_identity")) throw new Error("platform: The platform service token is not admitted by Access.");
|
|
1120
1127
|
}
|
|
1121
1128
|
//#endregion
|
|
1129
|
+
//#region src/cli/platform-setup.ts
|
|
1130
|
+
function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
|
|
1131
|
+
return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + import_picocolors.default.underline(page.title) + "\x1B]8;;\x1B\\" : page.title + ": " + import_picocolors.default.underline(page.url);
|
|
1132
|
+
}
|
|
1133
|
+
function platformSetupFieldHelp(key) {
|
|
1134
|
+
const docs = (page, section, title) => platformSetupLink({
|
|
1135
|
+
title,
|
|
1136
|
+
url: `https://void.cloud/guide/platform/installation/${page}#${section}`
|
|
1137
|
+
});
|
|
1138
|
+
switch (key) {
|
|
1139
|
+
case "databaseUrl": return {
|
|
1140
|
+
placeholder: "postgresql://user:pw@…/database",
|
|
1141
|
+
description: "Provide a PostgreSQL database URL for platform users, projects, and deployments. Use an empty hosted database, e.g. " + platformSetupLink({
|
|
1142
|
+
title: "Neon",
|
|
1143
|
+
url: "https://neon.com/"
|
|
1144
|
+
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
1145
|
+
};
|
|
1146
|
+
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") };
|
|
1147
|
+
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") };
|
|
1148
|
+
default: return;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function supportsSetupLinks() {
|
|
1152
|
+
return !!process.stdout.isTTY && ([
|
|
1153
|
+
"ghostty",
|
|
1154
|
+
"iTerm.app",
|
|
1155
|
+
"WezTerm",
|
|
1156
|
+
"vscode",
|
|
1157
|
+
"WarpTerminal"
|
|
1158
|
+
].includes(process.env.TERM_PROGRAM ?? "") || !!process.env.WT_SESSION || !!process.env.VTE_VERSION || !!process.env.KONSOLE_VERSION || process.env.TERM === "xterm-kitty");
|
|
1159
|
+
}
|
|
1160
|
+
/** Dashboard keys use permission-catalog labels, which can differ from product names. */
|
|
1161
|
+
function platformAccountTokenPermissions(includeAiGateway) {
|
|
1162
|
+
return [
|
|
1163
|
+
{
|
|
1164
|
+
key: "account_settings",
|
|
1165
|
+
type: "read"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
key: "workers_scripts",
|
|
1169
|
+
type: "edit"
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
key: "workers_kv_storage",
|
|
1173
|
+
type: "edit"
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
key: "workers_r2",
|
|
1177
|
+
type: "edit"
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
key: "d1",
|
|
1181
|
+
type: "edit"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
key: "queues",
|
|
1185
|
+
type: "edit"
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
key: "account_analytics",
|
|
1189
|
+
type: "read"
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
key: "workers_tail",
|
|
1193
|
+
type: "read"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
key: "query_cache",
|
|
1197
|
+
type: "edit"
|
|
1198
|
+
},
|
|
1199
|
+
...includeAiGateway ? [{
|
|
1200
|
+
key: "aig",
|
|
1201
|
+
type: "edit"
|
|
1202
|
+
}] : []
|
|
1203
|
+
];
|
|
1204
|
+
}
|
|
1205
|
+
function platformManagementSetupPage(plan, options = {}) {
|
|
1206
|
+
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1207
|
+
const zoneName = plan.deployment.application.zoneName;
|
|
1208
|
+
const apiHostname = plan.deployment.controlPlane.apiHostname;
|
|
1209
|
+
const access = options.access ?? (plan.deployment.authentication.accessProtection || plan.deployment.authentication.providers.includes("cloudflare-access") ? {
|
|
1210
|
+
protection: !!plan.deployment.authentication.accessProtection,
|
|
1211
|
+
readOnly: false
|
|
1212
|
+
} : void 0);
|
|
1213
|
+
const accessLevel = access && access.readOnly ? "read" : "edit";
|
|
1214
|
+
const appAccess = !!plan.deployment.appAccess;
|
|
1215
|
+
const appAccessLevel = appAccess ? "edit" : accessLevel;
|
|
1216
|
+
const permissions = [
|
|
1217
|
+
...platformAccountTokenPermissions(true),
|
|
1218
|
+
...zoneName || apiHostname || createsZone ? [
|
|
1219
|
+
{
|
|
1220
|
+
key: "zone",
|
|
1221
|
+
type: createsZone ? "edit" : "read"
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
key: "dns",
|
|
1225
|
+
type: "edit"
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
key: "workers_routes",
|
|
1229
|
+
type: "edit"
|
|
1230
|
+
}
|
|
1231
|
+
] : [],
|
|
1232
|
+
...access || appAccess ? [
|
|
1233
|
+
{
|
|
1234
|
+
key: "access",
|
|
1235
|
+
type: appAccessLevel
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
key: "access_acct",
|
|
1239
|
+
type: "read"
|
|
1240
|
+
},
|
|
1241
|
+
...access && access.protection ? [{
|
|
1242
|
+
key: "access_service_token",
|
|
1243
|
+
type: accessLevel
|
|
1244
|
+
}] : []
|
|
1245
|
+
] : [],
|
|
1246
|
+
...zoneName && plan.deployment.application.baseDomain !== zoneName ? [{
|
|
1247
|
+
key: "ssl_and_certificates",
|
|
1248
|
+
type: "read"
|
|
1249
|
+
}] : []
|
|
1250
|
+
];
|
|
1251
|
+
return {
|
|
1252
|
+
title: "Cloudflare management API token",
|
|
1253
|
+
linkInTitle: true,
|
|
1254
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1255
|
+
to: "/" + plan.account.id + "/api-tokens",
|
|
1256
|
+
name: plan.deployment.displayName + " Management",
|
|
1257
|
+
permissionGroupKeys: JSON.stringify(permissions)
|
|
1258
|
+
}),
|
|
1259
|
+
instructions: [
|
|
1260
|
+
"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.",
|
|
1261
|
+
`Scope it to ${plan.account.name}${createsZone ? "; allow zone creation." : zoneName ? ` and zone ${zoneName}.` : "."}`,
|
|
1262
|
+
...apiHostname && apiHostname !== zoneName && !apiHostname.endsWith(`.${zoneName}`) ? [`Also include the zone for ${apiHostname}.`] : []
|
|
1263
|
+
].join("\n")
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function platformSetupPages(plan) {
|
|
1267
|
+
const permissions = [
|
|
1268
|
+
...platformAccountTokenPermissions(plan.actions.some((action) => action.kind === "ai-gateway" && action.action === "inspect")),
|
|
1269
|
+
...plan.deployment.application.zoneName ? [{
|
|
1270
|
+
key: "cache",
|
|
1271
|
+
type: "purge"
|
|
1272
|
+
}] : [],
|
|
1273
|
+
...plan.deployment.capabilities.customDomains ? [{
|
|
1274
|
+
key: "ssl_and_certificates",
|
|
1275
|
+
type: "edit"
|
|
1276
|
+
}] : [],
|
|
1277
|
+
...plan.deployment.capabilities.email ? [
|
|
1278
|
+
{
|
|
1279
|
+
key: "zone",
|
|
1280
|
+
type: "read"
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
key: "zone_settings",
|
|
1284
|
+
type: "edit"
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
key: "email_routing_address",
|
|
1288
|
+
type: "edit"
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
key: "email_routing_rule",
|
|
1292
|
+
type: "edit"
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
key: "email_sending",
|
|
1296
|
+
type: "edit"
|
|
1297
|
+
}
|
|
1298
|
+
] : []
|
|
1299
|
+
];
|
|
1300
|
+
return {
|
|
1301
|
+
runtime: {
|
|
1302
|
+
title: "Cloudflare runtime token",
|
|
1303
|
+
linkInTitle: true,
|
|
1304
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1305
|
+
to: "/" + plan.account.id + "/api-tokens",
|
|
1306
|
+
name: plan.deployment.displayName + " Runtime",
|
|
1307
|
+
permissionGroupKeys: JSON.stringify(permissions)
|
|
1308
|
+
}),
|
|
1309
|
+
instructions: "Create a Cloudflare runtime token that Void can use to manage the platform. Please review the permissions and then paste the token here."
|
|
1310
|
+
},
|
|
1311
|
+
github: {
|
|
1312
|
+
title: "GitHub OAuth App",
|
|
1313
|
+
url: "https://github.com/settings/applications/new",
|
|
1314
|
+
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"
|
|
1315
|
+
},
|
|
1316
|
+
r2: {
|
|
1317
|
+
title: "R2 upload token",
|
|
1318
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
1319
|
+
to: "/" + plan.account.id + "/r2/api-tokens/create",
|
|
1320
|
+
type: "account"
|
|
1321
|
+
}),
|
|
1322
|
+
instructions: [
|
|
1323
|
+
"Select Object Read & Write.",
|
|
1324
|
+
"Keep Apply to all buckets in this account selected.",
|
|
1325
|
+
"Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
|
|
1326
|
+
].join("\n")
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function formatPlatformSetupPlan(plan, options = {}) {
|
|
1331
|
+
const writesDns = plan.actions.some((action) => action.kind === "zone" && action.action === "create" || action.kind === "dns-record" && action.action !== "reuse");
|
|
1332
|
+
return [
|
|
1333
|
+
"The installation will guide you through setting up the following credentials:",
|
|
1334
|
+
"",
|
|
1335
|
+
...[
|
|
1336
|
+
...options.managementToken ? [options.managementAccess ? `Cloudflare management API token for ${writesDns ? "DNS changes and " : ""}Access setup` : "Cloudflare management API token for DNS changes"] : [],
|
|
1337
|
+
"PostgreSQL database URL",
|
|
1338
|
+
"Cloudflare runtime token",
|
|
1339
|
+
"R2 upload token",
|
|
1340
|
+
...plan.deployment.authentication.providers.includes("github") ? ["GitHub OAuth App client ID and secret"] : [],
|
|
1341
|
+
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => (provider === "google" ? "Google OAuth" : provider === "oidc" ? "OIDC" : "Cloudflare Access OIDC") + " client ID and secret"),
|
|
1342
|
+
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access application and installer credentials"] : [],
|
|
1343
|
+
...plan.deployment.appAccess ? ["Cloudflare Access check and app-management token when needed (stored encrypted)"] : [],
|
|
1344
|
+
"JWT signing key and project encryption key"
|
|
1345
|
+
].map((credential) => `• ${credential}`)
|
|
1346
|
+
].join("\n");
|
|
1347
|
+
}
|
|
1348
|
+
function platformInstallCommandArguments(options) {
|
|
1349
|
+
const parts = [
|
|
1350
|
+
options.commandName,
|
|
1351
|
+
"platform",
|
|
1352
|
+
"install"
|
|
1353
|
+
];
|
|
1354
|
+
const flag = (name, value) => {
|
|
1355
|
+
if (value) parts.push(name, value);
|
|
1356
|
+
};
|
|
1357
|
+
if (options.resumeId) {
|
|
1358
|
+
parts.push("--resume");
|
|
1359
|
+
flag("--name", options.resumeId);
|
|
1360
|
+
} else {
|
|
1361
|
+
flag("--name", options.installationSlug);
|
|
1362
|
+
flag("--account", options.accountId);
|
|
1363
|
+
flag("--display-name", options.displayName);
|
|
1364
|
+
if (options.workersDev) parts.push("--workers-dev");
|
|
1365
|
+
else {
|
|
1366
|
+
flag("--application-domain", options.applicationDomain);
|
|
1367
|
+
flag("--zone", options.zoneName);
|
|
1368
|
+
if (options.dedicatedZone) parts.push("--dedicated-zone");
|
|
1369
|
+
else parts.push("--no-dedicated-zone");
|
|
1370
|
+
}
|
|
1371
|
+
flag("--control-plane-domain", options.controlPlaneDomain);
|
|
1372
|
+
if (!options.controlPlaneDomain) parts.push("--no-control-plane-domain");
|
|
1373
|
+
}
|
|
1374
|
+
flag("--hyperdrive", options.hyperdrive ?? "");
|
|
1375
|
+
flag("--auth-config", options.authConfig ?? "");
|
|
1376
|
+
if (!options.resumeId && options.appAccess) flag("--app-access", options.appAccess);
|
|
1377
|
+
if (!options.authConfig && !options.resumeId && options.authentication) {
|
|
1378
|
+
flag("--login-methods", options.authentication.providers.join(","));
|
|
1379
|
+
parts.push(options.authentication.accessProtection ? "--access-protection" : "--no-access-protection");
|
|
1380
|
+
}
|
|
1381
|
+
flag("--dashboard-url", options.dashboardUrl ?? "");
|
|
1382
|
+
flag("--runtime", options.runtime);
|
|
1383
|
+
if (options.yes) parts.push("--yes");
|
|
1384
|
+
return parts;
|
|
1385
|
+
}
|
|
1386
|
+
function formatPlatformInstallCommand(options) {
|
|
1387
|
+
return platformInstallCommandArguments(options).map((value) => {
|
|
1388
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
1389
|
+
return process.platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
1390
|
+
}).join(" ");
|
|
1391
|
+
}
|
|
1392
|
+
function openPlatformSetupPage(page, dependencies = {}) {
|
|
1393
|
+
log.info((page.linkInTitle ? platformSetupLink(page) : page.title) + "\n" + page.instructions);
|
|
1394
|
+
if (!page.linkInTitle) console.log(platformSetupLink({
|
|
1395
|
+
...page,
|
|
1396
|
+
title: "Open " + page.title
|
|
1397
|
+
}));
|
|
1398
|
+
if (dependencies.interactive ?? process.stdin.isTTY === true) try {
|
|
1399
|
+
(dependencies.open ?? ((url) => openBrowser(url, execFile)))(page.url);
|
|
1400
|
+
} catch {
|
|
1401
|
+
log.info("Could not open the browser. Use the link above.");
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
//#endregion
|
|
1122
1405
|
//#region src/cli/platform-access-setup.ts
|
|
1406
|
+
function accessSetupGuide() {
|
|
1407
|
+
return platformSetupLink({
|
|
1408
|
+
title: "Setup guide and required permissions",
|
|
1409
|
+
url: "https://void.cloud/guide/platform/installation/setup#cloudflare-access"
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
async function selectCompanyAccessPolicies(client, initialPolicies, accountId, mode) {
|
|
1413
|
+
const create = "__create__";
|
|
1414
|
+
const refresh = "__refresh__";
|
|
1415
|
+
let policies = initialPolicies;
|
|
1416
|
+
let selectedIds = [];
|
|
1417
|
+
while (true) {
|
|
1418
|
+
const options = policies.filter((policy) => typeof policy.id === "string" && ["allow", "deny"].includes(String(policy.decision))).map((policy) => ({
|
|
1419
|
+
value: String(policy.id),
|
|
1420
|
+
label: stripVTControlCharacters(String(policy.name ?? policy.id))
|
|
1421
|
+
}));
|
|
1422
|
+
const available = new Set(options.map((option) => option.value));
|
|
1423
|
+
selectedIds = selectedIds.filter((id) => available.has(id));
|
|
1424
|
+
const actions = [{
|
|
1425
|
+
value: create,
|
|
1426
|
+
label: "Create new",
|
|
1427
|
+
hint: "Set up a policy in Cloudflare, then return here"
|
|
1428
|
+
}, {
|
|
1429
|
+
value: refresh,
|
|
1430
|
+
label: "Refresh list"
|
|
1431
|
+
}];
|
|
1432
|
+
let choices;
|
|
1433
|
+
if (options.length === 0) {
|
|
1434
|
+
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.");
|
|
1435
|
+
choices = [answer$2(await select({
|
|
1436
|
+
message: "Company Access policies:",
|
|
1437
|
+
options: actions
|
|
1438
|
+
}))];
|
|
1439
|
+
} else choices = answer$2(await multiselect({
|
|
1440
|
+
message: "Company Access policies (select all that apply):",
|
|
1441
|
+
options: [...options, ...actions],
|
|
1442
|
+
initialValues: selectedIds,
|
|
1443
|
+
required: true
|
|
1444
|
+
}));
|
|
1445
|
+
selectedIds = choices.filter((id) => available.has(id));
|
|
1446
|
+
if (choices.includes(create)) {
|
|
1447
|
+
note([
|
|
1448
|
+
"1. Open Zero Trust → Access controls → Policies → Add a policy in this account.",
|
|
1449
|
+
"2. Name the policy and choose Allow.",
|
|
1450
|
+
"3. Include your company’s identity-provider group or Emails ending in your company domain, such as example.com.",
|
|
1451
|
+
"4. Require your selected company login method and your company’s MFA/device checks.",
|
|
1452
|
+
...mode === "existing" ? ["5. Save the policy and attach it to the existing Access applications you’re connecting, then return here."] : ["5. Save the policy, then return here."]
|
|
1453
|
+
].join("\n"), import_picocolors.default.bold("Create an Access policy"));
|
|
1454
|
+
console.log(platformSetupLink({
|
|
1455
|
+
title: "Open Cloudflare account",
|
|
1456
|
+
url: `https://dash.cloudflare.com/${encodeURIComponent(accountId)}`
|
|
1457
|
+
}));
|
|
1458
|
+
if (!answer$2(await confirm({
|
|
1459
|
+
message: "Have you saved the new policy?",
|
|
1460
|
+
initialValue: true
|
|
1461
|
+
}))) continue;
|
|
1462
|
+
} else if (!choices.includes(refresh)) return selectedIds;
|
|
1463
|
+
policies = await client.list("policies");
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1123
1466
|
function platformAccessResourceInventory(encoded, installationId) {
|
|
1124
1467
|
if (!encoded) return [];
|
|
1125
1468
|
const state = JSON.parse(encoded);
|
|
@@ -1144,6 +1487,10 @@ function checkpointPlatformAccessResources(manifest, encoded, updatedAt) {
|
|
|
1144
1487
|
}
|
|
1145
1488
|
function isPlatformAuthenticationPreparationKey(name) {
|
|
1146
1489
|
return name.startsWith("authenticationConnection:") || [
|
|
1490
|
+
"appAccessToken",
|
|
1491
|
+
"appAccessTokenRejected",
|
|
1492
|
+
"appAccessIdentityProviderIds",
|
|
1493
|
+
"appAccessPolicyIds",
|
|
1147
1494
|
"accessManagementToken",
|
|
1148
1495
|
"accessSetupSpecJson",
|
|
1149
1496
|
"accessExistingServiceTokenJson",
|
|
@@ -1189,7 +1536,7 @@ function parsePlatformAccessSetupSpec(input) {
|
|
|
1189
1536
|
}
|
|
1190
1537
|
return value;
|
|
1191
1538
|
}
|
|
1192
|
-
function answer$
|
|
1539
|
+
function answer$2(value) {
|
|
1193
1540
|
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
1194
1541
|
return value;
|
|
1195
1542
|
}
|
|
@@ -1217,22 +1564,32 @@ function activatePlatformAccessCredentials(encoded, origins) {
|
|
|
1217
1564
|
}
|
|
1218
1565
|
async function preparePlatformAccessSetup(input) {
|
|
1219
1566
|
if (!input.login && !input.protection) return;
|
|
1567
|
+
if (process.stdin.isTTY) {
|
|
1568
|
+
note([
|
|
1569
|
+
input.login && input.protection ? "Access lets employees sign in with your company login and controls who can reach the platform’s UI and APIs." : input.login ? "Access lets employees sign in to the platform with your company login." : "Access controls who can reach the platform’s UI and APIs.",
|
|
1570
|
+
"",
|
|
1571
|
+
"Void can create dedicated Access applications using your company’s existing login providers and policies, or use Access applications your company already manages."
|
|
1572
|
+
].join("\n"), import_picocolors.default.bold("Set up Cloudflare Access"));
|
|
1573
|
+
console.log(accessSetupGuide());
|
|
1574
|
+
}
|
|
1220
1575
|
let spec = input.spec ?? (input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : void 0);
|
|
1221
1576
|
if (!spec) {
|
|
1222
1577
|
if (!process.stdin.isTTY) {
|
|
1223
1578
|
if (input.protection) throw new Error("platform: Protection requires cloudflareAccess create or existing application verification in --auth-config.");
|
|
1224
1579
|
return;
|
|
1225
1580
|
}
|
|
1226
|
-
const mode = answer$
|
|
1581
|
+
const mode = answer$2(await select({
|
|
1227
1582
|
message: "Cloudflare Access setup:",
|
|
1228
1583
|
options: [
|
|
1229
1584
|
{
|
|
1230
1585
|
value: "create",
|
|
1231
|
-
label: "Create applications
|
|
1586
|
+
label: "Create Access applications for this platform",
|
|
1587
|
+
hint: "Use this account’s existing Zero Trust setup"
|
|
1232
1588
|
},
|
|
1233
1589
|
{
|
|
1234
1590
|
value: "existing",
|
|
1235
|
-
label: "
|
|
1591
|
+
label: "Use Access applications your company already manages",
|
|
1592
|
+
hint: "May use another account"
|
|
1236
1593
|
},
|
|
1237
1594
|
...!input.protection ? [{
|
|
1238
1595
|
value: "manual",
|
|
@@ -1243,12 +1600,19 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1243
1600
|
if (mode === "manual") return;
|
|
1244
1601
|
spec = { mode };
|
|
1245
1602
|
if (mode === "existing") {
|
|
1246
|
-
|
|
1603
|
+
log.info("Enter the account that owns your company’s Access configuration. This may differ from the account hosting Void.");
|
|
1604
|
+
spec.accountId = answer$2(await text$1({
|
|
1247
1605
|
message: "Cloudflare identity account ID:",
|
|
1248
1606
|
initialValue: input.plan.account.id
|
|
1249
1607
|
}));
|
|
1250
|
-
if (input.login)
|
|
1251
|
-
|
|
1608
|
+
if (input.login) {
|
|
1609
|
+
log.info("Enter the ID of the Access application that provides company sign-in to Void.");
|
|
1610
|
+
spec.loginApplicationId = answer$2(await text$1({ message: "Existing Access OIDC application ID:" }));
|
|
1611
|
+
}
|
|
1612
|
+
if (input.protection) {
|
|
1613
|
+
log.info("Enter the ID of the Access application that protects the platform’s UI and APIs.");
|
|
1614
|
+
spec.protectionApplicationId = answer$2(await text$1({ message: "Existing Access protection application ID:" }));
|
|
1615
|
+
}
|
|
1252
1616
|
}
|
|
1253
1617
|
}
|
|
1254
1618
|
spec = parsePlatformAccessSetupSpec(spec);
|
|
@@ -1265,8 +1629,9 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1265
1629
|
policies = await client.list("policies");
|
|
1266
1630
|
} catch (error) {
|
|
1267
1631
|
if (!(error instanceof PlatformCloudflareApiError) || ![401, 403].includes(error.status) || !process.stdin.isTTY || credential) throw error;
|
|
1268
|
-
|
|
1269
|
-
|
|
1632
|
+
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.`);
|
|
1633
|
+
console.log(accessSetupGuide());
|
|
1634
|
+
const token = answer$2(await password({ message: "Access setup API token:" }));
|
|
1270
1635
|
input.save({ accessManagementToken: token });
|
|
1271
1636
|
management = new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl });
|
|
1272
1637
|
client = new PlatformAccessClient(management, accountId);
|
|
@@ -1274,22 +1639,24 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1274
1639
|
providers = await client.list("identity_providers");
|
|
1275
1640
|
policies = await client.list("policies");
|
|
1276
1641
|
}
|
|
1277
|
-
|
|
1642
|
+
if (!spec.identityProviderIds && process.stdin.isTTY) log.info("Choose how employees sign in—for example, your company’s Okta or Google Workspace connection.");
|
|
1643
|
+
const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ? answer$2(await multiselect({
|
|
1278
1644
|
message: "Company identity providers:",
|
|
1279
1645
|
required: true,
|
|
1280
|
-
options: providers.filter((provider) => typeof provider.id === "string").map((provider) => ({
|
|
1281
|
-
value:
|
|
1282
|
-
label: stripVTControlCharacters(
|
|
1283
|
-
}))
|
|
1284
|
-
})) : []);
|
|
1285
|
-
const policyIds = spec.policyIds ?? (process.stdin.isTTY ? answer$1(await multiselect({
|
|
1286
|
-
message: "Existing company Access policies (MFA and device requirements are preserved):",
|
|
1287
|
-
required: true,
|
|
1288
|
-
options: policies.filter((policy) => typeof policy.id === "string" && ["allow", "deny"].includes(String(policy.decision))).map((policy) => ({
|
|
1289
|
-
value: String(policy.id),
|
|
1290
|
-
label: stripVTControlCharacters(String(policy.name ?? policy.id))
|
|
1646
|
+
options: providers.filter((provider) => typeof provider.id === "string").map(platformAccessIdentityProvider).map((provider) => ({
|
|
1647
|
+
value: provider.id,
|
|
1648
|
+
label: stripVTControlCharacters(provider.name)
|
|
1291
1649
|
}))
|
|
1292
1650
|
})) : []);
|
|
1651
|
+
if (identityProviderIds.length) {
|
|
1652
|
+
spec = {
|
|
1653
|
+
...spec,
|
|
1654
|
+
identityProviderIds
|
|
1655
|
+
};
|
|
1656
|
+
input.save({ accessSetupSpecJson: JSON.stringify(spec) });
|
|
1657
|
+
}
|
|
1658
|
+
if (!spec.policyIds && process.stdin.isTTY) log.info("Choose which existing access rules apply to this platform, including MFA and device requirements.");
|
|
1659
|
+
const policyIds = spec.policyIds ?? (process.stdin.isTTY ? await selectCompanyAccessPolicies(client, policies, accountId, spec.mode) : []);
|
|
1293
1660
|
if (!identityProviderIds.length || !policyIds.length) throw new Error("platform: Access setup requires explicit identityProviderIds and policyIds.");
|
|
1294
1661
|
spec = {
|
|
1295
1662
|
...spec,
|
|
@@ -1306,10 +1673,11 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1306
1673
|
if (!serviceToken && spec.mode === "existing" && input.protection) {
|
|
1307
1674
|
if (input.checkpoint.accessExistingServiceTokenJson) serviceToken = JSON.parse(input.checkpoint.accessExistingServiceTokenJson);
|
|
1308
1675
|
else if (process.stdin.isTTY) {
|
|
1676
|
+
log.info("This lets the installer reach the protected platform during setup. Use a service token allowed by the existing Access application.");
|
|
1309
1677
|
serviceToken = {
|
|
1310
|
-
id: answer$
|
|
1311
|
-
clientId: answer$
|
|
1312
|
-
clientSecret: answer$
|
|
1678
|
+
id: answer$2(await text$1({ message: "Existing admitted Access service token ID:" })),
|
|
1679
|
+
clientId: answer$2(await password({ message: "Access service token client ID:" })),
|
|
1680
|
+
clientSecret: answer$2(await password({ message: "Access service token client secret:" }))
|
|
1313
1681
|
};
|
|
1314
1682
|
input.save({ accessExistingServiceTokenJson: JSON.stringify(serviceToken) });
|
|
1315
1683
|
}
|
|
@@ -1330,7 +1698,7 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1330
1698
|
};
|
|
1331
1699
|
console.log(JSON.stringify({ accessPlan: reviewed }, null, 2));
|
|
1332
1700
|
if (!input.yes) {
|
|
1333
|
-
if (!process.stdin.isTTY || !answer$
|
|
1701
|
+
if (!process.stdin.isTTY || !answer$2(await confirm({
|
|
1334
1702
|
message: "Apply this Access configuration?",
|
|
1335
1703
|
initialValue: false
|
|
1336
1704
|
}))) throw new Error("platform: Access setup changes require confirmation or --yes.");
|
|
@@ -1344,7 +1712,8 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1344
1712
|
};
|
|
1345
1713
|
let loginClientSecret = spec.loginClientSecretEnv ? secretReference(spec.loginClientSecretEnv) : input.checkpoint.accessExistingLoginSecret;
|
|
1346
1714
|
if (input.login && spec.mode === "existing" && !loginClientSecret && process.stdin.isTTY) {
|
|
1347
|
-
|
|
1715
|
+
log.info("Enter the client secret from the existing Access login application’s OIDC settings.");
|
|
1716
|
+
loginClientSecret = answer$2(await password({ message: "Existing Access OIDC client secret:" }));
|
|
1348
1717
|
input.save({ accessExistingLoginSecret: loginClientSecret });
|
|
1349
1718
|
}
|
|
1350
1719
|
const result = await provisionPlatformAccess(client, {
|
|
@@ -1376,13 +1745,34 @@ async function preparePlatformAccessSetup(input) {
|
|
|
1376
1745
|
}
|
|
1377
1746
|
//#endregion
|
|
1378
1747
|
//#region src/cli/platform-auth.ts
|
|
1379
|
-
|
|
1380
|
-
|
|
1748
|
+
/** Check Access inventory permissions before the installer starts provisioning. */
|
|
1749
|
+
async function verifyPlatformManagementAccess(client, accountId, access) {
|
|
1750
|
+
const base = `/accounts/${encodeURIComponent(accountId)}/access`;
|
|
1751
|
+
await Promise.all([client.request(`${base}/organizations`), ...[
|
|
1752
|
+
"identity_providers",
|
|
1753
|
+
"groups",
|
|
1754
|
+
"policies",
|
|
1755
|
+
"apps",
|
|
1756
|
+
...access.protection ? ["service_tokens"] : []
|
|
1757
|
+
].map((resource) => client.request(`${base}/${resource}?per_page=1`))]);
|
|
1758
|
+
}
|
|
1759
|
+
function assertPlatformOperatorCredentialCanApplyPlan(credential, plan, access) {
|
|
1760
|
+
if (!platformOperatorNeedsManagementToken(credential, plan, access)) return;
|
|
1381
1761
|
const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
|
|
1382
1762
|
const writesDns = plan.actions.some((action) => action.kind === "dns-record" && action.action !== "reuse");
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1763
|
+
const permissions = [
|
|
1764
|
+
...createsZone ? ["Account → Zone → Edit"] : [],
|
|
1765
|
+
...writesDns ? ["Zone → DNS → Edit"] : [],
|
|
1766
|
+
...access ? [
|
|
1767
|
+
`Account → Access: Apps and Policies → ${access.readOnly ? "Read" : "Edit"}`,
|
|
1768
|
+
"Account → Access: Organizations, Identity Providers, and Groups → Read",
|
|
1769
|
+
...access.protection ? [`Account → Access: Service Tokens → ${access.readOnly ? "Read" : "Edit"}`] : []
|
|
1770
|
+
] : []
|
|
1771
|
+
];
|
|
1772
|
+
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.`);
|
|
1773
|
+
}
|
|
1774
|
+
function platformOperatorNeedsManagementToken(credential, plan, access) {
|
|
1775
|
+
return credential.source === "oauth" && (!!access || plan.actions.some((action) => action.kind === "zone" && action.action === "create" || action.kind === "dns-record" && action.action !== "reuse"));
|
|
1386
1776
|
}
|
|
1387
1777
|
async function resolvePlatformCloudflareCredential(dependencies = {}) {
|
|
1388
1778
|
const { credential } = await resolvePlatformCloudflareCredentialWithInput(async () => void 0, dependencies);
|
|
@@ -1417,9 +1807,9 @@ async function resolvePlatformCloudflareCredentialWithInput(readInput, dependenc
|
|
|
1417
1807
|
input,
|
|
1418
1808
|
credential: result.credential
|
|
1419
1809
|
};
|
|
1420
|
-
const login = await runWrangler(
|
|
1810
|
+
const login = await runWrangler({ type: "auth.login" }, false);
|
|
1421
1811
|
assertNotCancelled();
|
|
1422
|
-
if (login.status !== 0) throw new Error("platform: Cloudflare browser login did not complete.");
|
|
1812
|
+
if (login.status !== 0 || login.signal || login.error) throw new Error("platform: Cloudflare browser login did not complete.");
|
|
1423
1813
|
const credential = await readWranglerOAuthCredential(runWrangler);
|
|
1424
1814
|
assertNotCancelled();
|
|
1425
1815
|
if (!credential) throw new Error("platform: Cloudflare sign-in completed without a usable OAuth credential.");
|
|
@@ -1438,19 +1828,20 @@ async function readExistingCredential(dependencies, runWrangler) {
|
|
|
1438
1828
|
source: "environment",
|
|
1439
1829
|
token: environmentToken
|
|
1440
1830
|
};
|
|
1831
|
+
const savedManagementToken = dependencies.savedManagementToken?.trim();
|
|
1832
|
+
if (savedManagementToken) return {
|
|
1833
|
+
source: "saved",
|
|
1834
|
+
token: savedManagementToken
|
|
1835
|
+
};
|
|
1441
1836
|
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.");
|
|
1442
1837
|
return await readWranglerOAuthCredential(runWrangler);
|
|
1443
1838
|
}
|
|
1444
1839
|
async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflareAuthCommand(process.cwd(), args)) {
|
|
1445
|
-
const result = await runWrangler(
|
|
1446
|
-
|
|
1447
|
-
"token",
|
|
1448
|
-
"--json"
|
|
1449
|
-
], true);
|
|
1450
|
-
if (result.status !== 0) return null;
|
|
1840
|
+
const result = await runWrangler({ type: "auth.token" }, true);
|
|
1841
|
+
if (result.status !== 0 || result.signal || result.error) return null;
|
|
1451
1842
|
try {
|
|
1452
|
-
const parsed =
|
|
1453
|
-
if (parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
1843
|
+
const parsed = result.data;
|
|
1844
|
+
if (!parsed || parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
|
|
1454
1845
|
return {
|
|
1455
1846
|
source: "oauth",
|
|
1456
1847
|
token: parsed.token.trim()
|
|
@@ -1460,6 +1851,116 @@ async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflare
|
|
|
1460
1851
|
}
|
|
1461
1852
|
}
|
|
1462
1853
|
//#endregion
|
|
1854
|
+
//#region src/cli/platform-access-health.ts
|
|
1855
|
+
/** These reserved endpoints require Void's probe token and cannot serve application traffic. */
|
|
1856
|
+
function platformAccessHealthTargets(plan) {
|
|
1857
|
+
const destination = `${plan.deployment.application.zoneName ? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${plan.deployment.application.baseDomain}` : `${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev`}/health`;
|
|
1858
|
+
return [{
|
|
1859
|
+
key: `health-dispatch-${createHash("sha256").update(destination).digest("hex").slice(0, 12)}`,
|
|
1860
|
+
destination
|
|
1861
|
+
}];
|
|
1862
|
+
}
|
|
1863
|
+
async function verifyHealthException(access, app, destination) {
|
|
1864
|
+
const destinations = platformAccessPublicDestinations(app);
|
|
1865
|
+
if (typeof app.id !== "string" || !app.id || app.type !== "self_hosted" || app.domain !== destination || !destinations.length || destinations.some((value) => value !== destination) || app.destinations !== void 0 && (!Array.isArray(app.destinations) || app.destinations.some((value) => !value || value.type !== "public" || value.uri !== destination)) || app.target_criteria != null && (!Array.isArray(app.target_criteria) || app.target_criteria.length > 0)) throw new Error(`platform: The Access health exception must cover only ${destination}.`);
|
|
1866
|
+
const policies = await access.list(`apps/${encodeURIComponent(app.id)}/policies`);
|
|
1867
|
+
const policy = policies[0];
|
|
1868
|
+
if (policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== "[{\"everyone\":{}}]" || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) throw new Error(`platform: The Access policy for ${destination} does not match the reserved health-check exception.`);
|
|
1869
|
+
}
|
|
1870
|
+
/** Keep the same path-only exception used by project Zero Trust available during bootstrap. */
|
|
1871
|
+
async function ensurePlatformAccessHealthChecks(input) {
|
|
1872
|
+
const appAccess = !input.manifest.steps.some((step) => step.key === "app-access-initialized:v1") && input.checkpoint.appAccessSetupJson ? parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson)) : null;
|
|
1873
|
+
if (appAccess && appAccess.accountId !== input.plan.account.id) throw new Error("platform: App access recovery belongs to another Cloudflare account.");
|
|
1874
|
+
if (!input.plan.deployment.authentication.accessProtection && !appAccess?.managed) return;
|
|
1875
|
+
const spec = appAccess?.managed ? {
|
|
1876
|
+
mode: "create",
|
|
1877
|
+
accountId: input.plan.account.id
|
|
1878
|
+
} : parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson || "null"));
|
|
1879
|
+
const accountId = input.plan.account.id;
|
|
1880
|
+
const identityAccountId = spec.accountId ?? accountId;
|
|
1881
|
+
const token = appAccess?.managed ? appAccess.apiToken : identityAccountId === accountId ? process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN || input.checkpoint.accessManagementToken : void 0;
|
|
1882
|
+
if (!token && !input.client.request) throw new Error("platform: Access management is required to configure the reserved health endpoints.");
|
|
1883
|
+
const access = new PlatformAccessClient(token ? new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }) : { request: input.client.request.bind(input.client) }, accountId);
|
|
1884
|
+
const nonce = createHash("sha256").update(`void-access-health:${input.manifest.ownershipNonce}`).digest("hex").slice(0, 32);
|
|
1885
|
+
const state = input.checkpoint.accessHealthProvisioningJson ? JSON.parse(input.checkpoint.accessHealthProvisioningJson) : {
|
|
1886
|
+
version: 1,
|
|
1887
|
+
accountId,
|
|
1888
|
+
installationId: input.manifest.id,
|
|
1889
|
+
nonce,
|
|
1890
|
+
resources: {}
|
|
1891
|
+
};
|
|
1892
|
+
if (!state || state.version !== 1 || state.accountId !== accountId || input.manifest.account.id !== accountId || state.installationId !== input.manifest.id || state.installationId !== input.plan.installationId || state.nonce !== nonce || !state.resources || typeof state.resources !== "object" || Array.isArray(state.resources)) throw new Error("platform: Access health recovery data belongs to a different installation.");
|
|
1893
|
+
const save = () => input.save(JSON.stringify(state));
|
|
1894
|
+
const inventory = await access.list("apps");
|
|
1895
|
+
for (const { key, destination } of platformAccessHealthTargets(input.plan)) {
|
|
1896
|
+
const expected = {
|
|
1897
|
+
type: "self_hosted",
|
|
1898
|
+
domain: destination,
|
|
1899
|
+
destinations: [{
|
|
1900
|
+
type: "public",
|
|
1901
|
+
uri: destination
|
|
1902
|
+
}],
|
|
1903
|
+
policies: [{
|
|
1904
|
+
name: "Void platform health check",
|
|
1905
|
+
decision: "bypass",
|
|
1906
|
+
precedence: 1,
|
|
1907
|
+
include: [{ everyone: {} }]
|
|
1908
|
+
}]
|
|
1909
|
+
};
|
|
1910
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(expected)).digest("hex");
|
|
1911
|
+
let recorded = state.resources[key];
|
|
1912
|
+
const name = `void-${input.manifest.id}-${key}`.slice(0, 90) + "-" + nonce.slice(0, 16);
|
|
1913
|
+
if (recorded && (recorded.name !== name || recorded.fingerprint !== fingerprint || recorded.adopted)) throw new Error("platform: The recorded Access health exception changed identity or scope.");
|
|
1914
|
+
const matching = inventory.filter((app) => platformAccessPublicDestinations(app).includes(destination));
|
|
1915
|
+
if (matching.length > 1 || recorded?.id && matching.some((app) => app.id !== recorded.id)) throw new Error(`platform: Multiple Access applications cover ${destination}. Reconcile them before resuming.`);
|
|
1916
|
+
if (!recorded && matching.length === 1) {
|
|
1917
|
+
await verifyHealthException(access, matching[0], destination);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (!recorded?.id && recorded?.pending) {
|
|
1921
|
+
const candidates = inventory.filter((app) => app.name === name);
|
|
1922
|
+
if (candidates.length !== 1 || typeof candidates[0]?.id !== "string") throw new Error("platform: Access health application creation has an uncertain outcome. Inspect Cloudflare before retrying; no duplicate was created.");
|
|
1923
|
+
recorded.id = candidates[0].id;
|
|
1924
|
+
save();
|
|
1925
|
+
}
|
|
1926
|
+
if (!recorded?.id) {
|
|
1927
|
+
if (spec.mode !== "create") throw new Error(`platform: Configure an Access exception limited to ${destination}, then resume. Void's private probe token still protects this endpoint.`);
|
|
1928
|
+
if (inventory.some((app) => app.name === name)) throw new Error("platform: An Access health application with this installation identity already exists. Inspect it before resuming.");
|
|
1929
|
+
recorded = {
|
|
1930
|
+
name,
|
|
1931
|
+
fingerprint,
|
|
1932
|
+
pending: true
|
|
1933
|
+
};
|
|
1934
|
+
state.resources[key] = recorded;
|
|
1935
|
+
save();
|
|
1936
|
+
try {
|
|
1937
|
+
const created = await access.create("apps", {
|
|
1938
|
+
...expected,
|
|
1939
|
+
name
|
|
1940
|
+
});
|
|
1941
|
+
if (!created || typeof created.id !== "string" || !created.id) throw new Error("platform: Cloudflare did not return the Access health application ID.");
|
|
1942
|
+
recorded.id = created.id;
|
|
1943
|
+
save();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (error instanceof PlatformCloudflareApiError && error.status >= 400 && error.status < 500 && ![
|
|
1946
|
+
408,
|
|
1947
|
+
409,
|
|
1948
|
+
429
|
|
1949
|
+
].includes(error.status)) {
|
|
1950
|
+
recorded.pending = false;
|
|
1951
|
+
save();
|
|
1952
|
+
}
|
|
1953
|
+
throw error;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
const app = await access.get(`apps/${encodeURIComponent(recorded.id)}`);
|
|
1957
|
+
if (!app || app.id !== recorded.id || app.name !== recorded.name) throw new Error("platform: The Access health application changed ownership identity.");
|
|
1958
|
+
await verifyHealthException(access, app, destination);
|
|
1959
|
+
recorded.pending = false;
|
|
1960
|
+
save();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
//#endregion
|
|
1463
1964
|
//#region src/cli/platform-access-preflight.ts
|
|
1464
1965
|
/** Recheck live protection without invoking any provisioning or configuration write. */
|
|
1465
1966
|
async function verifyInstalledPlatformAccessProtection(input) {
|
|
@@ -1490,6 +1991,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1490
1991
|
if ([
|
|
1491
1992
|
input.plan.apiUrl,
|
|
1492
1993
|
input.plan.proxyUrl,
|
|
1994
|
+
...input.dashboardUrl ? [input.dashboardUrl] : [],
|
|
1493
1995
|
...input.manifest.endpoints.dashboardUrl ? [input.manifest.endpoints.dashboardUrl] : []
|
|
1494
1996
|
].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.");
|
|
1495
1997
|
for (const other of await access.list("apps")) {
|
|
@@ -1505,7 +2007,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1505
2007
|
}
|
|
1506
2008
|
const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
|
|
1507
2009
|
if (existingWorkers.length) {
|
|
1508
|
-
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-
|
|
2010
|
+
const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-BLadnnct.mjs");
|
|
1509
2011
|
await verifyProtectionWorkerOrigins({
|
|
1510
2012
|
request: input.client.request.bind(input.client),
|
|
1511
2013
|
listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
|
|
@@ -1514,8 +2016,243 @@ async function verifyInstalledPlatformAccessProtection(input) {
|
|
|
1514
2016
|
}
|
|
1515
2017
|
}
|
|
1516
2018
|
//#endregion
|
|
1517
|
-
//#region src/cli/platform-
|
|
1518
|
-
|
|
2019
|
+
//#region src/cli/platform-app-access.ts
|
|
2020
|
+
function answer$1(value) {
|
|
2021
|
+
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
2022
|
+
return value;
|
|
2023
|
+
}
|
|
2024
|
+
async function selectInstallationAppAccess(authentication, recorded, choice) {
|
|
2025
|
+
const config = authentication.file?.appAccess;
|
|
2026
|
+
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.");
|
|
2027
|
+
if (choice && config?.mode && choice !== config.mode) throw new Error("platform: --app-access conflicts with the authentication file’s app access choice.");
|
|
2028
|
+
const supplied = choice ?? config?.mode;
|
|
2029
|
+
if (supplied !== void 0 && ![
|
|
2030
|
+
"public",
|
|
2031
|
+
"platform",
|
|
2032
|
+
"custom"
|
|
2033
|
+
].includes(supplied)) throw new Error("platform: App access must be public, platform, or custom.");
|
|
2034
|
+
if (recorded) {
|
|
2035
|
+
if (supplied !== void 0 && supplied !== recorded.appAccess) throw new Error("platform: Resume with the saved app access choice. Manage an installed platform’s app access in Admin → Zero Trust.");
|
|
2036
|
+
return recorded.appAccess;
|
|
2037
|
+
}
|
|
2038
|
+
const protectedPlatform = !!authentication.authentication.accessProtection;
|
|
2039
|
+
const selected = supplied ?? (!process.stdin.isTTY ? protectedPlatform ? "platform" : "public" : answer$1(await select({
|
|
2040
|
+
message: "Who should be able to access deployed apps?",
|
|
2041
|
+
initialValue: protectedPlatform ? "platform" : "public",
|
|
2042
|
+
options: [
|
|
2043
|
+
...protectedPlatform ? [{
|
|
2044
|
+
value: "platform",
|
|
2045
|
+
label: "Same people allowed by the platform’s Access policy",
|
|
2046
|
+
hint: "recommended"
|
|
2047
|
+
}] : [],
|
|
2048
|
+
{
|
|
2049
|
+
value: "public",
|
|
2050
|
+
label: "Anyone — public apps"
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
value: "custom",
|
|
2054
|
+
label: protectedPlatform ? "Choose a different Access policy" : "Set up Cloudflare Access protection"
|
|
2055
|
+
}
|
|
2056
|
+
]
|
|
2057
|
+
})));
|
|
2058
|
+
if (selected === "public" && (config?.identityProviderIds?.length || config?.policyIds?.length)) throw new Error("platform: Public apps cannot select identity policies. Choose custom app access to require company login.");
|
|
2059
|
+
if (selected === "platform" && !protectedPlatform) throw new Error("platform: Reusing the platform Access policy requires --access-protection. Choose --app-access custom to protect apps separately.");
|
|
2060
|
+
if (![
|
|
2061
|
+
"public",
|
|
2062
|
+
"platform",
|
|
2063
|
+
"custom"
|
|
2064
|
+
].includes(String(selected))) throw new Error("platform: Choose who can access deployed apps.");
|
|
2065
|
+
return selected;
|
|
2066
|
+
}
|
|
2067
|
+
/** Collect the hosting-account policy once, with explicit consent for a durable management token. */
|
|
2068
|
+
async function prepareInstallationAppAccess(input) {
|
|
2069
|
+
const choice = input.plan.deployment.appAccess;
|
|
2070
|
+
if (!choice) throw new Error("platform: App access was not selected for this installation.");
|
|
2071
|
+
const explicitToken = (input.authentication.file?.appAccess?.tokenEnv ? process.env[input.authentication.file.appAccess.tokenEnv] : void 0) || process.env.VOID_PLATFORM_APP_ACCESS_TOKEN;
|
|
2072
|
+
if (explicitToken && /\s/.test(explicitToken)) throw new Error("platform: App access tokens must not contain whitespace.");
|
|
2073
|
+
if (input.checkpoint.appAccessSetupJson && !input.checkpoint.appAccessTokenRejected) {
|
|
2074
|
+
const saved = parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson));
|
|
2075
|
+
if (saved.accountId !== input.plan.account.id || saved.mode !== (choice === "public" ? "public" : "protected")) throw new Error("platform: Saved app access belongs to another account or choice.");
|
|
2076
|
+
if (saved.managed) {
|
|
2077
|
+
const refreshed = explicitToken ? JSON.stringify({
|
|
2078
|
+
...saved,
|
|
2079
|
+
apiToken: explicitToken
|
|
2080
|
+
}) : input.checkpoint.appAccessSetupJson;
|
|
2081
|
+
input.save({ appAccessSetupJson: refreshed });
|
|
2082
|
+
return refreshed;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const file = input.authentication.file?.appAccess;
|
|
2086
|
+
if (file?.tokenEnv && !/^[A-Z_][A-Z0-9_]*$/.test(file.tokenEnv)) throw new Error("platform: Invalid app access token environment reference.");
|
|
2087
|
+
let token = explicitToken || (input.checkpoint.appAccessTokenRejected ? void 0 : input.checkpoint.appAccessToken);
|
|
2088
|
+
if (input.checkpoint.appAccessTokenRejected) log.info("The previous app access token was rejected. Provide a replacement to continue.");
|
|
2089
|
+
const setupSpec = input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : input.authentication.file?.cloudflareAccess;
|
|
2090
|
+
const setupAccount = setupSpec?.accountId ?? input.plan.account.id;
|
|
2091
|
+
const reusable = setupAccount === input.plan.account.id ? process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN || input.checkpoint.accessManagementToken || input.managementToken : input.managementToken;
|
|
2092
|
+
const tokenPage = () => openPlatformSetupPage({
|
|
2093
|
+
title: "App access API token",
|
|
2094
|
+
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
2095
|
+
to: `/${input.plan.account.id}/api-tokens`,
|
|
2096
|
+
name: `${input.plan.deployment.displayName} App Access`,
|
|
2097
|
+
permissionGroupKeys: JSON.stringify([{
|
|
2098
|
+
key: "access",
|
|
2099
|
+
type: "edit"
|
|
2100
|
+
}, {
|
|
2101
|
+
key: "access_acct",
|
|
2102
|
+
type: "read"
|
|
2103
|
+
}])
|
|
2104
|
+
}),
|
|
2105
|
+
instructions: `Scope the token to ${input.plan.account.name}: Access Apps and Policies Edit; Organizations, Identity Providers, and Groups Read. Void stores it encrypted to manage app access.`
|
|
2106
|
+
});
|
|
2107
|
+
const collectToken = async () => {
|
|
2108
|
+
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.");
|
|
2109
|
+
tokenPage();
|
|
2110
|
+
const value = String(answer$1(await password({ message: "App access API token (stored encrypted when app rules are needed):" }))).trim();
|
|
2111
|
+
if (!value || /\s/.test(value)) throw new Error("platform: An app access token without whitespace is required.");
|
|
2112
|
+
input.save({
|
|
2113
|
+
appAccessToken: value,
|
|
2114
|
+
appAccessTokenRejected: ""
|
|
2115
|
+
});
|
|
2116
|
+
return value;
|
|
2117
|
+
};
|
|
2118
|
+
if (!token && input.checkpoint.appAccessTokenRejected) token = await collectToken();
|
|
2119
|
+
let access = new PlatformAccessClient(token ? new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }) : input.client, input.plan.account.id);
|
|
2120
|
+
let organization;
|
|
2121
|
+
const readOrganization = async () => {
|
|
2122
|
+
try {
|
|
2123
|
+
return await access.get("organizations");
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
if (error instanceof PlatformCloudflareApiError && error.status === 404) return null;
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
try {
|
|
2130
|
+
organization = await readOrganization();
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
if (!(error instanceof PlatformCloudflareApiError) || ![401, 403].includes(error.status)) throw error;
|
|
2133
|
+
log.info("Check this account’s Access requirements before opening app traffic.");
|
|
2134
|
+
token = await collectToken();
|
|
2135
|
+
access = new PlatformAccessClient(new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }), input.plan.account.id);
|
|
2136
|
+
organization = await readOrganization();
|
|
2137
|
+
}
|
|
2138
|
+
const defaultDeny = organization?.deny_unmatched_requests === true;
|
|
2139
|
+
if (choice === "public" && !defaultDeny) {
|
|
2140
|
+
const result = JSON.stringify({
|
|
2141
|
+
version: 1,
|
|
2142
|
+
accountId: input.plan.account.id,
|
|
2143
|
+
mode: "public",
|
|
2144
|
+
managed: false,
|
|
2145
|
+
identityProviderIds: [],
|
|
2146
|
+
policyIds: []
|
|
2147
|
+
});
|
|
2148
|
+
input.save({ appAccessSetupJson: result });
|
|
2149
|
+
return result;
|
|
2150
|
+
}
|
|
2151
|
+
if (!organization) throw new Error(`platform: Enable Zero Trust in account ${input.plan.account.name}, then resume to configure app access.`);
|
|
2152
|
+
if (!token) {
|
|
2153
|
+
log.info("Void needs an Access-management token stored encrypted to manage access for future apps and custom domains.");
|
|
2154
|
+
if (reusable && process.stdin.isTTY && answer$1(await confirm({
|
|
2155
|
+
message: "Reuse the setup token for ongoing app access management?",
|
|
2156
|
+
initialValue: true
|
|
2157
|
+
}))) {
|
|
2158
|
+
token = reusable;
|
|
2159
|
+
input.save({ appAccessToken: token });
|
|
2160
|
+
} else token = await collectToken();
|
|
2161
|
+
access = new PlatformAccessClient(new PlatformCloudflareClient(token, { apiBase: input.client.apiBaseUrl }), input.plan.account.id);
|
|
2162
|
+
}
|
|
2163
|
+
let identityProviderIds = [];
|
|
2164
|
+
let policyIds = [];
|
|
2165
|
+
if (choice !== "public") {
|
|
2166
|
+
const reuse = choice === "platform" && setupAccount === input.plan.account.id;
|
|
2167
|
+
if (choice === "platform" && !reuse) log.info("The platform uses Access in another account. Choose equivalent identities and policies in the hosting account for apps.");
|
|
2168
|
+
identityProviderIds = input.checkpoint.appAccessIdentityProviderIds ? JSON.parse(input.checkpoint.appAccessIdentityProviderIds) : (reuse ? setupSpec?.identityProviderIds : void 0) ?? file?.identityProviderIds ?? [];
|
|
2169
|
+
policyIds = input.checkpoint.appAccessPolicyIds ? JSON.parse(input.checkpoint.appAccessPolicyIds) : (reuse ? setupSpec?.policyIds : void 0) ?? file?.policyIds ?? [];
|
|
2170
|
+
if (!identityProviderIds.length) {
|
|
2171
|
+
if (!process.stdin.isTTY) throw new Error("platform: Set appAccess.identityProviderIds in --auth-config for the hosting account.");
|
|
2172
|
+
const providers = (await access.list("identity_providers")).map(platformAccessIdentityProvider);
|
|
2173
|
+
if (!providers.length) throw new Error(`platform: Add an identity provider in Zero Trust for ${input.plan.account.name}, then resume.`);
|
|
2174
|
+
identityProviderIds = answer$1(await multiselect({
|
|
2175
|
+
message: "App identity providers:",
|
|
2176
|
+
required: true,
|
|
2177
|
+
options: providers.map((p) => ({
|
|
2178
|
+
value: p.id,
|
|
2179
|
+
label: stripVTControlCharacters(p.name)
|
|
2180
|
+
}))
|
|
2181
|
+
}));
|
|
2182
|
+
input.save({ appAccessIdentityProviderIds: JSON.stringify(identityProviderIds) });
|
|
2183
|
+
}
|
|
2184
|
+
if (!policyIds.length) {
|
|
2185
|
+
if (!process.stdin.isTTY) throw new Error("platform: Set appAccess.policyIds in --auth-config for the hosting account.");
|
|
2186
|
+
policyIds = await selectCompanyAccessPolicies(access, await access.list("policies"), input.plan.account.id, "create");
|
|
2187
|
+
input.save({ appAccessPolicyIds: JSON.stringify(policyIds) });
|
|
2188
|
+
}
|
|
2189
|
+
} else log.info("This account requires Access. Void will create public rules scoped to app hostnames.");
|
|
2190
|
+
const setup = parsePlatformAppAccessSetup({
|
|
2191
|
+
version: 1,
|
|
2192
|
+
accountId: input.plan.account.id,
|
|
2193
|
+
mode: choice === "public" ? "public" : "protected",
|
|
2194
|
+
managed: true,
|
|
2195
|
+
apiToken: token,
|
|
2196
|
+
identityProviderIds,
|
|
2197
|
+
policyIds
|
|
2198
|
+
});
|
|
2199
|
+
if (setup.mode === "protected") try {
|
|
2200
|
+
await inspectPlatformAccess(access, setup);
|
|
2201
|
+
} catch (error) {
|
|
2202
|
+
if (error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) input.save({ appAccessTokenRejected: "true" });
|
|
2203
|
+
else input.save({
|
|
2204
|
+
appAccessIdentityProviderIds: "",
|
|
2205
|
+
appAccessPolicyIds: ""
|
|
2206
|
+
});
|
|
2207
|
+
throw error;
|
|
2208
|
+
}
|
|
2209
|
+
const result = JSON.stringify(setup);
|
|
2210
|
+
input.save({
|
|
2211
|
+
appAccessSetupJson: result,
|
|
2212
|
+
appAccessIdentityProviderIds: JSON.stringify(identityProviderIds),
|
|
2213
|
+
appAccessPolicyIds: JSON.stringify(policyIds),
|
|
2214
|
+
appAccessTokenRejected: ""
|
|
2215
|
+
});
|
|
2216
|
+
return result;
|
|
2217
|
+
}
|
|
2218
|
+
var PlatformAppAccessCredentialError = class extends Error {};
|
|
2219
|
+
/** Bounded, resumable handoff to the service that owns Zero Trust state. */
|
|
2220
|
+
async function initializeInstalledAppAccess(input, fetcher = fetch) {
|
|
2221
|
+
parsePlatformAppAccessSetup(JSON.parse(input.setupJson));
|
|
2222
|
+
if (!input.setupToken || input.setupToken.length < 32) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
|
|
2223
|
+
const signature = createHmac("sha256", input.setupToken).update(`${PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX}\n${input.installationId}\n${input.setupJson}`).digest("base64url");
|
|
2224
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2225
|
+
const response = await fetcher(new URL("/internal/platform/app-access", input.apiUrl), {
|
|
2226
|
+
method: "POST",
|
|
2227
|
+
headers: {
|
|
2228
|
+
"Content-Type": "application/json",
|
|
2229
|
+
"X-Void-Platform-Probe": input.probeToken,
|
|
2230
|
+
"X-Void-App-Access-Setup": signature,
|
|
2231
|
+
...cfAccessHeaders(process.env, input.apiUrl)
|
|
2232
|
+
},
|
|
2233
|
+
body: input.setupJson,
|
|
2234
|
+
redirect: "manual",
|
|
2235
|
+
signal: AbortSignal.timeout(6e4)
|
|
2236
|
+
});
|
|
2237
|
+
if (response.status === 403) {
|
|
2238
|
+
const detail = await response.clone().text();
|
|
2239
|
+
if (/1050|Default-Deny/i.test(detail)) throw new Error(`platform: Account Default-Deny blocks the platform API at ${input.apiUrl}. Configure Access for the platform API and its installer credentials, then resume.`);
|
|
2240
|
+
}
|
|
2241
|
+
let body;
|
|
2242
|
+
try {
|
|
2243
|
+
body = await response.json();
|
|
2244
|
+
} catch {
|
|
2245
|
+
throw new Error(`platform: App access setup did not respond (HTTP ${response.status}). Check platform Access permissions and resume.`);
|
|
2246
|
+
}
|
|
2247
|
+
if (body.code === "app_access_credentials") throw new PlatformAppAccessCredentialError("App access token rejected. Resume installation to provide a replacement.");
|
|
2248
|
+
if (!response.ok || body.installationId !== input.installationId) throw new Error(`platform: ${body.error || "App access setup failed."} Resume installation after correcting the Access configuration.`);
|
|
2249
|
+
if (body.ready) return;
|
|
2250
|
+
}
|
|
2251
|
+
throw new Error("platform: App access setup is safely checkpointed. Resume installation to finish before app traffic opens.");
|
|
2252
|
+
}
|
|
2253
|
+
//#endregion
|
|
2254
|
+
//#region src/cli/platform-auth-bootstrap.ts
|
|
2255
|
+
/** Checkpoint a replacement before submission so an interrupted response is safely replayable. */
|
|
1519
2256
|
async function resumeInstalledPlatformAuthentication(input, initialize = initializeInstalledPlatformAuthentication) {
|
|
1520
2257
|
const result = await initialize(input.apiUrl, input.installationId, input.setupToken, input.setupJson);
|
|
1521
2258
|
if (!result.setupPending || !result.expiresAt || Date.parse(result.expiresAt) > Date.now()) return result;
|
|
@@ -1558,6 +2295,26 @@ async function initializeInstalledPlatformAuthentication(apiUrl, installationId,
|
|
|
1558
2295
|
}
|
|
1559
2296
|
//#endregion
|
|
1560
2297
|
//#region src/cli/platform-auth-database.ts
|
|
2298
|
+
/** Restore the selected external origin before an adoption receipt exists. */
|
|
2299
|
+
function readPlatformHyperdriveSelection(checkpoint) {
|
|
2300
|
+
if (!checkpoint?.externalHyperdriveJson) return;
|
|
2301
|
+
let value;
|
|
2302
|
+
try {
|
|
2303
|
+
value = JSON.parse(checkpoint.externalHyperdriveJson);
|
|
2304
|
+
} catch {
|
|
2305
|
+
throw new Error("platform: The saved Hyperdrive selection is invalid.");
|
|
2306
|
+
}
|
|
2307
|
+
const selected = value;
|
|
2308
|
+
const nonempty = (value) => typeof value === "string" && value.trim() !== "";
|
|
2309
|
+
if (!selected || typeof selected !== "object" || Array.isArray(selected) || !nonempty(selected.id) || !nonempty(selected.host) || !nonempty(selected.user) || selected.name !== void 0 && (typeof selected.name !== "string" || !selected.name) || selected.database !== void 0 && (typeof selected.database !== "string" || !selected.database)) throw new Error("platform: The saved Hyperdrive selection is invalid.");
|
|
2310
|
+
return {
|
|
2311
|
+
id: selected.id,
|
|
2312
|
+
host: selected.host,
|
|
2313
|
+
user: selected.user,
|
|
2314
|
+
...selected.name ? { name: selected.name } : {},
|
|
2315
|
+
...selected.database ? { database: selected.database } : {}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
1561
2318
|
/** Read the database name from an owner URL without applying Hyperdrive origin restrictions. */
|
|
1562
2319
|
function platformDatabaseNameFromOwnerUrl(databaseUrl) {
|
|
1563
2320
|
let url;
|
|
@@ -1598,6 +2355,7 @@ async function verifyPlatformAuthenticationDatabase(client, accountId, resource,
|
|
|
1598
2355
|
//#endregion
|
|
1599
2356
|
//#region src/cli/platform-plan.ts
|
|
1600
2357
|
async function createPlatformInstallPlan(client, input, existing, options) {
|
|
2358
|
+
const dashboardUrl = input.dashboardUrl !== void 0 ? normalizePlatformDashboardUrl(input.dashboardUrl) : existing ? existing.deployment.controlPlane.dashboardUrl ?? (existing.endpoints.dashboardUrl ? normalizePlatformDashboardUrl(existing.endpoints.dashboardUrl) : null) ?? platformDashboardUrl(existing.deployment) : null;
|
|
1601
2359
|
const account = resolveAccount(await client.listAccounts(), input.accountId);
|
|
1602
2360
|
if (existing && existing.account.id !== account.id) throw new Error(`platform: Refusing to resume installation ${existing.id} in a different Cloudflare account.`);
|
|
1603
2361
|
const providedEmailSharedZoneId = input.emailSharedZoneId?.trim();
|
|
@@ -1634,8 +2392,10 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1634
2392
|
...enableEmail ? { email: true } : {}
|
|
1635
2393
|
};
|
|
1636
2394
|
if (capabilities.email && options?.includeEmailGateway !== false && !enableEmail) throw new Error("platform: Email-enabled installations require VOID_EMAIL_SENDER_DOMAIN and VOID_EMAIL_SHARED_ZONE_ID before deploying this runtime.");
|
|
2395
|
+
if (input.appAccess && input.appAccess !== "public" && input.controlPlaneDomain.toLowerCase().replace(/\.$/, "").endsWith(`.${input.applicationDomain.toLowerCase()}`)) throw new Error("platform: The API hostname must be outside the app Access wildcard. Use the application domain apex or workers.dev for the API, then plan again.");
|
|
1637
2396
|
const deployment = createPlatformDeploymentManifest({
|
|
1638
2397
|
authentication: input.authentication ?? existing?.deployment.authentication,
|
|
2398
|
+
appAccess: input.appAccess ?? existing?.deployment.appAccess,
|
|
1639
2399
|
...Object.keys(capabilities).length > 0 ? { capabilities } : {},
|
|
1640
2400
|
application: {
|
|
1641
2401
|
baseDomain: testing ? `${accountSubdomain}.workers.dev` : input.applicationDomain,
|
|
@@ -1646,7 +2406,8 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1646
2406
|
},
|
|
1647
2407
|
controlPlane: {
|
|
1648
2408
|
apiHostname: input.controlPlaneDomain || null,
|
|
1649
|
-
dashboardHostname: null,
|
|
2409
|
+
dashboardHostname: dashboardUrl ? new URL(dashboardUrl).hostname : null,
|
|
2410
|
+
...dashboardUrl ? { dashboardUrl } : {},
|
|
1650
2411
|
proxyHostname: null
|
|
1651
2412
|
},
|
|
1652
2413
|
displayName: input.displayName,
|
|
@@ -1664,13 +2425,21 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1664
2425
|
sandbox: true
|
|
1665
2426
|
}
|
|
1666
2427
|
} : existingWithoutEmailZone;
|
|
1667
|
-
const
|
|
2428
|
+
const existingWithDashboard = {
|
|
1668
2429
|
...existingWithSandbox,
|
|
2430
|
+
controlPlane: {
|
|
2431
|
+
...existingWithSandbox.controlPlane,
|
|
2432
|
+
dashboardHostname: deployment.controlPlane.dashboardHostname,
|
|
2433
|
+
...deployment.controlPlane.dashboardUrl ? { dashboardUrl: deployment.controlPlane.dashboardUrl } : {}
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
const comparableExistingWithoutEmailZone = existing && deployment.capabilities.email && !existing.deployment.capabilities.email ? {
|
|
2437
|
+
...existingWithDashboard,
|
|
1669
2438
|
capabilities: {
|
|
1670
|
-
...
|
|
2439
|
+
...existingWithDashboard.capabilities,
|
|
1671
2440
|
email: true
|
|
1672
2441
|
}
|
|
1673
|
-
} :
|
|
2442
|
+
} : existingWithDashboard;
|
|
1674
2443
|
if (existing && (JSON.stringify(comparableExistingWithoutEmailZone) !== JSON.stringify(deploymentWithoutEmailZone) || recordedEmailZone !== void 0 && recordedEmailZone !== configuredEmailZone || recordedSenderDomain !== void 0 && recordedSenderDomain !== configuredSenderDomain)) throw new Error(`platform: --resume inputs do not match installation ${existing.id}. Use its recorded configuration.`);
|
|
1675
2444
|
const names = deployment.resources;
|
|
1676
2445
|
const wildcardHostname = `*.${deployment.application.baseDomain}`;
|
|
@@ -1694,7 +2463,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1694
2463
|
}
|
|
1695
2464
|
throw error;
|
|
1696
2465
|
});
|
|
1697
|
-
const [workersSubdomain, aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
|
|
2466
|
+
const [workersSubdomain, aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, deploymentQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
|
|
1698
2467
|
Promise.resolve(accountSubdomain),
|
|
1699
2468
|
gatewayInventory,
|
|
1700
2469
|
client.listKvNamespaces(account.id, names.routingKv),
|
|
@@ -1703,6 +2472,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1703
2472
|
client.listR2Buckets(account.id),
|
|
1704
2473
|
client.listQueues(account.id, names.usageQueue),
|
|
1705
2474
|
client.listQueues(account.id, names.cronQueue),
|
|
2475
|
+
options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue") ? client.listQueues(account.id, names.deploymentQueue) : Promise.resolve([]),
|
|
1706
2476
|
client.listDispatchNamespaces(account.id),
|
|
1707
2477
|
client.listHyperdriveConfigs(account.id),
|
|
1708
2478
|
client.listWorkerNames(account.id)
|
|
@@ -1737,11 +2507,13 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1737
2507
|
const expectedPatterns = new Set(createApplicationRoutes(deployment.application).map((route) => route.pattern));
|
|
1738
2508
|
const expectedWorker = deployment.resources.dispatchWorker;
|
|
1739
2509
|
const ownedWorker = intentProofs.has("dispatch-worker") || existing?.resources.some((resource) => resource.key === "dispatch-worker" && resource.kind === "worker" && resource.name === expectedWorker && resource.state === "ready");
|
|
1740
|
-
const
|
|
2510
|
+
const [workerRoutes, workerDomains] = await Promise.all([client.listWorkerRoutes(zone.id), client.listWorkerDomains(account.id)]);
|
|
2511
|
+
const conflicts = workerRoutes.filter((route) => expectedPatterns.has(route.pattern) && (route.script !== expectedWorker || !ownedWorker));
|
|
1741
2512
|
if (conflicts.length > 0) {
|
|
1742
2513
|
const conflict = conflicts[0];
|
|
1743
2514
|
throw new Error(`platform: Application route ${JSON.stringify(conflict.pattern)} is already assigned to ${JSON.stringify(conflict.script ?? "no Worker")}. Choose another application domain.`);
|
|
1744
2515
|
}
|
|
2516
|
+
assertApplicationRoutesPreserveWorkerDomains([...expectedPatterns], expectedWorker, ownedWorker === true, zone.name, workerRoutes, workerDomains);
|
|
1745
2517
|
}
|
|
1746
2518
|
if (deployment.controlPlane.apiHostname) {
|
|
1747
2519
|
const apiHostname = deployment.controlPlane.apiHostname;
|
|
@@ -1784,6 +2556,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1784
2556
|
addNamedPlan(actions, existing, "assets-bucket", "r2-bucket", names.assetsBucket, r2Buckets.filter((resource) => resource.name === names.assetsBucket), "reuse", options, remoteOwnership, intentProofs, ambiguousResources);
|
|
1785
2557
|
addNamedPlan(actions, existing, "usage-queue", "queue", names.usageQueue, usageQueues, "reuse", options, remoteOwnership);
|
|
1786
2558
|
addNamedPlan(actions, existing, "cron-queue", "queue", names.cronQueue, cronQueues, "reuse", options, remoteOwnership);
|
|
2559
|
+
if (options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue")) addNamedPlan(actions, existing, "deployment-queue", "queue", names.deploymentQueue, deploymentQueues, "reuse", options, remoteOwnership);
|
|
1787
2560
|
addNamedPlan(actions, existing, "dispatch-namespace", "dispatch-namespace", names.dispatchNamespace, dispatchNamespaces.filter((resource) => resource.name === names.dispatchNamespace), "reuse", options, remoteOwnership);
|
|
1788
2561
|
const recordedHyperdrive = existing?.resources.find((resource) => resource.key === "hyperdrive");
|
|
1789
2562
|
const externalHyperdriveId = input.externalHyperdrive?.id || (recordedHyperdrive?.ownership === "adopted" ? recordedHyperdrive.id : "");
|
|
@@ -1828,6 +2601,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
|
|
|
1828
2601
|
actions,
|
|
1829
2602
|
apiUrl,
|
|
1830
2603
|
deployment: resolvedDeployment,
|
|
2604
|
+
...resolvedDeployment.appAccess ? { appAccessSetupRequired: !existing?.steps.some((step) => step.key === "app-access-initialized:v1") } : {},
|
|
1831
2605
|
...input.externalHyperdrive ? { externalHyperdrive: input.externalHyperdrive } : {},
|
|
1832
2606
|
installationId,
|
|
1833
2607
|
proxyUrl,
|
|
@@ -1861,7 +2635,8 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
|
|
|
1861
2635
|
}, manifest, {
|
|
1862
2636
|
...options,
|
|
1863
2637
|
includeEmailGateway: original.actions.some((entry) => entry.key === "email-gateway-worker"),
|
|
1864
|
-
enableSandbox: original.deployment.capabilities.sandbox
|
|
2638
|
+
enableSandbox: original.deployment.capabilities.sandbox,
|
|
2639
|
+
deploymentExecution: original.actions.some((entry) => entry.key === "deployment-queue")
|
|
1865
2640
|
})).actions.find((entry) => entry.key === action.key);
|
|
1866
2641
|
if (!current || current.kind !== action.kind || current.name !== action.name || current.action !== action.action) throw new Error(`platform: Cloudflare changed after the installation plan was approved for ${action.kind} ${JSON.stringify(action.name)}. Review a new plan before retrying.`);
|
|
1867
2642
|
if (action.kind === "worker" && action.action === "update") {
|
|
@@ -2055,6 +2830,7 @@ const RESOURCE_NAME_KEY_BY_ACTION = {
|
|
|
2055
2830
|
"api-worker": "apiWorker",
|
|
2056
2831
|
"assets-bucket": "assetsBucket",
|
|
2057
2832
|
"cron-queue": "cronQueue",
|
|
2833
|
+
"deployment-queue": "deploymentQueue",
|
|
2058
2834
|
"dispatch-namespace": "dispatchNamespace",
|
|
2059
2835
|
"dispatch-worker": "dispatchWorker",
|
|
2060
2836
|
hyperdrive: "hyperdrive",
|
|
@@ -2066,6 +2842,33 @@ const RESOURCE_NAME_KEY_BY_ACTION = {
|
|
|
2066
2842
|
"tail-worker": "tailWorker",
|
|
2067
2843
|
"usage-queue": "usageQueue"
|
|
2068
2844
|
};
|
|
2845
|
+
function assertApplicationRoutesPreserveWorkerDomains(patterns, dispatchWorker, ownedDispatchWorker, zoneName, workerRoutes, workerDomains) {
|
|
2846
|
+
const plannedRoutes = patterns.map(parseWorkerRoutePattern);
|
|
2847
|
+
const existingRoutes = workerRoutes.map((route) => ({
|
|
2848
|
+
...parseWorkerRoutePattern(route.pattern),
|
|
2849
|
+
script: route.script
|
|
2850
|
+
}));
|
|
2851
|
+
for (const domain of workerDomains) {
|
|
2852
|
+
const hostname = domain.hostname.toLowerCase();
|
|
2853
|
+
if (!(domain.zoneName ? domain.zoneName.toLowerCase() === zoneName.toLowerCase() : hostname === zoneName.toLowerCase() || hostname.endsWith(`.${zoneName.toLowerCase()}`)) || domain.service === dispatchWorker && ownedDispatchWorker) continue;
|
|
2854
|
+
for (const planned of plannedRoutes) {
|
|
2855
|
+
if (!workerRouteHostMatches(planned.host, hostname)) continue;
|
|
2856
|
+
if (!["http", "https"].every((scheme) => existingRoutes.some((route) => route.script !== dispatchWorker && route.path === "/*" && (!route.scheme || route.scheme === scheme) && workerRouteHostMatches(route.host, hostname) && route.host.replace(/^\*/, "").length > planned.host.replace(/^\*/, "").length))) throw new Error(`platform: Application route ${JSON.stringify(planned.pattern)} would intercept Worker custom domain ${JSON.stringify(domain.hostname)}, currently served by ${JSON.stringify(domain.service)}. Choose another application domain, or use \`void platform install --workers-dev\` for a new installation and add a suitable domain later.`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
function parseWorkerRoutePattern(pattern) {
|
|
2861
|
+
const match = /^(?:(https?):\/\/)?([^/]+)(\/.*)?$/.exec(pattern);
|
|
2862
|
+
return {
|
|
2863
|
+
host: match?.[2]?.toLowerCase() ?? "",
|
|
2864
|
+
path: match?.[3] ?? "/",
|
|
2865
|
+
pattern,
|
|
2866
|
+
scheme: match?.[1] ?? null
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function workerRouteHostMatches(host, hostname) {
|
|
2870
|
+
return host.startsWith("*") ? hostname.endsWith(host.slice(1)) : host === hostname;
|
|
2871
|
+
}
|
|
2069
2872
|
function platformProvisionIntentHash(manifest, key, kind, name, previousResourceId = null) {
|
|
2070
2873
|
return createHash("sha256").update(JSON.stringify({
|
|
2071
2874
|
key,
|
|
@@ -2101,16 +2904,21 @@ async function readRemoteOwnership(client, accountId, routingNamespaces, existin
|
|
|
2101
2904
|
}
|
|
2102
2905
|
function formatPlatformInstallPlan(plan) {
|
|
2103
2906
|
const lines = [
|
|
2104
|
-
`${import_picocolors
|
|
2105
|
-
|
|
2106
|
-
`${import_picocolors
|
|
2107
|
-
`${import_picocolors
|
|
2108
|
-
`${import_picocolors
|
|
2109
|
-
`${import_picocolors
|
|
2110
|
-
`${import_picocolors
|
|
2111
|
-
|
|
2112
|
-
...plan.deployment.
|
|
2113
|
-
|
|
2907
|
+
`${import_picocolors.default.bold(`${plan.deployment.displayName} in ${plan.account.name}`)}`,
|
|
2908
|
+
`${import_picocolors.default.bold("Account:")} ${plan.account.id}`,
|
|
2909
|
+
plan.deployment.application.zoneName ? `${import_picocolors.default.bold("Applications:")} *.${plan.deployment.application.baseDomain}` : `${import_picocolors.default.bold("Applications:")} workers.dev for testing. Add a domain with void platform domain set <domain>`,
|
|
2910
|
+
`${import_picocolors.default.bold("Control plane:")} ${plan.apiUrl}`,
|
|
2911
|
+
`${import_picocolors.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
|
|
2912
|
+
...platformDashboardUrl(plan.deployment) ? [`${import_picocolors.default.bold("Dashboard:")} ${platformDashboardUrl(plan.deployment)} (deploy separately)`] : [],
|
|
2913
|
+
`${import_picocolors.default.bold("Login methods:")} ${plan.deployment.authentication.providers.join(", ")}`,
|
|
2914
|
+
`${import_picocolors.default.bold("Access protection:")} ${plan.deployment.authentication.accessProtection ? "Enabled" : "Not enabled"}`,
|
|
2915
|
+
...plan.deployment.appAccess ? [plan.appAccessSetupRequired === false ? `${import_picocolors.default.bold("App access:")} Preserve administrator settings` : `${import_picocolors.default.bold("Default app access:")} ${{
|
|
2916
|
+
public: "Public apps",
|
|
2917
|
+
platform: "Same as platform Access policy",
|
|
2918
|
+
custom: "Selected Access policy"
|
|
2919
|
+
}[plan.deployment.appAccess]}`] : [],
|
|
2920
|
+
`${import_picocolors.default.bold("Login callback:")} ${plan.apiUrl}/auth/callback`,
|
|
2921
|
+
`${import_picocolors.default.bold("Database:")} ${plan.externalHyperdrive?.database || "PostgreSQL"} via Hyperdrive ${plan.deployment.resources.hyperdrive}`,
|
|
2114
2922
|
""
|
|
2115
2923
|
];
|
|
2116
2924
|
for (const action of plan.actions) lines.push(`${action.action.padEnd(6)} ${action.kind.padEnd(19)} ${action.name}`);
|
|
@@ -2600,12 +3408,17 @@ function createPlatformInstallationSecrets(input) {
|
|
|
2600
3408
|
});
|
|
2601
3409
|
return {
|
|
2602
3410
|
authenticationPreparation: Object.fromEntries(Object.entries(checkpoint).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
3411
|
+
...checkpoint.appAccessSetupToken ? { appAccessSetupToken: checkpoint.appAccessSetupToken } : {},
|
|
3412
|
+
...checkpoint.appAccessSetupJson ? { appAccessSetupJson: checkpoint.appAccessSetupJson } : {},
|
|
3413
|
+
...checkpoint.managementApiToken ? { managementApiToken: checkpoint.managementApiToken } : {},
|
|
3414
|
+
...checkpoint.externalHyperdriveJson ? { externalHyperdriveJson: checkpoint.externalHyperdriveJson } : {},
|
|
2603
3415
|
...modernAuthentication ? {
|
|
2604
3416
|
authenticationSetupJson,
|
|
2605
3417
|
authSetupToken,
|
|
2606
3418
|
authBootstrapCode
|
|
2607
3419
|
} : {},
|
|
2608
3420
|
...checkpoint.accessProvisioningJson ? { accessProvisioningJson: checkpoint.accessProvisioningJson } : {},
|
|
3421
|
+
...checkpoint.accessHealthProvisioningJson ? { accessHealthProvisioningJson: checkpoint.accessHealthProvisioningJson } : {},
|
|
2609
3422
|
...checkpoint.accessCredentialsJson ? { accessCredentialsJson: checkpoint.accessCredentialsJson } : {},
|
|
2610
3423
|
workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
|
|
2611
3424
|
pendingWorkerSecretUpdateIntent: input.updateExistingWorkers ? pendingWorkerSecretUpdateIntent : void 0,
|
|
@@ -2675,6 +3488,10 @@ function assertCanonicalProjectSecretKey(value, name, uniqueToInstallation = fal
|
|
|
2675
3488
|
}
|
|
2676
3489
|
function secretCheckpoint(secrets, workerSecretUpdateIntent) {
|
|
2677
3490
|
return {
|
|
3491
|
+
...secrets.appAccessSetupToken ? { appAccessSetupToken: secrets.appAccessSetupToken } : {},
|
|
3492
|
+
...secrets.appAccessSetupJson ? { appAccessSetupJson: secrets.appAccessSetupJson } : {},
|
|
3493
|
+
...secrets.managementApiToken ? { managementApiToken: secrets.managementApiToken } : {},
|
|
3494
|
+
...secrets.externalHyperdriveJson ? { externalHyperdriveJson: secrets.externalHyperdriveJson } : {},
|
|
2678
3495
|
...Object.fromEntries(Object.entries(secrets.authenticationPreparation ?? {}).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
|
|
2679
3496
|
...secrets.authSetupToken ? {
|
|
2680
3497
|
authenticationSetupJson: secrets.authenticationSetupJson ?? "",
|
|
@@ -2682,6 +3499,7 @@ function secretCheckpoint(secrets, workerSecretUpdateIntent) {
|
|
|
2682
3499
|
authBootstrapCode: secrets.authBootstrapCode ?? ""
|
|
2683
3500
|
} : {},
|
|
2684
3501
|
...secrets.accessProvisioningJson ? { accessProvisioningJson: secrets.accessProvisioningJson } : {},
|
|
3502
|
+
...secrets.accessHealthProvisioningJson ? { accessHealthProvisioningJson: secrets.accessHealthProvisioningJson } : {},
|
|
2685
3503
|
...secrets.accessCredentialsJson ? { accessCredentialsJson: secrets.accessCredentialsJson } : {},
|
|
2686
3504
|
bootstrapGitHubId: secrets.bootstrapGitHubId,
|
|
2687
3505
|
bootstrapGitHubLogin: secrets.bootstrapGitHubLogin,
|
|
@@ -2933,11 +3751,12 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
2933
3751
|
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.");
|
|
2934
3752
|
const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
2935
3753
|
const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl ?? resolvePinnedApiBaseUrl();
|
|
2936
|
-
const runPinnedPlatformWrangler = (
|
|
2937
|
-
const runParallelPinnedPlatformWrangler = (
|
|
3754
|
+
const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl);
|
|
3755
|
+
const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true);
|
|
2938
3756
|
const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
|
|
2939
3757
|
const runtimeDirectory = resolvedRuntime.directory;
|
|
2940
3758
|
const runtime = resolvedRuntime.manifest;
|
|
3759
|
+
if (plan.deployment.appAccess && runtime.appAccessSetup !== 1) throw new Error("platform: This runtime does not support the installation’s app access setup.");
|
|
2941
3760
|
const hasEmailGatewayArtifact = runtime.artifacts.emailGateway !== void 0;
|
|
2942
3761
|
if (runtime.emailProtocolVersion !== 1 && plan.deployment.capabilities.email) throw new Error("platform: This runtime cannot serve enabled platform email without email protocol version 1.");
|
|
2943
3762
|
if (runtime.emailProtocolVersion !== 1 && existing?.resources.some((resource) => resource.key === "email-gateway-worker" && resource.state === "ready")) throw new Error("platform: This runtime cannot replace an installed email gateway Worker. Use a runtime with email protocol version 1.");
|
|
@@ -3030,6 +3849,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3030
3849
|
parsePostgresConnectionString(secrets.databaseUrl);
|
|
3031
3850
|
assertVoidCreatedHyperdriveTrustsDatabase(secrets.databaseUrl);
|
|
3032
3851
|
}
|
|
3852
|
+
if (plan.externalHyperdrive) secrets = {
|
|
3853
|
+
...secrets,
|
|
3854
|
+
externalHyperdriveJson: JSON.stringify(plan.externalHyperdrive)
|
|
3855
|
+
};
|
|
3033
3856
|
const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
|
|
3034
3857
|
writePendingInstallationSecrets(manifest, {
|
|
3035
3858
|
...pendingSecrets,
|
|
@@ -3070,10 +3893,6 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3070
3893
|
throw error;
|
|
3071
3894
|
}
|
|
3072
3895
|
if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
|
|
3073
|
-
let workerDeploymentSecrets = {
|
|
3074
|
-
...secrets,
|
|
3075
|
-
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
3076
|
-
};
|
|
3077
3896
|
try {
|
|
3078
3897
|
assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
|
|
3079
3898
|
if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
|
|
@@ -3152,6 +3971,12 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3152
3971
|
manifest = transitionInstallation(manifest, "provisioning", now());
|
|
3153
3972
|
writeInstallationManifest(manifest);
|
|
3154
3973
|
}
|
|
3974
|
+
if (runtime.deploymentExecution !== 1 && (manifest.deploymentExecution === 1 || dependencies.databaseRollbackSource?.manifest.deploymentExecution === 1)) {
|
|
3975
|
+
if (!releaseDatabaseLock.checkpoint) throw new Error("platform: The database lifecycle lock cannot fence deployment admission.");
|
|
3976
|
+
await releaseDatabaseLock.checkpoint(manifest);
|
|
3977
|
+
dependencies.onProgress?.("Verifying durable deployment operations have drained");
|
|
3978
|
+
await (dependencies.verifyDeploymentExecutionDrain ?? verifyPlatformDeploymentExecutionDrain)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce), manifest.id);
|
|
3979
|
+
}
|
|
3155
3980
|
if (dependencies.prepareAuthentication) {
|
|
3156
3981
|
dependencies.onProgress?.("Preparing platform authentication");
|
|
3157
3982
|
try {
|
|
@@ -3186,15 +4011,47 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3186
4011
|
activateAccessCredentials();
|
|
3187
4012
|
assertRequiredPlatformSecrets(plan, manifest, secrets);
|
|
3188
4013
|
if (secrets.authenticationSetupJson && createHash("sha256").update(secrets.authBootstrapCode ?? "").digest("hex") !== parsePlatformAuthenticationSetup(JSON.parse(secrets.authenticationSetupJson)).bootstrapCredentialHash) throw new Error("platform: Prepared authentication does not match its administrator setup code.");
|
|
3189
|
-
workerDeploymentSecrets = {
|
|
3190
|
-
...secrets,
|
|
3191
|
-
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
3192
|
-
};
|
|
3193
4014
|
writePendingInstallationSecrets(manifest, {
|
|
3194
4015
|
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
3195
4016
|
databaseClaimState: "claimed"
|
|
3196
4017
|
});
|
|
3197
4018
|
}
|
|
4019
|
+
const appAccessSetupRequired = !!plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
4020
|
+
if (appAccessSetupRequired) {
|
|
4021
|
+
if (dependencies.prepareAppAccess) try {
|
|
4022
|
+
const appAccessSetupJson = await dependencies.prepareAppAccess(manifest, secrets);
|
|
4023
|
+
const latest = readPendingInstallationSecrets(manifest) ?? {};
|
|
4024
|
+
secrets = {
|
|
4025
|
+
...secrets,
|
|
4026
|
+
appAccessSetupJson,
|
|
4027
|
+
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
|
|
4028
|
+
};
|
|
4029
|
+
writePendingInstallationSecrets(manifest, {
|
|
4030
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4031
|
+
databaseClaimState: "claimed"
|
|
4032
|
+
});
|
|
4033
|
+
} catch (error) {
|
|
4034
|
+
const latest = readPendingInstallationSecrets(manifest) ?? {};
|
|
4035
|
+
secrets = {
|
|
4036
|
+
...secrets,
|
|
4037
|
+
appAccessSetupJson: latest.appAccessSetupJson || secrets.appAccessSetupJson,
|
|
4038
|
+
authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
|
|
4039
|
+
};
|
|
4040
|
+
throw error;
|
|
4041
|
+
}
|
|
4042
|
+
if (!secrets.appAccessSetupJson) throw new Error("platform: Restore the encrypted app access setup checkpoint before resuming.");
|
|
4043
|
+
if (!secrets.appAccessSetupToken) {
|
|
4044
|
+
if (manifest.resources.some((resource) => resource.key === "api-worker" && resource.state === "ready")) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
|
|
4045
|
+
secrets = {
|
|
4046
|
+
...secrets,
|
|
4047
|
+
appAccessSetupToken: randomBytes(32).toString("hex")
|
|
4048
|
+
};
|
|
4049
|
+
writePendingInstallationSecrets(manifest, {
|
|
4050
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4051
|
+
databaseClaimState: "claimed"
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
3198
4055
|
if (plan.deployment.authentication.accessProtection) {
|
|
3199
4056
|
const protection = liveAuthentication ? liveAuthentication.protection : secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(secrets.authenticationSetupJson)).protection : null;
|
|
3200
4057
|
if (!protection) throw new Error("platform: The active Access protection configuration is unavailable. Restore its database and encrypted setup checkpoint before maintenance.");
|
|
@@ -3203,10 +4060,49 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3203
4060
|
client: dependencies.client,
|
|
3204
4061
|
manifest,
|
|
3205
4062
|
plan,
|
|
4063
|
+
dashboardUrl: platformDashboardUrl(plan.deployment),
|
|
3206
4064
|
protection,
|
|
3207
4065
|
checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent)
|
|
3208
4066
|
});
|
|
3209
4067
|
}
|
|
4068
|
+
if (plan.deployment.authentication.accessProtection || appAccessSetupRequired && secrets.appAccessSetupJson && JSON.parse(secrets.appAccessSetupJson).managed === true) {
|
|
4069
|
+
dependencies.onProgress?.("Preparing Access for authenticated platform health checks");
|
|
4070
|
+
try {
|
|
4071
|
+
await (dependencies.prepareHealthAccess ?? ensurePlatformAccessHealthChecks)({
|
|
4072
|
+
client: dependencies.client,
|
|
4073
|
+
manifest,
|
|
4074
|
+
plan,
|
|
4075
|
+
checkpoint: secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4076
|
+
save: (accessHealthProvisioningJson) => {
|
|
4077
|
+
secrets = {
|
|
4078
|
+
...secrets,
|
|
4079
|
+
accessHealthProvisioningJson
|
|
4080
|
+
};
|
|
4081
|
+
manifest = checkpointAuthenticationResources(manifest, secrets, now());
|
|
4082
|
+
writePendingInstallationSecrets(manifest, {
|
|
4083
|
+
...secretCheckpoint(secrets, workerSecretUpdateIntent),
|
|
4084
|
+
databaseClaimState: "claimed"
|
|
4085
|
+
});
|
|
4086
|
+
writeInstallationManifest(manifest);
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
} catch (error) {
|
|
4090
|
+
if (appAccessSetupRequired && secrets.appAccessSetupJson && JSON.parse(secrets.appAccessSetupJson).managed && error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) {
|
|
4091
|
+
secrets = {
|
|
4092
|
+
...secrets,
|
|
4093
|
+
authenticationPreparation: {
|
|
4094
|
+
...secrets.authenticationPreparation,
|
|
4095
|
+
appAccessTokenRejected: "true"
|
|
4096
|
+
}
|
|
4097
|
+
};
|
|
4098
|
+
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
4099
|
+
throw new PlatformAppAccessCredentialError("platform: App access token rejected while preparing health checks. Resume installation to provide a replacement.");
|
|
4100
|
+
}
|
|
4101
|
+
throw error;
|
|
4102
|
+
} finally {
|
|
4103
|
+
await releaseDatabaseLock.checkpoint?.(manifest);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
3210
4106
|
const routingAction = plan.actions.find((action) => action.key === "routing-kv");
|
|
3211
4107
|
if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
|
|
3212
4108
|
if (routingAction.action === "reuse") {
|
|
@@ -3270,11 +4166,6 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3270
4166
|
if (targetSandboxDrainProtocol >= 1 && !prepareSandboxDrain) throw new Error("platform: The PostgreSQL lifecycle lock cannot establish the sandbox-drain safety checkpoint.");
|
|
3271
4167
|
const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 3 ? 3 : targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
|
|
3272
4168
|
if (sandboxDrainPreparation && sandboxDrainPreparation.admissionProtocol < targetSandboxDrainProtocol) throw new Error("platform: The database did not arm the Sandbox admission protocol required by this runtime.");
|
|
3273
|
-
const sandboxDrainRoutingKvId = requireId(Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id])), "routing-kv");
|
|
3274
|
-
const writeSandboxDrainMirror = async (preparation) => {
|
|
3275
|
-
await dependencies.client.putKvValue(plan.account.id, sandboxDrainRoutingKvId, PLATFORM_SANDBOX_DRAIN_KEY, preparation.mirrorValue);
|
|
3276
|
-
};
|
|
3277
|
-
if (sandboxDrainPreparation) await writeSandboxDrainMirror(sandboxDrainPreparation);
|
|
3278
4169
|
const sandboxDrainState = sandboxDrainPreparation?.state ?? "complete";
|
|
3279
4170
|
if (targetSandboxDrainProtocol >= 1 && (manifest.safetyProtocols?.sandboxDrain ?? 0) < targetSandboxDrainProtocol) {
|
|
3280
4171
|
manifest = {
|
|
@@ -3313,7 +4204,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3313
4204
|
const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
|
|
3314
4205
|
const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
|
|
3315
4206
|
const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
|
|
3316
|
-
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain;
|
|
4207
|
+
const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain || !!plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1");
|
|
3317
4208
|
if (requiresTrafficGuard) {
|
|
3318
4209
|
await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
|
|
3319
4210
|
workerTrafficGuarded = true;
|
|
@@ -3330,9 +4221,11 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3330
4221
|
}
|
|
3331
4222
|
const configInput = {
|
|
3332
4223
|
authenticationDatabaseUncached: true,
|
|
4224
|
+
proxyHealthBinding: true,
|
|
3333
4225
|
accountId: plan.account.id,
|
|
3334
4226
|
apiUrl: plan.apiUrl,
|
|
3335
4227
|
deployment: plan.deployment,
|
|
4228
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
3336
4229
|
healthProbeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
3337
4230
|
installationId: manifest.id,
|
|
3338
4231
|
ownershipNonce: manifest.ownershipNonce,
|
|
@@ -3349,6 +4242,10 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3349
4242
|
runtimeEntrypoints: entrypoints,
|
|
3350
4243
|
...runtime.assets ? { runtimeApiAssetsDirectory: join(runtimeDirectory, PLATFORM_ADMIN_ASSETS_DIRECTORY) } : {}
|
|
3351
4244
|
};
|
|
4245
|
+
const workerDeploymentSecrets = {
|
|
4246
|
+
...secrets,
|
|
4247
|
+
workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
|
|
4248
|
+
};
|
|
3352
4249
|
const requiresStagedCommit = !preserveDisabled && requiresTrafficGuard;
|
|
3353
4250
|
const enabledConfigs = createPlatformWorkerConfigs({
|
|
3354
4251
|
...configInput,
|
|
@@ -3544,7 +4441,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3544
4441
|
} else for (const key of workerKeys) await deployOneWorker(key);
|
|
3545
4442
|
manifest = setInstallationEndpoints(manifest, {
|
|
3546
4443
|
apiUrl: plan.apiUrl,
|
|
3547
|
-
dashboardUrl:
|
|
4444
|
+
dashboardUrl: platformDashboardUrl(plan.deployment),
|
|
3548
4445
|
proxyUrl: plan.proxyUrl
|
|
3549
4446
|
}, now());
|
|
3550
4447
|
if (preserveDisabled) {
|
|
@@ -3555,6 +4452,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3555
4452
|
manifest = {
|
|
3556
4453
|
...manifest,
|
|
3557
4454
|
deployment: plan.deployment,
|
|
4455
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
3558
4456
|
platformVersion: version,
|
|
3559
4457
|
runtime: runtimeProvenance,
|
|
3560
4458
|
updatedAt: now(),
|
|
@@ -3614,6 +4512,37 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3614
4512
|
writeInstallationManifest(manifest);
|
|
3615
4513
|
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
3616
4514
|
}
|
|
4515
|
+
if (plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1")) {
|
|
4516
|
+
dependencies.onProgress?.("Configuring and verifying deployed-app access");
|
|
4517
|
+
try {
|
|
4518
|
+
await (dependencies.initializeAppAccess ?? initializeInstalledAppAccess)({
|
|
4519
|
+
apiUrl: plan.apiUrl,
|
|
4520
|
+
installationId: manifest.id,
|
|
4521
|
+
probeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
|
|
4522
|
+
setupJson: secrets.appAccessSetupJson,
|
|
4523
|
+
setupToken: secrets.appAccessSetupToken
|
|
4524
|
+
});
|
|
4525
|
+
} catch (error) {
|
|
4526
|
+
if (error instanceof PlatformAppAccessCredentialError) {
|
|
4527
|
+
secrets = {
|
|
4528
|
+
...secrets,
|
|
4529
|
+
authenticationPreparation: {
|
|
4530
|
+
...secrets.authenticationPreparation,
|
|
4531
|
+
appAccessTokenRejected: "true"
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
|
|
4535
|
+
}
|
|
4536
|
+
throw error;
|
|
4537
|
+
}
|
|
4538
|
+
manifest = recordInstallationStep(manifest, {
|
|
4539
|
+
key: "app-access-initialized:v1",
|
|
4540
|
+
completedAt: now(),
|
|
4541
|
+
resultHash: hash(secrets.appAccessSetupJson)
|
|
4542
|
+
}, now());
|
|
4543
|
+
writeInstallationManifest(manifest);
|
|
4544
|
+
await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
|
|
4545
|
+
}
|
|
3617
4546
|
dependencies.onProgress?.("Verifying platform health");
|
|
3618
4547
|
const emailGatewayUrl = hasEmailGatewayArtifact && plan.deployment.capabilities.email ? `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev` : void 0;
|
|
3619
4548
|
if (plan.deployment.authentication.accessProtection) {
|
|
@@ -3622,6 +4551,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3622
4551
|
}
|
|
3623
4552
|
const healthCheckContext = {
|
|
3624
4553
|
accountName: plan.account.name,
|
|
4554
|
+
...plan.deployment.authentication.accessProtection ? { accessProtected: true } : {},
|
|
3625
4555
|
workers: [
|
|
3626
4556
|
plan.deployment.resources.apiWorker,
|
|
3627
4557
|
plan.deployment.resources.proxyWorker,
|
|
@@ -3653,9 +4583,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3653
4583
|
zoneNameServers: zone?.nameServers ?? []
|
|
3654
4584
|
};
|
|
3655
4585
|
}
|
|
3656
|
-
|
|
3657
|
-
if (completedDrain.state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
|
|
3658
|
-
await writeSandboxDrainMirror(completedDrain);
|
|
4586
|
+
if ((await prepareSandboxDrain()).state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
|
|
3659
4587
|
}
|
|
3660
4588
|
if (requiresStagedCommit) {
|
|
3661
4589
|
if (!authenticationCutoverCommitted) {
|
|
@@ -3766,6 +4694,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3766
4694
|
manifest = {
|
|
3767
4695
|
...manifest,
|
|
3768
4696
|
deployment: plan.deployment,
|
|
4697
|
+
deploymentExecution: runtime.deploymentExecution,
|
|
3769
4698
|
platformVersion: version,
|
|
3770
4699
|
runtime: runtimeProvenance,
|
|
3771
4700
|
safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
|
|
@@ -3794,7 +4723,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
|
|
|
3794
4723
|
}
|
|
3795
4724
|
}
|
|
3796
4725
|
function checkpointAuthenticationResources(manifest, secrets, updatedAt) {
|
|
3797
|
-
return checkpointPlatformAccessResources(manifest, secrets.accessProvisioningJson, updatedAt);
|
|
4726
|
+
return checkpointPlatformAccessResources(checkpointPlatformAccessResources(manifest, secrets.accessProvisioningJson, updatedAt), secrets.accessHealthProvisioningJson, updatedAt);
|
|
3798
4727
|
}
|
|
3799
4728
|
async function writeR2OwnershipMarkerIfNeeded(client, plan, manifest, action) {
|
|
3800
4729
|
if (action.kind !== "r2-bucket") return;
|
|
@@ -4044,6 +4973,7 @@ function apiWorkerSecrets(secrets, accountId, zoneId) {
|
|
|
4044
4973
|
JWT_SECRET: secrets.jwtSecret,
|
|
4045
4974
|
PLATFORM_BOOTSTRAP_GITHUB_ID: secrets.bootstrapGitHubId,
|
|
4046
4975
|
PLATFORM_AUTH_SETUP_TOKEN: secrets.authSetupToken ?? "",
|
|
4976
|
+
PLATFORM_APP_ACCESS_SETUP_TOKEN: secrets.appAccessSetupToken ?? "",
|
|
4047
4977
|
...secrets.projectSecretKeysJson && secrets.projectSecretActiveKeyVersion ? {
|
|
4048
4978
|
PROJECT_SECRET_ACTIVE_KEY_VERSION: secrets.projectSecretActiveKeyVersion,
|
|
4049
4979
|
PROJECT_SECRET_KEYS_JSON: secrets.projectSecretKeysJson
|
|
@@ -4404,14 +5334,6 @@ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionPr
|
|
|
4404
5334
|
return {
|
|
4405
5335
|
admissionProtocol: row.admission_protocol,
|
|
4406
5336
|
generation: row.generation,
|
|
4407
|
-
mirrorValue: JSON.stringify({
|
|
4408
|
-
version: 1,
|
|
4409
|
-
generation: row.generation,
|
|
4410
|
-
state: row.state,
|
|
4411
|
-
startedAt: row.started_at,
|
|
4412
|
-
updatedAt: row.updated_at,
|
|
4413
|
-
...row.completed_at ? { completedAt: row.completed_at } : {}
|
|
4414
|
-
}),
|
|
4415
5337
|
state: row.state === "complete" ? "complete" : "pending"
|
|
4416
5338
|
};
|
|
4417
5339
|
};
|
|
@@ -4578,11 +5500,12 @@ let cachedPlatformUploaderHash;
|
|
|
4578
5500
|
function platformWorkerUploaderHash() {
|
|
4579
5501
|
if (cachedPlatformUploaderHash !== void 0) return cachedPlatformUploaderHash;
|
|
4580
5502
|
try {
|
|
4581
|
-
const launcher =
|
|
5503
|
+
const launcher = resolveCloudflareOperationArgv(process.cwd())[1];
|
|
4582
5504
|
if (!launcher) return cachedPlatformUploaderHash = null;
|
|
4583
5505
|
const digest = createHash("sha256");
|
|
4584
5506
|
digest.update("platform-uploader:v1\0");
|
|
4585
5507
|
digest.update(process.version);
|
|
5508
|
+
digest.update(readFileSync(launcher));
|
|
4586
5509
|
digest.update(readFileSync(resolve(dirname(launcher), "..", "..", "package.json")));
|
|
4587
5510
|
const cfDirectory = dirname(fileURLToPath(import.meta.resolve("cf/package.json")));
|
|
4588
5511
|
digest.update(readFileSync(join(cfDirectory, "package.json")));
|
|
@@ -4692,28 +5615,27 @@ function snapshotPlatformWorkerUploadInputs(config, directory) {
|
|
|
4692
5615
|
}
|
|
4693
5616
|
function platformWorkerApiReadRunner(accountId, workerName, readCloudflare, fallback) {
|
|
4694
5617
|
const base = `/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}`;
|
|
4695
|
-
return async (
|
|
5618
|
+
return async (operation, account, cwd, capture) => {
|
|
4696
5619
|
try {
|
|
4697
|
-
if (
|
|
5620
|
+
if (operation.type === "deployments.status" || operation.type === "deployments.list") {
|
|
4698
5621
|
const response = await readCloudflare(`${base}/deployments`);
|
|
4699
5622
|
const deployments = response && typeof response === "object" && !Array.isArray(response) ? response.deployments : null;
|
|
4700
5623
|
if (!Array.isArray(deployments)) throw new Error(`platform: Cloudflare returned malformed deployment history for ${workerName}.`);
|
|
4701
5624
|
return {
|
|
4702
|
-
status:
|
|
5625
|
+
status: operation.type === "deployments.status" && deployments.length === 0 ? 1 : 0,
|
|
4703
5626
|
stderr: "",
|
|
4704
|
-
stdout:
|
|
4705
|
-
|
|
4706
|
-
}
|
|
4707
|
-
if (args[0] === "versions" && args[1] === "view" && args[2]) {
|
|
4708
|
-
const version = await readCloudflare(`${base}/versions/${encodeURIComponent(args[2])}`);
|
|
4709
|
-
return {
|
|
4710
|
-
status: 0,
|
|
4711
|
-
stderr: "",
|
|
4712
|
-
stdout: JSON.stringify(version)
|
|
5627
|
+
stdout: "",
|
|
5628
|
+
data: operation.type === "deployments.status" ? deployments[0] : deployments
|
|
4713
5629
|
};
|
|
4714
5630
|
}
|
|
5631
|
+
if (operation.type === "versions.view") return {
|
|
5632
|
+
status: 0,
|
|
5633
|
+
stderr: "",
|
|
5634
|
+
stdout: "",
|
|
5635
|
+
data: await readCloudflare(`${base}/versions/${encodeURIComponent(operation.versionId)}`)
|
|
5636
|
+
};
|
|
4715
5637
|
} catch {}
|
|
4716
|
-
return fallback(
|
|
5638
|
+
return fallback(operation, account, cwd, capture);
|
|
4717
5639
|
};
|
|
4718
5640
|
}
|
|
4719
5641
|
async function deployPlatformWorker(config, secrets, options) {
|
|
@@ -4831,27 +5753,21 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4831
5753
|
};
|
|
4832
5754
|
writeFileSync(configPath, `${JSON.stringify(uploadConfig, null, 2)}\n`, { mode: 384 });
|
|
4833
5755
|
}
|
|
4834
|
-
const
|
|
4835
|
-
|
|
4836
|
-
"--config",
|
|
5756
|
+
const operation = {
|
|
5757
|
+
type: options.workerExisted ? "versions.upload" : "worker.deploy",
|
|
4837
5758
|
configPath,
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
"--experimental-auto-create=false",
|
|
4841
|
-
...!options.workerExisted ? ["--autoconfig=false"] : [],
|
|
4842
|
-
"--message",
|
|
4843
|
-
encodeCloudflareVersionMessage([], nextTriggers, uploadedContentHash ?? void 0)
|
|
4844
|
-
];
|
|
5759
|
+
message: encodeCloudflareVersionMessage([], nextTriggers, uploadedContentHash ?? void 0)
|
|
5760
|
+
};
|
|
4845
5761
|
if (secrets) {
|
|
4846
5762
|
const secretsPath = join(directory, "secrets.json");
|
|
4847
5763
|
writeFileSync(secretsPath, JSON.stringify(secrets), { mode: 384 });
|
|
4848
|
-
|
|
5764
|
+
operation.secretsFilePath = secretsPath;
|
|
4849
5765
|
}
|
|
4850
5766
|
const releaseUploadSlot = await options.acquireUploadSlot?.();
|
|
4851
5767
|
let uploaded;
|
|
4852
5768
|
let uploadError;
|
|
4853
5769
|
try {
|
|
4854
|
-
uploaded = options.workerExisted ? await options.runner(
|
|
5770
|
+
uploaded = options.workerExisted ? await options.runner(operation, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(operation, accountId, directory, true, workerName, options.apiBaseUrl);
|
|
4855
5771
|
assertCleanPlatformWrangler(uploaded, `${options.workerExisted ? "upload" : "bootstrap"} ${workerName}`);
|
|
4856
5772
|
} catch (error) {
|
|
4857
5773
|
uploadError = error;
|
|
@@ -4859,7 +5775,8 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4859
5775
|
} finally {
|
|
4860
5776
|
releaseUploadSlot?.(uploadError);
|
|
4861
5777
|
}
|
|
4862
|
-
|
|
5778
|
+
const uploadData = uploaded.data;
|
|
5779
|
+
uploadedVersionId = typeof uploadData?.versionId === "string" ? uploadData.versionId : null;
|
|
4863
5780
|
if (!options.workerExisted) activatedVersionId = uploadedVersionId;
|
|
4864
5781
|
if (!uploadedVersionId) throw new Error(`platform: Wrangler uploaded ${workerName} without reporting its immutable version id.`);
|
|
4865
5782
|
await options.onStaged(previousVersionId, uploadedVersionId, previousTriggers);
|
|
@@ -4869,16 +5786,15 @@ async function deployPlatformWorker(config, secrets, options) {
|
|
|
4869
5786
|
const deploymentsBeforeActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4870
5787
|
if (deploymentsBeforeActivation[0]?.id !== previousDeploymentId || !deploymentRunsOnlyVersion(deploymentsBeforeActivation[0], previousVersionId)) throw new Error(`platform: Worker ${workerName} changed concurrently before activation; refusing to overwrite it.`);
|
|
4871
5788
|
activatedVersionId = uploadedVersionId;
|
|
4872
|
-
assertCleanPlatformWrangler(await options.runner(
|
|
4873
|
-
"versions",
|
|
4874
|
-
"deploy",
|
|
4875
|
-
`${uploadedVersionId}@100`,
|
|
4876
|
-
"--config",
|
|
5789
|
+
assertCleanPlatformWrangler(await options.runner({
|
|
5790
|
+
type: "versions.deploy",
|
|
4877
5791
|
configPath,
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
5792
|
+
versions: [{
|
|
5793
|
+
versionId: uploadedVersionId,
|
|
5794
|
+
percentage: 100
|
|
5795
|
+
}],
|
|
5796
|
+
message: `Void platform activation ${uploadedVersionId}`
|
|
5797
|
+
}, accountId, directory, true), `activate ${workerName}`);
|
|
4882
5798
|
}
|
|
4883
5799
|
const deploymentsAfterActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName, readRunner);
|
|
4884
5800
|
const lineage = classifyPlatformWorkerDeploymentLineage(deploymentsAfterActivation, uploadedVersionId, previousDeploymentId);
|
|
@@ -5072,28 +5988,24 @@ async function rollbackPlatformWorker(input) {
|
|
|
5072
5988
|
}
|
|
5073
5989
|
}
|
|
5074
5990
|
function platformWorkerCommandRunner(accountId, cwd, runner) {
|
|
5075
|
-
return (
|
|
5991
|
+
return (operation, options) => runner(operation, accountId, cwd, options.capture);
|
|
5076
5992
|
}
|
|
5077
|
-
async function runPlatformWrangler(
|
|
5078
|
-
return runPlatformWranglerAtApiBase(
|
|
5993
|
+
async function runPlatformWrangler(operation, accountId, cwd, capture) {
|
|
5994
|
+
return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
|
|
5079
5995
|
}
|
|
5080
5996
|
const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
|
|
5081
|
-
async function runPlatformWranglerAtApiBase(
|
|
5997
|
+
async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false) {
|
|
5082
5998
|
const startedAt = performance.now();
|
|
5083
5999
|
const result = await createDefaultRunWrangler(process.cwd(), {
|
|
5084
|
-
launcher:
|
|
6000
|
+
launcher: resolveCloudflareOperationArgv(process.cwd()),
|
|
5085
6001
|
parallelWorkerUpload,
|
|
5086
6002
|
timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
|
|
5087
|
-
})(
|
|
6003
|
+
})(operation, {
|
|
5088
6004
|
accountId,
|
|
5089
6005
|
apiBaseUrl,
|
|
5090
|
-
captureJson: capture,
|
|
5091
6006
|
cwd
|
|
5092
6007
|
});
|
|
5093
|
-
if (process.env.VOID_PLATFORM_TIMINGS === "1") {
|
|
5094
|
-
const operation = args[0] === "versions" || args[0] === "deployments" ? `${args[0]} ${args[1] ?? ""}` : args[0] ?? "unknown";
|
|
5095
|
-
process.stderr.write(`[platform timing] wrangler ${operation} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
|
|
5096
|
-
}
|
|
6008
|
+
if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] Cloudflare ${operation.type} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
|
|
5097
6009
|
const stdout = result.stdout || "";
|
|
5098
6010
|
const stderr = result.stderr || "";
|
|
5099
6011
|
if (!capture && result.status !== 0) {
|
|
@@ -5101,7 +6013,7 @@ async function runPlatformWranglerAtApiBase(args, accountId, cwd, capture, apiBa
|
|
|
5101
6013
|
if (stderr) process.stderr.write(stderr);
|
|
5102
6014
|
}
|
|
5103
6015
|
return {
|
|
5104
|
-
|
|
6016
|
+
...result,
|
|
5105
6017
|
stderr,
|
|
5106
6018
|
stdout
|
|
5107
6019
|
};
|
|
@@ -5222,48 +6134,30 @@ async function withConditionalPlatformWorkerCreate(accountId, workerName, operat
|
|
|
5222
6134
|
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.`);
|
|
5223
6135
|
return result;
|
|
5224
6136
|
}
|
|
5225
|
-
async function runPlatformWranglerForConditionalCreate(
|
|
5226
|
-
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(
|
|
6137
|
+
async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl) {
|
|
6138
|
+
return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl), upstreamApiBaseUrl);
|
|
5227
6139
|
}
|
|
5228
6140
|
async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
5229
|
-
const result = await runner(
|
|
5230
|
-
"deployments",
|
|
5231
|
-
"status",
|
|
5232
|
-
"--config",
|
|
6141
|
+
const result = await runner({
|
|
6142
|
+
type: "deployments.status",
|
|
5233
6143
|
configPath,
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
"--json"
|
|
5237
|
-
], accountId, process.cwd(), true);
|
|
6144
|
+
workerName
|
|
6145
|
+
}, accountId, process.cwd(), true);
|
|
5238
6146
|
if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
|
|
5239
|
-
|
|
5240
|
-
try {
|
|
5241
|
-
value = JSON.parse(result.stdout);
|
|
5242
|
-
} catch {
|
|
5243
|
-
throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
|
|
5244
|
-
}
|
|
6147
|
+
const value = result.data;
|
|
5245
6148
|
if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
|
|
5246
6149
|
const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
|
|
5247
6150
|
if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
|
|
5248
6151
|
return active[0].version_id;
|
|
5249
6152
|
}
|
|
5250
6153
|
async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
|
|
5251
|
-
const result = await runner(
|
|
5252
|
-
"deployments",
|
|
5253
|
-
"list",
|
|
5254
|
-
"--config",
|
|
6154
|
+
const result = await runner({
|
|
6155
|
+
type: "deployments.list",
|
|
5255
6156
|
configPath,
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
"--json"
|
|
5259
|
-
], accountId, process.cwd(), true);
|
|
6157
|
+
workerName
|
|
6158
|
+
}, accountId, process.cwd(), true);
|
|
5260
6159
|
if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
|
|
5261
|
-
|
|
5262
|
-
try {
|
|
5263
|
-
value = JSON.parse(result.stdout);
|
|
5264
|
-
} catch {
|
|
5265
|
-
throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
5266
|
-
}
|
|
6160
|
+
const value = result.data;
|
|
5267
6161
|
if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
5268
6162
|
return value.map((entry, index) => {
|
|
5269
6163
|
if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.created_on !== "string" || !Array.isArray(entry.versions)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
|
|
@@ -5298,16 +6192,15 @@ async function restorePlatformWorkerDeployment(input) {
|
|
|
5298
6192
|
if (restorationVersions.length === 0 || restorationVersions.length > 2 || restorationVersions.some((version, index) => version.percentage > 100 || restorationVersions.findIndex((entry) => entry.versionId === version.versionId) !== index) || Math.abs(totalPercentage - 100) > .001) throw new Error(`Worker ${input.workerName} deployment ${input.restoration.id} has an unsupported traffic distribution.`);
|
|
5299
6193
|
const before = await readPlatformWorkerDeployments(input.configPath, input.accountId, input.workerName, input.runner);
|
|
5300
6194
|
if (before[0]?.id !== input.expectedActiveDeploymentId || !deploymentRunsOnlyVersion(before[0], input.expectedActiveVersionId) || before[1]?.id !== input.restoration.id || !samePlatformWorkerTraffic(before[1], input.restoration)) throw new Error(`Worker ${input.workerName} changed concurrently before deployment restoration; refusing to overwrite the newer deployment.`);
|
|
5301
|
-
assertCleanPlatformWrangler(await input.runner(
|
|
5302
|
-
"versions",
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
`Void platform restoration ${input.restoration.id}
|
|
5309
|
-
|
|
5310
|
-
], input.accountId, input.directory, true), `restore deployment ${input.restoration.id}`);
|
|
6195
|
+
assertCleanPlatformWrangler(await input.runner({
|
|
6196
|
+
type: "versions.deploy",
|
|
6197
|
+
configPath: input.configPath,
|
|
6198
|
+
versions: restorationVersions.map((version) => ({
|
|
6199
|
+
versionId: version.versionId,
|
|
6200
|
+
percentage: version.percentage
|
|
6201
|
+
})),
|
|
6202
|
+
message: `Void platform restoration ${input.restoration.id}`
|
|
6203
|
+
}, input.accountId, input.directory, true), `restore deployment ${input.restoration.id}`);
|
|
5311
6204
|
const after = await readPlatformWorkerDeployments(input.configPath, input.accountId, input.workerName, input.runner);
|
|
5312
6205
|
if (!samePlatformWorkerTraffic(after[0], input.restoration) || after[1]?.id !== input.expectedActiveDeploymentId) throw new Error(`Worker ${input.workerName} deployment restoration was superseded or did not restore deployment ${input.restoration.id}.`);
|
|
5313
6206
|
}
|
|
@@ -5363,23 +6256,14 @@ async function readActivePlatformWorkerSnapshot(configPath, accountId, workerNam
|
|
|
5363
6256
|
});
|
|
5364
6257
|
}
|
|
5365
6258
|
async function readPlatformWorkerVersionSnapshot(configPath, accountId, workerName, expectedOwnershipMarker, versionId, runner, validateCurrentVersion) {
|
|
5366
|
-
const result = await runner(
|
|
5367
|
-
"versions",
|
|
5368
|
-
"view",
|
|
6259
|
+
const result = await runner({
|
|
6260
|
+
type: "versions.view",
|
|
5369
6261
|
versionId,
|
|
5370
|
-
"--config",
|
|
5371
6262
|
configPath,
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
"--json"
|
|
5375
|
-
], accountId, process.cwd(), true);
|
|
6263
|
+
workerName
|
|
6264
|
+
}, accountId, process.cwd(), true);
|
|
5376
6265
|
assertCleanPlatformWrangler(result, `read rollback metadata for ${workerName}`);
|
|
5377
|
-
|
|
5378
|
-
try {
|
|
5379
|
-
value = JSON.parse(result.stdout);
|
|
5380
|
-
} catch {
|
|
5381
|
-
throw new Error(`platform: Wrangler returned malformed version metadata for ${workerName}.`);
|
|
5382
|
-
}
|
|
6266
|
+
const value = result.data;
|
|
5383
6267
|
if (!value || typeof value !== "object") throw new Error(`platform: Wrangler returned malformed version metadata for ${workerName}.`);
|
|
5384
6268
|
const resources = value.resources;
|
|
5385
6269
|
const bindings = resources && typeof resources === "object" && !Array.isArray(resources) ? resources.bindings : null;
|
|
@@ -5405,11 +6289,8 @@ function withoutCompletedPlatformWorkerMigrations(config, activeMigrationTag) {
|
|
|
5405
6289
|
delete result.migrations;
|
|
5406
6290
|
return result;
|
|
5407
6291
|
}
|
|
5408
|
-
function parseUploadedPlatformWorkerVersion(output) {
|
|
5409
|
-
return /(?:Worker|Current) Version ID:\s*([0-9a-f]{8}-[0-9a-f-]{27})/i.exec(output)?.[1] ?? null;
|
|
5410
|
-
}
|
|
5411
6292
|
function assertCleanPlatformWrangler(result, operation) {
|
|
5412
|
-
if (result.status !== 0) throw new Error(`platform: Wrangler failed to ${operation} (${result.stderr.trim() || `exit ${result.status}`}).`);
|
|
6293
|
+
if (result.status !== 0 || result.signal || result.error) throw new Error(`platform: Wrangler failed to ${operation} (${result.stderr.trim() || `exit ${result.status}`}).`);
|
|
5413
6294
|
}
|
|
5414
6295
|
function cleanPlatformCommandResult() {
|
|
5415
6296
|
return {
|
|
@@ -5530,18 +6411,18 @@ function parseWorkerCutoverSnapshot(value) {
|
|
|
5530
6411
|
return null;
|
|
5531
6412
|
}
|
|
5532
6413
|
}
|
|
5533
|
-
function
|
|
6414
|
+
function workerCutoverGenerations(manifest) {
|
|
5534
6415
|
const generations = /* @__PURE__ */ new Map();
|
|
5535
6416
|
for (const step of manifest.steps) {
|
|
5536
6417
|
const resolution = /^worker-cutover:(\d+):(committed|restored)$/.exec(step.key);
|
|
5537
6418
|
if (resolution) {
|
|
5538
6419
|
const generation = Number(resolution[1]);
|
|
5539
6420
|
const entry = generations.get(generation) ?? {
|
|
5540
|
-
|
|
6421
|
+
resolution: null,
|
|
5541
6422
|
staged: /* @__PURE__ */ new Map(),
|
|
5542
6423
|
unsafeTraffic: /* @__PURE__ */ new Set()
|
|
5543
6424
|
};
|
|
5544
|
-
entry.
|
|
6425
|
+
entry.resolution = resolution[2];
|
|
5545
6426
|
generations.set(generation, entry);
|
|
5546
6427
|
continue;
|
|
5547
6428
|
}
|
|
@@ -5549,7 +6430,7 @@ function interruptedWorkerCutover(manifest) {
|
|
|
5549
6430
|
if (unsafeTraffic) {
|
|
5550
6431
|
const generation = Number(unsafeTraffic[1]);
|
|
5551
6432
|
const entry = generations.get(generation) ?? {
|
|
5552
|
-
|
|
6433
|
+
resolution: null,
|
|
5553
6434
|
staged: /* @__PURE__ */ new Map(),
|
|
5554
6435
|
unsafeTraffic: /* @__PURE__ */ new Set()
|
|
5555
6436
|
};
|
|
@@ -5563,14 +6444,17 @@ function interruptedWorkerCutover(manifest) {
|
|
|
5563
6444
|
const snapshot = parseWorkerCutoverSnapshot(step.resultHash);
|
|
5564
6445
|
if (!snapshot) throw new Error(`platform: Invalid Worker cutover checkpoint ${step.key}.`);
|
|
5565
6446
|
const entry = generations.get(generation) ?? {
|
|
5566
|
-
|
|
6447
|
+
resolution: null,
|
|
5567
6448
|
staged: /* @__PURE__ */ new Map(),
|
|
5568
6449
|
unsafeTraffic: /* @__PURE__ */ new Set()
|
|
5569
6450
|
};
|
|
5570
6451
|
entry.staged.set(staged[2], snapshot);
|
|
5571
6452
|
generations.set(generation, entry);
|
|
5572
6453
|
}
|
|
5573
|
-
|
|
6454
|
+
return generations;
|
|
6455
|
+
}
|
|
6456
|
+
function interruptedWorkerCutover(manifest) {
|
|
6457
|
+
const unresolved = [...workerCutoverGenerations(manifest).entries()].filter(([, entry]) => entry.resolution === null && entry.staged.size > 0).sort(([left], [right]) => right - left)[0];
|
|
5574
6458
|
return unresolved ? {
|
|
5575
6459
|
generation: unresolved[0],
|
|
5576
6460
|
staged: unresolved[1].staged,
|
|
@@ -5584,8 +6468,8 @@ async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs
|
|
|
5584
6468
|
"tail",
|
|
5585
6469
|
"dispatch"
|
|
5586
6470
|
]) {
|
|
5587
|
-
const cutover =
|
|
5588
|
-
if (cutover
|
|
6471
|
+
const cutover = workerCutoverGenerations(manifest).get(generation);
|
|
6472
|
+
if (!cutover || cutover.resolution === "restored" || cutover.staged.size !== workerKeys.length) throw new Error(`platform: Worker cutover generation ${generation} is missing a complete immutable-version checkpoint.`);
|
|
5589
6473
|
const directory = mkdtempSync(join(tmpdir(), "void-platform-cutover-verify-"));
|
|
5590
6474
|
try {
|
|
5591
6475
|
for (const key of workerKeys) {
|
|
@@ -5638,7 +6522,15 @@ function errorMessage(error) {
|
|
|
5638
6522
|
}
|
|
5639
6523
|
var PlatformAccessBlockedError = class extends Error {
|
|
5640
6524
|
constructor(url, context) {
|
|
5641
|
-
super([
|
|
6525
|
+
super(context?.accessProtected ? [
|
|
6526
|
+
"platform: Cloudflare Access blocked a protected platform health check (Default-Deny, error 1050).",
|
|
6527
|
+
`Blocked URL: ${url}`,
|
|
6528
|
+
"Keep employee Access protection and account-wide Default-Deny enabled.",
|
|
6529
|
+
"The reserved dispatch /health endpoint uses a path-only Access exception and still requires Void’s private probe token.",
|
|
6530
|
+
"Resume to verify the health exceptions and saved installer credentials.",
|
|
6531
|
+
...context.resumeCommand ? [`Resume: ${context.resumeCommand}`] : [],
|
|
6532
|
+
"https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
|
|
6533
|
+
].join("\n") : [
|
|
5642
6534
|
"platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
|
|
5643
6535
|
`Blocked URL: ${url}`,
|
|
5644
6536
|
"",
|
|
@@ -5684,16 +6576,16 @@ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicatio
|
|
|
5684
6576
|
request(`${dispatchUrl}/health`),
|
|
5685
6577
|
context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
|
|
5686
6578
|
]);
|
|
5687
|
-
if (!health.ok) throw await platformProbeFailure("Health check", apiUrl
|
|
5688
|
-
if (!discovery.ok) throw await platformProbeFailure("Discovery check", apiUrl
|
|
6579
|
+
if (!health.ok) throw await platformProbeFailure("Health check", `${apiUrl}/health`, health, context);
|
|
6580
|
+
if (!discovery.ok) throw await platformProbeFailure("Discovery check", `${apiUrl}/.well-known/void-platform`, discovery, context);
|
|
5689
6581
|
const body = await discovery.json();
|
|
5690
6582
|
if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
|
|
5691
6583
|
if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
|
|
5692
6584
|
if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
|
|
5693
|
-
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", proxyUrl
|
|
5694
|
-
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", dispatchUrl
|
|
6585
|
+
if (!proxy.ok) throw await platformProbeFailure("Proxy health check", `${proxyUrl}/health`, proxy, context);
|
|
6586
|
+
if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", `${dispatchUrl}/health`, dispatch, context);
|
|
5695
6587
|
if (emailGateway) {
|
|
5696
|
-
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", context.emailGatewayUrl
|
|
6588
|
+
if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", `${context.emailGatewayUrl}/health`, emailGateway, context);
|
|
5697
6589
|
const identity = await emailGateway.json();
|
|
5698
6590
|
if (identity?.installationId !== installationId || identity.platformVersion !== platformVersion || identity.emailProtocolVersion !== 1) throw new Error("platform: Deployed email gateway identity, version, or protocol does not match this installation.");
|
|
5699
6591
|
}
|
|
@@ -5721,9 +6613,8 @@ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fe
|
|
|
5721
6613
|
if (status.version !== 1 || !validState || typeof status.complete !== "boolean" || !validPhase || status.generation !== null && (typeof status.generation !== "string" || status.generation.length === 0) || status.updatedAt !== null && (typeof status.updatedAt !== "string" || !Number.isFinite(Date.parse(status.updatedAt))) || status.state === "required" && (status.complete || status.generation !== null || status.updatedAt !== null || status.phase !== null) || status.state === "draining" && (status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null) || status.state === "complete" && (!status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
|
|
5722
6614
|
return status.state === "complete";
|
|
5723
6615
|
}
|
|
5724
|
-
async function
|
|
5725
|
-
const response = await fetcher(`${apiUrl}/
|
|
5726
|
-
method: "POST",
|
|
6616
|
+
async function verifyPlatformDeploymentExecutionDrain(apiUrl, healthProbeToken, installationId, fetcher = fetch) {
|
|
6617
|
+
const response = await fetcher(`${apiUrl}/__void/platform/deployment-execution`, {
|
|
5727
6618
|
headers: {
|
|
5728
6619
|
"X-Void-Platform-Probe": healthProbeToken,
|
|
5729
6620
|
...cfAccessHeaders(process.env, apiUrl)
|
|
@@ -5731,29 +6622,124 @@ async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher =
|
|
|
5731
6622
|
signal: AbortSignal.timeout(3e4),
|
|
5732
6623
|
redirect: "manual"
|
|
5733
6624
|
});
|
|
6625
|
+
if (!response.ok) throw new Error(`platform: Deployment execution drain probe failed (HTTP ${response.status}). Keep the current runtime until its durable deployment operations have drained.`);
|
|
5734
6626
|
let value;
|
|
5735
6627
|
try {
|
|
5736
6628
|
value = await response.json();
|
|
5737
6629
|
} catch {
|
|
5738
|
-
throw new Error(
|
|
6630
|
+
throw new Error("platform: Deployment execution drain probe returned an invalid document.");
|
|
5739
6631
|
}
|
|
5740
|
-
const status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5741
|
-
if (
|
|
5742
|
-
|
|
6632
|
+
const status = value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6633
|
+
if (status?.protocolVersion !== 1 || status.installationId !== installationId || typeof status.admissionClosed !== "boolean" || !Number.isSafeInteger(status.pendingDeployments) || Number(status.pendingDeployments) < 0) throw new Error("platform: Deployment execution drain probe returned an invalid document.");
|
|
6634
|
+
if (!status.admissionClosed || status.pendingDeployments !== 0) throw new Error("platform: Durable deployment operations are still admitted or unfinished. Keep the current runtime and retry this command after they finish.");
|
|
6635
|
+
}
|
|
6636
|
+
async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
|
|
6637
|
+
const deadline = Date.now() + 3e5;
|
|
6638
|
+
let operationId = null;
|
|
6639
|
+
const timedOut = () => /* @__PURE__ */ new Error(`platform: Shared email bootstrap did not finish within five minutes at ${apiUrl}.${operationId ? ` Operation ${operationId} remains incomplete.` : ""} Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
|
|
6640
|
+
const waitForProgress = async () => {
|
|
6641
|
+
const remaining = deadline - Date.now();
|
|
6642
|
+
if (remaining <= 0) throw timedOut();
|
|
6643
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, remaining)));
|
|
6644
|
+
};
|
|
6645
|
+
let response;
|
|
6646
|
+
let value;
|
|
6647
|
+
let status;
|
|
6648
|
+
for (;;) {
|
|
6649
|
+
const remaining = deadline - Date.now();
|
|
6650
|
+
if (remaining <= 0) throw timedOut();
|
|
6651
|
+
const signal = AbortSignal.timeout(Math.min(12e4, remaining));
|
|
6652
|
+
try {
|
|
6653
|
+
response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
|
|
6654
|
+
method: "POST",
|
|
6655
|
+
headers: {
|
|
6656
|
+
"X-Void-Platform-Probe": healthProbeToken,
|
|
6657
|
+
...cfAccessHeaders(process.env, apiUrl)
|
|
6658
|
+
},
|
|
6659
|
+
signal,
|
|
6660
|
+
redirect: "manual"
|
|
6661
|
+
});
|
|
6662
|
+
} catch (error) {
|
|
6663
|
+
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
6664
|
+
await waitForProgress();
|
|
6665
|
+
continue;
|
|
6666
|
+
}
|
|
6667
|
+
throw new Error(`platform: Shared email bootstrap request failed at ${apiUrl}. Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
|
|
6668
|
+
}
|
|
6669
|
+
try {
|
|
6670
|
+
value = await response.json();
|
|
6671
|
+
} catch (error) {
|
|
6672
|
+
if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
|
|
6673
|
+
await waitForProgress();
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
|
|
6677
|
+
}
|
|
6678
|
+
status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6679
|
+
const observedOperationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
|
|
6680
|
+
if (status?.version === 1 && observedOperationId) {
|
|
6681
|
+
if (operationId && operationId !== observedOperationId) throw new Error("platform: Shared email bootstrap operation identity changed while waiting. Traffic remains disabled. Inspect the installation before retrying.");
|
|
6682
|
+
operationId = observedOperationId;
|
|
6683
|
+
}
|
|
6684
|
+
if ([
|
|
6685
|
+
200,
|
|
6686
|
+
202,
|
|
6687
|
+
503
|
|
6688
|
+
].includes(response.status) && status?.version === 1 && status.state === "incomplete" && observedOperationId && (status.operationStatus === "pending" || status.operationStatus === "running") && status.failureCode == null) {
|
|
6689
|
+
await waitForProgress();
|
|
6690
|
+
continue;
|
|
6691
|
+
}
|
|
6692
|
+
break;
|
|
6693
|
+
}
|
|
6694
|
+
if (!response.ok || status?.version !== 1 || status.state !== "ready" || operationId === null || status.operationId !== operationId || status.operationStatus != null && status.operationStatus !== "completed" || status.failureCode != null) {
|
|
6695
|
+
const code = (value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value) ? value : null;
|
|
6696
|
+
const operationStatus = code(status?.operationStatus);
|
|
6697
|
+
const operationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
|
|
6698
|
+
const details = operationId ? [`Operation ${operationId}: ${operationStatus ?? "incomplete"}.`] : operationStatus ? [`Operation status: ${operationStatus}.`] : [];
|
|
6699
|
+
const currentStep = code(status?.currentStep);
|
|
6700
|
+
const failureCode = code(status?.failureCode);
|
|
6701
|
+
if (currentStep) details.push(`Step: ${currentStep}.`);
|
|
6702
|
+
if (failureCode) details.push(`Failure: ${failureCode}.`);
|
|
6703
|
+
let domain = null;
|
|
6704
|
+
if (typeof status?.domain === "string") try {
|
|
6705
|
+
domain = normalizeApplicationDomain(status.domain);
|
|
6706
|
+
} catch {}
|
|
6707
|
+
const inboundStatus = code(status?.inboundStatus);
|
|
6708
|
+
if (domain) details.push(`Domain: ${domain}${inboundStatus ? ` (inbound: ${inboundStatus})` : ""}.`);
|
|
6709
|
+
const mxDetail = domain ? `MX records for ${domain} do not route mail to Cloudflare Email Routing` : null;
|
|
6710
|
+
const safeDetails = [
|
|
6711
|
+
...domain ? [mxDetail, `enabled explicit Cloudflare email rules conflict with ${domain}`] : [],
|
|
6712
|
+
"zone catch-all does not point to this ingress",
|
|
6713
|
+
"another enabled catch-all owns this zone"
|
|
6714
|
+
];
|
|
6715
|
+
if (typeof status?.failureDetail === "string" && safeDetails.includes(status.failureDetail)) {
|
|
6716
|
+
details.push(`${status.failureDetail}.`);
|
|
6717
|
+
if (currentStep === "inbound_probe" && status.failureDetail === mxDetail) details.push(`Enable Email Routing for ${domain} in the Cloudflare dashboard, then resume this installation.`);
|
|
6718
|
+
}
|
|
6719
|
+
const detail = details.length ? ` ${details.join(" ")}` : "";
|
|
5743
6720
|
throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
|
|
5744
6721
|
}
|
|
5745
|
-
return
|
|
6722
|
+
return operationId;
|
|
5746
6723
|
}
|
|
5747
6724
|
function createPlatformHealthProbeToken(ownershipNonce) {
|
|
5748
6725
|
return createHash("sha256").update("void-platform-health-probe:v1\0").update(ownershipNonce).digest("base64url");
|
|
5749
6726
|
}
|
|
5750
6727
|
//#endregion
|
|
5751
6728
|
//#region src/cli/platform-install-auth.ts
|
|
6729
|
+
function platformManagementAccessForAuthentication(selection, accountId, checkpoint) {
|
|
6730
|
+
if (!selection.authentication.accessProtection && (!selection.authentication.providers.includes("cloudflare-access") || selection.file && !selection.file.cloudflareAccess)) return;
|
|
6731
|
+
const spec = selection.file?.cloudflareAccess ?? (checkpoint?.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(checkpoint.accessSetupSpecJson)) : void 0);
|
|
6732
|
+
if (spec?.accountId && spec.accountId !== accountId) return;
|
|
6733
|
+
return {
|
|
6734
|
+
protection: !!selection.authentication.accessProtection,
|
|
6735
|
+
readOnly: spec?.mode === "existing"
|
|
6736
|
+
};
|
|
6737
|
+
}
|
|
5752
6738
|
function answer(value) {
|
|
5753
6739
|
if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
|
|
5754
6740
|
return value;
|
|
5755
6741
|
}
|
|
5756
|
-
async function selectInstallationAuthentication(file, recorded) {
|
|
6742
|
+
async function selectInstallationAuthentication(file, recorded, choices = {}) {
|
|
5757
6743
|
if (file) {
|
|
5758
6744
|
const input = JSON.parse(await readFile(file, "utf8"));
|
|
5759
6745
|
if (!input || typeof input !== "object" || Array.isArray(input) || !("connections" in input) || !Array.isArray(input.connections) || !input.connections.length || input.connections.length > 32) throw new Error("platform: An authentication file must define between 1 and 32 connections.");
|
|
@@ -5772,11 +6758,14 @@ async function selectInstallationAuthentication(file, recorded) {
|
|
|
5772
6758
|
file: selected
|
|
5773
6759
|
};
|
|
5774
6760
|
}
|
|
5775
|
-
if (recorded)
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
6761
|
+
if (recorded) {
|
|
6762
|
+
if (choices.loginMethods && JSON.stringify(choices.loginMethods) !== JSON.stringify(recorded.providers) || choices.accessProtection !== void 0 && choices.accessProtection !== !!recorded.accessProtection) throw new Error("platform: Resume with the recorded login methods and protection settings.");
|
|
6763
|
+
return { authentication: recorded };
|
|
6764
|
+
}
|
|
6765
|
+
if (!process.stdin.isTTY && !choices.loginMethods && choices.accessProtection === void 0) return { authentication: { providers: ["github"] } };
|
|
6766
|
+
const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] : answer(await multiselect({
|
|
6767
|
+
message: "Platform login methods (select all that apply):",
|
|
6768
|
+
initialValues: [],
|
|
5780
6769
|
required: true,
|
|
5781
6770
|
options: [
|
|
5782
6771
|
{
|
|
@@ -5796,11 +6785,11 @@ async function selectInstallationAuthentication(file, recorded) {
|
|
|
5796
6785
|
label: "Cloudflare Access login"
|
|
5797
6786
|
}
|
|
5798
6787
|
]
|
|
5799
|
-
}));
|
|
5800
|
-
const protection = answer(await confirm({
|
|
6788
|
+
})));
|
|
6789
|
+
const protection = choices.accessProtection ?? (!process.stdin.isTTY ? false : answer(await confirm({
|
|
5801
6790
|
message: "Also protect the platform UI and APIs with Cloudflare Access?",
|
|
5802
6791
|
initialValue: false
|
|
5803
|
-
}));
|
|
6792
|
+
})));
|
|
5804
6793
|
return { authentication: {
|
|
5805
6794
|
providers,
|
|
5806
6795
|
...protection || providers.length !== 1 || providers[0] !== "github" ? { configurable: true } : {},
|
|
@@ -5837,6 +6826,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
5837
6826
|
const accessEntry = entries.find((entry) => entry.configuration.kind === "cloudflare-access");
|
|
5838
6827
|
const prepared = access && (selection.authentication.accessProtection || accessEntry && (!selection.file || selection.file.cloudflareAccess)) ? await preparePlatformAccessSetup({
|
|
5839
6828
|
plan,
|
|
6829
|
+
...platformDashboardUrl(plan.deployment) ? { additionalOrigins: [platformDashboardUrl(plan.deployment)] } : {},
|
|
5840
6830
|
ownershipNonce: access.ownershipNonce,
|
|
5841
6831
|
protection: !!selection.authentication.accessProtection,
|
|
5842
6832
|
...accessEntry ? { login: {
|
|
@@ -5983,6 +6973,72 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
|
|
|
5983
6973
|
return result;
|
|
5984
6974
|
}
|
|
5985
6975
|
//#endregion
|
|
6976
|
+
//#region src/cli/platform-saved-plans.ts
|
|
6977
|
+
function planId(args) {
|
|
6978
|
+
return createHash("sha256").update(JSON.stringify([args.account, args.name])).digest("hex");
|
|
6979
|
+
}
|
|
6980
|
+
function parseSavedPlan(value) {
|
|
6981
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected an installation plan.");
|
|
6982
|
+
const record = value;
|
|
6983
|
+
if (record.schemaVersion !== 1 || !Array.isArray(record.arguments) || !record.arguments.every((arg) => typeof arg === "string")) throw new Error("Invalid installation plan format.");
|
|
6984
|
+
const args = parsePlatformArgs(record.arguments);
|
|
6985
|
+
if (args.subcommand !== "install" || args.plan || args.resume || args.yes || !args.account || !args.name || !args.displayName) throw new Error("Saved plans must contain installation choices without mutation approval.");
|
|
6986
|
+
if (typeof record.accountName !== "string" || !record.accountName || typeof record.savedAt !== "string" || !Number.isFinite(Date.parse(record.savedAt))) throw new Error("Invalid installation plan metadata.");
|
|
6987
|
+
return {
|
|
6988
|
+
id: planId(args),
|
|
6989
|
+
args,
|
|
6990
|
+
accountName: record.accountName,
|
|
6991
|
+
savedAt: record.savedAt
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
function savePlatformInstallChoices(arguments_, accountName, directory = platformRegistryDirectory()) {
|
|
6995
|
+
const record = {
|
|
6996
|
+
schemaVersion: 1,
|
|
6997
|
+
arguments: arguments_,
|
|
6998
|
+
accountName,
|
|
6999
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7000
|
+
};
|
|
7001
|
+
const plan = parseSavedPlan(record);
|
|
7002
|
+
mkdirSync(directory, {
|
|
7003
|
+
recursive: true,
|
|
7004
|
+
mode: 448
|
|
7005
|
+
});
|
|
7006
|
+
const path = join(directory, `plan-${plan.id}.json`);
|
|
7007
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
7008
|
+
try {
|
|
7009
|
+
writeFileSync(temporary, JSON.stringify(record, null, 2) + "\n", {
|
|
7010
|
+
mode: 384,
|
|
7011
|
+
flag: "wx"
|
|
7012
|
+
});
|
|
7013
|
+
renameSync(temporary, path);
|
|
7014
|
+
} finally {
|
|
7015
|
+
rmSync(temporary, { force: true });
|
|
7016
|
+
}
|
|
7017
|
+
return plan;
|
|
7018
|
+
}
|
|
7019
|
+
function listSavedPlatformInstallPlans(directory = platformRegistryDirectory()) {
|
|
7020
|
+
let entries;
|
|
7021
|
+
try {
|
|
7022
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
7023
|
+
} catch (error) {
|
|
7024
|
+
if (error.code === "ENOENT") return [];
|
|
7025
|
+
throw error;
|
|
7026
|
+
}
|
|
7027
|
+
return entries.filter((entry) => entry.isFile() && /^plan-[a-f0-9]{64}\.json$/.test(entry.name)).map((entry) => {
|
|
7028
|
+
const path = join(directory, entry.name);
|
|
7029
|
+
try {
|
|
7030
|
+
const plan = parseSavedPlan(JSON.parse(readFileSync(path, "utf8")));
|
|
7031
|
+
if (entry.name !== `plan-${plan.id}.json`) throw new Error("Plan identity does not match its file.");
|
|
7032
|
+
return plan;
|
|
7033
|
+
} catch (error) {
|
|
7034
|
+
throw new Error(`platform: Invalid saved installation plan ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7035
|
+
}
|
|
7036
|
+
}).sort((left, right) => right.savedAt.localeCompare(left.savedAt) || left.id.localeCompare(right.id));
|
|
7037
|
+
}
|
|
7038
|
+
function removeSavedPlatformInstallPlan(args, directory = platformRegistryDirectory()) {
|
|
7039
|
+
rmSync(join(directory, `plan-${planId(args)}.json`), { force: true });
|
|
7040
|
+
}
|
|
7041
|
+
//#endregion
|
|
5986
7042
|
//#region src/cli/platform-setup-draft.ts
|
|
5987
7043
|
const STARTED = "credential-setup:v1";
|
|
5988
7044
|
const COMPLETED = "credential-setup-complete:v1";
|
|
@@ -6018,7 +7074,7 @@ function savePlatformSetupDraft(plan, version) {
|
|
|
6018
7074
|
resultHash: plan.installationId
|
|
6019
7075
|
}, now);
|
|
6020
7076
|
writeInstallationManifest(manifest);
|
|
6021
|
-
writePendingInstallationSecrets(manifest, {});
|
|
7077
|
+
writePendingInstallationSecrets(manifest, plan.externalHyperdrive ? { externalHyperdriveJson: JSON.stringify(plan.externalHyperdrive) } : {});
|
|
6022
7078
|
return manifest;
|
|
6023
7079
|
}
|
|
6024
7080
|
function completePlatformSetupDraft(manifest) {
|
|
@@ -6032,234 +7088,10 @@ function completePlatformSetupDraft(manifest) {
|
|
|
6032
7088
|
return completed;
|
|
6033
7089
|
}
|
|
6034
7090
|
//#endregion
|
|
6035
|
-
//#region src/cli/platform-setup.ts
|
|
6036
|
-
function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
|
|
6037
|
-
return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + page.title + "\x1B]8;;\x1B\\" : page.title + ": " + page.url;
|
|
6038
|
-
}
|
|
6039
|
-
function platformSetupFieldHelp(key) {
|
|
6040
|
-
const docs = (page, section, title) => platformSetupLink({
|
|
6041
|
-
title,
|
|
6042
|
-
url: `https://void.cloud/guide/platform/installation/${page}#${section}`
|
|
6043
|
-
});
|
|
6044
|
-
switch (key) {
|
|
6045
|
-
case "databaseUrl": return {
|
|
6046
|
-
placeholder: "postgresql://user:pw@…/database",
|
|
6047
|
-
description: "Stores platform users, projects, and deployments; use an empty hosted database, e.g. " + platformSetupLink({
|
|
6048
|
-
title: "Neon",
|
|
6049
|
-
url: "https://neon.com/"
|
|
6050
|
-
}) + ". " + docs("prerequisites", "platform-database", "Database setup")
|
|
6051
|
-
};
|
|
6052
|
-
case "jwtSecret": return { description: "Signs platform login tokens. Run openssl rand -base64 32; save the key in your password manager. " + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
6053
|
-
case "projectSecretKey": return { description: "Encrypts app secrets. Run openssl rand -base64 32 separately; save this key for recovery. " + docs("credentials", "signing-and-encryption-keys", "Key setup") };
|
|
6054
|
-
default: return;
|
|
6055
|
-
}
|
|
6056
|
-
}
|
|
6057
|
-
function supportsSetupLinks() {
|
|
6058
|
-
return !!process.stdout.isTTY && ([
|
|
6059
|
-
"ghostty",
|
|
6060
|
-
"iTerm.app",
|
|
6061
|
-
"WezTerm",
|
|
6062
|
-
"vscode",
|
|
6063
|
-
"WarpTerminal"
|
|
6064
|
-
].includes(process.env.TERM_PROGRAM ?? "") || !!process.env.WT_SESSION || !!process.env.VTE_VERSION || !!process.env.KONSOLE_VERSION || process.env.TERM === "xterm-kitty");
|
|
6065
|
-
}
|
|
6066
|
-
/** Dashboard keys use permission-catalog labels, which can differ from product names. */
|
|
6067
|
-
function platformSetupPages(plan) {
|
|
6068
|
-
const needsGateway = plan.actions.some((action) => action.kind === "ai-gateway" && action.action === "inspect");
|
|
6069
|
-
const permissions = [
|
|
6070
|
-
{
|
|
6071
|
-
key: "account_settings",
|
|
6072
|
-
type: "read"
|
|
6073
|
-
},
|
|
6074
|
-
{
|
|
6075
|
-
key: "workers_scripts",
|
|
6076
|
-
type: "edit"
|
|
6077
|
-
},
|
|
6078
|
-
{
|
|
6079
|
-
key: "workers_kv_storage",
|
|
6080
|
-
type: "edit"
|
|
6081
|
-
},
|
|
6082
|
-
{
|
|
6083
|
-
key: "workers_r2",
|
|
6084
|
-
type: "edit"
|
|
6085
|
-
},
|
|
6086
|
-
{
|
|
6087
|
-
key: "d1",
|
|
6088
|
-
type: "edit"
|
|
6089
|
-
},
|
|
6090
|
-
{
|
|
6091
|
-
key: "queues",
|
|
6092
|
-
type: "edit"
|
|
6093
|
-
},
|
|
6094
|
-
{
|
|
6095
|
-
key: "account_analytics",
|
|
6096
|
-
type: "read"
|
|
6097
|
-
},
|
|
6098
|
-
{
|
|
6099
|
-
key: "workers_tail",
|
|
6100
|
-
type: "read"
|
|
6101
|
-
},
|
|
6102
|
-
{
|
|
6103
|
-
key: "query_cache",
|
|
6104
|
-
type: "edit"
|
|
6105
|
-
},
|
|
6106
|
-
...needsGateway ? [{
|
|
6107
|
-
key: "aig",
|
|
6108
|
-
type: "edit"
|
|
6109
|
-
}] : [],
|
|
6110
|
-
...plan.deployment.application.zoneName ? [{
|
|
6111
|
-
key: "cache",
|
|
6112
|
-
type: "purge"
|
|
6113
|
-
}] : [],
|
|
6114
|
-
...plan.deployment.capabilities.customDomains ? [{
|
|
6115
|
-
key: "ssl_and_certificates",
|
|
6116
|
-
type: "edit"
|
|
6117
|
-
}] : [],
|
|
6118
|
-
...plan.deployment.capabilities.email ? [
|
|
6119
|
-
{
|
|
6120
|
-
key: "zone",
|
|
6121
|
-
type: "read"
|
|
6122
|
-
},
|
|
6123
|
-
{
|
|
6124
|
-
key: "zone_settings",
|
|
6125
|
-
type: "edit"
|
|
6126
|
-
},
|
|
6127
|
-
{
|
|
6128
|
-
key: "email_routing_address",
|
|
6129
|
-
type: "edit"
|
|
6130
|
-
},
|
|
6131
|
-
{
|
|
6132
|
-
key: "email_routing_rule",
|
|
6133
|
-
type: "edit"
|
|
6134
|
-
},
|
|
6135
|
-
{
|
|
6136
|
-
key: "email_sending",
|
|
6137
|
-
type: "edit"
|
|
6138
|
-
}
|
|
6139
|
-
] : []
|
|
6140
|
-
];
|
|
6141
|
-
return {
|
|
6142
|
-
runtime: {
|
|
6143
|
-
title: "Cloudflare runtime token",
|
|
6144
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
6145
|
-
to: "/" + plan.account.id + "/api-tokens",
|
|
6146
|
-
name: plan.deployment.displayName + " Runtime",
|
|
6147
|
-
permissionGroupKeys: JSON.stringify(permissions)
|
|
6148
|
-
}),
|
|
6149
|
-
instructions: [
|
|
6150
|
-
"Review the preselected permissions for account " + plan.account.name + ".",
|
|
6151
|
-
"Edit: Workers Scripts, KV, R2, D1, Queues, Hyperdrive" + (needsGateway ? ", AI Gateway." : "."),
|
|
6152
|
-
"Read: Account Settings, Account Analytics, Workers Tail.",
|
|
6153
|
-
...plan.deployment.application.zoneName ? ["Zone " + plan.deployment.application.zoneName + ": Cache Purge: Purge."] : [],
|
|
6154
|
-
...plan.deployment.capabilities.customDomains ? ["Custom project domains also need SSL and Certificates: Edit on that zone."] : [],
|
|
6155
|
-
...plan.deployment.capabilities.email ? ["Email-enabled installations need Email Routing Addresses: Edit and Email Sending: Edit on the account, plus Zone: Read, Zone Settings: Edit and Email Routing Rules: Edit on the mail zones."] : [],
|
|
6156
|
-
"If the form differs, match the permissions above. Create the token, then paste it here."
|
|
6157
|
-
].join("\n")
|
|
6158
|
-
},
|
|
6159
|
-
github: {
|
|
6160
|
-
title: "GitHub OAuth App",
|
|
6161
|
-
url: "https://github.com/settings/applications/new",
|
|
6162
|
-
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"
|
|
6163
|
-
},
|
|
6164
|
-
r2: {
|
|
6165
|
-
title: "R2 upload token",
|
|
6166
|
-
url: "https://dash.cloudflare.com/?" + new URLSearchParams({
|
|
6167
|
-
to: "/" + plan.account.id + "/r2/api-tokens/create",
|
|
6168
|
-
type: "account"
|
|
6169
|
-
}),
|
|
6170
|
-
instructions: [
|
|
6171
|
-
"Select Object Read & Write (the form defaults to read-only).",
|
|
6172
|
-
"Keep Apply to all buckets in this account (including newly created buckets) selected.",
|
|
6173
|
-
"Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
|
|
6174
|
-
].join("\n")
|
|
6175
|
-
}
|
|
6176
|
-
};
|
|
6177
|
-
}
|
|
6178
|
-
function formatPlatformSetupPlan(plan) {
|
|
6179
|
-
const pages = platformSetupPages(plan);
|
|
6180
|
-
return [
|
|
6181
|
-
"Credentials needed (paste them into the installer; keep a password-manager copy):",
|
|
6182
|
-
platformSetupLink(pages.runtime),
|
|
6183
|
-
pages.runtime.instructions,
|
|
6184
|
-
"",
|
|
6185
|
-
platformSetupLink(pages.r2),
|
|
6186
|
-
pages.r2.instructions,
|
|
6187
|
-
"",
|
|
6188
|
-
...plan.deployment.authentication.providers.includes("github") ? [platformSetupLink(pages.github), pages.github.instructions] : [],
|
|
6189
|
-
...plan.deployment.authentication.providers.filter((provider) => provider !== "github").map((provider) => `${provider === "google" ? "Google OAuth client" : provider === "oidc" ? "OIDC connection" : "Cloudflare Access OIDC application"}: client ID and secret; callback ${plan.apiUrl}/auth/callback.`),
|
|
6190
|
-
...plan.deployment.authentication.accessProtection ? ["Cloudflare Access protection: configure API and proxy origins and admitted installer/CI credentials."] : []
|
|
6191
|
-
].join("\n");
|
|
6192
|
-
}
|
|
6193
|
-
function formatPlatformInstallCommand(options) {
|
|
6194
|
-
const quote = (value) => {
|
|
6195
|
-
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
|
|
6196
|
-
return process.platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
6197
|
-
};
|
|
6198
|
-
const parts = [
|
|
6199
|
-
options.commandName,
|
|
6200
|
-
"platform",
|
|
6201
|
-
"install"
|
|
6202
|
-
];
|
|
6203
|
-
const flag = (name, value) => {
|
|
6204
|
-
if (value) parts.push(name, quote(value));
|
|
6205
|
-
};
|
|
6206
|
-
if (options.resumeId) {
|
|
6207
|
-
parts.push("--resume");
|
|
6208
|
-
flag("--name", options.resumeId);
|
|
6209
|
-
} else {
|
|
6210
|
-
flag("--name", options.installationSlug);
|
|
6211
|
-
flag("--account", options.accountId);
|
|
6212
|
-
flag("--display-name", options.displayName);
|
|
6213
|
-
if (options.workersDev) parts.push("--workers-dev");
|
|
6214
|
-
else {
|
|
6215
|
-
flag("--application-domain", options.applicationDomain);
|
|
6216
|
-
flag("--zone", options.zoneName);
|
|
6217
|
-
if (options.dedicatedZone) parts.push("--dedicated-zone");
|
|
6218
|
-
}
|
|
6219
|
-
flag("--control-plane-domain", options.controlPlaneDomain);
|
|
6220
|
-
}
|
|
6221
|
-
flag("--auth-config", options.authConfig ?? "");
|
|
6222
|
-
flag("--runtime", options.runtime);
|
|
6223
|
-
if (options.yes) parts.push("--yes");
|
|
6224
|
-
return parts.join(" ");
|
|
6225
|
-
}
|
|
6226
|
-
function openPlatformSetupPage(page, dependencies = {}) {
|
|
6227
|
-
log.info(page.title + "\n" + page.instructions);
|
|
6228
|
-
console.log(platformSetupLink({
|
|
6229
|
-
...page,
|
|
6230
|
-
title: "Open " + page.title
|
|
6231
|
-
}));
|
|
6232
|
-
if (dependencies.interactive ?? process.stdin.isTTY === true) try {
|
|
6233
|
-
(dependencies.open ?? ((url) => openBrowser(url, execFile)))(page.url);
|
|
6234
|
-
} catch {
|
|
6235
|
-
log.info("Could not open the browser. Use the link above.");
|
|
6236
|
-
}
|
|
6237
|
-
}
|
|
6238
|
-
//#endregion
|
|
6239
7091
|
//#region src/cli/prompt-password.ts
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
return await new u$1({
|
|
6244
|
-
input: options.input,
|
|
6245
|
-
output: options.output,
|
|
6246
|
-
signal: options.signal,
|
|
6247
|
-
mask: options.mask ?? S_PASSWORD_MASK,
|
|
6248
|
-
validate: options.validate,
|
|
6249
|
-
render() {
|
|
6250
|
-
const guide = options.withGuide ?? settings.withGuide;
|
|
6251
|
-
const prefix = `${guide ? `${import_picocolors.default.gray(S_BAR)}\n` : ""}${symbol(this.state)} ${options.message}\n`;
|
|
6252
|
-
const bar = guide ? `${import_picocolors.default.gray(S_BAR)} ` : "";
|
|
6253
|
-
if (this.state === "submit" || this.state === "cancel") return `${prefix}${bar}${import_picocolors.default.dim(this.masked)}`;
|
|
6254
|
-
if (this.state === "error") {
|
|
6255
|
-
const masked = this.masked;
|
|
6256
|
-
if (options.clearOnError) this.clear();
|
|
6257
|
-
return `${prefix}${bar}${import_picocolors.default.dim(masked)}\n${guide ? `${import_picocolors.default.yellow(S_BAR_END)} ` : ""}${import_picocolors.default.yellow(this.error)}\n`;
|
|
6258
|
-
}
|
|
6259
|
-
const input = this.userInput.length === 0 ? import_picocolors.default.dim(options.placeholder) : this.userInputWithCursor;
|
|
6260
|
-
return `${prefix}${guide ? `${import_picocolors.default.cyan(S_BAR)} ` : ""}${input}\n${guide ? import_picocolors.default.cyan(S_BAR_END) : ""}\n`;
|
|
6261
|
-
}
|
|
6262
|
-
}).prompt() ?? CANCEL_SYMBOL;
|
|
7092
|
+
/** A visual example for an empty secret field; never a default value or unmasked input. */
|
|
7093
|
+
function passwordWithPlaceholder(options) {
|
|
7094
|
+
return password(options);
|
|
6263
7095
|
}
|
|
6264
7096
|
//#endregion
|
|
6265
7097
|
//#region src/cli/platform-lifecycle.ts
|
|
@@ -6276,17 +7108,17 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
|
|
|
6276
7108
|
}
|
|
6277
7109
|
return explicit || recorded;
|
|
6278
7110
|
}
|
|
6279
|
-
async function runPlatformLifecycleCommand(
|
|
6280
|
-
if (
|
|
6281
|
-
const { runPlatformDomainCommand } = await import("./platform-domain-
|
|
6282
|
-
await runPlatformDomainCommand(
|
|
7111
|
+
async function runPlatformLifecycleCommand(commandArgs) {
|
|
7112
|
+
if (commandArgs.subcommand === "domain") {
|
|
7113
|
+
const { runPlatformDomainCommand } = await import("./platform-domain-Di6EYrQl.mjs");
|
|
7114
|
+
await runPlatformDomainCommand(commandArgs);
|
|
6283
7115
|
return;
|
|
6284
7116
|
}
|
|
6285
|
-
if (
|
|
7117
|
+
if (commandArgs.subcommand === "discover") {
|
|
6286
7118
|
const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
|
|
6287
|
-
const accountId = await selectAccount(await readAccounts(client),
|
|
6288
|
-
const { recoverPlatformInstallations } = await import("./platform-recovery-
|
|
6289
|
-
const recovered = await recoverPlatformInstallations(client, accountId, void 0,
|
|
7119
|
+
const accountId = await selectAccount(await readAccounts(client), commandArgs.account);
|
|
7120
|
+
const { recoverPlatformInstallations } = await import("./platform-recovery-BJnQJEtQ.mjs");
|
|
7121
|
+
const recovered = await recoverPlatformInstallations(client, accountId, void 0, commandArgs.selector);
|
|
6290
7122
|
if (recovered.length === 0) {
|
|
6291
7123
|
log.info(`No recoverable Void platform installations were found in ${accountId}.`);
|
|
6292
7124
|
return;
|
|
@@ -6294,18 +7126,20 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6294
7126
|
for (const manifest of recovered) log.success(`Recovered ${manifest.deployment.displayName} (${manifest.id}) at ${manifest.endpoints.apiUrl}.`);
|
|
6295
7127
|
return;
|
|
6296
7128
|
}
|
|
6297
|
-
if (
|
|
6298
|
-
const { runInstalledPlatformCommand } = await import("./platform-management-
|
|
6299
|
-
await runInstalledPlatformCommand(
|
|
7129
|
+
if (commandArgs.subcommand !== "install") {
|
|
7130
|
+
const { runInstalledPlatformCommand } = await import("./platform-management-DG47jys8.mjs");
|
|
7131
|
+
await runInstalledPlatformCommand(commandArgs);
|
|
6300
7132
|
return;
|
|
6301
7133
|
}
|
|
6302
|
-
|
|
7134
|
+
let args = commandArgs;
|
|
7135
|
+
let runtime = loadPlatformRuntime(args.runtime);
|
|
6303
7136
|
assertNoninteractivePlatformMutationAcknowledged(args, process.stdin.isTTY === true);
|
|
6304
7137
|
console.log();
|
|
6305
7138
|
intro(cliTitle("platform install"));
|
|
6306
7139
|
let existing = args.resume ? resolveInstallationManifest(args.name) : null;
|
|
6307
7140
|
let selectedInstall = false;
|
|
6308
7141
|
let selectedDraft = false;
|
|
7142
|
+
let installationName = args.name;
|
|
6309
7143
|
const { input: installationSlug, credential: operatorCredential } = await resolvePlatformCloudflareCredentialWithInput(async () => {
|
|
6310
7144
|
let requestedName = args.name;
|
|
6311
7145
|
const resumableInstalls = () => listInstallationManifests().filter((manifest) => isPlatformInstallInProgress(manifest) && (!args.account || manifest.account.id === args.account || manifest.account.name.toLowerCase() === args.account.toLowerCase()));
|
|
@@ -6321,21 +7155,42 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6321
7155
|
};
|
|
6322
7156
|
if (!args.resume && process.stdin.isTTY) {
|
|
6323
7157
|
const installs = resumableInstalls().filter((manifest) => !args.name || manifest.deployment.installationSlug === normalizeInstallationSlug(args.name));
|
|
6324
|
-
|
|
7158
|
+
const manifests = listInstallationManifests();
|
|
7159
|
+
const savedPlans = !args.applicationDomain && !args.workersDev && !args.zone && !args.displayName && !args.hyperdrive && !args.controlPlaneDomain && !args.noControlPlaneDomain && !args.authConfig && !args.loginMethods && !args.runtime && args.accessProtection === void 0 && args.appAccess === void 0 && !args.dedicatedZoneSpecified && args.dashboardUrl === void 0 ? listSavedPlatformInstallPlans().filter((saved) => (!args.name || saved.args.name === normalizeInstallationSlug(args.name)) && (!args.account || saved.args.account === args.account || saved.accountName.toLowerCase() === args.account.toLowerCase()) && !manifests.some((manifest) => manifest.status !== "uninstalled" && manifest.account.id === saved.args.account && manifest.deployment.installationSlug === saved.args.name)) : [];
|
|
7160
|
+
if (installs.length > 0 || savedPlans.length > 0) {
|
|
6325
7161
|
const choice = await select({
|
|
6326
7162
|
message: "Continue a previous platform install?",
|
|
6327
|
-
options: [
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
7163
|
+
options: [
|
|
7164
|
+
...installs.map((install) => ({
|
|
7165
|
+
value: install.id,
|
|
7166
|
+
label: `Continue setup for ${install.deployment.displayName} (${install.deployment.installationSlug})`,
|
|
7167
|
+
hint: install.account.name
|
|
7168
|
+
})),
|
|
7169
|
+
...savedPlans.map((saved) => ({
|
|
7170
|
+
value: `plan:${saved.id}`,
|
|
7171
|
+
label: `Use saved plan for ${saved.args.displayName} (${saved.args.name})`,
|
|
7172
|
+
hint: `${saved.accountName} · ${saved.args.workersDev ? "workers.dev" : saved.args.applicationDomain}`
|
|
7173
|
+
})),
|
|
7174
|
+
{
|
|
7175
|
+
value: "new",
|
|
7176
|
+
label: "Start a new platform install"
|
|
7177
|
+
}
|
|
7178
|
+
]
|
|
6335
7179
|
});
|
|
6336
7180
|
if (isCancel(choice) || typeof choice === "symbol") throw new Error("platform: Cancelled.");
|
|
6337
7181
|
if (choice === "new") requestedName = "";
|
|
6338
|
-
else {
|
|
7182
|
+
else if (choice.startsWith("plan:")) {
|
|
7183
|
+
const saved = savedPlans.find((plan) => `plan:${plan.id}` === choice);
|
|
7184
|
+
if (!saved) throw new Error("platform: The selected plan is no longer available.");
|
|
7185
|
+
args = {
|
|
7186
|
+
...saved.args,
|
|
7187
|
+
plan: args.plan,
|
|
7188
|
+
yes: args.yes
|
|
7189
|
+
};
|
|
7190
|
+
runtime = loadPlatformRuntime(args.runtime);
|
|
7191
|
+
installationName = args.name;
|
|
7192
|
+
requestedName = args.name;
|
|
7193
|
+
} else {
|
|
6339
7194
|
const selected = installs.find((install) => install.id === choice);
|
|
6340
7195
|
if (!selected) throw new Error("platform: The selected setup is no longer available.");
|
|
6341
7196
|
selectInstall(selected);
|
|
@@ -6379,6 +7234,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6379
7234
|
continue;
|
|
6380
7235
|
}
|
|
6381
7236
|
assertNewNameAvailable(slug);
|
|
7237
|
+
installationName = name.trim();
|
|
6382
7238
|
return slug;
|
|
6383
7239
|
}
|
|
6384
7240
|
if (existing.endpoints.apiUrl && !readPendingInstallationSecrets(existing)) throw new Error(`platform: Installation ${existing.id} has already been deployed. Use \`void platform repair ${existing.id}\` or \`void platform upgrade ${existing.id}\` instead.`);
|
|
@@ -6407,12 +7263,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6407
7263
|
if (isCancel(mode)) throw new Error("platform: Cancelled.");
|
|
6408
7264
|
workersDev = mode === "workers-dev";
|
|
6409
7265
|
}
|
|
6410
|
-
const applicationDomain = recorded?.application.baseDomain ?? (workersDev ? "" : normalizeApplicationDomain(args.applicationDomain || await promptText("Application base domain:", "example.
|
|
7266
|
+
const applicationDomain = recorded?.application.baseDomain ?? (workersDev ? "" : normalizeApplicationDomain(args.applicationDomain || await promptText("Application base domain:", { placeholder: "example.com" })));
|
|
6411
7267
|
const zoneName = recorded?.application.zoneName ?? (workersDev ? "" : normalizeApplicationDomain(args.zone || await promptText("Cloudflare zone:", applicationDomain)));
|
|
6412
|
-
const displayName = recorded?.displayName ?? (args.displayName || await promptText("Platform display name:", `${installationSlug} Void`));
|
|
6413
|
-
|
|
7268
|
+
const displayName = recorded?.displayName ?? (args.displayName || await promptText("Platform display name:", `${installationName || installationSlug} Void`));
|
|
7269
|
+
let controlPlaneDomain = recorded ? recorded.controlPlane.apiHostname ?? "" : args.controlPlaneDomain || (process.stdin.isTTY && !args.noControlPlaneDomain ? await promptText("Where do you want to host the Void Platform API? Leave empty for a workers.dev fallback.", { placeholder: applicationDomain || "platform.example.com" }, true) : "");
|
|
6414
7270
|
let dedicatedZone = recorded?.application.dedicatedZone ?? args.dedicatedZone;
|
|
6415
|
-
if (!workersDev && !recorded && !args.dedicatedZone && applicationDomain === zoneName && process.stdin.isTTY) {
|
|
7271
|
+
if (!workersDev && !recorded && !args.dedicatedZone && !args.dedicatedZoneSpecified && applicationDomain === zoneName && process.stdin.isTTY) {
|
|
6416
7272
|
const catchAll = await confirm({
|
|
6417
7273
|
message: `Dedicate all unmatched ${zoneName} traffic to Void apps?`,
|
|
6418
7274
|
initialValue: false
|
|
@@ -6437,6 +7293,26 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6437
7293
|
}
|
|
6438
7294
|
const checkpoint = existing ? readPendingInstallationSecrets(existing) : null;
|
|
6439
7295
|
const databaseUrl = resolvePlatformInstallDatabaseUrl(checkpoint, existing);
|
|
7296
|
+
let authoritativeProvisionIntents = false;
|
|
7297
|
+
if (existing && !isPlatformSetupDraft(existing)) {
|
|
7298
|
+
if (!databaseUrl) throw new Error("platform: DATABASE_URL is required to verify an interrupted installation before resuming it.");
|
|
7299
|
+
progress = startProgress("Reading the saved installation");
|
|
7300
|
+
const authoritative = await readPlatformDatabaseLifecycleManifest(databaseUrl, existing.id, existing.ownershipNonce);
|
|
7301
|
+
progress?.stop("Read the saved installation.");
|
|
7302
|
+
progress = void 0;
|
|
7303
|
+
if (authoritative) {
|
|
7304
|
+
const { selectAuthoritativePlatformManifest } = await import("./platform-management-DG47jys8.mjs");
|
|
7305
|
+
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
7306
|
+
authoritativeProvisionIntents = true;
|
|
7307
|
+
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
7308
|
+
existing = selected;
|
|
7309
|
+
if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
const savedHyperdrive = readPlatformHyperdriveSelection(checkpoint);
|
|
7313
|
+
const requestedHyperdriveId = args.hyperdrive || savedHyperdrive?.id;
|
|
7314
|
+
const recordedHyperdrive = existing?.resources.find((resource) => resource.key === "hyperdrive");
|
|
7315
|
+
if (savedHyperdrive && requestedHyperdriveId !== savedHyperdrive.id || recordedHyperdrive && requestedHyperdriveId && (requestedHyperdriveId === "create" ? recordedHyperdrive.ownership === "adopted" : recordedHyperdrive.id !== requestedHyperdriveId) || savedHyperdrive && recordedHyperdrive && (recordedHyperdrive.ownership !== "adopted" || recordedHyperdrive.id !== savedHyperdrive.id)) throw new Error("platform: The selected Hyperdrive differs from the saved installation. Resume with its original Hyperdrive.");
|
|
6440
7316
|
const externalHyperdriveId = process.env.VOID_PLATFORM_HYPERDRIVE_ID?.trim() || "";
|
|
6441
7317
|
const externalHyperdriveHost = process.env.VOID_PLATFORM_HYPERDRIVE_ORIGIN_HOST?.trim() || "";
|
|
6442
7318
|
const externalHyperdriveUser = process.env.VOID_PLATFORM_HYPERDRIVE_ORIGIN_USER?.trim() || "";
|
|
@@ -6454,11 +7330,14 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6454
7330
|
host: externalHyperdriveHost,
|
|
6455
7331
|
user: externalHyperdriveUser
|
|
6456
7332
|
} : void 0;
|
|
6457
|
-
if (
|
|
7333
|
+
if (requestedHyperdriveId && externalHyperdrive && requestedHyperdriveId !== externalHyperdrive.id) throw new Error("platform: --hyperdrive conflicts with VOID_PLATFORM_HYPERDRIVE_ID.");
|
|
7334
|
+
if (savedHyperdrive && externalHyperdrive && (savedHyperdrive.host.toLowerCase() !== externalHyperdrive.host.toLowerCase() || savedHyperdrive.user !== externalHyperdrive.user)) throw new Error("platform: The Hyperdrive origin differs from the saved installation.");
|
|
7335
|
+
if (savedHyperdrive) externalHyperdrive = recordedHyperdrive ? savedHyperdrive : void 0;
|
|
7336
|
+
if (!externalHyperdrive && requestedHyperdriveId !== "create" && (process.stdin.isTTY || requestedHyperdriveId) && (!existing || isPlatformSetupDraft(existing) || requestedHyperdriveId) && !recordedHyperdrive) {
|
|
6458
7337
|
const suggestedName = existing?.deployment.resources.hyperdrive ?? createPlatformResourceNames(installationSlug).hyperdrive;
|
|
6459
7338
|
const configurations = await client.listHyperdriveConfigs(accountId);
|
|
6460
7339
|
const suggestedNameAvailable = !configurations.some((resource) => resource.name === suggestedName);
|
|
6461
|
-
const choice = await select({
|
|
7340
|
+
const choice = requestedHyperdriveId ? "existing" : await select({
|
|
6462
7341
|
message: "How should Void set up Hyperdrive?",
|
|
6463
7342
|
options: [
|
|
6464
7343
|
...configurations.length > 0 ? [{
|
|
@@ -6478,12 +7357,12 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6478
7357
|
});
|
|
6479
7358
|
if (isCancel(choice)) throw new Error("platform: Cancelled.");
|
|
6480
7359
|
if (choice === "setup") {
|
|
6481
|
-
note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? `
|
|
7360
|
+
note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? ` such as ${suggestedName}` : ""}. Point it at the same dedicated PostgreSQL database you will give Void, use a runtime database user, and disable SQL result caching. If your organization manages Hyperdrive, ask its administrator to create it. Then rerun this command and select it from the list.\nSetup: https://developers.cloudflare.com/hyperdrive/get-started/`, import_picocolors.default.bold("Set up Hyperdrive first"));
|
|
6482
7361
|
outro("No Cloudflare or local installation state was changed.");
|
|
6483
7362
|
return;
|
|
6484
7363
|
}
|
|
6485
7364
|
if (choice === "existing") {
|
|
6486
|
-
const selectedId = await select({
|
|
7365
|
+
const selectedId = requestedHyperdriveId ?? await select({
|
|
6487
7366
|
message: "Which Hyperdrive should Void use?",
|
|
6488
7367
|
options: [...configurations].sort((a, b) => Number(b.name === suggestedName) - Number(a.name === suggestedName) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((resource) => ({
|
|
6489
7368
|
value: resource.id,
|
|
@@ -6496,43 +7375,38 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6496
7375
|
if (!selected) throw new Error("platform: The selected Hyperdrive is no longer available.");
|
|
6497
7376
|
const live = await client.getHyperdriveConfig(accountId, selected.id);
|
|
6498
7377
|
if (live.id !== selected.id || live.name !== selected.name || !live.origin.user) throw new Error(`platform: Could not verify the database user for existing Hyperdrive ${selected.name}.`);
|
|
6499
|
-
|
|
7378
|
+
if (savedHyperdrive && (live.origin.host !== savedHyperdrive.host.toLowerCase() || live.origin.user !== savedHyperdrive.user || savedHyperdrive.name && live.name !== savedHyperdrive.name || savedHyperdrive.database && live.origin.database !== savedHyperdrive.database) || existing && !isPlatformSetupDraft(existing) && live.name !== existing.deployment.resources.hyperdrive) throw new Error("platform: The Hyperdrive origin or name differs from the saved installation.");
|
|
7379
|
+
if (!requestedHyperdriveId) note([
|
|
7380
|
+
`${import_picocolors.default.bold("Database:")} ${live.origin.database}`,
|
|
7381
|
+
`${import_picocolors.default.bold("Host:")} ${live.origin.host}`,
|
|
7382
|
+
`${import_picocolors.default.bold("Port:")} ${live.origin.port ?? "configured by Cloudflare Tunnel"}`,
|
|
7383
|
+
`${import_picocolors.default.bold("User:")} ${live.origin.user}`,
|
|
7384
|
+
`${import_picocolors.default.bold("SQL result caching:")} ${live.cachingDisabled ? "disabled" : "enabled"}`
|
|
7385
|
+
].join("\n"), import_picocolors.default.bold(`Existing Hyperdrive ${live.name}`));
|
|
6500
7386
|
if (!live.cachingDisabled) throw new Error(`platform: Disable SQL result caching on existing Hyperdrive ${live.name} before installing.`);
|
|
6501
|
-
const confirmed = await confirm({
|
|
6502
|
-
message: `Use ${live.name} for this platform?`,
|
|
6503
|
-
initialValue: true
|
|
6504
|
-
});
|
|
6505
|
-
if (isCancel(confirmed) || !confirmed) throw new Error("platform: Cancelled.");
|
|
6506
7387
|
externalHyperdrive = {
|
|
6507
7388
|
id: live.id,
|
|
6508
7389
|
name: live.name,
|
|
7390
|
+
database: live.origin.database,
|
|
6509
7391
|
host: live.origin.host,
|
|
6510
7392
|
user: live.origin.user
|
|
6511
7393
|
};
|
|
6512
7394
|
}
|
|
6513
7395
|
}
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
progress?.stop("Read the saved installation.");
|
|
6520
|
-
progress = void 0;
|
|
6521
|
-
if (authoritative) {
|
|
6522
|
-
const { selectAuthoritativePlatformManifest } = await import("./platform-management-DOtss0BN.mjs");
|
|
6523
|
-
const selected = selectAuthoritativePlatformManifest(existing, authoritative);
|
|
6524
|
-
authoritativeProvisionIntents = true;
|
|
6525
|
-
if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
|
|
6526
|
-
existing = selected;
|
|
6527
|
-
if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
|
|
6528
|
-
}
|
|
7396
|
+
const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication, args);
|
|
7397
|
+
const appAccess = await selectInstallationAppAccess(authenticationSelection, existing?.deployment, args.appAccess);
|
|
7398
|
+
while (!existing && process.stdin.isTTY && !args.controlPlaneDomain && appAccess && appAccess !== "public" && controlPlaneDomain.toLowerCase().replace(/\.$/, "").endsWith(`.${applicationDomain.toLowerCase()}`)) {
|
|
7399
|
+
log.info("App Access covers subdomains. Use the domain apex or workers.dev for the platform API.");
|
|
7400
|
+
controlPlaneDomain = await promptText("Platform API hostname. Leave empty for workers.dev:", { placeholder: applicationDomain }, true);
|
|
6529
7401
|
}
|
|
6530
|
-
|
|
7402
|
+
if (appAccess && runtime.manifest.appAccessSetup !== 1) throw new Error("platform: This runtime does not support guided app access. Use the current packaged runtime.");
|
|
6531
7403
|
const planInput = {
|
|
7404
|
+
appAccess,
|
|
6532
7405
|
authentication: authenticationSelection.authentication,
|
|
6533
7406
|
accountId,
|
|
6534
7407
|
applicationDomain,
|
|
6535
7408
|
controlPlaneDomain,
|
|
7409
|
+
...args.dashboardUrl !== void 0 ? { dashboardUrl: args.dashboardUrl } : {},
|
|
6536
7410
|
dedicatedZone,
|
|
6537
7411
|
displayName,
|
|
6538
7412
|
...externalHyperdrive ? { externalHyperdrive } : {},
|
|
@@ -6542,50 +7416,118 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6542
7416
|
zoneName,
|
|
6543
7417
|
...workersDev ? { workersDev: true } : {}
|
|
6544
7418
|
};
|
|
6545
|
-
|
|
7419
|
+
const planOptions = {
|
|
6546
7420
|
authoritativeProvisionIntents,
|
|
6547
7421
|
allowUnverifiedAiGateway: true,
|
|
6548
7422
|
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
6549
|
-
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
7423
|
+
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3,
|
|
7424
|
+
deploymentExecution: runtime.manifest.deploymentExecution === 1
|
|
7425
|
+
};
|
|
7426
|
+
let plan = await createPlatformInstallPlan(client, planInput, existing, planOptions);
|
|
7427
|
+
const commandOptions = {
|
|
7428
|
+
commandName: cliCommandName(),
|
|
7429
|
+
accountId,
|
|
7430
|
+
installationSlug,
|
|
7431
|
+
displayName,
|
|
7432
|
+
applicationDomain,
|
|
7433
|
+
zoneName,
|
|
7434
|
+
controlPlaneDomain,
|
|
7435
|
+
...args.dashboardUrl !== void 0 ? { dashboardUrl: args.dashboardUrl } : {},
|
|
7436
|
+
dedicatedZone,
|
|
7437
|
+
workersDev,
|
|
7438
|
+
authConfig: args.authConfig,
|
|
7439
|
+
appAccess,
|
|
7440
|
+
authentication: authenticationSelection.authentication,
|
|
7441
|
+
hyperdrive: !existing || isPlatformSetupDraft(existing) || externalHyperdrive && !recordedHyperdrive ? externalHyperdrive?.id ?? "create" : void 0,
|
|
7442
|
+
runtime: args.runtime,
|
|
7443
|
+
...existing ? { resumeId: existing.id } : {},
|
|
7444
|
+
yes: args.yes || process.stdin.isTTY !== true
|
|
7445
|
+
};
|
|
7446
|
+
if (!existing) savePlatformInstallChoices(platformInstallCommandArguments({
|
|
7447
|
+
...commandOptions,
|
|
7448
|
+
runtime: args.runtime ? resolve(args.runtime) : "",
|
|
7449
|
+
...args.authConfig ? { authConfig: resolve(args.authConfig) } : {},
|
|
7450
|
+
yes: false
|
|
7451
|
+
}).slice(2), plan.account.name);
|
|
7452
|
+
note(formatPlatformInstallPlan(plan), import_picocolors.default.bold(args.plan ? "Read-only installation plan" : "Installation plan"));
|
|
7453
|
+
note(formatPlatformRuntime(runtime), import_picocolors.default.bold("Platform runtime"));
|
|
7454
|
+
const managementAccess = process.env.VOID_PLATFORM_ACCESS_SETUP_TOKEN ? void 0 : platformManagementAccessForAuthentication(authenticationSelection, accountId, checkpoint);
|
|
7455
|
+
const needsManagementToken = platformOperatorNeedsManagementToken(operatorCredential, plan, managementAccess);
|
|
6553
7456
|
if (args.plan) {
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
zoneName,
|
|
6562
|
-
controlPlaneDomain,
|
|
6563
|
-
dedicatedZone,
|
|
6564
|
-
workersDev,
|
|
6565
|
-
authConfig: args.authConfig,
|
|
6566
|
-
runtime: args.runtime,
|
|
6567
|
-
...existing ? { resumeId: existing.id } : {},
|
|
6568
|
-
yes: args.yes || process.stdin.isTTY !== true
|
|
6569
|
-
})}\n`);
|
|
6570
|
-
if (!existing && !args.authConfig && process.stdin.isTTY) console.log("Choose the same login methods when prompted.\n");
|
|
6571
|
-
outro("No Cloudflare or local installation state was changed.");
|
|
7457
|
+
note(formatPlatformSetupPlan(plan, {
|
|
7458
|
+
managementToken: needsManagementToken,
|
|
7459
|
+
managementAccess: !!managementAccess
|
|
7460
|
+
}), import_picocolors.default.bold("Required Credentials"));
|
|
7461
|
+
if (!existing) log.info("Plan saved locally for your next install.");
|
|
7462
|
+
outro("Plan complete and ready to execute.");
|
|
7463
|
+
console.log(`\n${import_picocolors.default.bold(existing ? "Next step: run this command to resume" : "Next, run this command to install the platform")}\n\n${import_picocolors.default.blue(formatPlatformInstallCommand(commandOptions))}\n`);
|
|
6572
7464
|
return;
|
|
6573
7465
|
}
|
|
6574
|
-
assertPlatformOperatorCredentialCanApplyPlan(operatorCredential, plan);
|
|
7466
|
+
if (!process.stdin.isTTY) assertPlatformOperatorCredentialCanApplyPlan(operatorCredential, plan, managementAccess);
|
|
6575
7467
|
if (process.stdin.isTTY && !args.yes) {
|
|
6576
7468
|
const proceed = await confirm({
|
|
6577
7469
|
message: "Apply this plan?",
|
|
6578
|
-
initialValue:
|
|
7470
|
+
initialValue: true
|
|
6579
7471
|
});
|
|
6580
7472
|
if (isCancel(proceed) || !proceed) {
|
|
6581
7473
|
outro("No changes made.");
|
|
6582
7474
|
return;
|
|
6583
7475
|
}
|
|
6584
7476
|
}
|
|
7477
|
+
let managementApiToken = operatorCredential.source === "oauth" ? checkpoint?.managementApiToken || "" : "";
|
|
7478
|
+
if (needsManagementToken || managementApiToken) {
|
|
7479
|
+
let token = managementApiToken;
|
|
7480
|
+
let openedManagementPage = false;
|
|
7481
|
+
const forgetRejectedManagementToken = () => {
|
|
7482
|
+
managementApiToken = "";
|
|
7483
|
+
if (existing && checkpoint?.managementApiToken) {
|
|
7484
|
+
delete checkpoint.managementApiToken;
|
|
7485
|
+
writePendingInstallationSecrets(existing, checkpoint);
|
|
7486
|
+
}
|
|
7487
|
+
token = "";
|
|
7488
|
+
};
|
|
7489
|
+
while (true) {
|
|
7490
|
+
if (!token) {
|
|
7491
|
+
if (!openedManagementPage) {
|
|
7492
|
+
openPlatformSetupPage(platformManagementSetupPage(plan, { access: managementAccess ?? false }));
|
|
7493
|
+
openedManagementPage = true;
|
|
7494
|
+
}
|
|
7495
|
+
token = (await promptSecret("Paste management API token:", true, (value) => value.trim() ? void 0 : "A value is required.")).trim();
|
|
7496
|
+
}
|
|
7497
|
+
const management = new PlatformCloudflareClient(token, { apiBase: client.apiBaseUrl });
|
|
7498
|
+
try {
|
|
7499
|
+
if (!(await management.listAccounts()).some((account) => account.id === accountId)) {
|
|
7500
|
+
forgetRejectedManagementToken();
|
|
7501
|
+
log.warn(`This token cannot access ${plan.account.name}. Check its account scope and try again.`);
|
|
7502
|
+
continue;
|
|
7503
|
+
}
|
|
7504
|
+
const verified = await createPlatformInstallPlan(management, planInput, existing, planOptions);
|
|
7505
|
+
assertPlatformInstallPlanUnchanged(plan, verified, "management API token");
|
|
7506
|
+
if (managementAccess) await verifyPlatformManagementAccess(management, accountId, managementAccess);
|
|
7507
|
+
client = management;
|
|
7508
|
+
plan = verified;
|
|
7509
|
+
managementApiToken = token;
|
|
7510
|
+
break;
|
|
7511
|
+
} catch (error) {
|
|
7512
|
+
if (!isPlatformTokenAuthorizationError(error)) throw error;
|
|
7513
|
+
forgetRejectedManagementToken();
|
|
7514
|
+
log.warn("Cloudflare rejected this token. Check its permissions and account/zone scope, then paste a replacement.");
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
} else if (managementAccess) await verifyPlatformManagementAccess(client, accountId, managementAccess);
|
|
6585
7518
|
if (!existing) existing = savePlatformSetupDraft(plan, runtime.manifest.platformVersion);
|
|
7519
|
+
removeSavedPlatformInstallPlan({
|
|
7520
|
+
...args,
|
|
7521
|
+
account: accountId,
|
|
7522
|
+
name: installationSlug
|
|
7523
|
+
});
|
|
6586
7524
|
const setupManifest = existing;
|
|
6587
7525
|
const credentialSetup = isPlatformSetupDraft(existing);
|
|
6588
7526
|
const values = { ...checkpoint };
|
|
7527
|
+
if (managementAccess) delete values.accessManagementToken;
|
|
7528
|
+
if (managementApiToken) values.managementApiToken = managementApiToken;
|
|
7529
|
+
if (externalHyperdrive) values.externalHyperdriveJson = JSON.stringify(externalHyperdrive);
|
|
7530
|
+
if (managementApiToken || externalHyperdrive || managementAccess) writePendingInstallationSecrets(setupManifest, values);
|
|
6589
7531
|
const saveValues = (updates) => {
|
|
6590
7532
|
Object.assign(values, updates);
|
|
6591
7533
|
if (credentialSetup) writePendingInstallationSecrets(setupManifest, values);
|
|
@@ -6623,7 +7565,7 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6623
7565
|
plan = await verifyPlatformAiGatewayPlan(client, planInput, existing, plan, authoritativeProvisionIntents);
|
|
6624
7566
|
}
|
|
6625
7567
|
} catch (error) {
|
|
6626
|
-
if (credentialSetup && token === values.cloudflareApiToken &&
|
|
7568
|
+
if (credentialSetup && token === values.cloudflareApiToken && isPlatformTokenAuthorizationError(error, checkingRuntimePermissions ? void 0 : "/ai-gateway/")) {
|
|
6627
7569
|
delete values.cloudflareApiToken;
|
|
6628
7570
|
writePendingInstallationSecrets(setupManifest, values);
|
|
6629
7571
|
rejectedSavedRuntimeToken = true;
|
|
@@ -6740,15 +7682,30 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6740
7682
|
accessCredentialsJson: values.accessCredentialsJson
|
|
6741
7683
|
};
|
|
6742
7684
|
} } : {},
|
|
7685
|
+
...appAccess ? { prepareAppAccess: async () => {
|
|
7686
|
+
progress?.stop("Preparing deployed-app access.");
|
|
7687
|
+
progress = void 0;
|
|
7688
|
+
const setupJson = await prepareInstallationAppAccess({
|
|
7689
|
+
plan,
|
|
7690
|
+
authentication: authenticationSelection,
|
|
7691
|
+
client,
|
|
7692
|
+
managementToken: values.managementApiToken || (operatorCredential.source !== "oauth" ? operatorCredential.token : void 0),
|
|
7693
|
+
checkpoint: values,
|
|
7694
|
+
save: saveAuthenticationValues
|
|
7695
|
+
});
|
|
7696
|
+
progress = startProgress("Continuing platform installation");
|
|
7697
|
+
return setupJson;
|
|
7698
|
+
} } : {},
|
|
6743
7699
|
replanWorkers: async (manifest) => (await createPlatformInstallPlan(client, planInput, manifest, {
|
|
6744
7700
|
authoritativeProvisionIntents: true,
|
|
6745
7701
|
repairMissing: true,
|
|
6746
7702
|
includeEmailGateway: runtime.manifest.artifacts.emailGateway !== void 0,
|
|
6747
|
-
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3
|
|
7703
|
+
enableSandbox: runtime.manifest.safetyProtocols.sandboxDrain >= 3,
|
|
7704
|
+
deploymentExecution: runtime.manifest.deploymentExecution === 1
|
|
6748
7705
|
})).actions.filter((action) => action.kind === "worker"),
|
|
6749
7706
|
revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
|
|
6750
7707
|
verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
|
|
6751
|
-
const { verifyPlatformInstallationOwnership } = await import("./platform-management-
|
|
7708
|
+
const { verifyPlatformInstallationOwnership } = await import("./platform-management-DG47jys8.mjs");
|
|
6752
7709
|
const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
|
|
6753
7710
|
const previous = manifest.resources.find((resource) => resource.key === action.key);
|
|
6754
7711
|
return action.action === "create" && previous && previous.name !== action.name;
|
|
@@ -6760,21 +7717,21 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6760
7717
|
});
|
|
6761
7718
|
}
|
|
6762
7719
|
});
|
|
6763
|
-
progress?.stop(result.waitingForDns || result.waitingForSandboxDrain ? "Installation paused
|
|
7720
|
+
progress?.stop(result.waitingForDns || result.waitingForSandboxDrain ? "Installation paused. Progress is saved." : "Platform installed.");
|
|
6764
7721
|
progress = void 0;
|
|
6765
7722
|
if (result.waitingForDns) {
|
|
6766
|
-
note(result.zoneNameServers.length > 0 ? `Update the domain at your registrar to use:\n${result.zoneNameServers.join("\n")}\n\nThen rerun: void platform install --resume --name ${result.manifest.id}` : `Wait for ${zoneName} to become active, then rerun:\nvoid platform install --resume --name ${result.manifest.id}`, "Waiting for nameservers");
|
|
7723
|
+
note(result.zoneNameServers.length > 0 ? `Update the domain at your registrar to use:\n${result.zoneNameServers.join("\n")}\n\nThen rerun: void platform install --resume --name ${result.manifest.id}` : `Wait for ${zoneName} to become active, then rerun:\nvoid platform install --resume --name ${result.manifest.id}`, import_picocolors.default.bold("Waiting for nameservers"));
|
|
6767
7724
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
6768
7725
|
return;
|
|
6769
7726
|
}
|
|
6770
7727
|
if (result.waitingForSandboxDrain) {
|
|
6771
|
-
note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan
|
|
7728
|
+
note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan`. Follow any returned `nextCursor` with `--cursor '<value>'`. Then apply with `void platform system sandbox-drain --yes` and rerun until it reports DB-backed `complete: true`. Rerun this install command afterward to enable traffic.", import_picocolors.default.bold("Waiting for managed Sandbox drain"));
|
|
6772
7729
|
outro(`Installation ${result.manifest.id} is safely checkpointed.`);
|
|
6773
7730
|
return;
|
|
6774
7731
|
}
|
|
6775
7732
|
log.success(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
|
|
6776
7733
|
log.info(`API URL: ${plan.apiUrl}`);
|
|
6777
|
-
if (result.emailOperationId) log.info(`Shared email routing ready
|
|
7734
|
+
if (result.emailOperationId) log.info(`Shared email routing ready. Operation: ${result.emailOperationId}.`);
|
|
6778
7735
|
const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
|
|
6779
7736
|
if (result.setupPending && completedAuthCode) {
|
|
6780
7737
|
log.info(`First administrator setup: ${plan.apiUrl}/setup`);
|
|
@@ -6796,16 +7753,16 @@ async function runPlatformLifecycleCommand(args) {
|
|
|
6796
7753
|
}
|
|
6797
7754
|
}
|
|
6798
7755
|
}
|
|
6799
|
-
function
|
|
7756
|
+
function isPlatformTokenAuthorizationError(error, requestPathIncludes) {
|
|
6800
7757
|
for (let depth = 0; depth < 5 && error instanceof Error; depth++, error = error.cause) if (error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status) && (!requestPathIncludes || error.requestPath?.includes(requestPathIncludes))) return true;
|
|
6801
7758
|
return false;
|
|
6802
7759
|
}
|
|
6803
7760
|
function formatPlatformRuntime(runtime) {
|
|
6804
7761
|
return [
|
|
6805
|
-
`${import_picocolors
|
|
6806
|
-
`${import_picocolors
|
|
6807
|
-
`${import_picocolors
|
|
6808
|
-
`${import_picocolors
|
|
7762
|
+
`${import_picocolors.default.bold("Version:")} ${runtime.manifest.platformVersion}`,
|
|
7763
|
+
`${import_picocolors.default.bold("Source:")} ${runtime.source}`,
|
|
7764
|
+
`${import_picocolors.default.bold("Revision:")} ${runtime.manifest.sourceRevision ?? "not recorded"}`,
|
|
7765
|
+
`${import_picocolors.default.bold("Manifest SHA-256:")} ${runtime.manifestSha256}`
|
|
6809
7766
|
].join("\n");
|
|
6810
7767
|
}
|
|
6811
7768
|
async function verifyPlatformAiGatewayPlan(client, input, existing, preview, authoritativeProvisionIntents) {
|
|
@@ -6814,12 +7771,17 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
|
|
|
6814
7771
|
verified = await createPlatformInstallPlan(client, input, existing, {
|
|
6815
7772
|
authoritativeProvisionIntents,
|
|
6816
7773
|
includeEmailGateway: preview.actions.some((entry) => entry.key === "email-gateway-worker"),
|
|
6817
|
-
enableSandbox: preview.deployment.capabilities.sandbox
|
|
7774
|
+
enableSandbox: preview.deployment.capabilities.sandbox,
|
|
7775
|
+
deploymentExecution: preview.actions.some((entry) => entry.key === "deployment-queue")
|
|
6818
7776
|
});
|
|
6819
7777
|
} catch (error) {
|
|
6820
7778
|
if (error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403) && error.requestPath?.includes("/ai-gateway/")) throw new Error("platform: The runtime token could not verify AI Gateway access. Include Account → AI Gateway → Edit for this account, then retry. No platform resources were created.", { cause: error });
|
|
6821
7779
|
throw error;
|
|
6822
7780
|
}
|
|
7781
|
+
assertPlatformInstallPlanUnchanged(preview, verified, "AI Gateway access");
|
|
7782
|
+
return verified;
|
|
7783
|
+
}
|
|
7784
|
+
function assertPlatformInstallPlanUnchanged(preview, verified, check) {
|
|
6823
7785
|
const infrastructure = (plan) => plan.actions.filter((action) => action.kind !== "ai-gateway").map(({ action, id, key, kind, name }) => ({
|
|
6824
7786
|
action,
|
|
6825
7787
|
id,
|
|
@@ -6827,8 +7789,7 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
|
|
|
6827
7789
|
kind,
|
|
6828
7790
|
name
|
|
6829
7791
|
}));
|
|
6830
|
-
if (verified.installationId !== preview.installationId || JSON.stringify(verified.deployment) !== JSON.stringify(preview.deployment) || JSON.stringify(infrastructure(verified)) !== JSON.stringify(infrastructure(preview))) throw new Error(
|
|
6831
|
-
return verified;
|
|
7792
|
+
if (verified.installationId !== preview.installationId || JSON.stringify(verified.deployment) !== JSON.stringify(preview.deployment) || JSON.stringify(infrastructure(verified)) !== JSON.stringify(infrastructure(preview))) throw new Error(`platform: Resources changed while checking ${check}. Rerun the command to review the updated plan. No platform resources were created.`);
|
|
6832
7793
|
}
|
|
6833
7794
|
function assertNoninteractivePlatformMutationAcknowledged(args, interactive) {
|
|
6834
7795
|
if (args.subcommand === "list" || args.subcommand === "use" || args.subcommand === "status" || args.subcommand === "discover" || args.plan || interactive || args.yes) return;
|
|
@@ -6842,7 +7803,8 @@ async function readAccounts(client) {
|
|
|
6842
7803
|
async function selectAccount(accounts, selector) {
|
|
6843
7804
|
if (selector) {
|
|
6844
7805
|
const matches = accounts.filter((account) => account.id === selector || account.name.toLowerCase() === selector.toLowerCase());
|
|
6845
|
-
if (matches.length
|
|
7806
|
+
if (matches.length === 0) throw new Error(`platform: Cloudflare account ${JSON.stringify(selector)} is unavailable through the current access token. Run ${import_picocolors.default.blue("void cloudflare status")} to see which accounts have access, and re-authenticate if necessary.`);
|
|
7807
|
+
if (matches.length > 1) throw new Error(`platform: Cloudflare account ${JSON.stringify(selector)} is ambiguous. Use --account <id> to select a specific account.`);
|
|
6846
7808
|
return matches[0].id;
|
|
6847
7809
|
}
|
|
6848
7810
|
if (accounts.length === 1) return accounts[0].id;
|
|
@@ -6863,10 +7825,17 @@ async function promptText(message, value, optional = false, validate) {
|
|
|
6863
7825
|
if (optional) return "";
|
|
6864
7826
|
throw new Error(`platform: ${message.replace(/:$/, "")} is required in non-interactive mode.`);
|
|
6865
7827
|
}
|
|
7828
|
+
const defaultValue = typeof value === "string" ? value : void 0;
|
|
6866
7829
|
const input = await text$1({
|
|
6867
7830
|
message,
|
|
6868
|
-
...typeof value === "string" ? {
|
|
6869
|
-
|
|
7831
|
+
...typeof value === "string" ? {
|
|
7832
|
+
placeholder: value,
|
|
7833
|
+
defaultValue
|
|
7834
|
+
} : value,
|
|
7835
|
+
validate: (input) => {
|
|
7836
|
+
const candidate = input || defaultValue;
|
|
7837
|
+
return candidate ? validate?.(candidate) : optional ? void 0 : "A value is required.";
|
|
7838
|
+
}
|
|
6870
7839
|
});
|
|
6871
7840
|
if (isCancel(input) || typeof input === "symbol") throw new Error("platform: Cancelled.");
|
|
6872
7841
|
return input;
|