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.
Files changed (301) hide show
  1. package/AGENTS_TEMPLATE.md +1 -1
  2. package/README.md +5 -1
  3. package/dist/{account-cmd-CjyxcGkM.mjs → account-cmd-vJhh5lrb.mjs} +5 -5
  4. package/dist/{scan-ClYmX3sa.mjs → application-analysis-BVsVqc11.mjs} +135 -18
  5. package/dist/application-routing-B7QEjqSR.mjs +27 -0
  6. package/dist/{auth-DLNN0D3Z.mjs → auth-CQkSDS50.mjs} +26 -96
  7. package/dist/auth-DX3JURcX.mjs +2 -0
  8. package/dist/{auth-link-BLO4ptzo.mjs → auth-link-D75t9wO2.mjs} +5 -5
  9. package/dist/{auth-router-ZUg9MF_U.mjs → auth-router-ClfhL1J7.mjs} +7 -7
  10. package/dist/{better-auth-shared-rsBGBvWJ.mjs → better-auth-shared-hy6RPh9W.mjs} +13 -2
  11. package/dist/{build-cmd-BxR5FROK.mjs → build-cmd-zMi1iFnS.mjs} +21 -9
  12. package/dist/{cache-D0sWhgKI.mjs → cache-ZtQmyxVV.mjs} +5 -5
  13. package/dist/{cancel-deploy-D4NUqFbi.mjs → cancel-deploy-Box0kkEV.mjs} +5 -5
  14. package/dist/{cf-access-P0iZnu0q.mjs → cf-access-BGe5sWXF.mjs} +4 -48
  15. package/dist/{cf-access-DRsQRe6k.mjs → cf-access-TWNzXltp.mjs} +3 -11
  16. package/dist/{cf-build-output-BJ6yGEIS.mjs → cf-build-output-BPqOT964.mjs} +2 -1
  17. package/dist/cf-build-output-_0HNWysu.mjs +2 -0
  18. package/dist/cli/cli.mjs +89 -461
  19. package/dist/cli/{cf-compat.mjs → cloudflare-operation-process.mjs} +454 -424
  20. package/dist/cli/env-schema-probe.d.mts +2 -1
  21. package/dist/cli/env-schema-probe.mjs +4 -4
  22. package/dist/client-B9UBI03V.mjs +2 -0
  23. package/dist/{client-BTZ3XkrB.mjs → client-DKAsFA4X.mjs} +161 -24
  24. package/dist/{cloudflare-auth-DQkqoMYa.mjs → cloudflare-auth-Qdc7tw8F.mjs} +41 -38
  25. package/dist/{cloudflare-cmd-BKlfGHAy.mjs → cloudflare-cmd-CmY8dGkd.mjs} +6 -6
  26. package/dist/cloudflare-config-Bktvwtpf.mjs +182 -0
  27. package/dist/{cloudflare-connect-Ctmrw579.mjs → cloudflare-connect-IiR3ZieC.mjs} +4 -4
  28. package/dist/{cloudflare-operations-BT6OWFBk.mjs → cloudflare-operations-BCdXSsmo.mjs} +1 -1
  29. package/dist/{cloudflare-operations-B9tzgjmf.mjs → cloudflare-operations-DzH-A966.mjs} +116 -172
  30. package/dist/{preset-UHj9ARyP.mjs → cloudflare-process-B-wekeR6.mjs} +93 -6
  31. package/dist/{config-VavjpDnp.d.mts → config-BMHb8RCj.d.mts} +1 -0
  32. package/dist/config-C_XRIPx2.mjs +89 -0
  33. package/dist/config-entry.d.mts +1 -1
  34. package/dist/{connect-Dg-WkW-C.mjs → connect-B-usTnlF.mjs} +9 -9
  35. package/dist/{create-project-l9J7pbtm.mjs → create-project-CYOOAYcn.mjs} +12 -12
  36. package/dist/{create-project-DdoqwFLF.mjs → create-project-FWbf5Olo.mjs} +1 -1
  37. package/dist/{db-gp2sCXyN.mjs → db-Ck2eZ2iB.mjs} +26 -26
  38. package/dist/{delete-TTedbH6B.mjs → delete-sK56qtzZ.mjs} +5 -5
  39. package/dist/{deploy-CH-o2CaZ.mjs → deploy-gCVaiJbS.mjs} +984 -616
  40. package/dist/{deploy-B19L2Bra.mjs → deploy-irP5q0-x.mjs} +1 -1
  41. package/dist/{domain-JRF59P_r.mjs → domain-DFfQE8jg.mjs} +6 -6
  42. package/dist/{email-Bo6G9LOZ.mjs → email-CdnhUHvJ.mjs} +15 -30
  43. package/dist/{env-BYOrWQnv.mjs → env-DmGdHi5i.mjs} +8 -8
  44. package/dist/{env-public-BxU_0yTL.d.mts → env-public-BX_r8HR6.d.mts} +1 -1
  45. package/dist/{env-types-C2_bIk_b.mjs → env-types-D5kds_OZ.mjs} +1 -1
  46. package/dist/{env-validation-BB4GkLxn.mjs → env-validation-C_8tPdhs.mjs} +20 -73
  47. package/dist/{env-validation-BXge7uyK.mjs → env-validation-zZOCcQVZ.mjs} +1 -1
  48. package/dist/{fetch-CXDChK7B.mjs → fetch-BIZJh7vR.mjs} +2 -2
  49. package/dist/{fetch-stream-AOByI7Ki.mjs → fetch-stream-IvCYKyQL.mjs} +24 -4
  50. package/dist/{gen-sCtlCOdA.mjs → gen-CshAkukR.mjs} +4 -4
  51. package/dist/gen-DvQTWzDU.mjs +2 -0
  52. package/dist/{github-cmd-0-7PexDq.mjs → github-cmd-DftuEvIt.mjs} +13 -13
  53. package/dist/{handler-HEcZsaij.d.mts → handler-CZ4nAylQ.d.mts} +1 -1
  54. package/dist/{headers-DWi2IXWx.mjs → headers-2rKda9Y6.mjs} +1 -1
  55. package/dist/{help-GKtwl07I.mjs → help-Dotbf6iz.mjs} +236 -750
  56. package/dist/help-mabuIV98.mjs +2 -0
  57. package/dist/{inbound-aVHEUhKo.mjs → inbound-fRkqcXaC.mjs} +1 -1
  58. package/dist/index.d.mts +1 -1
  59. package/dist/index.mjs +299 -1517
  60. package/dist/info-B97bTX9N.mjs +113 -0
  61. package/dist/{init-FZx3Elvz.mjs → init-BTQEZZGJ.mjs} +61 -27
  62. package/dist/limits-Bq5LG8Id.d.mts +27 -0
  63. package/dist/limits-Cjuk2VPm.mjs +68 -0
  64. package/dist/{link-BrQfb_CU.mjs → link-B46sYH0K.mjs} +6 -6
  65. package/dist/{list-D44jmIAM.mjs → list-BnPrIsU_.mjs} +6 -6
  66. package/dist/{live-CKJlvlNp.d.mts → live-Chw1eIMv.d.mts} +1 -1
  67. package/dist/{local-d1-Bg9OzEEO.mjs → local-d1-2CMnpuW_.mjs} +2 -2
  68. package/dist/log-vqY3pqwI.mjs +29 -0
  69. package/dist/{login-CAsAsQ-_.mjs → login-BuiMyUrx.mjs} +8 -8
  70. package/dist/login-DIy9Sb-U.mjs +2 -0
  71. package/dist/{logs-BjKvFnVM.mjs → logs-CZG9d-ic.mjs} +10 -11
  72. package/dist/migrate-BCiJ8w0f.mjs +2 -0
  73. package/dist/{migrate-BPITvDJN.mjs → migrate-q4_njtaW.mjs} +4 -3
  74. package/dist/{node-Dt7z256D.mjs → node-BkyRWRx8.mjs} +1 -1
  75. package/dist/operator-args-B3BTmSoi.mjs +697 -0
  76. package/dist/{operator-auth-8KnOVXVd.mjs → operator-auth-7erYc6Lt.mjs} +2 -2
  77. package/dist/{operator-client-DUwumHpQ.mjs → operator-client-CpHjTd0t.mjs} +3 -3
  78. package/dist/{operator-cmd-BK5CCiT9.mjs → operator-cmd-DGqPJUD3.mjs} +70 -24
  79. package/dist/{output-Dm_A4Tbv.mjs → output-CbM85T4k.mjs} +18 -10
  80. package/dist/pages/client.d.mts +34 -2
  81. package/dist/pages/client.mjs +59 -3
  82. package/dist/pages/index.d.mts +1 -1
  83. package/dist/pages/index.mjs +2 -2
  84. package/dist/pages/islands-plugin.mjs +1 -1
  85. package/dist/pages/protocol.d.mts +2 -2
  86. package/dist/pages/protocol.mjs +2 -308
  87. package/dist/{parse-filename-DioPHiR9.mjs → parse-filename-CUbj-1MP.mjs} +35 -1
  88. package/dist/plan-D1Q5rf-r.mjs +2 -0
  89. package/dist/plan-NPpwZ_kc.mjs +58 -0
  90. package/dist/platform-args-3k8X9-H_.mjs +514 -0
  91. package/dist/platform-args-DdnM6WIf.mjs +2 -0
  92. package/dist/{platform-auth-config-BlN8xTdD.mjs → platform-auth-config-DDrxUy4m.mjs} +6 -6
  93. package/dist/{platform-auth-protection-7d5aV2Jg.mjs → platform-auth-protection-BLadnnct.mjs} +8 -8
  94. package/dist/{platform-auth-recovery-Dxij8ZbR.mjs → platform-auth-recovery-CmCzX2Fl.mjs} +6 -6
  95. package/dist/{platform-cmd-B3hwKrFK.mjs → platform-cmd-DMDEc9uS.mjs} +29 -8
  96. package/dist/{platform-cmd-E0FuL212.mjs → platform-cmd-pMTRhvYs.mjs} +1 -1
  97. package/dist/{platform-domain-D6Xcy9ZX.mjs → platform-domain-Di6EYrQl.mjs} +5 -5
  98. package/dist/{platform-lifecycle-k0E0xoxx.mjs → platform-lifecycle-BBoNzojY.mjs} +1541 -572
  99. package/dist/{platform-lifecycle-DMh_qrry.mjs → platform-lifecycle-BFdSi9Qt.mjs} +1 -1
  100. package/dist/{platform-management-Brz_BDiT.mjs → platform-management-CWOQOWXm.mjs} +10 -7
  101. package/dist/{platform-management-DOtss0BN.mjs → platform-management-DG47jys8.mjs} +1 -1
  102. package/dist/platform-plans-config-Vl9BP3H4.mjs +359 -0
  103. package/dist/{platform-recovery-BvtcXON5.mjs → platform-recovery-BJnQJEtQ.mjs} +4 -4
  104. package/dist/{platform-registry-QBa4DK32.mjs → platform-registry-DkPPXfp_.mjs} +2 -2
  105. package/dist/{plugin-inference-BMfKRSqE.mjs → plugin-inference-CXWnn79A.mjs} +174 -64
  106. package/dist/{prepare-C-6YZyyg.mjs → prepare-CBLT3CHH.mjs} +3 -3
  107. package/dist/prepare-CnjcTV2s.mjs +2 -0
  108. package/dist/{prepare-CRhJbVrG.mjs → prepare-DnaKBi--.mjs} +5 -21
  109. package/dist/{project-C0Fqu05O.mjs → project-DKG5Ra8p.mjs} +15 -82
  110. package/dist/project-DsblYifa.mjs +2 -0
  111. package/dist/{project-cmd-BZLCqOqw.mjs → project-cmd-DxnXstAq.mjs} +19 -19
  112. package/dist/{project-team-BvgM0WoX.mjs → project-team-BmHcaALJ.mjs} +5 -5
  113. package/dist/{project-token-l5DqK6Az.mjs → project-token-GionZlBI.mjs} +5 -5
  114. package/dist/{project-zero-trust-v6Mvpp8y.mjs → project-zero-trust-JLCjVQA7.mjs} +5 -5
  115. package/dist/prompts-2TGE6oXx.mjs +2 -0
  116. package/dist/prompts-BNUoTiaf.mjs +2044 -0
  117. package/dist/{protocol-BBa6cstI.d.mts → protocol-CjF_iI9X.d.mts} +2 -2
  118. package/dist/protocol-U7bfjHmA.mjs +331 -0
  119. package/dist/{provision-Fr9pRqce.mjs → provision-CVku1JFU.mjs} +1 -1
  120. package/dist/{provision-C4IGqkBf.mjs → provision-CaoxO_U7.mjs} +90 -199
  121. package/dist/{requests-DN-BbNiM.mjs → requests-BOjIVAeF.mjs} +5 -5
  122. package/dist/resource-name-C7LVpcRm.mjs +11 -0
  123. package/dist/{rollback-D7rzSaZM.mjs → rollback-CwR3pLj3.mjs} +6 -6
  124. package/dist/route-url-CG7U-cRN.mjs +15 -0
  125. package/dist/{runner-B8wXwWlo.mjs → runner-CXA9Fh8h.mjs} +1 -1
  126. package/dist/{runner-p-dMs2UN.mjs → runner-Ol0TNjk6.mjs} +2 -2
  127. package/dist/runtime/ai.d.mts +12 -8
  128. package/dist/runtime/ai.mjs +87 -22
  129. package/dist/runtime/better-auth-mysql.mjs +1 -1
  130. package/dist/runtime/better-auth-pg.mjs +1 -1
  131. package/dist/runtime/better-auth.mjs +1 -1
  132. package/dist/runtime/client-react.mjs +2 -2
  133. package/dist/runtime/client-solid.mjs +2 -2
  134. package/dist/runtime/client-svelte.mjs +2 -2
  135. package/dist/runtime/client-vue.mjs +2 -2
  136. package/dist/runtime/client.mjs +2 -2
  137. package/dist/runtime/durable.d.mts +3 -1
  138. package/dist/runtime/durable.mjs +4 -1
  139. package/dist/runtime/email/testing.mjs +2 -2
  140. package/dist/runtime/email.mjs +1 -1
  141. package/dist/runtime/env-public.d.mts +1 -1
  142. package/dist/runtime/fetch-stream.mjs +1 -1
  143. package/dist/runtime/fetch.mjs +1 -1
  144. package/dist/runtime/handler.d.mts +1 -1
  145. package/dist/runtime/kv.mjs +0 -1
  146. package/dist/runtime/limits.d.mts +2 -0
  147. package/dist/runtime/limits.mjs +2 -0
  148. package/dist/runtime/live-client.d.mts +1 -1
  149. package/dist/runtime/live-server.mjs +25 -16
  150. package/dist/runtime/live.d.mts +1 -1
  151. package/dist/runtime/migration-handler.mjs +62 -42
  152. package/dist/runtime/remote/index.mjs +2 -2
  153. package/dist/runtime/route-url.d.mts +4 -0
  154. package/dist/runtime/route-url.mjs +2 -0
  155. package/dist/runtime/routing.d.mts +180 -0
  156. package/dist/runtime/routing.mjs +1082 -0
  157. package/dist/runtime/sandbox-container.d.mts +1 -1
  158. package/dist/runtime/sandbox-container.mjs +1 -1
  159. package/dist/runtime/sandbox.d.mts +3 -3
  160. package/dist/runtime/sandbox.mjs +75 -45
  161. package/dist/runtime/sse.mjs +1 -1
  162. package/dist/runtime/validator.d.mts +1 -1
  163. package/dist/runtime/ws-server.d.mts +4 -2
  164. package/dist/runtime/ws-server.mjs +27 -2
  165. package/dist/runtime/ws.d.mts +2 -2
  166. package/dist/runtime/ws.mjs +8 -6
  167. package/dist/{sandbox-qpNBT8a3.d.mts → sandbox-XZAqzFlG.d.mts} +11 -8
  168. package/dist/{sandbox-container-DdNEBfCc.d.mts → sandbox-container-Bo0eiFKz.d.mts} +2 -1
  169. package/dist/{sandbox-container-4fdqLnyb.mjs → sandbox-container-C6ItmVuN.mjs} +4 -2
  170. package/dist/{scan-4tfN-PSn.mjs → scan-C7okrLyM.mjs} +4 -35
  171. package/dist/{secret-BOtOl_cb.mjs → secret-ByWJWBUG.mjs} +7 -7
  172. package/dist/{skills-D1II1Juz.mjs → skills-C1PlclPt.mjs} +1 -1
  173. package/dist/{sse-BaC1jXko.mjs → sse-CQNaDFFV.mjs} +6 -3
  174. package/dist/{subcommand-prompt-CY1C4fvl.mjs → subcommand-prompt-klFCfcor.mjs} +40 -32
  175. package/dist/validate-Dq_L3s0S.mjs +2 -0
  176. package/dist/{validate-CIUwFpjB.mjs → validate-ctOrgiS3.mjs} +2 -1
  177. package/dist/{wrangler-DQF1vKyf.mjs → wrangler-7K-bW_DL.mjs} +8 -239
  178. package/dist/{ws-BoY7vQML.d.mts → ws-CL1w7GXU.d.mts} +13 -2
  179. package/package.json +16 -10
  180. package/skills/migrate-vite-cloudflare-to-void/SKILL.md +34 -157
  181. package/skills/void/SKILL.md +55 -135
  182. package/skills/void/docs/guide/ai.md +94 -84
  183. package/skills/void/docs/guide/app-types.md +3 -32
  184. package/skills/void/docs/guide/auth.md +12 -116
  185. package/skills/void/docs/guide/database/d1.md +9 -54
  186. package/skills/void/docs/guide/database/mysql.md +1 -1
  187. package/skills/void/docs/guide/database/postgresql.md +5 -26
  188. package/skills/void/docs/guide/database.md +23 -75
  189. package/skills/void/docs/guide/deployment.md +48 -112
  190. package/skills/void/docs/guide/durable-state.md +43 -18
  191. package/skills/void/docs/guide/edge/headers.md +4 -48
  192. package/skills/void/docs/guide/edge/prerendering.md +5 -20
  193. package/skills/void/docs/guide/edge/redirects.md +11 -64
  194. package/skills/void/docs/guide/edge/revalidation.md +5 -18
  195. package/skills/void/docs/guide/edge/rewrites.md +56 -284
  196. package/skills/void/docs/guide/edge/static-assets.md +22 -72
  197. package/skills/void/docs/guide/email/domains.md +116 -0
  198. package/skills/void/docs/guide/email/receiving.md +139 -0
  199. package/skills/void/docs/guide/email/sending.md +231 -0
  200. package/skills/void/docs/guide/email.md +13 -619
  201. package/skills/void/docs/guide/env-migration.md +11 -11
  202. package/skills/void/docs/guide/env-vars.md +9 -29
  203. package/skills/void/docs/guide/index.md +0 -15
  204. package/skills/void/docs/guide/jobs.md +3 -18
  205. package/skills/void/docs/guide/kv.md +5 -11
  206. package/skills/void/docs/guide/live.md +5 -56
  207. package/skills/void/docs/guide/pages-routing/actions-and-forms.md +78 -125
  208. package/skills/void/docs/guide/pages-routing/head.md +10 -10
  209. package/skills/void/docs/guide/pages-routing/islands.md +6 -36
  210. package/skills/void/docs/guide/pages-routing/layouts.md +6 -128
  211. package/skills/void/docs/guide/pages-routing/loaders.md +3 -19
  212. package/skills/void/docs/guide/pages-routing/markdown.md +13 -171
  213. package/skills/void/docs/guide/pages-routing/overview.md +7 -17
  214. package/skills/void/docs/guide/pages-routing/view-transitions.md +1 -1
  215. package/skills/void/docs/guide/platform/administration/access.md +1 -4
  216. package/skills/void/docs/guide/platform/administration/email.md +35 -8
  217. package/skills/void/docs/guide/platform/administration/operations.md +26 -4
  218. package/skills/void/docs/guide/platform/administration/plans.md +126 -0
  219. package/skills/void/docs/guide/platform/administration/projects.md +5 -2
  220. package/skills/void/docs/guide/platform/administration/zero-trust.md +49 -160
  221. package/skills/void/docs/guide/platform/development/runtime.md +3 -13
  222. package/skills/void/docs/guide/platform/development/schema-ci.md +0 -58
  223. package/skills/void/docs/guide/platform/installation/ci.md +1 -1
  224. package/skills/void/docs/guide/platform/installation/credentials.md +15 -6
  225. package/skills/void/docs/guide/platform/installation/domains.md +35 -2
  226. package/skills/void/docs/guide/platform/installation/first-deployment.md +4 -2
  227. package/skills/void/docs/guide/platform/installation/maintenance.md +15 -1
  228. package/skills/void/docs/guide/platform/installation/prerequisites.md +24 -18
  229. package/skills/void/docs/guide/platform/installation/setup.md +88 -14
  230. package/skills/void/docs/guide/platform-administration.md +1 -0
  231. package/skills/void/docs/guide/queues.md +7 -9
  232. package/skills/void/docs/guide/quickstart.md +38 -37
  233. package/skills/void/docs/guide/remote-dev.md +5 -10
  234. package/skills/void/docs/guide/sandboxes.md +29 -21
  235. package/skills/void/docs/guide/server-routing.md +9 -72
  236. package/skills/void/docs/guide/sse.md +4 -18
  237. package/skills/void/docs/guide/ssg.md +3 -15
  238. package/skills/void/docs/guide/ssr.md +14 -62
  239. package/skills/void/docs/guide/storage.md +9 -4
  240. package/skills/void/docs/guide/type-safety.md +3 -14
  241. package/skills/void/docs/guide/typed-fetch.md +3 -7
  242. package/skills/void/docs/guide/websockets.md +68 -40
  243. package/skills/void/docs/integrations/agents.md +3 -3
  244. package/skills/void/docs/integrations/cloudflare.md +85 -316
  245. package/skills/void/docs/integrations/frameworks/analog.md +5 -64
  246. package/skills/void/docs/integrations/frameworks/astro.md +4 -73
  247. package/skills/void/docs/integrations/frameworks/nuxt.md +5 -62
  248. package/skills/void/docs/integrations/frameworks/overview.md +11 -54
  249. package/skills/void/docs/integrations/frameworks/react-router.md +5 -60
  250. package/skills/void/docs/integrations/frameworks/sveltekit.md +6 -65
  251. package/skills/void/docs/integrations/frameworks/tanstack-start.md +4 -62
  252. package/skills/void/docs/integrations/nodejs-bun-deno.md +5 -69
  253. package/skills/void/docs/reference/api/auth.md +156 -0
  254. package/skills/void/docs/reference/api/client.md +87 -0
  255. package/skills/void/docs/reference/api/database.md +95 -0
  256. package/skills/void/docs/reference/api/durable.md +46 -0
  257. package/skills/void/docs/reference/api/env.md +50 -0
  258. package/skills/void/docs/reference/api/handlers.md +254 -0
  259. package/skills/void/docs/reference/api/pages.md +241 -0
  260. package/skills/void/docs/reference/api/plugin.md +39 -0
  261. package/skills/void/docs/reference/api/resources.md +109 -0
  262. package/skills/void/docs/reference/api/rewrites.md +76 -0
  263. package/skills/void/docs/reference/api/types.md +92 -0
  264. package/skills/void/docs/reference/api.md +56 -1218
  265. package/skills/void/docs/reference/cli/auth.md +89 -0
  266. package/skills/void/docs/reference/cli/database.md +128 -0
  267. package/skills/void/docs/reference/cli/deploy.md +105 -0
  268. package/skills/void/docs/reference/cli/domains.md +41 -0
  269. package/skills/void/docs/reference/cli/email.md +129 -0
  270. package/skills/void/docs/reference/cli/generate.md +116 -0
  271. package/skills/void/docs/reference/cli/github.md +189 -0
  272. package/skills/void/docs/reference/cli/platform-config.md +92 -0
  273. package/skills/void/docs/reference/cli/platform-email.md +81 -0
  274. package/skills/void/docs/reference/cli/platform-installation.md +142 -0
  275. package/skills/void/docs/reference/cli/platform-operations.md +90 -0
  276. package/skills/void/docs/reference/cli/platform-users.md +89 -0
  277. package/skills/void/docs/reference/cli/platform-zero-trust.md +59 -0
  278. package/skills/void/docs/reference/cli/platform.md +70 -0
  279. package/skills/void/docs/reference/cli/project.md +214 -0
  280. package/skills/void/docs/reference/cli/secrets.md +76 -0
  281. package/skills/void/docs/reference/cli/setup.md +70 -0
  282. package/skills/void/docs/reference/cli.md +32 -1682
  283. package/skills/void/docs/reference/config.md +12 -18
  284. package/skills/void/docs/reference/resource-inference.md +3 -58
  285. package/skills/void/docs/reference/structure.md +14 -41
  286. package/dist/auth-CZuiVsFh.mjs +0 -2
  287. package/dist/canonical-json-DuDiiUsQ.mjs +0 -13
  288. package/dist/client-Czz8o5jP.mjs +0 -2
  289. package/dist/dist-BuDuKZJv.mjs +0 -2
  290. package/dist/dist-CTBk70IR.mjs +0 -1310
  291. package/dist/gen-CNJ62MM7.mjs +0 -2
  292. package/dist/help-CmZzxUba.mjs +0 -2
  293. package/dist/log-ChfPKsVd.mjs +0 -26
  294. package/dist/login-CGcRKEoi.mjs +0 -2
  295. package/dist/migrate-CYfbKkXh.mjs +0 -2
  296. package/dist/plan-BEZ8VJW0.mjs +0 -256
  297. package/dist/plan-DpuOr14e.mjs +0 -2
  298. package/dist/prepare-Bm3iq-u4.mjs +0 -2
  299. package/dist/project-BuUwfj6f.mjs +0 -2
  300. package/dist/validate-EKmJWxmy.mjs +0 -2
  301. /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 require_picocolors } from "./picocolors-BTps1_gs.mjs";
