void 0.27.0 → 0.28.0

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