void 0.25.0 → 0.26.1

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