4
- import { t as cfAccessHeaders } from "./cf-access-DRsQRe6k.mjs";
5
- import { d as openBrowser } from "./auth-DLNN0D3Z.mjs";
6
- import { a as listInstallationManifests, d as readInstallationRecoverySecrets, f as readPendingInstallationSecrets, g as savePlatformConnection, h as resolveInstallationManifest, i as latestPlatformWorkerCutoverGeneration, l as platformWorkerSecretUpdateIntentState, n as assertPlatformInstallationSlugAvailable, s as normalizePlatformApiUrl, t as acquirePlatformLifecycleLock, u as promoteInstallationSecrets, v as writeInstallationManifest, y as writePendingInstallationSecrets } from "./platform-registry-QBa4DK32.mjs";
7
- import { n as ensurePlatformAccessToken } from "./cf-access-P0iZnu0q.mjs";
8
- import { B as text$1, E as confirm, F as password, G as settings, I as select, K as u$1, M as multiselect, N as note, O as intro, P as outro, R as symbol, U as CANCEL_SYMBOL, W as isCancel, g as S_PASSWORD_MASK, i as S_BAR_END, j as log, r as S_BAR } from "./dist-CTBk70IR.mjs";
9
- import { o as resolveWranglerArgv } from "./preset-UHj9ARyP.mjs";
10
- import { s as runCloudflareAuthCommand } from "./cloudflare-auth-DQkqoMYa.mjs";
11
- import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-C4IGqkBf.mjs";
12
- import { c as import_picocolors$1, i as createSpinner, n as cliCommandName, r as cliTitle } from "./output-Dm_A4Tbv.mjs";
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, PLATFORM_SANDBOX_DRAIN_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
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 ? `Check ${setupUrl}. If no subdomain is shown, finish Workers onboarding and choose one; otherwise check account access and retry.` : "Rerun the command. If the dashboard already shows a subdomain, no Workers onboarding change is needed.";
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 port = typeof record.port === "number" ? record.port : Number(record.port);
696
- if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error("platform: Cloudflare Hyperdrive config has an invalid origin port.");
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((provider) => ({
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$1(value) {
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$1(await select({
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 automatically in this Cloudflare account"
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: "Connect existing applications (including another account)"
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
- spec.accountId = answer$1(await text$1({
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) spec.loginApplicationId = answer$1(await text$1({ message: "Existing Access OIDC application ID:" }));
1251
- if (input.protection) spec.protectionApplicationId = answer$1(await text$1({ message: "Existing Access protection application ID:" }));
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
- console.error(`Access setup for account ${accountId} needs Access applications/policies permissions and organization/identity-provider/group read access${input.protection ? ", plus service-token permissions" : ""}.`);
1269
- const token = answer$1(await password({ message: "Access setup API token (kept out of the platform runtime):" }));
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
- const identityProviderIds = spec.identityProviderIds ?? (process.stdin.isTTY ? answer$1(await multiselect({
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: String(provider.id),
1282
- label: stripVTControlCharacters(String(provider.name ?? provider.id))
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$1(await text$1({ message: "Existing admitted Access service token ID:" })),
1311
- clientId: answer$1(await password({ message: "Access service token client ID:" })),
1312
- clientSecret: answer$1(await password({ message: "Access service token client secret:" }))
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$1(await confirm({
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
- loginClientSecret = answer$1(await password({ message: "Existing Access OIDC client secret:" }));
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
- function assertPlatformOperatorCredentialCanApplyPlan(credential, plan) {
1380
- if (credential.source !== "oauth") return;
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
- if (!createsZone && !writesDns) return;
1384
- const permissions = [...createsZone ? ["Account → Zone → Edit"] : [], ...writesDns ? ["Zone → DNS → Edit"] : []];
1385
- throw new Error(`platform: Cloudflare browser OAuth grants Zone Read and cannot apply the planned zone or DNS changes. Set CLOUDFLARE_API_TOKEN with ${permissions.join(" and ")}, scoped to the selected account and application zone, then rerun the command.`);
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(["login"], false);
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
- "auth",
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 = JSON.parse(result.stdout);
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-7d5aV2Jg.mjs");
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-auth-bootstrap.ts
1518
- /** Checkpoint a replacement before submission so an interrupted response is safely replayable. */
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 comparableExistingWithoutEmailZone = existing && deployment.capabilities.email && !existing.deployment.capabilities.email ? {
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
- ...existingWithSandbox.capabilities,
2439
+ ...existingWithDashboard.capabilities,
1671
2440
  email: true
1672
2441
  }
1673
- } : existingWithSandbox;
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 conflicts = (await client.listWorkerRoutes(zone.id)).filter((route) => expectedPatterns.has(route.pattern) && (route.script !== expectedWorker || !ownedWorker));
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$1.default.bold(`${plan.deployment.displayName} in ${plan.account.name}`)} (${plan.account.id})`,
2105
- plan.deployment.application.zoneName ? `${import_picocolors$1.default.bold("Applications:")} *.${plan.deployment.application.baseDomain}` : `${import_picocolors$1.default.bold("Applications:")} workers.dev (testing); add a domain with void platform domain set <domain>`,
2106
- `${import_picocolors$1.default.bold("Control plane:")} ${plan.apiUrl}`,
2107
- `${import_picocolors$1.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
2108
- `${import_picocolors$1.default.bold("Login methods:")} ${plan.deployment.authentication.providers.join(", ")}`,
2109
- `${import_picocolors$1.default.bold("Access protection:")} ${plan.deployment.authentication.accessProtection ? "Enabled" : "Not enabled"}`,
2110
- `${import_picocolors$1.default.bold("Login callback:")} ${plan.apiUrl}/auth/callback`,
2111
- ...plan.deployment.authentication.providers.includes("cloudflare-access") ? ["Access login: create a dedicated OIDC application or connect an existing issuer."] : [],
2112
- ...plan.deployment.authentication.accessProtection ? [`Access protection: verify an application covering ${plan.apiUrl} and ${plan.proxyUrl}, with a separate scoped service token for installation and CI.`] : [],
2113
- `${import_picocolors$1.default.bold("Database:")} Verify uncached Hyperdrive reads for authentication`,
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 = (args, accountId, cwd, capture) => runPlatformWranglerAtApiBase(args, accountId, cwd, capture, cloudflareApiBaseUrl);
2937
- const runParallelPinnedPlatformWrangler = (args, accountId, cwd, capture) => runPlatformWranglerAtApiBase(args, accountId, cwd, capture, cloudflareApiBaseUrl, true);
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: null,
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
- const completedDrain = await prepareSandboxDrain();
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 = resolveWranglerArgv(process.cwd())[1];
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 (args, account, cwd, capture) => {
5618
+ return async (operation, account, cwd, capture) => {
4696
5619
  try {
4697
- if (args[0] === "deployments" && (args[1] === "status" || args[1] === "list")) {
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: args[1] === "status" && deployments.length === 0 ? 1 : 0,
5625
+ status: operation.type === "deployments.status" && deployments.length === 0 ? 1 : 0,
4703
5626
  stderr: "",
4704
- stdout: JSON.stringify(args[1] === "status" ? deployments[0] : deployments)
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(args, account, cwd, capture);
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 argv = [
4835
- ...options.workerExisted ? ["versions", "upload"] : ["deploy"],
4836
- "--config",
5756
+ const operation = {
5757
+ type: options.workerExisted ? "versions.upload" : "worker.deploy",
4837
5758
  configPath,
4838
- "--strict",
4839
- "--experimental-provision=false",
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
- argv.push("--secrets-file", secretsPath);
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(argv, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(argv, accountId, directory, true, workerName, options.apiBaseUrl);
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
- uploadedVersionId = parseUploadedPlatformWorkerVersion(`${uploaded.stdout}\n${uploaded.stderr}`);
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
- "--message",
4879
- `Void platform activation ${uploadedVersionId}`,
4880
- "--yes"
4881
- ], accountId, directory, true), `activate ${workerName}`);
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 (args, options) => runner(args, accountId, cwd, options.capture);
5991
+ return (operation, options) => runner(operation, accountId, cwd, options.capture);
5076
5992
  }
5077
- async function runPlatformWrangler(args, accountId, cwd, capture) {
5078
- return runPlatformWranglerAtApiBase(args, accountId, cwd, capture);
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(args, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false) {
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: resolveWranglerArgv(process.cwd()),
6000
+ launcher: resolveCloudflareOperationArgv(process.cwd()),
5085
6001
  parallelWorkerUpload,
5086
6002
  timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
5087
- })([...args], {
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
- status: result.status,
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(args, accountId, cwd, capture, workerName, upstreamApiBaseUrl) {
5226
- return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(args, accountId, cwd, capture, apiBaseUrl), upstreamApiBaseUrl);
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
- "--name",
5235
- workerName,
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
- let value;
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
- "--name",
5257
- workerName,
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
- let value;
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
- "deploy",
5304
- ...restorationVersions.map((version) => `${version.versionId}@${version.percentage}`),
5305
- "--config",
5306
- input.configPath,
5307
- "--message",
5308
- `Void platform restoration ${input.restoration.id}`,
5309
- "--yes"
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
- "--name",
5373
- workerName,
5374
- "--json"
5375
- ], accountId, process.cwd(), true);
6263
+ workerName
6264
+ }, accountId, process.cwd(), true);
5376
6265
  assertCleanPlatformWrangler(result, `read rollback metadata for ${workerName}`);
5377
- let value;
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 interruptedWorkerCutover(manifest) {
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
- resolved: false,
6421
+ resolution: null,
5541
6422
  staged: /* @__PURE__ */ new Map(),
5542
6423
  unsafeTraffic: /* @__PURE__ */ new Set()
5543
6424
  };
5544
- entry.resolved = true;
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
- resolved: false,
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
- resolved: false,
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
- const unresolved = [...generations.entries()].filter(([, entry]) => !entry.resolved && entry.staged.size > 0).sort(([left], [right]) => right - left)[0];
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 = interruptedWorkerCutover(manifest);
5588
- if (cutover?.generation !== generation || cutover.staged.size !== workerKeys.length) throw new Error(`platform: Worker cutover generation ${generation} is missing a complete immutable-version checkpoint.`);
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, health, context);
5688
- if (!discovery.ok) throw await platformProbeFailure("Discovery check", apiUrl, discovery, context);
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, proxy, context);
5694
- if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", dispatchUrl, dispatch, context);
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, emailGateway, context);
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 bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
5725
- const response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
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(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
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 (!response.ok || status?.version !== 1 || status.state !== "ready" || typeof status.operationId !== "string" || status.operationId.length === 0) {
5742
- const detail = status && typeof status.operationId === "string" ? ` Operation ${status.operationId}: ${typeof status.operationStatus === "string" ? status.operationStatus : "incomplete"}.` : status && typeof status.operationStatus === "string" ? ` Operation status: ${status.operationStatus}.` : "";
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 status.operationId;
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) return { authentication: recorded };
5776
- if (!process.stdin.isTTY) return { authentication: { providers: ["github"] } };
5777
- const providers = answer(await multiselect({
5778
- message: "Platform login methods:",
5779
- initialValues: ["github"],
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
- var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
6241
- /** A visual example for an empty secret field; never a default value or an unmasked input. */
6242
- async function passwordWithPlaceholder(options) {
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(args) {
6280
- if (args.subcommand === "domain") {
6281
- const { runPlatformDomainCommand } = await import("./platform-domain-D6Xcy9ZX.mjs");
6282
- await runPlatformDomainCommand(args);
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 (args.subcommand === "discover") {
7117
+ if (commandArgs.subcommand === "discover") {
6286
7118
  const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
6287
- const accountId = await selectAccount(await readAccounts(client), args.account);
6288
- const { recoverPlatformInstallations } = await import("./platform-recovery-BvtcXON5.mjs");
6289
- const recovered = await recoverPlatformInstallations(client, accountId, void 0, args.selector);
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 (args.subcommand !== "install") {
6298
- const { runInstalledPlatformCommand } = await import("./platform-management-DOtss0BN.mjs");
6299
- await runInstalledPlatformCommand(args);
7129
+ if (commandArgs.subcommand !== "install") {
7130
+ const { runInstalledPlatformCommand } = await import("./platform-management-DG47jys8.mjs");
7131
+ await runInstalledPlatformCommand(commandArgs);
6300
7132
  return;
6301
7133
  }
6302
- const runtime = loadPlatformRuntime(args.runtime);
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
- if (installs.length > 0) {
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: [...installs.map((install) => ({
6328
- value: install.id,
6329
- label: `Continue setup for ${install.deployment.displayName} (${install.deployment.installationSlug})`,
6330
- hint: install.account.name
6331
- })), {
6332
- value: "new",
6333
- label: "Start a new platform install"
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.app")));
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
- const controlPlaneDomain = recorded ? recorded.controlPlane.apiHostname ?? "" : args.controlPlaneDomain || (process.stdin.isTTY ? await promptText("Optional custom API hostname (leave empty for workers.dev):", "", true) : "");
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 (!externalHyperdrive && process.stdin.isTTY && (!existing || isPlatformSetupDraft(existing)) && !existing?.resources.some((resource) => resource.key === "hyperdrive")) {
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 ? ` (for example ${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/`, "Set up Hyperdrive first");
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
- note(`Database: ${live.origin.database}\nHost: ${live.origin.host}\nPort: ${live.origin.port}\nUser: ${live.origin.user}\nSQL result caching: ${live.cachingDisabled ? "disabled" : "enabled"}`, `Existing Hyperdrive ${live.name}`);
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
- let authoritativeProvisionIntents = false;
6515
- if (existing && !isPlatformSetupDraft(existing)) {
6516
- if (!databaseUrl) throw new Error("platform: DATABASE_URL is required to verify an interrupted installation before resuming it.");
6517
- progress = startProgress("Reading the saved installation");
6518
- const authoritative = await readPlatformDatabaseLifecycleManifest(databaseUrl, existing.id, existing.ownershipNonce);
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
- const authenticationSelection = await selectInstallationAuthentication(args.authConfig, existing?.deployment.authentication);
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
- let plan = await createPlatformInstallPlan(client, planInput, existing, {
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
- note(formatPlatformInstallPlan(plan), args.plan ? "Read-only installation plan" : "Installation plan");
6552
- note(formatPlatformRuntime(runtime), "Platform runtime");
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
- console.log(formatPlatformSetupPlan(plan));
6555
- console.log(`\n${existing ? "To resume this installation later:" : "To install with these settings later:"}\n${formatPlatformInstallCommand({
6556
- commandName: cliCommandName(),
6557
- accountId,
6558
- installationSlug,
6559
- displayName,
6560
- applicationDomain,
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: false
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 && isRuntimeTokenAuthorizationError(error, checkingRuntimePermissions ? void 0 : "/ai-gateway/")) {
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-DOtss0BN.mjs");
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; progress is saved." : "Platform installed.");
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`; 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.", "Waiting for managed Sandbox drain");
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 (operation ${result.emailOperationId}).`);
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 isRuntimeTokenAuthorizationError(error, requestPathIncludes) {
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$1.default.bold("Version:")} ${runtime.manifest.platformVersion}`,
6806
- `${import_picocolors$1.default.bold("Source:")} ${runtime.source}`,
6807
- `${import_picocolors$1.default.bold("Revision:")} ${runtime.manifest.sourceRevision ?? "not recorded"}`,
6808
- `${import_picocolors$1.default.bold("Manifest SHA-256:")} ${runtime.manifestSha256}`
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("platform: Resources changed while checking AI Gateway access. Rerun the command to review the updated plan. No platform resources were created.");
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 !== 1) throw new Error(`platform: Cloudflare account ${JSON.stringify(selector)} is ambiguous or unavailable.`);
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" ? { initialValue: value } : value,
6869
- validate: (input) => input ? validate?.(input) : optional ? void 0 : "A value is required."
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;