void 0.27.0 → 1.0.3

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 (337) hide show
  1. package/README.md +3 -1
  2. package/dist/{account-cmd-l-FM7Ioq.mjs → account-cmd-BTcUzv9L.mjs} +6 -6
  3. package/dist/{agents-B_pX_HmQ.mjs → agents-CuNtT0D_.mjs} +5 -2
  4. package/dist/{application-analysis-DKy02UY0.mjs → application-analysis-AB_YYWgH.mjs} +118 -135
  5. package/dist/{application-routing-DQyUh_5S.mjs → application-routing-L7V-TFhZ.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-Cbqi1iVX.mjs +1 -0
  9. package/dist/{auth-link-vz9Z2kmG.mjs → auth-link-DUJuvRx5.mjs} +11 -11
  10. package/dist/{auth-migrations-BwHgeEsD.mjs → auth-migrations-Cgs0k6lS.mjs} +28 -5
  11. package/dist/{auth-router-DvsCUVw4.mjs → auth-router-CIxiMG7i.mjs} +5 -5
  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-BkIv25PA.mjs} +6 -6
  16. package/dist/{cache-CZRdzKx1.mjs → cache-Ctzfig-W.mjs} +6 -6
  17. package/dist/{cancel-deploy-CQ3NnMvf.mjs → cancel-deploy-CD9o-SsC.mjs} +5 -5
  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-BU3hqdHu.mjs +1 -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 +199 -312
  24. package/dist/cli/cloudflare-operation-process.mjs +41 -15
  25. package/dist/cli/env-schema-probe.d.mts +7 -44
  26. package/dist/cli/env-schema-probe.mjs +14 -80
  27. package/dist/client-B60PwCC4.mjs +1 -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-BIAF_BIY.mjs} +7 -6
  31. package/dist/{cloudflare-config-BmhBv3HV.mjs → cloudflare-config-DV60XFFK.mjs} +16 -28
  32. package/dist/{cloudflare-connect-2G3rL2rP.mjs → cloudflare-connect-WjwnVOJA.mjs} +4 -4
  33. package/dist/{cloudflare-operations-CwI1bChl.mjs → cloudflare-operations-B42EZeJy.mjs} +26 -34
  34. package/dist/cloudflare-operations-CrSUFRzJ.mjs +1 -0
  35. package/dist/{cloudflare-process-2-TfIEzB.mjs → cloudflare-process-CeDWJRhj.mjs} +12 -7
  36. package/dist/collect-Cae8A7Zr.mjs +1 -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-DPByhVck.mjs → config-D0GY5ItR.mjs} +1 -1
  42. package/dist/config-QrJWu0IX.mjs +1 -0
  43. package/dist/config-entry.d.mts +1 -1
  44. package/dist/{connect-BfPBHHm-.mjs → connect-CKtk-EZe.mjs} +9 -9
  45. package/dist/create-project-B-ewF_Af.mjs +1 -0
  46. package/dist/{create-project-BdXEFKal.mjs → create-project-yInGo72M.mjs} +8 -8
  47. package/dist/database-provider.mjs +1 -2
  48. package/dist/{db-Xs80hcaA.mjs → db-7I_gfdVq.mjs} +177 -268
  49. package/dist/db-types-Bb4WBWIs.mjs +49 -0
  50. package/dist/{delete-d2vuou3j.mjs → delete-Cy-M1u-r.mjs} +6 -6
  51. package/dist/deploy-9lgfhrXS.mjs +2 -0
  52. package/dist/deploy-BYYlnsiy.mjs +6164 -0
  53. package/dist/deploy-managed-static-Ca4TFdmx.mjs +1595 -0
  54. package/dist/deploy-transport-DWlWBe6M.mjs +180 -0
  55. package/dist/deploy.d.mts +10 -0
  56. package/dist/deploy.mjs +82 -0
  57. package/dist/dev-inbox-Rd6j_8vM.mjs +217 -0
  58. package/dist/discover-ClzFSwns.mjs +1 -0
  59. package/dist/{discover-CkCYv9T_.mjs → discover-D2PGT63u.mjs} +2 -2
  60. package/dist/dispatch-DrTTk8Xg.mjs +431 -0
  61. package/dist/{dist-D3m88sjf.mjs → dist-BVdsO7Py.mjs} +5 -6
  62. package/dist/dist-CfgIkieS.mjs +1 -0
  63. package/dist/{dist-BVqXeR_h.mjs → dist-ipQi0Q8y.mjs} +2 -3
  64. package/dist/{domain-BsDUBAvt.mjs → domain-CZGxPv9u.mjs} +8 -23
  65. package/dist/{dotenv-BfU6eqFt.mjs → dotenv-CNazY3-w.mjs} +4 -16
  66. package/dist/edge.mjs +1 -2
  67. package/dist/{email-d_Aux1vM.mjs → email-BcjSyAL0.mjs} +4 -20
  68. package/dist/{email-_lGAHOhp.mjs → email-DzyNVuoU.mjs} +100 -192
  69. package/dist/email-cloudflare-DfG6UEMm.mjs +1096 -0
  70. package/dist/email-protocol-D9nTvhCp.mjs +82 -0
  71. package/dist/{entry-CMAkqBmU.mjs → entry-ehmBvs5w.mjs} +2 -2
  72. package/dist/{env-DMA3cXZt.mjs → env-0jrMQJI_.mjs} +6 -14
  73. package/dist/{env-BxocywBB.mjs → env-BJjULvL0.mjs} +2 -2
  74. package/dist/{env-C74QepDE.mjs → env-Nx3yf50r.mjs} +9 -9
  75. package/dist/{env-public-DR7ZM6d-.d.mts → env-public-CCdykygU.d.mts} +6 -23
  76. package/dist/{env-raw-jZpP5n47.mjs → env-raw-D74P4ebM.mjs} +15 -21
  77. package/dist/{env-types-CwXgeMgL.mjs → env-types-DS2_y4X8.mjs} +2 -2
  78. package/dist/env-validation-BT7z-qpQ.mjs +1 -0
  79. package/dist/env-validation-DYxjz_Wo.mjs +488 -0
  80. package/dist/{gen-DtkQhvQg.mjs → gen-CufHEf42.mjs} +132 -254
  81. package/dist/gen-sXyp5DFO.mjs +1 -0
  82. package/dist/generate-ClI89brV.mjs +1 -0
  83. package/dist/{generate-GDbL7sUr.mjs → generate-Dat0UTry.mjs} +38 -4
  84. package/dist/generated-wrangler-config-DeWj5F9Y.mjs +18 -0
  85. package/dist/{github-cmd-DaDdBbSS.mjs → github-cmd-FU8EjFyU.mjs} +28 -72
  86. package/dist/{handler-BHAn2fZJ.mjs → handler-BZ1t7-Rv.mjs} +2 -2
  87. package/dist/{handler-CNAwjRcW.d.mts → handler-_PqY82xe.d.mts} +1 -1
  88. package/dist/{headers-DlfR78Nu.mjs → headers-Di1FsD3e.mjs} +26 -81
  89. package/dist/{help-0bTe_lID.mjs → help-BT2hovNI.mjs} +26 -14
  90. package/dist/help-i2YGGnTH.mjs +1 -0
  91. package/dist/{inbound-PfQDLXc5.mjs → inbound-BuTfm11U.mjs} +71 -222
  92. package/dist/{inbound-CH5Mksyy.d.mts → inbound-brj2-TM5.d.mts} +19 -58
  93. package/dist/index.d.mts +2 -18
  94. package/dist/index.mjs +714 -762
  95. package/dist/{info-PYWwkWyt.mjs → info-l249eUdH.mjs} +5 -5
  96. package/dist/{init-DaBGmaXC.mjs → init-D0A6M64Q.mjs} +229 -392
  97. package/dist/isr-config-fZaJJ8t7.mjs +165 -0
  98. package/dist/{link-C6fjafep.mjs → link-WMoJFf4i.mjs} +9 -9
  99. package/dist/{list-BTFBlqKl.mjs → list-CQ2EQEHL.mjs} +7 -18
  100. package/dist/{live-CgQuTpqD.mjs → live-BcHoTDFe.mjs} +19 -17
  101. package/dist/{live-Du8jXwOW.d.mts → live-DJONVlQK.d.mts} +1 -1
  102. package/dist/load-user-env-DD5qveNU.mjs +49 -0
  103. package/dist/{local-d1-ga5O_idV.mjs → local-d1-BxvQYEFi.mjs} +6 -6
  104. package/dist/{login-GKcNUmtI.mjs → login-C2T1oVG-.mjs} +12 -12
  105. package/dist/login-XD844Xs-.mjs +1 -0
  106. package/dist/{logs-BiMSULmc.mjs → logs-DnC1d7xB.mjs} +6 -6
  107. package/dist/{migrate-NLsjBbuM.mjs → migrate-BDIprOYd.mjs} +70 -22
  108. package/dist/migrate-BRwU-blY.mjs +1 -0
  109. package/dist/migration-handler-Cg0Y4EPu.mjs +470 -0
  110. package/dist/{migration-handler-DM4clYj5.d.mts → migration-handler-CwlEQUoo.d.mts} +17 -26
  111. package/dist/{mime-DxCznlJq.mjs → mime-srOklVVv.mjs} +92 -395
  112. package/dist/mysql-migration-sql-BIS5vbNk.mjs +241 -0
  113. package/dist/nitro.d.mts +30 -0
  114. package/dist/nitro.mjs +88 -0
  115. package/dist/{node-DJicPvPj.mjs → node-cPadMgUO.mjs} +9 -25
  116. package/dist/{operator-args-DOBhIaIg.mjs → operator-args-UnE8Y2eK.mjs} +27 -26
  117. package/dist/{operator-auth-BEZ_u0Cz.mjs → operator-auth-Cr2zCc-J.mjs} +2 -2
  118. package/dist/{operator-client-DbDv4vtE.mjs → operator-client-C-ROjk1I.mjs} +4 -4
  119. package/dist/operator-cmd-D6gnZsnK.mjs +1 -0
  120. package/dist/{operator-cmd-BT4hgavL.mjs → operator-cmd-DHieWtFp.mjs} +8 -8
  121. package/dist/{output-C50ag2r3.mjs → output-Ba6ZbrzY.mjs} +37 -6
  122. package/dist/{package-json-DXfiT1jV.mjs → package-json-BjYqR-So.mjs} +1 -1
  123. package/dist/pages/client.d.mts +1 -1
  124. package/dist/pages/client.mjs +91 -5
  125. package/dist/pages/head.mjs +1 -2
  126. package/dist/pages/index.d.mts +1 -2
  127. package/dist/pages/index.mjs +6 -6
  128. package/dist/pages/islands-plugin.mjs +5 -83
  129. package/dist/pages/prefetch.mjs +1 -2
  130. package/dist/pages/protocol.d.mts +2 -2
  131. package/dist/pages/protocol.mjs +1 -1
  132. package/dist/pages/serialize.mjs +1 -2
  133. package/dist/plan-CKX_jWnD.mjs +1 -0
  134. package/dist/platform-args-B0HeTh0C.mjs +1 -0
  135. package/dist/{platform-args-DUeXY9Om.mjs → platform-args-CaCL_1Ch.mjs} +43 -60
  136. package/dist/{platform-auth-config-CkSx3usH.mjs → platform-auth-config-Z16-I8nT.mjs} +11 -12
  137. package/dist/{platform-auth-protection-BJimB_p9.mjs → platform-auth-protection-BXwZiJSF.mjs} +37 -27
  138. package/dist/{platform-auth-recovery-BU6izFAU.mjs → platform-auth-recovery-CXjfqyOv.mjs} +9 -10
  139. package/dist/{platform-cmd-BbY1dWiM.mjs → platform-cmd-DaAOiWTl.mjs} +16 -16
  140. package/dist/platform-cmd-PT4YwMLM.mjs +1 -0
  141. package/dist/{platform-discovery-DIYz8V63.mjs → platform-discovery-CHhNs8Nn.mjs} +1 -1
  142. package/dist/{platform-domain-CHVS6cV0.mjs → platform-domain-CxpvUeo0.mjs} +6 -6
  143. package/dist/{platform-lifecycle-DzCwqhFq.mjs → platform-lifecycle-DBiS0cJd.mjs} +1693 -2120
  144. package/dist/platform-lifecycle-u9pMOvxC.mjs +1 -0
  145. package/dist/{platform-management-CuuVDhyt.mjs → platform-management-DhEFdQAf.mjs} +696 -130
  146. package/dist/platform-management-DvBS83rC.mjs +1 -0
  147. package/dist/{platform-plans-config-CN-heS4q.mjs → platform-plans-config-BWn1S2Rv.mjs} +16 -12
  148. package/dist/{platform-recovery-CskSJgLh.mjs → platform-recovery-BUGOB4RC.mjs} +6 -9
  149. package/dist/{platform-registry-Cjv2Qf0r.mjs → platform-registry-B9mWX9LO.mjs} +13 -18
  150. package/dist/{platform-setup-BBxRxmFC.mjs → platform-setup-YvN9VQXV.mjs} +21 -28
  151. package/dist/{plugin-inference-BlQbOL0P.mjs → plugin-inference-DsARdQKE.mjs} +18 -33
  152. package/dist/prepare-CYT6tAzD.mjs +1 -0
  153. package/dist/{prepare-Do96654b.mjs → prepare-DEB5SeVJ.mjs} +12 -13
  154. package/dist/{prepare-Dq9hTIbo.mjs → prepare-PQCgsny0.mjs} +4 -4
  155. package/dist/{prerender-render-Ct20jftc.mjs → prerender-render-Bfsumdkv.mjs} +8 -2
  156. package/dist/prerender-render.d.mts +2 -0
  157. package/dist/prerender-render.mjs +1 -2
  158. package/dist/{project-cmd-DaoRrn-8.mjs → project-cmd-DVatD8Ev.mjs} +20 -20
  159. package/dist/{project-paths-GgcMozYz.mjs → project-paths-DMvEeSau.mjs} +9 -14
  160. package/dist/{project-BAm2aB6Q.mjs → project-rqDnVsJN.mjs} +8 -4
  161. package/dist/project-slug-6cyz8E7k.mjs +1 -0
  162. package/dist/{project-team-Q_lCA7Io.mjs → project-team-DJrWEAG1.mjs} +7 -7
  163. package/dist/{project-token-BArgK9o1.mjs → project-token-CBgLBuL6.mjs} +6 -6
  164. package/dist/{project-tsconfig-CVJhNMxs.mjs → project-tsconfig-bveqhlYA.mjs} +3 -5
  165. package/dist/project-zdB3fs9G.mjs +1 -0
  166. package/dist/{project-zero-trust-KO9Hu2Pq.mjs → project-zero-trust-92gLtZ25.mjs} +6 -6
  167. package/dist/{protocol-BPZaNqBo.d.mts → protocol-4H7V_3Os.d.mts} +1 -1
  168. package/dist/{protocol-CfaUwNs8.mjs → protocol-RNP0u7UY.mjs} +9 -3
  169. package/dist/{provision-BfaLiWbj.mjs → provision-BnSSkiED.mjs} +119 -866
  170. package/dist/{requests-C7Oeoh5u.mjs → requests-7W1Pygt2.mjs} +5 -5
  171. package/dist/resolve-project-PnhzMtY6.mjs +1 -0
  172. package/dist/resolve-project-WpwkCB_c.mjs +48 -0
  173. package/dist/{rollback-DVAtrLAl.mjs → rollback-BcASVnwP.mjs} +8 -18
  174. package/dist/{route-types-CbtsIeFn.mjs → route-types-BZi26DVS.mjs} +9 -91
  175. package/dist/routing-assets-VkrKIbxE.mjs +139 -0
  176. package/dist/{runner-DMBKSrRk.mjs → runner-CabsQ-iM.mjs} +22 -34
  177. package/dist/runner-So9mYfcm.mjs +1 -0
  178. package/dist/runner-mysql-BhRaONkO.mjs +146 -0
  179. package/dist/runner-mysql-DWwE8jKi.mjs +1 -0
  180. package/dist/{runner-pg-DV8p_zjA.mjs → runner-pg-BRemq_GX.mjs} +3 -3
  181. package/dist/runner-pg-_Xncr3H5.mjs +1 -0
  182. package/dist/runtime/ai.mjs +23 -25
  183. package/dist/runtime/auth-client-react.mjs +1 -2
  184. package/dist/runtime/auth-client-solid.mjs +1 -2
  185. package/dist/runtime/auth-client-svelte.mjs +1 -2
  186. package/dist/runtime/auth-client-vue.mjs +1 -2
  187. package/dist/runtime/auth-client.mjs +1 -2
  188. package/dist/runtime/auth.mjs +1 -2
  189. package/dist/runtime/better-auth-mysql.d.mts +2 -5
  190. package/dist/runtime/better-auth-mysql.mjs +9 -31
  191. package/dist/runtime/better-auth-pg.d.mts +2 -5
  192. package/dist/runtime/better-auth-pg.mjs +6 -32
  193. package/dist/runtime/better-auth.d.mts +2 -4
  194. package/dist/runtime/better-auth.mjs +9 -19
  195. package/dist/runtime/db.mjs +6 -2
  196. package/dist/runtime/durable.d.mts +18 -1
  197. package/dist/runtime/durable.mjs +26 -2
  198. package/dist/runtime/email/testing.d.mts +1 -1
  199. package/dist/runtime/email/testing.mjs +9 -23
  200. package/dist/runtime/email-protocol.d.mts +12 -0
  201. package/dist/runtime/email-protocol.mjs +1 -70
  202. package/dist/runtime/email.d.mts +1 -1
  203. package/dist/runtime/email.mjs +79 -221
  204. package/dist/runtime/env-public.d.mts +1 -1
  205. package/dist/runtime/env-public.mjs +12 -38
  206. package/dist/runtime/env.d.mts +1 -12
  207. package/dist/runtime/env.mjs +1 -2
  208. package/dist/runtime/handler.d.mts +1 -1
  209. package/dist/runtime/handler.mjs +1 -1
  210. package/dist/runtime/isr.mjs +1 -1
  211. package/dist/runtime/kv.mjs +1 -1
  212. package/dist/runtime/limits.mjs +1 -2
  213. package/dist/runtime/live-client.d.mts +3 -2
  214. package/dist/runtime/live-server.d.mts +2 -1
  215. package/dist/runtime/live-server.mjs +1 -16
  216. package/dist/runtime/live.d.mts +1 -1
  217. package/dist/runtime/live.mjs +1 -2
  218. package/dist/runtime/log.d.mts +2 -1
  219. package/dist/runtime/migration-handler-mysql.d.mts +2 -2
  220. package/dist/runtime/migration-handler-mysql.mjs +39 -8
  221. package/dist/runtime/migration-handler-pg.d.mts +2 -2
  222. package/dist/runtime/migration-handler-pg.mjs +1 -1
  223. package/dist/runtime/migration-handler.d.mts +2 -2
  224. package/dist/runtime/migration-handler.mjs +1 -470
  225. package/dist/runtime/queues.mjs +1 -1
  226. package/dist/runtime/remote/binding-handler.mjs +62 -62
  227. package/dist/runtime/remote/index.mjs +16 -77
  228. package/dist/runtime/response.mjs +1 -2
  229. package/dist/runtime/route-url.mjs +1 -2
  230. package/dist/runtime/routing.d.mts +3 -5
  231. package/dist/runtime/routing.mjs +82 -218
  232. package/dist/runtime/sandbox-container.d.mts +2 -2
  233. package/dist/runtime/sandbox-container.mjs +1 -2
  234. package/dist/runtime/sandbox.d.mts +2 -2
  235. package/dist/runtime/sandbox.mjs +13 -6
  236. package/dist/runtime/seed.d.mts +2 -1
  237. package/dist/runtime/sse.mjs +1 -2
  238. package/dist/runtime/storage.mjs +1 -1
  239. package/dist/runtime/validator.d.mts +1 -1
  240. package/dist/runtime/validator.mjs +1 -2
  241. package/dist/runtime/ws-server.d.mts +4 -4
  242. package/dist/runtime/ws-server.mjs +6 -28
  243. package/dist/runtime/ws.d.mts +1 -1
  244. package/dist/{sandbox-container-Bo0eiFKz.d.mts → sandbox-container-ChQRum-j.d.mts} +3 -1
  245. package/dist/{sandbox-container-C6ItmVuN.mjs → sandbox-container-a9LRUtii.mjs} +13 -2
  246. package/dist/{sandbox-C5LFoJMf.d.mts → sandbox-q3waMc2T.d.mts} +1 -1
  247. package/dist/{scan-C3uDzz6J.mjs → scan-C0BMdmEd.mjs} +84 -7
  248. package/dist/{secret-BvtB7IjY.mjs → secret-bg73f1Ge.mjs} +63 -81
  249. package/dist/{serialize-BPvnNQuA.mjs → serialize-BtQyaYoC.mjs} +10 -4
  250. package/dist/{skills-BgVMvQdy.mjs → skills-C_yRTg0g.mjs} +3 -3
  251. package/dist/{subcommand-prompt-BSlnD1fI.mjs → subcommand-prompt-Bm3ZlYgA.mjs} +39 -44
  252. package/dist/sveltekit.d.mts +2 -1
  253. package/dist/sveltekit.mjs +1 -1
  254. package/dist/validate-BQFdf0Pi.mjs +1 -0
  255. package/dist/{validate-C9qJArv7.mjs → validate-CheZGRE7.mjs} +4 -8
  256. package/dist/version-metadata-DIrqEHQo.mjs +73 -0
  257. package/dist/{wrangler-D9KTuqnv.mjs → wrangler-BYs07Rxy.mjs} +119 -378
  258. package/dist/wrapper-CwhpNWtH.mjs +663 -0
  259. package/dist/{ws-CChrrAU-.d.mts → ws-aKVYB-fA.d.mts} +1 -1
  260. package/dist/yarn-pnp-DLbEFMT0.mjs +92 -0
  261. package/docs/guide/ai.md +8 -2
  262. package/docs/guide/auth.md +2 -0
  263. package/docs/guide/database/mysql.md +14 -3
  264. package/docs/guide/database/postgresql.md +2 -0
  265. package/docs/guide/database.md +3 -5
  266. package/docs/guide/durable-state.md +32 -4
  267. package/docs/guide/platform/administration/access.md +4 -0
  268. package/docs/guide/platform/administration/connected-apps.md +55 -0
  269. package/docs/guide/platform/installation/credentials.md +21 -3
  270. package/docs/guide/platform/installation/domains.md +1 -1
  271. package/docs/guide/platform/installation/maintenance.md +38 -1
  272. package/docs/guide/platform/installation/prerequisites.md +6 -2
  273. package/docs/guide/platform/installation/setup.md +23 -1
  274. package/docs/guide/platform/installation/uninstall.md +6 -1
  275. package/docs/guide/quickstart.md +2 -0
  276. package/docs/guide/sandboxes.md +3 -1
  277. package/docs/guide/self-hosted-platform.md +1 -1
  278. package/docs/integrations/cloudflare.md +2 -2
  279. package/docs/integrations/frameworks/analog.md +3 -11
  280. package/docs/integrations/frameworks/nuxt.md +7 -10
  281. package/docs/integrations/nodejs-bun-deno.md +2 -0
  282. package/docs/reference/api/deploy.md +23 -0
  283. package/docs/reference/api/pages.md +2 -0
  284. package/docs/reference/api.md +3 -1
  285. package/docs/reference/cli/database.md +9 -3
  286. package/docs/reference/cli/platform-installation.md +19 -7
  287. package/docs/reference/cli/setup.md +2 -0
  288. package/docs/reference/config.md +2 -0
  289. package/package.json +39 -36
  290. package/sandbox.Dockerfile +1 -1
  291. package/skills/void/SKILL.md +2 -1
  292. package/dist/auth-LMJcNESe.mjs +0 -2
  293. package/dist/cf-build-output-H9a97vME.mjs +0 -2
  294. package/dist/cf-user-agent-lKblH3zU.mjs +0 -17
  295. package/dist/client-tso2OfcG.mjs +0 -2
  296. package/dist/cloudflare-operations-BlYLlowZ.mjs +0 -2
  297. package/dist/collect-DLyFDiKW.mjs +0 -2
  298. package/dist/config-5r-YpMC-.mjs +0 -2
  299. package/dist/config-B_ggY-IO.mjs +0 -89
  300. package/dist/config-BtSFvXZx.mjs +0 -2
  301. package/dist/config-write-AHGesphx.mjs +0 -61
  302. package/dist/create-project-qFDC45zg.mjs +0 -2
  303. package/dist/deploy-Bqe1PYBw.mjs +0 -10458
  304. package/dist/deploy-VsWsVQkw.mjs +0 -2
  305. package/dist/dev-inbox-CK-UuPob.mjs +0 -297
  306. package/dist/discover-EDUf6JJw.mjs +0 -2
  307. package/dist/dist-Bx1z6Eva.mjs +0 -2
  308. package/dist/dist-_UUlL6bg.mjs +0 -2
  309. package/dist/env-validation-CrVNjsPm.mjs +0 -2
  310. package/dist/env-validation-Dxdx_5wo.mjs +0 -951
  311. package/dist/gen-dA9hW7AV.mjs +0 -2
  312. package/dist/head-client-DzmJGN4C.mjs +0 -90
  313. package/dist/help-DfQYVfGY.mjs +0 -2
  314. package/dist/login-mx_ydhmp.mjs +0 -2
  315. package/dist/migrate-BHp07yJE.mjs +0 -2
  316. package/dist/operator-cmd-CwBB-1yb.mjs +0 -2
  317. package/dist/pages/head-client.d.mts +0 -13
  318. package/dist/pages/head-client.mjs +0 -2
  319. package/dist/plan-CH2G51je.mjs +0 -2
  320. package/dist/platform-args-D4E1PjVw.mjs +0 -2
  321. package/dist/platform-cmd-BbmJiq5O.mjs +0 -2
  322. package/dist/platform-lifecycle-ByAlpkhV.mjs +0 -2
  323. package/dist/platform-management-CBOT4BSM.mjs +0 -2
  324. package/dist/prepare-DRUV-g3X.mjs +0 -2
  325. package/dist/project-DedqCp59.mjs +0 -2
  326. package/dist/project-slug-DofjTFd-.mjs +0 -2
  327. package/dist/provision-Wu9ZBk9L.mjs +0 -2
  328. package/dist/resolve-project-BIeYs9Cw.mjs +0 -2
  329. package/dist/resolve-project-CP8rZs_p.mjs +0 -28
  330. package/dist/runner-9wjsSZx-.mjs +0 -2
  331. package/dist/runner-mysql-CEzKtmFb.mjs +0 -54
  332. package/dist/runner-mysql-CkFqZ6CE.mjs +0 -2
  333. package/dist/runner-pg-BGYRFdRp.mjs +0 -2
  334. package/dist/runtime/env-helpers.d.mts +0 -2
  335. package/dist/runtime/env-helpers.mjs +0 -2
  336. package/dist/validate-BJr9LfXy.mjs +0 -2
  337. package/dist/yarn-pnp-CUX_iTPA.mjs +0 -188
