void 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/README.md +3 -1
  2. package/dist/{account-cmd-l-FM7Ioq.mjs → account-cmd-Ci00cncT.mjs} +5 -5
  3. package/dist/{agents-B_pX_HmQ.mjs → agents-CuNtT0D_.mjs} +5 -2
  4. package/dist/{application-analysis-DKy02UY0.mjs → application-analysis-D72ZQfMZ.mjs} +118 -135
  5. package/dist/{application-routing-DQyUh_5S.mjs → application-routing-Dy589Vgn.mjs} +1 -2
  6. package/dist/atomic-write-Dq6R0BPN.mjs +18 -0
  7. package/dist/{auth-Dn2vw-SG.mjs → auth-B6iOCjKI.mjs} +23 -56
  8. package/dist/{auth-link-vz9Z2kmG.mjs → auth-link-DWr6Afao.mjs} +10 -10
  9. package/dist/{auth-migrations-BwHgeEsD.mjs → auth-migrations-Cgs0k6lS.mjs} +28 -5
  10. package/dist/{auth-router-DvsCUVw4.mjs → auth-router-DP14wVkZ.mjs} +4 -4
  11. package/dist/auth-wn6F8dB8.mjs +2 -0
  12. package/dist/base64-DVrOndmI.mjs +16 -0
  13. package/dist/{better-auth-shared-CPvWD3nG.d.mts → better-auth-shared-B74NJIus.d.mts} +8 -13
  14. package/dist/{better-auth-shared-DPpj8zxc.mjs → better-auth-shared-CMXlZG7W.mjs} +56 -68
  15. package/dist/{build-cmd-D6we0VjO.mjs → build-cmd-Cr3FJxv0.mjs} +5 -5
  16. package/dist/{cache-CZRdzKx1.mjs → cache-D0Tt28cn.mjs} +5 -5
  17. package/dist/{cancel-deploy-CQ3NnMvf.mjs → cancel-deploy-CJKvk7AN.mjs} +4 -4
  18. package/dist/{cf-access-TWNzXltp.mjs → cf-access-BM9G0lhO.mjs} +4 -3
  19. package/dist/{cf-access-BcIBSKqG.mjs → cf-access-CWP4avfP.mjs} +3 -7
  20. package/dist/cf-build-output-2u583yJK.mjs +2 -0
  21. package/dist/{cf-build-output-DjvB_872.mjs → cf-build-output-Bc8i3dzE.mjs} +2 -2
  22. package/dist/cf-user-agent-ChjQp2MU.mjs +33 -0
  23. package/dist/cli/cli.mjs +198 -311
  24. package/dist/cli/cloudflare-operation-process.mjs +41 -15
  25. package/dist/cli/env-schema-probe.d.mts +5 -43
  26. package/dist/cli/env-schema-probe.mjs +14 -80
  27. package/dist/client-C1BjRn3g.mjs +2 -0
  28. package/dist/{client-DlDJuY0s.mjs → client-DM9Kcf4e.mjs} +161 -415
  29. package/dist/{cloudflare-auth-CuxfF6OA.mjs → cloudflare-auth-CqFBDo52.mjs} +12 -39
  30. package/dist/{cloudflare-cmd-lHqNlJGW.mjs → cloudflare-cmd-CTrn-lXJ.mjs} +6 -5
  31. package/dist/{cloudflare-config-BmhBv3HV.mjs → cloudflare-config-DV60XFFK.mjs} +16 -28
  32. package/dist/{cloudflare-connect-2G3rL2rP.mjs → cloudflare-connect-BosmCNlq.mjs} +3 -3
  33. package/dist/{cloudflare-operations-CwI1bChl.mjs → cloudflare-operations-BKdSfNBN.mjs} +26 -34
  34. package/dist/{cloudflare-operations-BlYLlowZ.mjs → cloudflare-operations-Ds-lX3Ts.mjs} +1 -1
  35. package/dist/{cloudflare-process-2-TfIEzB.mjs → cloudflare-process-CeDWJRhj.mjs} +12 -7
  36. package/dist/collect-NUlJMpLY.mjs +2 -0
  37. package/dist/{collect-DiTn9yr6.mjs → collect-xjffdCdk.mjs} +1 -1
  38. package/dist/{config-CSa15FvD.d.mts → config-7qoq2p9v.d.mts} +2 -2
  39. package/dist/{config-CLYAosnQ.mjs → config-BEm9vRvS.mjs} +11 -5
  40. package/dist/{config-DiowPT80.mjs → config-Bx_1SpnO.mjs} +87 -177
  41. package/dist/config-CNnrSMYm.mjs +2 -0
  42. package/dist/{config-DPByhVck.mjs → config-D0GY5ItR.mjs} +1 -1
  43. package/dist/config-entry.d.mts +1 -1
  44. package/dist/{connect-BfPBHHm-.mjs → connect-nukQSoEO.mjs} +8 -8
  45. package/dist/{create-project-BdXEFKal.mjs → create-project-D4APeBwX.mjs} +7 -7
  46. package/dist/{create-project-qFDC45zg.mjs → create-project-DL7YXpjj.mjs} +1 -1
  47. package/dist/{db-Xs80hcaA.mjs → db-27RztQLy.mjs} +173 -263
  48. package/dist/{delete-d2vuou3j.mjs → delete-BbsZFZIw.mjs} +5 -5
  49. package/dist/deploy-B06EQq4t.mjs +6009 -0
  50. package/dist/deploy-Db2ALnQP.mjs +3 -0
  51. package/dist/deploy-managed-static-CGswbcay.mjs +2277 -0
  52. package/dist/deploy-transport-BiicfoQW.mjs +180 -0
  53. package/dist/deploy.d.mts +10 -0
  54. package/dist/deploy.mjs +82 -0
  55. package/dist/dev-inbox-Rd6j_8vM.mjs +217 -0
  56. package/dist/discover-CTUiSsfG.mjs +2 -0
  57. package/dist/{discover-CkCYv9T_.mjs → discover-D2PGT63u.mjs} +2 -2
  58. package/dist/{dist-BVqXeR_h.mjs → dist-ipQi0Q8y.mjs} +2 -3
  59. package/dist/{domain-BsDUBAvt.mjs → domain-Bfq9n03H.mjs} +7 -22
  60. package/dist/{dotenv-BfU6eqFt.mjs → dotenv-CNazY3-w.mjs} +4 -16
  61. package/dist/{email-d_Aux1vM.mjs → email-BcjSyAL0.mjs} +4 -20
  62. package/dist/{email-_lGAHOhp.mjs → email-DIwaQU4P.mjs} +99 -191
  63. package/dist/email-cloudflare-CH053RxK.mjs +1096 -0
  64. package/dist/email-protocol-D9nTvhCp.mjs +82 -0
  65. package/dist/{entry-CMAkqBmU.mjs → entry-ehmBvs5w.mjs} +2 -2
  66. package/dist/{env-BxocywBB.mjs → env-BJjULvL0.mjs} +2 -2
  67. package/dist/{env-DMA3cXZt.mjs → env-BhdWMrNy.mjs} +2 -13
  68. package/dist/{env-C74QepDE.mjs → env-C6ipBLp3.mjs} +8 -8
  69. package/dist/{env-public-DR7ZM6d-.d.mts → env-public-CCdykygU.d.mts} +6 -23
  70. package/dist/{env-raw-jZpP5n47.mjs → env-raw-Cklzcde3.mjs} +3 -20
  71. package/dist/{env-types-CwXgeMgL.mjs → env-types-DS2_y4X8.mjs} +2 -2
  72. package/dist/{env-validation-CrVNjsPm.mjs → env-validation-CTn-HhG1.mjs} +1 -1
  73. package/dist/env-validation-cE0zcmzO.mjs +486 -0
  74. package/dist/{gen-DtkQhvQg.mjs → gen-CnN1Xidx.mjs} +131 -253
  75. package/dist/gen-F8pSZnhc.mjs +2 -0
  76. package/dist/{generate-GDbL7sUr.mjs → generate-Dat0UTry.mjs} +38 -4
  77. package/dist/generate-Rii9klHB.mjs +2 -0
  78. package/dist/generated-wrangler-config-DeWj5F9Y.mjs +18 -0
  79. package/dist/{github-cmd-DaDdBbSS.mjs → github-cmd-BdkU4Nhe.mjs} +27 -71
  80. package/dist/{handler-BHAn2fZJ.mjs → handler-BZ1t7-Rv.mjs} +2 -2
  81. package/dist/{handler-CNAwjRcW.d.mts → handler-_PqY82xe.d.mts} +1 -1
  82. package/dist/{headers-DlfR78Nu.mjs → headers-Di1FsD3e.mjs} +26 -81
  83. package/dist/{help-0bTe_lID.mjs → help-Bug7pyK_.mjs} +18 -10
  84. package/dist/help-s9q7LTcu.mjs +2 -0
  85. package/dist/{inbound-PfQDLXc5.mjs → inbound-DmkTQeWp.mjs} +71 -222
  86. package/dist/{inbound-CH5Mksyy.d.mts → inbound-brj2-TM5.d.mts} +19 -58
  87. package/dist/index.d.mts +2 -18
  88. package/dist/index.mjs +200 -496
  89. package/dist/{info-PYWwkWyt.mjs → info-CujPwtMe.mjs} +5 -5
  90. package/dist/{init-DaBGmaXC.mjs → init-ZWOJvc-Z.mjs} +179 -388
  91. package/dist/isr-config-DPjXM8J2.mjs +194 -0
  92. package/dist/{link-C6fjafep.mjs → link-UEc7bnF4.mjs} +8 -8
  93. package/dist/{list-BTFBlqKl.mjs → list-CuWeOnzk.mjs} +6 -17
  94. package/dist/{live-CgQuTpqD.mjs → live-BxLX-JqJ.mjs} +19 -17
  95. package/dist/{live-Du8jXwOW.d.mts → live-DJONVlQK.d.mts} +1 -1
  96. package/dist/load-user-env-DD5qveNU.mjs +49 -0
  97. package/dist/{local-d1-ga5O_idV.mjs → local-d1-BM3p74mm.mjs} +6 -6
  98. package/dist/{login-GKcNUmtI.mjs → login-CZ2Cqa-D.mjs} +11 -11
  99. package/dist/login-DhK7o1oJ.mjs +2 -0
  100. package/dist/{logs-BiMSULmc.mjs → logs-TihRcLTk.mjs} +5 -5
  101. package/dist/migrate-BgT-hmu8.mjs +2 -0
  102. package/dist/{migrate-NLsjBbuM.mjs → migrate-uFwojusJ.mjs} +69 -21
  103. package/dist/migration-handler-Cg0Y4EPu.mjs +470 -0
  104. package/dist/{migration-handler-DM4clYj5.d.mts → migration-handler-CwlEQUoo.d.mts} +17 -26
  105. package/dist/{mime-DxCznlJq.mjs → mime-srOklVVv.mjs} +92 -395
  106. package/dist/{node-DJicPvPj.mjs → node-DoKbo1SZ.mjs} +8 -24
  107. package/dist/{operator-args-DOBhIaIg.mjs → operator-args-UnE8Y2eK.mjs} +27 -26
  108. package/dist/{operator-auth-BEZ_u0Cz.mjs → operator-auth-Cr2zCc-J.mjs} +2 -2
  109. package/dist/{operator-client-DbDv4vtE.mjs → operator-client-C-ROjk1I.mjs} +4 -4
  110. package/dist/{operator-cmd-BT4hgavL.mjs → operator-cmd-C5lCctnc.mjs} +8 -8
  111. package/dist/operator-cmd-Cjk5DiRx.mjs +2 -0
  112. package/dist/{output-C50ag2r3.mjs → output-FNHKw5Ac.mjs} +36 -5
  113. package/dist/{package-json-DXfiT1jV.mjs → package-json-BjYqR-So.mjs} +1 -1
  114. package/dist/pages/client.d.mts +1 -1
  115. package/dist/pages/client.mjs +91 -5
  116. package/dist/pages/index.d.mts +1 -2
  117. package/dist/pages/index.mjs +6 -6
  118. package/dist/pages/islands-plugin.mjs +5 -83
  119. package/dist/pages/protocol.d.mts +2 -2
  120. package/dist/pages/protocol.mjs +1 -1
  121. package/dist/pages/serialize.mjs +1 -1
  122. package/dist/{platform-args-DUeXY9Om.mjs → platform-args-C6Mb_I3a.mjs} +34 -58
  123. package/dist/platform-args-Cm7x-fot.mjs +2 -0
  124. package/dist/{platform-auth-config-CkSx3usH.mjs → platform-auth-config-CJmNkFT2.mjs} +10 -11
  125. package/dist/{platform-auth-protection-BJimB_p9.mjs → platform-auth-protection-D5wvsI-Y.mjs} +36 -26
  126. package/dist/{platform-auth-recovery-BU6izFAU.mjs → platform-auth-recovery-D_0I3n-1.mjs} +8 -9
  127. package/dist/{platform-cmd-BbmJiq5O.mjs → platform-cmd-Cuj4oUVo.mjs} +1 -1
  128. package/dist/{platform-cmd-BbY1dWiM.mjs → platform-cmd-DxxZO6e5.mjs} +15 -15
  129. package/dist/{platform-discovery-DIYz8V63.mjs → platform-discovery-CHhNs8Nn.mjs} +1 -1
  130. package/dist/{platform-domain-CHVS6cV0.mjs → platform-domain-zLXnW1G_.mjs} +5 -5
  131. package/dist/{platform-lifecycle-DzCwqhFq.mjs → platform-lifecycle-DOW-wLfx.mjs} +1398 -2043
  132. package/dist/{platform-lifecycle-ByAlpkhV.mjs → platform-lifecycle-_gfnjUBr.mjs} +1 -1
  133. package/dist/{platform-management-CBOT4BSM.mjs → platform-management-CYrLvTGH.mjs} +1 -1
  134. package/dist/{platform-management-CuuVDhyt.mjs → platform-management-t7HKy4-U.mjs} +632 -124
  135. package/dist/{platform-plans-config-CN-heS4q.mjs → platform-plans-config-CNH6zbPX.mjs} +15 -11
  136. package/dist/{platform-recovery-CskSJgLh.mjs → platform-recovery-DeucI7A9.mjs} +6 -9
  137. package/dist/{platform-registry-Cjv2Qf0r.mjs → platform-registry-B9mWX9LO.mjs} +13 -18
  138. package/dist/{platform-setup-BBxRxmFC.mjs → platform-setup-CTf6MMT9.mjs} +19 -26
  139. package/dist/{plugin-inference-BlQbOL0P.mjs → plugin-inference-DsARdQKE.mjs} +18 -33
  140. package/dist/{prepare-Dq9hTIbo.mjs → prepare-C4zfwks4.mjs} +3 -3
  141. package/dist/{prepare-Do96654b.mjs → prepare-CSH9dKTI.mjs} +11 -13
  142. package/dist/prepare-dr9uxQSV.mjs +2 -0
  143. package/dist/{prerender-render-Ct20jftc.mjs → prerender-render-Bfsumdkv.mjs} +8 -2
  144. package/dist/prerender-render.d.mts +2 -0
  145. package/dist/prerender-render.mjs +2 -2
  146. package/dist/project-DiiOBZPF.mjs +2 -0
  147. package/dist/{project-cmd-DaoRrn-8.mjs → project-cmd-Dq75AXzy.mjs} +19 -19
  148. package/dist/{project-paths-GgcMozYz.mjs → project-paths-DMvEeSau.mjs} +9 -14
  149. package/dist/{project-BAm2aB6Q.mjs → project-rqDnVsJN.mjs} +8 -4
  150. package/dist/{project-team-Q_lCA7Io.mjs → project-team-B3_QcStG.mjs} +6 -6
  151. package/dist/{project-token-BArgK9o1.mjs → project-token-CvsRvv2I.mjs} +5 -5
  152. package/dist/{project-tsconfig-CVJhNMxs.mjs → project-tsconfig-bveqhlYA.mjs} +3 -5
  153. package/dist/{project-zero-trust-KO9Hu2Pq.mjs → project-zero-trust-D1ICiwN_.mjs} +5 -5
  154. package/dist/{protocol-BPZaNqBo.d.mts → protocol-4H7V_3Os.d.mts} +1 -1
  155. package/dist/{protocol-CfaUwNs8.mjs → protocol-RNP0u7UY.mjs} +9 -3
  156. package/dist/{provision-BfaLiWbj.mjs → provision-DDh7ITVS.mjs} +118 -865
  157. package/dist/{requests-C7Oeoh5u.mjs → requests-3CL3BuZh.mjs} +4 -4
  158. package/dist/resolve-project-B9Hr0D3n.mjs +2 -0
  159. package/dist/resolve-project-C_Cu7E8a.mjs +48 -0
  160. package/dist/{rollback-DVAtrLAl.mjs → rollback-Ce1d_0J9.mjs} +7 -17
  161. package/dist/{route-types-CbtsIeFn.mjs → route-types-IrBuwJ7u.mjs} +35 -70
  162. package/dist/routing-assets-VkrKIbxE.mjs +139 -0
  163. package/dist/{runner-9wjsSZx-.mjs → runner-B5huK2eC.mjs} +1 -1
  164. package/dist/{runner-DMBKSrRk.mjs → runner-DcVDrzuR.mjs} +7 -28
  165. package/dist/{runner-mysql-CEzKtmFb.mjs → runner-mysql-CCtyI0p6.mjs} +4 -3
  166. package/dist/runner-mysql-CaJq-hd6.mjs +2 -0
  167. package/dist/runner-pg-BJhFZZG4.mjs +2 -0
  168. package/dist/{runner-pg-DV8p_zjA.mjs → runner-pg-CPd-CB_F.mjs} +1 -1
  169. package/dist/runtime/ai.mjs +9 -22
  170. package/dist/runtime/better-auth-mysql.d.mts +2 -5
  171. package/dist/runtime/better-auth-mysql.mjs +6 -31
  172. package/dist/runtime/better-auth-pg.d.mts +2 -5
  173. package/dist/runtime/better-auth-pg.mjs +6 -32
  174. package/dist/runtime/better-auth.d.mts +2 -4
  175. package/dist/runtime/better-auth.mjs +9 -19
  176. package/dist/runtime/db.mjs +1 -1
  177. package/dist/runtime/durable.d.mts +16 -0
  178. package/dist/runtime/durable.mjs +26 -2
  179. package/dist/runtime/email/testing.d.mts +1 -1
  180. package/dist/runtime/email/testing.mjs +9 -23
  181. package/dist/runtime/email-protocol.d.mts +12 -0
  182. package/dist/runtime/email-protocol.mjs +2 -70
  183. package/dist/runtime/email.d.mts +1 -1
  184. package/dist/runtime/email.mjs +79 -221
  185. package/dist/runtime/env-public.d.mts +1 -1
  186. package/dist/runtime/env-public.mjs +12 -38
  187. package/dist/runtime/env.d.mts +1 -12
  188. package/dist/runtime/env.mjs +1 -1
  189. package/dist/runtime/handler.d.mts +1 -1
  190. package/dist/runtime/handler.mjs +1 -1
  191. package/dist/runtime/isr.mjs +1 -1
  192. package/dist/runtime/kv.mjs +1 -1
  193. package/dist/runtime/live-client.d.mts +1 -1
  194. package/dist/runtime/live-server.mjs +1 -16
  195. package/dist/runtime/live.d.mts +1 -1
  196. package/dist/runtime/live.mjs +1 -1
  197. package/dist/runtime/migration-handler-mysql.d.mts +2 -2
  198. package/dist/runtime/migration-handler-mysql.mjs +3 -2
  199. package/dist/runtime/migration-handler-pg.d.mts +2 -2
  200. package/dist/runtime/migration-handler-pg.mjs +1 -1
  201. package/dist/runtime/migration-handler.d.mts +2 -2
  202. package/dist/runtime/migration-handler.mjs +2 -470
  203. package/dist/runtime/queues.mjs +1 -1
  204. package/dist/runtime/remote/binding-handler.mjs +62 -62
  205. package/dist/runtime/remote/index.mjs +16 -77
  206. package/dist/runtime/routing.d.mts +1 -4
  207. package/dist/runtime/routing.mjs +82 -218
  208. package/dist/runtime/sandbox-container.d.mts +2 -2
  209. package/dist/runtime/sandbox-container.mjs +2 -2
  210. package/dist/runtime/sandbox.d.mts +2 -2
  211. package/dist/runtime/sandbox.mjs +13 -6
  212. package/dist/runtime/storage.mjs +1 -1
  213. package/dist/runtime/validator.d.mts +1 -1
  214. package/dist/runtime/ws-server.d.mts +2 -3
  215. package/dist/runtime/ws-server.mjs +6 -28
  216. package/dist/runtime/ws.d.mts +1 -1
  217. package/dist/{sandbox-container-Bo0eiFKz.d.mts → sandbox-container-ChQRum-j.d.mts} +3 -1
  218. package/dist/{sandbox-container-C6ItmVuN.mjs → sandbox-container-DBKIqDEK.mjs} +12 -1
  219. package/dist/{sandbox-C5LFoJMf.d.mts → sandbox-q3waMc2T.d.mts} +1 -1
  220. package/dist/{scan-C3uDzz6J.mjs → scan-C0BMdmEd.mjs} +84 -7
  221. package/dist/{secret-BvtB7IjY.mjs → secret-B5J8bBsI.mjs} +62 -80
  222. package/dist/{serialize-BPvnNQuA.mjs → serialize-BtQyaYoC.mjs} +10 -4
  223. package/dist/{skills-BgVMvQdy.mjs → skills-BJsan98C.mjs} +2 -2
  224. package/dist/{subcommand-prompt-BSlnD1fI.mjs → subcommand-prompt-gQ-NDA-u.mjs} +38 -43
  225. package/dist/sveltekit.mjs +1 -1
  226. package/dist/validate-C9xtFOYR.mjs +2 -0
  227. package/dist/{validate-C9qJArv7.mjs → validate-CheZGRE7.mjs} +4 -8
  228. package/dist/version-metadata-DIrqEHQo.mjs +73 -0
  229. package/dist/{wrangler-D9KTuqnv.mjs → wrangler-BYs07Rxy.mjs} +119 -378
  230. package/dist/{ws-CChrrAU-.d.mts → ws-aKVYB-fA.d.mts} +1 -1
  231. package/dist/yarn-pnp-DLbEFMT0.mjs +92 -0
  232. package/docs/guide/auth.md +2 -0
  233. package/docs/guide/database/postgresql.md +2 -0
  234. package/docs/guide/database.md +2 -0
  235. package/docs/guide/durable-state.md +24 -0
  236. package/docs/guide/platform/administration/access.md +4 -0
  237. package/docs/guide/platform/administration/connected-apps.md +55 -0
  238. package/docs/guide/platform/installation/credentials.md +3 -1
  239. package/docs/guide/quickstart.md +2 -0
  240. package/docs/guide/sandboxes.md +2 -0
  241. package/docs/reference/api/deploy.md +23 -0
  242. package/docs/reference/api/pages.md +2 -0
  243. package/docs/reference/api.md +2 -1
  244. package/docs/reference/cli/database.md +9 -3
  245. package/docs/reference/cli/platform-installation.md +1 -1
  246. package/docs/reference/cli/setup.md +2 -0
  247. package/package.json +13 -16
  248. package/skills/void/SKILL.md +1 -0
  249. package/dist/auth-LMJcNESe.mjs +0 -2
  250. package/dist/cf-build-output-H9a97vME.mjs +0 -2
  251. package/dist/cf-user-agent-lKblH3zU.mjs +0 -17
  252. package/dist/client-tso2OfcG.mjs +0 -2
  253. package/dist/collect-DLyFDiKW.mjs +0 -2
  254. package/dist/config-5r-YpMC-.mjs +0 -2
  255. package/dist/config-B_ggY-IO.mjs +0 -89
  256. package/dist/config-BtSFvXZx.mjs +0 -2
  257. package/dist/config-write-AHGesphx.mjs +0 -61
  258. package/dist/deploy-Bqe1PYBw.mjs +0 -10458
  259. package/dist/deploy-VsWsVQkw.mjs +0 -2
  260. package/dist/dev-inbox-CK-UuPob.mjs +0 -297
  261. package/dist/discover-EDUf6JJw.mjs +0 -2
  262. package/dist/dist-_UUlL6bg.mjs +0 -2
  263. package/dist/env-validation-Dxdx_5wo.mjs +0 -951
  264. package/dist/gen-dA9hW7AV.mjs +0 -2
  265. package/dist/head-client-DzmJGN4C.mjs +0 -90
  266. package/dist/help-DfQYVfGY.mjs +0 -2
  267. package/dist/login-mx_ydhmp.mjs +0 -2
  268. package/dist/migrate-BHp07yJE.mjs +0 -2
  269. package/dist/operator-cmd-CwBB-1yb.mjs +0 -2
  270. package/dist/pages/head-client.d.mts +0 -13
  271. package/dist/pages/head-client.mjs +0 -2
  272. package/dist/platform-args-D4E1PjVw.mjs +0 -2
  273. package/dist/prepare-DRUV-g3X.mjs +0 -2
  274. package/dist/project-DedqCp59.mjs +0 -2
  275. package/dist/provision-Wu9ZBk9L.mjs +0 -2
  276. package/dist/resolve-project-BIeYs9Cw.mjs +0 -2
  277. package/dist/resolve-project-CP8rZs_p.mjs +0 -28
  278. package/dist/runner-mysql-CkFqZ6CE.mjs +0 -2
  279. package/dist/runner-pg-BGYRFdRp.mjs +0 -2
  280. package/dist/runtime/env-helpers.d.mts +0 -2
  281. package/dist/runtime/env-helpers.mjs +0 -2
  282. package/dist/validate-BJr9LfXy.mjs +0 -2
  283. package/dist/yarn-pnp-CUX_iTPA.mjs +0 -188
@@ -1,22 +1,23 @@
1
- import { a as join, n as dirname, u as resolve } from "./dist-BVqXeR_h.mjs";
2
- import { S as writePendingInstallationSecrets, a as latestPlatformWorkerCutoverGeneration, c as normalizePlatformApiUrl, d as platformRegistryDirectory, f as platformWorkerSecretUpdateIntentState, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, n as assertPlatformInstallationSlugAvailable, o as listInstallationManifests, p as promoteInstallationSecrets, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest, y as savePlatformConnection } from "./platform-registry-Cjv2Qf0r.mjs";
3
- import { t as cfAccessHeaders } from "./cf-access-TWNzXltp.mjs";
4
- import { n as ensurePlatformAccessToken } from "./cf-access-BcIBSKqG.mjs";
1
+ import { a as join, c as resolve, n as dirname } from "./dist-ipQi0Q8y.mjs";
2
+ import { t as writeFileAtomic } from "./atomic-write-Dq6R0BPN.mjs";
3
+ import { S as writePendingInstallationSecrets, a as latestPlatformWorkerCutoverGeneration, c as normalizePlatformApiUrl, d as platformRegistryDirectory, f as platformWorkerSecretUpdateIntentState, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, n as assertPlatformInstallationSlugAvailable, o as listInstallationManifests, p as promoteInstallationSecrets, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest, y as savePlatformConnection } from "./platform-registry-B9mWX9LO.mjs";
4
+ import { t as cfAccessHeaders } from "./cf-access-BM9G0lhO.mjs";
5
+ import { t as ensurePlatformAccessToken } from "./cf-access-CWP4avfP.mjs";
5
6
  import { A as confirm, H as outro, K as select, L as log, U as password, X as text$1, tt as isCancel, z as multiselect } from "./dist-D3m88sjf.mjs";
6
- import { d as printSuccess, l as import_picocolors, n as cliCommandName, r as createSpinner, u as note } from "./output-C50ag2r3.mjs";
7
- import { l as resolveCloudflareOperationArgv } from "./cloudflare-process-2-TfIEzB.mjs";
8
- import { o as runCloudflareAuthCommand } from "./cloudflare-auth-CuxfF6OA.mjs";
9
- import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-BfaLiWbj.mjs";
10
- import { t as parsePlatformArgs } from "./platform-args-DUeXY9Om.mjs";
11
- import { a as platformManagementSetupPage, c as platformSetupLink, i as platformInstallCommandArguments, l as platformSetupPages, n as formatPlatformSetupPlan, o as platformPromptAnswer, r as openPlatformSetupPage, s as platformSetupFieldHelp, t as formatPlatformInstallCommand } from "./platform-setup-BBxRxmFC.mjs";
12
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
+ import { f as import_picocolors, l as formatUrl, m as printSuccess, n as cliCommandName, p as note, r as createSpinner, u as getErrorMessage } from "./output-FNHKw5Ac.mjs";
8
+ import { l as resolveCloudflareOperationArgv } from "./cloudflare-process-CeDWJRhj.mjs";
9
+ import { d as resolvePinnedApiBaseUrl, i as createDefaultRunWrangler } from "./provision-DDh7ITVS.mjs";
10
+ import { t as parsePlatformArgs } from "./platform-args-C6Mb_I3a.mjs";
11
+ import { o as runCloudflareAuthCommand } from "./cloudflare-auth-CqFBDo52.mjs";
12
+ import { a as platformManagementSetupPage, c as platformSetupLink, i as platformInstallCommandArguments, l as platformSetupPages, n as formatPlatformSetupPlan, o as platformPromptAnswer, r as openPlatformSetupPage, s as platformSetupFieldHelp, t as formatPlatformInstallCommand } from "./platform-setup-CTf6MMT9.mjs";
13
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
13
14
  import { fileURLToPath } from "node:url";
14
15
  import { isRecord } from "@void/deploy-core";
15
- import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY as REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAppAccessSetup, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
16
+ import { PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_AUTH_SETUP_PROOF_PREFIX, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_HEALTH_HOSTNAME_LABEL, PLATFORM_LOGIN_KINDS, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY as REMOTE_INSTALLATION_MANIFEST_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, normalizePlatformDashboardUrl, parsePlatformAccessProtection, parsePlatformAdmissionPolicy, parsePlatformAppAccessSetup, parsePlatformAuthenticationSetup, parsePlatformInstallationManifest, parsePlatformLoginConfiguration, parsePlatformResourceOwnershipMarker, platformDashboardUrl, recordInstallationResource, recordInstallationStep, setInstallationEndpoints, transitionInstallation } from "@void/platform";
16
17
  import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
17
18
  import { tmpdir } from "node:os";
18
19
  import { stripVTControlCharacters } from "node:util";
19
- import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
20
+ import { cloudflareCommandDiagnostic, encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
20
21
  import { createServer, request } from "node:http";
21
22
  import { readFile } from "node:fs/promises";
22
23
  import { PLATFORM_ADMIN_ASSETS_DIRECTORY, platformDatabaseSchemaDigest, resolvePlatformRuntime } from "@void/platform/installer";
@@ -201,7 +202,19 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
201
202
  return null;
202
203
  }
203
204
  async listWorkerDomains(accountId) {
204
- return (await this.#listWorkerDomainRecords(accountId)).map((entry) => ({
205
+ const result = [];
206
+ const perPage = 100;
207
+ for (let page = 1;; page++) {
208
+ const query = new URLSearchParams({
209
+ page: String(page),
210
+ per_page: String(perPage)
211
+ });
212
+ const response = await this.#requestEnvelope(`/accounts/${encodeURIComponent(accountId)}/workers/domains?${query}`);
213
+ result.push(...response.result);
214
+ const totalPages = response.resultInfo?.total_pages;
215
+ if (typeof totalPages === "number" && page >= totalPages || totalPages === void 0 && response.result.length < perPage) break;
216
+ }
217
+ return result.map((entry) => ({
205
218
  hostname: requiredString(entry.hostname, "Worker custom domain hostname"),
206
219
  id: requiredString(entry.id, "Worker custom domain id"),
207
220
  service: requiredString(entry.service, "Worker custom domain service"),
@@ -241,26 +254,8 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
241
254
  } catch (error) {
242
255
  throw new PlatformWorkerCustomDomainsChangedError(`platform: Custom Domain publication for Worker ${JSON.stringify(workerName)} had an ambiguous outcome; retaining live domains for safe reconciliation.`, { cause: error });
243
256
  }
244
- const expected = [...origins].map((origin) => origin.hostname).sort();
245
257
  const inventory = await this.listWorkerDomains(accountId);
246
- const actual = inventory.filter((domain) => domain.service === workerName).map((domain) => domain.hostname).sort();
247
- if (!expected.every((hostname) => inventory.filter((domain) => domain.hostname === hostname && domain.service === workerName).length === 1) || expected.some((hostname) => !actual.includes(hostname))) throw new PlatformWorkerCustomDomainsChangedError(`platform: Worker ${JSON.stringify(workerName)} Custom Domains changed concurrently after publication; refusing to overwrite the newer traffic state.`);
248
- }
249
- async #listWorkerDomainRecords(accountId, service) {
250
- const entries = [];
251
- const perPage = 100;
252
- for (let page = 1;; page++) {
253
- const query = new URLSearchParams({
254
- page: String(page),
255
- per_page: String(perPage)
256
- });
257
- if (service) query.set("service", service);
258
- const response = await this.#requestEnvelope(`/accounts/${encodeURIComponent(accountId)}/workers/domains?${query}`);
259
- entries.push(...response.result);
260
- const totalPages = response.resultInfo?.total_pages;
261
- if (typeof totalPages === "number" && page >= totalPages || totalPages === void 0 && response.result.length < perPage) break;
262
- }
263
- return entries;
258
+ if (!origins.every(({ hostname }) => inventory.filter((domain) => domain.hostname === hostname && domain.service === workerName).length === 1)) throw new PlatformWorkerCustomDomainsChangedError(`platform: Worker ${JSON.stringify(workerName)} Custom Domains changed concurrently after publication; refusing to overwrite the newer traffic state.`);
264
259
  }
265
260
  async listWorkerRoutes(zoneId) {
266
261
  return (await this.request(`/zones/${encodeURIComponent(zoneId)}/workers/routes`)).map((entry) => ({
@@ -586,9 +581,8 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
586
581
  async deleteResource(accountId, resource, zoneId) {
587
582
  const { kind } = resource;
588
583
  if (kind === "ai-gateway" && this.#aiGatewayClient) return this.#aiGatewayClient.deleteResource(accountId, resource, zoneId);
589
- const account = encodeURIComponent(accountId);
590
584
  const encoded = encodeURIComponent(kind === "dispatch-namespace" || kind === "worker" ? resource.name : resource.id);
591
- const path = kind === "kv-namespace" ? `/accounts/${account}/storage/kv/namespaces/${encoded}` : kind === "dns-record" ? zoneId ? `/zones/${encodeURIComponent(zoneId)}/dns_records/${encoded}` : null : kind === "ai-gateway" ? `/accounts/${account}/ai-gateway/gateways/${encoded}` : kind === "r2-bucket" ? `/accounts/${account}/r2/buckets/${encoded}` : kind === "queue" ? `/accounts/${account}/queues/${encoded}` : kind === "dispatch-namespace" ? `/accounts/${account}/workers/dispatch/namespaces/${encoded}` : kind === "hyperdrive" ? `/accounts/${account}/hyperdrive/configs/${encoded}` : kind === "worker" ? `/accounts/${account}/workers/scripts/${encoded}` : kind === "zone" ? `/zones/${encoded}` : null;
585
+ const path = deletionPath(kind, encodeURIComponent(accountId), encoded, zoneId);
592
586
  if (!path) throw new Error(`platform: Unsupported Cloudflare deletion kind ${kind}.`);
593
587
  await this.#deleteIfPresent(path);
594
588
  }
@@ -718,9 +712,6 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
718
712
  })
719
713
  });
720
714
  }
721
- async removeRoutes(_accountId, _zoneId, tracked) {
722
- if (tracked.routes.length > 0 || tracked.customDomains.length > 0) throw new Error("platform: Cloudflare does not support conditional Worker traffic deletion. Retaining routes and custom domains for explicit dashboard cleanup.");
723
- }
724
715
  };
725
716
  function accessResourcePath(accountId, kind, id) {
726
717
  return `/accounts/${encodeURIComponent(accountId)}/access/${kind === "application" ? "apps" : "service_tokens"}/${encodeURIComponent(id)}`;
@@ -734,6 +725,20 @@ function parseEmailRoutingTarget(rule) {
734
725
  workerName: action?.type === "worker" && value.length === 1 && typeof value[0] === "string" ? value[0] : null
735
726
  };
736
727
  }
728
+ function deletionPath(kind, account, encoded, zoneId) {
729
+ switch (kind) {
730
+ case "kv-namespace": return `/accounts/${account}/storage/kv/namespaces/${encoded}`;
731
+ case "dns-record": return zoneId ? `/zones/${encodeURIComponent(zoneId)}/dns_records/${encoded}` : null;
732
+ case "ai-gateway": return `/accounts/${account}/ai-gateway/gateways/${encoded}`;
733
+ case "r2-bucket": return `/accounts/${account}/r2/buckets/${encoded}`;
734
+ case "queue": return `/accounts/${account}/queues/${encoded}`;
735
+ case "dispatch-namespace": return `/accounts/${account}/workers/dispatch/namespaces/${encoded}`;
736
+ case "hyperdrive": return `/accounts/${account}/hyperdrive/configs/${encoded}`;
737
+ case "worker": return `/accounts/${account}/workers/scripts/${encoded}`;
738
+ case "zone": return `/zones/${encoded}`;
739
+ default: return null;
740
+ }
741
+ }
737
742
  function cloudflareErrorDetails(envelope, status) {
738
743
  return envelope.errors?.filter((error) => error.message).map((error) => error.code === void 0 ? error.message : `${error.message} (code ${error.code})`).join(", ") || `HTTP ${status}`;
739
744
  }
@@ -872,6 +877,9 @@ function sameSet(left, right) {
872
877
  function fingerprint(value) {
873
878
  return createHash("sha256").update(JSON.stringify(value)).digest("hex");
874
879
  }
880
+ function platformAccessNonce(ownershipNonce) {
881
+ return createHash("sha256").update(`void-access:${ownershipNonce}`).digest("hex").slice(0, 32);
882
+ }
875
883
  var PlatformAccessClient = class {
876
884
  client;
877
885
  accountId;
@@ -1015,8 +1023,6 @@ async function inspectPlatformAccess(client, input) {
1015
1023
  };
1016
1024
  }
1017
1025
  async function provisionPlatformAccess(client, input, previous, save) {
1018
- if (!input.login && !input.protection) return { resources: [] };
1019
- if (client.accountId !== input.accountId) throw new Error("platform: Access account selection does not match its management client.");
1020
1026
  const inspected = await inspectPlatformAccess(client, input);
1021
1027
  const origins = [...new Set([
1022
1028
  input.apiUrl,
@@ -1027,13 +1033,7 @@ async function provisionPlatformAccess(client, input, previous, save) {
1027
1033
  if (url.origin !== value || url.protocol !== "https:") throw new Error("platform: Access requires exact HTTPS origins without credentials, paths, or query strings.");
1028
1034
  return url.origin;
1029
1035
  }))];
1030
- const state = previous ? structuredClone(previous) : {
1031
- version: 1,
1032
- accountId: input.accountId,
1033
- installationId: input.installationId,
1034
- nonce: randomBytes(16).toString("hex"),
1035
- resources: {}
1036
- };
1036
+ const state = structuredClone(previous);
1037
1037
  if (state.version !== 1 || state.accountId !== input.accountId || state.installationId !== input.installationId || !/^[a-f0-9]{32}$/.test(state.nonce)) throw new Error("platform: Access recovery data belongs to a different installation.");
1038
1038
  save(state);
1039
1039
  const policies = input.policyIds.map((id, index) => ({
@@ -1569,7 +1569,7 @@ async function preparePlatformAccessSetup(input) {
1569
1569
  version: 1,
1570
1570
  accountId,
1571
1571
  installationId: input.plan.installationId,
1572
- nonce: createHash("sha256").update(`void-access:${input.ownershipNonce}`).digest("hex").slice(0, 32),
1572
+ nonce: platformAccessNonce(input.ownershipNonce),
1573
1573
  resources: {}
1574
1574
  };
1575
1575
  let loginClientSecret = spec.loginClientSecretEnv ? secretReference(spec.loginClientSecretEnv) : input.checkpoint.accessExistingLoginSecret;
@@ -1774,11 +1774,6 @@ async function selectInstallationAppAccess(authentication, recorded, choice) {
1774
1774
  })));
1775
1775
  if (selected === "public" && (config?.identityProviderIds?.length || config?.policyIds?.length)) throw new Error("platform: Public apps cannot select identity policies. Choose custom app access to require company login.");
1776
1776
  if (selected === "platform" && !protectedPlatform) throw new Error("platform: Reusing the platform Access policy requires --access-protection. Choose --app-access custom to protect apps separately.");
1777
- if (![
1778
- "public",
1779
- "platform",
1780
- "custom"
1781
- ].includes(String(selected))) throw new Error("platform: Choose who can access deployed apps.");
1782
1777
  return selected;
1783
1778
  }
1784
1779
  /** Collect the hosting-account policy once, with explicit consent for a durable management token. */
@@ -1800,7 +1795,6 @@ async function prepareInstallationAppAccess(input) {
1800
1795
  }
1801
1796
  }
1802
1797
  const file = input.authentication.file?.appAccess;
1803
- if (file?.tokenEnv && !/^[A-Z_][A-Z0-9_]*$/.test(file.tokenEnv)) throw new Error("platform: Invalid app access token environment reference.");
1804
1798
  let token = explicitToken || (input.checkpoint.appAccessTokenRejected ? void 0 : input.checkpoint.appAccessToken);
1805
1799
  if (input.checkpoint.appAccessTokenRejected) log.info("The previous app access token was rejected. Provide a replacement to continue.");
1806
1800
  const setupSpec = input.checkpoint.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(input.checkpoint.accessSetupSpecJson)) : input.authentication.file?.cloudflareAccess;
@@ -2070,207 +2064,161 @@ async function verifyPlatformAuthenticationDatabase(client, accountId, resource,
2070
2064
  if ((await read()).cachingDisabled !== true) throw new Error("platform: Could not verify uncached Hyperdrive reads. Authentication setup remains closed.");
2071
2065
  }
2072
2066
  //#endregion
2073
- //#region src/cli/platform-installer-health.ts
2074
- var PlatformAccessBlockedError = class extends Error {
2075
- constructor(url, context) {
2076
- super(context?.accessProtected ? [
2077
- "platform: Cloudflare Access blocked a protected platform health check (Default-Deny, error 1050).",
2078
- `Blocked URL: ${url}`,
2079
- "Keep employee Access protection and account-wide Default-Deny enabled.",
2080
- "The reserved dispatch /health endpoint uses a path-only Access exception and still requires Void’s private probe token.",
2081
- "Resume to verify the health exceptions and saved installer credentials.",
2082
- ...context.resumeCommand ? [`Resume: ${context.resumeCommand}`] : [],
2083
- "https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
2084
- ].join("\n") : [
2085
- "platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
2086
- `Blocked URL: ${url}`,
2087
- "",
2088
- "To make this test platform publicly reachable:",
2089
- "",
2090
- `1. In ${context ? `account ${JSON.stringify(context.accountName)}` : "your Cloudflare account"}, open:`,
2091
- " Zero Trust → Access controls → Applications → Create new application.",
2092
- "2. Choose Self-hosted. When adding destinations, choose Worker and select:",
2093
- ` ${context ? context.workers.join(", ") + "." : "This installation's API, proxy, and dispatch Workers."}`,
2094
- "3. Add a policy: Action → Bypass; Include → Everyone.",
2095
- context?.resumeCommand ? `4. Save the policy and application, then resume:\n\n ${context.resumeCommand}` : "4. Save the policy and application, then rerun your platform command.",
2096
- "",
2097
- "This removes Cloudflare Access protection for these Workers.",
2098
- "Void’s own authentication stays enabled. Keep account-wide Default-Deny on.",
2099
- "",
2100
- "For platforms that must remain Access-protected:",
2101
- "https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
2102
- ].join("\n"));
2103
- }
2104
- };
2105
- async function platformProbeFailure(label, url, response, context) {
2106
- const message = `platform: ${label} failed at ${url} (HTTP ${response.status}).`;
2107
- if (response.status === 403 && response.headers.get("server")?.toLowerCase() === "cloudflare" && response.headers.get("content-type")?.toLowerCase().includes("text/html")) {
2108
- const body = await response.text().catch(() => "");
2109
- if (body.includes("Default-Deny") && /errorCode:\s*1050\b/.test(body)) return new PlatformAccessBlockedError(url, context);
2110
- }
2111
- return new Error(message);
2067
+ //#region src/cli/platform-installer-process.ts
2068
+ function platformWorkerCommandRunner(accountId, cwd, runner) {
2069
+ return (operation, options) => runner(operation, accountId, cwd, options.capture);
2112
2070
  }
2113
- async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicationBaseDomain, platformVersion, usageProtocolVersion, healthProbeToken, fetcher = fetch, dispatchOrigin, context) {
2114
- const request = (url) => fetcher(url, {
2115
- headers: {
2116
- "X-Void-Platform-Probe": healthProbeToken,
2117
- ...cfAccessHeaders(process.env, url)
2118
- },
2119
- redirect: "manual",
2120
- signal: AbortSignal.timeout(3e4)
2121
- });
2122
- const dispatchUrl = dispatchOrigin ?? `https://${PLATFORM_HEALTH_HOSTNAME_LABEL}.${applicationBaseDomain}`;
2123
- const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
2124
- request(`${apiUrl}/health`),
2125
- request(`${apiUrl}/.well-known/void-platform`),
2126
- request(`${proxyUrl}/health`),
2127
- request(`${dispatchUrl}/health`),
2128
- context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
2129
- ]);
2130
- if (!health.ok) throw await platformProbeFailure("Health check", `${apiUrl}/health`, health, context);
2131
- if (!discovery.ok) throw await platformProbeFailure("Discovery check", `${apiUrl}/.well-known/void-platform`, discovery, context);
2132
- const body = await discovery.json();
2133
- if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
2134
- if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
2135
- if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
2136
- if (!proxy.ok) throw await platformProbeFailure("Proxy health check", `${proxyUrl}/health`, proxy, context);
2137
- if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", `${dispatchUrl}/health`, dispatch, context);
2138
- if (emailGateway) {
2139
- if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", `${context.emailGatewayUrl}/health`, emailGateway, context);
2140
- const identity = await emailGateway.json();
2141
- if (identity?.installationId !== installationId || identity.platformVersion !== platformVersion || identity.emailProtocolVersion !== 1) throw new Error("platform: Deployed email gateway identity, version, or protocol does not match this installation.");
2142
- }
2071
+ async function runPlatformWrangler(operation, accountId, cwd, capture) {
2072
+ return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
2143
2073
  }
2144
- async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fetch) {
2145
- const response = await fetcher(`${apiUrl}/internal/platform/sandbox-drain`, {
2146
- headers: {
2147
- "X-Void-Platform-Probe": healthProbeToken,
2148
- ...cfAccessHeaders(process.env, apiUrl)
2149
- },
2150
- signal: AbortSignal.timeout(3e4),
2151
- redirect: "manual"
2074
+ const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
2075
+ /** Opt-in maintainer diagnostics, enabled with VOID_PLATFORM_TIMINGS=1. */
2076
+ function writePlatformTiming(label, startedAt) {
2077
+ if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] ${label} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
2078
+ }
2079
+ async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false, apiToken) {
2080
+ const startedAt = performance.now();
2081
+ const result = await createDefaultRunWrangler(process.cwd(), {
2082
+ launcher: resolveCloudflareOperationArgv(),
2083
+ parallelWorkerUpload,
2084
+ timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
2085
+ })(operation, {
2086
+ accountId,
2087
+ apiBaseUrl,
2088
+ apiToken,
2089
+ cwd
2152
2090
  });
2153
- if (!response.ok) throw new Error(`platform: Sandbox drain probe failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
2154
- let value;
2155
- try {
2156
- value = await response.json();
2157
- } catch {
2158
- throw new Error("platform: Sandbox drain probe did not return its DB-backed protocol v1 document. Traffic remains disabled.");
2091
+ writePlatformTiming(`Cloudflare ${operation.type}`, startedAt);
2092
+ const stdout = result.stdout || "";
2093
+ const stderr = result.stderr || "";
2094
+ if (!capture && result.status !== 0) {
2095
+ if (stdout) process.stdout.write(stdout);
2096
+ if (stderr) process.stderr.write(stderr);
2159
2097
  }
2160
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
2161
- const status = value;
2162
- const validState = status.state === "required" || status.state === "draining" || status.state === "complete";
2163
- const validPhase = status.phase === null || status.phase === "deployments" || status.phase === "resources";
2164
- if (status.version !== 1 || !validState || typeof status.complete !== "boolean" || !validPhase || status.generation !== null && (typeof status.generation !== "string" || status.generation.length === 0) || status.updatedAt !== null && (typeof status.updatedAt !== "string" || !Number.isFinite(Date.parse(status.updatedAt))) || status.state === "required" && (status.complete || status.generation !== null || status.updatedAt !== null || status.phase !== null) || status.state === "draining" && (status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null) || status.state === "complete" && (!status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
2165
- return status.state === "complete";
2098
+ return {
2099
+ ...result,
2100
+ stderr,
2101
+ stdout
2102
+ };
2166
2103
  }
2167
- async function verifyPlatformDeploymentExecutionDrain(apiUrl, healthProbeToken, installationId, fetcher = fetch) {
2168
- const response = await fetcher(`${apiUrl}/__void/platform/deployment-execution`, {
2169
- headers: {
2170
- "X-Void-Platform-Probe": healthProbeToken,
2171
- ...cfAccessHeaders(process.env, apiUrl)
2172
- },
2173
- signal: AbortSignal.timeout(3e4),
2174
- redirect: "manual"
2104
+ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
2105
+ "connection",
2106
+ "keep-alive",
2107
+ "proxy-authenticate",
2108
+ "proxy-authorization",
2109
+ "te",
2110
+ "trailer",
2111
+ "transfer-encoding",
2112
+ "upgrade"
2113
+ ]);
2114
+ function forwardHeaders(headers) {
2115
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => !HOP_BY_HOP_HEADERS.has(name.toLowerCase())));
2116
+ }
2117
+ function closeHttpServer(server) {
2118
+ return new Promise((resolvePromise, rejectPromise) => {
2119
+ server.close((error) => {
2120
+ if (error) rejectPromise(error);
2121
+ else resolvePromise();
2122
+ });
2123
+ server.closeAllConnections();
2175
2124
  });
2176
- if (!response.ok) throw new Error(`platform: Deployment execution drain probe failed (HTTP ${response.status}). Keep the current runtime until its durable deployment operations have drained.`);
2177
- let value;
2178
- try {
2179
- value = await response.json();
2180
- } catch {
2181
- throw new Error("platform: Deployment execution drain probe returned an invalid document.");
2182
- }
2183
- const status = value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2184
- if (status?.protocolVersion !== 1 || status.installationId !== installationId || typeof status.admissionClosed !== "boolean" || !Number.isSafeInteger(status.pendingDeployments) || Number(status.pendingDeployments) < 0) throw new Error("platform: Deployment execution drain probe returned an invalid document.");
2185
- if (!status.admissionClosed || status.pendingDeployments !== 0) throw new Error("platform: Durable deployment operations are still admitted or unfinished. Keep the current runtime and retry this command after they finish.");
2186
2125
  }
2187
- async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
2188
- const deadline = Date.now() + 3e5;
2189
- let operationId = null;
2190
- const timedOut = () => /* @__PURE__ */ new Error(`platform: Shared email bootstrap did not finish within five minutes at ${apiUrl}.${operationId ? ` Operation ${operationId} remains incomplete.` : ""} Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
2191
- const waitForProgress = async () => {
2192
- const remaining = deadline - Date.now();
2193
- if (remaining <= 0) throw timedOut();
2194
- await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, remaining)));
2126
+ async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName) {
2127
+ const upstreamBase = new URL(upstreamApiBaseUrl);
2128
+ if (upstreamBase.protocol !== "http:" && upstreamBase.protocol !== "https:") throw new Error("platform: Cloudflare API base URL must use HTTP or HTTPS.");
2129
+ const basePath = upstreamBase.pathname.replace(/\/+$/, "");
2130
+ const accountPath = `${basePath}/accounts/${encodeURIComponent(accountId)}`;
2131
+ const expectedPath = `${accountPath}/workers/scripts/${encodeURIComponent(workerName)}`;
2132
+ const assetUploadPath = `${accountPath}/workers/assets/upload`;
2133
+ const state = {
2134
+ assetSessionStarted: false,
2135
+ attempted: false,
2136
+ conflictStatus: null,
2137
+ created: false,
2138
+ forwarded: false,
2139
+ rejectedMutation: null
2195
2140
  };
2196
- let response;
2197
- let value;
2198
- let status;
2199
- for (;;) {
2200
- const remaining = deadline - Date.now();
2201
- if (remaining <= 0) throw timedOut();
2202
- const signal = AbortSignal.timeout(Math.min(12e4, remaining));
2203
- try {
2204
- response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
2205
- method: "POST",
2206
- headers: {
2207
- "X-Void-Platform-Probe": healthProbeToken,
2208
- ...cfAccessHeaders(process.env, apiUrl)
2209
- },
2210
- signal,
2211
- redirect: "manual"
2212
- });
2213
- } catch (error) {
2214
- if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
2215
- await waitForProgress();
2216
- continue;
2217
- }
2218
- throw new Error(`platform: Shared email bootstrap request failed at ${apiUrl}. Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
2219
- }
2220
- try {
2221
- value = await response.json();
2222
- } catch (error) {
2223
- if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
2224
- await waitForProgress();
2225
- continue;
2141
+ const server = createServer((incoming, outgoing) => {
2142
+ (async () => {
2143
+ const incomingUrl = new URL(incoming.url ?? "/", "http://127.0.0.1");
2144
+ if (incomingUrl.pathname !== basePath && !incomingUrl.pathname.startsWith(`${basePath}/`)) {
2145
+ outgoing.writeHead(502, { "Content-Type": "application/json" });
2146
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Invalid API proxy path\"}]}");
2147
+ return;
2226
2148
  }
2227
- throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
2228
- }
2229
- status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
2230
- const observedOperationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
2231
- if (status?.version === 1 && observedOperationId) {
2232
- if (operationId && operationId !== observedOperationId) throw new Error("platform: Shared email bootstrap operation identity changed while waiting. Traffic remains disabled. Inspect the installation before retrying.");
2233
- operationId = observedOperationId;
2234
- }
2235
- if ([
2236
- 200,
2237
- 202,
2238
- 503
2239
- ].includes(response.status) && status?.version === 1 && status.state === "incomplete" && observedOperationId && (status.operationStatus === "pending" || status.operationStatus === "running") && status.failureCode == null) {
2240
- await waitForProgress();
2241
- continue;
2242
- }
2243
- break;
2244
- }
2245
- if (!response.ok || status?.version !== 1 || status.state !== "ready" || operationId === null || status.operationId !== operationId || status.operationStatus != null && status.operationStatus !== "completed" || status.failureCode != null) {
2246
- const code = (value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value) ? value : null;
2247
- const operationStatus = code(status?.operationStatus);
2248
- const operationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
2249
- const details = operationId ? [`Operation ${operationId}: ${operationStatus ?? "incomplete"}.`] : operationStatus ? [`Operation status: ${operationStatus}.`] : [];
2250
- const currentStep = code(status?.currentStep);
2251
- const failureCode = code(status?.failureCode);
2252
- if (currentStep) details.push(`Step: ${currentStep}.`);
2253
- if (failureCode) details.push(`Failure: ${failureCode}.`);
2254
- let domain = null;
2255
- if (typeof status?.domain === "string") try {
2256
- domain = normalizeApplicationDomain(status.domain);
2257
- } catch {}
2258
- const inboundStatus = code(status?.inboundStatus);
2259
- if (domain) details.push(`Domain: ${domain}${inboundStatus ? ` (inbound: ${inboundStatus})` : ""}.`);
2260
- const mxDetail = domain ? `MX records for ${domain} do not route mail to Cloudflare Email Routing` : null;
2261
- const safeDetails = [
2262
- ...domain ? [mxDetail, `enabled explicit Cloudflare email rules conflict with ${domain}`] : [],
2263
- "zone catch-all does not point to this ingress",
2264
- "another enabled catch-all owns this zone"
2265
- ];
2266
- if (typeof status?.failureDetail === "string" && safeDetails.includes(status.failureDetail)) {
2267
- details.push(`${status.failureDetail}.`);
2268
- if (currentStep === "inbound_probe" && status.failureDetail === mxDetail) details.push(`Enable Email Routing for ${domain} in the Cloudflare dashboard, then resume this installation.`);
2269
- }
2270
- const detail = details.length ? ` ${details.join(" ")}` : "";
2271
- throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
2149
+ const createQuery = [...incomingUrl.searchParams];
2150
+ const supportedCreateQuery = new Set(createQuery.map(([key]) => key)).size === createQuery.length && createQuery.every(([key, value]) => key === "excludeScript" && value === "true" || key === "bindings_inherit" && value === "strict");
2151
+ const isConditionalCreate = incoming.method === "PUT" && incomingUrl.pathname === expectedPath && supportedCreateQuery;
2152
+ const isTopLevelWorkerUpload = incoming.method === "PUT" && new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/accounts/[^/]+/workers/scripts/[^/]+$`).test(incomingUrl.pathname);
2153
+ const isReadOnly = incoming.method === "GET" || incoming.method === "HEAD" || incoming.method === "OPTIONS";
2154
+ const isAssetUploadSession = incoming.method === "POST" && incomingUrl.pathname === `${expectedPath}/assets-upload-session` && incomingUrl.search === "";
2155
+ const isAssetUpload = incoming.method === "POST" && state.assetSessionStarted && (incomingUrl.pathname === assetUploadPath && incomingUrl.search === "?base64=true" || incomingUrl.search === "" && incomingUrl.pathname.startsWith(`${assetUploadPath}/`) && /^[0-9a-f]{32}$/.test(incomingUrl.pathname.slice(assetUploadPath.length + 1)));
2156
+ if (isTopLevelWorkerUpload && !isConditionalCreate || !isReadOnly && !isConditionalCreate && !isAssetUploadSession && !isAssetUpload && !state.created) {
2157
+ state.rejectedMutation = `${incoming.method ?? "UNKNOWN"} ${incomingUrl.pathname}${incomingUrl.pathname === expectedPath && !supportedCreateQuery ? " (unsupported Worker create query)" : ""}`;
2158
+ incoming.resume();
2159
+ outgoing.writeHead(409, { "Content-Type": "application/json" });
2160
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Mutation blocked until conditional Worker creation succeeds\"}]}");
2161
+ return;
2162
+ }
2163
+ const headers = forwardHeaders(incoming.headers);
2164
+ delete headers.host;
2165
+ if (isConditionalCreate) {
2166
+ state.attempted = true;
2167
+ headers["if-none-match"] = "*";
2168
+ }
2169
+ if (isAssetUploadSession) state.assetSessionStarted = true;
2170
+ const upstreamUrl = new URL(`${incomingUrl.pathname}${incomingUrl.search}`, upstreamBase.origin);
2171
+ const request$2 = (upstreamUrl.protocol === "https:" ? request$1 : request)(upstreamUrl, {
2172
+ headers,
2173
+ method: incoming.method
2174
+ }, (response) => {
2175
+ if (isConditionalCreate) {
2176
+ state.forwarded = true;
2177
+ if (response.statusCode === 409 || response.statusCode === 412) state.conflictStatus = response.statusCode;
2178
+ state.created = response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300;
2179
+ }
2180
+ const responseHeaders = forwardHeaders(response.headers);
2181
+ outgoing.writeHead(response.statusCode ?? 502, responseHeaders);
2182
+ response.pipe(outgoing);
2183
+ });
2184
+ request$2.on("error", () => {
2185
+ if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
2186
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy request failed\"}]}");
2187
+ });
2188
+ incoming.pipe(request$2);
2189
+ })().catch(() => {
2190
+ if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
2191
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy failed\"}]}");
2192
+ });
2193
+ });
2194
+ await new Promise((resolvePromise, rejectPromise) => {
2195
+ server.once("error", rejectPromise);
2196
+ server.listen(0, "127.0.0.1", () => {
2197
+ server.off("error", rejectPromise);
2198
+ resolvePromise();
2199
+ });
2200
+ });
2201
+ return {
2202
+ apiBaseUrl: `http://127.0.0.1:${server.address().port}${basePath}`,
2203
+ close: () => closeHttpServer(server),
2204
+ state
2205
+ };
2206
+ }
2207
+ async function withConditionalPlatformWorkerCreate(accountId, workerName, operation, upstreamApiBaseUrl = resolvePinnedApiBaseUrl()) {
2208
+ const proxy = await startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName);
2209
+ let result;
2210
+ try {
2211
+ result = await operation(proxy.apiBaseUrl);
2212
+ } finally {
2213
+ await proxy.close();
2272
2214
  }
2273
- return operationId;
2215
+ if (proxy.state.rejectedMutation) throw new Error(`platform: Wrangler attempted unexpected mutation ${proxy.state.rejectedMutation} while conditionally creating new Worker ${workerName}; the mutation was blocked.`);
2216
+ if (!proxy.state.attempted || !proxy.state.forwarded) throw new Error(`platform: Wrangler did not use the guarded create endpoint for new Worker ${workerName}; refusing an unverified deployment.`);
2217
+ if (proxy.state.conflictStatus !== null) throw new Error(`platform: Worker ${workerName} was created concurrently (HTTP ${proxy.state.conflictStatus}); no existing Worker was changed. Re-run to inspect the collision.`);
2218
+ return result;
2219
+ }
2220
+ async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl, apiToken) {
2221
+ return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl, false, apiToken), upstreamApiBaseUrl);
2274
2222
  }
2275
2223
  //#endregion
2276
2224
  //#region src/cli/platform-plan.ts
@@ -2383,8 +2331,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2383
2331
  }
2384
2332
  throw error;
2385
2333
  });