@@ -1,9 +1,597 @@
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";
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";
1
+ import { S as writePendingInstallationSecrets, _ as removePlatformConnectionForInstallation, g as removeInstallationRecoverySecrets, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, o as listInstallationManifests, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest } from "./platform-registry-B9mWX9LO.mjs";
2
+ import { t as ensurePlatformAccessToken } from "./cf-access-CWP4avfP.mjs";
3
+ import { A as confirm, H as outro, K as select, L as log, tt as isCancel } from "./dist-BVdsO7Py.mjs";
4
+ import { f as import_picocolors, l as formatUrl, m as printSuccess, p as note, r as createSpinner } from "./output-Ba6ZbrzY.mjs";
5
+ import { A as matchesRecordedR2Fingerprint, B as activatePlatformAccessCredentials, C as platformTrafficAttachmentWorker, D as formatPlatformInstallPlan, E as dnsRecordConfigHash, F as platformDatabaseNameFromOwnerUrl, G as PlatformCloudflareClient, I as turnOffInstalledProjectZeroTrust, J as hyperdriveOriginMatches, K as hyperdriveOriginConfigHash, L as assertPlatformOperatorCredentialCanApplyPlan, M as r2BucketConfigHash, N as resourceMarkerProblem, O as hasProvisionIntentStep, P as revalidatePlatformInstallAction, R as resolvePlatformCloudflareCredential, T as createPlatformInstallPlan, W as PlatformCloudflareApiError, Y as verifyPlatformRuntimeTokenPermissions, _ as readPlatformDatabase, a as authenticationSetupRequestHash, b as createPlatformInstallationSecrets, c as REMOTE_INSTALLATION_MANIFEST_KEY, g as formatZeroTrustApplications, h as assertProjectZeroTrustAllowsUninstall, j as platformAdoptionIntentHash, k as legacyResourceConfigHash, l as installPlatform, m as readPlatformProjectZeroTrustState, n as resolvePlatformInstallDatabaseUrl, o as readLoginReplacementFile, p as readPlatformProjectSlugs, q as hyperdriveOriginFromConnectionString, s as replacePendingInstallationAuthentication, t as assertNoninteractivePlatformMutationAcknowledged, u as acquirePlatformDatabaseLifecycleLock, v as loadPlatformRuntime, x as resolveBootstrapGitHubId, y as createPlatformHealthProbeToken, z as verifyPlatformCloudflareCredential } from "./platform-lifecycle-DBiS0cJd.mjs";
6
+ import { PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_R2_OWNERSHIP_MARKER_KEY, parsePlatformInstallationManifest, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, transitionInstallation } from "@void/platform";
7
+ import { setTimeout } from "node:timers/promises";
8
+ //#region src/cli/platform-teardown.ts
9
+ /**
10
+ * `void platform uninstall --purge-resources` deletes every Cloudflare resource the
11
+ * installation created, in an order Cloudflare accepts:
12
+ *
13
+ * traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
14
+ * -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
15
+ * -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
16
+ *
17
+ * Each resource is proven to belong to this installation right before its
18
+ * DELETE, and Cloudflare refuses a resource that something outside the
19
+ * installation still uses. The only forced DELETE is for a platform Worker
20
+ * that Cloudflare refused while every Worker it names as a dependent is one
21
+ * this installation created: the API and proxy Workers bind to each other, so
22
+ * neither can go first without it. A route, custom domain, DNS
23
+ * record, or Access resource that no longer matches what was recorded is kept
24
+ * and listed with the reason instead of stopping the run. Every finished
25
+ * deletion is checkpointed, so rerunning the command continues where it
26
+ * stopped. The routing KV namespace holds that checkpoint, so uninstall
27
+ * deletes it last.
28
+ *
29
+ * An uninstall without --purge-resources records what it leaves as retained. If it
30
+ * stopped before it finished, --purge-resources considers those resources again.
31
+ */
32
+ const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
33
+ const WORKER_ORDER = [
34
+ "email-gateway-worker",
35
+ "proxy-worker",
36
+ "dispatch-worker",
37
+ "api-worker"
38
+ ];
39
+ const PERMISSIONS = {
40
+ access: "Account → Access: Apps and Policies → Edit",
41
+ aiGateway: "Account → AI Gateway → Edit",
42
+ dns: "Zone → DNS → Edit",
43
+ emailRouting: "Zone → Email Routing Rules → Edit",
44
+ hyperdrive: "Account → Hyperdrive → Edit",
45
+ kv: "Account → Workers KV Storage → Edit",
46
+ queues: "Account → Queues → Edit",
47
+ r2: "Account → Workers R2 Storage → Edit",
48
+ routes: "Zone → Workers Routes → Edit",
49
+ serviceTokens: "Account → Access: Service Tokens → Edit",
50
+ workers: "Account → Workers Scripts → Edit"
51
+ };
52
+ function isPlatformTailWorker(manifest, resource) {
53
+ return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
54
+ }
55
+ function purgeResourcesResumeCommand(manifest) {
56
+ return `void platform uninstall ${manifest.id} --purge-resources`;
57
+ }
58
+ function accessStepKey(access) {
59
+ return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
60
+ }
61
+ /** Access resources that --purge-resources already deleted. */
62
+ function isPurgedAccessResource(manifest, access) {
63
+ return manifest.steps.some((step) => step.key === accessStepKey(access));
64
+ }
65
+ function hasStep(manifest, key) {
66
+ return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
67
+ }
68
+ function keptStepKey(key) {
69
+ return `kept:${key}`;
70
+ }
71
+ /**
72
+ * Why --purge-resources kept a resource it was going to delete, for example because
73
+ * a route now serves another Worker; null when it did not keep it. Access
74
+ * resources use the key `access:<key>`.
75
+ */
76
+ function purgeResourcesKeptReason(manifest, key) {
77
+ return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
78
+ }
79
+ /** An uninstall that started has not finished, so what it kept can be reconsidered. */
80
+ function isReopenable(manifest) {
81
+ return manifest.status !== "uninstalled";
82
+ }
83
+ /**
84
+ * A resource the installation created that --purge-resources may still delete: one
85
+ * that is ready, or one that an uninstall which stopped before it finished
86
+ * recorded as retained. Resources --purge-resources itself kept, and earlier
87
+ * versions of a resource kept when it was replaced, are never reconsidered.
88
+ */
89
+ function isPurgeResourcesCandidate(manifest, resource) {
90
+ return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
91
+ }
92
+ /** The zone that routes and DNS records live in, if it can still be used. */
93
+ function usableZone(manifest) {
94
+ return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
95
+ }
96
+ /**
97
+ * A recorded resource whose live configuration no longer matches the
98
+ * installation's record. --purge-resources keeps such a DNS record and continues.
99
+ */
100
+ var PlatformResourceChangedError = class extends Error {
101
+ resourceKey;
102
+ reason;
103
+ constructor(resourceKey, reason, message) {
104
+ super(message);
105
+ this.resourceKey = resourceKey;
106
+ this.reason = reason;
107
+ this.name = "PlatformResourceChangedError";
108
+ }
109
+ };
110
+ /** The zone whose Email Routing rules may send mail to the gateway Worker. */
111
+ function emailRoutingZone(manifest) {
112
+ return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
113
+ }
114
+ /** Everything --purge-resources will delete, in deletion order. */
115
+ function purgeResourcesTargets(manifest) {
116
+ const resources = manifest.resources;
117
+ const routing = resources.find((resource) => resource.key === "routing-kv");
118
+ const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
119
+ if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
120
+ const zone = usableZone(manifest);
121
+ const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
122
+ const targets = [];
123
+ for (const resource of resources) {
124
+ 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;
125
+ const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
126
+ const worker = workers.find((entry) => entry.name === name);
127
+ if (worker) targets.push({
128
+ phase: "traffic",
129
+ resource,
130
+ worker
131
+ });
132
+ }
133
+ for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
134
+ phase: "dns",
135
+ resource
136
+ });
137
+ const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
138
+ for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
139
+ phase: "access",
140
+ access: entry
141
+ });
142
+ const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
143
+ if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
144
+ phase: "email",
145
+ resource: gateway
146
+ });
147
+ const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
148
+ for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
149
+ phase: "queue-consumers",
150
+ resource
151
+ });
152
+ const rank = (resource) => {
153
+ const index = WORKER_ORDER.indexOf(resource.key);
154
+ return index < 0 ? WORKER_ORDER.length : index;
155
+ };
156
+ for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
157
+ phase: "workers",
158
+ resource
159
+ });
160
+ for (const resource of queues) targets.push({
161
+ phase: "queues",
162
+ resource
163
+ });
164
+ for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
165
+ phase: "dispatch",
166
+ resource
167
+ });
168
+ for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
169
+ phase: "tail",
170
+ resource
171
+ });
172
+ for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
173
+ phase: "data",
174
+ resource
175
+ });
176
+ for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
177
+ phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
178
+ resource
179
+ });
180
+ if (candidate(routing)) targets.push({
181
+ phase: "routing",
182
+ resource: routing
183
+ });
184
+ return targets;
185
+ }
186
+ /** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
187
+ function purgeResourcesHandlesEmailRouting(manifest) {
188
+ return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
189
+ }
190
+ /** Manifest resources that --purge-resources leaves in the account. */
191
+ function purgeResourcesKeptResources(manifest) {
192
+ const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
193
+ return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
194
+ }
195
+ function resourceLabel(resource) {
196
+ return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
197
+ }
198
+ /** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
199
+ function describePurgeResourcesPlan(manifest) {
200
+ const targets = purgeResourcesTargets(manifest);
201
+ const lines = [];
202
+ let step = 0;
203
+ let previous = null;
204
+ const consumerQueues = [];
205
+ for (const target of targets) {
206
+ if (target.phase === "email") {
207
+ step++;
208
+ previous = target.phase;
209
+ 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)`);
210
+ continue;
211
+ }
212
+ if (target.phase !== previous) {
213
+ step++;
214
+ previous = target.phase;
215
+ }
216
+ const prefix = ` ${step}. `;
217
+ if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
218
+ else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
219
+ else if (target.phase === "queue-consumers") {
220
+ consumerQueues.push(target.resource.name);
221
+ if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
222
+ lines.push(` ${target.resource.name}`);
223
+ } else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
224
+ else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
225
+ else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
226
+ else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
227
+ else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
228
+ else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
229
+ else lines.push(`${prefix}${resourceLabel(target.resource)}`);
230
+ }
231
+ 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."];
232
+ }
233
+ /** Lines naming what --purge-resources never removes. */
234
+ function purgeResourcesAlwaysKept() {
235
+ return [
236
+ " The external PostgreSQL database and its data.",
237
+ " Analytics Engine datasets; Cloudflare has no API to delete them.",
238
+ " Access policies and identity providers you chose.",
239
+ " API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
240
+ ];
241
+ }
242
+ /** Token permissions --purge-resources needs for this manifest. */
243
+ function purgeResourcesPermissions(manifest) {
244
+ const targets = purgeResourcesTargets(manifest);
245
+ const needed = /* @__PURE__ */ new Set();
246
+ for (const target of targets) {
247
+ if (target.phase === "access") {
248
+ needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
249
+ continue;
250
+ }
251
+ if (target.phase === "email") {
252
+ needed.add(PERMISSIONS.emailRouting);
253
+ continue;
254
+ }
255
+ const { kind } = target.resource;
256
+ if (kind === "worker-route") needed.add(PERMISSIONS.routes);
257
+ else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
258
+ else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
259
+ else if (kind === "queue") needed.add(PERMISSIONS.queues);
260
+ else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
261
+ else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
262
+ else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
263
+ else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
264
+ }
265
+ return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
266
+ }
267
+ function deniedStatus(error) {
268
+ return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
269
+ }
270
+ /**
271
+ * Check resource reads before the first change. Browser login cannot change
272
+ * DNS or Access, so those need an API token. Read probes cannot prove Edit;
273
+ * the plan lists the required permissions.
274
+ */
275
+ async function checkPurgeResourcesCredentials(client, manifest, credential) {
276
+ const targets = purgeResourcesTargets(manifest);
277
+ const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
278
+ const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
279
+ const has = (kind) => resources.some((resource) => resource.kind === kind);
280
+ const zone = manifest.resources.find((resource) => resource.key === "zone");
281
+ const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
282
+ const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
283
+ 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}` : ""}`;
284
+ if (credential.source === "oauth") {
285
+ const browserMissing = [
286
+ ...has("dns-record") ? [PERMISSIONS.dns] : [],
287
+ ...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
288
+ ...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
289
+ ...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
290
+ ];
291
+ 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.`);
292
+ }
293
+ const probes = [];
294
+ if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
295
+ if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
296
+ if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
297
+ for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
298
+ else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
299
+ for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
300
+ if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
301
+ if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
302
+ if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
303
+ if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
304
+ if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
305
+ if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
306
+ const missing = /* @__PURE__ */ new Set();
307
+ for (const [permission, probe] of probes) try {
308
+ await probe();
309
+ } catch (error) {
310
+ if (!deniedStatus(error)) throw error;
311
+ missing.add(permission);
312
+ }
313
+ 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.`);
314
+ }
315
+ const LISTED_PROJECTS = 20;
316
+ /**
317
+ * Projects own Workers, storage, and domains that only the running platform
318
+ * can delete. --purge-resources deletes the platform, so it refuses before any
319
+ * change while a project remains.
320
+ */
321
+ async function assertNoPlatformProjects(read) {
322
+ 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.`);
323
+ if (slugs.length) throw new Error([
324
+ `platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
325
+ "--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
326
+ "Delete each project first with `void project delete <name>`, then retry:",
327
+ ...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
328
+ ...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
329
+ ].join("\n"));
330
+ }
331
+ const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
332
+ /**
333
+ * AI Gateway ids are chosen by the caller and can be reused after a delete,
334
+ * and gateways carry no ownership marker. Only a creation time next to the
335
+ * installer's provision intent shows that this is the gateway it created.
336
+ */
337
+ function createdDuringInstallation(manifest, resource, createdAt) {
338
+ const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
339
+ if (Number.isNaN(created)) return false;
340
+ 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);
341
+ }
342
+ function ownershipRefusal(manifest, description, reason) {
343
+ 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)}`);
344
+ }
345
+ function cloudflareRefusal(manifest, description, error, hint) {
346
+ if (!(error instanceof PlatformCloudflareApiError)) return error;
347
+ return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
348
+ }
349
+ /**
350
+ * Delete every target in order. Returns the manifest with each deleted
351
+ * resource recorded; routing KV is left for the caller to delete last.
352
+ */
353
+ async function runPurgeResourcesTeardown(context, initial) {
354
+ const { client } = context;
355
+ let next = initial;
356
+ const accountId = next.account.id;
357
+ const save = async (manifest) => {
358
+ next = manifest;
359
+ await context.checkpoint(next);
360
+ };
361
+ const markDeleted = async (key) => {
362
+ const resource = next.resources.find((entry) => entry.key === key);
363
+ await save(recordInstallationResource(next, {
364
+ ...resource,
365
+ state: "deleted"
366
+ }, (/* @__PURE__ */ new Date()).toISOString()));
367
+ };
368
+ const recordStep = async (key, resultHash = "completed") => {
369
+ const now = (/* @__PURE__ */ new Date()).toISOString();
370
+ await save(recordInstallationStep(next, {
371
+ completedAt: now,
372
+ key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
373
+ resultHash
374
+ }, now));
375
+ };
376
+ /**
377
+ * Live proof for these keys. A resource an earlier uninstall recorded as
378
+ * retained is proven like a ready one; only its recorded state differs.
379
+ */
380
+ const verify = (keys) => context.verify({
381
+ ...next,
382
+ resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
383
+ ...resource,
384
+ state: "ready"
385
+ } : resource)
386
+ }, keys);
387
+ /**
388
+ * Prove the resource; false when it is already gone (and recorded).
389
+ *
390
+ * Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
391
+ * addressed by name, and a route or custom domain can be changed in place.
392
+ * Something replaced between this proof and the DELETE can still be removed.
393
+ * That one-request window is accepted because --purge-resources is opt-in and its
394
+ * plan lists every resource first. A forced Worker DELETE has the same
395
+ * window between reading its dependents and the DELETE.
396
+ */
397
+ const prove = async (resource) => {
398
+ if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
399
+ await markDeleted(resource.key);
400
+ return false;
401
+ }
402
+ return true;
403
+ };
404
+ /** Keep a resource that no longer matches the record, and save why. */
405
+ const keep = async (key, reason) => {
406
+ const resource = next.resources.find((entry) => entry.key === key);
407
+ if (resource?.state === "ready") await save(recordInstallationResource(next, {
408
+ ...resource,
409
+ state: "retained"
410
+ }, (/* @__PURE__ */ new Date()).toISOString()));
411
+ await recordStep(keptStepKey(key), reason);
412
+ };
413
+ const remove = async (resource, hint, zoneId) => {
414
+ if (!await prove(resource)) return;
415
+ try {
416
+ await client.deleteResource(accountId, resource, zoneId);
417
+ } catch (error) {
418
+ throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
419
+ }
420
+ await markDeleted(resource.key);
421
+ };
422
+ const zone = usableZone(next);
423
+ const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
424
+ /** The recorded Worker under this name that this run still deletes, if any. */
425
+ const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
426
+ 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.";
427
+ /**
428
+ * Delete a platform Worker. When Cloudflare refuses because other Workers
429
+ * still bind to it, force the DELETE only if each of them is a Worker this
430
+ * run deletes too and still carries this installation's ownership marker.
431
+ */
432
+ const removeWorker = async (resource) => {
433
+ if (!await prove(resource)) return;
434
+ try {
435
+ await client.deleteResource(accountId, resource);
436
+ } catch (error) {
437
+ if (!(error instanceof PlatformCloudflareApiError)) throw error;
438
+ const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
439
+ const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
440
+ const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
441
+ const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
442
+ 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);
443
+ try {
444
+ await client.forceDeleteWorker(accountId, resource.name);
445
+ } catch (forced) {
446
+ throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
447
+ }
448
+ }
449
+ await markDeleted(resource.key);
450
+ };
451
+ for (const target of purgeResourcesTargets(initial)) {
452
+ if (target.phase === "access") {
453
+ const { access } = target;
454
+ const live = await client.getAccessResource(access.accountId, access.kind, access.id);
455
+ if (live) {
456
+ if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
457
+ await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
458
+ continue;
459
+ }
460
+ try {
461
+ await client.deleteAccessResource(access.accountId, access.kind, access.id);
462
+ } catch (error) {
463
+ throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
464
+ }
465
+ }
466
+ await recordStep(`access:${access.key}`);
467
+ continue;
468
+ }
469
+ const current = next.resources.find((resource) => resource.key === target.resource.key);
470
+ if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
471
+ switch (target.phase) {
472
+ case "traffic": {
473
+ const worker = next.resources.find((resource) => resource.key === target.worker.key);
474
+ if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
475
+ if (current.kind === "worker-route") {
476
+ const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
477
+ if (live && (live.pattern !== current.name || live.script !== worker.name)) {
478
+ await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
479
+ break;
480
+ }
481
+ if (live) await client.deleteWorkerRoute(zone.id, current.id);
482
+ } else {
483
+ const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
484
+ if (live && (live.hostname !== current.name || live.service !== worker.name)) {
485
+ await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
486
+ break;
487
+ }
488
+ if (live) await client.deleteWorkerDomain(accountId, current.id);
489
+ }
490
+ await markDeleted(current.key);
491
+ break;
492
+ }
493
+ case "dns":
494
+ try {
495
+ await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
496
+ } catch (error) {
497
+ if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
498
+ await keep(current.key, error.reason);
499
+ }
500
+ break;
501
+ case "email": {
502
+ if (!await prove(current)) break;
503
+ const zoneId = emailRoutingZone(next);
504
+ const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
505
+ try {
506
+ for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
507
+ if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
508
+ } catch (error) {
509
+ throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
510
+ }
511
+ await recordStep("email-routing");
512
+ break;
513
+ }
514
+ case "queue-consumers":
515
+ if (!await prove(current)) break;
516
+ 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);
517
+ await recordStep(`consumers:${current.key}`);
518
+ break;
519
+ case "workers":
520
+ await removeWorker(current);
521
+ break;
522
+ case "tail":
523
+ await remove(current, workerHint);
524
+ break;
525
+ case "queues":
526
+ 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.");
527
+ break;
528
+ case "dispatch": {
529
+ if (!await prove(current)) break;
530
+ const projects = await context.readProjects();
531
+ 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)}`);
532
+ for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
533
+ await client.deleteDispatchNamespaceScript(accountId, current.name, script);
534
+ } catch (error) {
535
+ throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
536
+ }
537
+ try {
538
+ await client.deleteResource(accountId, current);
539
+ } catch (error) {
540
+ throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
541
+ }
542
+ await markDeleted(current.key);
543
+ break;
544
+ }
545
+ case "ai-gateway": {
546
+ const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
547
+ if (live && !createdDuringInstallation(next, current, live.createdAt)) {
548
+ await keep(current.key, "Cloudflare does not show it was created during installation");
549
+ break;
550
+ }
551
+ await remove(current, "Check that nothing outside this installation still uses it.");
552
+ break;
553
+ }
554
+ case "data":
555
+ await remove(current, "Check that nothing outside this installation still uses it.");
556
+ break;
557
+ case "r2": await purgeBucket(current);
558
+ }
559
+ }
560
+ return next;
561
+ async function purgeBucket(bucket) {
562
+ const markerStep = `r2-marker:${bucket.key}`;
563
+ if (!hasStep(next, markerStep)) {
564
+ if (!await prove(bucket)) return;
565
+ await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
566
+ 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");
567
+ });
568
+ if (!await prove(bucket)) return;
569
+ const generation = await context.bucketGeneration(next, bucket);
570
+ if (generation === null) {
571
+ await markDeleted(bucket.key);
572
+ return;
573
+ }
574
+ await recordStep(markerStep, generation);
575
+ await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
576
+ } else {
577
+ const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
578
+ const generation = await context.bucketGeneration(next, bucket);
579
+ if (generation === null) {
580
+ await markDeleted(bucket.key);
581
+ return;
582
+ }
583
+ if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
584
+ await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
585
+ }
586
+ try {
587
+ await client.deleteResource(accountId, bucket);
588
+ } catch (error) {
589
+ 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.");
590
+ }
591
+ await markDeleted(bucket.key);
592
+ }
593
+ }
594
+ //#endregion
7
595
  //#region src/cli/platform-management.ts