2386
- const [workersSubdomain, aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, deploymentQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
2387
- Promise.resolve(accountSubdomain),
2334
+ const [aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, deploymentQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
2388
2335
  gatewayInventory,
2389
2336
  client.listKvNamespaces(account.id, names.routingKv),
2390
2337
  client.listKvNamespaces(account.id, names.isrCacheKv),
@@ -2405,10 +2352,20 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2405
2352
  const bucket = r2Buckets.find((resource) => resource.name === names.assetsBucket);
2406
2353
  const recordedBucket = existing.resources.find((resource) => resource.key === "assets-bucket" && resource.kind === "r2-bucket" && resource.ownership === "created" && resource.state === "ready");
2407
2354
  if (bucket && recordedBucket?.id === bucket.id && recordedBucket.name === bucket.name) {
2408
- if (!matchesResourceMarker(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, "assets-bucket", "r2-bucket", bucket) || !matchesRecordedR2Fingerprint(recordedBucket.configHash, bucket)) ambiguousResources.add("assets-bucket");
2355
+ if (resourceMarkerProblem(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, {
2356
+ id: bucket.id,
2357
+ key: "assets-bucket",
2358
+ kind: "r2-bucket",
2359
+ name: bucket.name
2360
+ }) || !matchesRecordedR2Fingerprint(recordedBucket.configHash, bucket)) ambiguousResources.add("assets-bucket");
2409
2361
  }
2410
2362
  if (bucket && hasUnrecordedProvisionIntent(existing, "assets-bucket", "r2-bucket", bucket.name)) {
2411
- if (matchesResourceMarker(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, "assets-bucket", "r2-bucket", bucket)) intentProofs.add("assets-bucket");
2363
+ if (!resourceMarkerProblem(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, {
2364
+ id: bucket.id,
2365
+ key: "assets-bucket",
2366
+ kind: "r2-bucket",
2367
+ name: bucket.name
2368
+ })) intentProofs.add("assets-bucket");
2412
2369
  }
2413
2370
  for (const [key, name] of [
2414
2371
  ["api-worker", names.apiWorker],
@@ -2417,8 +2374,10 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2417
2374
  ["tail-worker", names.tailWorker],
2418
2375
  ["dispatch-worker", names.dispatchWorker]
2419
2376
  ]) if (workers.includes(name) && hasUnrecordedProvisionIntent(existing, key, "worker", name)) {
2420
- if (matchesResourceMarker((await client.getWorkerPlainTextBindings(account.id, name)).PLATFORM_OWNERSHIP_MARKER, existing, key, "worker", {
2377
+ if (!resourceMarkerProblem((await client.getWorkerPlainTextBindings(account.id, name)).PLATFORM_OWNERSHIP_MARKER, existing, {
2421
2378
  id: name,
2379
+ key,
2380
+ kind: "worker",
2422
2381
  name
2423
2382
  })) intentProofs.add(key);
2424
2383
  }
@@ -2446,6 +2405,14 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2446
2405
  if (records.length > 0 && (!matchingDomain || !ownedApiWorker)) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} already has a DNS record that installation would replace. Choose another hostname or remove the record explicitly.`);
2447
2406
  }
2448
2407
  const actions = [];
2408
+ const named = {
2409
+ actions,
2410
+ existing,
2411
+ options,
2412
+ remoteOwnership,
2413
+ intentProofs,
2414
+ ambiguousResources
2415
+ };
2449
2416
  if (!testing) {
2450
2417
  const recordedZone = existing?.resources.find((resource) => resource.key === "zone");
2451
2418
  const remotelyOwnedZone = remoteOwnership?.resources.some((resource) => resource.key === "zone" && resource.id === zone?.id);
@@ -2469,15 +2436,60 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2469
2436
  name: names.aiGateway,
2470
2437
  reason: "AI Gateway access will be verified with the runtime token before provisioning (AI Gateway: Edit required)."
2471
2438
  });
2472
- else addNamedPlan(actions, existing, "ai-gateway", "ai-gateway", names.aiGateway, aiGateways.filter((resource) => resource.id === names.aiGateway), "reuse", options, remoteOwnership, intentProofs);
2473
- addNamedPlan(actions, existing, "routing-kv", "kv-namespace", names.routingKv, routingKv, "reuse", options, remoteOwnership, intentProofs);
2474
- addNamedPlan(actions, existing, "isr-cache-kv", "kv-namespace", names.isrCacheKv, isrKv, "reuse", options, remoteOwnership);
2475
- addNamedPlan(actions, existing, "legacy-assets-kv", "kv-namespace", names.legacyAssetsKv, legacyKv, "reuse", options, remoteOwnership);
2476
- addNamedPlan(actions, existing, "assets-bucket", "r2-bucket", names.assetsBucket, r2Buckets.filter((resource) => resource.name === names.assetsBucket), "reuse", options, remoteOwnership, intentProofs, ambiguousResources);
2477
- addNamedPlan(actions, existing, "usage-queue", "queue", names.usageQueue, usageQueues, "reuse", options, remoteOwnership);
2478
- addNamedPlan(actions, existing, "cron-queue", "queue", names.cronQueue, cronQueues, "reuse", options, remoteOwnership);
2479
- if (options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue")) addNamedPlan(actions, existing, "deployment-queue", "queue", names.deploymentQueue, deploymentQueues, "reuse", options, remoteOwnership);
2480
- addNamedPlan(actions, existing, "dispatch-namespace", "dispatch-namespace", names.dispatchNamespace, dispatchNamespaces.filter((resource) => resource.name === names.dispatchNamespace), "reuse", options, remoteOwnership);
2439
+ else addNamedPlan(named, {
2440
+ key: "ai-gateway",
2441
+ kind: "ai-gateway",
2442
+ name: names.aiGateway,
2443
+ matches: aiGateways.filter((resource) => resource.id === names.aiGateway)
2444
+ });
2445
+ addNamedPlan(named, {
2446
+ key: "routing-kv",
2447
+ kind: "kv-namespace",
2448
+ name: names.routingKv,
2449
+ matches: routingKv
2450
+ });
2451
+ addNamedPlan(named, {
2452
+ key: "isr-cache-kv",
2453
+ kind: "kv-namespace",
2454
+ name: names.isrCacheKv,
2455
+ matches: isrKv
2456
+ });
2457
+ addNamedPlan(named, {
2458
+ key: "legacy-assets-kv",
2459
+ kind: "kv-namespace",
2460
+ name: names.legacyAssetsKv,
2461
+ matches: legacyKv
2462
+ });
2463
+ addNamedPlan(named, {
2464
+ key: "assets-bucket",
2465
+ kind: "r2-bucket",
2466
+ name: names.assetsBucket,
2467
+ matches: r2Buckets.filter((resource) => resource.name === names.assetsBucket)
2468
+ });
2469
+ addNamedPlan(named, {
2470
+ key: "usage-queue",
2471
+ kind: "queue",
2472
+ name: names.usageQueue,
2473
+ matches: usageQueues
2474
+ });
2475
+ addNamedPlan(named, {
2476
+ key: "cron-queue",
2477
+ kind: "queue",
2478
+ name: names.cronQueue,
2479
+ matches: cronQueues
2480
+ });
2481
+ if (options?.deploymentExecution ?? existing?.resources.some((resource) => resource.key === "deployment-queue")) addNamedPlan(named, {
2482
+ key: "deployment-queue",
2483
+ kind: "queue",
2484
+ name: names.deploymentQueue,
2485
+ matches: deploymentQueues
2486
+ });
2487
+ addNamedPlan(named, {
2488
+ key: "dispatch-namespace",
2489
+ kind: "dispatch-namespace",
2490
+ name: names.dispatchNamespace,
2491
+ matches: dispatchNamespaces.filter((resource) => resource.name === names.dispatchNamespace)
2492
+ });
2481
2493
  const recordedHyperdrive = existing?.resources.find((resource) => resource.key === "hyperdrive");
2482
2494
  const externalHyperdriveId = input.externalHyperdrive?.id || (recordedHyperdrive?.ownership === "adopted" ? recordedHyperdrive.id : "");
2483
2495
  if (externalHyperdriveId) {
@@ -2494,17 +2506,28 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2494
2506
  name: remote.name,
2495
2507
  reason: recordedHyperdrive ? "Reuse the recorded externally managed Hyperdrive." : "Use the externally managed Hyperdrive without taking ownership."
2496
2508
  });
2497
- } else addNamedPlan(actions, existing, "hyperdrive", "hyperdrive", names.hyperdrive, hyperdrives.filter((resource) => resource.name === names.hyperdrive), "reuse", options, remoteOwnership);
2509
+ } else addNamedPlan(named, {
2510
+ key: "hyperdrive",
2511
+ kind: "hyperdrive",
2512
+ name: names.hyperdrive,
2513
+ matches: hyperdrives.filter((resource) => resource.name === names.hyperdrive)
2514
+ });
2498
2515
  for (const [key, name] of [
2499
2516
  ["api-worker", names.apiWorker],
2500
2517
  ["proxy-worker", names.proxyWorker],
2501
2518
  ...options?.includeEmailGateway === false ? [] : [["email-gateway-worker", names.emailGatewayWorker]],
2502
2519
  ["tail-worker", names.tailWorker],
2503
2520
  ["dispatch-worker", names.dispatchWorker]
2504
- ]) addNamedPlan(actions, existing, key, "worker", name, workers.includes(name) ? [{
2505
- id: name,
2506
- name
2507
- }] : [], "update", options, remoteOwnership, intentProofs);
2521
+ ]) addNamedPlan(named, {
2522
+ key,
2523
+ kind: "worker",
2524
+ name,
2525
+ matches: workers.includes(name) ? [{
2526
+ id: name,
2527
+ name
2528
+ }] : [],
2529
+ existingAction: "update"
2530
+ });
2508
2531
  const resolvedResources = { ...deployment.resources };
2509
2532
  for (const action of actions) {
2510
2533
  const resourceKey = RESOURCE_NAME_KEY_BY_ACTION[action.key];
@@ -2514,8 +2537,8 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2514
2537
  ...deployment,
2515
2538
  resources: resolvedResources
2516
2539
  });
2517
- const apiUrl = resolvedDeployment.controlPlane.apiHostname ? `https://${deployment.controlPlane.apiHostname}` : `https://${resolvedDeployment.resources.apiWorker}.${workersSubdomain}.workers.dev`;
2518
- const proxyUrl = resolvedDeployment.controlPlane.proxyHostname ? `https://${deployment.controlPlane.proxyHostname}` : `https://${resolvedDeployment.resources.proxyWorker}.${workersSubdomain}.workers.dev`;
2540
+ const apiUrl = resolvedDeployment.controlPlane.apiHostname ? `https://${deployment.controlPlane.apiHostname}` : `https://${resolvedDeployment.resources.apiWorker}.${accountSubdomain}.workers.dev`;
2541
+ const proxyUrl = `https://${resolvedDeployment.resources.proxyWorker}.${accountSubdomain}.workers.dev`;
2519
2542
  return {
2520
2543
  account,
2521
2544
  actions,
@@ -2525,10 +2548,13 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2525
2548
  ...input.externalHyperdrive ? { externalHyperdrive: input.externalHyperdrive } : {},
2526
2549
  installationId,
2527
2550
  proxyUrl,
2528
- workersSubdomain,
2551
+ workersSubdomain: accountSubdomain,
2529
2552
  zone
2530
2553
  };
2531
2554
  }
2555
+ function platformWorkersDevUrl(plan, workerName) {
2556
+ return `https://${workerName}.${plan.workersSubdomain}.workers.dev`;
2557
+ }
2532
2558
  async function revalidatePlatformInstallAction(client, original, manifest, action, options) {
2533
2559
  if (options?.stableWorkerUpdate && !options.repairMissing && action.kind === "worker" && action.action === "update" && original.actions.some((entry) => entry.key === action.key && entry.kind === action.kind && entry.action === action.action && entry.id === action.id && entry.name === action.name) && original.actions.every((entry) => {
2534
2560
  const resource = manifest.resources.find((candidate) => candidate.key === entry.key);
@@ -2536,8 +2562,10 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
2536
2562
  })) {
2537
2563
  const recorded = manifest.resources.find((resource) => resource.key === action.key);
2538
2564
  if (!recorded || recorded.kind !== "worker" || recorded.state !== "ready" || recorded.id !== action.name || recorded.name !== action.name) throw new Error(`platform: Worker ${JSON.stringify(action.name)} no longer matches the approved installation plan. Review a new plan before retrying.`);
2539
- if (!matchesResourceMarker((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, action.key, "worker", {
2565
+ if (resourceMarkerProblem((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, {
2540
2566
  id: action.name,
2567
+ key: action.key,
2568
+ kind: "worker",
2541
2569
  name: action.name
2542
2570
  })) throw new Error(`platform: Cloudflare Worker ${JSON.stringify(action.name)} no longer has this installation's ownership marker. Review a new plan before retrying.`);
2543
2571
  return;
@@ -2560,8 +2588,10 @@ async function revalidatePlatformInstallAction(client, original, manifest, actio
2560
2588
  })).actions.find((entry) => entry.key === action.key);
2561
2589
  if (!current || current.kind !== action.kind || current.name !== action.name || current.action !== action.action) throw new Error(`platform: Cloudflare changed after the installation plan was approved for ${action.kind} ${JSON.stringify(action.name)}. Review a new plan before retrying.`);
2562
2590
  if (action.kind === "worker" && action.action === "update") {
2563
- if (!matchesResourceMarker((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, action.key, "worker", {
2591
+ if (resourceMarkerProblem((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, {
2564
2592
  id: action.name,
2593
+ key: action.key,
2594
+ kind: "worker",
2565
2595
  name: action.name
2566
2596
  })) throw new Error(`platform: Cloudflare Worker ${JSON.stringify(action.name)} no longer has this installation's ownership marker. Review a new plan before retrying.`);
2567
2597
  }
@@ -2618,7 +2648,9 @@ function resolveAccount(accounts, selector) {
2618
2648
  if (matches.length !== 1) throw new Error(matches.length === 0 ? `platform: Cloudflare account ${JSON.stringify(selector)} is not available to this token.` : "platform: Select a Cloudflare account with --account <id>.");
2619
2649
  return matches[0];
2620
2650
  }
2621
- function addNamedPlan(actions, existing, key, kind, name, matches, existingAction = "reuse", options, remoteOwnership, intentProofs = /* @__PURE__ */ new Set(), ambiguousResources = /* @__PURE__ */ new Set()) {
2651
+ function addNamedPlan(context, target) {
2652
+ const { actions, existing, options, remoteOwnership, intentProofs, ambiguousResources } = context;
2653
+ const { key, kind, name, matches, existingAction = "reuse" } = target;
2622
2654
  if (matches.length > 1) throw new Error(`platform: More than one ${kind} named ${JSON.stringify(name)} exists.`);
2623
2655
  const remote = matches[0];
2624
2656
  const recorded = existing?.resources.find((resource) => resource.key === key);
@@ -2685,15 +2717,16 @@ function addNamedPlan(actions, existing, key, kind, name, matches, existingActio
2685
2717
  });
2686
2718
  }
2687
2719
  function hasUnrecordedProvisionIntent(manifest, key, kind, name) {
2688
- return !manifest.resources.some((resource) => resource.key === key && resource.state === "ready") && manifest.steps.some((step) => isProvisionIntentStep(step.key, key) && step.resultHash === platformProvisionIntentHash(manifest, key, kind, name));
2720
+ return !manifest.resources.some((resource) => resource.key === key && resource.state === "ready") && hasProvisionIntentStep(manifest, key, kind, name);
2689
2721
  }
2690
2722
  function hasAmbiguousProvisionIntent(manifest, key, kind, remoteId, name) {
2691
2723
  const recorded = manifest.resources.find((resource) => resource.key === key);
2692
2724
  if (recorded?.id === remoteId && recorded.kind === kind && recorded.name === name) return false;
2693
- return manifest.steps.some((step) => isProvisionIntentStep(step.key, key) && step.resultHash === platformProvisionIntentHash(manifest, key, kind, name, recorded?.id ?? null));
2725
+ return hasProvisionIntentStep(manifest, key, kind, name, recorded?.id ?? null);
2694
2726
  }
2695
- function isProvisionIntentStep(stepKey, resourceKey) {
2696
- return stepKey === `provision-intent:${resourceKey}` || stepKey.startsWith(`provision-intent:${resourceKey}:`);
2727
+ /** Whether the manifest records an intent to provision this resource. */
2728
+ function hasProvisionIntentStep(manifest, key, kind, name, previousResourceId = null) {
2729
+ return manifest.steps.some((step) => (step.key === `provision-intent:${key}` || step.key.startsWith(`provision-intent:${key}:`)) && step.resultHash === platformProvisionIntentHash(manifest, key, kind, name, previousResourceId));
2697
2730
  }
2698
2731
  function interruptedProvisionRetryName(manifest, key, kind, name) {
2699
2732
  const suffix = `-retry-${createHash("sha256").update(JSON.stringify({
@@ -2704,36 +2737,53 @@ function interruptedProvisionRetryName(manifest, key, kind, name) {
2704
2737
  })).digest("hex").slice(0, 32)}`;
2705
2738
  return `${name.slice(0, 63 - suffix.length).replace(/-+$/, "")}${suffix}`;
2706
2739
  }
2707
- function matchesRecordedR2Fingerprint(configHash, resource) {
2708
- if (!resource.fingerprint) return false;
2709
- const liveHash = createHash("sha256").update(JSON.stringify({
2710
- fingerprint: resource.fingerprint,
2711
- kind: "r2-bucket",
2712
- name: resource.name
2740
+ /** Name-only config hash, recorded for resources without a live fingerprint. */
2741
+ function legacyResourceConfigHash(kind, name) {
2742
+ return createHash("sha256").update(JSON.stringify({
2743
+ kind,
2744
+ name
2713
2745
  })).digest("hex");
2714
- const legacyHash = createHash("sha256").update(JSON.stringify({
2746
+ }
2747
+ function r2BucketConfigHash(name, fingerprint) {
2748
+ return createHash("sha256").update(JSON.stringify({
2749
+ fingerprint,
2715
2750
  kind: "r2-bucket",
2716
- name: resource.name
2751
+ name
2752
+ })).digest("hex");
2753
+ }
2754
+ function dnsRecordConfigHash(name, type, content, proxied) {
2755
+ return createHash("sha256").update(JSON.stringify({
2756
+ content,
2757
+ kind: "dns-record",
2758
+ name,
2759
+ proxied,
2760
+ type
2717
2761
  })).digest("hex");
2718
- return configHash === liveHash || configHash === legacyHash;
2719
2762
  }
2720
- function matchesResourceMarker(value, manifest, key, kind, resource) {
2721
- if (!value) return false;
2763
+ /** Whether a live R2 bucket still matches the creation fingerprint recorded for it. */
2764
+ function matchesRecordedR2Fingerprint(configHash, bucket) {
2765
+ if (!bucket.fingerprint) return false;
2766
+ return configHash === r2BucketConfigHash(bucket.name, bucket.fingerprint) || configHash === legacyResourceConfigHash("r2-bucket", bucket.name);
2767
+ }
2768
+ /** Why `value` is not this installation's ownership marker for the resource, or null when it is. */
2769
+ function resourceMarkerProblem(value, manifest, resource) {
2770
+ if (!value) return "has no ownership marker";
2771
+ let actual;
2722
2772
  try {
2723
- const actual = parsePlatformResourceOwnershipMarker(JSON.parse(value));
2724
- const expected = createPlatformResourceOwnershipMarker({
2725
- accountId: manifest.account.id,
2726
- installationId: manifest.id,
2727
- ownershipNonce: manifest.ownershipNonce,
2728
- resourceId: resource.id,
2729
- resourceKey: key,
2730
- resourceKind: kind,
2731
- resourceName: resource.name
2732
- });
2733
- return JSON.stringify(actual) === JSON.stringify(expected);
2773
+ actual = parsePlatformResourceOwnershipMarker(JSON.parse(value));
2734
2774
  } catch {
2735
- return false;
2775
+ return "has an invalid ownership marker";
2736
2776
  }
2777
+ const expected = createPlatformResourceOwnershipMarker({
2778
+ accountId: manifest.account.id,
2779
+ installationId: manifest.id,
2780
+ ownershipNonce: manifest.ownershipNonce,
2781
+ resourceId: resource.id,
2782
+ resourceKey: resource.key,
2783
+ resourceKind: resource.kind,
2784
+ resourceName: resource.name
2785
+ });
2786
+ return JSON.stringify(actual) === JSON.stringify(expected) ? null : "belongs to another installation";
2737
2787
  }
2738
2788
  function isLegacyUniqueProvisionAttempt(manifest, key, kind, name) {
2739
2789
  const resourceSlug = resourceSlugForManifest(manifest);
@@ -2896,12 +2946,6 @@ function platformTrafficAttachmentWorker(deployment, resource) {
2896
2946
  }
2897
2947
  return null;
2898
2948
  }
2899
- function trackedPlatformWorkerTraffic(_manifest, _workers) {
2900
- return {
2901
- customDomains: [],
2902
- routes: []
2903
- };
2904
- }
2905
2949
  function recordPlatformWorkerTraffic(manifest, workerName, inventory, updatedAt) {
2906
2950
  const desired = platformManagedWorkerTraffic(manifest.deployment, /* @__PURE__ */ new Set([workerName]));
2907
2951
  let next = manifest;
@@ -3009,17 +3053,17 @@ function saveInstalledPlatformConnection(plan, manifest) {
3009
3053
  async function writeRemoteInstallationCheckpointWithoutDatabase(client, plan, manifest, databaseLock) {
3010
3054
  const startedAt = performance.now();
3011
3055
  await databaseLock?.checkpoint?.(manifest);
3012
- if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] database checkpoint ${(performance.now() - startedAt).toFixed(0)} ms\n`);
3056
+ writePlatformTiming("database checkpoint", startedAt);
3013
3057
  const routing = manifest.resources.find((resource) => resource.key === "routing-kv" && resource.kind === "kv-namespace" && resource.ownership === "created" && resource.state === "ready");
3014
3058
  if (!routing) return;
3015
3059
  const kvStartedAt = performance.now();
3016
3060
  await client.putKvValue(plan.account.id, routing.id, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, JSON.stringify(manifest));
3017
- if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] remote checkpoint ${(performance.now() - kvStartedAt).toFixed(0)} ms\n`);
3061
+ writePlatformTiming("remote checkpoint", kvStartedAt);
3018
3062
  }
3019
3063
  function recordProvisionIntent(manifest, action, now) {
3020
3064
  const previousResourceId = manifest.resources.find((resource) => resource.key === action.key)?.id ?? null;
3065
+ if (hasProvisionIntentStep(manifest, action.key, action.kind, action.name, previousResourceId)) return manifest;
3021
3066
  const resultHash = platformProvisionIntentHash(manifest, action.key, action.kind, action.name, previousResourceId);
3022
- if (manifest.steps.find((step) => (step.key === `provision-intent:${action.key}` || step.key.startsWith(`provision-intent:${action.key}:`)) && step.resultHash === resultHash)) return manifest;
3023
3067
  return recordInstallationStep(manifest, {
3024
3068
  completedAt: now,
3025
3069
  key: `provision-intent:${action.key}:${resultHash.slice(0, 16)}`,
@@ -3035,7 +3079,7 @@ function recordAdoptionIntent(manifest, action, id, now) {
3035
3079
  }
3036
3080
  function hasProvisionIntent(manifest, action) {
3037
3081
  const previousResourceId = manifest.resources.find((resource) => resource.key === action.key)?.id ?? null;
3038
- return manifest.steps.some((step) => (step.key === `provision-intent:${action.key}` || step.key.startsWith(`provision-intent:${action.key}:`)) && step.resultHash === platformProvisionIntentHash(manifest, action.key, action.kind, action.name, previousResourceId));
3082
+ return hasProvisionIntentStep(manifest, action.key, action.kind, action.name, previousResourceId);
3039
3083
  }
3040
3084
  async function createResource(client, plan, action, databaseUrl, zoneId) {
3041
3085
  switch (action.kind) {
@@ -3052,10 +3096,7 @@ async function createResource(client, plan, action, databaseUrl, zoneId) {
3052
3096
  function recordResource(manifest, action, id, ownership, now, databaseUrl, resourceFingerprint) {
3053
3097
  const previous = manifest.resources.find((resource) => resource.key === action.key);
3054
3098
  return recordInstallationResource(manifest, {
3055
- configHash: action.kind === "dns-record" ? dnsRecordConfigHash(action.name, "AAAA", "100::", true) : action.kind === "hyperdrive" && resourceFingerprint ? resourceFingerprint : action.kind === "hyperdrive" && databaseUrl ? hyperdriveOriginConfigHash(hyperdriveOriginFromConnectionString(databaseUrl)) : action.kind === "r2-bucket" && resourceFingerprint ? r2BucketConfigHash(action.name, resourceFingerprint) : hash(JSON.stringify({
3056
- kind: action.kind,
3057
- name: action.name
3058
- })),
3099
+ configHash: action.kind === "dns-record" ? dnsRecordConfigHash(action.name, "AAAA", "100::", true) : action.kind === "hyperdrive" && resourceFingerprint ? resourceFingerprint : action.kind === "hyperdrive" && databaseUrl ? hyperdriveOriginConfigHash(hyperdriveOriginFromConnectionString(databaseUrl)) : action.kind === "r2-bucket" && resourceFingerprint ? r2BucketConfigHash(action.name, resourceFingerprint) : legacyResourceConfigHash(action.kind, action.name),
3059
3100
  id,
3060
3101
  key: action.key,
3061
3102
  kind: action.kind,
@@ -3064,13 +3105,6 @@ function recordResource(manifest, action, id, ownership, now, databaseUrl, resou
3064
3105
  state: "ready"
3065
3106
  }, now);
3066
3107
  }
3067
- function r2BucketConfigHash(name, fingerprint) {
3068
- return hash(JSON.stringify({
3069
- fingerprint,
3070
- kind: "r2-bucket",
3071
- name
3072
- }));
3073
- }
3074
3108
  function retainReplacedInstallationResource(manifest, action, now) {
3075
3109
  const previous = manifest.resources.find((resource) => resource.key === action.key);
3076
3110
  if (action.action !== "create" || !previous || previous.state !== "ready" || previous.name === action.name) return manifest;
@@ -3098,7 +3132,6 @@ function validateReplannedWorkerActions(original, manifest, actions) {
3098
3132
  return actions;
3099
3133
  }
3100
3134
  async function inventoryUnmanagedPlatformWorkerTraffic(client, plan, workerName) {
3101
- if (!client.listWorkerDomains || !client.listWorkerServiceRoutes) throw new Error(`platform: Cloudflare traffic inventory is required before updating Worker ${JSON.stringify(workerName)}.`);
3102
3135
  const [routes, domains] = await Promise.all([client.listWorkerServiceRoutes(plan.account.id, workerName), client.listWorkerDomains(plan.account.id)]);
3103
3136
  const managed = platformManagedWorkerTraffic(plan.deployment, /* @__PURE__ */ new Set([workerName]));
3104
3137
  const managedRoutes = new Set(managed.routes.map((route) => route.pattern));
@@ -3176,10 +3209,6 @@ function requiredBoolean(value, field) {
3176
3209
  if (typeof value !== "boolean") throw new Error(`platform: ${field} must be a boolean.`);
3177
3210
  return value;
3178
3211
  }
3179
- async function publishPlatformWorkerCustomDomains(client, accountId, workerName, origins) {
3180
- if (!client.publishWorkerCustomDomainsSafely) throw new Error("platform: Safe Cloudflare Custom Domain publication is unavailable.");
3181
- await client.publishWorkerCustomDomainsSafely(accountId, workerName, origins);
3182
- }
3183
3212
  function uniqueTriggerRoutes(routes) {
3184
3213
  const seen = /* @__PURE__ */ new Set();
3185
3214
  return routes.filter((route) => {
@@ -3189,15 +3218,6 @@ function uniqueTriggerRoutes(routes) {
3189
3218
  return true;
3190
3219
  });
3191
3220
  }
3192
- function dnsRecordConfigHash(name, type, content, proxied) {
3193
- return hash(JSON.stringify({
3194
- content,
3195
- kind: "dns-record",
3196
- name,
3197
- proxied,
3198
- type
3199
- }));
3200
- }
3201
3221
  function hash(value) {
3202
3222
  return createHash("sha256").update(value).digest("hex");
3203
3223
  }
@@ -3257,7 +3277,7 @@ function createPlatformInstallationSecrets(input) {
3257
3277
  if (modernAuthentication && !existingInstallation && (authSetupToken.length < 32 || !/^[a-f0-9]{64}$/.test(authBootstrapCode))) throw new Error("platform: Valid setup authority and an administrator setup code are required.");
3258
3278
  if (authenticationSetupJson) {
3259
3279
  const setup = parsePlatformAuthenticationSetup(JSON.parse(authenticationSetupJson));
3260
- if (!existingInstallation && (authSetupToken.length < 32 || !/^[a-f0-9]{64}$/.test(authBootstrapCode) || createHash("sha256").update(authBootstrapCode).digest("hex") !== setup.bootstrapCredentialHash)) throw new Error("platform: Valid installation setup authority and a matching administrator setup code are required.");
3280
+ if (!existingInstallation && createHash("sha256").update(authBootstrapCode).digest("hex") !== setup.bootstrapCredentialHash) throw new Error("platform: Valid installation setup authority and a matching administrator setup code are required.");
3261
3281
  }
3262
3282
  const jwtSecret = restoredIdentity(input.jwtSecret || "", "jwtSecret");
3263
3283
  const resolvedJwtSecret = input.existingInstallation ? jwtSecret : required(jwtSecret, "VOID_PLATFORM_JWT_SECRET");
@@ -3282,6 +3302,11 @@ function createPlatformInstallationSecrets(input) {
3282
3302
  });
3283
3303
  const pendingWorkerSecretUpdateIntent = parseWorkerSecretUpdateIntent(checkpoint.workerSecretUpdatesJson);
3284
3304
  const emailSigningSecretUpdateAuthorized = emailSigningSecretInScope && !!emailSigningSecret && (emailEnabled && (!existingEmailEnabled || !checkpoint.emailSigningSecret) || !!input.emailSigningSecret);
3305
+ const githubOptional = existingInstallation || modernAuthentication;
3306
+ const resolve = (key, requiredName, optional) => {
3307
+ const value = configured(input[key], key);
3308
+ return optional ? value : required(value, requiredName);
3309
+ };
3285
3310
  const explicitWorkerSecretUpdates = withoutEmptySecrets({
3286
3311
  CF_API_TOKEN: input.cloudflareApiToken,
3287
3312
  ...emailSigningSecretUpdateAuthorized ? { EMAIL_SIGNING_SECRET: emailSigningSecret } : {},
@@ -3306,20 +3331,20 @@ function createPlatformInstallationSecrets(input) {
3306
3331
  ...checkpoint.accessCredentialsJson ? { accessCredentialsJson: checkpoint.accessCredentialsJson } : {},
3307
3332
  workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
3308
3333
  pendingWorkerSecretUpdateIntent: input.updateExistingWorkers ? pendingWorkerSecretUpdateIntent : void 0,
3309
- bootstrapGitHubId: existingInstallation || modernAuthentication ? configured(input.bootstrapGitHubId, "bootstrapGitHubId") : required(configured(input.bootstrapGitHubId, "bootstrapGitHubId"), "resolved administrator GitHub account id"),
3310
- bootstrapGitHubLogin: existingInstallation || modernAuthentication ? configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin") : required(configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin"), "VOID_PLATFORM_ADMIN_GITHUB_LOGIN"),
3311
- cloudflareApiToken: existingInstallation ? configured(input.cloudflareApiToken, "cloudflareApiToken") : required(configured(input.cloudflareApiToken, "cloudflareApiToken"), "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN"),
3334
+ bootstrapGitHubId: resolve("bootstrapGitHubId", "resolved administrator GitHub account id", githubOptional),
3335
+ bootstrapGitHubLogin: resolve("bootstrapGitHubLogin", "VOID_PLATFORM_ADMIN_GITHUB_LOGIN", githubOptional),
3336
+ cloudflareApiToken: resolve("cloudflareApiToken", "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN", existingInstallation),
3312
3337
  databaseUrl: required(input.databaseUrl || checkpoint.databaseUrl || "", "DATABASE_URL"),
3313
3338
  emailEnabled,
3314
3339
  emailSigningSecret,
3315
- githubClientId: existingInstallation || modernAuthentication ? configured(input.githubClientId, "githubClientId") : required(configured(input.githubClientId, "githubClientId"), "VOID_PLATFORM_GITHUB_CLIENT_ID"),
3316
- githubClientSecret: existingInstallation || modernAuthentication ? configured(input.githubClientSecret, "githubClientSecret") : required(configured(input.githubClientSecret, "githubClientSecret"), "VOID_PLATFORM_GITHUB_CLIENT_SECRET"),
3340
+ githubClientId: resolve("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID", githubOptional),
3341
+ githubClientSecret: resolve("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET", githubOptional),
3317
3342
  jwtSecret: resolvedJwtSecret,
3318
3343
  projectSecretActiveKeyVersion: projectSecretKeyring.activeVersion,
3319
3344
  projectSecretKey: projectSecretKeyring.v1Key,
3320
3345
  projectSecretKeysJson: projectSecretKeyring.keysJson,
3321
- r2AccessKeyId: existingInstallation ? configured(input.r2AccessKeyId, "r2AccessKeyId") : required(configured(input.r2AccessKeyId, "r2AccessKeyId"), "VOID_PLATFORM_R2_ACCESS_KEY_ID"),
3322
- r2SecretAccessKey: existingInstallation ? configured(input.r2SecretAccessKey, "r2SecretAccessKey") : required(configured(input.r2SecretAccessKey, "r2SecretAccessKey"), "VOID_PLATFORM_R2_SECRET_ACCESS_KEY")
3346
+ r2AccessKeyId: resolve("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID", existingInstallation),
3347
+ r2SecretAccessKey: resolve("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY", existingInstallation)
3323
3348
  };
3324
3349
  }
3325
3350
  function resolveProjectSecretKeyring(input) {
@@ -3461,14 +3486,7 @@ function platformWorkerSecretPayload(worker, secrets, accountId, zoneId, existin
3461
3486
  }
3462
3487
  return proxyWorkerSecrets(secrets, accountId);
3463
3488
  }
3464
- const allowed = worker === "api" ? [
3465
- "CF_API_TOKEN",
3466
- "EMAIL_SIGNING_SECRET",
3467
- "GITHUB_CLIENT_ID",
3468
- "GITHUB_CLIENT_SECRET",
3469
- "R2_ACCESS_KEY_ID",
3470
- "R2_SECRET_ACCESS_KEY"
3471
- ] : worker === "proxy" ? ["CF_API_TOKEN", "EMAIL_SIGNING_SECRET"] : ["EMAIL_SIGNING_SECRET"];
3489
+ const allowed = worker === "api" ? PLATFORM_WORKER_SECRET_UPDATE_NAMES : worker === "proxy" ? ["CF_API_TOKEN", "EMAIL_SIGNING_SECRET"] : ["EMAIL_SIGNING_SECRET"];
3472
3490
  const updates = Object.fromEntries(Object.entries(secrets.workerSecretUpdates ?? {}).filter(([name, value]) => allowed.includes(name) && typeof value === "string" && value !== ""));
3473
3491
  return Object.keys(updates).length > 0 ? updates : null;
3474
3492
  }
@@ -3542,220 +3560,64 @@ function createPlatformHealthProbeToken(ownershipNonce) {
3542
3560
  //#endregion
3543
3561
  //#region src/cli/platform-installer-errors.ts
3544
3562
  function assertCleanPlatformWrangler(result, operation) {
3545
- if (result.status !== 0 || result.signal || result.error) throw new Error(`platform: Wrangler failed to ${operation} (${result.stderr.trim() || `exit ${result.status}`}).`);
3546
- }
3547
- function cleanPlatformCommandResult() {
3548
- return {
3549
- status: 0,
3550
- stderr: "",
3551
- stdout: ""
3552
- };
3563
+ if (result.status !== 0 || result.signal || result.error) {
3564
+ const diagnostic = cloudflareCommandDiagnostic({
3565
+ ...result,
3566
+ stderr: result.stderr.trim() || result.errors?.map((error) => error.message).join("\n") || ""
3567
+ });
3568
+ throw new Error(`platform: Wrangler failed to ${operation} (${diagnostic || `exit ${result.status}`}).`);
3569
+ }
3553
3570
  }
3554
3571
  function assertPlatformTriggerStabilization(result, workerName, targetVersionId, operation) {
3555
3572
  if (!result.ok) throw new Error(`platform: Failed to ${operation} triggers for ${workerName} (${result.reason}).`);
3556
3573
  if (!result.targetOwnsTraffic) throw new Error(`platform: Worker ${workerName} was superseded by ${result.activeVersionId} while publishing ${targetVersionId}; its active trigger state was repaired.`);
3557
3574
  }
3558
- function errorMessage(error) {
3559
- return error instanceof Error ? error.message : String(error);
3560
- }
3561
3575
  //#endregion
3562
- //#region src/cli/platform-installer-process.ts
3563
- function platformWorkerCommandRunner(accountId, cwd, runner) {
3564
- return (operation, options) => runner(operation, accountId, cwd, options.capture);
3565
- }
3566
- async function runPlatformWrangler(operation, accountId, cwd, capture) {
3567
- return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
3576
+ //#region src/cli/platform-installer-inspection.ts
3577
+ var PlatformWorkerOwnershipError = class extends Error {};
3578
+ async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
3579
+ const result = await runner({
3580
+ type: "deployments.status",
3581
+ configPath,
3582
+ workerName
3583
+ }, accountId, process.cwd(), true);
3584
+ if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
3585
+ const value = result.data;
3586
+ if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
3587
+ const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
3588
+ if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
3589
+ return active[0].version_id;
3568
3590
  }
3569
- const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
3570
- async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false, apiToken) {
3571
- const startedAt = performance.now();
3572
- const result = await createDefaultRunWrangler(process.cwd(), {
3573
- launcher: resolveCloudflareOperationArgv(process.cwd()),
3574
- parallelWorkerUpload,
3575
- timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
3576
- })(operation, {
3577
- accountId,
3578
- apiBaseUrl,
3579
- apiToken,
3580
- cwd
3581
- });
3582
- if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] Cloudflare ${operation.type} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
3583
- const stdout = result.stdout || "";
3584
- const stderr = result.stderr || "";
3585
- if (!capture && result.status !== 0) {
3586
- if (stdout) process.stdout.write(stdout);
3587
- if (stderr) process.stderr.write(stderr);
3588
- }
3589
- return {
3590
- ...result,
3591
- stderr,
3592
- stdout
3593
- };
3594
- }
3595
- const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
3596
- "connection",
3597
- "keep-alive",
3598
- "proxy-authenticate",
3599
- "proxy-authorization",
3600
- "te",
3601
- "trailer",
3602
- "transfer-encoding",
3603
- "upgrade"
3604
- ]);
3605
- function forwardHeaders(headers) {
3606
- return Object.fromEntries(Object.entries(headers).filter(([name]) => !HOP_BY_HOP_HEADERS.has(name.toLowerCase())));
3607
- }
3608
- function closeHttpServer(server) {
3609
- return new Promise((resolvePromise, rejectPromise) => {
3610
- server.close((error) => {
3611
- if (error) rejectPromise(error);
3612
- else resolvePromise();
3613
- });
3614
- server.closeAllConnections();
3615
- });
3616
- }
3617
- async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName) {
3618
- const upstreamBase = new URL(upstreamApiBaseUrl);
3619
- if (upstreamBase.protocol !== "http:" && upstreamBase.protocol !== "https:") throw new Error("platform: Cloudflare API base URL must use HTTP or HTTPS.");
3620
- const basePath = upstreamBase.pathname.replace(/\/+$/, "");
3621
- const accountPath = `${basePath}/accounts/${encodeURIComponent(accountId)}`;
3622
- const expectedPath = `${accountPath}/workers/scripts/${encodeURIComponent(workerName)}`;
3623
- const assetUploadPath = `${accountPath}/workers/assets/upload`;
3624
- const state = {
3625
- assetSessionStarted: false,
3626
- attempted: false,
3627
- conflictStatus: null,
3628
- created: false,
3629
- forwarded: false,
3630
- rejectedMutation: null
3631
- };
3632
- const server = createServer((incoming, outgoing) => {
3633
- (async () => {
3634
- const incomingUrl = new URL(incoming.url ?? "/", "http://127.0.0.1");
3635
- if (incomingUrl.pathname !== basePath && !incomingUrl.pathname.startsWith(`${basePath}/`)) {
3636
- outgoing.writeHead(502, { "Content-Type": "application/json" });
3637
- outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Invalid API proxy path\"}]}");
3638
- return;
3639
- }
3640
- const createQuery = [...incomingUrl.searchParams];
3641
- const supportedCreateQuery = new Set(createQuery.map(([key]) => key)).size === createQuery.length && createQuery.every(([key, value]) => key === "excludeScript" && value === "true" || key === "bindings_inherit" && value === "strict");
3642
- const isConditionalCreate = incoming.method === "PUT" && incomingUrl.pathname === expectedPath && supportedCreateQuery;
3643
- const isTopLevelWorkerUpload = incoming.method === "PUT" && new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/accounts/[^/]+/workers/scripts/[^/]+$`).test(incomingUrl.pathname);
3644
- const isReadOnly = incoming.method === "GET" || incoming.method === "HEAD" || incoming.method === "OPTIONS";
3645
- const isAssetUploadSession = incoming.method === "POST" && incomingUrl.pathname === `${expectedPath}/assets-upload-session` && incomingUrl.search === "";
3646
- const isAssetUpload = incoming.method === "POST" && state.assetSessionStarted && (incomingUrl.pathname === assetUploadPath && incomingUrl.search === "?base64=true" || incomingUrl.search === "" && incomingUrl.pathname.startsWith(`${assetUploadPath}/`) && /^[0-9a-f]{32}$/.test(incomingUrl.pathname.slice(assetUploadPath.length + 1)));
3647
- if (isTopLevelWorkerUpload && !isConditionalCreate || !isReadOnly && !isConditionalCreate && !isAssetUploadSession && !isAssetUpload && !state.created) {
3648
- state.rejectedMutation = `${incoming.method ?? "UNKNOWN"} ${incomingUrl.pathname}${incomingUrl.pathname === expectedPath && !supportedCreateQuery ? " (unsupported Worker create query)" : ""}`;
3649
- incoming.resume();
3650
- outgoing.writeHead(409, { "Content-Type": "application/json" });
3651
- outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Mutation blocked until conditional Worker creation succeeds\"}]}");
3652
- return;
3653
- }
3654
- const headers = forwardHeaders(incoming.headers);
3655
- delete headers.host;
3656
- if (isConditionalCreate) {
3657
- state.attempted = true;
3658
- headers["if-none-match"] = "*";
3659
- }
3660
- if (isAssetUploadSession) state.assetSessionStarted = true;
3661
- const upstreamUrl = new URL(`${incomingUrl.pathname}${incomingUrl.search}`, upstreamBase.origin);
3662
- const request$2 = (upstreamUrl.protocol === "https:" ? request$1 : request)(upstreamUrl, {
3663
- headers,
3664
- method: incoming.method
3665
- }, (response) => {
3666
- if (isConditionalCreate) {
3667
- state.forwarded = true;
3668
- if (response.statusCode === 409 || response.statusCode === 412) state.conflictStatus = response.statusCode;
3669
- state.created = response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300;
3670
- }
3671
- const responseHeaders = forwardHeaders(response.headers);
3672
- outgoing.writeHead(response.statusCode ?? 502, responseHeaders);
3673
- response.pipe(outgoing);
3674
- });
3675
- request$2.on("error", () => {
3676
- if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
3677
- outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy request failed\"}]}");
3678
- });
3679
- incoming.pipe(request$2);
3680
- })().catch(() => {
3681
- if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
3682
- outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy failed\"}]}");
3683
- });
3684
- });
3685
- await new Promise((resolvePromise, rejectPromise) => {
3686
- server.once("error", rejectPromise);
3687
- server.listen(0, "127.0.0.1", () => {
3688
- server.off("error", rejectPromise);
3689
- resolvePromise();
3690
- });
3691
- });
3692
- return {
3693
- apiBaseUrl: `http://127.0.0.1:${server.address().port}${basePath}`,
3694
- close: () => closeHttpServer(server),
3695
- state
3696
- };
3697
- }
3698
- async function withConditionalPlatformWorkerCreate(accountId, workerName, operation, upstreamApiBaseUrl = resolvePinnedApiBaseUrl()) {
3699
- const proxy = await startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName);
3700
- let result;
3701
- try {
3702
- result = await operation(proxy.apiBaseUrl);
3703
- } finally {
3704
- await proxy.close();
3705
- }
3706
- if (proxy.state.rejectedMutation) throw new Error(`platform: Wrangler attempted unexpected mutation ${proxy.state.rejectedMutation} while conditionally creating new Worker ${workerName}; the mutation was blocked.`);
3707
- if (!proxy.state.attempted || !proxy.state.forwarded) throw new Error(`platform: Wrangler did not use the guarded create endpoint for new Worker ${workerName}; refusing an unverified deployment.`);
3708
- if (proxy.state.conflictStatus !== null) throw new Error(`platform: Worker ${workerName} was created concurrently (HTTP ${proxy.state.conflictStatus}); no existing Worker was changed. Re-run to inspect the collision.`);
3709
- return result;
3710
- }
3711
- async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl, apiToken) {
3712
- return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl, false, apiToken), upstreamApiBaseUrl);
3713
- }
3714
- //#endregion
3715
- //#region src/cli/platform-installer-inspection.ts
3716
- async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
3717
- const result = await runner({
3718
- type: "deployments.status",
3719
- configPath,
3720
- workerName
3721
- }, accountId, process.cwd(), true);
3722
- if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
3723
- const value = result.data;
3724
- if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
3725
- const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
3726
- if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
3727
- return active[0].version_id;
3728
- }
3729
- async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
3730
- const result = await runner({
3731
- type: "deployments.list",
3732
- configPath,
3733
- workerName
3734
- }, accountId, process.cwd(), true);
3735
- if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
3736
- const value = result.data;
3737
- if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3738
- return value.map((entry, index) => {
3739
- if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.created_on !== "string" || !Array.isArray(entry.versions)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3740
- const createdAt = Date.parse(entry.created_on);
3741
- if (!Number.isFinite(createdAt)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3742
- const versions = entry.versions.map((version) => {
3743
- if (!version || typeof version !== "object" || typeof version.version_id !== "string" || !Number.isFinite(Number(version.percentage))) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3744
- return {
3745
- percentage: Number(version.percentage),
3746
- versionId: version.version_id
3747
- };
3748
- });
3749
- return {
3750
- createdAt,
3751
- id: entry.id,
3752
- index,
3753
- versions
3754
- };
3755
- }).sort((left, right) => right.createdAt - left.createdAt || left.index - right.index).map(({ id, versions }) => ({
3756
- id,
3757
- versions
3758
- }));
3591
+ async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
3592
+ const result = await runner({
3593
+ type: "deployments.list",
3594
+ configPath,
3595
+ workerName
3596
+ }, accountId, process.cwd(), true);
3597
+ if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
3598
+ const value = result.data;
3599
+ if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3600
+ return value.map((entry, index) => {
3601
+ if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.created_on !== "string" || !Array.isArray(entry.versions)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3602
+ const createdAt = Date.parse(entry.created_on);
3603
+ if (!Number.isFinite(createdAt)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3604
+ const versions = entry.versions.map((version) => {
3605
+ if (!version || typeof version !== "object" || typeof version.version_id !== "string" || !Number.isFinite(Number(version.percentage))) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3606
+ return {
3607
+ percentage: Number(version.percentage),
3608
+ versionId: version.version_id
3609
+ };
3610
+ });
3611
+ return {
3612
+ createdAt,
3613
+ id: entry.id,
3614
+ index,
3615
+ versions
3616
+ };
3617
+ }).sort((left, right) => right.createdAt - left.createdAt || left.index - right.index).map(({ id, versions }) => ({
3618
+ id,
3619
+ versions
3620
+ }));
3759
3621
  }
3760
3622
  function samePlatformWorkerTraffic(left, right) {
3761
3623
  if (!left) return false;
@@ -3777,7 +3639,6 @@ function classifyPlatformWorkerDeploymentLineage(deployments, installedVersionId
3777
3639
  };
3778
3640
  }
3779
3641
  async function inspectNewPlatformWorker(client, accountId, manifest, resourceKey, workerName) {
3780
- if (!client.listWorkerNames || !client.getWorkerPlainTextBindings) throw new Error("platform: Cloudflare Worker ownership inspection is unavailable.");
3781
3642
  if (!(await client.listWorkerNames(accountId)).includes(workerName)) return "missing";
3782
3643
  const bindings = await client.getWorkerPlainTextBindings(accountId, workerName);
3783
3644
  let marker;
@@ -3826,7 +3687,7 @@ async function readPlatformWorkerVersionSnapshot(configPath, accountId, workerNa
3826
3687
  const resources = value.resources;
3827
3688
  const bindings = resources && typeof resources === "object" && !Array.isArray(resources) ? resources.bindings : null;
3828
3689
  const ownershipBindings = Array.isArray(bindings) ? bindings.filter((binding) => binding && typeof binding === "object" && binding.name === "PLATFORM_OWNERSHIP_MARKER" && binding.type === "plain_text" && typeof binding.text === "string") : [];
3829
- if (ownershipBindings.length !== 1 || ownershipBindings[0].text !== expectedOwnershipMarker) throw new Error(`platform: Active Worker version ${versionId} for ${workerName} does not belong to this installation.`);
3690
+ if (ownershipBindings.length !== 1 || ownershipBindings[0].text !== expectedOwnershipMarker) throw new PlatformWorkerOwnershipError(`platform: Active Worker version ${versionId} for ${workerName} does not belong to this installation.`);
3830
3691
  await validateCurrentVersion?.();
3831
3692
  const annotations = value.annotations;
3832
3693
  const message = annotations && typeof annotations === "object" && typeof annotations["workers/message"] === "string" ? annotations["workers/message"] ?? null : null;
@@ -4033,7 +3894,7 @@ function resolvePlatformPreviousTriggerConfig(versionSnapshot, configuredSnapsho
4033
3894
  //#endregion
4034
3895
  //#region src/cli/platform-trigger-proof.ts
4035
3896
  function record(value) {
4036
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3897
+ return isRecord(value) ? value : null;
4037
3898
  }
4038
3899
  function sameStrings(actual, expected) {
4039
3900
  const sortedExpected = [...expected].sort();
@@ -4122,7 +3983,23 @@ async function platformWorkerLiveTriggersMatch(options) {
4122
3983
  }
4123
3984
  //#endregion
4124
3985
  //#region src/cli/platform-installer-workers.ts
4125
- async function recoverInterruptedPlatformWorkerCutover(manifest, interruptedCutover, workerActions, configs, previousConfigs, plan, dependencies, ids, zoneId, previousPublicEndpoints, hasEmailGatewayArtifact, runPinnedPlatformWrangler, now, writeRemoteInstallationCheckpoint, onManifestChange) {
3986
+ function platformWorkerResourceKey(key) {
3987
+ return key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`;
3988
+ }
3989
+ /** Callbacks shared by a platform Worker deploy and its interrupted-cutover rollback. */
3990
+ function platformWorkerTrafficCallbacks(input) {
3991
+ const { client, accountId, ids, key, workerName, readManifest } = input;
3992
+ return {
3993
+ inspectNewWorker: () => inspectNewPlatformWorker(client, accountId, readManifest(), platformWorkerResourceKey(key), workerName),
3994
+ publishCustomDomains: (name, origins) => client.publishWorkerCustomDomainsSafely(accountId, name, origins),
3995
+ retainNewWorker: async () => {
3996
+ await client.putKvValue(accountId, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
3997
+ }
3998
+ };
3999
+ }
4000
+ async function recoverInterruptedPlatformWorkerCutover(input) {
4001
+ const { interruptedCutover, configs, previousConfigs, plan, dependencies, ids, previousPublicEndpoints, hasEmailGatewayArtifact, runPinnedPlatformWrangler, now, writeRemoteInstallationCheckpoint, onManifestChange } = input;
4002
+ let { manifest, workerActions } = input;
4126
4003
  if (interruptedCutover) {
4127
4004
  for (const key of [
4128
4005
  "dispatch",
@@ -4163,15 +4040,14 @@ async function recoverInterruptedPlatformWorkerCutover(manifest, interruptedCuto
4163
4040
  previousTriggers: mergePreservedTriggerRoutes(withoutPlatformCustomDomainTriggers(snapshot.previousTriggers), preservedRoutes),
4164
4041
  previousVersionId: snapshot.previousVersionId,
4165
4042
  preserveCustomDomains: interruptedCutover.unsafeTraffic.has(key),
4166
- inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`, snapshot.workerName),
4167
- removeCustomDomains: async () => {
4168
- if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
4169
- await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(manifest, /* @__PURE__ */ new Set([snapshot.workerName])));
4170
- },
4171
- publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
4172
- retainNewWorker: async () => {
4173
- await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
4174
- },
4043
+ ...platformWorkerTrafficCallbacks({
4044
+ client: dependencies.client,
4045
+ accountId: plan.account.id,
4046
+ ids,
4047
+ key,
4048
+ workerName: snapshot.workerName,
4049
+ readManifest: () => manifest
4050
+ }),
4175
4051
  runner: runPinnedPlatformWrangler,
4176
4052
  workerExisted: snapshot.previousVersionId !== null,
4177
4053
  workerName: snapshot.workerName
@@ -4180,15 +4056,10 @@ async function recoverInterruptedPlatformWorkerCutover(manifest, interruptedCuto
4180
4056
  }
4181
4057
  manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "restored", now());
4182
4058
  manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, previousPublicEndpoints && interruptedCutover.unsafeTraffic.size === 0, now());
4183
- let retainedNewWorker = false;
4184
- for (const [, snapshot] of interruptedCutover.staged) {
4185
- if (snapshot.previousVersionId !== null) continue;
4186
- retainedNewWorker = true;
4187
- }
4188
4059
  onManifestChange(manifest);
4189
4060
  writeInstallationManifest(manifest);
4190
4061
  await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
4191
- if (retainedNewWorker) {
4062
+ if ([...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null)) {
4192
4063
  if (!dependencies.replanWorkers) throw new Error("platform: Recovering retained Workers requires a fresh Cloudflare installation plan.");
4193
4064
  workerActions = validateReplannedWorkerActions(plan, manifest, await dependencies.replanWorkers(manifest)).filter((action) => hasEmailGatewayArtifact || action.key !== "email-gateway-worker");
4194
4065
  }
@@ -4202,7 +4073,7 @@ let cachedPlatformUploaderHash;
4202
4073
  function platformWorkerUploaderHash() {
4203
4074
  if (cachedPlatformUploaderHash !== void 0) return cachedPlatformUploaderHash;
4204
4075
  try {
4205
- const launcher = resolveCloudflareOperationArgv(process.cwd())[1];
4076
+ const launcher = resolveCloudflareOperationArgv()[1];
4206
4077
  if (!launcher) return cachedPlatformUploaderHash = null;
4207
4078
  const digest = createHash("sha256");
4208
4079
  digest.update("platform-uploader:v1\0");
@@ -4376,10 +4247,6 @@ async function deployPlatformWorker(config, secrets, options) {
4376
4247
  knownTriggerConfigs: { [versionId]: nextTriggers },
4377
4248
  manageRoutes: false,
4378
4249
  previousTriggerConfig: triggerConfig,
4379
- publishEmptyCustomDomains: async () => {
4380
- await options.removeCustomDomains();
4381
- return cleanPlatformCommandResult();
4382
- },
4383
4250
  runWrangler: platformWorkerCommandRunner(accountId, directory, runner),
4384
4251
  targetTriggerConfig: nextTriggers,
4385
4252
  targetVersionId: versionId,
@@ -4401,7 +4268,7 @@ async function deployPlatformWorker(config, secrets, options) {
4401
4268
  rollback: null,
4402
4269
  unsafeTraffic: true
4403
4270
  });
4404
- throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} traffic reconciliation failed: ${errorMessage(error)}${failures.length > 0 ? ` Safety recovery also failed: ${failures.join("; ")}` : ""}`, { cause: error });
4271
+ throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} traffic reconciliation failed: ${getErrorMessage(error)}${failures.length > 0 ? ` Safety recovery also failed: ${failures.join("; ")}` : ""}`, { cause: error });
4405
4272
  }
4406
4273
  };
4407
4274
  if (options.workerExisted && contentHash && reuseMatchingVersion && options.readCloudflare) {
@@ -4419,7 +4286,7 @@ async function deployPlatformWorker(config, secrets, options) {
4419
4286
  snapshot
4420
4287
  };
4421
4288
  } catch (error) {
4422
- if (error instanceof Error && (error.message.includes("changed concurrently") || error.message.includes("does not belong to this installation"))) throw error;
4289
+ if (error instanceof PlatformWorkerOwnershipError) throw error;
4423
4290
  }
4424
4291
  if (changedDuringRead) throw new Error(`platform: Worker ${workerName} changed concurrently while its deployment lineage was inspected.`);
4425
4292
  if (candidate && platformWorkerUploadFingerprint(config, options.managedRoutes) === contentHash) {
@@ -4528,7 +4395,7 @@ async function deployPlatformWorker(config, secrets, options) {
4528
4395
  rollback: null,
4529
4396
  unsafeTraffic: true
4530
4397
  });
4531
- throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} changed concurrently during activation, and restoring intervening deployment ${interveningDeployment.id} failed: ${errorMessage(recoveryError)}${recoveryFailures.length > 0 ? ` Safety recovery also failed: ${recoveryFailures.join("; ")}` : ""}`, { cause: recoveryError });
4398
+ throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} changed concurrently during activation, and restoring intervening deployment ${interveningDeployment.id} failed: ${getErrorMessage(recoveryError)}${recoveryFailures.length > 0 ? ` Safety recovery also failed: ${recoveryFailures.join("; ")}` : ""}`, { cause: recoveryError });
4532
4399
  }
4533
4400
  throw new Error(`platform: Worker ${workerName} changed concurrently during activation. The intervening deployment was restored without deleting deployment history.`);
4534
4401
  }
@@ -4540,10 +4407,6 @@ async function deployPlatformWorker(config, secrets, options) {
4540
4407
  },
4541
4408
  manageRoutes: false,
4542
4409
  previousTriggerConfig: previousTriggers,
4543
- publishEmptyCustomDomains: async () => {
4544
- await options.removeCustomDomains();
4545
- return cleanPlatformCommandResult();
4546
- },
4547
4410
  runWrangler: platformWorkerCommandRunner(accountId, directory, readRunner),
4548
4411
  targetTriggerConfig: nextTriggers,
4549
4412
  targetVersionId: uploadedVersionId,
@@ -4565,7 +4428,6 @@ async function deployPlatformWorker(config, secrets, options) {
4565
4428
  previousVersionId,
4566
4429
  preserveCustomDomains: false,
4567
4430
  inspectNewWorker: options.inspectNewWorker,
4568
- removeCustomDomains: options.removeCustomDomains,
4569
4431
  publishCustomDomains: options.publishCustomDomains,
4570
4432
  retainNewWorker: options.retainNewWorker,
4571
4433
  runner: readRunner,
@@ -4584,7 +4446,7 @@ async function deployPlatformWorker(config, secrets, options) {
4584
4446
  activatedDeploymentId,
4585
4447
  activatedVersionId: rollbackVersionId,
4586
4448
  config,
4587
- nextTriggers: readTriggerConfigFromObject(nextTraffic.wranglerConfig),
4449
+ nextTriggers,
4588
4450
  previousConfig: options.previousConfig,
4589
4451
  previousCustomDomains: previousTraffic.customDomains,
4590
4452
  previousDeployment,
@@ -4592,7 +4454,6 @@ async function deployPlatformWorker(config, secrets, options) {
4592
4454
  previousVersionId,
4593
4455
  preserveCustomDomains: unsafeTraffic,
4594
4456
  inspectNewWorker: options.inspectNewWorker,
4595
- removeCustomDomains: options.removeCustomDomains,
4596
4457
  publishCustomDomains: options.publishCustomDomains,
4597
4458
  retainNewWorker: options.retainNewWorker,
4598
4459
  runner: readRunner,
@@ -4601,8 +4462,8 @@ async function deployPlatformWorker(config, secrets, options) {
4601
4462
  }) : null,
4602
4463
  unsafeTraffic
4603
4464
  });
4604
- if (recoveryFailures.length > 0) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${errorMessage(error)} Automatic recovery for ${workerName} was incomplete: ${recoveryFailures.join("; ")}`, { cause: error });
4605
- if (unsafeTraffic) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${errorMessage(error)} The routing gate remains closed and live Custom Domains were retained for safe reconciliation on retry.`, { cause: error });
4465
+ if (recoveryFailures.length > 0) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${getErrorMessage(error)} Automatic recovery for ${workerName} was incomplete: ${recoveryFailures.join("; ")}`, { cause: error });
4466
+ if (unsafeTraffic) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${getErrorMessage(error)} The routing gate remains closed and live Custom Domains were retained for safe reconciliation on retry.`, { cause: error });
4606
4467
  throw error;
4607
4468
  } finally {
4608
4469
  rmSync(directory, {
@@ -4617,7 +4478,7 @@ async function recoverFailedPlatformWorkerDeployment(input) {
4617
4478
  try {
4618
4479
  await operation();
4619
4480
  } catch (error) {
4620
- failures.push(`${label}: ${errorMessage(error)}`);
4481
+ failures.push(`${label}: ${getErrorMessage(error)}`);
4621
4482
  }
4622
4483
  };
4623
4484
  if (input.unsafeTraffic) {
@@ -4639,7 +4500,6 @@ async function rollbackPlatformWorker(input) {
4639
4500
  if (currentVersionId !== input.activatedVersionId) throw new Error(`Worker ${input.workerName} changed concurrently from installer version ${input.activatedVersionId} to ${currentVersionId ?? "no active version"}; refusing to delete it.`);
4640
4501
  if (await input.inspectNewWorker() === "missing") return;
4641
4502
  await input.retainNewWorker();
4642
- if (!input.preserveCustomDomains) await input.removeCustomDomains();
4643
4503
  return;
4644
4504
  }
4645
4505
  const currentVersionId = await readActivePlatformWorkerVersion(configPath, input.accountId, input.workerName, input.runner);
@@ -4672,10 +4532,6 @@ async function rollbackPlatformWorker(input) {
4672
4532
  },
4673
4533
  manageRoutes: false,
4674
4534
  previousTriggerConfig: input.nextTriggers,
4675
- publishEmptyCustomDomains: async () => {
4676
- await input.removeCustomDomains();
4677
- return cleanPlatformCommandResult();
4678
- },
4679
4535
  runWrangler: platformWorkerCommandRunner(input.accountId, directory, input.runner),
4680
4536
  targetTriggerConfig: input.previousTriggers,
4681
4537
  targetVersionId: input.previousVersionId,
@@ -4708,149 +4564,509 @@ async function restorePlatformWorkerDeployment(input) {
4708
4564
  if (!samePlatformWorkerTraffic(after[0], input.restoration) || after[1]?.id !== input.expectedActiveDeploymentId) throw new Error(`Worker ${input.workerName} deployment restoration was superseded or did not restore deployment ${input.restoration.id}.`);
4709
4565
  }
4710
4566
  //#endregion
4711
- //#region src/cli/platform-install-activation.ts
4712
- async function enableInstallationServices(input) {
4713
- const { checkpointOperation, requiresStagedCommit, authenticationCutoverCommitted, now, writeRemoteInstallationCheckpoint, dependencies, plan, secrets, workerKeys, workerActions, workerDeploymentSecrets, zoneId, enabledConfigs, configs, cloudflareApiBaseUrl, managedRoutes, ids, runPinnedPlatformWrangler, shouldBootstrapSharedEmail, releaseDatabaseLock, wasDisabled, previousPublicEndpoints, version, targetUsageProtocolVersion, healthCheckContext, runtime, runtimeProvenance } = input;
4714
- await checkpointOperation("enabling");
4715
- if (requiresStagedCommit) {
4716
- if (!authenticationCutoverCommitted) {
4717
- input.manifest = recordWorkerCutoverResolution(input.manifest, input.workerCutoverGeneration, "committed", now());
4718
- input.manifest = recordWorkerTrafficState(input.manifest, input.workerCutoverGeneration, false, now());
4719
- writeInstallationManifest(input.manifest);
4720
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
4721
- input.workerRollbacks = [];
4722
- }
4723
- input.workerCutoverGeneration = nextWorkerCutoverGeneration(input.manifest);
4724
- if (input.workerSecretUpdateIntent) {
4725
- input.manifest = recordWorkerSecretUpdateState(input.manifest, input.workerCutoverGeneration, "pending", input.workerSecretUpdateIntent.id, now());
4726
- writeInstallationManifest(input.manifest);
4727
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
4728
- input.workerSecretUpdateIntent = {
4729
- ...input.workerSecretUpdateIntent,
4730
- generation: input.workerCutoverGeneration
4731
- };
4732
- writePendingInstallationSecrets(input.manifest, secretCheckpoint(secrets, input.workerSecretUpdateIntent));
4733
- }
4734
- for (const key of workerKeys) {
4735
- dependencies.onProgress?.(`Enabling the ${key} service`);
4736
- const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
4737
- const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
4738
- const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
4739
- const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
4740
- const previousWorkerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
4741
- const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
4742
- apiBaseUrl: cloudflareApiBaseUrl,
4743
- ...dependencies.client.request ? { readCloudflare: (path) => dependencies.client.request(path) } : {},
4744
- onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
4745
- input.manifest = recordWorkerCutoverStage(input.manifest, input.workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
4746
- writeInstallationManifest(input.manifest);
4747
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
4748
- },
4749
- onTrafficPublished: async () => {
4750
- if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
4751
- const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
4752
- const next = recordPlatformWorkerTraffic(input.manifest, action.name, {
4753
- customDomains,
4754
- routes
4755
- }, now());
4756
- if (JSON.stringify(next.resources) !== JSON.stringify(input.manifest.resources)) {
4757
- input.manifest = next;
4758
- writeInstallationManifest(input.manifest);
4759
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
4760
- }
4761
- },
4762
- onUnsafeTraffic: async () => {
4763
- input.manifest = recordWorkerUnsafeTraffic(input.manifest, input.workerCutoverGeneration, key, action.name, now());
4764
- writeInstallationManifest(input.manifest);
4765
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
4766
- },
4767
- managedRoutes: managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
4768
- preservedRoutes,
4769
- publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
4770
- publishRoutes: async (workerName, patterns) => {
4771
- if (!dependencies.client.publishWorkerRoutesSafely) throw new Error("platform: Safe Cloudflare Worker route publication is unavailable.");
4772
- if (zoneId) await dependencies.client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
4773
- else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
4774
- },
4775
- previousConfig: previousWorkerConfig,
4776
- previousPublicEndpoints: true,
4777
- inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, input.manifest, action.key, action.name),
4778
- removeCustomDomains: async () => {
4779
- if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
4780
- await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(input.manifest, /* @__PURE__ */ new Set([action.name])));
4781
- },
4782
- retainNewWorker: async () => {
4783
- await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
4784
- },
4785
- reuseMatchingVersion: false,
4786
- runner: runPinnedPlatformWrangler,
4787
- workerExisted: true
4567
+ //#region src/cli/platform-install-workers.ts
4568
+ function platformWorkerAction(workerActions, key) {
4569
+ return workerActions.find((entry) => entry.key === platformWorkerResourceKey(key));
4570
+ }
4571
+ /** Deploy callbacks shared by the upload and enable phases for one platform Worker. */
4572
+ function platformWorkerDeployCallbacks(input) {
4573
+ const { client, plan, zoneId, ids, key, action, generation, now, readManifest, writeManifest, checkpoint } = input;
4574
+ return {
4575
+ readCloudflare: (path) => client.request(path),
4576
+ onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
4577
+ await checkpoint(async () => {
4578
+ await writeManifest(recordWorkerCutoverStage(readManifest(), generation, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now()));
4579
+ });
4580
+ },
4581
+ onTrafficPublished: async () => {
4582
+ const [customDomains, routes] = await Promise.all([client.listWorkerDomains(plan.account.id), zoneId ? client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
4583
+ await checkpoint(async () => {
4584
+ const manifest = readManifest();
4585
+ const next = recordPlatformWorkerTraffic(manifest, action.name, {
4586
+ customDomains,
4587
+ routes
4588
+ }, now());
4589
+ if (JSON.stringify(next.resources) !== JSON.stringify(manifest.resources)) await writeManifest(next);
4788
4590
  });
4789
- if (rollback) input.workerRollbacks.push(rollback);
4591
+ },
4592
+ onUnsafeTraffic: async () => {
4593
+ await checkpoint(async () => {
4594
+ await writeManifest(recordWorkerUnsafeTraffic(readManifest(), generation, key, action.name, now()));
4595
+ }, true);
4596
+ },
4597
+ ...platformWorkerTrafficCallbacks({
4598
+ client,
4599
+ accountId: plan.account.id,
4600
+ ids,
4601
+ key,
4602
+ workerName: action.name,
4603
+ readManifest
4604
+ }),
4605
+ publishRoutes: async (workerName, patterns) => {
4606
+ if (zoneId) await client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
4607
+ else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
4790
4608
  }
4791
- }
4792
- const verifyCurrentWorkerCutover = async () => {
4793
- if (!requiresStagedCommit) return;
4794
- if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(input.manifest, input.workerCutoverGeneration);
4795
- else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(input.manifest, input.workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler, workerKeys);
4796
- };
4797
- await verifyCurrentWorkerCutover();
4798
- let emailOperationId;
4799
- if (shouldBootstrapSharedEmail) {
4800
- input.workerDeploymentCommitted = true;
4801
- input.manifest = recordWorkerCutoverResolution(input.manifest, input.workerCutoverGeneration, "committed", now());
4802
- if (input.workerSecretUpdateIntent) input.manifest = recordWorkerSecretUpdateState(input.manifest, input.workerCutoverGeneration, "committed", input.workerSecretUpdateIntent.id, now());
4803
- writeInstallationManifest(input.manifest);
4804
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
4805
- input.workerRollbacks = [];
4806
- dependencies.onProgress?.("Preparing shared email routing");
4807
- emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(input.manifest.ownershipNonce));
4808
- }
4809
- await releaseDatabaseLock.liftProjectZeroTrustFence?.();
4810
- if (dependencies.client.deleteKvValue) await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
4811
- else if (wasDisabled || !previousPublicEndpoints) throw new Error("platform: Routing disablement removal is unavailable.");
4812
- if (requiresStagedCommit) {
4813
- dependencies.onProgress?.("Verifying the enabled platform");
4814
- await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, input.manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(input.manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ? `https://${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev` : void 0, healthCheckContext);
4815
- await verifyCurrentWorkerCutover();
4816
- }
4817
- input.workerDeploymentCommitted = true;
4818
- input.manifest = recordWorkerCutoverResolution(input.manifest, input.workerCutoverGeneration, "committed", now());
4819
- input.manifest = recordWorkerTrafficState(input.manifest, input.workerCutoverGeneration, true, now());
4820
- if (input.workerSecretUpdateIntent) input.manifest = recordWorkerSecretUpdateState(input.manifest, input.workerCutoverGeneration, "committed", input.workerSecretUpdateIntent.id, now());
4821
- input.manifest = {
4822
- ...input.manifest,
4823
- deployment: plan.deployment,
4824
- deploymentExecution: runtime.deploymentExecution,
4825
- platformVersion: version,
4826
- runtime: runtimeProvenance,
4827
- safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
4828
- updatedAt: now(),
4829
- usageProtocolVersion: targetUsageProtocolVersion
4830
4609
  };
4831
- input.manifest = transitionInstallation(input.manifest, "installed", now());
4832
- return { emailOperationId };
4833
- }
4834
- //#endregion
4835
- //#region src/cli/platform-access-health.ts
4836
- /** These reserved endpoints require Void's probe token and cannot serve application traffic. */
4837
- function platformAccessHealthTargets(plan) {
4838
- const destination = `${plan.deployment.application.zoneName ? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${plan.deployment.application.baseDomain}` : `${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev`}/health`;
4839
- return [{
4840
- key: `health-dispatch-${createHash("sha256").update(destination).digest("hex").slice(0, 12)}`,
4841
- destination
4842
- }];
4843
- }
4844
- async function verifyHealthException(access, app, destination) {
4845
- const destinations = platformAccessPublicDestinations(app);
4846
- if (typeof app.id !== "string" || !app.id || app.type !== "self_hosted" || app.domain !== destination || !destinations.length || destinations.some((value) => value !== destination) || app.destinations !== void 0 && (!Array.isArray(app.destinations) || app.destinations.some((value) => !value || value.type !== "public" || value.uri !== destination)) || app.target_criteria != null && (!Array.isArray(app.target_criteria) || app.target_criteria.length > 0)) throw new Error(`platform: The Access health exception must cover only ${destination}.`);
4847
- const policies = await access.list(`apps/${encodeURIComponent(app.id)}/policies`);
4848
- const policy = policies[0];
4849
- if (policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== "[{\"everyone\":{}}]" || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) throw new Error(`platform: The Access policy for ${destination} does not match the reserved health-check exception.`);
4850
4610
  }
4851
- /** Keep the same path-only exception used by project Zero Trust available during bootstrap. */
4852
- async function ensurePlatformAccessHealthChecks(input) {
4853
- const appAccess = !input.manifest.steps.some((step) => step.key === "app-access-initialized:v1") && input.checkpoint.appAccessSetupJson ? parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson)) : null;
4611
+ async function uploadInstallationWorkers(input) {
4612
+ const { state, dependencies, requiresStagedCommit, recoveringUnsafeTraffic, workerActions, workerKeys, workerDeploymentSecrets, zoneId, now, writeRemoteInstallationCheckpoint, configs, previousConfigs, cloudflareApiBaseUrl, preserveDisabled, managedRoutes, previousPublicEndpoints, ids, runParallelPinnedPlatformWrangler, runPinnedPlatformWrangler } = input;
4613
+ const { plan, workerSecretUpdateIntent, workerCutoverGeneration, workerRollbacks } = state;
4614
+ const parallelWorkerUploads = !dependencies.deployWorker && !requiresStagedCommit && !workerSecretUpdateIntent && !recoveringUnsafeTraffic && workerActions.length === workerKeys.length && workerActions.every((action) => action.action === "update");
4615
+ let workerManifestQueue = Promise.resolve();
4616
+ let workerManifestFailed = false;
4617
+ let workerManifestFailure;
4618
+ const checkpointWorkerManifest = (update, safetyRecovery = false) => {
4619
+ if (!parallelWorkerUploads) return update();
4620
+ const result = workerManifestQueue.then(async () => {
4621
+ if (workerManifestFailed && !safetyRecovery) throw workerManifestFailure;
4622
+ try {
4623
+ await update();
4624
+ } catch (error) {
4625
+ workerManifestFailed = true;
4626
+ workerManifestFailure = error;
4627
+ throw error;
4628
+ }
4629
+ });
4630
+ workerManifestQueue = result.then(() => void 0, () => void 0);
4631
+ return result;
4632
+ };
4633
+ const writeManifest = async (next) => {
4634
+ state.manifest = next;
4635
+ writeInstallationManifest(next);
4636
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, next);
4637
+ };
4638
+ let availableUploadSlots = 2;
4639
+ let uploadFailure;
4640
+ const uploadWaiters = [];
4641
+ const acquireUploadSlot = async () => {
4642
+ if (uploadFailure !== void 0) throw uploadFailure;
4643
+ if (availableUploadSlots === 0) await new Promise((resolve, reject) => uploadWaiters.push({
4644
+ resolve,
4645
+ reject
4646
+ }));
4647
+ else availableUploadSlots--;
4648
+ return (failure) => {
4649
+ if (failure !== void 0 && uploadFailure === void 0) {
4650
+ uploadFailure = failure;
4651
+ for (const waiter of uploadWaiters.splice(0)) waiter.reject(failure);
4652
+ }
4653
+ const next = uploadFailure === void 0 ? uploadWaiters.shift() : void 0;
4654
+ if (next) next.resolve();
4655
+ else availableUploadSlots++;
4656
+ };
4657
+ };
4658
+ const workerReady = (parallelWorkerUploads ? workerKeys : []).map(() => {
4659
+ let resolve;
4660
+ let reject;
4661
+ return {
4662
+ promise: new Promise((onResolve, onReject) => {
4663
+ resolve = onResolve;
4664
+ reject = onReject;
4665
+ }),
4666
+ resolve,
4667
+ reject
4668
+ };
4669
+ });
4670
+ const uploadBatch = Promise.all(workerReady.map((ready) => ready.promise)).then(() => ({ error: null }), (error) => ({ error }));
4671
+ const deployOneWorker = async (key, waitForPriorWorker, onReadyForActivation) => {
4672
+ dependencies.onProgress?.(`Deploying the ${key} service`);
4673
+ let workerStepStarted = performance.now();
4674
+ const markWorkerStep = (step) => {
4675
+ writePlatformTiming(`worker ${key} ${step}`, workerStepStarted);
4676
+ workerStepStarted = performance.now();
4677
+ };
4678
+ const action = platformWorkerAction(workerActions, key);
4679
+ const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
4680
+ await checkpointWorkerManifest(async () => {
4681
+ const intended = recordProvisionIntent(state.manifest, action, now());
4682
+ if (intended !== state.manifest) await writeManifest(intended);
4683
+ });
4684
+ await dependencies.revalidateBeforeWorkerDeploy(state.manifest, action);
4685
+ markWorkerStep("revalidate");
4686
+ const preservedRoutes = dependencies.deployWorker ? [] : action.action === "create" ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
4687
+ markWorkerStep("traffic inventory");
4688
+ const workerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
4689
+ const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
4690
+ const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
4691
+ ...platformWorkerDeployCallbacks({
4692
+ client: dependencies.client,
4693
+ plan,
4694
+ zoneId,
4695
+ ids,
4696
+ key,
4697
+ action,
4698
+ generation: workerCutoverGeneration,
4699
+ now,
4700
+ readManifest: () => state.manifest,
4701
+ writeManifest,
4702
+ checkpoint: checkpointWorkerManifest
4703
+ }),
4704
+ apiBaseUrl: cloudflareApiBaseUrl,
4705
+ operatorApiToken: dependencies.operatorApiToken,
4706
+ ...parallelWorkerUploads ? {
4707
+ acquireUploadSlot,
4708
+ onReadyForActivation,
4709
+ waitForPriorWorker
4710
+ } : {},
4711
+ managedRoutes: preserveDisabled ? [] : managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
4712
+ preservedRoutes,
4713
+ previousConfig: previousWorkerConfig,
4714
+ previousPublicEndpoints,
4715
+ runner: parallelWorkerUploads ? runParallelPinnedPlatformWrangler : runPinnedPlatformWrangler,
4716
+ workerExisted: action.action !== "create"
4717
+ });
4718
+ markWorkerStep("deploy");
4719
+ if (rollback) workerRollbacks.push(rollback);
4720
+ await checkpointWorkerManifest(async () => {
4721
+ const recorded = recordResource(state.manifest, action, action.name, "created", now());
4722
+ if (rollback || dependencies.deployWorker || JSON.stringify(recorded.resources) !== JSON.stringify(state.manifest.resources)) await writeManifest(recorded);
4723
+ });
4724
+ markWorkerStep("record");
4725
+ };
4726
+ if (parallelWorkerUploads) {
4727
+ const tasks = [];
4728
+ for (const [index, key] of workerKeys.entries()) {
4729
+ const priorWorker = tasks.at(-1);
4730
+ const ready = workerReady[index];
4731
+ const task = deployOneWorker(key, priorWorker ? () => priorWorker : void 0, async () => {
4732
+ ready.resolve();
4733
+ const batch = await uploadBatch;
4734
+ if (batch.error !== null) throw batch.error;
4735
+ });
4736
+ task.then(() => ready.resolve(), (error) => ready.reject(error));
4737
+ tasks.push(task);
4738
+ }
4739
+ const firstFailure = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
4740
+ if (firstFailure) throw firstFailure.reason;
4741
+ } else for (const key of workerKeys) await deployOneWorker(key);
4742
+ }
4743
+ //#endregion
4744
+ //#region src/cli/platform-installer-health.ts
4745
+ var PlatformAccessBlockedError = class extends Error {
4746
+ constructor(url, context) {
4747
+ super(context?.accessProtected ? [
4748
+ "platform: Cloudflare Access blocked a protected platform health check (Default-Deny, error 1050).",
4749
+ `Blocked URL: ${url}`,
4750
+ "Keep employee Access protection and account-wide Default-Deny enabled.",
4751
+ "The reserved dispatch /health endpoint uses a path-only Access exception and still requires Void’s private probe token.",
4752
+ "Resume to verify the health exceptions and saved installer credentials.",
4753
+ ...context.resumeCommand ? [`Resume: ${context.resumeCommand}`] : [],
4754
+ "https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
4755
+ ].join("\n") : [
4756
+ "platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
4757
+ `Blocked URL: ${url}`,
4758
+ "",
4759
+ "To make this test platform publicly reachable:",
4760
+ "",
4761
+ `1. In ${context ? `account ${JSON.stringify(context.accountName)}` : "your Cloudflare account"}, open:`,
4762
+ " Zero Trust → Access controls → Applications → Create new application.",
4763
+ "2. Choose Self-hosted. When adding destinations, choose Worker and select:",
4764
+ ` ${context ? context.workers.join(", ") + "." : "This installation's API, proxy, and dispatch Workers."}`,
4765
+ "3. Add a policy: Action → Bypass; Include → Everyone.",
4766
+ context?.resumeCommand ? `4. Save the policy and application, then resume:\n\n ${context.resumeCommand}` : "4. Save the policy and application, then rerun your platform command.",
4767
+ "",
4768
+ "This removes Cloudflare Access protection for these Workers.",
4769
+ "Void’s own authentication stays enabled. Keep account-wide Default-Deny on.",
4770
+ "",
4771
+ "For platforms that must remain Access-protected:",
4772
+ "https://void.cloud/guide/platform/installation/maintenance#cloudflare-access"
4773
+ ].join("\n"));
4774
+ }
4775
+ };
4776
+ async function platformProbeFailure(label, url, response, context) {
4777
+ const message = `platform: ${label} failed at ${url} (HTTP ${response.status}).`;
4778
+ if (response.status === 403 && response.headers.get("server")?.toLowerCase() === "cloudflare" && response.headers.get("content-type")?.toLowerCase().includes("text/html")) {
4779
+ const body = await response.text().catch(() => "");
4780
+ if (body.includes("Default-Deny") && /errorCode:\s*1050\b/.test(body)) return new PlatformAccessBlockedError(url, context);
4781
+ }
4782
+ return new Error(message);
4783
+ }
4784
+ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicationBaseDomain, platformVersion, usageProtocolVersion, healthProbeToken, fetcher = fetch, dispatchOrigin, context) {
4785
+ const request = (url) => fetcher(url, {
4786
+ headers: {
4787
+ "X-Void-Platform-Probe": healthProbeToken,
4788
+ ...cfAccessHeaders(process.env, url)
4789
+ },
4790
+ redirect: "manual",
4791
+ signal: AbortSignal.timeout(3e4)
4792
+ });
4793
+ const dispatchUrl = dispatchOrigin ?? `https://${PLATFORM_HEALTH_HOSTNAME_LABEL}.${applicationBaseDomain}`;
4794
+ const [health, discovery, proxy, dispatch, emailGateway] = await Promise.all([
4795
+ request(`${apiUrl}/health`),
4796
+ request(`${apiUrl}/.well-known/void-platform`),
4797
+ request(`${proxyUrl}/health`),
4798
+ request(`${dispatchUrl}/health`),
4799
+ context?.emailGatewayUrl ? request(`${context.emailGatewayUrl}/health`) : void 0
4800
+ ]);
4801
+ if (!health.ok) throw await platformProbeFailure("Health check", `${apiUrl}/health`, health, context);
4802
+ if (!discovery.ok) throw await platformProbeFailure("Discovery check", `${apiUrl}/.well-known/void-platform`, discovery, context);
4803
+ const body = await discovery.json();
4804
+ if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
4805
+ if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
4806
+ if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
4807
+ if (!proxy.ok) throw await platformProbeFailure("Proxy health check", `${proxyUrl}/health`, proxy, context);
4808
+ if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", `${dispatchUrl}/health`, dispatch, context);
4809
+ if (emailGateway) {
4810
+ if (!emailGateway.ok) throw await platformProbeFailure("Email gateway health check", `${context.emailGatewayUrl}/health`, emailGateway, context);
4811
+ const identity = await emailGateway.json();
4812
+ if (identity?.installationId !== installationId || identity.platformVersion !== platformVersion || identity.emailProtocolVersion !== 1) throw new Error("platform: Deployed email gateway identity, version, or protocol does not match this installation.");
4813
+ }
4814
+ }
4815
+ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fetch) {
4816
+ const response = await fetcher(`${apiUrl}/internal/platform/sandbox-drain`, {
4817
+ headers: {
4818
+ "X-Void-Platform-Probe": healthProbeToken,
4819
+ ...cfAccessHeaders(process.env, apiUrl)
4820
+ },
4821
+ signal: AbortSignal.timeout(3e4),
4822
+ redirect: "manual"
4823
+ });
4824
+ if (!response.ok) throw new Error(`platform: Sandbox drain probe failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
4825
+ let value;
4826
+ try {
4827
+ value = await response.json();
4828
+ } catch {
4829
+ throw new Error("platform: Sandbox drain probe did not return its DB-backed protocol v1 document. Traffic remains disabled.");
4830
+ }
4831
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
4832
+ const status = value;
4833
+ const validState = status.state === "required" || status.state === "draining" || status.state === "complete";
4834
+ const validPhase = status.phase === null || status.phase === "deployments" || status.phase === "resources";
4835
+ if (status.version !== 1 || !validState || typeof status.complete !== "boolean" || !validPhase || status.generation !== null && (typeof status.generation !== "string" || status.generation.length === 0) || status.updatedAt !== null && (typeof status.updatedAt !== "string" || !Number.isFinite(Date.parse(status.updatedAt))) || status.state === "required" && (status.complete || status.generation !== null || status.updatedAt !== null || status.phase !== null) || status.state === "draining" && (status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null) || status.state === "complete" && (!status.complete || typeof status.generation !== "string" || typeof status.updatedAt !== "string" || status.phase === null)) throw new Error("platform: Sandbox drain probe returned an invalid DB-backed protocol v1 document. Traffic remains disabled.");
4836
+ return status.state === "complete";
4837
+ }
4838
+ async function verifyPlatformDeploymentExecutionDrain(apiUrl, healthProbeToken, installationId, fetcher = fetch) {
4839
+ const response = await fetcher(`${apiUrl}/__void/platform/deployment-execution`, {
4840
+ headers: {
4841
+ "X-Void-Platform-Probe": healthProbeToken,
4842
+ ...cfAccessHeaders(process.env, apiUrl)
4843
+ },
4844
+ signal: AbortSignal.timeout(3e4),
4845
+ redirect: "manual"
4846
+ });
4847
+ if (!response.ok) throw new Error(`platform: Deployment execution drain probe failed (HTTP ${response.status}). Keep the current runtime until its durable deployment operations have drained.`);
4848
+ let value;
4849
+ try {
4850
+ value = await response.json();
4851
+ } catch {
4852
+ throw new Error("platform: Deployment execution drain probe returned an invalid document.");
4853
+ }
4854
+ const status = value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
4855
+ if (status?.protocolVersion !== 1 || status.installationId !== installationId || typeof status.admissionClosed !== "boolean" || !Number.isSafeInteger(status.pendingDeployments) || Number(status.pendingDeployments) < 0) throw new Error("platform: Deployment execution drain probe returned an invalid document.");
4856
+ if (!status.admissionClosed || status.pendingDeployments !== 0) throw new Error("platform: Durable deployment operations are still admitted or unfinished. Keep the current runtime and retry this command after they finish.");
4857
+ }
4858
+ async function bootstrapPlatformSharedEmail(apiUrl, healthProbeToken, fetcher = fetch) {
4859
+ const deadline = Date.now() + 3e5;
4860
+ let operationId = null;
4861
+ const timedOut = () => /* @__PURE__ */ new Error(`platform: Shared email bootstrap did not finish within five minutes at ${apiUrl}.${operationId ? ` Operation ${operationId} remains incomplete.` : ""} Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
4862
+ const waitForProgress = async () => {
4863
+ const remaining = deadline - Date.now();
4864
+ if (remaining <= 0) throw timedOut();
4865
+ await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, remaining)));
4866
+ };
4867
+ let response;
4868
+ let value;
4869
+ let status;
4870
+ for (;;) {
4871
+ const remaining = deadline - Date.now();
4872
+ if (remaining <= 0) throw timedOut();
4873
+ const signal = AbortSignal.timeout(Math.min(12e4, remaining));
4874
+ try {
4875
+ response = await fetcher(`${apiUrl}/internal/platform/email-bootstrap`, {
4876
+ method: "POST",
4877
+ headers: {
4878
+ "X-Void-Platform-Probe": healthProbeToken,
4879
+ ...cfAccessHeaders(process.env, apiUrl)
4880
+ },
4881
+ signal,
4882
+ redirect: "manual"
4883
+ });
4884
+ } catch (error) {
4885
+ if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
4886
+ await waitForProgress();
4887
+ continue;
4888
+ }
4889
+ throw new Error(`platform: Shared email bootstrap request failed at ${apiUrl}. Traffic remains disabled. Retry the installation's repair or resume command to reconcile the existing email operation.`);
4890
+ }
4891
+ try {
4892
+ value = await response.json();
4893
+ } catch (error) {
4894
+ if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
4895
+ await waitForProgress();
4896
+ continue;
4897
+ }
4898
+ throw new Error(`platform: Shared email bootstrap failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
4899
+ }
4900
+ status = value && typeof value === "object" && !Array.isArray(value) ? value : null;
4901
+ const observedOperationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
4902
+ if (status?.version === 1 && observedOperationId) {
4903
+ if (operationId && operationId !== observedOperationId) throw new Error("platform: Shared email bootstrap operation identity changed while waiting. Traffic remains disabled. Inspect the installation before retrying.");
4904
+ operationId = observedOperationId;
4905
+ }
4906
+ if ([
4907
+ 200,
4908
+ 202,
4909
+ 503
4910
+ ].includes(response.status) && status?.version === 1 && status.state === "incomplete" && observedOperationId && (status.operationStatus === "pending" || status.operationStatus === "running") && status.failureCode == null) {
4911
+ await waitForProgress();
4912
+ continue;
4913
+ }
4914
+ break;
4915
+ }
4916
+ if (!response.ok || status?.version !== 1 || status.state !== "ready" || operationId === null || status.operationId !== operationId || status.operationStatus != null && status.operationStatus !== "completed" || status.failureCode != null) {
4917
+ const code = (value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value) ? value : null;
4918
+ const operationStatus = code(status?.operationStatus);
4919
+ const operationId = typeof status?.operationId === "string" && /^[A-Za-z0-9_-]{1,100}$/.test(status.operationId) ? status.operationId : null;
4920
+ const details = operationId ? [`Operation ${operationId}: ${operationStatus ?? "incomplete"}.`] : operationStatus ? [`Operation status: ${operationStatus}.`] : [];
4921
+ const currentStep = code(status?.currentStep);
4922
+ const failureCode = code(status?.failureCode);
4923
+ if (currentStep) details.push(`Step: ${currentStep}.`);
4924
+ if (failureCode) details.push(`Failure: ${failureCode}.`);
4925
+ let domain = null;
4926
+ if (typeof status?.domain === "string") try {
4927
+ domain = normalizeApplicationDomain(status.domain);
4928
+ } catch {}
4929
+ const inboundStatus = code(status?.inboundStatus);
4930
+ if (domain) details.push(`Domain: ${domain}${inboundStatus ? ` (inbound: ${inboundStatus})` : ""}.`);
4931
+ const mxDetail = domain ? `MX records for ${domain} do not route mail to Cloudflare Email Routing` : null;
4932
+ const safeDetails = [
4933
+ ...domain ? [mxDetail, `enabled explicit Cloudflare email rules conflict with ${domain}`] : [],
4934
+ "zone catch-all does not point to this ingress",
4935
+ "another enabled catch-all owns this zone"
4936
+ ];
4937
+ if (typeof status?.failureDetail === "string" && safeDetails.includes(status.failureDetail)) {
4938
+ details.push(`${status.failureDetail}.`);
4939
+ if (currentStep === "inbound_probe" && status.failureDetail === mxDetail) details.push(`Enable Email Routing for ${domain} in the Cloudflare dashboard, then resume this installation.`);
4940
+ }
4941
+ const detail = details.length ? ` ${details.join(" ")}` : "";
4942
+ throw new Error(`platform: Shared email bootstrap did not reach inbound readiness at ${apiUrl} (HTTP ${response.status}).${detail} Traffic remains disabled.`);
4943
+ }
4944
+ return operationId;
4945
+ }
4946
+ //#endregion
4947
+ //#region src/cli/platform-install-activation.ts
4948
+ async function enableInstallationServices(input) {
4949
+ const { state, checkpointOperation, requiresStagedCommit, authenticationCutoverCommitted, now, writeRemoteInstallationCheckpoint, dependencies, workerKeys, workerActions, workerDeploymentSecrets, zoneId, enabledConfigs, configs, cloudflareApiBaseUrl, managedRoutes, ids, runPinnedPlatformWrangler, shouldBootstrapSharedEmail, releaseDatabaseLock, version, targetUsageProtocolVersion, healthCheckContext, runtime, runtimeProvenance } = input;
4950
+ const { plan, secrets } = state;
4951
+ await checkpointOperation("enabling");
4952
+ if (requiresStagedCommit) {
4953
+ if (!authenticationCutoverCommitted) {
4954
+ state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
4955
+ state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
4956
+ writeInstallationManifest(state.manifest);
4957
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
4958
+ state.workerRollbacks = [];
4959
+ }
4960
+ state.workerCutoverGeneration = nextWorkerCutoverGeneration(state.manifest);
4961
+ if (state.workerSecretUpdateIntent) {
4962
+ state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "pending", state.workerSecretUpdateIntent.id, now());
4963
+ writeInstallationManifest(state.manifest);
4964
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
4965
+ state.workerSecretUpdateIntent = {
4966
+ ...state.workerSecretUpdateIntent,
4967
+ generation: state.workerCutoverGeneration
4968
+ };
4969
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(secrets, state.workerSecretUpdateIntent));
4970
+ }
4971
+ const writeManifest = async (next) => {
4972
+ state.manifest = next;
4973
+ writeInstallationManifest(next);
4974
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, next);
4975
+ };
4976
+ for (const key of workerKeys) {
4977
+ dependencies.onProgress?.(`Enabling the ${key} service`);
4978
+ const action = platformWorkerAction(workerActions, key);
4979
+ const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
4980
+ const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
4981
+ const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
4982
+ const previousWorkerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
4983
+ const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
4984
+ ...platformWorkerDeployCallbacks({
4985
+ client: dependencies.client,
4986
+ plan,
4987
+ zoneId,
4988
+ ids,
4989
+ key,
4990
+ action,
4991
+ generation: state.workerCutoverGeneration,
4992
+ now,
4993
+ readManifest: () => state.manifest,
4994
+ writeManifest,
4995
+ checkpoint: (update) => update()
4996
+ }),
4997
+ apiBaseUrl: cloudflareApiBaseUrl,
4998
+ managedRoutes: managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
4999
+ preservedRoutes,
5000
+ previousConfig: previousWorkerConfig,
5001
+ previousPublicEndpoints: true,
5002
+ reuseMatchingVersion: false,
5003
+ runner: runPinnedPlatformWrangler,
5004
+ workerExisted: true
5005
+ });
5006
+ if (rollback) state.workerRollbacks.push(rollback);
5007
+ }
5008
+ }
5009
+ const verifyCurrentWorkerCutover = async () => {
5010
+ if (!requiresStagedCommit) return;
5011
+ if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(state.manifest, state.workerCutoverGeneration);
5012
+ else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(state.manifest, state.workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler, workerKeys);
5013
+ };
5014
+ await verifyCurrentWorkerCutover();
5015
+ let emailOperationId;
5016
+ if (shouldBootstrapSharedEmail) {
5017
+ state.workerDeploymentCommitted = true;
5018
+ state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
5019
+ if (state.workerSecretUpdateIntent) state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "committed", state.workerSecretUpdateIntent.id, now());
5020
+ writeInstallationManifest(state.manifest);
5021
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
5022
+ state.workerRollbacks = [];
5023
+ dependencies.onProgress?.("Preparing shared email routing");
5024
+ emailOperationId = await (dependencies.bootstrapEmail ?? bootstrapPlatformSharedEmail)(plan.apiUrl, createPlatformHealthProbeToken(state.manifest.ownershipNonce));
5025
+ }
5026
+ await releaseDatabaseLock.liftProjectZeroTrustFence?.();
5027
+ await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
5028
+ if (requiresStagedCommit) {
5029
+ dependencies.onProgress?.("Verifying the enabled platform");
5030
+ await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, state.manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(state.manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ? platformWorkersDevUrl(plan, plan.deployment.resources.dispatchWorker) : void 0, healthCheckContext);
5031
+ await verifyCurrentWorkerCutover();
5032
+ }
5033
+ state.workerDeploymentCommitted = true;
5034
+ state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
5035
+ state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, true, now());
5036
+ if (state.workerSecretUpdateIntent) state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "committed", state.workerSecretUpdateIntent.id, now());
5037
+ state.manifest = {
5038
+ ...state.manifest,
5039
+ deployment: plan.deployment,
5040
+ deploymentExecution: runtime.deploymentExecution,
5041
+ platformVersion: version,
5042
+ runtime: runtimeProvenance,
5043
+ safetyProtocols: runtime.safetyProtocols,
5044
+ updatedAt: now(),
5045
+ usageProtocolVersion: targetUsageProtocolVersion
5046
+ };
5047
+ state.manifest = transitionInstallation(state.manifest, "installed", now());
5048
+ return { emailOperationId };
5049
+ }
5050
+ //#endregion
5051
+ //#region src/cli/platform-access-health.ts
5052
+ /** These reserved endpoints require Void's probe token and cannot serve application traffic. */
5053
+ function platformAccessHealthTargets(plan) {
5054
+ const destination = `${plan.deployment.application.zoneName ? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${plan.deployment.application.baseDomain}` : `${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev`}/health`;
5055
+ return [{
5056
+ key: `health-dispatch-${createHash("sha256").update(destination).digest("hex").slice(0, 12)}`,
5057
+ destination
5058
+ }];
5059
+ }
5060
+ async function verifyHealthException(access, app, destination) {
5061
+ const destinations = platformAccessPublicDestinations(app);
5062
+ if (typeof app.id !== "string" || !app.id || app.type !== "self_hosted" || app.domain !== destination || !destinations.length || destinations.some((value) => value !== destination) || app.destinations !== void 0 && (!Array.isArray(app.destinations) || app.destinations.some((value) => !value || value.type !== "public" || value.uri !== destination)) || app.target_criteria != null && (!Array.isArray(app.target_criteria) || app.target_criteria.length > 0)) throw new Error(`platform: The Access health exception must cover only ${destination}.`);
5063
+ const policies = await access.list(`apps/${encodeURIComponent(app.id)}/policies`);
5064
+ const policy = policies[0];
5065
+ if (policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== "[{\"everyone\":{}}]" || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) throw new Error(`platform: The Access policy for ${destination} does not match the reserved health-check exception.`);
5066
+ }
5067
+ /** Keep the same path-only exception used by project Zero Trust available during bootstrap. */
5068
+ async function ensurePlatformAccessHealthChecks(input) {
5069
+ const appAccess = !input.manifest.steps.some((step) => step.key === "app-access-initialized:v1") && input.checkpoint.appAccessSetupJson ? parsePlatformAppAccessSetup(JSON.parse(input.checkpoint.appAccessSetupJson)) : null;
4854
5070
  if (appAccess && appAccess.accountId !== input.plan.account.id) throw new Error("platform: App access recovery belongs to another Cloudflare account.");
4855
5071
  if (!input.plan.deployment.authentication.accessProtection && !appAccess?.managed) return;
4856
5072
  const spec = appAccess?.managed ? {
@@ -4949,7 +5165,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
4949
5165
  platformAccessResourceInventory(encoded, input.manifest.id);
4950
5166
  const state = JSON.parse(encoded);
4951
5167
  const spec = parsePlatformAccessSetupSpec(JSON.parse(specEncoded));
4952
- if (state.accountId !== (spec.accountId ?? input.manifest.account.id) || state.nonce !== createHash("sha256").update(`void-access:${input.manifest.ownershipNonce}`).digest("hex").slice(0, 32)) throw new Error("platform: The saved Access setup does not match this installation and identity account.");
5168
+ if (state.accountId !== (spec.accountId ?? input.manifest.account.id) || state.nonce !== platformAccessNonce(input.manifest.ownershipNonce)) throw new Error("platform: The saved Access setup does not match this installation and identity account.");
4953
5169
  const application = state.resources.protection;
4954
5170
  const service = state.resources["service-token"];
4955
5171
  if (!application?.id || application.pending || !service?.id || service.pending || spec.protectionApplicationId && spec.protectionApplicationId !== application.id) throw new Error("platform: Complete or restore the Access application and service-token checkpoint before maintenance.");
@@ -4984,7 +5200,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
4984
5200
  }
4985
5201
  const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
4986
5202
  if (existingWorkers.length) {
4987
- const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-BJimB_p9.mjs");
5203
+ const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-D5wvsI-Y.mjs");
4988
5204
  await verifyProtectionWorkerOrigins({
4989
5205
  request: input.client.request.bind(input.client),
4990
5206
  listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
@@ -4994,213 +5210,200 @@ async function verifyInstalledPlatformAccessProtection(input) {
4994
5210
  }
4995
5211
  //#endregion
4996
5212
  //#region src/cli/platform-install-preparation.ts
5213
+ function withAuthentication(deployment, authentication) {
5214
+ return createPlatformDeploymentManifest({
5215
+ ...deployment,
5216
+ authentication,
5217
+ capabilities: {
5218
+ ...deployment.capabilities,
5219
+ githubOAuth: authentication.providers.includes("github"),
5220
+ googleOAuth: authentication.providers.includes("google")
5221
+ }
5222
+ });
5223
+ }
4997
5224
  async function prepareInstallationAuthentication(input) {
4998
- const { releaseDatabaseLock, runtime, authenticationProtocolTransition, now, dependencies, workerSecretUpdateIntent } = input;
5225
+ const { state, releaseDatabaseLock, runtime, authenticationProtocolTransition, now, dependencies } = input;
5226
+ const { workerSecretUpdateIntent } = state;
4999
5227
  const liveAuthentication = await releaseDatabaseLock.readAuthenticationState?.();
5000
5228
  if (liveAuthentication) {
5001
- input.authenticationEnforcementActive = liveAuthentication.sessionsRequired;
5229
+ state.authenticationEnforcementActive = liveAuthentication.sessionsRequired;
5002
5230
  if (liveAuthentication.sessionsRequired && runtime.authenticationProtocolVersion !== 1) throw new Error("platform: This runtime cannot enforce the active authentication configuration. Use an authentication-compatible runtime.");
5003
- const deployment = mergePlatformLiveAuthenticationDeployment(input.plan.deployment, liveAuthentication);
5004
- input.plan = {
5005
- ...input.plan,
5231
+ const deployment = mergePlatformLiveAuthenticationDeployment(state.plan.deployment, liveAuthentication);
5232
+ state.plan = {
5233
+ ...state.plan,
5006
5234
  deployment
5007
5235
  };
5008
- if (!authenticationProtocolTransition) input.manifest = {
5009
- ...input.manifest,
5010
- deployment
5011
- };
5012
- }
5013
- if (!liveAuthentication && input.manifest.deployment.authentication.configurable && !input.plan.deployment.authentication.configurable) {
5014
- const authentication = input.manifest.deployment.authentication;
5015
- const providers = authentication.providers;
5016
- input.plan = {
5017
- ...input.plan,
5018
- deployment: createPlatformDeploymentManifest({
5019
- ...input.plan.deployment,
5020
- authentication,
5021
- capabilities: {
5022
- ...input.plan.deployment.capabilities,
5023
- githubOAuth: providers.includes("github"),
5024
- googleOAuth: providers.includes("google")
5025
- }
5026
- })
5027
- };
5028
- }
5029
- if (authenticationProtocolTransition && !input.plan.deployment.authentication.configurable) {
5030
- const providers = input.plan.deployment.authentication.providers;
5031
- const deployment = createPlatformDeploymentManifest({
5032
- ...input.plan.deployment,
5033
- authentication: {
5034
- providers,
5035
- configurable: true
5036
- },
5037
- capabilities: {
5038
- ...input.plan.deployment.capabilities,
5039
- githubOAuth: providers.includes("github"),
5040
- googleOAuth: providers.includes("google")
5041
- }
5042
- });
5043
- input.plan = {
5044
- ...input.plan,
5236
+ if (!authenticationProtocolTransition) state.manifest = {
5237
+ ...state.manifest,
5045
5238
  deployment
5046
5239
  };
5047
5240
  }
5241
+ if (!liveAuthentication && state.manifest.deployment.authentication.configurable && !state.plan.deployment.authentication.configurable) state.plan = {
5242
+ ...state.plan,
5243
+ deployment: withAuthentication(state.plan.deployment, state.manifest.deployment.authentication)
5244
+ };
5245
+ if (authenticationProtocolTransition && !state.plan.deployment.authentication.configurable) state.plan = {
5246
+ ...state.plan,
5247
+ deployment: withAuthentication(state.plan.deployment, {
5248
+ providers: state.plan.deployment.authentication.providers,
5249
+ configurable: true
5250
+ })
5251
+ };
5048
5252
  const activateAccessCredentials = () => {
5049
- if (!input.secrets.accessCredentialsJson) return;
5050
- const credentials = JSON.parse(input.secrets.accessCredentialsJson);
5253
+ if (!state.secrets.accessCredentialsJson) return;
5254
+ const credentials = JSON.parse(state.secrets.accessCredentialsJson);
5051
5255
  if (!credentials || typeof credentials !== "object" || Array.isArray(credentials)) throw new Error("platform: Invalid saved Access credentials.");
5052
- const origins = [input.plan.apiUrl, input.plan.proxyUrl];
5256
+ const origins = [state.plan.apiUrl, state.plan.proxyUrl];
5053
5257
  activatePlatformAccessCredentials(JSON.stringify(Object.fromEntries(Object.entries(credentials).filter(([origin]) => origins.includes(origin)))), origins);
5054
5258
  };
5055
5259
  activateAccessCredentials();
5056
- input.manifest = checkpointAuthenticationResources(input.manifest, input.secrets, now());
5057
- writeInstallationManifest(input.manifest);
5058
- await releaseDatabaseLock.checkpoint?.(input.manifest);
5260
+ state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
5261
+ writeInstallationManifest(state.manifest);
5262
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
5059
5263
  dependencies.onProgress?.("Checking and creating Cloudflare resources");
5060
- if (!authenticationProtocolTransition && JSON.stringify(input.manifest.deployment) !== JSON.stringify(input.plan.deployment)) {
5061
- input.manifest = {
5062
- ...input.manifest,
5063
- deployment: input.plan.deployment,
5264
+ if (!authenticationProtocolTransition && JSON.stringify(state.manifest.deployment) !== JSON.stringify(state.plan.deployment)) {
5265
+ state.manifest = {
5266
+ ...state.manifest,
5267
+ deployment: state.plan.deployment,
5064
5268
  updatedAt: now()
5065
5269
  };
5066
- writeInstallationManifest(input.manifest);
5067
- await releaseDatabaseLock.checkpoint?.(input.manifest);
5270
+ writeInstallationManifest(state.manifest);
5271
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
5068
5272
  }
5069
- await releaseDatabaseLock.checkpoint?.(input.manifest);
5070
- if (input.manifest.status !== "provisioning") {
5071
- input.manifest = transitionInstallation(input.manifest, "provisioning", now());
5072
- writeInstallationManifest(input.manifest);
5273
+ if (state.manifest.status !== "provisioning") {
5274
+ state.manifest = transitionInstallation(state.manifest, "provisioning", now());
5275
+ writeInstallationManifest(state.manifest);
5073
5276
  }
5074
- if (runtime.deploymentExecution !== 1 && (input.manifest.deploymentExecution === 1 || dependencies.databaseRollbackSource?.manifest.deploymentExecution === 1)) {
5277
+ if (runtime.deploymentExecution !== 1 && (state.manifest.deploymentExecution === 1 || dependencies.databaseRollbackSource?.manifest.deploymentExecution === 1)) {
5075
5278
  if (!releaseDatabaseLock.checkpoint) throw new Error("platform: The database lifecycle lock cannot fence deployment admission.");
5076
- await releaseDatabaseLock.checkpoint(input.manifest);
5279
+ await releaseDatabaseLock.checkpoint(state.manifest);
5077
5280
  dependencies.onProgress?.("Verifying durable deployment operations have drained");
5078
- await (dependencies.verifyDeploymentExecutionDrain ?? verifyPlatformDeploymentExecutionDrain)(input.plan.apiUrl, createPlatformHealthProbeToken(input.manifest.ownershipNonce), input.manifest.id);
5281
+ await (dependencies.verifyDeploymentExecutionDrain ?? verifyPlatformDeploymentExecutionDrain)(state.plan.apiUrl, createPlatformHealthProbeToken(state.manifest.ownershipNonce), state.manifest.id);
5079
5282
  }
5080
5283
  if (dependencies.prepareAuthentication) {
5081
5284
  dependencies.onProgress?.("Preparing platform authentication");
5082
5285
  try {
5083
- input.secrets = {
5084
- ...input.secrets,
5085
- ...await dependencies.prepareAuthentication(input.manifest, input.secrets)
5286
+ state.secrets = {
5287
+ ...state.secrets,
5288
+ ...await dependencies.prepareAuthentication(state.manifest, state.secrets)
5086
5289
  };
5087
5290
  } catch (error) {
5088
- const latest = readPendingInstallationSecrets(input.manifest) ?? {};
5089
- input.secrets = {
5090
- ...input.secrets,
5291
+ const latest = readPendingInstallationSecrets(state.manifest) ?? {};
5292
+ state.secrets = {
5293
+ ...state.secrets,
5091
5294
  authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name))),
5092
- authenticationSetupJson: latest.authenticationSetupJson || input.secrets.authenticationSetupJson,
5093
- authSetupToken: latest.authSetupToken || input.secrets.authSetupToken,
5094
- authBootstrapCode: latest.authBootstrapCode || input.secrets.authBootstrapCode,
5095
- accessProvisioningJson: latest.accessProvisioningJson || input.secrets.accessProvisioningJson,
5096
- accessCredentialsJson: latest.accessCredentialsJson || input.secrets.accessCredentialsJson
5295
+ authenticationSetupJson: latest.authenticationSetupJson || state.secrets.authenticationSetupJson,
5296
+ authSetupToken: latest.authSetupToken || state.secrets.authSetupToken,
5297
+ authBootstrapCode: latest.authBootstrapCode || state.secrets.authBootstrapCode,
5298
+ accessProvisioningJson: latest.accessProvisioningJson || state.secrets.accessProvisioningJson,
5299
+ accessCredentialsJson: latest.accessCredentialsJson || state.secrets.accessCredentialsJson
5097
5300
  };
5098
- input.manifest = checkpointAuthenticationResources(input.manifest, input.secrets, now());
5099
- writeInstallationManifest(input.manifest);
5100
- await releaseDatabaseLock.checkpoint?.(input.manifest);
5301
+ state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
5302
+ writeInstallationManifest(state.manifest);
5303
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
5101
5304
  throw error;
5102
5305
  }
5103
- const latest = readPendingInstallationSecrets(input.manifest) ?? {};
5104
- input.secrets = {
5105
- ...input.secrets,
5306
+ const latest = readPendingInstallationSecrets(state.manifest) ?? {};
5307
+ state.secrets = {
5308
+ ...state.secrets,
5106
5309
  authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([name]) => isPlatformAuthenticationPreparationKey(name)))
5107
5310
  };
5108
- input.manifest = checkpointAuthenticationResources(input.manifest, input.secrets, now());
5109
- writeInstallationManifest(input.manifest);
5110
- await releaseDatabaseLock.checkpoint?.(input.manifest);
5311
+ state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
5312
+ writeInstallationManifest(state.manifest);
5313
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
5111
5314
  activateAccessCredentials();
5112
- assertRequiredPlatformSecrets(input.plan, input.manifest, input.secrets);
5113
- if (input.secrets.authenticationSetupJson && createHash("sha256").update(input.secrets.authBootstrapCode ?? "").digest("hex") !== parsePlatformAuthenticationSetup(JSON.parse(input.secrets.authenticationSetupJson)).bootstrapCredentialHash) throw new Error("platform: Prepared authentication does not match its administrator setup code.");
5114
- writePendingInstallationSecrets(input.manifest, {
5115
- ...secretCheckpoint(input.secrets, workerSecretUpdateIntent),
5315
+ assertRequiredPlatformSecrets(state.plan, state.manifest, state.secrets);
5316
+ if (state.secrets.authenticationSetupJson && createHash("sha256").update(state.secrets.authBootstrapCode ?? "").digest("hex") !== parsePlatformAuthenticationSetup(JSON.parse(state.secrets.authenticationSetupJson)).bootstrapCredentialHash) throw new Error("platform: Prepared authentication does not match its administrator setup code.");
5317
+ writePendingInstallationSecrets(state.manifest, {
5318
+ ...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
5116
5319
  databaseClaimState: "claimed"
5117
5320
  });
5118
5321
  }
5119
- const appAccessSetupRequired = !!input.plan.deployment.appAccess && !input.manifest.steps.some((step) => step.key === "app-access-initialized:v1");
5322
+ const appAccessSetupRequired = !!state.plan.deployment.appAccess && !state.manifest.steps.some((step) => step.key === "app-access-initialized:v1");
5120
5323
  if (appAccessSetupRequired) {
5121
5324
  if (dependencies.prepareAppAccess) try {
5122
- const appAccessSetupJson = await dependencies.prepareAppAccess(input.manifest, input.secrets);
5123
- const latest = readPendingInstallationSecrets(input.manifest) ?? {};
5124
- input.secrets = {
5125
- ...input.secrets,
5325
+ const appAccessSetupJson = await dependencies.prepareAppAccess(state.manifest, state.secrets);
5326
+ const latest = readPendingInstallationSecrets(state.manifest) ?? {};
5327
+ state.secrets = {
5328
+ ...state.secrets,
5126
5329
  appAccessSetupJson,
5127
5330
  authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
5128
5331
  };
5129
- writePendingInstallationSecrets(input.manifest, {
5130
- ...secretCheckpoint(input.secrets, workerSecretUpdateIntent),
5332
+ writePendingInstallationSecrets(state.manifest, {
5333
+ ...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
5131
5334
  databaseClaimState: "claimed"
5132
5335
  });
5133
5336
  } catch (error) {
5134
- const latest = readPendingInstallationSecrets(input.manifest) ?? {};
5135
- input.secrets = {
5136
- ...input.secrets,
5137
- appAccessSetupJson: latest.appAccessSetupJson || input.secrets.appAccessSetupJson,
5337
+ const latest = readPendingInstallationSecrets(state.manifest) ?? {};
5338
+ state.secrets = {
5339
+ ...state.secrets,
5340
+ appAccessSetupJson: latest.appAccessSetupJson || state.secrets.appAccessSetupJson,
5138
5341
  authenticationPreparation: Object.fromEntries(Object.entries(latest).filter(([key]) => isPlatformAuthenticationPreparationKey(key)))
5139
5342
  };
5140
5343
  throw error;
5141
5344
  }
5142
- if (!input.secrets.appAccessSetupJson) throw new Error("platform: Restore the encrypted app access setup checkpoint before resuming.");
5143
- if (!input.secrets.appAccessSetupToken) {
5144
- if (input.manifest.resources.some((resource) => resource.key === "api-worker" && resource.state === "ready")) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
5145
- input.secrets = {
5146
- ...input.secrets,
5345
+ if (!state.secrets.appAccessSetupJson) throw new Error("platform: Restore the encrypted app access setup checkpoint before resuming.");
5346
+ if (!state.secrets.appAccessSetupToken) {
5347
+ if (state.manifest.resources.some((resource) => resource.key === "api-worker" && resource.state === "ready")) throw new Error("platform: Restore the encrypted app access setup authority before resuming.");
5348
+ state.secrets = {
5349
+ ...state.secrets,
5147
5350
  appAccessSetupToken: randomBytes(32).toString("hex")
5148
5351
  };
5149
- writePendingInstallationSecrets(input.manifest, {
5150
- ...secretCheckpoint(input.secrets, workerSecretUpdateIntent),
5352
+ writePendingInstallationSecrets(state.manifest, {
5353
+ ...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
5151
5354
  databaseClaimState: "claimed"
5152
5355
  });
5153
5356
  }
5154
5357
  }
5155
- if (input.plan.deployment.authentication.accessProtection) {
5156
- const protection = liveAuthentication ? liveAuthentication.protection : input.secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(input.secrets.authenticationSetupJson)).protection : null;
5358
+ if (state.plan.deployment.authentication.accessProtection) {
5359
+ const protection = liveAuthentication ? liveAuthentication.protection : state.secrets.authenticationSetupJson ? parsePlatformAuthenticationSetup(JSON.parse(state.secrets.authenticationSetupJson)).protection : null;
5157
5360
  if (!protection) throw new Error("platform: The active Access protection configuration is unavailable. Restore its database and encrypted setup checkpoint before maintenance.");
5158
5361
  dependencies.onProgress?.("Verifying current Access policies and protected origins");
5159
5362
  await (dependencies.verifyAccessProtection ?? verifyInstalledPlatformAccessProtection)({
5160
5363
  client: dependencies.client,
5161
- manifest: input.manifest,
5162
- plan: input.plan,
5163
- dashboardUrl: platformDashboardUrl(input.plan.deployment),
5364
+ manifest: state.manifest,
5365
+ plan: state.plan,
5366
+ dashboardUrl: platformDashboardUrl(state.plan.deployment),
5164
5367
  protection,
5165
- checkpoint: secretCheckpoint(input.secrets, workerSecretUpdateIntent)
5368
+ checkpoint: secretCheckpoint(state.secrets, workerSecretUpdateIntent)
5166
5369
  });
5167
5370
  }
5168
- if (input.plan.deployment.authentication.accessProtection || appAccessSetupRequired && input.secrets.appAccessSetupJson && JSON.parse(input.secrets.appAccessSetupJson).managed === true) {
5371
+ if (state.plan.deployment.authentication.accessProtection || appAccessSetupRequired && state.secrets.appAccessSetupJson && JSON.parse(state.secrets.appAccessSetupJson).managed === true) {
5169
5372
  dependencies.onProgress?.("Preparing Access for authenticated platform health checks");
5170
5373
  try {
5171
5374
  await (dependencies.prepareHealthAccess ?? ensurePlatformAccessHealthChecks)({
5172
5375
  client: dependencies.client,
5173
- manifest: input.manifest,
5174
- plan: input.plan,
5175
- checkpoint: secretCheckpoint(input.secrets, workerSecretUpdateIntent),
5376
+ manifest: state.manifest,
5377
+ plan: state.plan,
5378
+ checkpoint: secretCheckpoint(state.secrets, workerSecretUpdateIntent),
5176
5379
  save: (accessHealthProvisioningJson) => {
5177
- input.secrets = {
5178
- ...input.secrets,
5380
+ state.secrets = {
5381
+ ...state.secrets,
5179
5382
  accessHealthProvisioningJson
5180
5383
  };
5181
- input.manifest = checkpointAuthenticationResources(input.manifest, input.secrets, now());
5182
- writePendingInstallationSecrets(input.manifest, {
5183
- ...secretCheckpoint(input.secrets, workerSecretUpdateIntent),
5384
+ state.manifest = checkpointAuthenticationResources(state.manifest, state.secrets, now());
5385
+ writePendingInstallationSecrets(state.manifest, {
5386
+ ...secretCheckpoint(state.secrets, workerSecretUpdateIntent),
5184
5387
  databaseClaimState: "claimed"
5185
5388
  });
5186
- writeInstallationManifest(input.manifest);
5389
+ writeInstallationManifest(state.manifest);
5187
5390
  }
5188
5391
  });
5189
5392
  } catch (error) {
5190
- if (appAccessSetupRequired && input.secrets.appAccessSetupJson && JSON.parse(input.secrets.appAccessSetupJson).managed && error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) {
5191
- input.secrets = {
5192
- ...input.secrets,
5393
+ if (appAccessSetupRequired && state.secrets.appAccessSetupJson && JSON.parse(state.secrets.appAccessSetupJson).managed && error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status)) {
5394
+ state.secrets = {
5395
+ ...state.secrets,
5193
5396
  authenticationPreparation: {
5194
- ...input.secrets.authenticationPreparation,
5397
+ ...state.secrets.authenticationPreparation,
5195
5398
  appAccessTokenRejected: "true"
5196
5399
  }
5197
5400
  };
5198
- writePendingInstallationSecrets(input.manifest, secretCheckpoint(input.secrets, workerSecretUpdateIntent));
5401
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, workerSecretUpdateIntent));
5199
5402
  throw new PlatformAppAccessCredentialError("platform: App access token rejected while preparing health checks. Resume installation to provide a replacement.");
5200
5403
  }
5201
5404
  throw error;
5202
5405
  } finally {
5203
- await releaseDatabaseLock.checkpoint?.(input.manifest);
5406
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
5204
5407
  }
5205
5408
  }
5206
5409
  }
@@ -5218,7 +5421,7 @@ function verifyRuntimeArtifacts(runtimeDirectory, manifest) {
5218
5421
  try {
5219
5422
  contents = readFileSync(path);
5220
5423
  } catch (error) {
5221
- throw new Error(`platform: Runtime artifact is missing: ${errorMessage(error)}`);
5424
+ throw new Error(`platform: Runtime artifact is missing: ${getErrorMessage(error)}`);
5222
5425
  }
5223
5426
  if (contents.byteLength !== artifact.size || hash(contents) !== artifact.sha256) throw new Error(`platform: Runtime artifact failed integrity verification: ${path}.`);
5224
5427
  }
@@ -5230,7 +5433,7 @@ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
5230
5433
  try {
5231
5434
  journal = readFileSync(journalPath);
5232
5435
  } catch (error) {
5233
- throw new Error(`platform: Runtime database migration journal is missing: ${errorMessage(error)}`);
5436
+ throw new Error(`platform: Runtime database migration journal is missing: ${getErrorMessage(error)}`);
5234
5437
  }
5235
5438
  if (hash(journal) !== database.journalSha256) throw new Error(`platform: Runtime database migration journal failed integrity verification: ${journalPath}.`);
5236
5439
  for (const migration of database.migrations) {
@@ -5239,611 +5442,12 @@ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
5239
5442
  try {
5240
5443
  contents = readFileSync(path);
5241
5444
  } catch (error) {
5242
- throw new Error(`platform: Runtime database migration is missing: ${errorMessage(error)}`);
5445
+ throw new Error(`platform: Runtime database migration is missing: ${getErrorMessage(error)}`);
5243
5446
  }
5244
5447
  if (hash(contents) !== migration.sha256) throw new Error(`platform: Runtime database migration failed integrity verification: ${path}.`);
5245
5448
  }
5246
5449
  }
5247
5450
  //#endregion
5248
- //#region src/cli/platform-teardown.ts
5249
- /**
5250
- * `void platform uninstall --purge-resources` deletes every Cloudflare resource the
5251
- * installation created, in an order Cloudflare accepts:
5252
- *
5253
- * traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
5254
- * -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
5255
- * -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
5256
- *
5257
- * Each resource is proven to belong to this installation right before its
5258
- * DELETE, and Cloudflare refuses a resource that something outside the
5259
- * installation still uses. The only forced DELETE is for a platform Worker
5260
- * that Cloudflare refused while every Worker it names as a dependent is one
5261
- * this installation created: the API and proxy Workers bind to each other, so
5262
- * neither can go first without it. A route, custom domain, DNS
5263
- * record, or Access resource that no longer matches what was recorded is kept
5264
- * and listed with the reason instead of stopping the run. Every finished
5265
- * deletion is checkpointed, so rerunning the command continues where it
5266
- * stopped. The routing KV namespace holds that checkpoint, so uninstall
5267
- * deletes it last.
5268
- *
5269
- * An uninstall without --purge-resources records what it leaves as retained. If it
5270
- * stopped before it finished, --purge-resources considers those resources again.
5271
- */
5272
- const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
5273
- const WORKER_ORDER = [
5274
- "email-gateway-worker",
5275
- "proxy-worker",
5276
- "dispatch-worker",
5277
- "api-worker"
5278
- ];
5279
- const PERMISSIONS = {
5280
- access: "Account → Access: Apps and Policies → Edit",
5281
- aiGateway: "Account → AI Gateway → Edit",
5282
- dns: "Zone → DNS → Edit",
5283
- emailRouting: "Zone → Email Routing Rules → Edit",
5284
- hyperdrive: "Account → Hyperdrive → Edit",
5285
- kv: "Account → Workers KV Storage → Edit",
5286
- queues: "Account → Queues → Edit",
5287
- r2: "Account → Workers R2 Storage → Edit",
5288
- routes: "Zone → Workers Routes → Edit",
5289
- serviceTokens: "Account → Access: Service Tokens → Edit",
5290
- workers: "Account → Workers Scripts → Edit"
5291
- };
5292
- function isPlatformTailWorker(manifest, resource) {
5293
- return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
5294
- }
5295
- function purgeResourcesResumeCommand(manifest) {
5296
- return `void platform uninstall ${manifest.id} --purge-resources`;
5297
- }
5298
- function accessStepKey(access) {
5299
- return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
5300
- }
5301
- /** Access resources that --purge-resources already deleted. */
5302
- function isPurgedAccessResource(manifest, access) {
5303
- return manifest.steps.some((step) => step.key === accessStepKey(access));
5304
- }
5305
- function hasStep(manifest, key) {
5306
- return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
5307
- }
5308
- function keptStepKey(key) {
5309
- return `kept:${key}`;
5310
- }
5311
- /**
5312
- * Why --purge-resources kept a resource it was going to delete, for example because
5313
- * a route now serves another Worker; null when it did not keep it. Access
5314
- * resources use the key `access:<key>`.
5315
- */
5316
- function purgeResourcesKeptReason(manifest, key) {
5317
- return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
5318
- }
5319
- /** An uninstall that started has not finished, so what it kept can be reconsidered. */
5320
- function isReopenable(manifest) {
5321
- return manifest.status !== "uninstalled";
5322
- }
5323
- /**
5324
- * A resource the installation created that --purge-resources may still delete: one
5325
- * that is ready, or one that an uninstall which stopped before it finished
5326
- * recorded as retained. Resources --purge-resources itself kept, and earlier
5327
- * versions of a resource kept when it was replaced, are never reconsidered.
5328
- */
5329
- function isPurgeResourcesCandidate(manifest, resource) {
5330
- return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
5331
- }
5332
- /** The zone that routes and DNS records live in, if it can still be used. */
5333
- function usableZone(manifest) {
5334
- return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
5335
- }
5336
- /**
5337
- * A recorded resource whose live configuration no longer matches the
5338
- * installation's record. --purge-resources keeps such a DNS record and continues.
5339
- */
5340
- var PlatformResourceChangedError = class extends Error {
5341
- resourceKey;
5342
- reason;
5343
- constructor(resourceKey, reason, message) {
5344
- super(message);
5345
- this.resourceKey = resourceKey;
5346
- this.reason = reason;
5347
- this.name = "PlatformResourceChangedError";
5348
- }
5349
- };
5350
- /** The zone whose Email Routing rules may send mail to the gateway Worker. */
5351
- function emailRoutingZone(manifest) {
5352
- return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
5353
- }
5354
- /** Everything --purge-resources will delete, in deletion order. */
5355
- function purgeResourcesTargets(manifest) {
5356
- const resources = manifest.resources;
5357
- const routing = resources.find((resource) => resource.key === "routing-kv");
5358
- const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
5359
- if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
5360
- const zone = usableZone(manifest);
5361
- const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
5362
- const targets = [];
5363
- for (const resource of resources) {
5364
- if (resource.kind !== "worker-route" && resource.kind !== "worker-custom-domain" || resource.state !== "ready" && resource.state !== "retained" || resource.kind === "worker-route" && !zone || purgeResourcesKeptReason(manifest, resource.key) !== null) continue;
5365
- const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
5366
- const worker = workers.find((entry) => entry.name === name);
5367
- if (worker) targets.push({
5368
- phase: "traffic",
5369
- resource,
5370
- worker
5371
- });
5372
- }
5373
- for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
5374
- phase: "dns",
5375
- resource
5376
- });
5377
- const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
5378
- for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
5379
- phase: "access",
5380
- access: entry
5381
- });
5382
- const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
5383
- if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
5384
- phase: "email",
5385
- resource: gateway
5386
- });
5387
- const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
5388
- for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
5389
- phase: "queue-consumers",
5390
- resource
5391
- });
5392
- const rank = (resource) => {
5393
- const index = WORKER_ORDER.indexOf(resource.key);
5394
- return index < 0 ? WORKER_ORDER.length : index;
5395
- };
5396
- for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
5397
- phase: "workers",
5398
- resource
5399
- });
5400
- for (const resource of queues) targets.push({
5401
- phase: "queues",
5402
- resource
5403
- });
5404
- for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
5405
- phase: "dispatch",
5406
- resource
5407
- });
5408
- for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
5409
- phase: "tail",
5410
- resource
5411
- });
5412
- for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
5413
- phase: "data",
5414
- resource
5415
- });
5416
- for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
5417
- phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
5418
- resource
5419
- });
5420
- if (candidate(routing)) targets.push({
5421
- phase: "routing",
5422
- resource: routing
5423
- });
5424
- return targets;
5425
- }
5426
- /** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
5427
- function purgeResourcesHandlesEmailRouting(manifest) {
5428
- return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
5429
- }
5430
- /** Manifest resources that --purge-resources leaves in the account. */
5431
- function purgeResourcesKeptResources(manifest) {
5432
- const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
5433
- return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
5434
- }
5435
- function resourceLabel(resource) {
5436
- return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
5437
- }
5438
- /** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
5439
- function describePurgeResourcesPlan(manifest) {
5440
- const targets = purgeResourcesTargets(manifest);
5441
- const lines = [];
5442
- let step = 0;
5443
- let previous = null;
5444
- const consumerQueues = [];
5445
- for (const target of targets) {
5446
- if (target.phase === "email") {
5447
- step++;
5448
- previous = target.phase;
5449
- lines.push(` ${step}. Email Routing rules in zone ${emailRoutingZone(manifest)} that send mail to ${target.resource.name}, and its catch-all rule if it does (reset to drop mail, disabled)`);
5450
- continue;
5451
- }
5452
- if (target.phase !== previous) {
5453
- step++;
5454
- previous = target.phase;
5455
- }
5456
- const prefix = ` ${step}. `;
5457
- if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
5458
- else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
5459
- else if (target.phase === "queue-consumers") {
5460
- consumerQueues.push(target.resource.name);
5461
- if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
5462
- lines.push(` ${target.resource.name}`);
5463
- } else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
5464
- else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
5465
- else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
5466
- else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
5467
- else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
5468
- else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
5469
- else lines.push(`${prefix}${resourceLabel(target.resource)}`);
5470
- }
5471
- return lines.length ? ["Delete every resource this installation created (--purge-resources), in this order:", ...lines] : ["--purge-resources finds no resource it can prove this installation created."];
5472
- }
5473
- /** Lines naming what --purge-resources never removes. */
5474
- function purgeResourcesAlwaysKept() {
5475
- return [
5476
- " The external PostgreSQL database and its data.",
5477
- " Analytics Engine datasets; Cloudflare has no API to delete them.",
5478
- " Access policies and identity providers you chose.",
5479
- " API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
5480
- ];
5481
- }
5482
- /** Token permissions --purge-resources needs for this manifest. */
5483
- function purgeResourcesPermissions(manifest) {
5484
- const targets = purgeResourcesTargets(manifest);
5485
- const needed = /* @__PURE__ */ new Set();
5486
- for (const target of targets) {
5487
- if (target.phase === "access") {
5488
- needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
5489
- continue;
5490
- }
5491
- if (target.phase === "email") {
5492
- needed.add(PERMISSIONS.emailRouting);
5493
- continue;
5494
- }
5495
- const { kind } = target.resource;
5496
- if (kind === "worker-route") needed.add(PERMISSIONS.routes);
5497
- else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
5498
- else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
5499
- else if (kind === "queue") needed.add(PERMISSIONS.queues);
5500
- else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
5501
- else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
5502
- else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
5503
- else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
5504
- }
5505
- return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
5506
- }
5507
- function deniedStatus(error) {
5508
- return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
5509
- }
5510
- /**
5511
- * Check resource reads before the first change. Browser login cannot change
5512
- * DNS or Access, so those need an API token. Read probes cannot prove Edit;
5513
- * the plan lists the required permissions.
5514
- */
5515
- async function checkPurgeResourcesCredentials(client, manifest, credential) {
5516
- const targets = purgeResourcesTargets(manifest);
5517
- const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
5518
- const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
5519
- const has = (kind) => resources.some((resource) => resource.kind === kind);
5520
- const zone = manifest.resources.find((resource) => resource.key === "zone");
5521
- const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
5522
- const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
5523
- const scope = `${accounts.length === 1 ? "account" : "accounts"} ${accounts.join(" and ")}${has("dns-record") || has("worker-route") ? ` and zone ${zone?.name ?? ""}` : ""}${emailZone ? ` and Email Routing zone ${emailZone}` : ""}`;
5524
- if (credential.source === "oauth") {
5525
- const browserMissing = [
5526
- ...has("dns-record") ? [PERMISSIONS.dns] : [],
5527
- ...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
5528
- ...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
5529
- ...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
5530
- ];
5531
- if (browserMissing.length) throw new Error(`platform: Cloudflare browser login cannot delete every resource --purge-resources plans. Set CLOUDFLARE_API_TOKEN to a token with ${purgeResourcesPermissions(manifest).join(", ")}, scoped to ${scope}, then rerun. Browser login is missing: ${browserMissing.join(", ")}. No Cloudflare resource was changed.`);
5532
- }
5533
- const probes = [];
5534
- if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
5535
- if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
5536
- if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
5537
- for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
5538
- else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
5539
- for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
5540
- if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
5541
- if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
5542
- if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
5543
- if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
5544
- if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
5545
- if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
5546
- const missing = /* @__PURE__ */ new Set();
5547
- for (const [permission, probe] of probes) try {
5548
- await probe();
5549
- } catch (error) {
5550
- if (!deniedStatus(error)) throw error;
5551
- missing.add(permission);
5552
- }
5553
- if (missing.size) throw new Error(`platform: The Cloudflare credential cannot read every resource --purge-resources would delete. Add ${[...missing].join(", ")} for ${scope}, then rerun. No Cloudflare resource was changed.`);
5554
- }
5555
- const LISTED_PROJECTS = 20;
5556
- /** Slugs of every project row, including projects whose deletion stopped partway. */
5557
- async function readPlatformProjectSlugsConnection(client) {
5558
- if ((await client.query(`SELECT to_regclass('public.projects') IS NOT NULL AS "present"`)).rows[0]?.present !== true) return [];
5559
- return (await client.query("SELECT \"slug\" FROM \"projects\" ORDER BY \"slug\"")).rows.map((row) => row.slug);
5560
- }
5561
- /**
5562
- * Projects own Workers, storage, and domains that only the running platform
5563
- * can delete. --purge-resources deletes the platform, so it refuses before any
5564
- * change while a project remains.
5565
- */
5566
- async function assertNoPlatformProjects(read) {
5567
- let slugs;
5568
- try {
5569
- if (!read) throw new Error("no platform database connection is available");
5570
- slugs = await read();
5571
- } catch (error) {
5572
- const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
5573
- throw new Error(`platform: Refusing --purge-resources because the project list could not be read from the platform database: ${reason}\nMake the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry.`);
5574
- }
5575
- if (slugs.length) throw new Error([
5576
- `platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
5577
- "--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
5578
- "Delete each project first with `void project delete <name>`, then retry:",
5579
- ...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
5580
- ...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
5581
- ].join("\n"));
5582
- }
5583
- const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
5584
- /**
5585
- * AI Gateway ids are chosen by the caller and can be reused after a delete,
5586
- * and gateways carry no ownership marker. Only a creation time next to the
5587
- * installer's provision intent shows that this is the gateway it created.
5588
- */
5589
- function createdDuringInstallation(manifest, resource, createdAt) {
5590
- const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
5591
- if (Number.isNaN(created)) return false;
5592
- return manifest.steps.some((step) => (step.key === `provision-intent:${resource.key}` || step.key.startsWith(`provision-intent:${resource.key}:`)) && Math.abs(created - Date.parse(step.completedAt)) <= AI_GATEWAY_CREATION_WINDOW_MS);
5593
- }
5594
- function ownershipRefusal(manifest, description, reason) {
5595
- return /* @__PURE__ */ new Error(`platform: Refusing to delete ${description} because ${reason}. It may no longer belong to this installation. Delete or fix it in the Cloudflare dashboard if it is yours, then rerun: ${purgeResourcesResumeCommand(manifest)}`);
5596
- }
5597
- function cloudflareRefusal(manifest, description, error, hint) {
5598
- if (!(error instanceof PlatformCloudflareApiError)) return error;
5599
- return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
5600
- }
5601
- /**
5602
- * Delete every target in order. Returns the manifest with each deleted
5603
- * resource recorded; routing KV is left for the caller to delete last.
5604
- */
5605
- async function runPurgeResourcesTeardown(context, initial) {
5606
- const { client } = context;
5607
- let next = initial;
5608
- const accountId = next.account.id;
5609
- const save = async (manifest) => {
5610
- next = manifest;
5611
- await context.checkpoint(next);
5612
- };
5613
- const markDeleted = async (key) => {
5614
- const resource = next.resources.find((entry) => entry.key === key);
5615
- await save(recordInstallationResource(next, {
5616
- ...resource,
5617
- state: "deleted"
5618
- }, (/* @__PURE__ */ new Date()).toISOString()));
5619
- };
5620
- const recordStep = async (key, resultHash = "completed") => {
5621
- const now = (/* @__PURE__ */ new Date()).toISOString();
5622
- await save(recordInstallationStep(next, {
5623
- completedAt: now,
5624
- key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
5625
- resultHash
5626
- }, now));
5627
- };
5628
- /**
5629
- * Live proof for these keys. A resource an earlier uninstall recorded as
5630
- * retained is proven like a ready one; only its recorded state differs.
5631
- */
5632
- const verify = (keys) => context.verify({
5633
- ...next,
5634
- resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
5635
- ...resource,
5636
- state: "ready"
5637
- } : resource)
5638
- }, keys);
5639
- /**
5640
- * Prove the resource; false when it is already gone (and recorded).
5641
- *
5642
- * Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
5643
- * addressed by name, and a route or custom domain can be changed in place.
5644
- * Something replaced between this proof and the DELETE can still be removed.
5645
- * That one-request window is accepted because --purge-resources is opt-in and its
5646
- * plan lists every resource first. A forced Worker DELETE has the same
5647
- * window between reading its dependents and the DELETE.
5648
- */
5649
- const prove = async (resource) => {
5650
- if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
5651
- await markDeleted(resource.key);
5652
- return false;
5653
- }
5654
- return true;
5655
- };
5656
- /** Keep a resource that no longer matches the record, and save why. */
5657
- const keep = async (key, reason) => {
5658
- const resource = next.resources.find((entry) => entry.key === key);
5659
- if (resource?.state === "ready") await save(recordInstallationResource(next, {
5660
- ...resource,
5661
- state: "retained"
5662
- }, (/* @__PURE__ */ new Date()).toISOString()));
5663
- await recordStep(keptStepKey(key), reason);
5664
- };
5665
- const remove = async (resource, hint, zoneId) => {
5666
- if (!await prove(resource)) return;
5667
- try {
5668
- await client.deleteResource(accountId, resource, zoneId);
5669
- } catch (error) {
5670
- throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
5671
- }
5672
- await markDeleted(resource.key);
5673
- };
5674
- const zone = usableZone(next);
5675
- const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
5676
- /** The recorded Worker under this name that this run still deletes, if any. */
5677
- const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
5678
- const workerHint = "A Worker this installation did not create may still use it, for example by sending it logs as a tail Worker or through a service binding. Remove that link in the Cloudflare dashboard.";
5679
- /**
5680
- * Delete a platform Worker. When Cloudflare refuses because other Workers
5681
- * still bind to it, force the DELETE only if each of them is a Worker this
5682
- * run deletes too and still carries this installation's ownership marker.
5683
- */
5684
- const removeWorker = async (resource) => {
5685
- if (!await prove(resource)) return;
5686
- try {
5687
- await client.deleteResource(accountId, resource);
5688
- } catch (error) {
5689
- if (!(error instanceof PlatformCloudflareApiError)) throw error;
5690
- const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
5691
- const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
5692
- const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
5693
- const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
5694
- if (!dependents || dependents.pagesFunction || dependents.workers.length === 0 || outside.length > 0 || !missing || keys.some((key) => missing.has(key))) throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, outside.length ? `Workers outside this installation still use it: ${outside.join(", ")}. Remove that link in the Cloudflare dashboard.` : workerHint);
5695
- try {
5696
- await client.forceDeleteWorker(accountId, resource.name);
5697
- } catch (forced) {
5698
- throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
5699
- }
5700
- }
5701
- await markDeleted(resource.key);
5702
- };
5703
- for (const target of purgeResourcesTargets(initial)) {
5704
- if (target.phase === "access") {
5705
- const { access } = target;
5706
- const live = await client.getAccessResource(access.accountId, access.kind, access.id);
5707
- if (live) {
5708
- if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
5709
- await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
5710
- continue;
5711
- }
5712
- try {
5713
- await client.deleteAccessResource(access.accountId, access.kind, access.id);
5714
- } catch (error) {
5715
- throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
5716
- }
5717
- }
5718
- await recordStep(`access:${access.key}`);
5719
- continue;
5720
- }
5721
- const current = next.resources.find((resource) => resource.key === target.resource.key);
5722
- if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
5723
- switch (target.phase) {
5724
- case "traffic": {
5725
- const worker = next.resources.find((resource) => resource.key === target.worker.key);
5726
- if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
5727
- if (current.kind === "worker-route") {
5728
- const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
5729
- if (live && (live.pattern !== current.name || live.script !== worker.name)) {
5730
- await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
5731
- break;
5732
- }
5733
- if (live) await client.deleteWorkerRoute(zone.id, current.id);
5734
- } else {
5735
- const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
5736
- if (live && (live.hostname !== current.name || live.service !== worker.name)) {
5737
- await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
5738
- break;
5739
- }
5740
- if (live) await client.deleteWorkerDomain(accountId, current.id);
5741
- }
5742
- await markDeleted(current.key);
5743
- break;
5744
- }
5745
- case "dns":
5746
- try {
5747
- await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
5748
- } catch (error) {
5749
- if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
5750
- await keep(current.key, error.reason);
5751
- }
5752
- break;
5753
- case "email": {
5754
- if (!await prove(current)) break;
5755
- const zoneId = emailRoutingZone(next);
5756
- const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
5757
- try {
5758
- for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
5759
- if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
5760
- } catch (error) {
5761
- throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
5762
- }
5763
- await recordStep("email-routing");
5764
- break;
5765
- }
5766
- case "queue-consumers":
5767
- if (!await prove(current)) break;
5768
- for (const consumer of await client.listQueueConsumers(accountId, current.id)) if (consumer.script && createdWorkerNames.has(consumer.script)) await client.deleteQueueConsumer(accountId, current.id, consumer.id);
5769
- await recordStep(`consumers:${current.key}`);
5770
- break;
5771
- case "workers":
5772
- await removeWorker(current);
5773
- break;
5774
- case "tail":
5775
- await remove(current, workerHint);
5776
- break;
5777
- case "queues":
5778
- await remove(current, "A Worker this installation did not create may still use the Queue as a producer or consumer. Remove it from that Worker in the Cloudflare dashboard.");
5779
- break;
5780
- case "dispatch": {
5781
- if (!await prove(current)) break;
5782
- const projects = await context.readProjects();
5783
- if (projects.length) throw new Error(`platform: Stopped before deleting the application Workers in dispatch namespace ${current.name} because ${projects.length === 1 ? "a project was" : "projects were"} created while this uninstall ran: ${projects.join(", ")}. Nothing in the namespace was deleted. --purge-resources refuses while the platform database lists a project. Remove each one from the "projects" table once you have dealt with its Cloudflare resources, then rerun: ${purgeResourcesResumeCommand(next)}`);
5784
- for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
5785
- await client.deleteDispatchNamespaceScript(accountId, current.name, script);
5786
- } catch (error) {
5787
- throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
5788
- }
5789
- try {
5790
- await client.deleteResource(accountId, current);
5791
- } catch (error) {
5792
- throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
5793
- }
5794
- await markDeleted(current.key);
5795
- break;
5796
- }
5797
- case "ai-gateway": {
5798
- const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
5799
- if (live && !createdDuringInstallation(next, current, live.createdAt)) {
5800
- await keep(current.key, "Cloudflare does not show it was created during installation");
5801
- break;
5802
- }
5803
- await remove(current, "Check that nothing outside this installation still uses it.");
5804
- break;
5805
- }
5806
- case "data":
5807
- await remove(current, "Check that nothing outside this installation still uses it.");
5808
- break;
5809
- case "r2": await purgeBucket(current);
5810
- }
5811
- }
5812
- return next;
5813
- async function purgeBucket(bucket) {
5814
- const markerStep = `r2-marker:${bucket.key}`;
5815
- if (!hasStep(next, markerStep)) {
5816
- if (!await prove(bucket)) return;
5817
- await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
5818
- if ((await verify(/* @__PURE__ */ new Set(["routing-kv", bucket.key]))).has(bucket.key)) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "it disappeared while it was being emptied");
5819
- });
5820
- if (!await prove(bucket)) return;
5821
- const generation = await context.bucketGeneration(next, bucket);
5822
- if (generation === null) {
5823
- await markDeleted(bucket.key);
5824
- return;
5825
- }
5826
- await recordStep(markerStep, generation);
5827
- await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
5828
- } else {
5829
- const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
5830
- const generation = await context.bucketGeneration(next, bucket);
5831
- if (generation === null) {
5832
- await markDeleted(bucket.key);
5833
- return;
5834
- }
5835
- if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
5836
- await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
5837
- }
5838
- try {
5839
- await client.deleteResource(accountId, bucket);
5840
- } catch (error) {
5841
- throw cloudflareRefusal(next, `r2-bucket ${bucket.name}`, error, "Cloudflare deletes only empty buckets. If objects were added after it was emptied, delete them in the Cloudflare dashboard.");
5842
- }
5843
- await markDeleted(bucket.key);
5844
- }
5845
- }
5846
- //#endregion
5847
5451
  //#region src/cli/platform-zero-trust-uninstall.ts
5848
5452
  /**
5849
5453
  * Uninstall turns off the platform API, and only that API can remove the
@@ -5858,6 +5462,16 @@ const ZERO_TRUST_SETTING = "zeroTrust.configuration";
5858
5462
  const ZERO_TRUST_LOCK = "hashtext('zero-trust:configuration')";
5859
5463
  const UNINSTALL_SETTING = "platform.uninstall";
5860
5464
  const LISTED_APPLICATIONS = 20;
5465
+ /** Runs an uninstall precheck read; any failure becomes the refusal `refuse` builds. */
5466
+ async function readPlatformDatabase(read, refuse) {
5467
+ try {
5468
+ if (!read) throw new Error("no platform database connection is available");
5469
+ return await read();
5470
+ } catch (error) {
5471
+ const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
5472
+ throw new Error(refuse(reason));
5473
+ }
5474
+ }
5861
5475
  function optionalText(value, label) {
5862
5476
  if (value === null || value === void 0) return null;
5863
5477
  if (typeof value !== "string") throw new Error(`the saved ${label} is malformed`);
@@ -5995,18 +5609,11 @@ function unfinishedDeletionLines(state) {
5995
5609
  * or while its state cannot be read. Runs before uninstall changes anything.
5996
5610
  */
5997
5611
  async function assertProjectZeroTrustAllowsUninstall(read) {
5998
- let state;
5999
- try {
6000
- if (!read) throw new Error("no platform database connection is available");
6001
- state = await read();
6002
- } catch (error) {
6003
- const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
6004
- throw new Error([
6005
- `platform: Refusing to uninstall because the project Zero Trust state could not be read from the platform database: ${reason}`,
6006
- "Uninstall turns off the platform API, which Void needs to remove the Cloudflare Access applications that project Zero Trust created.",
6007
- "Make the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry. If project Zero Trust was ever enabled, run `void platform zero-trust disable` first."
6008
- ].join("\n"));
6009
- }
5612
+ const state = await readPlatformDatabase(read, (reason) => [
5613
+ `platform: Refusing to uninstall because the project Zero Trust state could not be read from the platform database: ${reason}`,
5614
+ "Uninstall turns off the platform API, which Void needs to remove the Cloudflare Access applications that project Zero Trust created.",
5615
+ "Make the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry. If project Zero Trust was ever enabled, run `void platform zero-trust disable` first."
5616
+ ].join("\n"));
6010
5617
  if (state.active) {
6011
5618
  const covered = state.destinations.length ? ` on ${state.destinations.join(" and ")}` : " on every hostname under the application domain";
6012
5619
  throw new Error([
@@ -6031,7 +5638,7 @@ async function assertProjectZeroTrustAllowsUninstall(read) {
6031
5638
  }
6032
5639
  //#endregion
6033
5640
  //#region src/cli/platform-installer-database.ts
6034
- async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan, secrets, dependencies, releaseDatabaseLock, now, writeRemoteInstallationCheckpoint, onManifestChange) {
5641
+ async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan, dependencies, releaseDatabaseLock, now, writeRemoteInstallationCheckpoint, onManifestChange) {
6035
5642
  dependencies.onProgress?.("Checking the platform database schema");
6036
5643
  const rollbackDatabase = dependencies.databaseRollbackSource;
6037
5644
  const migrationStep = `database-schema:${runtime.database.schemaDigest}`;
@@ -6049,14 +5656,9 @@ async function preparePlatformDatabaseSchema(manifest, runtime, runtimeDirectory
6049
5656
  await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
6050
5657
  } else if (!manifest.steps.some((step) => step.key === migrationStep)) {
6051
5658
  dependencies.onProgress?.("Applying platform database migrations");
6052
- const legacyMigrationStep = `database-migrations:${runtime.database.journalSha256}`;
6053
- if (dependencies.migrateDatabase) {
6054
- if (!manifest.steps.some((step) => step.key === legacyMigrationStep)) await dependencies.migrateDatabase(secrets.databaseUrl, join(runtimeDirectory, "drizzle"));
6055
- } else {
6056
- const prepareDatabase = dependencies.prepareDatabase ?? releaseDatabaseLock.prepareDatabase;
6057
- if (!prepareDatabase) throw new Error("platform: The PostgreSQL lifecycle lock cannot verify or migrate this database.");
6058
- await prepareDatabase(runtimeDirectory, runtime.database, sourcePlatformVersion, hadPriorWorkers);
6059
- }
5659
+ const prepareDatabase = dependencies.prepareDatabase ?? releaseDatabaseLock.prepareDatabase;
5660
+ if (!prepareDatabase) throw new Error("platform: The PostgreSQL lifecycle lock cannot verify or migrate this database.");
5661
+ await prepareDatabase(runtimeDirectory, runtime.database, sourcePlatformVersion, hadPriorWorkers);
6060
5662
  manifest = recordInstallationStep(manifest, {
6061
5663
  completedAt: now(),
6062
5664
  key: migrationStep,
@@ -6123,20 +5725,28 @@ function platformDatabaseClientUrl(databaseUrl) {
6123
5725
  url.searchParams.set("sslmode", "verify-full");
6124
5726
  return url.toString();
6125
5727
  }
6126
- async function claimPlatformDatabase(databaseUrl, installationId, ownershipNonce) {
5728
+ async function withPlatformDatabasePool(databaseUrl, use) {
6127
5729
  const { Pool } = await import("pg");
6128
5730
  const pool = new Pool({
6129
5731
  connectionString: platformDatabaseClientUrl(databaseUrl),
6130
5732
  max: 1
6131
5733
  });
6132
- const client = await pool.connect();
6133
5734
  try {
6134
- await claimPlatformDatabaseConnection(client, installationId, ownershipNonce);
5735
+ return await use(pool);
6135
5736
  } finally {
6136
- client.release();
6137
5737
  await pool.end();
6138
5738
  }
6139
5739
  }
5740
+ function claimPlatformDatabase(databaseUrl, installationId, ownershipNonce) {
5741
+ return withPlatformDatabasePool(databaseUrl, async (pool) => {
5742
+ const client = await pool.connect();
5743
+ try {
5744
+ await claimPlatformDatabaseConnection(client, installationId, ownershipNonce);
5745
+ } finally {
5746
+ client.release();
5747
+ }
5748
+ });
5749
+ }
6140
5750
  async function acquirePlatformDatabaseLifecycleLock(databaseUrl, installationId, ownershipNonce) {
6141
5751
  const { Pool } = await import("pg");
6142
5752
  const pool = new Pool({
@@ -6162,49 +5772,35 @@ async function acquirePlatformDatabaseLifecycleLock(databaseUrl, installationId,
6162
5772
  }
6163
5773
  }
6164
5774
  async function readPlatformDatabaseLifecycleManifest(databaseUrl, installationId, ownershipNonce) {
6165
- const { Pool } = await import("pg");
6166
- const pool = new Pool({
6167
- connectionString: platformDatabaseClientUrl(databaseUrl),
6168
- max: 1
5775
+ return withPlatformDatabasePool(databaseUrl, async (pool) => {
5776
+ try {
5777
+ return await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
5778
+ } catch (error) {
5779
+ if (error instanceof Error && error.message.startsWith("platform:")) throw error;
5780
+ throw new Error(`platform: Could not read the PostgreSQL lifecycle checkpoint: ${error instanceof Error ? error.message : String(error)}`);
5781
+ }
6169
5782
  });
6170
- try {
6171
- return await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
6172
- } catch (error) {
6173
- if (error instanceof Error && error.message.startsWith("platform:")) throw error;
6174
- throw new Error(`platform: Could not read the PostgreSQL lifecycle checkpoint: ${error instanceof Error ? error.message : String(error)}`);
6175
- } finally {
6176
- await pool.end();
6177
- }
6178
5783
  }
6179
- /** Read project Zero Trust state for a plan, which holds no lifecycle lock. */
6180
- async function readPlatformProjectZeroTrustState(databaseUrl, installationId, ownershipNonce) {
5784
+ /** Plans hold no lifecycle lock, so verify the installation claim before reading. */
5785
+ async function readClaimedPlatformDatabase(databaseUrl, installationId, ownershipNonce, read) {
6181
5786
  if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
6182
- const { Pool } = await import("pg");
6183
- const pool = new Pool({
6184
- connectionString: platformDatabaseClientUrl(databaseUrl),
6185
- max: 1
6186
- });
6187
- try {
5787
+ return withPlatformDatabasePool(databaseUrl, async (pool) => {
6188
5788
  await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
6189
- return await readPlatformProjectZeroTrustStateConnection(pool);
6190
- } finally {
6191
- await pool.end();
6192
- }
6193
- }
6194
- /** Read project slugs for a --purge-resources plan, which holds no lifecycle lock. */
6195
- async function readPlatformProjectSlugs(databaseUrl, installationId, ownershipNonce) {
6196
- if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
6197
- const { Pool } = await import("pg");
6198
- const pool = new Pool({
6199
- connectionString: platformDatabaseClientUrl(databaseUrl),
6200
- max: 1
5789
+ return read(pool);
6201
5790
  });
6202
- try {
6203
- await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
6204
- return await readPlatformProjectSlugsConnection(pool);
6205
- } finally {
6206
- await pool.end();
6207
- }
5791
+ }
5792
+ /** Read project Zero Trust state for a plan. */
5793
+ function readPlatformProjectZeroTrustState(databaseUrl, installationId, ownershipNonce) {
5794
+ return readClaimedPlatformDatabase(databaseUrl, installationId, ownershipNonce, readPlatformProjectZeroTrustStateConnection);
5795
+ }
5796
+ /** Read project slugs for a --purge-resources plan. */
5797
+ function readPlatformProjectSlugs(databaseUrl, installationId, ownershipNonce) {
5798
+ return readClaimedPlatformDatabase(databaseUrl, installationId, ownershipNonce, readPlatformProjectSlugsConnection);
5799
+ }
5800
+ /** Slugs of every project row, including projects whose deletion stopped partway. */
5801
+ async function readPlatformProjectSlugsConnection(client) {
5802
+ if ((await client.query(`SELECT to_regclass('public.projects') IS NOT NULL AS "present"`)).rows[0]?.present !== true) return [];
5803
+ return (await client.query("SELECT \"slug\" FROM \"projects\" ORDER BY \"slug\"")).rows.map((row) => row.slug);
6208
5804
  }
6209
5805
  async function inspectPlatformDatabaseStateConnection(client, database) {
6210
5806
  const relation = await client.query(`SELECT to_regclass('"drizzle"."__drizzle_migrations"')::text AS "relation"`);
@@ -6217,14 +5813,12 @@ async function inspectPlatformDatabaseStateConnection(client, database) {
6217
5813
  ORDER BY "created_at", "id"`);
6218
5814
  if (result.rows.length > database.migrations.length) throw new Error("platform: PostgreSQL contains migrations newer than this Void Platform package.");
6219
5815
  let previousTimestamp = -1;
6220
- const seenTimestamps = /* @__PURE__ */ new Set();
6221
5816
  for (const [position, row] of result.rows.entries()) {
6222
5817
  const timestampMillis = databaseInteger(row.created_at);
6223
- if (timestampMillis === null || timestampMillis <= previousTimestamp || seenTimestamps.has(timestampMillis)) throw new Error("platform: PostgreSQL migration history is duplicated or not strictly ordered.");
5818
+ if (timestampMillis === null || timestampMillis <= previousTimestamp) throw new Error("platform: PostgreSQL migration history is duplicated or not strictly ordered.");
6224
5819
  const expected = database.migrations[position];
6225
5820
  if (!expected || timestampMillis !== expected.timestampMillis || row.hash !== expected.sha256) throw new Error("platform: PostgreSQL migration history is not an exact prefix of this Void Platform package. Refusing to apply edited, forked, or out-of-order migrations.");
6226
5821
  previousTimestamp = timestampMillis;
6227
- seenTimestamps.add(timestampMillis);
6228
5822
  }
6229
5823
  const applied = database.migrations.slice(0, result.rows.length);
6230
5824
  return {
@@ -6359,15 +5953,21 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
6359
5953
  release.verifyDatabase = (runtimeDirectory, database) => verifyPlatformDatabaseConnection(client, runtimeDirectory, database);
6360
5954
  return release;
6361
5955
  }
6362
- async function preparePlatformSandboxDrainConnection(client, requiredAdmissionProtocol = 0) {
6363
- if (!(await client.query(`SELECT to_regclass('"sandbox_drain_checkpoints"')::text AS "relation"`)).rows[0]?.relation) throw new Error("platform: Runtime declares sandbox-drain safety but its database checkpoint table is missing.");
6364
- const hasAdmissionColumn = (await client.query(`SELECT EXISTS(
5956
+ /** Null when the sandbox-drain checkpoint table does not exist. */
5957
+ async function readSandboxDrainTable(client) {
5958
+ if (!(await client.query(`SELECT to_regclass('"sandbox_drain_checkpoints"')::text AS "relation"`)).rows[0]?.relation) return null;
5959
+ return { hasAdmissionColumn: (await client.query(`SELECT EXISTS(
6365
5960
  SELECT 1
6366
5961
  FROM information_schema.columns
6367
5962
  WHERE table_schema = 'public'
6368
5963
  AND table_name = 'sandbox_drain_checkpoints'
6369
5964
  AND column_name = 'admission_protocol'
6370
- ) AS "present"`)).rows[0]?.present === true;
5965
+ ) AS "present"`)).rows[0]?.present === true };
5966
+ }
5967
+ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionProtocol = 0) {
5968
+ const table = await readSandboxDrainTable(client);
5969
+ if (!table) throw new Error("platform: Runtime declares sandbox-drain safety but its database checkpoint table is missing.");
5970
+ const { hasAdmissionColumn } = table;
6371
5971
  if (requiredAdmissionProtocol >= 2 && !hasAdmissionColumn) throw new Error("platform: The runtime database does not include the sandbox deployment admission barrier.");
6372
5972
  const readCheckpoint = () => client.query(`SELECT ${hasAdmissionColumn ? "\"admission_protocol\"" : "0 AS \"admission_protocol\""}, "generation", "state", "phase", "cursor", "started_at", "updated_at", "completed_at"
6373
5973
  FROM "sandbox_drain_checkpoints"
@@ -6471,14 +6071,9 @@ async function verifyPlatformDatabaseConnection(client, runtimeDirectory, databa
6471
6071
  if (state.pendingMigrations.length !== 0 || state.schemaDigest !== database.schemaDigest) throw new Error("platform: PostgreSQL is not at the exact forward-only schema recorded by the installed runtime.");
6472
6072
  }
6473
6073
  async function readRequiredPlatformSafetyProtocols(client) {
6474
- if (!(await client.query(`SELECT to_regclass('"sandbox_drain_checkpoints"')::text AS "relation"`)).rows[0]?.relation) return { sandboxDrain: 0 };
6475
- if ((await client.query(`SELECT EXISTS(
6476
- SELECT 1
6477
- FROM information_schema.columns
6478
- WHERE table_schema = 'public'
6479
- AND table_name = 'sandbox_drain_checkpoints'
6480
- AND column_name = 'admission_protocol'
6481
- ) AS "present"`)).rows[0]?.present === true) {
6074
+ const table = await readSandboxDrainTable(client);
6075
+ if (!table) return { sandboxDrain: 0 };
6076
+ if (table.hasAdmissionColumn) {
6482
6077
  const admission = await client.query(`SELECT EXISTS(
6483
6078
  SELECT 1
6484
6079
  FROM "sandbox_drain_checkpoints"
@@ -6498,7 +6093,7 @@ async function readRequiredPlatformSafetyProtocols(client) {
6498
6093
  ) AS "required"`)).rows[0]?.required === true ? 1 : 0 };
6499
6094
  }
6500
6095
  function assertPlatformRuntimeSafety(runtime, installed, database = { sandboxDrain: 0 }) {
6501
- const requiredSandboxDrain = Math.max(installed?.sandboxDrain ?? 0, database?.sandboxDrain ?? 0);
6096
+ const requiredSandboxDrain = Math.max(installed.sandboxDrain, database.sandboxDrain);
6502
6097
  if ((runtime.safetyProtocols?.sandboxDrain ?? 0) < requiredSandboxDrain) throw new Error(`platform: Void Platform ${runtime.platformVersion} predates the required sandbox-drain safety protocol. Refusing to upload or activate a runtime that could reopen sandbox creation after drain.`);
6503
6098
  }
6504
6099
  async function claimPlatformDatabaseConnection(client, installationId, ownershipNonce) {
@@ -6544,48 +6139,49 @@ async function claimPlatformDatabaseConnection(client, installationId, ownership
6544
6139
  //#endregion
6545
6140
  //#region src/cli/platform-install-provisioning.ts
6546
6141
  async function provisionInstallationServices(input) {
6547
- const { plan, now, releaseDatabaseLock, dependencies, secrets, writeRemoteInstallationCheckpoint, checkpointOperation, externalHyperdriveOriginHash, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers } = input;
6142
+ const { state, now, releaseDatabaseLock, dependencies, writeRemoteInstallationCheckpoint, checkpointOperation, externalHyperdriveOriginHash, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers } = input;
6143
+ const { plan, secrets } = state;
6548
6144
  const routingAction = plan.actions.find((action) => action.key === "routing-kv");
6549
6145
  if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
6550
6146
  if (routingAction.action === "reuse") {
6551
- if (!input.manifest.resources.some((resource) => resource.key === routingAction.key) && routingAction.id) {
6552
- input.manifest = recordResource(input.manifest, routingAction, routingAction.id, "created", now());
6553
- writeInstallationManifest(input.manifest);
6147
+ if (!state.manifest.resources.some((resource) => resource.key === routingAction.key) && routingAction.id) {
6148
+ state.manifest = recordResource(state.manifest, routingAction, routingAction.id, "created", now());
6149
+ writeInstallationManifest(state.manifest);
6554
6150
  }
6555
6151
  } else {
6556
- input.manifest = recordProvisionIntent(input.manifest, routingAction, now());
6557
- writeInstallationManifest(input.manifest);
6558
- await releaseDatabaseLock.checkpoint?.(input.manifest);
6152
+ state.manifest = recordProvisionIntent(state.manifest, routingAction, now());
6153
+ writeInstallationManifest(state.manifest);
6154
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
6559
6155
  const routing = await createResource(dependencies.client, plan, routingAction, secrets.databaseUrl, "");
6560
- input.manifest = recordResource(input.manifest, routingAction, routing.id, "created", now());
6561
- writeInstallationManifest(input.manifest);
6156
+ state.manifest = recordResource(state.manifest, routingAction, routing.id, "created", now());
6157
+ writeInstallationManifest(state.manifest);
6562
6158
  }
6563
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6159
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
6564
6160
  const zoneAction = plan.actions.find((action) => action.key === "zone");
6565
6161
  let zone = plan.zone;
6566
6162
  if (zoneAction) {
6567
6163
  if (zoneAction.action === "create") {
6568
- input.manifest = recordProvisionIntent(input.manifest, zoneAction, now());
6569
- writeInstallationManifest(input.manifest);
6570
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6164
+ state.manifest = recordProvisionIntent(state.manifest, zoneAction, now());
6165
+ writeInstallationManifest(state.manifest);
6166
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
6571
6167
  zone = await dependencies.client.createZone(plan.account.id, zoneAction.name);
6572
6168
  }
6573
6169
  if (!zone) throw new Error("platform: Cloudflare did not return the platform zone.");
6574
6170
  if (zoneAction.action === "adopt") {
6575
- input.manifest = recordAdoptionIntent(input.manifest, zoneAction, zone.id, now());
6576
- writeInstallationManifest(input.manifest);
6577
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6171
+ state.manifest = recordAdoptionIntent(state.manifest, zoneAction, zone.id, now());
6172
+ writeInstallationManifest(state.manifest);
6173
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
6578
6174
  }
6579
- input.manifest = recordResource(input.manifest, zoneAction, zone.id, zoneAction.action === "adopt" ? "adopted" : zoneAction.action === "create" || hasProvisionIntent(input.manifest, zoneAction) ? "created" : void 0, now());
6580
- writeInstallationManifest(input.manifest);
6581
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6175
+ state.manifest = recordResource(state.manifest, zoneAction, zone.id, zoneAction.action === "adopt" ? "adopted" : zoneAction.action === "create" || hasProvisionIntent(state.manifest, zoneAction) ? "created" : void 0, now());
6176
+ writeInstallationManifest(state.manifest);
6177
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
6582
6178
  if (zone.status !== "active") {
6583
- input.manifest = transitionInstallation(input.manifest, "waiting-for-dns", now());
6179
+ state.manifest = transitionInstallation(state.manifest, "waiting-for-dns", now());
6584
6180
  await checkpointOperation("waiting-for-dns");
6585
- writeInstallationManifest(input.manifest);
6586
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6181
+ writeInstallationManifest(state.manifest);
6182
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
6587
6183
  return {
6588
- manifest: input.manifest,
6184
+ manifest: state.manifest,
6589
6185
  waitingForDns: true,
6590
6186
  waitingForSandboxDrain: false,
6591
6187
  zoneNameServers: zone.nameServers
@@ -6593,47 +6189,47 @@ async function provisionInstallationServices(input) {
6593
6189
  }
6594
6190
  }
6595
6191
  const zoneId = zone?.id ?? "";
6596
- input.manifest = await reconcilePlatformInstallationResources(input.manifest, plan, secrets, zoneId, externalHyperdriveOriginHash, dependencies, now, writeRemoteInstallationCheckpoint, (next) => {
6597
- input.manifest = next;
6192
+ state.manifest = await reconcilePlatformInstallationResources(state.manifest, plan, secrets, zoneId, externalHyperdriveOriginHash, dependencies, now, writeRemoteInstallationCheckpoint, (next) => {
6193
+ state.manifest = next;
6598
6194
  });
6599
6195
  dependencies.onProgress?.("Verifying uncached authentication database reads");
6600
- const authenticationDatabase = input.manifest.resources.find((resource) => resource.key === "hyperdrive");
6196
+ const authenticationDatabase = state.manifest.resources.find((resource) => resource.key === "hyperdrive");
6601
6197
  if (!authenticationDatabase) throw new Error("platform: The authentication database resource is missing.");
6602
6198
  await verifyPlatformAuthenticationDatabase(dependencies.client, plan.account.id, authenticationDatabase, secrets.databaseUrl);
6603
- input.manifest = await preparePlatformDatabaseSchema(input.manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan, secrets, dependencies, releaseDatabaseLock, now, writeRemoteInstallationCheckpoint, (next) => {
6604
- input.manifest = next;
6199
+ state.manifest = await preparePlatformDatabaseSchema(state.manifest, runtime, runtimeDirectory, sourcePlatformVersion, hadPriorWorkers, plan, dependencies, releaseDatabaseLock, now, writeRemoteInstallationCheckpoint, (next) => {
6200
+ state.manifest = next;
6605
6201
  });
6606
6202
  dependencies.onProgress?.("Checking platform safety");
6607
- const targetSandboxDrainProtocol = runtime.safetyProtocols?.sandboxDrain ?? 0;
6203
+ const targetSandboxDrainProtocol = runtime.safetyProtocols.sandboxDrain;
6608
6204
  const prepareSandboxDrain = dependencies.prepareSandboxDrain ?? releaseDatabaseLock.prepareSandboxDrain;
6609
6205
  if (targetSandboxDrainProtocol >= 1 && !prepareSandboxDrain) throw new Error("platform: The PostgreSQL lifecycle lock cannot establish the sandbox-drain safety checkpoint.");
6610
6206
  const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 3 ? 3 : targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
6611
6207
  if (sandboxDrainPreparation && sandboxDrainPreparation.admissionProtocol < targetSandboxDrainProtocol) throw new Error("platform: The database did not arm the Sandbox admission protocol required by this runtime.");
6612
6208
  const sandboxDrainState = sandboxDrainPreparation?.state ?? "complete";
6613
- if (targetSandboxDrainProtocol >= 1 && (input.manifest.safetyProtocols?.sandboxDrain ?? 0) < targetSandboxDrainProtocol) {
6614
- input.manifest = {
6615
- ...input.manifest,
6209
+ if (targetSandboxDrainProtocol >= 1 && (state.manifest.safetyProtocols?.sandboxDrain ?? 0) < targetSandboxDrainProtocol) {
6210
+ state.manifest = {
6211
+ ...state.manifest,
6616
6212
  safetyProtocols: { sandboxDrain: targetSandboxDrainProtocol },
6617
6213
  updatedAt: now()
6618
6214
  };
6619
- writeInstallationManifest(input.manifest);
6620
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6215
+ writeInstallationManifest(state.manifest);
6216
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, state.manifest);
6621
6217
  }
6622
- input.manifest = transitionInstallation(input.manifest, "deploying", now());
6218
+ state.manifest = transitionInstallation(state.manifest, "deploying", now());
6623
6219
  await checkpointOperation("deploying");
6624
- writeInstallationManifest(input.manifest);
6625
- const ids = Object.fromEntries(input.manifest.resources.map((resource) => [resource.key, resource.id]));
6220
+ writeInstallationManifest(state.manifest);
6221
+ const ids = Object.fromEntries(state.manifest.resources.map((resource) => [resource.key, resource.id]));
6626
6222
  const bootstrapStep = "bootstrap-access:v1";
6627
- if (!input.manifest.steps.some((step) => step.key === bootstrapStep)) {
6223
+ if (!state.manifest.steps.some((step) => step.key === bootstrapStep)) {
6628
6224
  const routingKv = requireId(ids, "routing-kv");
6629
6225
  await dependencies.client.putKvValue(plan.account.id, routingKv, "signup:enabled", "true");
6630
6226
  await dependencies.client.putKvValue(plan.account.id, routingKv, "signup:allowlist", JSON.stringify([]));
6631
- input.manifest = recordInstallationStep(input.manifest, {
6227
+ state.manifest = recordInstallationStep(state.manifest, {
6632
6228
  completedAt: now(),
6633
6229
  key: bootstrapStep,
6634
6230
  resultHash: hash(secrets.authenticationSetupJson ?? secrets.bootstrapGitHubId)
6635
6231
  }, now());
6636
- writeInstallationManifest(input.manifest);
6232
+ writeInstallationManifest(state.manifest);
6637
6233
  }
6638
6234
  const entrypoints = Object.fromEntries(Object.entries(runtime.artifacts).map(([key, artifact]) => [key, join(runtimeDirectory, artifact.path)]));
6639
6235
  return {
@@ -6647,182 +6243,7 @@ async function provisionInstallationServices(input) {
6647
6243
  };
6648
6244
  }
6649
6245
  //#endregion
6650
- //#region src/cli/platform-install-workers.ts
6651
- async function uploadInstallationWorkers(input) {
6652
- const { dependencies, requiresStagedCommit, workerSecretUpdateIntent, recoveringUnsafeTraffic, workerActions, workerKeys, workerDeploymentSecrets, plan, zoneId, now, writeRemoteInstallationCheckpoint, configs, previousConfigs, cloudflareApiBaseUrl, workerCutoverGeneration, preserveDisabled, managedRoutes, previousPublicEndpoints, ids, runParallelPinnedPlatformWrangler, runPinnedPlatformWrangler, workerRollbacks } = input;
6653
- const parallelWorkerUploads = !dependencies.deployWorker && !requiresStagedCommit && !workerSecretUpdateIntent && !recoveringUnsafeTraffic && workerActions.length === workerKeys.length && workerActions.every((action) => action.action === "update");
6654
- let workerManifestQueue = Promise.resolve();
6655
- let workerManifestFailed = false;
6656
- let workerManifestFailure;
6657
- const checkpointWorkerManifest = (update, safetyRecovery = false) => {
6658
- if (!parallelWorkerUploads) return update();
6659
- const result = workerManifestQueue.then(async () => {
6660
- if (workerManifestFailed && !safetyRecovery) throw workerManifestFailure;
6661
- try {
6662
- await update();
6663
- } catch (error) {
6664
- workerManifestFailed = true;
6665
- workerManifestFailure = error;
6666
- throw error;
6667
- }
6668
- });
6669
- workerManifestQueue = result.then(() => void 0, () => void 0);
6670
- return result;
6671
- };
6672
- let availableUploadSlots = 2;
6673
- let uploadFailure;
6674
- const uploadWaiters = [];
6675
- const acquireUploadSlot = async () => {
6676
- if (uploadFailure !== void 0) throw uploadFailure;
6677
- if (availableUploadSlots === 0) await new Promise((resolve, reject) => uploadWaiters.push({
6678
- resolve,
6679
- reject
6680
- }));
6681
- else availableUploadSlots--;
6682
- return (failure) => {
6683
- if (failure !== void 0 && uploadFailure === void 0) {
6684
- uploadFailure = failure;
6685
- for (const waiter of uploadWaiters.splice(0)) waiter.reject(failure);
6686
- }
6687
- const next = uploadFailure === void 0 ? uploadWaiters.shift() : void 0;
6688
- if (next) next.resolve();
6689
- else availableUploadSlots++;
6690
- };
6691
- };
6692
- const workerReady = (parallelWorkerUploads ? workerKeys : []).map(() => {
6693
- let resolve;
6694
- let reject;
6695
- return {
6696
- promise: new Promise((onResolve, onReject) => {
6697
- resolve = onResolve;
6698
- reject = onReject;
6699
- }),
6700
- resolve,
6701
- reject
6702
- };
6703
- });
6704
- const uploadBatch = Promise.all(workerReady.map((ready) => ready.promise)).then(() => ({ error: null }), (error) => ({ error }));
6705
- const deployOneWorker = async (key, waitForPriorWorker, onReadyForActivation) => {
6706
- dependencies.onProgress?.(`Deploying the ${key} service`);
6707
- let workerStepStarted = performance.now();
6708
- const markWorkerStep = (step) => {
6709
- if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] worker ${key} ${step} ${(performance.now() - workerStepStarted).toFixed(0)} ms\n`);
6710
- workerStepStarted = performance.now();
6711
- };
6712
- const action = workerActions.find((entry) => entry.key === (key === "emailGateway" ? "email-gateway-worker" : `${key}-worker`));
6713
- const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
6714
- await checkpointWorkerManifest(async () => {
6715
- const intended = recordProvisionIntent(input.manifest, action, now());
6716
- if (intended !== input.manifest) {
6717
- input.manifest = intended;
6718
- writeInstallationManifest(input.manifest);
6719
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6720
- }
6721
- });
6722
- await dependencies.revalidateBeforeWorkerDeploy(input.manifest, action);
6723
- markWorkerStep("revalidate");
6724
- const preservedRoutes = dependencies.deployWorker ? [] : action.action === "create" ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
6725
- markWorkerStep("traffic inventory");
6726
- const workerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
6727
- const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
6728
- const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
6729
- apiBaseUrl: cloudflareApiBaseUrl,
6730
- operatorApiToken: dependencies.operatorApiToken,
6731
- ...parallelWorkerUploads ? {
6732
- acquireUploadSlot,
6733
- onReadyForActivation,
6734
- waitForPriorWorker
6735
- } : {},
6736
- ...dependencies.client.request ? { readCloudflare: (path) => dependencies.client.request(path) } : {},
6737
- onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
6738
- await checkpointWorkerManifest(async () => {
6739
- input.manifest = recordWorkerCutoverStage(input.manifest, workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
6740
- writeInstallationManifest(input.manifest);
6741
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6742
- });
6743
- },
6744
- onTrafficPublished: async () => {
6745
- if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
6746
- const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
6747
- await checkpointWorkerManifest(async () => {
6748
- const next = recordPlatformWorkerTraffic(input.manifest, action.name, {
6749
- customDomains,
6750
- routes
6751
- }, now());
6752
- if (JSON.stringify(next.resources) !== JSON.stringify(input.manifest.resources)) {
6753
- input.manifest = next;
6754
- writeInstallationManifest(input.manifest);
6755
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6756
- }
6757
- });
6758
- },
6759
- onUnsafeTraffic: async () => {
6760
- await checkpointWorkerManifest(async () => {
6761
- input.manifest = recordWorkerUnsafeTraffic(input.manifest, workerCutoverGeneration, key, action.name, now());
6762
- writeInstallationManifest(input.manifest);
6763
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6764
- }, true);
6765
- },
6766
- managedRoutes: preserveDisabled ? [] : managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
6767
- preservedRoutes,
6768
- publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
6769
- publishRoutes: async (workerName, patterns) => {
6770
- if (!dependencies.client.publishWorkerRoutesSafely) throw new Error("platform: Safe Cloudflare Worker route publication is unavailable.");
6771
- if (zoneId) await dependencies.client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
6772
- else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
6773
- },
6774
- previousConfig: previousWorkerConfig,
6775
- previousPublicEndpoints,
6776
- inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, input.manifest, action.key, action.name),
6777
- removeCustomDomains: async () => {
6778
- if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
6779
- await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(input.manifest, /* @__PURE__ */ new Set([action.name])));
6780
- },
6781
- retainNewWorker: async () => {
6782
- await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
6783
- },
6784
- runner: parallelWorkerUploads ? runParallelPinnedPlatformWrangler : runPinnedPlatformWrangler,
6785
- workerExisted: action.action !== "create"
6786
- });
6787
- markWorkerStep("deploy");
6788
- if (rollback) workerRollbacks.push(rollback);
6789
- await checkpointWorkerManifest(async () => {
6790
- const recorded = recordResource(input.manifest, action, action.name, "created", now());
6791
- if (rollback || dependencies.deployWorker || JSON.stringify(recorded.resources) !== JSON.stringify(input.manifest.resources)) {
6792
- input.manifest = recorded;
6793
- writeInstallationManifest(input.manifest);
6794
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, input.manifest);
6795
- }
6796
- });
6797
- markWorkerStep("record");
6798
- };
6799
- if (parallelWorkerUploads) {
6800
- const tasks = [];
6801
- for (const [index, key] of workerKeys.entries()) {
6802
- const priorWorker = tasks.at(-1);
6803
- const ready = workerReady[index];
6804
- const task = deployOneWorker(key, priorWorker ? () => priorWorker : void 0, async () => {
6805
- ready.resolve();
6806
- const batch = await uploadBatch;
6807
- if (batch.error !== null) throw batch.error;
6808
- });
6809
- task.then(() => ready.resolve(), (error) => ready.reject(error));
6810
- tasks.push(task);
6811
- }
6812
- const firstFailure = (await Promise.allSettled(tasks)).find((result) => result.status === "rejected");
6813
- if (firstFailure) throw firstFailure.reason;
6814
- } else for (const key of workerKeys) await deployOneWorker(key);
6815
- }
6816
- //#endregion
6817
6246
  //#region src/cli/platform-secret-update-authority.ts
6818
- const COMPLETE_WORKER_SECRET_UPDATE_NAMES = [
6819
- "CF_API_TOKEN",
6820
- "GITHUB_CLIENT_ID",
6821
- "GITHUB_CLIENT_SECRET",
6822
- "R2_ACCESS_KEY_ID",
6823
- "R2_SECRET_ACCESS_KEY"
6824
- ];
6825
- const EMAIL_WORKER_SECRET_UPDATE_NAME = "EMAIL_SIGNING_SECRET";
6826
6247
  /**
6827
6248
  * Prevents a host without the encrypted values for an interrupted credential
6828
6249
  * update from carrying an opaque, possibly partial update into another cutover.
@@ -6860,33 +6281,34 @@ function readLatestWorkerGeneration(manifest) {
6860
6281
  }));
6861
6282
  }
6862
6283
  function hasCompleteWorkerSecretReplacement(updates, requiresEmailSigningSecret) {
6863
- return COMPLETE_WORKER_SECRET_UPDATE_NAMES.every((name) => typeof updates[name] === "string" && updates[name] !== "") && (!requiresEmailSigningSecret || typeof updates[EMAIL_WORKER_SECRET_UPDATE_NAME] === "string" && updates[EMAIL_WORKER_SECRET_UPDATE_NAME] !== "");
6284
+ return PLATFORM_WORKER_SECRET_UPDATE_NAMES.every((name) => name === "EMAIL_SIGNING_SECRET" && !requiresEmailSigningSecret || typeof updates[name] === "string" && updates[name] !== "");
6864
6285
  }
6865
6286
  //#endregion
6866
6287
  //#region src/cli/platform-installer.ts
6867
- async function recoverFailedPlatformInstallation(error, manifest, plan, dependencies, workerTrafficGuarded, workerDeploymentCommitted, workerRollbacks, authenticationEnforcementActive, authenticationProtocolTransition, workerCutoverGeneration, previousPublicEndpoints, operationStarted, now, writeRemoteInstallationCheckpoint) {
6288
+ async function recoverFailedPlatformInstallation(error, state) {
6289
+ const { plan, dependencies, workerTrafficGuarded, workerDeploymentCommitted, workerRollbacks, authenticationEnforcementActive, authenticationProtocolTransition, workerCutoverGeneration, previousPublicEndpoints, operationStarted, now, writeRemoteInstallationCheckpoint } = state;
6290
+ let { manifest } = state;
6868
6291
  let failure = error;
6869
6292
  const currentWorkerRecoveryIncomplete = error instanceof PlatformWorkerRecoveryIncompleteError;
6870
6293
  if (workerTrafficGuarded) try {
6871
6294
  await dependencies.client.putKvValue(plan.account.id, requireId(Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id])), "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
6872
6295
  } catch (guardError) {
6873
- failure = new Error(`platform: ${errorMessage(error)} The installation also failed to retain its routing safety gate: ${errorMessage(guardError)}`, { cause: error });
6296
+ failure = new Error(`platform: ${getErrorMessage(error)} The installation also failed to retain its routing safety gate: ${getErrorMessage(guardError)}`, { cause: error });
6874
6297
  }
6875
6298
  if (!workerDeploymentCommitted && workerRollbacks.length > 0 && !(authenticationEnforcementActive && authenticationProtocolTransition)) {
6876
6299
  const rollbackFailures = [];
6877
6300
  for (const rollback of workerRollbacks.reverse()) try {
6878
6301
  await rollback();
6879
6302
  } catch (rollbackError) {
6880
- rollbackFailures.push(errorMessage(rollbackError));
6303
+ rollbackFailures.push(getErrorMessage(rollbackError));
6881
6304
  }
6882
- workerRollbacks = [];
6883
- if (rollbackFailures.length > 0 || currentWorkerRecoveryIncomplete) failure = new Error(rollbackFailures.length > 0 ? `platform: ${errorMessage(error)} Automatic Worker rollback was incomplete: ${rollbackFailures.join("; ")} Inspect the installation before retrying.` : `platform: ${errorMessage(error)} The current Worker rollback was incomplete. The cutover remains unresolved and will be recovered on retry.`, { cause: error });
6305
+ if (rollbackFailures.length > 0 || currentWorkerRecoveryIncomplete) failure = new Error(rollbackFailures.length > 0 ? `platform: ${getErrorMessage(error)} Automatic Worker rollback was incomplete: ${rollbackFailures.join("; ")} Inspect the installation before retrying.` : `platform: ${getErrorMessage(error)} The current Worker rollback was incomplete. The cutover remains unresolved and will be recovered on retry.`, { cause: error });
6884
6306
  else if (workerCutoverGeneration > 0) {
6885
6307
  manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "restored", now());
6886
6308
  manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, previousPublicEndpoints, now());
6887
6309
  writeInstallationManifest(manifest);
6888
6310
  }
6889
- } else if (authenticationEnforcementActive && authenticationProtocolTransition) workerRollbacks = [];
6311
+ }
6890
6312
  if (manifest.status !== "uninstalled" && (manifest.status !== "failed" || operationStarted)) try {
6891
6313
  const updatedAt = now();
6892
6314
  manifest = transitionInstallation(manifest, "failed", updatedAt);
@@ -6906,9 +6328,10 @@ async function recoverFailedPlatformInstallation(error, manifest, plan, dependen
6906
6328
  async function installPlatform(plan, secrets, existing, dependencies) {
6907
6329
  if (plan.actions.some((action) => action.action === "inspect")) throw new Error("platform: AI Gateway access must be verified before provisioning. Provide AI Gateway: Edit on the runtime token, then retry.");
6908
6330
  const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
6909
- const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl ?? resolvePinnedApiBaseUrl();
6910
- const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, false, dependencies.operatorApiToken);
6911
- const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true, dependencies.operatorApiToken);
6331
+ const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl;
6332
+ const pinnedPlatformWrangler = (parallelWorkerUpload) => (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, parallelWorkerUpload, dependencies.operatorApiToken);
6333
+ const runPinnedPlatformWrangler = pinnedPlatformWrangler(false);
6334
+ const runParallelPinnedPlatformWrangler = pinnedPlatformWrangler(true);
6912
6335
  const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
6913
6336
  const runtimeDirectory = resolvedRuntime.directory;
6914
6337
  const runtime = resolvedRuntime.manifest;
@@ -6959,18 +6382,13 @@ async function installPlatform(plan, secrets, existing, dependencies) {
6959
6382
  const targetUsageProtocolVersion = runtime.usageProtocolVersion;
6960
6383
  const sourcePlatformVersion = existing?.platformVersion ?? null;
6961
6384
  const hadPriorWorkers = existing !== null && (existing.resources.some((resource) => resource.kind === "worker" && resource.state === "ready") || plan.actions.some((action) => action.kind === "worker" && action.action !== "create"));
6962
- const wasDisabled = existing?.status === "disabled";
6963
6385
  const preserveDisabled = existing?.desiredState === "disabled";
6964
6386
  const shouldBootstrapSharedEmail = !preserveDisabled && plan.deployment.capabilities.email && runtime.emailProtocolVersion === 1 && !!plan.deployment.emailSharedZoneId;
6965
6387
  const previousPublicEndpoints = stablePublicEndpointState(existing);
6966
6388
  const authenticationProtocolTransition = hadPriorWorkers && existing?.deployment.authentication.configurable !== true && runtime.authenticationProtocolVersion === 1;
6967
- let workerCutoverGeneration = 0;
6968
- let workerRollbacks = [];
6969
- let workerDeploymentCommitted = false;
6970
6389
  let workerTrafficGuarded = false;
6971
- let authenticationEnforcementActive = false;
6972
- let manifest = existing;
6973
- if (!manifest) {
6390
+ let manifest;
6391
+ if (!existing) {
6974
6392
  const createdAt = now();
6975
6393
  manifest = createPlatformInstallationManifest({
6976
6394
  account: plan.account,
@@ -6982,11 +6400,12 @@ async function installPlatform(plan, secrets, existing, dependencies) {
6982
6400
  });
6983
6401
  manifest = transitionInstallation(manifest, "planned", now());
6984
6402
  writeInstallationManifest(manifest);
6985
- } else if (manifest.status === "uninstalled") throw new Error(`platform: Installation ${manifest.id} has already been uninstalled.`);
6403
+ } else if (existing.status === "uninstalled") throw new Error(`platform: Installation ${existing.id} has already been uninstalled.`);
6404
+ else manifest = existing;
6986
6405
  const explicitWorkerSecretUpdates = sanitizeWorkerSecretUpdates(secrets.workerSecretUpdates);
6987
6406
  const hasExplicitWorkerSecretUpdates = Object.keys(explicitWorkerSecretUpdates).length > 0;
6988
6407
  const resumedWorkerSecretUpdateIntent = secrets.pendingWorkerSecretUpdateIntent && platformWorkerSecretUpdateIntentState(manifest, secrets.pendingWorkerSecretUpdateIntent) === "active" ? secrets.pendingWorkerSecretUpdateIntent : null;
6989
- let workerSecretUpdateIntent = hasExplicitWorkerSecretUpdates ? {
6408
+ const workerSecretUpdateIntent = hasExplicitWorkerSecretUpdates ? {
6990
6409
  baselineGeneration: latestPlatformWorkerCutoverGeneration(manifest),
6991
6410
  generation: null,
6992
6411
  id: randomUUID(),
@@ -6996,10 +6415,6 @@ async function installPlatform(plan, secrets, existing, dependencies) {
6996
6415
  ...explicitWorkerSecretUpdates
6997
6416
  }
6998
6417
  } : resumedWorkerSecretUpdateIntent;
6999
- if (preserveDisabled && manifest.desiredState !== "disabled") {
7000
- manifest = setInstallationDesiredState(manifest, "disabled", now());
7001
- writeInstallationManifest(manifest);
7002
- }
7003
6418
  assertRequiredPlatformSecrets(plan, manifest, secrets, !!dependencies.prepareAuthentication);
7004
6419
  if (hyperdriveAction?.action === "create") {
7005
6420
  parsePostgresConnectionString(secrets.databaseUrl);
@@ -7009,30 +6424,40 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7009
6424
  ...secrets,
7010
6425
  externalHyperdriveJson: JSON.stringify(plan.externalHyperdrive)
7011
6426
  };
7012
- const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
7013
- writePendingInstallationSecrets(manifest, {
6427
+ const state = {
6428
+ manifest,
6429
+ plan,
6430
+ secrets,
6431
+ workerSecretUpdateIntent,
6432
+ workerCutoverGeneration: 0,
6433
+ workerRollbacks: [],
6434
+ workerDeploymentCommitted: false,
6435
+ authenticationEnforcementActive: false
6436
+ };
6437
+ const pendingSecrets = secretCheckpoint(state.secrets, state.workerSecretUpdateIntent);
6438
+ writePendingInstallationSecrets(state.manifest, {
7014
6439
  ...pendingSecrets,
7015
6440
  databaseClaimState: "claiming"
7016
6441
  });
7017
6442
  dependencies.onProgress?.("Preparing the platform database");
7018
6443
  try {
7019
- await (dependencies.claimDatabase ?? claimPlatformDatabase)(secrets.databaseUrl, manifest.id, manifest.ownershipNonce);
6444
+ await (dependencies.claimDatabase ?? claimPlatformDatabase)(state.secrets.databaseUrl, state.manifest.id, state.manifest.ownershipNonce);
7020
6445
  } catch (error) {
7021
- writePendingInstallationSecrets(manifest, {
6446
+ writePendingInstallationSecrets(state.manifest, {
7022
6447
  ...pendingSecrets,
7023
6448
  databaseClaimState: "failed"
7024
6449
  });
7025
6450
  throw error;
7026
6451
  }
7027
- writePendingInstallationSecrets(manifest, {
6452
+ writePendingInstallationSecrets(state.manifest, {
7028
6453
  ...pendingSecrets,
7029
6454
  databaseClaimState: "claimed"
7030
6455
  });
7031
6456
  dependencies.onProgress?.("Acquiring the database lock");
7032
- const releaseDatabaseLock = await dependencies.acquireDatabaseLifecycleLock(secrets.databaseUrl, manifest.id, manifest.ownershipNonce);
6457
+ const releaseDatabaseLock = await dependencies.acquireDatabaseLifecycleLock(state.secrets.databaseUrl, state.manifest.id, state.manifest.ownershipNonce);
7033
6458
  const writeRemoteInstallationCheckpoint = (client, checkpointPlan, checkpointManifest) => writeRemoteInstallationCheckpointWithoutDatabase(client, checkpointPlan, checkpointManifest, releaseDatabaseLock);
7034
6459
  const authoritative = releaseDatabaseLock.authoritativeManifest;
7035
- if (authoritative && JSON.stringify(authoritative) !== JSON.stringify(manifest)) {
6460
+ if (authoritative && JSON.stringify(authoritative) !== JSON.stringify(state.manifest)) {
7036
6461
  writeInstallationManifest(authoritative);
7037
6462
  await releaseDatabaseLock();
7038
6463
  throw new Error("platform: This installation changed on another host. Local state was refreshed; rerun the command to review a current plan.");
@@ -7040,41 +6465,40 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7040
6465
  try {
7041
6466
  assertPlatformWorkerSecretUpdateAuthority({
7042
6467
  explicitUpdates: explicitWorkerSecretUpdates,
7043
- localIntent: secrets.pendingWorkerSecretUpdateIntent,
7044
- manifest,
7045
- requiresEmailSigningSecret: manifest.deployment.capabilities.email || plan.deployment.capabilities.email
6468
+ localIntent: state.secrets.pendingWorkerSecretUpdateIntent,
6469
+ manifest: state.manifest,
6470
+ requiresEmailSigningSecret: state.manifest.deployment.capabilities.email || state.plan.deployment.capabilities.email
7046
6471
  });
7047
6472
  } catch (error) {
7048
6473
  await releaseDatabaseLock();
7049
6474
  throw error;
7050
6475
  }
7051
- if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
7052
6476
  const checkpointOperation = async (phase) => {
7053
- if (!manifest?.operation) return;
6477
+ if (!state.manifest.operation) return;
7054
6478
  const updatedAt = now();
7055
- manifest = {
7056
- ...manifest,
6479
+ state.manifest = {
6480
+ ...state.manifest,
7057
6481
  operation: {
7058
- ...manifest.operation,
6482
+ ...state.manifest.operation,
7059
6483
  phase,
7060
6484
  updatedAt
7061
6485
  },
7062
6486
  updatedAt
7063
6487
  };
7064
- writeInstallationManifest(manifest);
7065
- await releaseDatabaseLock.checkpoint?.(manifest);
6488
+ writeInstallationManifest(state.manifest);
6489
+ await releaseDatabaseLock.checkpoint?.(state.manifest);
7066
6490
  };
7067
6491
  let operationStarted = false;
7068
6492
  try {
7069
- assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
7070
- if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
6493
+ assertPlatformRuntimeSafety(runtime, state.manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
6494
+ if (existing) await dependencies.verifyExistingOwnership?.(state.manifest, authoritative !== null && authoritative !== void 0);
7071
6495
  const action = dependencies.operation ?? (existing ? "upgrade" : "install");
7072
- const previousOperation = manifest.operation;
6496
+ const previousOperation = state.manifest.operation;
7073
6497
  const resuming = previousOperation?.action === action && previousOperation.toVersion === version && previousOperation.runtimeManifestSha256 === resolvedRuntime.manifestSha256 && previousOperation.phase !== "complete";
7074
6498
  const startedAt = now();
7075
6499
  operationStarted = true;
7076
- manifest = {
7077
- ...manifest,
6500
+ state.manifest = {
6501
+ ...state.manifest,
7078
6502
  operation: {
7079
6503
  id: resuming ? previousOperation.id : randomUUID(),
7080
6504
  action,
@@ -7089,49 +6513,18 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7089
6513
  };
7090
6514
  await checkpointOperation("preparing");
7091
6515
  await prepareInstallationAuthentication({
6516
+ state,
7092
6517
  releaseDatabaseLock,
7093
- get authenticationEnforcementActive() {
7094
- return authenticationEnforcementActive;
7095
- },
7096
- set authenticationEnforcementActive(value) {
7097
- authenticationEnforcementActive = value;
7098
- },
7099
6518
  runtime,
7100
- get plan() {
7101
- return plan;
7102
- },
7103
- set plan(value) {
7104
- plan = value;
7105
- },
7106
6519
  authenticationProtocolTransition,
7107
- get manifest() {
7108
- return manifest;
7109
- },
7110
- set manifest(value) {
7111
- manifest = value;
7112
- },
7113
- get secrets() {
7114
- return secrets;
7115
- },
7116
- set secrets(value) {
7117
- secrets = value;
7118
- },
7119
6520
  now,
7120
- dependencies,
7121
- workerSecretUpdateIntent
6521
+ dependencies
7122
6522
  });
7123
- const provisionInstallationServicesResult = await provisionInstallationServices({
7124
- plan,
7125
- get manifest() {
7126
- return manifest;
7127
- },
7128
- set manifest(value) {
7129
- manifest = value;
7130
- },
6523
+ const provisioned = await provisionInstallationServices({
6524
+ state,
7131
6525
  now,
7132
6526
  releaseDatabaseLock,
7133
6527
  dependencies,
7134
- secrets,
7135
6528
  writeRemoteInstallationCheckpoint,
7136
6529
  checkpointOperation,
7137
6530
  externalHyperdriveOriginHash,
@@ -7140,55 +6533,49 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7140
6533
  sourcePlatformVersion,
7141
6534
  hadPriorWorkers
7142
6535
  });
7143
- if ("waitingForDns" in provisionInstallationServicesResult) return provisionInstallationServicesResult;
7144
- const zone = provisionInstallationServicesResult.zone;
7145
- const zoneId = provisionInstallationServicesResult.zoneId;
7146
- const targetSandboxDrainProtocol = provisionInstallationServicesResult.targetSandboxDrainProtocol;
7147
- const prepareSandboxDrain = provisionInstallationServicesResult.prepareSandboxDrain;
7148
- const sandboxDrainState = provisionInstallationServicesResult.sandboxDrainState;
7149
- const ids = provisionInstallationServicesResult.ids;
7150
- const entrypoints = provisionInstallationServicesResult.entrypoints;
7151
- let interruptedCutover = interruptedWorkerCutover(manifest);
7152
- if (interruptedCutover && authenticationProtocolTransition && authenticationEnforcementActive) {
7153
- manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "committed", now());
7154
- manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, false, now());
7155
- writeInstallationManifest(manifest);
7156
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
6536
+ if ("waitingForDns" in provisioned) return provisioned;
6537
+ const { zone, zoneId, targetSandboxDrainProtocol, prepareSandboxDrain, sandboxDrainState, ids, entrypoints } = provisioned;
6538
+ let interruptedCutover = interruptedWorkerCutover(state.manifest);
6539
+ if (interruptedCutover && authenticationProtocolTransition && state.authenticationEnforcementActive) {
6540
+ state.manifest = recordWorkerCutoverResolution(state.manifest, interruptedCutover.generation, "committed", now());
6541
+ state.manifest = recordWorkerTrafficState(state.manifest, interruptedCutover.generation, false, now());
6542
+ writeInstallationManifest(state.manifest);
6543
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
7157
6544
  interruptedCutover = null;
7158
6545
  }
7159
6546
  const recoveringUnsafeTraffic = (interruptedCutover?.unsafeTraffic.size ?? 0) > 0;
7160
- const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
6547
+ const createsWorker = state.plan.actions.some((action) => action.kind === "worker" && action.action === "create" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
7161
6548
  const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
7162
6549
  const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
7163
- const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain || !!plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1");
6550
+ const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || state.manifest.usageProtocolVersion !== targetUsageProtocolVersion || authenticationProtocolTransition || shouldBootstrapSharedEmail || requiresSandboxDrain || !!state.plan.deployment.appAccess && !state.manifest.steps.some((step) => step.key === "app-access-initialized:v1");
7164
6551
  if (requiresTrafficGuard) {
7165
- await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
6552
+ await dependencies.client.putKvValue(state.plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
7166
6553
  workerTrafficGuarded = true;
7167
6554
  dependencies.onProgress?.("Application traffic paused for a safe platform cutover");
7168
6555
  }
7169
6556
  if (!preserveDisabled) {
7170
- const trafficGeneration = nextTrafficGeneration(manifest);
7171
- manifest = recordInstallationStep(manifest, {
6557
+ const trafficGeneration = nextTrafficGeneration(state.manifest);
6558
+ state.manifest = recordInstallationStep(state.manifest, {
7172
6559
  completedAt: now(),
7173
6560
  key: `traffic-enabled:${trafficGeneration}`,
7174
6561
  resultHash: String(trafficGeneration)
7175
6562
  }, now());
7176
- writeInstallationManifest(manifest);
7177
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
6563
+ writeInstallationManifest(state.manifest);
6564
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
7178
6565
  }
7179
6566
  const configInput = {
7180
6567
  authenticationDatabaseUncached: true,
7181
6568
  proxyHealthBinding: true,
7182
- accountId: plan.account.id,
7183
- apiUrl: plan.apiUrl,
7184
- deployment: plan.deployment,
6569
+ accountId: state.plan.account.id,
6570
+ apiUrl: state.plan.apiUrl,
6571
+ deployment: state.plan.deployment,
7185
6572
  deploymentExecution: runtime.deploymentExecution,
7186
- healthProbeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
7187
- installationId: manifest.id,
7188
- ownershipNonce: manifest.ownershipNonce,
6573
+ healthProbeToken: createPlatformHealthProbeToken(state.manifest.ownershipNonce),
6574
+ installationId: state.manifest.id,
6575
+ ownershipNonce: state.manifest.ownershipNonce,
7189
6576
  platformVersion: version,
7190
- proxyUrl: plan.proxyUrl,
7191
- emailGatewayUrl: `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev`,
6577
+ proxyUrl: state.plan.proxyUrl,
6578
+ emailGatewayUrl: platformWorkersDevUrl(state.plan, state.plan.deployment.resources.emailGatewayWorker),
7192
6579
  resources: {
7193
6580
  hyperdrive: requireId(ids, "hyperdrive"),
7194
6581
  isrCacheKv: requireId(ids, "isr-cache-kv"),
@@ -7200,8 +6587,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7200
6587
  ...runtime.assets ? { runtimeApiAssetsDirectory: join(runtimeDirectory, PLATFORM_ADMIN_ASSETS_DIRECTORY) } : {}
7201
6588
  };
7202
6589
  const workerDeploymentSecrets = {
7203
- ...secrets,
7204
- workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
6590
+ ...state.secrets,
6591
+ workerSecretUpdates: state.workerSecretUpdateIntent?.updates ?? {}
7205
6592
  };
7206
6593
  const requiresStagedCommit = !preserveDisabled && requiresTrafficGuard;
7207
6594
  const enabledConfigs = createPlatformWorkerConfigs({
@@ -7219,80 +6606,86 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7219
6606
  publicEndpoints: previousPublicEndpoints,
7220
6607
  trafficState: previousPublicEndpoints ? "enabled" : "staging"
7221
6608
  });
7222
- const managedRoutes = platformManagedWorkerTraffic(plan.deployment).routes;
7223
- let workerActions = plan.actions.filter((action) => action.kind === "worker" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
7224
- ({manifest, workerActions} = await recoverInterruptedPlatformWorkerCutover(manifest, interruptedCutover, workerActions, configs, previousConfigs, plan, dependencies, ids, zoneId, previousPublicEndpoints, hasEmailGatewayArtifact, runPinnedPlatformWrangler, now, writeRemoteInstallationCheckpoint, (next) => {
7225
- manifest = next;
6609
+ const managedRoutes = platformManagedWorkerTraffic(state.plan.deployment).routes;
6610
+ let workerActions = state.plan.actions.filter((action) => action.kind === "worker" && (hasEmailGatewayArtifact || action.key !== "email-gateway-worker"));
6611
+ ({manifest: state.manifest, workerActions} = await recoverInterruptedPlatformWorkerCutover({
6612
+ manifest: state.manifest,
6613
+ interruptedCutover,
6614
+ workerActions,
6615
+ configs,
6616
+ previousConfigs,
6617
+ plan: state.plan,
6618
+ dependencies,
6619
+ ids,
6620
+ previousPublicEndpoints,
6621
+ hasEmailGatewayArtifact,
6622
+ runPinnedPlatformWrangler,
6623
+ now,
6624
+ writeRemoteInstallationCheckpoint,
6625
+ onManifestChange: (next) => {
6626
+ state.manifest = next;
6627
+ }
7226
6628
  }));
7227
- workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
7228
- if (workerSecretUpdateIntent) {
7229
- manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "pending", workerSecretUpdateIntent.id, now());
7230
- writeInstallationManifest(manifest);
7231
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
7232
- workerSecretUpdateIntent = {
7233
- ...workerSecretUpdateIntent,
7234
- generation: workerCutoverGeneration
6629
+ state.workerCutoverGeneration = nextWorkerCutoverGeneration(state.manifest);
6630
+ if (state.workerSecretUpdateIntent) {
6631
+ state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "pending", state.workerSecretUpdateIntent.id, now());
6632
+ writeInstallationManifest(state.manifest);
6633
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
6634
+ state.workerSecretUpdateIntent = {
6635
+ ...state.workerSecretUpdateIntent,
6636
+ generation: state.workerCutoverGeneration
7235
6637
  };
7236
- writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
6638
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, state.workerSecretUpdateIntent));
7237
6639
  }
7238
6640
  await uploadInstallationWorkers({
6641
+ state,
7239
6642
  dependencies,
7240
6643
  requiresStagedCommit,
7241
- workerSecretUpdateIntent,
7242
6644
  recoveringUnsafeTraffic,
7243
6645
  workerActions,
7244
6646
  workerKeys,
7245
6647
  workerDeploymentSecrets,
7246
- plan,
7247
6648
  zoneId,
7248
- get manifest() {
7249
- return manifest;
7250
- },
7251
- set manifest(value) {
7252
- manifest = value;
7253
- },
7254
6649
  now,
7255
6650
  writeRemoteInstallationCheckpoint,
7256
6651
  configs,
7257
6652
  previousConfigs,
7258
6653
  cloudflareApiBaseUrl,
7259
- workerCutoverGeneration,
7260
6654
  preserveDisabled,
7261
6655
  managedRoutes,
7262
6656
  previousPublicEndpoints,
7263
6657
  ids,
7264
6658
  runParallelPinnedPlatformWrangler,
7265
- runPinnedPlatformWrangler,
7266
- workerRollbacks
6659
+ runPinnedPlatformWrangler
7267
6660
  });
7268
- manifest = setInstallationEndpoints(manifest, {
7269
- apiUrl: plan.apiUrl,
7270
- dashboardUrl: platformDashboardUrl(plan.deployment),
7271
- proxyUrl: plan.proxyUrl
6661
+ state.manifest = setInstallationEndpoints(state.manifest, {
6662
+ apiUrl: state.plan.apiUrl,
6663
+ dashboardUrl: platformDashboardUrl(state.plan.deployment),
6664
+ proxyUrl: state.plan.proxyUrl
7272
6665
  }, now());
7273
6666
  if (preserveDisabled) {
7274
- workerDeploymentCommitted = true;
7275
- manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
7276
- manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
7277
- if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
7278
- manifest = {
7279
- ...manifest,
7280
- deployment: plan.deployment,
6667
+ state.workerDeploymentCommitted = true;
6668
+ state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
6669
+ state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
6670
+ if (state.workerSecretUpdateIntent) state.manifest = recordWorkerSecretUpdateState(state.manifest, state.workerCutoverGeneration, "committed", state.workerSecretUpdateIntent.id, now());
6671
+ state.manifest = {
6672
+ ...state.manifest,
6673
+ deployment: state.plan.deployment,
7281
6674
  deploymentExecution: runtime.deploymentExecution,
7282
6675
  platformVersion: version,
7283
6676
  runtime: runtimeProvenance,
7284
6677
  updatedAt: now(),
7285
6678
  usageProtocolVersion: targetUsageProtocolVersion
7286
6679
  };
7287
- manifest = transitionInstallation(manifest, "disabled", now());
6680
+ state.manifest = transitionInstallation(state.manifest, "disabled", now());
7288
6681
  await checkpointOperation("complete");
7289
- writeInstallationManifest(manifest);
7290
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
7291
- writePendingInstallationSecrets(manifest, secretCheckpoint(secrets));
7292
- promoteInstallationSecrets(manifest);
7293
- saveInstalledPlatformConnection(plan, manifest);
6682
+ writeInstallationManifest(state.manifest);
6683
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
6684
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets));
6685
+ promoteInstallationSecrets(state.manifest);
6686
+ saveInstalledPlatformConnection(state.plan, state.manifest);
7294
6687
  return {
7295
- manifest,
6688
+ manifest: state.manifest,
7296
6689
  waitingForDns: false,
7297
6690
  waitingForSandboxDrain: false,
7298
6691
  zoneNameServers: zone?.nameServers ?? []
@@ -7300,113 +6693,113 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7300
6693
  }
7301
6694
  let authenticationCutoverCommitted = false;
7302
6695
  if (authenticationProtocolTransition && requiresStagedCommit) {
7303
- manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
7304
- manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
7305
- writeInstallationManifest(manifest);
7306
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
7307
- workerRollbacks = [];
6696
+ state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
6697
+ state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
6698
+ writeInstallationManifest(state.manifest);
6699
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
6700
+ state.workerRollbacks = [];
7308
6701
  authenticationCutoverCommitted = true;
7309
6702
  }
7310
- let authenticationStep = secrets.authenticationSetupJson ? "authentication-initialized:v1:" + hash(secrets.authenticationSetupJson) : "";
6703
+ let authenticationStep = state.secrets.authenticationSetupJson ? "authentication-initialized:v1:" + hash(state.secrets.authenticationSetupJson) : "";
7311
6704
  let setupPending = false;
7312
- manifest = transitionInstallation(manifest, "verifying", now());
6705
+ state.manifest = transitionInstallation(state.manifest, "verifying", now());
7313
6706
  await checkpointOperation("verifying");
7314
- writeInstallationManifest(manifest);
7315
- if (plan.deployment.authentication.configurable && authenticationStep) {
7316
- if (!secrets.authenticationSetupJson || !secrets.authSetupToken) throw new Error("platform: Restore the encrypted authentication setup checkpoint before resuming.");
6707
+ writeInstallationManifest(state.manifest);
6708
+ if (state.plan.deployment.authentication.configurable && authenticationStep) {
6709
+ if (!state.secrets.authenticationSetupJson || !state.secrets.authSetupToken) throw new Error("platform: Restore the encrypted authentication setup checkpoint before resuming.");
7317
6710
  dependencies.onProgress?.("Initializing platform authentication");
7318
6711
  if ((await resumeInstalledPlatformAuthentication({
7319
- apiUrl: plan.apiUrl,
7320
- installationId: manifest.id,
7321
- setupToken: secrets.authSetupToken,
7322
- setupJson: secrets.authenticationSetupJson,
6712
+ apiUrl: state.plan.apiUrl,
6713
+ installationId: state.manifest.id,
6714
+ setupToken: state.secrets.authSetupToken,
6715
+ setupJson: state.secrets.authenticationSetupJson,
7323
6716
  checkpoint: (replacement) => {
7324
- secrets = {
7325
- ...secrets,
6717
+ state.secrets = {
6718
+ ...state.secrets,
7326
6719
  ...replacement
7327
6720
  };
7328
6721
  authenticationStep = "authentication-initialized:v1:" + hash(replacement.authenticationSetupJson);
7329
- writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
6722
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, state.workerSecretUpdateIntent));
7330
6723
  }
7331
6724
  }, dependencies.initializeAuthentication ?? initializeInstalledPlatformAuthentication)).setupPending) {
7332
6725
  setupPending = true;
7333
- dependencies.onProgress?.(`Administrator setup pending: ${plan.apiUrl}/setup`);
6726
+ dependencies.onProgress?.(`Administrator setup pending: ${state.plan.apiUrl}/setup`);
7334
6727
  }
7335
- manifest = recordInstallationStep(manifest, {
6728
+ state.manifest = recordInstallationStep(state.manifest, {
7336
6729
  key: authenticationStep,
7337
6730
  completedAt: now(),
7338
- resultHash: hash(secrets.authenticationSetupJson)
6731
+ resultHash: hash(state.secrets.authenticationSetupJson)
7339
6732
  }, now());
7340
- writeInstallationManifest(manifest);
7341
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
6733
+ writeInstallationManifest(state.manifest);
6734
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
7342
6735
  }
7343
- if (plan.deployment.appAccess && !manifest.steps.some((step) => step.key === "app-access-initialized:v1")) {
6736
+ if (state.plan.deployment.appAccess && !state.manifest.steps.some((step) => step.key === "app-access-initialized:v1")) {
7344
6737
  dependencies.onProgress?.("Configuring and verifying deployed-app access");
7345
6738
  try {
7346
6739
  await (dependencies.initializeAppAccess ?? initializeInstalledAppAccess)({
7347
- apiUrl: plan.apiUrl,
7348
- installationId: manifest.id,
7349
- probeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
7350
- setupJson: secrets.appAccessSetupJson,
7351
- setupToken: secrets.appAccessSetupToken
6740
+ apiUrl: state.plan.apiUrl,
6741
+ installationId: state.manifest.id,
6742
+ probeToken: createPlatformHealthProbeToken(state.manifest.ownershipNonce),
6743
+ setupJson: state.secrets.appAccessSetupJson,
6744
+ setupToken: state.secrets.appAccessSetupToken
7352
6745
  });
7353
6746
  } catch (error) {
7354
6747
  if (error instanceof PlatformAppAccessCredentialError) {
7355
- secrets = {
7356
- ...secrets,
6748
+ state.secrets = {
6749
+ ...state.secrets,
7357
6750
  authenticationPreparation: {
7358
- ...secrets.authenticationPreparation,
6751
+ ...state.secrets.authenticationPreparation,
7359
6752
  appAccessTokenRejected: "true"
7360
6753
  }
7361
6754
  };
7362
- writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
6755
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets, state.workerSecretUpdateIntent));
7363
6756
  }
7364
6757
  throw error;
7365
6758
  }
7366
- manifest = recordInstallationStep(manifest, {
6759
+ state.manifest = recordInstallationStep(state.manifest, {
7367
6760
  key: "app-access-initialized:v1",
7368
6761
  completedAt: now(),
7369
- resultHash: hash(secrets.appAccessSetupJson)
6762
+ resultHash: hash(state.secrets.appAccessSetupJson)
7370
6763
  }, now());
7371
- writeInstallationManifest(manifest);
7372
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
6764
+ writeInstallationManifest(state.manifest);
6765
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
7373
6766
  }
7374
6767
  dependencies.onProgress?.("Verifying platform health");
7375
- const emailGatewayUrl = hasEmailGatewayArtifact && plan.deployment.capabilities.email ? `https://${plan.deployment.resources.emailGatewayWorker}.${plan.workersSubdomain}.workers.dev` : void 0;
7376
- if (plan.deployment.authentication.accessProtection) {
7377
- ensurePlatformAccessToken(plan.apiUrl, false, true);
7378
- ensurePlatformAccessToken(plan.proxyUrl, false, true);
6768
+ const emailGatewayUrl = hasEmailGatewayArtifact && state.plan.deployment.capabilities.email ? platformWorkersDevUrl(state.plan, state.plan.deployment.resources.emailGatewayWorker) : void 0;
6769
+ if (state.plan.deployment.authentication.accessProtection) {
6770
+ ensurePlatformAccessToken(state.plan.apiUrl, false, true);
6771
+ ensurePlatformAccessToken(state.plan.proxyUrl, false, true);
7379
6772
  }
7380
6773
  const healthCheckContext = {
7381
- accountName: plan.account.name,
7382
- ...plan.deployment.authentication.accessProtection ? { accessProtected: true } : {},
6774
+ accountName: state.plan.account.name,
6775
+ ...state.plan.deployment.authentication.accessProtection ? { accessProtected: true } : {},
7383
6776
  workers: [
7384
- plan.deployment.resources.apiWorker,
7385
- plan.deployment.resources.proxyWorker,
7386
- ...emailGatewayUrl ? [plan.deployment.resources.emailGatewayWorker] : [],
7387
- plan.deployment.resources.dispatchWorker
6777
+ state.plan.deployment.resources.apiWorker,
6778
+ state.plan.deployment.resources.proxyWorker,
6779
+ ...emailGatewayUrl ? [state.plan.deployment.resources.emailGatewayWorker] : [],
6780
+ state.plan.deployment.resources.dispatchWorker
7388
6781
  ],
7389
6782
  resumeCommand: dependencies.healthCheckResumeCommand,
7390
6783
  ...emailGatewayUrl ? { emailGatewayUrl } : {}
7391
6784
  };
7392
- await (dependencies.verifyHealth ?? verifyPlatformHealth)(plan.apiUrl, manifest.id, plan.proxyUrl, plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(manifest.ownershipNonce), void 0, !plan.deployment.application.zoneName ? `https://${plan.deployment.resources.dispatchWorker}.${plan.workersSubdomain}.workers.dev` : void 0, healthCheckContext);
6785
+ await (dependencies.verifyHealth ?? verifyPlatformHealth)(state.plan.apiUrl, state.manifest.id, state.plan.proxyUrl, state.plan.deployment.application.baseDomain, version, targetUsageProtocolVersion, createPlatformHealthProbeToken(state.manifest.ownershipNonce), void 0, !state.plan.deployment.application.zoneName ? platformWorkersDevUrl(state.plan, state.plan.deployment.resources.dispatchWorker) : void 0, healthCheckContext);
7393
6786
  if (requiresSandboxDrain) {
7394
- if (!await (dependencies.verifySandboxDrain ?? verifyPlatformSandboxDrain)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce))) {
7395
- manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
7396
- manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
7397
- manifest = recordInstallationStep(manifest, {
6787
+ if (!await (dependencies.verifySandboxDrain ?? verifyPlatformSandboxDrain)(state.plan.apiUrl, createPlatformHealthProbeToken(state.manifest.ownershipNonce))) {
6788
+ state.manifest = recordWorkerCutoverResolution(state.manifest, state.workerCutoverGeneration, "committed", now());
6789
+ state.manifest = recordWorkerTrafficState(state.manifest, state.workerCutoverGeneration, false, now());
6790
+ state.manifest = recordInstallationStep(state.manifest, {
7398
6791
  completedAt: now(),
7399
6792
  key: "sandbox-drain:maintenance-runtime:v1",
7400
6793
  resultHash: resolvedRuntime.manifestSha256
7401
6794
  }, now());
7402
6795
  await checkpointOperation("waiting-for-sandbox-drain");
7403
- writeInstallationManifest(manifest);
7404
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
7405
- workerRollbacks = [];
7406
- workerDeploymentCommitted = true;
7407
- saveInstalledPlatformConnection(plan, manifest);
6796
+ writeInstallationManifest(state.manifest);
6797
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
6798
+ state.workerRollbacks = [];
6799
+ state.workerDeploymentCommitted = true;
6800
+ saveInstalledPlatformConnection(state.plan, state.manifest);
7408
6801
  return {
7409
- manifest,
6802
+ manifest: state.manifest,
7410
6803
  waitingForDns: false,
7411
6804
  waitingForSandboxDrain: true,
7412
6805
  zoneNameServers: zone?.nameServers ?? []
@@ -7415,38 +6808,13 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7415
6808
  if ((await prepareSandboxDrain()).state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
7416
6809
  }
7417
6810
  const emailOperationId = (await enableInstallationServices({
6811
+ state,
7418
6812
  checkpointOperation,
7419
6813
  requiresStagedCommit,
7420
6814
  authenticationCutoverCommitted,
7421
- get manifest() {
7422
- return manifest;
7423
- },
7424
- set manifest(value) {
7425
- manifest = value;
7426
- },
7427
- get workerCutoverGeneration() {
7428
- return workerCutoverGeneration;
7429
- },
7430
- set workerCutoverGeneration(value) {
7431
- workerCutoverGeneration = value;
7432
- },
7433
6815
  now,
7434
6816
  writeRemoteInstallationCheckpoint,
7435
6817
  dependencies,
7436
- plan,
7437
- get workerRollbacks() {
7438
- return workerRollbacks;
7439
- },
7440
- set workerRollbacks(value) {
7441
- workerRollbacks = value;
7442
- },
7443
- get workerSecretUpdateIntent() {
7444
- return workerSecretUpdateIntent;
7445
- },
7446
- set workerSecretUpdateIntent(value) {
7447
- workerSecretUpdateIntent = value;
7448
- },
7449
- secrets,
7450
6818
  workerKeys,
7451
6819
  workerActions,
7452
6820
  workerDeploymentSecrets,
@@ -7458,15 +6826,7 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7458
6826
  ids,
7459
6827
  runPinnedPlatformWrangler,
7460
6828
  shouldBootstrapSharedEmail,
7461
- get workerDeploymentCommitted() {
7462
- return workerDeploymentCommitted;
7463
- },
7464
- set workerDeploymentCommitted(value) {
7465
- workerDeploymentCommitted = value;
7466
- },
7467
6829
  releaseDatabaseLock,
7468
- wasDisabled,
7469
- previousPublicEndpoints,
7470
6830
  version,
7471
6831
  targetUsageProtocolVersion,
7472
6832
  healthCheckContext,
@@ -7474,14 +6834,14 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7474
6834
  runtimeProvenance
7475
6835
  })).emailOperationId;
7476
6836
  await checkpointOperation("complete");
7477
- writeInstallationManifest(manifest);
7478
- await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
6837
+ writeInstallationManifest(state.manifest);
6838
+ await writeRemoteInstallationCheckpoint(dependencies.client, state.plan, state.manifest);
7479
6839
  workerTrafficGuarded = false;
7480
- writePendingInstallationSecrets(manifest, secretCheckpoint(secrets));
7481
- promoteInstallationSecrets(manifest);
7482
- saveInstalledPlatformConnection(plan, manifest);
6840
+ writePendingInstallationSecrets(state.manifest, secretCheckpoint(state.secrets));
6841
+ promoteInstallationSecrets(state.manifest);
6842
+ saveInstalledPlatformConnection(state.plan, state.manifest);
7483
6843
  return {
7484
- manifest,
6844
+ manifest: state.manifest,
7485
6845
  ...emailOperationId ? { emailOperationId } : {},
7486
6846
  setupPending,
7487
6847
  waitingForDns: false,
@@ -7489,13 +6849,25 @@ async function installPlatform(plan, secrets, existing, dependencies) {
7489
6849
  zoneNameServers: zone?.nameServers ?? []
7490
6850
  };
7491
6851
  } catch (error) {
7492
- throw await recoverFailedPlatformInstallation(error, manifest, plan, dependencies, workerTrafficGuarded, workerDeploymentCommitted, workerRollbacks, authenticationEnforcementActive, authenticationProtocolTransition, workerCutoverGeneration, previousPublicEndpoints, operationStarted, now, writeRemoteInstallationCheckpoint);
6852
+ throw await recoverFailedPlatformInstallation(error, {
6853
+ ...state,
6854
+ dependencies,
6855
+ workerTrafficGuarded,
6856
+ authenticationProtocolTransition,
6857
+ previousPublicEndpoints,
6858
+ operationStarted,
6859
+ now,
6860
+ writeRemoteInstallationCheckpoint
6861
+ });
7493
6862
  } finally {
7494
6863
  await releaseDatabaseLock();
7495
6864
  }
7496
6865
  }
7497
6866
  //#endregion
7498
6867
  //#region src/cli/platform-install-auth.ts
6868
+ function defaultConnectionId(kind) {
6869
+ return kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind;
6870
+ }
7499
6871
  function platformManagementAccessForAuthentication(selection, accountId, checkpoint) {
7500
6872
  if (!selection.authentication.accessProtection && (!selection.authentication.providers.includes("cloudflare-access") || selection.file && !selection.file.cloudflareAccess)) return;
7501
6873
  const spec = selection.file?.cloudflareAccess ?? (checkpoint?.accessSetupSpecJson ? parsePlatformAccessSetupSpec(JSON.parse(checkpoint.accessSetupSpecJson)) : void 0);
@@ -7528,7 +6900,6 @@ async function selectInstallationAuthentication(file, recorded, choices = {}) {
7528
6900
  if (choices.loginMethods && JSON.stringify(choices.loginMethods) !== JSON.stringify(recorded.providers) || choices.accessProtection !== void 0 && choices.accessProtection !== !!recorded.accessProtection) throw new Error("platform: Resume with the recorded login methods and protection settings.");
7529
6901
  return { authentication: recorded };
7530
6902
  }
7531
- if (!process.stdin.isTTY && !choices.loginMethods && choices.accessProtection === void 0) return { authentication: { providers: ["github"] } };
7532
6903
  const providers = choices.loginMethods ?? (!process.stdin.isTTY ? ["github"] : platformPromptAnswer(await multiselect({
7533
6904
  message: "Platform login methods (select all that apply):",
7534
6905
  initialValues: [],
@@ -7583,7 +6954,7 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
7583
6954
  const entries = selection.file?.connections ?? selection.authentication.providers.map((kind) => ({
7584
6955
  configuration: {
7585
6956
  kind,
7586
- id: kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind,
6957
+ id: defaultConnectionId(kind),
7587
6958
  label: kind === "github" ? "GitHub" : kind === "google" ? "Google" : kind === "oidc" ? "Company SSO" : "Cloudflare Access"
7588
6959
  },
7589
6960
  clientSecretEnv: void 0
@@ -7606,11 +6977,11 @@ async function collectInstallationAuthentication(selection, plan, checkpoint, sa
7606
6977
  save,
7607
6978
  yes: access.yes
7608
6979
  }) : void 0;
7609
- console.log(`Login callback URL: ${plan.apiUrl}/auth/callback`);
6980
+ console.log(`Login callback URL: ${formatUrl(`${plan.apiUrl}/auth/callback`)}`);
7610
6981
  for (const entry of entries) {
7611
6982
  const config = { ...entry.configuration };
7612
6983
  const kind = config.kind;
7613
- if (!config.id) config.id = kind === "oidc" ? "company" : kind === "cloudflare-access" ? "access" : kind;
6984
+ if (!config.id) config.id = defaultConnectionId(kind);
7614
6985
  if (prepared?.login && prepared.login.configuration.id === config.id) {
7615
6986
  const connection = prepared.login;
7616
6987
  save({ [`authenticationConnection:${connection.configuration.id}`]: JSON.stringify(connection) });
@@ -7769,17 +7140,7 @@ function savePlatformInstallChoices(arguments_, accountName, directory = platfor
7769
7140
  recursive: true,
7770
7141
  mode: 448
7771
7142
  });
7772
- const path = join(directory, `plan-${plan.id}.json`);
7773
- const temporary = `${path}.${randomUUID()}.tmp`;
7774
- try {
7775
- writeFileSync(temporary, JSON.stringify(record, null, 2) + "\n", {
7776
- mode: 384,
7777
- flag: "wx"
7778
- });
7779
- renameSync(temporary, path);
7780
- } finally {
7781
- rmSync(temporary, { force: true });
7782
- }
7143
+ writeFileAtomic(join(directory, `plan-${plan.id}.json`), JSON.stringify(record, null, 2) + "\n", 384);
7783
7144
  return plan;
7784
7145
  }
7785
7146
  function listSavedPlatformInstallPlans(directory = platformRegistryDirectory()) {
@@ -7854,12 +7215,6 @@ function completePlatformSetupDraft(manifest) {
7854
7215
  return completed;
7855
7216
  }
7856
7217
  //#endregion
7857
- //#region src/cli/prompt-password.ts
7858
- /** A visual example for an empty secret field; never a default value or unmasked input. */
7859
- function passwordWithPlaceholder(options) {
7860
- return password(options);
7861
- }
7862
- //#endregion
7863
7218
  //#region src/cli/platform-lifecycle.ts
7864
7219
  function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
7865
7220
  const explicit = process.env.VOID_PLATFORM_DATABASE_URL || process.env.DATABASE_URL || "";
@@ -7876,14 +7231,14 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
7876
7231
  }
7877
7232
  async function runPlatformLifecycleCommand(commandArgs) {
7878
7233
  if (commandArgs.subcommand === "domain") {
7879
- const { runPlatformDomainCommand } = await import("./platform-domain-CHVS6cV0.mjs");
7234
+ const { runPlatformDomainCommand } = await import("./platform-domain-zLXnW1G_.mjs");
7880
7235
  await runPlatformDomainCommand(commandArgs);
7881
7236
  return;
7882
7237
  }
7883
7238
  if (commandArgs.subcommand === "discover") {
7884
7239
  const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
7885
7240
  const accountId = await selectAccount(await readAccounts(client), commandArgs.account);
7886
- const { recoverPlatformInstallations } = await import("./platform-recovery-CskSJgLh.mjs");
7241
+ const { recoverPlatformInstallations } = await import("./platform-recovery-DeucI7A9.mjs");
7887
7242
  const recovered = await recoverPlatformInstallations(client, accountId, void 0, commandArgs.selector);
7888
7243
  if (recovered.length === 0) {
7889
7244
  log.info(`No recoverable Void platform installations were found in ${accountId}.`);
@@ -7894,7 +7249,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
7894
7249
  return;
7895
7250
  }
7896
7251
  if (commandArgs.subcommand !== "install") {
7897
- const { runInstalledPlatformCommand } = await import("./platform-management-CBOT4BSM.mjs");
7252
+ const { runInstalledPlatformCommand } = await import("./platform-management-CYrLvTGH.mjs");
7898
7253
  await runInstalledPlatformCommand(commandArgs);
7899
7254
  return;
7900
7255
  }
@@ -8067,7 +7422,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
8067
7422
  progress?.stop("Read the saved installation.");
8068
7423
  progress = void 0;
8069
7424
  if (authoritative) {
8070
- const { selectAuthoritativePlatformManifest } = await import("./platform-management-CBOT4BSM.mjs");
7425
+ const { selectAuthoritativePlatformManifest } = await import("./platform-management-CYrLvTGH.mjs");
8071
7426
  const selected = selectAuthoritativePlatformManifest(existing, authoritative);
8072
7427
  authoritativeProvisionIntents = true;
8073
7428
  if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
@@ -8123,7 +7478,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
8123
7478
  });
8124
7479
  if (isCancel(choice)) throw new Error("platform: Cancelled.");
8125
7480
  if (choice === "setup") {
8126
- note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? ` such as ${suggestedName}` : ""}. Point it at the same dedicated PostgreSQL database you will give Void, use a runtime database user, and disable SQL result caching. If your organization manages Hyperdrive, ask its administrator to create it. Then rerun this command and select it from the list.\nSetup: https://developers.cloudflare.com/hyperdrive/get-started/`, import_picocolors.default.bold("Set up Hyperdrive first"));
7481
+ note(`Create a Hyperdrive in account ${accountId} with any unused name${suggestedNameAvailable ? ` such as ${suggestedName}` : ""}. Point it at the same dedicated PostgreSQL database you will give Void, use a runtime database user, and disable SQL result caching. If your organization manages Hyperdrive, ask its administrator to create it. Then rerun this command and select it from the list.\nSetup: ${formatUrl("https://developers.cloudflare.com/hyperdrive/get-started/")}`, import_picocolors.default.bold("Set up Hyperdrive first"));
8127
7482
  outro("No Cloudflare or local installation state was changed.");
8128
7483
  return;
8129
7484
  }
@@ -8472,7 +7827,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
8472
7827
  })).actions.filter((action) => action.kind === "worker"),
8473
7828
  revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
8474
7829
  verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
8475
- const { verifyPlatformInstallationOwnership } = await import("./platform-management-CBOT4BSM.mjs");
7830
+ const { verifyPlatformInstallationOwnership } = await import("./platform-management-CYrLvTGH.mjs");
8476
7831
  const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
8477
7832
  const previous = manifest.resources.find((resource) => resource.key === action.key);
8478
7833
  return action.action === "create" && previous && previous.name !== action.name;
@@ -8496,11 +7851,11 @@ async function runPlatformLifecycleCommand(commandArgs) {
8496
7851
  outro(`Installation ${result.manifest.id} is safely checkpointed.`);
8497
7852
  return;
8498
7853
  }
8499
- log.info(`API URL: ${plan.apiUrl}`);
7854
+ log.info(`API URL: ${formatUrl(plan.apiUrl)}`);
8500
7855
  if (result.emailOperationId) log.info(`Shared email routing ready. Operation: ${result.emailOperationId}.`);
8501
7856
  const completedAuthCode = readInstallationRecoverySecrets(result.manifest)?.authBootstrapCode;
8502
7857
  if (result.setupPending && completedAuthCode) {
8503
- log.info(`First administrator setup: ${plan.apiUrl}/setup`);
7858
+ log.info(`First administrator setup: ${formatUrl(`${plan.apiUrl}/setup`)}`);
8504
7859
  console.log(`One-time administrator setup code: ${completedAuthCode}`);
8505
7860
  log.info("Confirm your identity in the browser, then test and enable any additional login methods in Settings.");
8506
7861
  }
@@ -8542,7 +7897,7 @@ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, aut
8542
7897
  deploymentExecution: preview.actions.some((entry) => entry.key === "deployment-queue")
8543
7898
  });
8544
7899
  } catch (error) {
8545
- if (error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403) && error.requestPath?.includes("/ai-gateway/")) throw new Error("platform: The runtime token could not verify AI Gateway access. Include Account → AI Gateway → Edit for this account, then retry. No platform resources were created.", { cause: error });
7900
+ if (isPlatformTokenAuthorizationError(error, "/ai-gateway/")) throw new Error("platform: The runtime token could not verify AI Gateway access. Include Account → AI Gateway → Edit for this account, then retry. No platform resources were created.", { cause: error });
8546
7901
  throw error;
8547
7902
  }
8548
7903
  assertPlatformInstallPlanUnchanged(preview, verified, "AI Gateway access");
@@ -8613,10 +7968,10 @@ async function promptSecret(message, masked = true, validate, placeholder) {
8613
7968
  message,
8614
7969
  validate: (input) => input ? validate?.(input) : "A value is required."
8615
7970
  };
8616
- const value = masked ? placeholder ? await passwordWithPlaceholder({
7971
+ const value = masked ? await password({
8617
7972
  ...options,
8618
7973
  placeholder
8619
- }) : await password(options) : await text$1({
7974
+ }) : await text$1({
8620
7975
  ...options,
8621
7976
  placeholder
8622
7977
  });
@@ -8624,4 +7979,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
8624
7979
  return value;
8625
7980
  }
8626
7981
  //#endregion
8627
- export { resolveBootstrapGitHubId as A, assertPlatformOperatorCredentialCanApplyPlan as B, purgeResourcesKeptReason as C, loadPlatformRuntime as D, runPurgeResourcesTeardown as E, formatPlatformInstallPlan as F, preparePlatformAccessSetup as G, verifyPlatformCloudflareCredential as H, platformAdoptionIntentHash as I, hyperdriveOriginFromConnectionString as J, PlatformCloudflareClient as K, platformProvisionIntentHash as L, recordPlatformWorkerTraffic as M, trackedPlatformWorkerTraffic as N, createPlatformHealthProbeToken as O, createPlatformInstallPlan as P, revalidatePlatformInstallAction as R, purgeResourcesHandlesEmailRouting as S, purgeResourcesPermissions as T, checkpointPlatformAccessResources as U, resolvePlatformCloudflareCredential as V, parsePlatformAccessSetupSpec as W, verifyPlatformRuntimeTokenPermissions as X, hyperdriveOriginMatches as Y, describePurgeResourcesPlan as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, isPurgedAccessResource as b, acquirePlatformDatabaseLifecycleLockConnection as c, readPlatformProjectZeroTrustState as d, assertProjectZeroTrustAllowsUninstall as f, checkPurgeResourcesCredentials as g, assertNoPlatformProjects as h, verifyPlatformAiGatewayPlan as i, recordResource as j, createPlatformInstallationSecrets as k, platformDatabaseClientUrl as l, PlatformResourceChangedError as m, resolvePlatformInstallDatabaseUrl as n, installPlatform as o, PURGE_RESOURCES_STEP_PREFIX as p, hyperdriveOriginConfigHash as q, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLock as s, assertNoninteractivePlatformMutationAcknowledged as t, readPlatformProjectSlugs as u, isPlatformTailWorker as v, purgeResourcesKeptResources as w, purgeResourcesAlwaysKept as x, isPurgeResourcesCandidate as y, platformDatabaseNameFromOwnerUrl as z };
7982
+ export { revalidatePlatformInstallAction as A, hyperdriveOriginConfigHash as B, formatPlatformInstallPlan as C, platformAdoptionIntentHash as D, matchesRecordedR2Fingerprint as E, checkpointPlatformAccessResources as F, hyperdriveOriginMatches as H, parsePlatformAccessSetupSpec as I, preparePlatformAccessSetup as L, assertPlatformOperatorCredentialCanApplyPlan as M, resolvePlatformCloudflareCredential as N, r2BucketConfigHash as O, verifyPlatformCloudflareCredential as P, PlatformCloudflareApiError as R, dnsRecordConfigHash as S, legacyResourceConfigHash as T, verifyPlatformRuntimeTokenPermissions as U, hyperdriveOriginFromConnectionString as V, resolveBootstrapGitHubId as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, recordPlatformWorkerTraffic as b, acquirePlatformDatabaseLifecycleLockConnection as c, readPlatformProjectZeroTrustState as d, assertProjectZeroTrustAllowsUninstall as f, createPlatformInstallationSecrets as g, createPlatformHealthProbeToken as h, verifyPlatformAiGatewayPlan as i, platformDatabaseNameFromOwnerUrl as j, resourceMarkerProblem as k, platformDatabaseClientUrl as l, loadPlatformRuntime as m, resolvePlatformInstallDatabaseUrl as n, installPlatform as o, readPlatformDatabase as p, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLock as s, assertNoninteractivePlatformMutationAcknowledged as t, readPlatformProjectSlugs as u, recordResource as v, hasProvisionIntentStep as w, createPlatformInstallPlan as x, platformTrafficAttachmentWorker as y, PlatformCloudflareClient as z };