8
596
  async function verifyPlatformInstallationOwnership(client, manifest, options = {}) {
9
597
  const ready = manifest.resources.filter((resource) => resource.state === "ready" && isOwnershipVerifiableKind(resource.kind) && !options.retainedReplacementKeys?.has(resource.key));
@@ -64,8 +652,7 @@ async function runInstalledPlatformCommand(args) {
64
652
  const rollbackSourceRuntime = args.subcommand === "rollback" ? loadPlatformRuntime(args.fromRuntime) : null;
65
653
  let progress;
66
654
  let reportResult;
67
- const execute = async (currentManifest, databaseLock, databaseCheckpointExists = false) => {
68
- const manifest = currentManifest;
655
+ const execute = async (manifest, databaseLock, databaseCheckpointExists = false) => {
69
656
  if (args.subcommand === "rollback") assertPlatformRollbackCompatibility(manifest, runtime, rollbackSourceRuntime);
70
657
  const recovery = readInstallationRecoverySecrets(manifest);
71
658
  const gatewayToken = process.env.VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN || recovery?.cloudflareApiToken;
@@ -79,6 +666,8 @@ async function runInstalledPlatformCommand(args) {
79
666
  return;
80
667
  }
81
668
  if (args.subcommand === "uninstall") {
669
+ const apiUrl = manifest.endpoints.apiUrl;
670
+ const appAccessSetupToken = recovery?.appAccessSetupToken;
82
671
  await uninstallPlatform(client, manifest, args, {
83
672
  checkpoint: databaseLock?.checkpoint,
84
673
  databaseUrl,
@@ -90,7 +679,19 @@ async function runInstalledPlatformCommand(args) {
90
679
  credential: {
91
680
  source: operatorCredential.source,
92
681
  aiGatewayToken: !!gatewayToken
93
- }
682
+ },
683
+ ...apiUrl && appAccessSetupToken ? { turnOffProjectZeroTrust: async () => {
684
+ const saved = recovery?.accessCredentialsJson ? JSON.parse(recovery.accessCredentialsJson) : {};
685
+ if (saved && typeof saved === "object" && apiUrl in saved) activatePlatformAccessCredentials(JSON.stringify({ [apiUrl]: saved[apiUrl] }), [apiUrl]);
686
+ if (manifest.deployment.authentication.accessProtection) ensurePlatformAccessToken(apiUrl, false, true);
687
+ await turnOffInstalledProjectZeroTrust({
688
+ apiUrl,
689
+ installationId: manifest.id,
690
+ probeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
691
+ setupToken: appAccessSetupToken,
692
+ updateCommand: readPendingInstallationSecrets(manifest) ? `void platform install --resume --name ${commandArgument(manifest.id)}` : `void platform upgrade ${commandArgument(manifest.id)}`
693
+ });
694
+ } } : {}
94
695
  });
95
696
  return;
96
697
  }
@@ -98,6 +699,13 @@ async function runInstalledPlatformCommand(args) {
98
699
  log.info(`${manifest.deployment.displayName} is already enabled.`);
99
700
  return;
100
701
  }
702
+ const loginFile = args.subcommand === "repair" ? args.authConfig : void 0;
703
+ if (loginFile !== void 0) {
704
+ if (manifest.desiredState === "disabled") throw new Error(`platform: This platform is disabled. Run \`void platform enable ${commandArgument(manifest.id)}\` first, then repair to change the login settings.`);
705
+ if (!manifest.deployment.authentication.configurable) throw new Error("platform: This installation signs in with the GitHub OAuth App from its installation settings. Set VOID_PLATFORM_GITHUB_CLIENT_ID and VOID_PLATFORM_GITHUB_CLIENT_SECRET when running repair to replace it.");
706
+ if (!recovery?.authenticationSetupJson || !recovery.authSetupToken) throw new Error("platform: Changing the login settings needs the setup authority saved on the computer that installed the platform. Run this repair there.");
707
+ await readLoginReplacementFile(loginFile, manifest.deployment.authentication, recovery.authenticationSetupJson);
708
+ }
101
709
  const deployment = manifest.deployment;
102
710
  const planInput = {
103
711
  accountId: manifest.account.id,
@@ -129,6 +737,7 @@ async function runInstalledPlatformCommand(args) {
129
737
  ].join("\n"), "Platform runtime");
130
738
  log.info(manifest.desiredState === "disabled" && args.subcommand !== "enable" ? "Application traffic will stay paused because this installation is disabled." : "Application traffic may pause briefly during a safe cutover.");
131
739
  if (args.plan) {
740
+ if (loginFile !== void 0) log.info(`Login settings will be replaced from ${loginFile}; a new setup code is printed at the end.`);
132
741
  outro("No Cloudflare or local installation state was changed.");
133
742
  return;
134
743
  }
@@ -140,6 +749,21 @@ async function runInstalledPlatformCommand(args) {
140
749
  databaseUrl
141
750
  });
142
751
  markPhase("ownership");
752
+ let replacedLogin;
753
+ if (args.subcommand === "repair" && (loginFile !== void 0 || process.stdin.isTTY && !args.yes) && manifest.desiredState !== "disabled" && manifest.deployment.authentication.configurable && recovery?.authenticationSetupJson && recovery.authSetupToken) {
754
+ const setup = await databaseLock?.readAdministratorSetup?.();
755
+ if (loginFile !== void 0) {
756
+ if (!setup) throw new Error("platform: The database lifecycle lock cannot read the administrator setup.");
757
+ if (setup.usersExist || setup.requestHash !== null && !setup.pending) throw new Error("platform: The first administrator has already signed in. Change login methods with `void platform config auth`.");
758
+ if (setup.requestHash === null) throw new Error(`platform: Administrator setup was never started. Resume the install with \`void platform install --resume --name ${commandArgument(manifest.id)}\`.`);
759
+ }
760
+ if (setup?.pending && !setup.usersExist && (loginFile !== void 0 || await promptForConfirmation("First administrator setup is not finished. Change the login settings before continuing?", false))) replacedLogin = await replacePendingInstallationAuthentication({
761
+ ...loginFile !== void 0 ? { file: loginFile } : {},
762
+ authentication: manifest.deployment.authentication,
763
+ setupJson: recovery.authenticationSetupJson,
764
+ apiUrl: plan.apiUrl
765
+ });
766
+ }
143
767
  const installation = args.subcommand === "enable" ? setInstallationDesiredState(manifest, "enabled", (/* @__PURE__ */ new Date()).toISOString()) : manifest;
144
768
  const bootstrapGitHubLogin = process.env.VOID_PLATFORM_ADMIN_GITHUB_LOGIN || recovery?.bootstrapGitHubLogin || "";
145
769
  const bootstrapGitHubId = process.env.VOID_PLATFORM_ADMIN_GITHUB_ID || recovery?.bootstrapGitHubId || (bootstrapGitHubLogin ? await resolveBootstrapGitHubId(bootstrapGitHubLogin) : "");
@@ -161,7 +785,8 @@ async function runInstalledPlatformCommand(args) {
161
785
  projectSecretKeysJson: process.env.VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON || "",
162
786
  projectSecretActiveKeyVersion: process.env.VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION || "",
163
787
  r2AccessKeyId: process.env.VOID_PLATFORM_R2_ACCESS_KEY_ID || "",
164
- r2SecretAccessKey: process.env.VOID_PLATFORM_R2_SECRET_ACCESS_KEY || ""
788
+ r2SecretAccessKey: process.env.VOID_PLATFORM_R2_SECRET_ACCESS_KEY || "",
789
+ ...replacedLogin
165
790
  });
166
791
  const runtimeTokenToPreflight = resolvePlatformRuntimeTokenToPreflight(plan, secrets);
167
792
  if (runtimeTokenToPreflight) await verifyPlatformRuntimeTokenPermissions(new PlatformCloudflareClient(runtimeTokenToPreflight), {
@@ -177,6 +802,17 @@ async function runInstalledPlatformCommand(args) {
177
802
  } });
178
803
  progress.start(args.subcommand === "upgrade" ? "Upgrading the platform" : args.subcommand === "rollback" ? "Rolling back the platform" : args.subcommand === "enable" ? "Enabling the platform" : "Repairing the platform");
179
804
  if (installation !== manifest) writeInstallationManifest(installation);
805
+ const keepAcceptedLogin = async (error) => {
806
+ const after = replacedLogin ? await databaseLock?.readAdministratorSetup?.().catch(() => void 0) : void 0;
807
+ if (!after || !recovery?.authenticationSetupJson || after.requestHash !== authenticationSetupRequestHash(recovery.authenticationSetupJson) || !after.usersExist && after.pending) return error;
808
+ const pending = readPendingInstallationSecrets(installation);
809
+ if (pending) writePendingInstallationSecrets(installation, {
810
+ ...pending,
811
+ authenticationSetupJson: recovery.authenticationSetupJson,
812
+ ...recovery.authBootstrapCode ? { authBootstrapCode: recovery.authBootstrapCode } : {}
813
+ });
814
+ return new Error("platform: The first administrator signed in before the new login settings were applied. Nothing changed. Change login methods with `void platform config auth`.", { cause: error });
815
+ };
180
816
  const result = await installPlatform(plan, secrets, installation, {
181
817
  acquireDatabaseLifecycleLock: async () => Object.assign(async () => {}, {
182
818
  checkpoint: databaseLock?.checkpoint,
@@ -207,6 +843,8 @@ async function runInstalledPlatformCommand(args) {
207
843
  repairMissing: args.subcommand === "repair",
208
844
  stableWorkerUpdate: args.subcommand === "upgrade"
209
845
  })
846
+ }).catch(async (error) => {
847
+ throw await keepAcceptedLogin(error);
210
848
  });
211
849
  progress.message("Finalizing platform changes");
212
850
  markPhase("installer complete");
@@ -233,8 +871,10 @@ async function runInstalledPlatformCommand(args) {
233
871
  if (result.setupPending) {
234
872
  const code = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
235
873
  if (code) {
236
- log.info(`First administrator setup: ${plan.apiUrl}/setup`);
874
+ log.info(`First administrator setup: ${formatUrl(`${plan.apiUrl}/setup`)}`);
237
875
  console.log(`One-time administrator setup code: ${code}`);
876
+ log.info(`Login callback URL: ${formatUrl(`${plan.apiUrl}/auth/callback`)}`);
877
+ log.info(`If sign-in fails, run \`void platform repair ${commandArgument(manifest.id)}\` to change the login settings.`);
238
878
  }
239
879
  }
240
880
  printSuccess(`${deployment.displayName}: runtime ${runtime.manifest.platformVersion} ${args.subcommand === "rollback" ? "restored" : "ready"}.`);
@@ -295,7 +935,7 @@ function assertPlatformRollbackCompatibility(installation, target, source) {
295
935
  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
936
  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
937
  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.");
938
+ if (target.manifest.safetyProtocols.sandboxDrain < installation.safetyProtocols.sandboxDrain) throw new Error("platform: The rollback target predates the installed sandbox-drain safety protocol.");
299
939
  }
300
940
  function resolvePlatformRuntimeTokenToPreflight(plan, secrets) {
301
941
  const explicitUpdate = secrets.workerSecretUpdates?.CF_API_TOKEN;
@@ -319,18 +959,17 @@ async function resolveInstalledPlatformManifest(selector, dependencies = {}) {
319
959
  return resolveInstallationManifest(String(picked));
320
960
  }
321
961
  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);
962
+ const manifest = await recoverDurablyMarkedProvisionIntents(client, initialManifest);
963
+ const saveCheckpoint = (state) => writeLifecycleCheckpoint(client, state, dependencies.checkpoint);
964
+ const saveRemoteManifest = (state) => writeRemoteManifest(client, state, dependencies.checkpoint);
325
965
  note("Block all platform traffic and the platform API. Routes, custom domains, Workers, and data stay installed.", "Disable plan");
326
966
  if (planOnly) {
327
967
  outro("No Cloudflare or local installation state was changed.");
328
968
  return;
329
969
  }
330
- const manifest = reconciled;
331
970
  if (manifest !== initialManifest) writeInstallationManifest(manifest);
332
971
  const alreadyDisabled = manifest.status === "disabled";
333
- const missing = await verifyPlatformMutationOwnership(client, manifest, /* @__PURE__ */ new Set([
972
+ await verifyPlatformMutationOwnership(client, manifest, /* @__PURE__ */ new Set([
334
973
  "routing-kv",
335
974
  "zone",
336
975
  ...manifest.resources.filter((resource) => resource.kind === "worker").map((resource) => resource.key)
@@ -342,30 +981,27 @@ async function disablePlatform(client, initialManifest, planOnly, dependencies =
342
981
  writeInstallationManifest(next);
343
982
  }
344
983
  try {
345
- await writeRemoteManifestIfPresent(client, next);
346
- const zone = next.resources.find((resource) => resource.key === "zone");
984
+ await saveRemoteManifest(next);
347
985
  const routing = requireResource(next, "routing-kv");
348
986
  const trafficGeneration = currentTrafficGeneration(next);
349
987
  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
988
  next = markTrafficAttachmentsRetained(next);
353
989
  if (!hasLifecycleStep(next, `disable:${trafficGeneration}:routes`)) next = recordLifecycleStep(next, `disable:${trafficGeneration}:routes`);
354
- await writeLifecycleCheckpoint(client, next);
990
+ await saveCheckpoint(next);
355
991
  next = transitionInstallation(next, "disabled", (/* @__PURE__ */ new Date()).toISOString());
356
- await writeLifecycleCheckpoint(client, next);
992
+ await saveCheckpoint(next);
357
993
  printSuccess(`${next.deployment.displayName} is disabled. Its data was not changed.`);
358
994
  } catch (error) {
359
995
  next = transitionInstallation(next, "failed", (/* @__PURE__ */ new Date()).toISOString());
360
996
  writeInstallationManifest(next);
361
- await writeRemoteManifestIfPresent(client, next).catch(() => {});
997
+ await saveRemoteManifest(next).catch(() => {});
362
998
  throw error;
363
999
  }
364
1000
  }
365
1001
  async function uninstallPlatform(client, initialManifest, args, dependencies = {}) {
366
1002
  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);
1003
+ const saveCheckpoint = (state) => writeLifecycleCheckpoint(client, state, dependencies.checkpoint);
1004
+ const saveRemoteManifest = (state) => writeRemoteManifest(client, state, dependencies.checkpoint);
369
1005
  const retainData = !args.purgeData;
370
1006
  const purgeResources = args.purgeResources;
371
1007
  const routingKv = manifest.resources.find((resource) => resource.key === "routing-kv");
@@ -375,17 +1011,16 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
375
1011
  const interactive = dependencies.interactive ?? process.stdin.isTTY;
376
1012
  const confirmAction = dependencies.confirm ?? promptForConfirmation;
377
1013
  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
- await assertProjectZeroTrustAllowsUninstall(dependencies.readProjectZeroTrust);
1014
+ const zeroTrust = await assertProjectZeroTrustAllowsUninstall(dependencies.readProjectZeroTrust, { canTurnOff: !!dependencies.turnOffProjectZeroTrust });
379
1015
  if (purgeResources) await assertNoPlatformProjects(dependencies.readProjects);
380
- const purgeResourcesClient = purgeResources ? requirePurgeResourcesClient(client) : null;
381
- if (args.plan || interactive && !args.yes) note(purgeResources ? purgeResourcesPlanText(manifest) : [
1016
+ if (args.plan || interactive && !args.yes) note([...zeroTrust ? zeroTrust.turnOff.length ? ["Turn off project Zero Trust; no platform user exists yet. Delete its Access applications:", formatZeroTrustApplications(zeroTrust.turnOff)] : ["Turn off project Zero Trust; no platform user exists yet."] : [], purgeResources ? purgeResourcesPlanText(manifest) : [
382
1017
  "Disable platform traffic.",
383
1018
  retainData ? "Keep Workers and data." : "Delete eligible data; keep Workers, R2, and AI Gateway.",
384
1019
  "Delete unused Queues.",
385
1020
  ...describeKeptResources(manifest, plannedKeptResources(manifest, retainData, hasRoutingCheckpoint), "Keep for manual cleanup:", "Keep Access resources:")
386
- ].join("\n"), import_picocolors.default.bold("Uninstall plan"));
387
- if (purgeResourcesClient) {
388
- const credentialCheck = checkPurgeResourcesCredentials(purgeResourcesClient, manifest, dependencies.credential ?? {
1021
+ ].join("\n")].join("\n"), import_picocolors.default.bold("Uninstall plan"));
1022
+ if (purgeResources) {
1023
+ const credentialCheck = checkPurgeResourcesCredentials(client, manifest, dependencies.credential ?? {
389
1024
  source: "environment",
390
1025
  aiGatewayToken: false
391
1026
  });
@@ -399,7 +1034,6 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
399
1034
  return;
400
1035
  }
401
1036
  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
1037
  if (interactive && !args.yes) {
404
1038
  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
1039
  outro("No changes made.");
@@ -433,6 +1067,10 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
433
1067
  }
434
1068
  const { fenceProjectZeroTrust, liftProjectZeroTrustFence, lockProjectZeroTrust } = dependencies;
435
1069
  if (!lockProjectZeroTrust || !fenceProjectZeroTrust || !liftProjectZeroTrustFence) throw new Error("platform: Refusing to uninstall because no platform database connection is available to hold off project Zero Trust changes.");
1070
+ if (zeroTrust) {
1071
+ log.step("Turning off project Zero Trust");
1072
+ await dependencies.turnOffProjectZeroTrust();
1073
+ }
436
1074
  const releaseProjectZeroTrust = await lockProjectZeroTrust();
437
1075
  let fenced = false;
438
1076
  let disablingTraffic = false;
@@ -455,7 +1093,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
455
1093
  throw error;
456
1094
  }
457
1095
  try {
458
- await writeRemoteManifestIfPresent(client, next);
1096
+ await saveRemoteManifest(next);
459
1097
  const routing = hasRoutingCheckpoint ? requireResource(next, "routing-kv") : void 0;
460
1098
  disablingTraffic = true;
461
1099
  if (routing) await client.putKvValue(next.account.id, routing.id, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
@@ -463,7 +1101,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
463
1101
  for (const key of missingResources) {
464
1102
  const resource = requireResource(next, key);
465
1103
  next = updateResourceState(next, key, resource.ownership === "created" ? "deleted" : "retained");
466
- await writeLifecycleCheckpoint(client, next);
1104
+ await saveCheckpoint(next);
467
1105
  }
468
1106
  const workers = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.state === "ready").map((resource) => resource.name));
469
1107
  const zone = next.resources.find((resource) => resource.key === "zone");
@@ -473,18 +1111,16 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
473
1111
  "zone",
474
1112
  worker.key
475
1113
  ]), { 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
1114
  next = markTrafficAttachmentsRetained(next);
479
1115
  next = recordLifecycleStep(next, `uninstall:routes:${worker.key}`);
480
- await writeLifecycleCheckpoint(client, next);
1116
+ await saveCheckpoint(next);
481
1117
  }
482
1118
  }
483
- if (purgeResourcesClient && hasRoutingCheckpoint) next = await runPurgeResourcesTeardown({
484
- client: purgeResourcesClient,
1119
+ if (purgeResources && hasRoutingCheckpoint) next = await runPurgeResourcesTeardown({
1120
+ client,
485
1121
  checkpoint: async (checkpointManifest) => {
486
1122
  next = checkpointManifest;
487
- await writeLifecycleCheckpoint(client, checkpointManifest);
1123
+ await saveCheckpoint(checkpointManifest);
488
1124
  },
489
1125
  verify: (checkpointManifest, keys) => verifyPlatformMutationOwnership(client, checkpointManifest, keys, {
490
1126
  allowDeletedRoutingCheckpoint: true,
@@ -518,18 +1154,18 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
518
1154
  if (resource.state !== "ready") continue;
519
1155
  if (!hasRoutingCheckpoint && resource.kind !== "zone") {
520
1156
  next = updateResourceState(next, resource.key, "retained");
521
- await writeLifecycleCheckpoint(client, next);
1157
+ await saveCheckpoint(next);
522
1158
  continue;
523
1159
  }
524
1160
  if (!shouldDeleteResource(resource, retainData)) {
525
1161
  const removed = resource.kind === "ai-gateway" && resource.ownership === "created" && !hasNamedResource(await client.listAiGateways(next.account.id), resource);
526
1162
  next = updateResourceState(next, resource.key, removed ? "deleted" : "retained");
527
- await writeLifecycleCheckpoint(client, next);
1163
+ await saveCheckpoint(next);
528
1164
  continue;
529
1165
  }
530
1166
  if (resource.kind === "dns-record" && !zone || resource.kind === "queue" && queuesInUse) {
531
1167
  next = updateResourceState(next, resource.key, "retained");
532
- await writeLifecycleCheckpoint(client, next);
1168
+ await saveCheckpoint(next);
533
1169
  continue;
534
1170
  }
535
1171
  if ((await verifyPlatformMutationOwnership(client, next, /* @__PURE__ */ new Set(["routing-kv", resource.key]), {
@@ -537,15 +1173,15 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
537
1173
  databaseUrl: dependencies.databaseUrl
538
1174
  })).has(resource.key)) {
539
1175
  next = updateResourceState(next, resource.key, resource.ownership === "created" ? "deleted" : "retained");
540
- await writeLifecycleCheckpoint(client, next);
1176
+ await saveCheckpoint(next);
541
1177
  continue;
542
1178
  }
543
1179
  await client.deleteResource(next.account.id, resource, zone?.id);
544
1180
  next = updateResourceState(next, resource.key, "deleted");
545
- await writeLifecycleCheckpoint(client, next);
1181
+ await saveCheckpoint(next);
546
1182
  }
547
1183
  const completed = transitionInstallation(next, "uninstalled", (/* @__PURE__ */ new Date()).toISOString());
548
- await writeRemoteManifestIfPresent(client, completed);
1184
+ await saveRemoteManifest(completed);
549
1185
  next = completed;
550
1186
  writeInstallationManifest(completed);
551
1187
  if (!retainData) removeInstallationRecoverySecrets(next);
@@ -557,7 +1193,7 @@ async function uninstallPlatform(client, initialManifest, args, dependencies = {
557
1193
  await stopProjectZeroTrustHold().catch(() => {});
558
1194
  next = transitionInstallation(next, "failed", (/* @__PURE__ */ new Date()).toISOString());
559
1195
  writeInstallationManifest(next);
560
- await writeRemoteManifestIfPresent(client, next).catch(() => {});
1196
+ await saveRemoteManifest(next).catch(() => {});
561
1197
  throw error;
562
1198
  }
563
1199
  }
@@ -593,7 +1229,7 @@ async function recoverDurablyMarkedProvisionIntents(client, manifest) {
593
1229
  "r2-bucket",
594
1230
  manifest.deployment.resources.assetsBucket
595
1231
  ]
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)));
1232
+ ].filter(([key, kind, name]) => !manifest.resources.some((resource) => resource.key === key) && hasProvisionIntentStep(manifest, key, kind, name));
597
1233
  if (pending.length === 0) return manifest;
598
1234
  const [workers, buckets] = await Promise.all([client.listWorkerNames(manifest.account.id), client.listR2Buckets(manifest.account.id)]);
599
1235
  let recovered = manifest;
@@ -604,10 +1240,7 @@ async function recoverDurablyMarkedProvisionIntents(client, manifest) {
604
1240
  } : null : buckets.find((bucket) => bucket.name === name) ?? null;
605
1241
  if (!live) continue;
606
1242
  const resource = {
607
- configHash: createHash("sha256").update(JSON.stringify({
608
- kind,
609
- name
610
- })).digest("hex"),
1243
+ configHash: legacyResourceConfigHash(kind, name),
611
1244
  id: live.id,
612
1245
  key,
613
1246
  kind,
@@ -667,28 +1300,6 @@ function purgeResourcesPlanText(manifest) {
667
1300
  ...permissions.length ? [`API token needs: ${permissions.join(", ")}. Browser login cannot delete DNS records or Access resources.`] : []
668
1301
  ].join("\n");
669
1302
  }
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
1303
  function describeKeptResources(manifest, resources, heading, accessHeading, emailRulesRemoved = false) {
693
1304
  const sorted = [...resources].sort((left, right) => keptResourceRank(manifest, left) - keptResourceRank(manifest, right));
694
1305
  const isTraffic = (resource) => KEPT_RESOURCE_ORDER.indexOf(resource.kind) < KEPT_RESOURCE_ORDER.indexOf("queue");
@@ -753,7 +1364,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
753
1364
  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
1365
  if (targetResources.every((resource) => hasMatchingResourceProof(remote, resource))) break;
755
1366
  }
756
- if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
1367
+ if (attempt < 2) await setTimeout(50 * 2 ** attempt);
757
1368
  }
758
1369
  if (rawRemote === null) {
759
1370
  if ((options.allowMissingRoutingCheckpoint || options.allowUninitializedRoutingCheckpoint) && routing.ownership === "created") {
@@ -800,10 +1411,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
800
1411
  const live = await client.getHyperdriveConfig(accountId, resource.id);
801
1412
  exists = live.id === resource.id && live.name === resource.name;
802
1413
  if (exists) {
803
- const legacyHash = createHash("sha256").update(JSON.stringify({
804
- kind: resource.kind,
805
- name: resource.name
806
- })).digest("hex");
1414
+ const legacyHash = legacyResourceConfigHash(resource.kind, resource.name);
807
1415
  const malformedLegacyHash = !/^[a-f0-9]{64}$/.test(resource.configHash);
808
1416
  if (options.databaseUrl && resource.ownership !== "adopted") {
809
1417
  const expected = hyperdriveOriginFromConnectionString(options.databaseUrl);
@@ -824,10 +1432,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
824
1432
  exists = current !== void 0;
825
1433
  if (current && resource.ownership === "created") {
826
1434
  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");
1435
+ const legacyHash = legacyResourceConfigHash(resource.kind, resource.name);
831
1436
  const expectedHash = dnsRecordConfigHash(resource.name, "AAAA", "100::", true);
832
1437
  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
1438
  }
@@ -839,12 +1444,7 @@ async function verifyPlatformMutationOwnership(client, manifest, targetKeys, opt
839
1444
  const live = r2Buckets.find((entry) => entry.id === resource.id && entry.name === resource.name);
840
1445
  exists = live !== void 0;
841
1446
  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`);
1447
+ if (!matchesRecordedR2Fingerprint(resource.configHash, live)) throw ownershipError(`r2-bucket ${resource.name} changed creation fingerprint`);
848
1448
  verifyResourceMarker(await client.getR2Object(accountId, resource.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), manifest, resource);
849
1449
  }
850
1450
  } else throw ownershipError(`live ownership checks do not support ${resource.kind}`);
@@ -857,10 +1457,7 @@ function hasMatchingResourceProof(manifest, resource) {
857
1457
  return proof?.id === resource.id && proof.kind === resource.kind && proof.name === resource.name && proof.ownership === resource.ownership;
858
1458
  }
859
1459
  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
- }
1460
+ if (resource.ownership === "created") return hasProvisionIntentStep(manifest, resource.key, resource.kind, resource.name);
864
1461
  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
1462
  return false;
866
1463
  }
@@ -877,40 +1474,9 @@ function isPlatformManifestMonotonicExtension(earlier, later) {
877
1474
  function hasNamedResource(entries, resource) {
878
1475
  return entries.some((entry) => entry.id === resource.id && entry.name === resource.name);
879
1476
  }
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
1477
  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`);
1478
+ const problem = resourceMarkerProblem(value, manifest, resource);
1479
+ if (problem) throw ownershipError(`${resource.kind} ${resource.name} ${problem}`);
914
1480
  }
915
1481
  function canResumeAfterRoutingCheckpointDeletion(manifest, targetKeys) {
916
1482
  if (manifest.status !== "failed" && manifest.status !== "uninstalling") return false;
@@ -953,11 +1519,11 @@ function recordLifecycleStep(manifest, key) {
953
1519
  resultHash: "completed"
954
1520
  }, now);
955
1521
  }
956
- async function writeLifecycleCheckpointWithoutDatabase(client, manifest, databaseCheckpoint) {
1522
+ async function writeLifecycleCheckpoint(client, manifest, databaseCheckpoint) {
957
1523
  writeInstallationManifest(manifest);
958
- await writeRemoteManifestIfPresentWithoutDatabase(client, manifest, databaseCheckpoint);
1524
+ await writeRemoteManifest(client, manifest, databaseCheckpoint);
959
1525
  }
960
- async function writeRemoteManifestIfPresentWithoutDatabase(client, manifest, databaseCheckpoint) {
1526
+ async function writeRemoteManifest(client, manifest, databaseCheckpoint) {
961
1527
  await databaseCheckpoint?.(manifest);
962
1528
  const routing = manifest.resources.find((resource) => resource.key === "routing-kv" && resource.state !== "deleted");
963
1529
  if (routing) await client.putKvValue(manifest.account.id, routing.id, REMOTE_INSTALLATION_MANIFEST_KEY, JSON.stringify(manifest));