void 0.10.13 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/AGENTS_TEMPLATE.md +6 -0
  2. package/{skills/void/docs/node_modules/void/node_modules/pglite-server/LICENSE.md → LICENSE} +1 -1
  3. package/README.md +34 -8
  4. package/dist/{auth-qgMlYp7Z.d.mts → auth-DkcFflXV.d.mts} +10 -11
  5. package/dist/auth-W9WII-mN.mjs +630 -0
  6. package/dist/{auth-cmd-BqsdZJp5.mjs → auth-cmd-CAH62yDU.mjs} +4 -3
  7. package/dist/{auth-migrations-BTZ-ATvQ.mjs → auth-migrations-9vif1uj8.mjs} +53 -12
  8. package/dist/{better-auth-shared-BQooDxbw.mjs → better-auth-shared-CYw1T3k4.mjs} +7 -10
  9. package/dist/{better-auth-shared-DealXecJ.d.mts → better-auth-shared-DSCeohOK.d.mts} +10 -5
  10. package/dist/{build-cmd-Bujrv5q-.mjs → build-cmd-CJvZvPQO.mjs} +6 -4
  11. package/dist/{cache-C11V8Fxq.mjs → cache-BlNeQjuP.mjs} +9 -5
  12. package/dist/{cancel-deploy-fwFYF04b.mjs → cancel-deploy-CmlAZ9P6.mjs} +3 -2
  13. package/dist/cf-access-AJ1ehiFR.mjs +42 -0
  14. package/dist/cf-access-DsSsZUPr.mjs +67 -0
  15. package/dist/cli/cli.d.mts +1 -1
  16. package/dist/cli/cli.mjs +1735 -165
  17. package/dist/cli/env-schema-probe.d.mts +48 -61
  18. package/dist/cli/env-schema-probe.mjs +10 -58
  19. package/dist/{client-Gb71-XkG.mjs → client-Clirrol3.mjs} +120 -463
  20. package/dist/cloudflare-auth-B1QtTO1b.mjs +217 -0
  21. package/dist/cloudflare-cmd-B6_OZx2V.mjs +62 -0
  22. package/dist/cloudflare-connect-j5D4hhrG.mjs +56 -0
  23. package/dist/cloudflare-operations-CPTpRW6d.mjs +566 -0
  24. package/dist/{config-qHGgPuWT.mjs → config-BQFq7QvD.mjs} +75 -41
  25. package/dist/{config-CutEMNGJ.mjs → config-NOG_U1aK.mjs} +10 -15
  26. package/dist/connect-C04Wdy_h.mjs +79 -0
  27. package/dist/{create-project-DsYvl3TB.mjs → create-project-ChGZ1DFd.mjs} +26 -16
  28. package/dist/database-provider.d.mts +21 -0
  29. package/dist/database-provider.mjs +6 -0
  30. package/dist/{db-C0i0sYMS.mjs → db-D2d_mUsB.mjs} +612 -117
  31. package/dist/{delete-mh6p-zkQ.mjs → delete-D8GigDk8.mjs} +9 -6
  32. package/dist/{deploy-jBJT1fUT.mjs → deploy-iXZ3F0N6.mjs} +3252 -1995
  33. package/dist/dev-inbox-DkgRWLkW.mjs +307 -0
  34. package/dist/{discover-CJHyvYfR.mjs → discover-xvfrgJeo.mjs} +3 -3
  35. package/dist/{magic-string.es-ZQjdJFFn.mjs → dist-BR1quN_w.mjs} +570 -216
  36. package/dist/{dist-DaKKDf8D.mjs → dist-BrsS7cai.mjs} +17 -2
  37. package/dist/{dist-BuiRJkTd.mjs → dist-m40_XgNh.mjs} +48 -33
  38. package/dist/{domain-DiaNQbrl.mjs → domain-B1VmoSr0.mjs} +41 -4
  39. package/dist/dotenv-VQxupEUv.mjs +181 -0
  40. package/dist/edge.d.mts +2 -0
  41. package/dist/edge.mjs +2 -0
  42. package/dist/email-Ce6SQq-i.mjs +182 -0
  43. package/dist/email-uKyQYUVY.mjs +1016 -0
  44. package/dist/{entry-D7yy4xVH.mjs → entry-DU3oDoQ3.mjs} +2 -2
  45. package/dist/env-BcQzYgoG.mjs +73 -0
  46. package/dist/env-D4Emu-M_.mjs +95 -0
  47. package/dist/env-helpers-CyKtOBpj.d.mts +22 -0
  48. package/dist/env-public-D_6u46fX.d.mts +135 -0
  49. package/dist/{env-types-QBj-ndax.mjs → env-types-BNPhro-M.mjs} +2 -2
  50. package/dist/{env-validation-Dea3v3ej.mjs → env-validation-ENpMy6Ez.mjs} +68 -122
  51. package/dist/{gen-BzXf3Jh2.mjs → gen-DI2YwdBM.mjs} +70 -15
  52. package/dist/generate-RTK8_kK1.mjs +47 -0
  53. package/dist/{github-cmd-DKcGUNsj.mjs → github-cmd-xItS5Zwf.mjs} +39 -31
  54. package/dist/{handler-Cjh8uM3Y.d.mts → handler-D1hLsObx.d.mts} +124 -122
  55. package/dist/{head-nmvOgFjd.d.mts → head-Do8P4puT.d.mts} +9 -8
  56. package/dist/{headers-ChAADPQu.mjs → headers-D8QfRX9Y.mjs} +12 -10
  57. package/dist/inbound-2d0zi2yS.mjs +706 -0
  58. package/dist/inbound-afAcWeQ9.d.mts +363 -0
  59. package/dist/index.d.mts +61 -34
  60. package/dist/index.mjs +1012 -293
  61. package/dist/{init-7JCAcKNQ.mjs → init-BD-9THgn.mjs} +329 -271
  62. package/dist/link-RMdgjF1v.mjs +52 -0
  63. package/dist/{list-CPwFDZ_c.mjs → list-3F52R_yO.mjs} +37 -5
  64. package/dist/{runner-kapo9aPs.mjs → local-d1-D2I6Ox5F.mjs} +116 -6
  65. package/dist/{login-BT3H8PN3.mjs → login-pV69H-ZO.mjs} +17 -15
  66. package/dist/{logs-Bt313ax7.mjs → logs-DFHHD6wE.mjs} +16 -2
  67. package/dist/mime-BJD7d_qL.mjs +1216 -0
  68. package/dist/neon-DHwd2zvC.mjs +54 -0
  69. package/dist/{node-pRg81HqV.mjs → node-Dk3H2jmU.mjs} +29 -8
  70. package/dist/operator-cmd-DYWRbWUA.mjs +348 -0
  71. package/dist/{agents-CtgBYqld.mjs → output-tFQLLj26.mjs} +676 -211
  72. package/dist/{package-json-Cx1osYo6.mjs → package-json-CPoWX79C.mjs} +1 -1
  73. package/dist/pages/client.d.mts +43 -41
  74. package/dist/pages/client.mjs +40 -33
  75. package/dist/pages/head-client.d.mts +10 -12
  76. package/dist/pages/head.d.mts +1 -1
  77. package/dist/pages/index.d.mts +37 -14
  78. package/dist/pages/index.mjs +5 -5
  79. package/dist/pages/islands-plugin.d.mts +25 -27
  80. package/dist/pages/islands-plugin.mjs +6 -4
  81. package/dist/pages/prefetch.d.mts +6 -7
  82. package/dist/pages/protocol.d.mts +2 -2
  83. package/dist/pages/protocol.mjs +2 -1
  84. package/dist/pages/serialize.d.mts +7 -8
  85. package/dist/platform-cmd-DxJ2FRwR.mjs +123 -0
  86. package/dist/platform-domain-ChvbJkdy.mjs +228 -0
  87. package/dist/platform-lifecycle-DN4MzJF_.mjs +4450 -0
  88. package/dist/platform-management-Db2PXw0B.mjs +698 -0
  89. package/dist/platform-recovery-C_YO-tIs.mjs +99 -0
  90. package/dist/{plugin-inference-DMeavIJ6.mjs → plugin-inference-BDRfZngg.mjs} +35 -19
  91. package/dist/prepare-BfJvFUtJ.mjs +14 -0
  92. package/dist/{prepare-BvvgAz-3.mjs → prepare-CBetXvsN.mjs} +15 -24
  93. package/dist/{preset-BjyR3lzz.mjs → preset-lAy0B0BQ.mjs} +25 -212
  94. package/dist/{project-cmd-D_w-4w5B.mjs → project-cmd-Mo0V9yKS.mjs} +25 -12
  95. package/dist/{project-paths-BQd7OmIo.mjs → project-paths-SK8nMHPp.mjs} +3 -1
  96. package/dist/{project-tsconfig-B-QtXjLQ.mjs → project-tsconfig-Ql2XsSQp.mjs} +2 -2
  97. package/dist/{protocol-Bnb0LFp3.d.mts → protocol-C-pqYJjE.d.mts} +3 -4
  98. package/dist/{provision-CPx2ZxsH.mjs → provision-Blnstcm2.mjs} +78 -45
  99. package/dist/r2-conditions-D1Wk8i7b.mjs +30 -0
  100. package/dist/{requests-B8sZxaFM.mjs → requests-BcKOVpRg.mjs} +5 -3
  101. package/dist/{resolve-project-BBMtLLV9.mjs → resolve-project--Vxawf7z.mjs} +2 -2
  102. package/dist/rollback-Bx85-0xh.mjs +166 -0
  103. package/dist/{rolldown-runtime-DJK8HYOj.mjs → rolldown-runtime-rQ84J-ij.mjs} +1 -1
  104. package/dist/{route-types-z1jtHEi_.mjs → route-types-Da-DpyUp.mjs} +77 -25
  105. package/dist/routes-stub.d.mts +22 -23
  106. package/dist/runner-mysql-7BPUNGmL.mjs +61 -0
  107. package/dist/{runner-pg-CHM76xuC.mjs → runner-pg-BkEza-dX.mjs} +18 -6
  108. package/dist/runtime/ai.d.mts +21 -14
  109. package/dist/runtime/ai.mjs +5 -4
  110. package/dist/runtime/auth-client-react.d.mts +3 -5
  111. package/dist/runtime/auth-client-solid.d.mts +3 -5
  112. package/dist/runtime/auth-client-svelte.d.mts +3 -5
  113. package/dist/runtime/auth-client-vue.d.mts +3 -5
  114. package/dist/runtime/auth-client.d.mts +3 -5
  115. package/dist/runtime/auth.d.mts +1 -1
  116. package/dist/runtime/better-auth-mysql.d.mts +10 -0
  117. package/dist/runtime/better-auth-mysql.mjs +49 -0
  118. package/dist/runtime/better-auth-pg.d.mts +8 -9
  119. package/dist/runtime/better-auth-pg.mjs +2 -2
  120. package/dist/runtime/better-auth.d.mts +8 -9
  121. package/dist/runtime/better-auth.mjs +2 -2
  122. package/dist/runtime/client-react.d.mts +1 -1
  123. package/dist/runtime/client-solid.d.mts +1 -1
  124. package/dist/runtime/client-svelte.d.mts +1 -1
  125. package/dist/runtime/client-vue.d.mts +1 -1
  126. package/dist/runtime/client.d.mts +1 -1
  127. package/dist/runtime/db-mysql.d.mts +2 -0
  128. package/dist/runtime/db-mysql.mjs +1 -0
  129. package/dist/runtime/db.d.mts +10 -11
  130. package/dist/runtime/durable.d.mts +47 -0
  131. package/dist/runtime/durable.mjs +146 -0
  132. package/dist/runtime/email/testing.d.mts +112 -0
  133. package/dist/runtime/email/testing.mjs +283 -0
  134. package/dist/runtime/email.d.mts +30 -0
  135. package/dist/runtime/email.mjs +573 -0
  136. package/dist/runtime/env-helpers.d.mts +2 -2
  137. package/dist/runtime/env-helpers.mjs +5 -42
  138. package/dist/runtime/env-public-client.d.mts +10 -11
  139. package/dist/runtime/env-public-client.mjs +1 -1
  140. package/dist/runtime/env-public.d.mts +2 -2
  141. package/dist/runtime/env-public.mjs +104 -49
  142. package/dist/runtime/env.d.mts +19 -18
  143. package/dist/runtime/env.mjs +15 -2
  144. package/dist/runtime/fetch-stream.d.mts +20 -21
  145. package/dist/runtime/fetch.d.mts +15 -16
  146. package/dist/runtime/handler.d.mts +1 -1
  147. package/dist/runtime/isr.d.mts +21 -22
  148. package/dist/runtime/isr.mjs +26 -8
  149. package/dist/runtime/kv.d.mts +9 -10
  150. package/dist/runtime/live-client.d.mts +5 -7
  151. package/dist/runtime/live-client.mjs +9 -7
  152. package/dist/runtime/live-server.d.mts +4 -5
  153. package/dist/runtime/live.d.mts +22 -24
  154. package/dist/runtime/live.mjs +1 -1
  155. package/dist/runtime/log.d.mts +16 -17
  156. package/dist/runtime/migration-handler-mysql.d.mts +4 -0
  157. package/dist/runtime/migration-handler-mysql.mjs +81 -0
  158. package/dist/runtime/migration-handler-pg.d.mts +2 -4
  159. package/dist/runtime/migration-handler.d.mts +5 -6
  160. package/dist/runtime/migration-handler.mjs +4 -3
  161. package/dist/runtime/queues.d.mts +3 -4
  162. package/dist/runtime/queues.mjs +2 -1
  163. package/dist/runtime/remote/binding-handler.d.mts +10 -12
  164. package/dist/runtime/remote/binding-handler.mjs +24 -3
  165. package/dist/runtime/remote/index.d.mts +5 -6
  166. package/dist/runtime/remote/index.mjs +21 -18
  167. package/dist/runtime/response.d.mts +10 -11
  168. package/dist/runtime/sandbox.d.mts +56 -55
  169. package/dist/runtime/sandbox.mjs +57 -49
  170. package/dist/runtime/schema-mysql.d.mts +1 -0
  171. package/dist/runtime/schema-mysql.mjs +2 -0
  172. package/dist/runtime/seed.d.mts +14 -9
  173. package/dist/runtime/sse-client.d.mts +6 -7
  174. package/dist/runtime/sse.d.mts +11 -12
  175. package/dist/runtime/storage.d.mts +3 -4
  176. package/dist/runtime/validator.d.mts +1 -1
  177. package/dist/runtime/ws-server.d.mts +12 -12
  178. package/dist/runtime/ws-server.mjs +32 -4
  179. package/dist/runtime/ws.d.mts +19 -21
  180. package/dist/{scan-ChWt4pX1.mjs → scan-BMH4rzlv.mjs} +42 -19
  181. package/dist/{scan-NU4xKGci.mjs → scan-CpK-57ug.mjs} +5 -4
  182. package/dist/{secret-Dt32J6RI.mjs → secret-ByhJ9AMl.mjs} +62 -5
  183. package/dist/{skills-CLjN0uUO.mjs → skills-Q46GZMO-.mjs} +6 -4
  184. package/dist/sqlite-validation-BzKMWnO4.mjs +25 -0
  185. package/dist/{standard-schema-DJ0HW7QP.d.mts → standard-schema-Fo_vCAZh.d.mts} +6 -6
  186. package/dist/{subcommand-prompt-BzV8iQZo.mjs → subcommand-prompt-WfySCQ7S.mjs} +67 -48
  187. package/dist/sveltekit.d.mts +12 -11
  188. package/dist/sveltekit.mjs +1 -1
  189. package/dist/types-BAp5AEBU.d.mts +79 -0
  190. package/dist/types-CKWnYgfy.d.mts +1 -0
  191. package/dist/{validate-Cw_RLeTj.mjs → validate-tBBN_dXH.mjs} +4 -3
  192. package/dist/wrangler--imS8n0d.mjs +1796 -0
  193. package/dist/{yarn-pnp-DJn3SAHF.mjs → yarn-pnp-DxSInkzL.mjs} +1 -1
  194. package/package.json +79 -65
  195. package/schema.json +22 -3
  196. package/skills/void/SKILL.md +59 -2
  197. package/skills/void/docs/guide/ai.md +32 -14
  198. package/skills/void/docs/guide/app-types.md +6 -6
  199. package/skills/void/docs/guide/auth.md +14 -16
  200. package/skills/void/docs/guide/database/d1.md +6 -0
  201. package/skills/void/docs/guide/database/mysql.md +60 -0
  202. package/skills/void/docs/guide/database/postgresql.md +14 -9
  203. package/skills/void/docs/guide/database.md +39 -26
  204. package/skills/void/docs/guide/deployment.md +99 -25
  205. package/skills/void/docs/guide/durable-state.md +140 -0
  206. package/skills/void/docs/guide/edge/headers.md +5 -5
  207. package/skills/void/docs/guide/edge/prerendering.md +2 -0
  208. package/skills/void/docs/guide/edge/revalidation.md +21 -6
  209. package/skills/void/docs/guide/edge/rewrites.md +18 -14
  210. package/skills/void/docs/guide/edge/static-assets.md +12 -10
  211. package/skills/void/docs/guide/email.md +640 -0
  212. package/skills/void/docs/guide/env-migration.md +109 -0
  213. package/skills/void/docs/guide/env-vars.md +70 -248
  214. package/skills/void/docs/guide/index.md +15 -17
  215. package/skills/void/docs/guide/jobs.md +8 -5
  216. package/skills/void/docs/guide/live.md +7 -15
  217. package/skills/void/docs/guide/pages-routing/actions-and-forms.md +8 -4
  218. package/skills/void/docs/guide/pages-routing/islands.md +3 -3
  219. package/skills/void/docs/guide/pages-routing/loaders.md +6 -4
  220. package/skills/void/docs/guide/pages-routing/overview.md +7 -7
  221. package/skills/void/docs/guide/platform-administration.md +212 -0
  222. package/skills/void/docs/guide/platform-development.md +264 -0
  223. package/skills/void/docs/guide/queues.md +10 -10
  224. package/skills/void/docs/guide/quickstart.md +46 -67
  225. package/skills/void/docs/guide/remote-dev.md +8 -6
  226. package/skills/void/docs/guide/sandboxes.md +33 -16
  227. package/skills/void/docs/guide/self-hosted-platform.md +553 -0
  228. package/skills/void/docs/guide/server-routing.md +5 -5
  229. package/skills/void/docs/guide/sse.md +4 -4
  230. package/skills/void/docs/guide/ssg.md +5 -3
  231. package/skills/void/docs/guide/storage.md +2 -2
  232. package/skills/void/docs/guide/websockets.md +15 -7
  233. package/skills/void/docs/index.md +3 -3
  234. package/skills/void/docs/integrations/agents.md +6 -64
  235. package/skills/void/docs/integrations/cloudflare.md +165 -146
  236. package/skills/void/docs/integrations/frameworks/analog.md +4 -4
  237. package/skills/void/docs/integrations/frameworks/astro.md +5 -5
  238. package/skills/void/docs/integrations/frameworks/nuxt.md +5 -5
  239. package/skills/void/docs/integrations/frameworks/overview.md +3 -3
  240. package/skills/void/docs/integrations/frameworks/react-router.md +3 -3
  241. package/skills/void/docs/integrations/frameworks/sveltekit.md +3 -3
  242. package/skills/void/docs/integrations/frameworks/tanstack-start.md +2 -2
  243. package/skills/void/docs/integrations/nodejs-bun-deno.md +11 -4
  244. package/skills/void/docs/reference/api.md +42 -6
  245. package/skills/void/docs/reference/cli.md +665 -160
  246. package/skills/void/docs/reference/config.md +67 -27
  247. package/skills/void/docs/reference/resource-inference.md +13 -9
  248. package/skills/void/docs/reference/structure.md +9 -11
  249. package/AGENT_PROMPT.md +0 -19
  250. package/dist/cf-access-Bqw81xAf.mjs +0 -22
  251. package/dist/env-CZy5MorI.mjs +0 -299
  252. package/dist/env-helpers-z4stu8uc.d.mts +0 -52
  253. package/dist/env-mask-Dd47NbR6.mjs +0 -90
  254. package/dist/env-public-BfiLcMBk.d.mts +0 -140
  255. package/dist/link-CdGHSIy-.mjs +0 -45
  256. package/dist/mcp-DoM3_nhd.mjs +0 -377
  257. package/dist/project-paths-GpziKeQQ.d.mts +0 -25
  258. package/dist/providers-BNKRacMr.d.mts +0 -7
  259. package/dist/proxy-D-3_D-Gl.mjs +0 -5
  260. package/dist/rollback-CkvTFXx5.mjs +0 -90
  261. package/dist/runtime/isr-cache.d.mts +0 -207
  262. package/dist/runtime/isr-cache.mjs +0 -523
  263. package/dist/types-lLjNE9Qp.d.mts +0 -51
  264. package/getting-started-prompt.txt +0 -28
  265. package/skills/void/command/void.md +0 -7
  266. package/skills/void/docs/integrations/auth-providers.md +0 -0
  267. package/skills/void/docs/integrations/payment-processors.md +0 -0
  268. package/skills/void/docs/node_modules/@iconify/vue/README.md +0 -408
  269. package/skills/void/docs/node_modules/@iconify/vue/offline/readme.md +0 -5
  270. package/skills/void/docs/node_modules/@voidzero-dev/vitepress-theme/README.md +0 -103
  271. package/skills/void/docs/node_modules/oxc-minify/README.md +0 -78
  272. package/skills/void/docs/node_modules/reka-ui/README.md +0 -80
  273. package/skills/void/docs/node_modules/vitepress/README.md +0 -28
  274. package/skills/void/docs/node_modules/vitepress/template/api-examples.md +0 -49
  275. package/skills/void/docs/node_modules/vitepress/template/index.md +0 -28
  276. package/skills/void/docs/node_modules/vitepress/template/markdown-examples.md +0 -85
  277. package/skills/void/docs/node_modules/vitepress-plugin-group-icons/README.md +0 -101
  278. package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +0 -19
  279. package/skills/void/docs/node_modules/void/CLAUDE.md +0 -221
  280. package/skills/void/docs/node_modules/void/README.md +0 -90
  281. package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/CHANGELOG.md +0 -685
  282. package/skills/void/docs/node_modules/void/node_modules/@clack/prompts/README.md +0 -396
  283. package/skills/void/docs/node_modules/void/node_modules/@cloudflare/sandbox/README.md +0 -219
  284. package/skills/void/docs/node_modules/void/node_modules/@cloudflare/vite-plugin/README.md +0 -37
  285. package/skills/void/docs/node_modules/void/node_modules/@cloudflare/workers-types/README.md +0 -135
  286. package/skills/void/docs/node_modules/void/node_modules/@electric-sql/pglite/README.md +0 -189
  287. package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/CHANGELOG.md +0 -143
  288. package/skills/void/docs/node_modules/void/node_modules/@hono/oauth-providers/README.md +0 -1272
  289. package/skills/void/docs/node_modules/void/node_modules/@napi-rs/keyring/README.md +0 -19
  290. package/skills/void/docs/node_modules/void/node_modules/@types/better-sqlite3/README.md +0 -15
  291. package/skills/void/docs/node_modules/void/node_modules/@types/node/README.md +0 -15
  292. package/skills/void/docs/node_modules/void/node_modules/@types/pg/README.md +0 -15
  293. package/skills/void/docs/node_modules/void/node_modules/@types/proper-lockfile/README.md +0 -51
  294. package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/README.md +0 -22
  295. package/skills/void/docs/node_modules/void/node_modules/@typescript/native-preview/vendor/vscode-jsonrpc/README.md +0 -69
  296. package/skills/void/docs/node_modules/void/node_modules/@void/md/README.md +0 -153
  297. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/engine-javascript/README.md +0 -9
  298. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@shikijs/transformers/README.md +0 -9
  299. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/@types/node/README.md +0 -15
  300. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/CHANGELOG.md +0 -24
  301. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/gray-matter/README.md +0 -565
  302. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-exit/README.md +0 -127
  303. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-anchor/README.md +0 -600
  304. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-attrs/README.md +0 -386
  305. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-container/README.md +0 -95
  306. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-emoji/README.md +0 -101
  307. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/markdown-it-footnote/README.md +0 -135
  308. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathslash/README.md +0 -64
  309. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/shiki/README.md +0 -15
  310. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/tinyglobby/README.md +0 -25
  311. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/AGENTS.md +0 -16
  312. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/README.md +0 -220
  313. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/build.md +0 -21
  314. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/check.md +0 -35
  315. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/create.md +0 -70
  316. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/fmt.md +0 -20
  317. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/index.md +0 -35
  318. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/lint.md +0 -26
  319. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/pack.md +0 -17
  320. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/run.md +0 -364
  321. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/staged.md +0 -15
  322. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/config/test.md +0 -18
  323. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/automatic-data-tracking.md +0 -145
  324. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/build.md +0 -40
  325. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/cache.md +0 -107
  326. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/check.md +0 -60
  327. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/ci.md +0 -62
  328. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/commit-hooks.md +0 -60
  329. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/create.md +0 -341
  330. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/dev.md +0 -24
  331. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/docker.md +0 -175
  332. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/env.md +0 -167
  333. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/fmt.md +0 -41
  334. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/github-actions-cache.md +0 -165
  335. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/ide-integration.md +0 -101
  336. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/implode.md +0 -23
  337. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/index.md +0 -134
  338. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/install.md +0 -199
  339. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/lint.md +0 -50
  340. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/migrate-rules.md +0 -347
  341. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/migrate.md +0 -197
  342. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/monorepo.md +0 -176
  343. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/pack.md +0 -69
  344. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/run.md +0 -356
  345. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/test.md +0 -35
  346. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/troubleshooting.md +0 -108
  347. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/upgrade.md +0 -101
  348. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/vpx.md +0 -66
  349. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/guide/why.md +0 -39
  350. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/index.md +0 -12
  351. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/docs/team.md +0 -35
  352. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/templates/generator/README.md +0 -35
  353. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vite-plus/templates/monorepo/README.md +0 -29
  354. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/vue/README.md +0 -58
  355. package/skills/void/docs/node_modules/void/node_modules/arktype/README.md +0 -165
  356. package/skills/void/docs/node_modules/void/node_modules/better-auth/LICENSE.md +0 -20
  357. package/skills/void/docs/node_modules/void/node_modules/better-auth/README.md +0 -32
  358. package/skills/void/docs/node_modules/void/node_modules/better-sqlite3/README.md +0 -99
  359. package/skills/void/docs/node_modules/void/node_modules/blake3-jit/README.md +0 -108
  360. package/skills/void/docs/node_modules/void/node_modules/drizzle-arktype/README.md +0 -51
  361. package/skills/void/docs/node_modules/void/node_modules/drizzle-kit/README.md +0 -79
  362. package/skills/void/docs/node_modules/void/node_modules/drizzle-orm/README.md +0 -44
  363. package/skills/void/docs/node_modules/void/node_modules/drizzle-valibot/README.md +0 -51
  364. package/skills/void/docs/node_modules/void/node_modules/drizzle-zod/README.md +0 -65
  365. package/skills/void/docs/node_modules/void/node_modules/es-module-lexer/README.md +0 -403
  366. package/skills/void/docs/node_modules/void/node_modules/estree-walker/README.md +0 -48
  367. package/skills/void/docs/node_modules/void/node_modules/hono/README.md +0 -85
  368. package/skills/void/docs/node_modules/void/node_modules/ignore/README.md +0 -452
  369. package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/CHANGELOG.md +0 -76
  370. package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/LICENSE.md +0 -21
  371. package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/README.md +0 -364
  372. package/skills/void/docs/node_modules/void/node_modules/jsonc-parser/SECURITY.md +0 -41
  373. package/skills/void/docs/node_modules/void/node_modules/magic-string/README.md +0 -325
  374. package/skills/void/docs/node_modules/void/node_modules/ofetch/README.md +0 -398
  375. package/skills/void/docs/node_modules/void/node_modules/pathslash/README.md +0 -64
  376. package/skills/void/docs/node_modules/void/node_modules/pg/README.md +0 -96
  377. package/skills/void/docs/node_modules/void/node_modules/pglite-server/README.md +0 -135
  378. package/skills/void/docs/node_modules/void/node_modules/picocolors/README.md +0 -21
  379. package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/CHANGELOG.md +0 -108
  380. package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/README.md +0 -183
  381. package/skills/void/docs/node_modules/void/node_modules/tinyglobby/README.md +0 -25
  382. package/skills/void/docs/node_modules/void/node_modules/valibot/LICENSE.md +0 -9
  383. package/skills/void/docs/node_modules/void/node_modules/valibot/README.md +0 -94
  384. package/skills/void/docs/node_modules/void/node_modules/vite-plus/AGENTS.md +0 -16
  385. package/skills/void/docs/node_modules/void/node_modules/vite-plus/README.md +0 -220
  386. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/build.md +0 -21
  387. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/check.md +0 -35
  388. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/create.md +0 -70
  389. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/fmt.md +0 -20
  390. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/index.md +0 -35
  391. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/lint.md +0 -26
  392. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/pack.md +0 -17
  393. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/run.md +0 -364
  394. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/staged.md +0 -15
  395. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/config/test.md +0 -18
  396. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/automatic-data-tracking.md +0 -145
  397. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/build.md +0 -40
  398. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/cache.md +0 -107
  399. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/check.md +0 -60
  400. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/ci.md +0 -62
  401. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/commit-hooks.md +0 -60
  402. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/create.md +0 -341
  403. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/dev.md +0 -24
  404. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/docker.md +0 -175
  405. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/env.md +0 -167
  406. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/fmt.md +0 -41
  407. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/github-actions-cache.md +0 -165
  408. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/ide-integration.md +0 -101
  409. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/implode.md +0 -23
  410. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/index.md +0 -134
  411. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/install.md +0 -199
  412. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/lint.md +0 -50
  413. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/migrate-rules.md +0 -347
  414. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/migrate.md +0 -197
  415. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/monorepo.md +0 -176
  416. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/pack.md +0 -69
  417. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/run.md +0 -356
  418. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/test.md +0 -35
  419. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/troubleshooting.md +0 -108
  420. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/upgrade.md +0 -101
  421. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/vpx.md +0 -66
  422. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/guide/why.md +0 -39
  423. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/index.md +0 -12
  424. package/skills/void/docs/node_modules/void/node_modules/vite-plus/docs/team.md +0 -35
  425. package/skills/void/docs/node_modules/void/node_modules/vite-plus/templates/generator/README.md +0 -35
  426. package/skills/void/docs/node_modules/void/node_modules/vite-plus/templates/monorepo/README.md +0 -29
  427. package/skills/void/docs/node_modules/void/node_modules/wrangler/README.md +0 -63
  428. package/skills/void/docs/node_modules/void/node_modules/zod/README.md +0 -191
  429. package/skills/void/docs/node_modules/void/skills/migrate-vite-cloudflare-to-void/SKILL.md +0 -175
  430. package/skills/void/docs/node_modules/void/skills/void/SKILL.md +0 -76
  431. package/skills/void/docs/node_modules/void/skills/void/command/void.md +0 -7
  432. package/skills/void/docs/node_modules/void/test/e2e/README.md +0 -85
@@ -0,0 +1,4450 @@
1
+ import { r as __toESM } from "./rolldown-runtime-rQ84J-ij.mjs";
2
+ import { o as join } from "./dist-BrsS7cai.mjs";
3
+ import { $ as resolveInstallationManifest, A as password, B as assertPlatformInstallationSlugAvailable, D as log, E as intro, F as isCancel, H as latestPlatformWorkerCutoverGeneration, I as settings, J as promoteInstallationSecrets, L as u$1, M as symbol, N as text, O as note, P as CANCEL_SYMBOL, U as listInstallationManifests, X as readPendingInstallationSecrets, c as import_picocolors$1, et as savePlatformConnection, h as S_BAR_END, i as createSpinner, j as select, k as outro, m as S_BAR, n as cliCommandName, nt as writeInstallationManifest, q as platformWorkerSecretUpdateIntentState, r as cliTitle, rt as writePendingInstallationSecrets, st as require_picocolors, v as S_PASSWORD_MASK, w as confirm, z as acquirePlatformLifecycleLock } from "./output-tFQLLj26.mjs";
4
+ import { t as cfAccessHeaders } from "./cf-access-AJ1ehiFR.mjs";
5
+ import { d as openBrowser } from "./auth-W9WII-mN.mjs";
6
+ import { o as resolveWranglerArgv } from "./preset-lAy0B0BQ.mjs";
7
+ import { l as runCloudflareAuthCommand, n as announceCloudflareLogin } from "./cloudflare-auth-B1QtTO1b.mjs";
8
+ import { n as createDefaultRunWrangler, o as resolvePinnedApiBaseUrl } from "./provision-Blnstcm2.mjs";
9
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
11
+ import { tmpdir } from "node:os";
12
+ import { execFile } from "node:child_process";
13
+ import { PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_R2_OWNERSHIP_MARKER_KEY, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY, PLATFORM_SANDBOX_DRAIN_KEY, createApplicationRoutes, createPlatformDeploymentManifest, createPlatformInstallationManifest, createPlatformResourceNames, createPlatformResourceOwnershipMarker, createPlatformWorkerConfigs, normalizeApplicationDomain, normalizeInstallationSlug, parsePlatformInstallationManifest, parsePlatformResourceOwnershipMarker, recordInstallationResource, recordInstallationStep, setInstallationDesiredState, setInstallationEndpoints, transitionInstallation } from "@void/platform";
14
+ import { encodeCloudflareVersionMessage, parseCloudflareVersionMessage, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, stabilizeCloudflareVersionTriggers } from "@void/deploy-cloudflare";
15
+ import { createServer, request } from "node:http";
16
+ import { request as request$1 } from "node:https";
17
+ import { platformDatabaseSchemaDigest, resolvePlatformRuntime } from "@void/platform/installer";
18
+ //#region src/cli/platform-auth.ts
19
+ function assertPlatformOperatorCredentialCanApplyPlan(credential, plan) {
20
+ if (credential.source !== "oauth") return;
21
+ const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
22
+ const writesDns = plan.actions.some((action) => action.kind === "dns-record" && action.action !== "reuse");
23
+ if (!createsZone && !writesDns) return;
24
+ const permissions = [...createsZone ? ["Account → Zone → Edit"] : [], ...writesDns ? ["Zone → DNS → Edit"] : []];
25
+ throw new Error(`platform: Cloudflare browser OAuth grants Zone Read and cannot apply the planned zone or DNS changes. Set CLOUDFLARE_API_TOKEN with ${permissions.join(" and ")}, scoped to the selected account and application zone, then rerun the command.`);
26
+ }
27
+ async function resolvePlatformCloudflareCredential(dependencies = {}) {
28
+ const { credential } = await resolvePlatformCloudflareCredentialWithInput(async () => void 0, dependencies);
29
+ return credential;
30
+ }
31
+ /** Check saved credentials during input, but never start browser login until it succeeds. */
32
+ async function resolvePlatformCloudflareCredentialWithInput(readInput, dependencies = {}) {
33
+ const controller = new AbortController();
34
+ const runner = dependencies.runWrangler ?? ((args) => runCloudflareAuthCommand(process.cwd(), args, controller.signal));
35
+ const runWrangler = (args, capture) => {
36
+ controller.signal.throwIfAborted();
37
+ return runner(args, capture);
38
+ };
39
+ const pending = readExistingCredential(dependencies, runWrangler).then((credential) => ({
40
+ ok: true,
41
+ credential
42
+ }), (error) => ({
43
+ ok: false,
44
+ error
45
+ }));
46
+ const cancel = () => controller.abort();
47
+ const assertNotCancelled = () => {
48
+ if (controller.signal.aborted) throw new Error("platform: Cancelled.");
49
+ };
50
+ try {
51
+ const input = await readInput();
52
+ process.once("SIGINT", cancel);
53
+ const result = await pending;
54
+ assertNotCancelled();
55
+ if (!result.ok) throw result.error;
56
+ if (result.credential) return {
57
+ input,
58
+ credential: result.credential
59
+ };
60
+ announceCloudflareLogin();
61
+ const login = await runWrangler(["login", "--use-keyring"], true);
62
+ assertNotCancelled();
63
+ if (login.status !== 0) throw new Error("platform: Cloudflare browser login did not complete.");
64
+ const credential = await readWranglerOAuthCredential(runWrangler);
65
+ assertNotCancelled();
66
+ if (!credential) throw new Error("platform: Cloudflare sign-in completed without a usable OAuth credential.");
67
+ return {
68
+ input,
69
+ credential
70
+ };
71
+ } finally {
72
+ process.removeListener("SIGINT", cancel);
73
+ controller.abort();
74
+ }
75
+ }
76
+ async function readExistingCredential(dependencies, runWrangler) {
77
+ const environmentToken = process.env.CLOUDFLARE_API_TOKEN?.trim() || process.env.CF_API_TOKEN?.trim() || "";
78
+ if (environmentToken) return {
79
+ source: "environment",
80
+ token: environmentToken
81
+ };
82
+ if (!(dependencies.interactive ?? process.stdin.isTTY === true)) throw new Error("platform: Cloudflare authentication is required. Run `void cloudflare login` interactively, or set CLOUDFLARE_API_TOKEN in CI.");
83
+ if ((await runWrangler([
84
+ "auth",
85
+ "keyring",
86
+ "enable"
87
+ ], true)).status !== 0) throw new Error("platform: Could not enable secure Cloudflare credential storage. Refusing to use a plaintext OAuth credential.");
88
+ return await readWranglerOAuthCredential(runWrangler);
89
+ }
90
+ async function readWranglerOAuthCredential(runWrangler = (args) => runCloudflareAuthCommand(process.cwd(), args)) {
91
+ const result = await runWrangler([
92
+ "auth",
93
+ "token",
94
+ "--json"
95
+ ], true);
96
+ if (result.status !== 0) return null;
97
+ try {
98
+ const parsed = JSON.parse(result.stdout);
99
+ if (parsed.type !== "oauth" || typeof parsed.token !== "string" || !parsed.token.trim()) return null;
100
+ return {
101
+ source: "oauth",
102
+ token: parsed.token.trim()
103
+ };
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ //#endregion
109
+ //#region src/cli/platform-cloudflare.ts
110
+ var PlatformCloudflareApiError = class extends Error {
111
+ status;
112
+ requestPath;
113
+ constructor(message, status, requestPath) {
114
+ super(message);
115
+ this.name = "PlatformCloudflareApiError";
116
+ this.status = status;
117
+ this.requestPath = requestPath;
118
+ }
119
+ };
120
+ var PlatformWorkerCustomDomainsChangedError = class extends Error {
121
+ constructor(message, options) {
122
+ super(message, options);
123
+ this.name = "PlatformWorkerCustomDomainsChangedError";
124
+ }
125
+ };
126
+ var PlatformWorkerRoutesChangedError = class extends Error {
127
+ constructor(message, options) {
128
+ super(message, options);
129
+ this.name = "PlatformWorkerRoutesChangedError";
130
+ }
131
+ };
132
+ async function verifyPlatformRuntimeTokenPermissions(client, input) {
133
+ try {
134
+ await client.listHyperdriveConfigs(input.accountId);
135
+ } catch (error) {
136
+ throw runtimeTokenPreflightError("Account → Hyperdrive → Write", error);
137
+ }
138
+ if (!input.zoneId) return;
139
+ const probeUrl = `https://${input.applicationDomain}/.well-known/void/runtime-token-preflight-${randomUUID()}`;
140
+ try {
141
+ await client.request(`/zones/${encodeURIComponent(input.zoneId)}/purge_cache`, {
142
+ body: JSON.stringify({ files: [probeUrl] }),
143
+ method: "POST"
144
+ });
145
+ } catch (error) {
146
+ throw runtimeTokenPreflightError("Zone → Cache Purge → Purge", error);
147
+ }
148
+ if (input.customDomains) try {
149
+ await client.request(`/zones/${encodeURIComponent(input.zoneId)}/custom_hostnames?per_page=1`);
150
+ } catch (error) {
151
+ throw runtimeTokenPreflightError("Zone → SSL and Certificates → Edit", error);
152
+ }
153
+ }
154
+ function runtimeTokenPreflightError(permission, cause) {
155
+ const status = cause instanceof PlatformCloudflareApiError ? cause.status : null;
156
+ const message = status === 401 || status === 403 ? `Cloudflare denied the runtime-token check (HTTP ${status}). Check that the token is valid, scoped to this account, and includes ${permission}.` : status === 429 ? `Cloudflare rate-limited the runtime-token check for ${permission} (HTTP 429). Retry shortly; this does not mean the token is missing a permission.` : `Could not complete the runtime-token check for ${permission}${status === null ? "" : ` (Cloudflare HTTP ${status})`}. Retry the check; this failure does not establish that a permission is missing.`;
157
+ return new Error(`platform: ${message}`, { cause });
158
+ }
159
+ var PlatformCloudflareClient = class PlatformCloudflareClient {
160
+ apiBaseUrl;
161
+ #fetcher;
162
+ #token;
163
+ #aiGatewayClient;
164
+ constructor(token, options) {
165
+ if (!token) throw new Error("platform: CLOUDFLARE_API_TOKEN is required. The installed control plane also uses this scoped token to provision app resources.");
166
+ this.#token = token;
167
+ this.apiBaseUrl = (options?.apiBase ?? resolvePinnedApiBaseUrl()).replace(/\/+$/, "");
168
+ this.#fetcher = options?.fetcher ?? fetch;
169
+ if (options?.aiGatewayToken) this.#aiGatewayClient = new PlatformCloudflareClient(options.aiGatewayToken, {
170
+ apiBase: this.apiBaseUrl,
171
+ fetcher: this.#fetcher
172
+ });
173
+ }
174
+ /** Keep operator auth for infrastructure; use the existing runtime credential only for AI Gateway. */
175
+ withAiGatewayToken(token) {
176
+ return new PlatformCloudflareClient(this.#token, {
177
+ apiBase: this.apiBaseUrl,
178
+ fetcher: this.#fetcher,
179
+ aiGatewayToken: token
180
+ });
181
+ }
182
+ async request(path, init) {
183
+ return (await this.#requestEnvelope(path, init)).result;
184
+ }
185
+ async #requestEnvelope(path, init) {
186
+ const response = await this.#fetcher(`${this.apiBaseUrl}${path}`, {
187
+ ...init,
188
+ headers: {
189
+ Accept: "application/json",
190
+ Authorization: `Bearer ${this.#token}`,
191
+ ...init?.body ? { "Content-Type": "application/json" } : {},
192
+ ...init?.headers
193
+ },
194
+ signal: init?.signal ?? AbortSignal.timeout(3e4)
195
+ });
196
+ let envelope;
197
+ try {
198
+ envelope = await response.json();
199
+ } catch {
200
+ throw new PlatformCloudflareApiError(`Cloudflare returned a non-JSON response (HTTP ${response.status}).`, response.status, path.split("?")[0]);
201
+ }
202
+ if (!response.ok || envelope.success !== true || envelope.result === void 0) throw new PlatformCloudflareApiError(`Cloudflare API error: ${envelope.errors?.map((error) => error.message).filter(Boolean).join(", ") || `HTTP ${response.status}`} (HTTP ${response.status}; ${init?.method ?? "GET"} ${path.split("?")[0]}).`, response.status, path.split("?")[0]);
203
+ return {
204
+ result: envelope.result,
205
+ resultInfo: envelope.result_info
206
+ };
207
+ }
208
+ async listAccounts() {
209
+ const entries = [];
210
+ for (let page = 1;; page++) {
211
+ const result = await this.request(`/accounts?page=${page}&per_page=50`);
212
+ entries.push(...result);
213
+ if (result.length < 50) break;
214
+ }
215
+ return entries.map((entry) => ({
216
+ id: requiredString(entry.id, "Cloudflare account id"),
217
+ name: requiredString(entry.name, "Cloudflare account name")
218
+ }));
219
+ }
220
+ async getWorkersSubdomain(accountId) {
221
+ const setupUrl = `https://dash.cloudflare.com/${encodeURIComponent(accountId)}/workers-and-pages`;
222
+ let result;
223
+ try {
224
+ result = await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/subdomain`);
225
+ } catch (error) {
226
+ const status = error instanceof PlatformCloudflareApiError ? error.status : null;
227
+ const reason = status === null ? "the request failed" : `HTTP ${status}`;
228
+ const guidance = status === 401 || status === 403 ? "Check your Cloudflare login or API token permissions for this account, then rerun the command." : status === 404 ? `Check ${setupUrl}. If no subdomain is shown, finish Workers onboarding and choose one; otherwise check account access and retry.` : "Rerun the command. If the dashboard already shows a subdomain, no Workers onboarding change is needed.";
229
+ throw new Error(`platform: Could not read the workers.dev subdomain for account ${accountId} (${reason}). ${guidance}`, { cause: error });
230
+ }
231
+ if (typeof result?.subdomain !== "string" || result.subdomain.trim() === "") throw new Error(`platform: This Cloudflare account does not have a workers.dev subdomain. Open ${setupUrl}, finish Workers onboarding, choose a subdomain, and rerun the command.`);
232
+ return result.subdomain.trim();
233
+ }
234
+ async listAiGateways(accountId) {
235
+ if (this.#aiGatewayClient) return this.#aiGatewayClient.listAiGateways(accountId);
236
+ const entries = [];
237
+ for (let page = 1;; page++) {
238
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/ai-gateway/gateways?page=${page}&per_page=50`);
239
+ entries.push(...result);
240
+ if (result.length < 50) break;
241
+ }
242
+ return entries.map((entry) => ({
243
+ id: requiredString(entry.id, "AI Gateway id"),
244
+ name: requiredString(entry.id, "AI Gateway id")
245
+ }));
246
+ }
247
+ async createAiGateway(accountId, id) {
248
+ if (this.#aiGatewayClient) return this.#aiGatewayClient.createAiGateway(accountId, id);
249
+ return {
250
+ id: requiredString((await this.request(`/accounts/${encodeURIComponent(accountId)}/ai-gateway/gateways`, {
251
+ method: "POST",
252
+ body: JSON.stringify({
253
+ cache_invalidate_on_update: false,
254
+ cache_ttl: null,
255
+ collect_logs: true,
256
+ id,
257
+ rate_limiting_interval: null,
258
+ rate_limiting_limit: null
259
+ })
260
+ })).id, "AI Gateway id"),
261
+ name: id
262
+ };
263
+ }
264
+ async findZone(accountId, name) {
265
+ const exact = (await this.request(`/zones?account.id=${encodeURIComponent(accountId)}&name=${encodeURIComponent(name)}&per_page=50`)).find((zone) => zone.name === name);
266
+ return exact ? parseZone(exact) : null;
267
+ }
268
+ async createZone(accountId, name) {
269
+ return parseZone(await this.request("/zones", {
270
+ method: "POST",
271
+ body: JSON.stringify({
272
+ account: { id: accountId },
273
+ name,
274
+ type: "full"
275
+ })
276
+ }));
277
+ }
278
+ async findZoneForHostname(accountId, hostname) {
279
+ const labels = hostname.split(".");
280
+ for (let index = 0; index <= labels.length - 2; index++) {
281
+ const zone = await this.findZone(accountId, labels.slice(index).join("."));
282
+ if (zone) return zone;
283
+ }
284
+ return null;
285
+ }
286
+ async listWorkerDomains(accountId) {
287
+ return (await this.#listWorkerDomainRecords(accountId)).map((entry) => ({
288
+ hostname: requiredString(entry.hostname, "Worker custom domain hostname"),
289
+ id: requiredString(entry.id, "Worker custom domain id"),
290
+ service: requiredString(entry.service, "Worker custom domain service"),
291
+ ...typeof entry.zone_name === "string" ? { zoneName: entry.zone_name } : {}
292
+ }));
293
+ }
294
+ /**
295
+ * Add missing Custom Domains without replacing the Worker's existing set or
296
+ * authorizing takeover of another Worker origin or DNS record. Wrangler
297
+ * enables destructive overrides automatically without a TTY, so the
298
+ * installer deliberately owns this mutation through the API instead.
299
+ */
300
+ async publishWorkerCustomDomainsSafely(accountId, workerName, origins) {
301
+ const before = await this.listWorkerDomains(accountId);
302
+ const pending = origins.filter((origin) => {
303
+ const matches = before.filter((domain) => domain.hostname === origin.hostname);
304
+ if (matches.length > 1 || matches.some((domain) => domain.service !== workerName)) throw new PlatformWorkerCustomDomainsChangedError(`platform: Custom Domain ${JSON.stringify(origin.hostname)} changed concurrently before publication; refusing to replace its traffic state.`);
305
+ return matches.length === 0;
306
+ });
307
+ if (pending.length === 0) return;
308
+ try {
309
+ await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}/domains/records`, {
310
+ method: "PUT",
311
+ body: JSON.stringify({
312
+ override_scope: false,
313
+ override_existing_origin: false,
314
+ override_existing_dns_record: false,
315
+ origins: pending.map((origin) => ({
316
+ hostname: origin.hostname,
317
+ ...origin.zoneId ? { zone_id: origin.zoneId } : {},
318
+ ...origin.zoneName ? { zone_name: origin.zoneName } : {},
319
+ ...origin.enabled === void 0 ? {} : { enabled: origin.enabled },
320
+ ...origin.previewsEnabled === void 0 ? {} : { previews_enabled: origin.previewsEnabled }
321
+ }))
322
+ })
323
+ });
324
+ } catch (error) {
325
+ throw new PlatformWorkerCustomDomainsChangedError(`platform: Custom Domain publication for Worker ${JSON.stringify(workerName)} had an ambiguous outcome; retaining live domains for safe reconciliation.`, { cause: error });
326
+ }
327
+ const expected = [...origins].map((origin) => origin.hostname).sort();
328
+ const inventory = await this.listWorkerDomains(accountId);
329
+ const actual = inventory.filter((domain) => domain.service === workerName).map((domain) => domain.hostname).sort();
330
+ 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.`);
331
+ }
332
+ async #listWorkerDomainRecords(accountId, service) {
333
+ const entries = [];
334
+ const perPage = 100;
335
+ for (let page = 1;; page++) {
336
+ const query = new URLSearchParams({
337
+ page: String(page),
338
+ per_page: String(perPage)
339
+ });
340
+ if (service) query.set("service", service);
341
+ const response = await this.#requestEnvelope(`/accounts/${encodeURIComponent(accountId)}/workers/domains?${query}`);
342
+ entries.push(...response.result);
343
+ const totalPages = response.resultInfo?.total_pages;
344
+ if (typeof totalPages === "number" && page >= totalPages || totalPages === void 0 && response.result.length < perPage) break;
345
+ }
346
+ return entries;
347
+ }
348
+ async listWorkerRoutes(zoneId) {
349
+ return (await this.request(`/zones/${encodeURIComponent(zoneId)}/workers/routes`)).map((entry) => ({
350
+ id: requiredString(entry.id, "Worker route id"),
351
+ pattern: requiredString(entry.pattern, "Worker route pattern"),
352
+ script: typeof entry.script === "string" ? entry.script : null
353
+ }));
354
+ }
355
+ async publishWorkerRoutesSafely(zoneId, workerName, patterns) {
356
+ const before = await this.listWorkerRoutes(zoneId);
357
+ const pending = patterns.filter((pattern) => {
358
+ const matches = before.filter((route) => route.pattern === pattern);
359
+ if (matches.length > 1 || matches.some((route) => route.script !== workerName)) throw new PlatformWorkerRoutesChangedError(`platform: Worker route ${JSON.stringify(pattern)} changed concurrently before publication; refusing to replace its traffic state.`);
360
+ return matches.length === 0;
361
+ });
362
+ for (const pattern of pending) try {
363
+ await this.request(`/zones/${encodeURIComponent(zoneId)}/workers/routes`, {
364
+ body: JSON.stringify({
365
+ pattern,
366
+ script: workerName
367
+ }),
368
+ method: "POST"
369
+ });
370
+ } catch (error) {
371
+ throw new PlatformWorkerRoutesChangedError(`platform: Worker route publication for ${JSON.stringify(pattern)} had an ambiguous outcome; retaining live routes for safe reconciliation.`, { cause: error });
372
+ }
373
+ const after = await this.listWorkerRoutes(zoneId);
374
+ for (const pattern of patterns) {
375
+ const matches = after.filter((route) => route.pattern === pattern);
376
+ if (matches.length !== 1 || matches[0].script !== workerName) throw new PlatformWorkerRoutesChangedError(`platform: Worker route ${JSON.stringify(pattern)} changed concurrently after publication; retaining live routes for safe reconciliation.`);
377
+ }
378
+ }
379
+ async listWorkerServiceRoutes(accountId, workerName) {
380
+ return (await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/services/${encodeURIComponent(workerName)}/environments/production/routes?show_zonename=true`)).map((entry) => ({
381
+ pattern: requiredString(entry.pattern, "Worker service route pattern"),
382
+ zoneName: requiredString(entry.zone_name, "Worker service route zone name")
383
+ }));
384
+ }
385
+ async listDnsRecords(zoneId, name) {
386
+ const query = new URLSearchParams({
387
+ name,
388
+ per_page: "100"
389
+ });
390
+ return (await this.request(`/zones/${encodeURIComponent(zoneId)}/dns_records?${query}`)).map(parseDnsRecord);
391
+ }
392
+ /** Read deployed edge certificates; requesting a pack is not proof of issuance. */
393
+ async hasActiveCertificateForHostname(zoneId, hostname) {
394
+ const expected = hostname.toLowerCase().replace(/\.$/, "");
395
+ const covers = (value) => {
396
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
397
+ const cert = value;
398
+ if (cert.status !== "active" || !Array.isArray(cert.hosts)) return false;
399
+ if (cert.expires_on !== void 0 && (typeof cert.expires_on !== "string" || !Number.isFinite(Date.parse(cert.expires_on)) || Date.parse(cert.expires_on) <= Date.now())) return false;
400
+ return cert.hosts.some((host) => typeof host === "string" && host.toLowerCase().replace(/\.$/, "") === expected);
401
+ };
402
+ for (const kind of ["ssl/certificate_packs", "custom_certificates"]) for (let page = 1; page <= 200; page++) {
403
+ const query = new URLSearchParams({
404
+ page: String(page),
405
+ per_page: "50"
406
+ });
407
+ if (kind === "ssl/certificate_packs") query.set("deploy", "production");
408
+ const { result, resultInfo } = await this.#requestEnvelope(`/zones/${encodeURIComponent(zoneId)}/${kind}?${query}`);
409
+ if (!Array.isArray(result)) throw new Error("platform: Cloudflare returned an invalid certificate inventory.");
410
+ for (const entry of result) if ((kind === "ssl/certificate_packs" ? Array.isArray(entry.certificates) ? entry.certificates : [] : [entry]).some(covers)) return true;
411
+ if (result.length === 0 || (resultInfo?.total_pages !== void 0 ? page >= resultInfo.total_pages : result.length < 50)) break;
412
+ if (page === 200) throw new Error("platform: Certificate inventory is too large to verify safely.");
413
+ }
414
+ return false;
415
+ }
416
+ async createDnsRecord(zoneId, name) {
417
+ return parseDnsRecord(await this.request(`/zones/${encodeURIComponent(zoneId)}/dns_records`, {
418
+ method: "POST",
419
+ body: JSON.stringify({
420
+ content: "100::",
421
+ name,
422
+ proxied: true,
423
+ ttl: 1,
424
+ type: "AAAA"
425
+ })
426
+ }));
427
+ }
428
+ async listKvNamespaces(accountId, title) {
429
+ const entries = [];
430
+ for (let page = 1;; page++) {
431
+ const query = new URLSearchParams({
432
+ page: String(page),
433
+ per_page: "100"
434
+ });
435
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces?${query}`);
436
+ entries.push(...result);
437
+ if (result.length < 100) break;
438
+ }
439
+ const namespaces = entries.map((entry) => ({
440
+ id: requiredString(entry.id, "KV namespace id"),
441
+ name: requiredString(entry.title, "KV namespace title")
442
+ }));
443
+ return title ? namespaces.filter((namespace) => namespace.name === title) : namespaces;
444
+ }
445
+ async createKvNamespace(accountId, title) {
446
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces`, {
447
+ method: "POST",
448
+ body: JSON.stringify({ title })
449
+ });
450
+ return {
451
+ id: requiredString(result.id, "KV namespace id"),
452
+ name: typeof result.title === "string" ? result.title : title
453
+ };
454
+ }
455
+ async listR2Buckets(accountId) {
456
+ const buckets = [];
457
+ let startAfter = "";
458
+ for (;;) {
459
+ const query = new URLSearchParams({
460
+ direction: "asc",
461
+ order: "name",
462
+ per_page: "1000"
463
+ });
464
+ if (startAfter) query.set("start_after", startAfter);
465
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/r2/buckets?${query}`);
466
+ const page = Array.isArray(result) ? result : "buckets" in result ? result.buckets ?? [] : [];
467
+ buckets.push(...page);
468
+ if (page.length < 1e3) break;
469
+ const next = requiredString(page.at(-1)?.name, "R2 bucket name");
470
+ if (next === startAfter) throw new Error("platform: Cloudflare returned a non-advancing R2 bucket page.");
471
+ startAfter = next;
472
+ }
473
+ return buckets.map((entry) => {
474
+ const name = requiredString(entry.name, "R2 bucket name");
475
+ return {
476
+ fingerprint: requiredString(entry.creation_date, "R2 bucket creation date"),
477
+ id: name,
478
+ name
479
+ };
480
+ });
481
+ }
482
+ async createR2Bucket(accountId, name) {
483
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/r2/buckets`, {
484
+ method: "POST",
485
+ body: JSON.stringify({ name })
486
+ });
487
+ const resultName = typeof result.name === "string" ? result.name : name;
488
+ const fingerprint = typeof result.creation_date === "string" && result.creation_date ? result.creation_date : (await this.listR2Buckets(accountId)).find((bucket) => bucket.name === resultName)?.fingerprint;
489
+ if (!fingerprint) throw new Error(`platform: Cloudflare created R2 bucket ${JSON.stringify(resultName)} without returning a verifiable creation fingerprint.`);
490
+ return {
491
+ fingerprint,
492
+ id: resultName,
493
+ name: resultName
494
+ };
495
+ }
496
+ async listQueues(accountId, name) {
497
+ return (await this.request(`/accounts/${encodeURIComponent(accountId)}/queues?name=${encodeURIComponent(name)}&per_page=100`)).map((entry) => ({
498
+ id: requiredString(entry.queue_id, "Queue id"),
499
+ name: requiredString(entry.queue_name, "Queue name")
500
+ }));
501
+ }
502
+ async createQueue(accountId, name) {
503
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/queues`, {
504
+ method: "POST",
505
+ body: JSON.stringify({ queue_name: name })
506
+ });
507
+ return {
508
+ id: requiredString(result.queue_id, "Queue id"),
509
+ name: typeof result.queue_name === "string" ? result.queue_name : name
510
+ };
511
+ }
512
+ async listDispatchNamespaces(accountId) {
513
+ return (await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/dispatch/namespaces`)).map((entry) => ({
514
+ id: requiredString(entry.namespace_id, "Dispatch namespace id"),
515
+ name: requiredString(entry.namespace_name, "Dispatch namespace name")
516
+ }));
517
+ }
518
+ async createDispatchNamespace(accountId, name) {
519
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/dispatch/namespaces`, {
520
+ method: "POST",
521
+ body: JSON.stringify({ name })
522
+ });
523
+ return {
524
+ id: requiredString(result.namespace_id, "Dispatch namespace id"),
525
+ name: typeof result.namespace_name === "string" ? result.namespace_name : name
526
+ };
527
+ }
528
+ async listHyperdriveConfigs(accountId) {
529
+ const entries = [];
530
+ for (let page = 1;; page++) {
531
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/hyperdrive/configs?page=${page}&per_page=50`);
532
+ entries.push(...result);
533
+ if (result.length < 50) break;
534
+ }
535
+ return entries.map((entry) => ({
536
+ id: requiredString(entry.id, "Hyperdrive id"),
537
+ name: requiredString(entry.name, "Hyperdrive name")
538
+ }));
539
+ }
540
+ async getHyperdriveConfig(accountId, id) {
541
+ return parseHyperdriveConfig(await this.request(`/accounts/${encodeURIComponent(accountId)}/hyperdrive/configs/${encodeURIComponent(id)}`));
542
+ }
543
+ async createHyperdrive(accountId, name, connectionString) {
544
+ const parsed = parsePostgresConnectionString(connectionString);
545
+ const result = await this.request(`/accounts/${encodeURIComponent(accountId)}/hyperdrive/configs`, {
546
+ method: "POST",
547
+ body: JSON.stringify({
548
+ name,
549
+ origin: {
550
+ database: parsed.database,
551
+ host: parsed.host,
552
+ password: parsed.password,
553
+ port: parsed.port,
554
+ scheme: "postgresql",
555
+ user: parsed.user
556
+ },
557
+ ...parsed.mtls ? { mtls: parsed.mtls } : {}
558
+ })
559
+ });
560
+ return {
561
+ id: requiredString(result.id, "Hyperdrive id"),
562
+ name: typeof result.name === "string" ? result.name : name
563
+ };
564
+ }
565
+ async listWorkerNames(accountId) {
566
+ return (await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/scripts`)).map((entry) => requiredString(entry.id, "Worker name"));
567
+ }
568
+ async putKvValue(accountId, namespaceId, key, value) {
569
+ await this.request(`/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}/values/${encodeURIComponent(key)}`, {
570
+ method: "PUT",
571
+ body: value,
572
+ headers: { "Content-Type": "application/json" }
573
+ });
574
+ }
575
+ async getKvValue(accountId, namespaceId, key) {
576
+ const response = await this.#fetcher(`${this.apiBaseUrl}/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}/values/${encodeURIComponent(key)}`, {
577
+ headers: { Authorization: `Bearer ${this.#token}` },
578
+ signal: AbortSignal.timeout(3e4)
579
+ });
580
+ if (response.status === 404) return null;
581
+ if (!response.ok) throw new PlatformCloudflareApiError(`Cloudflare API error reading the installation manifest (HTTP ${response.status}).`, response.status);
582
+ return response.text();
583
+ }
584
+ async putR2Object(accountId, bucketName, key, value) {
585
+ const response = await this.#fetcher(`${this.apiBaseUrl}/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects/${encodeR2ObjectKey(key)}`, {
586
+ body: value,
587
+ headers: {
588
+ Authorization: `Bearer ${this.#token}`,
589
+ "Content-Type": "application/json"
590
+ },
591
+ method: "PUT",
592
+ signal: AbortSignal.timeout(3e4)
593
+ });
594
+ if (!response.ok) throw new PlatformCloudflareApiError(`Cloudflare API error writing the R2 ownership marker (HTTP ${response.status}).`, response.status);
595
+ }
596
+ async getR2Object(accountId, bucketName, key) {
597
+ const response = await this.#fetcher(`${this.apiBaseUrl}/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects/${encodeR2ObjectKey(key)}`, {
598
+ headers: { Authorization: `Bearer ${this.#token}` },
599
+ signal: AbortSignal.timeout(3e4)
600
+ });
601
+ if (response.status === 404) return null;
602
+ if (!response.ok) throw new PlatformCloudflareApiError(`Cloudflare API error reading the R2 ownership marker (HTTP ${response.status}).`, response.status);
603
+ return response.text();
604
+ }
605
+ async deleteR2Object(accountId, bucketName, key) {
606
+ try {
607
+ await this.deleteR2Objects(accountId, bucketName, [key]);
608
+ } catch (error) {
609
+ if (error instanceof PlatformCloudflareApiError && error.status === 404) return;
610
+ throw error;
611
+ }
612
+ }
613
+ async getWorkerPlainTextBindings(accountId, workerName) {
614
+ const settings = await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}/settings`);
615
+ const bindings = {};
616
+ for (const binding of settings.bindings ?? []) if (binding.type === "plain_text" && typeof binding.name === "string" && typeof binding.text === "string") bindings[binding.name] = binding.text;
617
+ return bindings;
618
+ }
619
+ async deleteKvValue(accountId, namespaceId, key) {
620
+ try {
621
+ await this.request(`/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}/values/${encodeURIComponent(key)}`, { method: "DELETE" });
622
+ } catch (error) {
623
+ if (error instanceof PlatformCloudflareApiError && error.status === 404) return;
624
+ throw error;
625
+ }
626
+ }
627
+ async emptyR2Bucket(accountId, bucketName, preservedKeys = /* @__PURE__ */ new Set()) {
628
+ let cursor = "";
629
+ const observedCursors = /* @__PURE__ */ new Set();
630
+ for (;;) {
631
+ const query = new URLSearchParams({ per_page: "1000" });
632
+ if (cursor) query.set("cursor", cursor);
633
+ const response = await this.#fetcher(`${this.apiBaseUrl}/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects?${query}`, {
634
+ headers: {
635
+ Accept: "application/json",
636
+ Authorization: `Bearer ${this.#token}`
637
+ },
638
+ signal: AbortSignal.timeout(3e4)
639
+ });
640
+ if (response.status === 404) return;
641
+ const envelope = await response.json();
642
+ if (!response.ok || envelope.success !== true || !Array.isArray(envelope.result)) throw new PlatformCloudflareApiError(`Cloudflare API error listing R2 objects: ${envelope.errors?.map((error) => error.message).filter(Boolean).join(", ") || `HTTP ${response.status}`}`, response.status);
643
+ const keys = envelope.result.map((object) => requiredString(object.key, "R2 object key")).filter((key) => !preservedKeys.has(key));
644
+ for (let offset = 0; offset < keys.length; offset += 1e3) await this.deleteR2Objects(accountId, bucketName, keys.slice(offset, offset + 1e3));
645
+ const nextCursor = envelope.result_info?.cursor ?? "";
646
+ if (!nextCursor) return;
647
+ if (observedCursors.has(nextCursor)) throw new Error("platform: Cloudflare returned a repeated R2 object cursor.");
648
+ observedCursors.add(nextCursor);
649
+ cursor = nextCursor;
650
+ }
651
+ }
652
+ async deleteR2Objects(accountId, bucketName, keys) {
653
+ if (keys.length === 0) return;
654
+ const deleted = await this.request(`/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects`, {
655
+ body: JSON.stringify(keys),
656
+ method: "DELETE"
657
+ });
658
+ const deletedKeys = new Set(deleted.map((entry) => requiredString(entry.key, "deleted R2 object key")));
659
+ if (keys.find((key) => !deletedKeys.has(key)) !== void 0) throw new Error("platform: Cloudflare did not confirm every requested R2 object deletion.");
660
+ }
661
+ async deleteResource(accountId, kind, id, zoneId) {
662
+ const account = encodeURIComponent(accountId);
663
+ const encoded = encodeURIComponent(id);
664
+ 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;
665
+ if (!path) throw new Error(`platform: Unsupported Cloudflare deletion kind ${kind}.`);
666
+ try {
667
+ await this.request(path, { method: "DELETE" });
668
+ } catch (error) {
669
+ if (error instanceof PlatformCloudflareApiError && error.status === 404) return;
670
+ throw error;
671
+ }
672
+ }
673
+ async removeRoutes(_accountId, _zoneId, tracked) {
674
+ 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.");
675
+ }
676
+ async setWorkerSubdomain(accountId, workerName, enabled) {
677
+ try {
678
+ await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}/subdomain`, {
679
+ body: JSON.stringify({
680
+ enabled,
681
+ previews_enabled: enabled
682
+ }),
683
+ headers: { "Cloudflare-Workers-Script-Api-Date": "2025-08-01" },
684
+ method: "POST"
685
+ });
686
+ } catch (error) {
687
+ if (!enabled && error instanceof PlatformCloudflareApiError && error.status === 404) return;
688
+ throw error;
689
+ }
690
+ }
691
+ };
692
+ function encodeR2ObjectKey(key) {
693
+ return key.split("/").map(encodeURIComponent).join("/");
694
+ }
695
+ function requiredString(value, label) {
696
+ if (typeof value !== "string" || value === "") throw new Error(`platform: ${label} is missing from the Cloudflare response.`);
697
+ return value;
698
+ }
699
+ function parseZone(value) {
700
+ return {
701
+ id: requiredString(value.id, "Zone id"),
702
+ name: requiredString(value.name, "Zone name"),
703
+ nameServers: Array.isArray(value.name_servers) ? value.name_servers.filter((entry) => typeof entry === "string") : [],
704
+ status: requiredString(value.status, "Zone status")
705
+ };
706
+ }
707
+ function parseDnsRecord(value) {
708
+ return {
709
+ content: requiredString(value.content, "DNS record content"),
710
+ id: requiredString(value.id, "DNS record id"),
711
+ name: requiredString(value.name, "DNS record name"),
712
+ proxied: value.proxied === true,
713
+ type: requiredString(value.type, "DNS record type")
714
+ };
715
+ }
716
+ function parsePostgresConnectionString(value) {
717
+ let url;
718
+ try {
719
+ url = new URL(value);
720
+ } catch {
721
+ throw new Error("platform: DATABASE_URL must be a PostgreSQL connection string.");
722
+ }
723
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:" || !url.hostname) throw new Error("platform: DATABASE_URL must use postgres:// or postgresql://.");
724
+ const database = decodeURIComponent(url.pathname.replace(/^\//, ""));
725
+ if (!url.username || !url.password || !database) throw new Error("platform: DATABASE_URL must include username, password, host, and database.");
726
+ const port = url.port ? Number(url.port) : 5432;
727
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error("platform: DATABASE_URL has an invalid PostgreSQL port.");
728
+ const sslmode = url.searchParams.get("sslmode");
729
+ let mtls;
730
+ if (sslmode) {
731
+ if (sslmode !== "require" && sslmode !== "verify-ca" && sslmode !== "verify-full") throw new Error("platform: Unsupported sslmode. Cloudflare Hyperdrive supports \"require\", \"verify-ca\", or \"verify-full\".");
732
+ mtls = { sslmode };
733
+ }
734
+ return {
735
+ database,
736
+ host: url.hostname,
737
+ ...mtls ? { mtls } : {},
738
+ password: decodeURIComponent(url.password),
739
+ port,
740
+ user: decodeURIComponent(url.username)
741
+ };
742
+ }
743
+ function hyperdriveOriginFromConnectionString(value) {
744
+ const parsed = parsePostgresConnectionString(value);
745
+ return {
746
+ database: parsed.database,
747
+ host: parsed.host.toLowerCase(),
748
+ port: parsed.port,
749
+ scheme: "postgresql",
750
+ user: parsed.user
751
+ };
752
+ }
753
+ function hyperdriveOriginConfigHash(origin) {
754
+ return createHash("sha256").update(JSON.stringify({
755
+ database: origin.database,
756
+ host: origin.host.toLowerCase(),
757
+ port: origin.port,
758
+ scheme: "postgresql",
759
+ user: origin.user
760
+ })).digest("hex");
761
+ }
762
+ function hyperdriveOriginMatches(live, expected) {
763
+ return live.database === expected.database && live.host.toLowerCase() === expected.host.toLowerCase() && live.port === expected.port && live.scheme === expected.scheme && (live.user === null || live.user === expected.user);
764
+ }
765
+ function parseHyperdriveConfig(value) {
766
+ const origin = value.origin;
767
+ if (!origin || typeof origin !== "object" || Array.isArray(origin)) throw new Error("platform: Cloudflare Hyperdrive config has no origin metadata.");
768
+ const record = origin;
769
+ const scheme = requiredString(record.scheme, "Hyperdrive origin scheme").toLowerCase();
770
+ if (scheme !== "postgres" && scheme !== "postgresql") throw new Error(`platform: Unsupported Hyperdrive origin scheme ${JSON.stringify(scheme)}.`);
771
+ const port = typeof record.port === "number" ? record.port : Number(record.port);
772
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error("platform: Cloudflare Hyperdrive config has an invalid origin port.");
773
+ return {
774
+ id: requiredString(value.id, "Hyperdrive id"),
775
+ name: requiredString(value.name, "Hyperdrive name"),
776
+ origin: {
777
+ database: requiredString(record.database, "Hyperdrive origin database"),
778
+ host: requiredString(record.host, "Hyperdrive origin host").toLowerCase(),
779
+ port,
780
+ scheme: "postgresql",
781
+ user: typeof record.user === "string" && record.user ? record.user : null
782
+ }
783
+ };
784
+ }
785
+ //#endregion
786
+ //#region src/cli/platform-plan.ts
787
+ async function createPlatformInstallPlan(client, input, existing, options) {
788
+ const account = resolveAccount(await client.listAccounts(), input.accountId);
789
+ if (existing && existing.account.id !== account.id) throw new Error(`platform: Refusing to resume installation ${existing.id} in a different Cloudflare account.`);
790
+ const installationSlug = normalizeInstallationSlug(input.installationSlug);
791
+ const installationId = existing?.id ?? `${installationSlug}-${createHash("sha256").update(JSON.stringify({
792
+ accountId: account.id,
793
+ applicationDomain: input.applicationDomain,
794
+ controlPlaneDomain: input.controlPlaneDomain,
795
+ installationSlug,
796
+ zoneName: input.zoneName
797
+ })).digest("hex").slice(0, 12)}`;
798
+ const accountSubdomain = await client.getWorkersSubdomain(account.id);
799
+ const workersDev = existing?.deployment.application.workersDev ?? (input.workersDev ? {
800
+ subdomain: accountSubdomain,
801
+ installationId
802
+ } : void 0);
803
+ if (workersDev && workersDev.subdomain !== accountSubdomain) throw new Error("platform: The account workers.dev subdomain changed. Restore it before continuing.");
804
+ const testing = !!workersDev && !input.zoneName;
805
+ const deployment = createPlatformDeploymentManifest({
806
+ application: {
807
+ baseDomain: testing ? `${accountSubdomain}.workers.dev` : input.applicationDomain,
808
+ dedicatedZone: input.dedicatedZone,
809
+ zoneName: input.zoneName,
810
+ ...workersDev ? { workersDev } : {},
811
+ ...existing?.deployment.application.zoneId ? { zoneId: existing.deployment.application.zoneId } : {}
812
+ },
813
+ controlPlane: {
814
+ apiHostname: input.controlPlaneDomain || null,
815
+ dashboardHostname: null,
816
+ proxyHostname: null
817
+ },
818
+ displayName: input.displayName,
819
+ installationSlug,
820
+ resources: existing?.deployment.resources ?? createPlatformResourceNames(installationSlug)
821
+ });
822
+ if (existing && JSON.stringify(existing.deployment) !== JSON.stringify(deployment)) throw new Error(`platform: --resume inputs do not match installation ${existing.id}. Use its recorded configuration.`);
823
+ const names = deployment.resources;
824
+ const wildcardHostname = `*.${deployment.application.baseDomain}`;
825
+ const zone = testing ? null : await client.findZone(account.id, deployment.application.zoneName);
826
+ if (!testing && deployment.application.baseDomain !== deployment.application.zoneName) {
827
+ const certificateHelp = `Use the zone apex ${deployment.application.zoneName} as --application-domain, or provision an active ${wildcardHostname} edge certificate in the existing zone before installing. Cloudflare Advanced Certificate Manager is a paid add-on; an existing custom wildcard certificate also works. Manage certificates at https://dash.cloudflare.com/${account.id}/${deployment.application.zoneName}/ssl-tls/edge-certificates. Certificate inspection requires Zone > SSL and Certificates > Read (or Edit) on this zone.`;
828
+ if (!zone || zone.status !== "active") throw new Error(`platform: Nested application domain ${deployment.application.baseDomain} needs an active zone and wildcard certificate. ${certificateHelp}`);
829
+ let covered;
830
+ try {
831
+ covered = await client.hasActiveCertificateForHostname(zone.id, wildcardHostname);
832
+ } catch (error) {
833
+ throw new Error(`platform: Could not verify edge certificate coverage for ${wildcardHostname}. ${certificateHelp}`, { cause: error });
834
+ }
835
+ if (!covered) throw new Error(`platform: No active edge certificate covers ${wildcardHostname}. Universal SSL for ${deployment.application.zoneName} does not cover these deeper application hostnames. ${certificateHelp}`);
836
+ }
837
+ let needsAiGatewayCredential = false;
838
+ const gatewayInventory = client.listAiGateways(account.id).catch((error) => {
839
+ if (options?.allowUnverifiedAiGateway && error instanceof PlatformCloudflareApiError && error.status === 403) {
840
+ needsAiGatewayCredential = true;
841
+ return [];
842
+ }
843
+ throw error;
844
+ });
845
+ const [workersSubdomain, aiGateways, routingKv, isrKv, legacyKv, r2Buckets, usageQueues, cronQueues, dispatchNamespaces, hyperdrives, workers] = await Promise.all([
846
+ Promise.resolve(accountSubdomain),
847
+ gatewayInventory,
848
+ client.listKvNamespaces(account.id, names.routingKv),
849
+ client.listKvNamespaces(account.id, names.isrCacheKv),
850
+ client.listKvNamespaces(account.id, names.legacyAssetsKv),
851
+ client.listR2Buckets(account.id),
852
+ client.listQueues(account.id, names.usageQueue),
853
+ client.listQueues(account.id, names.cronQueue),
854
+ client.listDispatchNamespaces(account.id),
855
+ client.listHyperdriveConfigs(account.id),
856
+ client.listWorkerNames(account.id)
857
+ ]);
858
+ const dnsRecords = zone ? await client.listDnsRecords(zone.id, wildcardHostname) : [];
859
+ const remoteOwnership = existing ? await readRemoteOwnership(client, account.id, routingKv, existing) : null;
860
+ const intentProofs = /* @__PURE__ */ new Set();
861
+ const ambiguousResources = /* @__PURE__ */ new Set();
862
+ if (existing) {
863
+ const bucket = r2Buckets.find((resource) => resource.name === names.assetsBucket);
864
+ const recordedBucket = existing.resources.find((resource) => resource.key === "assets-bucket" && resource.kind === "r2-bucket" && resource.ownership === "created" && resource.state === "ready");
865
+ if (bucket && recordedBucket?.id === bucket.id && recordedBucket.name === bucket.name) {
866
+ 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");
867
+ }
868
+ if (bucket && hasUnrecordedProvisionIntent(existing, "assets-bucket", "r2-bucket", bucket.name)) {
869
+ if (matchesResourceMarker(await client.getR2Object(account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY), existing, "assets-bucket", "r2-bucket", bucket)) intentProofs.add("assets-bucket");
870
+ }
871
+ for (const [key, name] of [
872
+ ["api-worker", names.apiWorker],
873
+ ["proxy-worker", names.proxyWorker],
874
+ ["tail-worker", names.tailWorker],
875
+ ["dispatch-worker", names.dispatchWorker]
876
+ ]) if (workers.includes(name) && hasUnrecordedProvisionIntent(existing, key, "worker", name)) {
877
+ if (matchesResourceMarker((await client.getWorkerPlainTextBindings(account.id, name)).PLATFORM_OWNERSHIP_MARKER, existing, key, "worker", {
878
+ id: name,
879
+ name
880
+ })) intentProofs.add(key);
881
+ }
882
+ }
883
+ if (zone) {
884
+ const expectedPatterns = new Set(createApplicationRoutes(deployment.application).map((route) => route.pattern));
885
+ const expectedWorker = deployment.resources.dispatchWorker;
886
+ const ownedWorker = intentProofs.has("dispatch-worker") || existing?.resources.some((resource) => resource.key === "dispatch-worker" && resource.kind === "worker" && resource.name === expectedWorker && resource.state === "ready");
887
+ const conflicts = (await client.listWorkerRoutes(zone.id)).filter((route) => expectedPatterns.has(route.pattern) && (route.script !== expectedWorker || !ownedWorker));
888
+ if (conflicts.length > 0) {
889
+ const conflict = conflicts[0];
890
+ throw new Error(`platform: Application route ${JSON.stringify(conflict.pattern)} is already assigned to ${JSON.stringify(conflict.script ?? "no Worker")}. Choose another application domain.`);
891
+ }
892
+ }
893
+ if (deployment.controlPlane.apiHostname) {
894
+ const apiHostname = deployment.controlPlane.apiHostname;
895
+ const [workerDomains, controlPlaneZone] = await Promise.all([client.listWorkerDomains(account.id), client.findZoneForHostname(account.id, apiHostname)]);
896
+ const matchingDomain = workerDomains.find((domain) => domain.hostname === apiHostname);
897
+ const expectedWorker = deployment.resources.apiWorker;
898
+ if (matchingDomain && matchingDomain.service !== expectedWorker) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} is already assigned to Worker ${JSON.stringify(matchingDomain.service)}. Choose another hostname.`);
899
+ const records = controlPlaneZone ? await client.listDnsRecords(controlPlaneZone.id, apiHostname) : [];
900
+ const ownedApiWorker = intentProofs.has("api-worker") || existing?.resources.some((resource) => resource.key === "api-worker" && resource.kind === "worker" && resource.name === expectedWorker && resource.state === "ready");
901
+ 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.`);
902
+ }
903
+ const actions = [];
904
+ if (!testing) {
905
+ const recordedZone = existing?.resources.find((resource) => resource.key === "zone");
906
+ const remotelyOwnedZone = remoteOwnership?.resources.some((resource) => resource.key === "zone" && resource.id === zone?.id);
907
+ const repairCreatedZone = options?.repairMissing && recordedZone?.ownership === "created" && !zone;
908
+ if (recordedZone && !repairCreatedZone && (!zone || recordedZone.id !== zone.id || recordedZone.kind !== "zone")) throw new Error(`platform: Recorded zone ${recordedZone.name} (${recordedZone.id}) no longer matches the Cloudflare account.`);
909
+ actions.push({
910
+ action: repairCreatedZone ? "create" : recordedZone || zone && remotelyOwnedZone ? "reuse" : zone ? "adopt" : "create",
911
+ id: zone?.id ?? null,
912
+ key: "zone",
913
+ kind: "zone",
914
+ name: deployment.application.zoneName,
915
+ reason: repairCreatedZone ? "Recreate the missing installer-owned zone." : recordedZone || remotelyOwnedZone ? "Reuse the recorded zone." : zone ? "Use the existing zone without taking ownership." : "Create the requested zone."
916
+ });
917
+ addDnsPlan(actions, existing, wildcardHostname, dnsRecords, options);
918
+ }
919
+ if (needsAiGatewayCredential) actions.push({
920
+ action: "inspect",
921
+ id: null,
922
+ key: "ai-gateway",
923
+ kind: "ai-gateway",
924
+ name: names.aiGateway,
925
+ reason: "AI Gateway access will be verified with the runtime token before provisioning (AI Gateway: Edit required)."
926
+ });
927
+ else addNamedPlan(actions, existing, "ai-gateway", "ai-gateway", names.aiGateway, aiGateways.filter((resource) => resource.id === names.aiGateway), "reuse", options, remoteOwnership, intentProofs);
928
+ addNamedPlan(actions, existing, "routing-kv", "kv-namespace", names.routingKv, routingKv, "reuse", options, remoteOwnership, intentProofs);
929
+ addNamedPlan(actions, existing, "isr-cache-kv", "kv-namespace", names.isrCacheKv, isrKv, "reuse", options, remoteOwnership);
930
+ addNamedPlan(actions, existing, "legacy-assets-kv", "kv-namespace", names.legacyAssetsKv, legacyKv, "reuse", options, remoteOwnership);
931
+ addNamedPlan(actions, existing, "assets-bucket", "r2-bucket", names.assetsBucket, r2Buckets.filter((resource) => resource.name === names.assetsBucket), "reuse", options, remoteOwnership, intentProofs, ambiguousResources);
932
+ addNamedPlan(actions, existing, "usage-queue", "queue", names.usageQueue, usageQueues, "reuse", options, remoteOwnership);
933
+ addNamedPlan(actions, existing, "cron-queue", "queue", names.cronQueue, cronQueues, "reuse", options, remoteOwnership);
934
+ addNamedPlan(actions, existing, "dispatch-namespace", "dispatch-namespace", names.dispatchNamespace, dispatchNamespaces.filter((resource) => resource.name === names.dispatchNamespace), "reuse", options, remoteOwnership);
935
+ addNamedPlan(actions, existing, "hyperdrive", "hyperdrive", names.hyperdrive, hyperdrives.filter((resource) => resource.name === names.hyperdrive), "reuse", options, remoteOwnership);
936
+ for (const [key, name] of [
937
+ ["api-worker", names.apiWorker],
938
+ ["proxy-worker", names.proxyWorker],
939
+ ["tail-worker", names.tailWorker],
940
+ ["dispatch-worker", names.dispatchWorker]
941
+ ]) addNamedPlan(actions, existing, key, "worker", name, workers.includes(name) ? [{
942
+ id: name,
943
+ name
944
+ }] : [], "update", options, remoteOwnership, intentProofs);
945
+ const resolvedResources = { ...deployment.resources };
946
+ for (const action of actions) {
947
+ const resourceKey = RESOURCE_NAME_KEY_BY_ACTION[action.key];
948
+ if (resourceKey && action.name !== resolvedResources[resourceKey]) resolvedResources[resourceKey] = action.name;
949
+ }
950
+ const resolvedDeployment = createPlatformDeploymentManifest({
951
+ ...deployment,
952
+ resources: resolvedResources
953
+ });
954
+ return {
955
+ account,
956
+ actions,
957
+ apiUrl: resolvedDeployment.controlPlane.apiHostname ? `https://${deployment.controlPlane.apiHostname}` : `https://${resolvedDeployment.resources.apiWorker}.${workersSubdomain}.workers.dev`,
958
+ deployment: resolvedDeployment,
959
+ installationId,
960
+ proxyUrl: resolvedDeployment.controlPlane.proxyHostname ? `https://${deployment.controlPlane.proxyHostname}` : `https://${resolvedDeployment.resources.proxyWorker}.${workersSubdomain}.workers.dev`,
961
+ workersSubdomain,
962
+ zone
963
+ };
964
+ }
965
+ async function revalidatePlatformInstallAction(client, original, manifest, action, options) {
966
+ const deployment = original.deployment;
967
+ const current = (await createPlatformInstallPlan(client, {
968
+ accountId: original.account.id,
969
+ applicationDomain: deployment.application.baseDomain,
970
+ controlPlaneDomain: deployment.controlPlane.apiHostname ?? "",
971
+ dedicatedZone: deployment.application.dedicatedZone,
972
+ displayName: deployment.displayName,
973
+ installationSlug: deployment.installationSlug,
974
+ zoneName: deployment.application.zoneName
975
+ }, manifest, options)).actions.find((entry) => entry.key === action.key);
976
+ 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.`);
977
+ if (action.kind === "worker" && action.action === "update") {
978
+ if (!matchesResourceMarker((await client.getWorkerPlainTextBindings(original.account.id, action.name)).PLATFORM_OWNERSHIP_MARKER, manifest, action.key, "worker", {
979
+ id: action.name,
980
+ name: action.name
981
+ })) throw new Error(`platform: Cloudflare Worker ${JSON.stringify(action.name)} no longer has this installation's ownership marker. Review a new plan before retrying.`);
982
+ }
983
+ }
984
+ function resourceSlugForManifest(manifest) {
985
+ const apiWorker = manifest.deployment.resources.apiWorker;
986
+ if (!apiWorker.startsWith("void-") || !apiWorker.endsWith("-api")) return manifest.deployment.installationSlug;
987
+ const resourceSlug = apiWorker.slice(5, -4);
988
+ const suffix = resourceSlug.slice(resourceSlug.lastIndexOf("-") + 1);
989
+ if (!/^(?:[a-f0-9]{12}|[a-f0-9]{32})$/.test(suffix)) return manifest.deployment.installationSlug;
990
+ return resourceSlug;
991
+ }
992
+ function addDnsPlan(actions, existing, name, records, options) {
993
+ const compatible = records.filter((record) => record.proxied);
994
+ if (records.some((record) => !record.proxied)) throw new Error(`platform: DNS name ${JSON.stringify(name)} contains an unproxied record. Proxy every record at this wildcard name through Cloudflare or choose another application domain.`);
995
+ const recorded = existing?.resources.find((resource) => resource.key === "application-dns");
996
+ const remote = recorded ? compatible.find((record) => record.id === recorded.id) : compatible.sort((left, right) => left.id.localeCompare(right.id))[0];
997
+ if (recorded) {
998
+ if (remote && recorded.id === remote.id && recorded.kind === "dns-record") {
999
+ actions.push({
1000
+ action: "reuse",
1001
+ id: remote.id,
1002
+ key: "application-dns",
1003
+ kind: "dns-record",
1004
+ name,
1005
+ reason: "Reuse the recorded wildcard application DNS record."
1006
+ });
1007
+ return;
1008
+ }
1009
+ if (!remote && options?.repairMissing && recorded.ownership === "created") {
1010
+ actions.push({
1011
+ action: "create",
1012
+ id: null,
1013
+ key: "application-dns",
1014
+ kind: "dns-record",
1015
+ name,
1016
+ reason: "Recreate the missing installer-owned wildcard application DNS record."
1017
+ });
1018
+ return;
1019
+ }
1020
+ throw new Error(`platform: Recorded DNS record ${JSON.stringify(recorded.name)} (${recorded.id}) no longer matches the Cloudflare zone.`);
1021
+ }
1022
+ actions.push({
1023
+ action: remote ? "adopt" : "create",
1024
+ id: remote?.id ?? null,
1025
+ key: "application-dns",
1026
+ kind: "dns-record",
1027
+ name,
1028
+ reason: remote ? "Use the existing compatible wildcard DNS record without taking ownership." : "Create an installer-owned proxied wildcard DNS record for application traffic."
1029
+ });
1030
+ }
1031
+ function resolveAccount(accounts, selector) {
1032
+ const matches = selector ? accounts.filter((account) => account.id === selector || account.name.toLowerCase() === selector.toLowerCase()) : accounts;
1033
+ 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>.");
1034
+ return matches[0];
1035
+ }
1036
+ function addNamedPlan(actions, existing, key, kind, name, matches, existingAction = "reuse", options, remoteOwnership, intentProofs = /* @__PURE__ */ new Set(), ambiguousResources = /* @__PURE__ */ new Set()) {
1037
+ if (matches.length > 1) throw new Error(`platform: More than one ${kind} named ${JSON.stringify(name)} exists.`);
1038
+ const remote = matches[0];
1039
+ const recorded = existing?.resources.find((resource) => resource.key === key);
1040
+ if (remote) {
1041
+ if (existing && ambiguousResources.has(key)) {
1042
+ if (!isLegacyUniqueProvisionAttempt(existing, key, kind, name)) throw new Error(`platform: Ownership of ${kind} ${JSON.stringify(name)} could not be verified. The resource was left untouched. Restore this installation's ownership record before resuming, or choose another installation name for a new platform.`);
1043
+ actions.push({
1044
+ action: "create",
1045
+ id: null,
1046
+ key,
1047
+ kind,
1048
+ name: interruptedProvisionRetryName(existing, key, kind, name),
1049
+ reason: "Retain the ambiguously owned resource and create a fresh installer-scoped replacement."
1050
+ });
1051
+ return;
1052
+ }
1053
+ const remotelyOwned = remoteOwnership?.resources.some((resource) => resource.key === key && resource.kind === kind && resource.id === remote.id && resource.name === name && resource.ownership === "created");
1054
+ const markedProvision = existing !== null && hasUnrecordedProvisionIntent(existing, key, kind, name) && intentProofs.has(key);
1055
+ const ambiguousProvision = existing !== null && options?.authoritativeProvisionIntents === true && isLegacyUniqueProvisionAttempt(existing, key, kind, name) && hasAmbiguousProvisionIntent(existing, key, kind, remote.id, name);
1056
+ if ((!recorded || recorded.id !== remote.id || recorded.kind !== kind) && !remotelyOwned && !markedProvision && !ambiguousProvision) throw new Error(`platform: ${kind} ${JSON.stringify(name)} already exists but is not owned by this installation. Resume the owning installation, or choose another installation name. Nothing was overwritten.`);
1057
+ if (ambiguousProvision && !markedProvision && !remotelyOwned) {
1058
+ actions.push({
1059
+ action: "create",
1060
+ id: null,
1061
+ key,
1062
+ kind,
1063
+ name: interruptedProvisionRetryName(existing, key, kind, name),
1064
+ reason: "Leave the ambiguous same-name resource untouched and create a fresh installer-scoped replacement."
1065
+ });
1066
+ return;
1067
+ }
1068
+ actions.push({
1069
+ action: existingAction,
1070
+ id: remote.id,
1071
+ key,
1072
+ kind,
1073
+ name,
1074
+ reason: markedProvision ? "Recover the resource with its matching ownership marker from an interrupted provisioning attempt." : existingAction === "update" ? "Deploy the current platform version." : "Reuse the recorded resource."
1075
+ });
1076
+ return;
1077
+ }
1078
+ if (recorded && recorded.state === "ready") {
1079
+ if (options?.repairMissing && recorded.ownership === "created") {
1080
+ actions.push({
1081
+ action: "create",
1082
+ id: null,
1083
+ key,
1084
+ kind,
1085
+ name,
1086
+ reason: `Recreate the missing installer-owned ${kind}.`
1087
+ });
1088
+ return;
1089
+ }
1090
+ if (options?.repairMissing && recorded.ownership !== "created") throw new Error(`platform: Recorded ${recorded.ownership} ${kind} ${JSON.stringify(name)} (${recorded.id}) is missing. Repair will not recreate a resource the installer does not own.`);
1091
+ throw new Error(`platform: Recorded ${kind} ${JSON.stringify(name)} (${recorded.id}) is missing. Run \`void platform repair ${existing.id}\` to reconcile it.`);
1092
+ }
1093
+ actions.push({
1094
+ action: "create",
1095
+ id: null,
1096
+ key,
1097
+ kind,
1098
+ name,
1099
+ reason: `Create an installer-owned ${kind}.`
1100
+ });
1101
+ }
1102
+ function hasUnrecordedProvisionIntent(manifest, key, kind, name) {
1103
+ 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));
1104
+ }
1105
+ function hasAmbiguousProvisionIntent(manifest, key, kind, remoteId, name) {
1106
+ const recorded = manifest.resources.find((resource) => resource.key === key);
1107
+ if (recorded?.id === remoteId && recorded.kind === kind && recorded.name === name) return false;
1108
+ return manifest.steps.some((step) => isProvisionIntentStep(step.key, key) && step.resultHash === platformProvisionIntentHash(manifest, key, kind, name, recorded?.id ?? null));
1109
+ }
1110
+ function isProvisionIntentStep(stepKey, resourceKey) {
1111
+ return stepKey === `provision-intent:${resourceKey}` || stepKey.startsWith(`provision-intent:${resourceKey}:`);
1112
+ }
1113
+ function interruptedProvisionRetryName(manifest, key, kind, name) {
1114
+ const suffix = `-retry-${createHash("sha256").update(JSON.stringify({
1115
+ key,
1116
+ kind,
1117
+ name,
1118
+ ownershipNonce: manifest.ownershipNonce
1119
+ })).digest("hex").slice(0, 32)}`;
1120
+ return `${name.slice(0, 63 - suffix.length).replace(/-+$/, "")}${suffix}`;
1121
+ }
1122
+ function matchesRecordedR2Fingerprint(configHash, resource) {
1123
+ if (!resource.fingerprint) return false;
1124
+ const liveHash = createHash("sha256").update(JSON.stringify({
1125
+ fingerprint: resource.fingerprint,
1126
+ kind: "r2-bucket",
1127
+ name: resource.name
1128
+ })).digest("hex");
1129
+ const legacyHash = createHash("sha256").update(JSON.stringify({
1130
+ kind: "r2-bucket",
1131
+ name: resource.name
1132
+ })).digest("hex");
1133
+ return configHash === liveHash || configHash === legacyHash;
1134
+ }
1135
+ function matchesResourceMarker(value, manifest, key, kind, resource) {
1136
+ if (!value) return false;
1137
+ try {
1138
+ const actual = parsePlatformResourceOwnershipMarker(JSON.parse(value));
1139
+ const expected = createPlatformResourceOwnershipMarker({
1140
+ accountId: manifest.account.id,
1141
+ installationId: manifest.id,
1142
+ ownershipNonce: manifest.ownershipNonce,
1143
+ resourceId: resource.id,
1144
+ resourceKey: key,
1145
+ resourceKind: kind,
1146
+ resourceName: resource.name
1147
+ });
1148
+ return JSON.stringify(actual) === JSON.stringify(expected);
1149
+ } catch {
1150
+ return false;
1151
+ }
1152
+ }
1153
+ function isLegacyUniqueProvisionAttempt(manifest, key, kind, name) {
1154
+ const resourceSlug = resourceSlugForManifest(manifest);
1155
+ if (resourceSlug === manifest.deployment.installationSlug) return false;
1156
+ const expectedNames = createPlatformResourceNames(resourceSlug);
1157
+ const resourceKey = RESOURCE_NAME_KEY_BY_ACTION[key];
1158
+ if (!resourceKey) return false;
1159
+ const expectedName = expectedNames[resourceKey];
1160
+ const uniquePrefix = expectedName.slice(0, expectedName.lastIndexOf("-") + 1);
1161
+ return manifest.deployment.resources[resourceKey] === name && (name === expectedName || name.startsWith(uniquePrefix)) && kind !== "zone" && kind !== "dns-record";
1162
+ }
1163
+ const RESOURCE_NAME_KEY_BY_ACTION = {
1164
+ "ai-gateway": "aiGateway",
1165
+ "api-worker": "apiWorker",
1166
+ "assets-bucket": "assetsBucket",
1167
+ "cron-queue": "cronQueue",
1168
+ "dispatch-namespace": "dispatchNamespace",
1169
+ "dispatch-worker": "dispatchWorker",
1170
+ hyperdrive: "hyperdrive",
1171
+ "isr-cache-kv": "isrCacheKv",
1172
+ "legacy-assets-kv": "legacyAssetsKv",
1173
+ "proxy-worker": "proxyWorker",
1174
+ "routing-kv": "routingKv",
1175
+ "tail-worker": "tailWorker",
1176
+ "usage-queue": "usageQueue"
1177
+ };
1178
+ function platformProvisionIntentHash(manifest, key, kind, name, previousResourceId = null) {
1179
+ return createHash("sha256").update(JSON.stringify({
1180
+ key,
1181
+ kind,
1182
+ name,
1183
+ ownershipNonce: manifest.ownershipNonce,
1184
+ ...previousResourceId ? { previousResourceId } : {}
1185
+ })).digest("hex");
1186
+ }
1187
+ function platformAdoptionIntentHash(manifest, key, kind, id, name) {
1188
+ return createHash("sha256").update(JSON.stringify({
1189
+ id,
1190
+ key,
1191
+ kind,
1192
+ name,
1193
+ ownershipNonce: manifest.ownershipNonce
1194
+ })).digest("hex");
1195
+ }
1196
+ async function readRemoteOwnership(client, accountId, routingNamespaces, existing) {
1197
+ for (const namespace of routingNamespaces) {
1198
+ const value = await client.getKvValue(accountId, namespace.id, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY);
1199
+ if (value === null) continue;
1200
+ let remote;
1201
+ try {
1202
+ remote = parsePlatformInstallationManifest(JSON.parse(value));
1203
+ } catch {
1204
+ throw new Error(`platform: Routing namespace ${JSON.stringify(namespace.name)} contains invalid ownership metadata.`);
1205
+ }
1206
+ const routing = remote.resources.find((resource) => resource.key === "routing-kv");
1207
+ if (remote.id === existing.id && remote.ownershipNonce === existing.ownershipNonce && remote.account.id === accountId && routing?.id === namespace.id && routing.name === namespace.name && routing.ownership === "created") return remote;
1208
+ }
1209
+ return null;
1210
+ }
1211
+ function formatPlatformInstallPlan(plan) {
1212
+ const lines = [
1213
+ `${import_picocolors$1.default.bold(`${plan.deployment.displayName} in ${plan.account.name}`)} (${plan.account.id})`,
1214
+ plan.deployment.application.zoneName ? `${import_picocolors$1.default.bold("Applications:")} *.${plan.deployment.application.baseDomain}` : `${import_picocolors$1.default.bold("Applications:")} workers.dev (testing); add a domain with void platform domain set <domain>`,
1215
+ `${import_picocolors$1.default.bold("Control plane:")} ${plan.apiUrl}`,
1216
+ `${import_picocolors$1.default.bold("Resource proxy:")} ${plan.proxyUrl}`,
1217
+ ""
1218
+ ];
1219
+ for (const action of plan.actions) lines.push(`${action.action.padEnd(6)} ${action.kind.padEnd(19)} ${action.name}`);
1220
+ return lines.join("\n");
1221
+ }
1222
+ //#endregion
1223
+ //#region src/cli/platform-secret-update-authority.ts
1224
+ const COMPLETE_WORKER_SECRET_UPDATE_NAMES = [
1225
+ "CF_API_TOKEN",
1226
+ "GITHUB_CLIENT_ID",
1227
+ "GITHUB_CLIENT_SECRET",
1228
+ "R2_ACCESS_KEY_ID",
1229
+ "R2_SECRET_ACCESS_KEY"
1230
+ ];
1231
+ /**
1232
+ * Prevents a host without the encrypted values for an interrupted credential
1233
+ * update from carrying an opaque, possibly partial update into another cutover.
1234
+ */
1235
+ function assertPlatformWorkerSecretUpdateAuthority(input) {
1236
+ const unresolved = unresolvedWorkerSecretUpdate(input.manifest);
1237
+ if (!unresolved) return;
1238
+ const latestWorkerGeneration = readLatestWorkerGeneration(input.manifest);
1239
+ if (unresolved.generation === latestWorkerGeneration && input.localIntent?.id === unresolved.id && (input.localIntent.generation === null || input.localIntent.generation === unresolved.generation) || hasCompleteWorkerSecretReplacement(input.explicitUpdates)) return;
1240
+ throw new Error("platform: An unresolved Worker credential update is owned by another encrypted recovery checkpoint. Resume on the host that owns that checkpoint, restore its encrypted pending recovery material, or provide all supported Worker credential values together as a complete replacement.");
1241
+ }
1242
+ function unresolvedWorkerSecretUpdate(manifest) {
1243
+ const markers = manifest.steps.flatMap((step) => {
1244
+ const match = /^worker-secret-updates:(\d+):(pending|committed)$/.exec(step.key);
1245
+ return match ? [{
1246
+ generation: Number(match[1]),
1247
+ id: step.resultHash,
1248
+ state: match[2]
1249
+ }] : [];
1250
+ });
1251
+ const latestGeneration = Math.max(0, ...markers.map((marker) => marker.generation));
1252
+ const latest = markers.filter((marker) => marker.generation === latestGeneration);
1253
+ const committed = new Set(latest.filter((marker) => marker.state === "committed").map((marker) => marker.id));
1254
+ const pending = latest.filter((marker) => marker.state === "pending" && !committed.has(marker.id));
1255
+ if (pending.length === 0) return null;
1256
+ return {
1257
+ generation: latestGeneration,
1258
+ id: pending[0].id
1259
+ };
1260
+ }
1261
+ function readLatestWorkerGeneration(manifest) {
1262
+ return Math.max(0, ...manifest.steps.flatMap((step) => {
1263
+ const match = /^worker-(?:cutover|secret-updates|traffic-state):(\d+)/.exec(step.key);
1264
+ return match ? [Number(match[1])] : [];
1265
+ }));
1266
+ }
1267
+ function hasCompleteWorkerSecretReplacement(updates) {
1268
+ return COMPLETE_WORKER_SECRET_UPDATE_NAMES.every((name) => typeof updates[name] === "string" && updates[name] !== "");
1269
+ }
1270
+ //#endregion
1271
+ //#region src/cli/platform-traffic.ts
1272
+ function platformManagedWorkerTraffic(deployment, workers) {
1273
+ const includes = (worker) => workers === void 0 || workers.has(worker);
1274
+ const names = deployment.resources;
1275
+ const customDomains = [];
1276
+ if (deployment.controlPlane.apiHostname && includes(names.apiWorker)) customDomains.push({
1277
+ hostname: deployment.controlPlane.apiHostname,
1278
+ service: names.apiWorker
1279
+ });
1280
+ if (deployment.controlPlane.proxyHostname && includes(names.proxyWorker)) customDomains.push({
1281
+ hostname: deployment.controlPlane.proxyHostname,
1282
+ service: names.proxyWorker
1283
+ });
1284
+ return {
1285
+ customDomains,
1286
+ routes: includes(names.dispatchWorker) ? createApplicationRoutes(deployment.application).map((route) => ({
1287
+ pattern: route.pattern,
1288
+ script: names.dispatchWorker
1289
+ })) : []
1290
+ };
1291
+ }
1292
+ function trafficConfigHash(kind, value) {
1293
+ return createHash("sha256").update(JSON.stringify({
1294
+ kind,
1295
+ value
1296
+ })).digest("hex");
1297
+ }
1298
+ function trackedPlatformWorkerTraffic(_manifest, _workers) {
1299
+ return {
1300
+ customDomains: [],
1301
+ routes: []
1302
+ };
1303
+ }
1304
+ function recordPlatformWorkerTraffic(manifest, workerName, inventory, updatedAt) {
1305
+ const desired = platformManagedWorkerTraffic(manifest.deployment, /* @__PURE__ */ new Set([workerName]));
1306
+ let next = manifest;
1307
+ for (const target of desired.routes) {
1308
+ const matches = inventory.routes.filter((route) => route.pattern === target.pattern && route.script === target.script);
1309
+ if (matches.length !== 1) throw new Error(`platform: Expected exactly one published Worker route for ${JSON.stringify(target.pattern)} and ${JSON.stringify(target.script)}.`);
1310
+ const route = matches[0];
1311
+ next = recordInstallationResource(next, {
1312
+ configHash: trafficConfigHash("worker-route", target),
1313
+ id: route.id,
1314
+ key: `worker-route:${route.id}`,
1315
+ kind: "worker-route",
1316
+ name: route.pattern,
1317
+ ownership: "external",
1318
+ state: "retained"
1319
+ }, updatedAt);
1320
+ }
1321
+ for (const target of desired.customDomains) {
1322
+ const matches = inventory.customDomains.filter((domain) => domain.hostname === target.hostname && domain.service === target.service);
1323
+ if (matches.length !== 1) throw new Error(`platform: Expected exactly one published Worker custom domain for ${JSON.stringify(target.hostname)} and ${JSON.stringify(target.service)}.`);
1324
+ const domain = matches[0];
1325
+ next = recordInstallationResource(next, {
1326
+ configHash: trafficConfigHash("worker-custom-domain", target),
1327
+ id: domain.id,
1328
+ key: `worker-custom-domain:${domain.id}`,
1329
+ kind: "worker-custom-domain",
1330
+ name: domain.hostname,
1331
+ ownership: "external",
1332
+ state: "retained"
1333
+ }, updatedAt);
1334
+ }
1335
+ return next;
1336
+ }
1337
+ //#endregion
1338
+ //#region src/cli/platform-installer.ts
1339
+ const REMOTE_INSTALLATION_MANIFEST_KEY = PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY;
1340
+ const PLATFORM_WORKER_SECRET_UPDATE_NAMES = [
1341
+ "CF_API_TOKEN",
1342
+ "GITHUB_CLIENT_ID",
1343
+ "GITHUB_CLIENT_SECRET",
1344
+ "R2_ACCESS_KEY_ID",
1345
+ "R2_SECRET_ACCESS_KEY"
1346
+ ];
1347
+ var PlatformWorkerRecoveryIncompleteError = class extends Error {
1348
+ constructor(message, options) {
1349
+ super(message, options);
1350
+ this.name = "PlatformWorkerRecoveryIncompleteError";
1351
+ }
1352
+ };
1353
+ function loadPlatformRuntime(customRuntimeDirectory) {
1354
+ const runtime = resolvePlatformRuntime(customRuntimeDirectory || void 0);
1355
+ verifyRuntimeArtifacts(runtime.directory, runtime.manifest);
1356
+ return runtime;
1357
+ }
1358
+ function createPlatformInstallationSecrets(input) {
1359
+ const checkpoint = input.checkpoint ?? {};
1360
+ const required = (value, name) => {
1361
+ if (!value) throw new Error(`platform: ${name} is required to install the platform.`);
1362
+ return value;
1363
+ };
1364
+ const existingInstallation = input.existingInstallation === true;
1365
+ const restoredIdentity = (explicit, name) => existingInstallation ? checkpoint[name] || explicit || "" : explicit || checkpoint[name] || "";
1366
+ const configured = (explicit, name) => explicit || checkpoint[name] || "";
1367
+ const jwtSecret = restoredIdentity(input.jwtSecret || "", "jwtSecret");
1368
+ const resolvedJwtSecret = input.existingInstallation ? jwtSecret : required(jwtSecret, "VOID_PLATFORM_JWT_SECRET");
1369
+ if (!existingInstallation && Buffer.byteLength(resolvedJwtSecret, "utf8") < 32) throw new Error("platform: VOID_PLATFORM_JWT_SECRET must contain at least 32 bytes of secret material unique to this installation.");
1370
+ const projectSecretKeyring = resolveProjectSecretKeyring({
1371
+ checkpoint,
1372
+ existingInstallation,
1373
+ projectSecretActiveKeyVersion: input.projectSecretActiveKeyVersion || "",
1374
+ projectSecretKey: input.projectSecretKey || "",
1375
+ projectSecretKeysJson: input.projectSecretKeysJson || "",
1376
+ required
1377
+ });
1378
+ const pendingWorkerSecretUpdateIntent = parseWorkerSecretUpdateIntent(checkpoint.workerSecretUpdatesJson);
1379
+ const explicitWorkerSecretUpdates = withoutEmptySecrets({
1380
+ CF_API_TOKEN: input.cloudflareApiToken,
1381
+ GITHUB_CLIENT_ID: input.githubClientId,
1382
+ GITHUB_CLIENT_SECRET: input.githubClientSecret,
1383
+ R2_ACCESS_KEY_ID: input.r2AccessKeyId,
1384
+ R2_SECRET_ACCESS_KEY: input.r2SecretAccessKey
1385
+ });
1386
+ return {
1387
+ workerSecretUpdates: input.updateExistingWorkers ? explicitWorkerSecretUpdates : {},
1388
+ pendingWorkerSecretUpdateIntent: input.updateExistingWorkers ? pendingWorkerSecretUpdateIntent : void 0,
1389
+ bootstrapGitHubId: existingInstallation ? configured(input.bootstrapGitHubId, "bootstrapGitHubId") : required(configured(input.bootstrapGitHubId, "bootstrapGitHubId"), "resolved administrator GitHub account id"),
1390
+ bootstrapGitHubLogin: existingInstallation ? configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin") : required(configured(input.bootstrapGitHubLogin, "bootstrapGitHubLogin"), "VOID_PLATFORM_ADMIN_GITHUB_LOGIN"),
1391
+ cloudflareApiToken: existingInstallation ? configured(input.cloudflareApiToken, "cloudflareApiToken") : required(configured(input.cloudflareApiToken, "cloudflareApiToken"), "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN"),
1392
+ databaseUrl: required(input.databaseUrl || checkpoint.databaseUrl || "", "DATABASE_URL"),
1393
+ githubClientId: existingInstallation ? configured(input.githubClientId, "githubClientId") : required(configured(input.githubClientId, "githubClientId"), "VOID_PLATFORM_GITHUB_CLIENT_ID"),
1394
+ githubClientSecret: existingInstallation ? configured(input.githubClientSecret, "githubClientSecret") : required(configured(input.githubClientSecret, "githubClientSecret"), "VOID_PLATFORM_GITHUB_CLIENT_SECRET"),
1395
+ jwtSecret: resolvedJwtSecret,
1396
+ projectSecretActiveKeyVersion: projectSecretKeyring.activeVersion,
1397
+ projectSecretKey: projectSecretKeyring.v1Key,
1398
+ projectSecretKeysJson: projectSecretKeyring.keysJson,
1399
+ r2AccessKeyId: existingInstallation ? configured(input.r2AccessKeyId, "r2AccessKeyId") : required(configured(input.r2AccessKeyId, "r2AccessKeyId"), "VOID_PLATFORM_R2_ACCESS_KEY_ID"),
1400
+ r2SecretAccessKey: existingInstallation ? configured(input.r2SecretAccessKey, "r2SecretAccessKey") : required(configured(input.r2SecretAccessKey, "r2SecretAccessKey"), "VOID_PLATFORM_R2_SECRET_ACCESS_KEY")
1401
+ };
1402
+ }
1403
+ function resolveProjectSecretKeyring(input) {
1404
+ const explicitFullKeyring = input.projectSecretKeysJson !== "" || input.projectSecretActiveKeyVersion !== "";
1405
+ if (explicitFullKeyring && (!input.projectSecretKeysJson || !input.projectSecretActiveKeyVersion)) throw new Error("platform: VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON and VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION must be supplied together.");
1406
+ if (explicitFullKeyring && !input.existingInstallation) throw new Error("platform: Fresh installations use VOID_PLATFORM_PROJECT_SECRET_KEY to initialize key version v1. The full project-secret keyring inputs are for restoring an existing installation.");
1407
+ const checkpointKeysJson = input.checkpoint.projectSecretKeysJson || "";
1408
+ const checkpointActiveVersion = input.checkpoint.projectSecretActiveKeyVersion || "";
1409
+ if (!explicitFullKeyring && (checkpointKeysJson !== "" || checkpointActiveVersion !== "") && (!checkpointKeysJson || !checkpointActiveVersion)) throw new Error("platform: Encrypted recovery material must contain both the project-secret keyring and its active version.");
1410
+ if (explicitFullKeyring) return normalizeProjectSecretKeyring(input.projectSecretKeysJson, input.projectSecretActiveKeyVersion, "VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON", "VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION");
1411
+ if (checkpointKeysJson) return normalizeProjectSecretKeyring(checkpointKeysJson, checkpointActiveVersion, "encrypted recovery project-secret keyring", "encrypted recovery project-secret active version");
1412
+ const v1Key = input.existingInstallation ? input.checkpoint.projectSecretKey || input.projectSecretKey || "" : input.required(input.projectSecretKey || input.checkpoint.projectSecretKey || "", "VOID_PLATFORM_PROJECT_SECRET_KEY");
1413
+ if (!v1Key) return {
1414
+ activeVersion: "",
1415
+ keysJson: "",
1416
+ v1Key: ""
1417
+ };
1418
+ assertCanonicalProjectSecretKey(v1Key, "VOID_PLATFORM_PROJECT_SECRET_KEY", true);
1419
+ return {
1420
+ activeVersion: "v1",
1421
+ keysJson: JSON.stringify({ v1: v1Key }),
1422
+ v1Key
1423
+ };
1424
+ }
1425
+ function normalizeProjectSecretKeyring(keysJson, activeVersion, keysName, activeVersionName) {
1426
+ let parsed;
1427
+ try {
1428
+ parsed = JSON.parse(keysJson);
1429
+ } catch {
1430
+ throw new Error(`platform: ${keysName} must be valid JSON.`);
1431
+ }
1432
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`platform: ${keysName} must be a JSON object.`);
1433
+ const entries = Object.entries(parsed);
1434
+ if (entries.length === 0) throw new Error(`platform: ${keysName} must define at least one key.`);
1435
+ for (const [version, key] of entries) {
1436
+ if (!version || typeof key !== "string" || !key) throw new Error(`platform: ${keysName} must map non-empty versions to keys.`);
1437
+ assertCanonicalProjectSecretKey(key, `${keysName} entry ${JSON.stringify(version)}`);
1438
+ }
1439
+ if (!Object.hasOwn(parsed, activeVersion)) throw new Error(`platform: ${activeVersionName} must identify a key in ${keysName}.`);
1440
+ const keys = Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
1441
+ return {
1442
+ activeVersion,
1443
+ keysJson: JSON.stringify(keys),
1444
+ v1Key: keys.v1 || ""
1445
+ };
1446
+ }
1447
+ function assertCanonicalProjectSecretKey(value, name, uniqueToInstallation = false) {
1448
+ const decoded = Buffer.from(value, "base64");
1449
+ if (decoded.byteLength !== 32 || decoded.toString("base64") !== value) throw new Error(`platform: ${name} must be the canonical base64 encoding of a 32-byte key${uniqueToInstallation ? " unique to this installation" : ""}.`);
1450
+ }
1451
+ function secretCheckpoint(secrets, workerSecretUpdateIntent) {
1452
+ return {
1453
+ bootstrapGitHubId: secrets.bootstrapGitHubId,
1454
+ bootstrapGitHubLogin: secrets.bootstrapGitHubLogin,
1455
+ cloudflareApiToken: secrets.cloudflareApiToken,
1456
+ databaseUrl: secrets.databaseUrl,
1457
+ githubClientId: secrets.githubClientId,
1458
+ githubClientSecret: secrets.githubClientSecret,
1459
+ jwtSecret: secrets.jwtSecret,
1460
+ projectSecretActiveKeyVersion: secrets.projectSecretActiveKeyVersion,
1461
+ projectSecretKey: secrets.projectSecretKey,
1462
+ projectSecretKeysJson: secrets.projectSecretKeysJson,
1463
+ r2AccessKeyId: secrets.r2AccessKeyId,
1464
+ r2SecretAccessKey: secrets.r2SecretAccessKey,
1465
+ ...workerSecretUpdateIntent ? { workerSecretUpdatesJson: JSON.stringify(workerSecretUpdateIntent) } : {}
1466
+ };
1467
+ }
1468
+ function parseWorkerSecretUpdateIntent(value) {
1469
+ if (!value) return;
1470
+ let parsed;
1471
+ try {
1472
+ parsed = JSON.parse(value);
1473
+ } catch {
1474
+ throw new Error("platform: Encrypted pending Worker secret updates must be valid JSON.");
1475
+ }
1476
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("schema" in parsed) || parsed.schema !== 1 || !("id" in parsed) || typeof parsed.id !== "string" || parsed.id === "" || !("baselineGeneration" in parsed) || !Number.isSafeInteger(parsed.baselineGeneration) || Number(parsed.baselineGeneration) < 0 || !("generation" in parsed) || parsed.generation !== null && (!Number.isSafeInteger(parsed.generation) || Number(parsed.generation) <= 0) || !("updates" in parsed) || !parsed.updates || typeof parsed.updates !== "object" || Array.isArray(parsed.updates)) throw new Error("platform: Encrypted pending Worker secret update intent is invalid.");
1477
+ const updates = {};
1478
+ for (const [name, entry] of Object.entries(parsed.updates)) {
1479
+ if (!PLATFORM_WORKER_SECRET_UPDATE_NAMES.includes(name) || typeof entry !== "string" || entry === "") throw new Error("platform: Encrypted pending Worker secret updates are invalid.");
1480
+ updates[name] = entry;
1481
+ }
1482
+ if (Object.keys(updates).length === 0) throw new Error("platform: Encrypted pending Worker secret update intent is empty.");
1483
+ return {
1484
+ baselineGeneration: Number(parsed.baselineGeneration),
1485
+ generation: parsed.generation === null ? null : Number(parsed.generation),
1486
+ id: parsed.id,
1487
+ schema: 1,
1488
+ updates
1489
+ };
1490
+ }
1491
+ function sanitizeWorkerSecretUpdates(updates) {
1492
+ return Object.fromEntries(Object.entries(updates ?? {}).filter(([name, value]) => PLATFORM_WORKER_SECRET_UPDATE_NAMES.includes(name) && typeof value === "string" && value !== ""));
1493
+ }
1494
+ async function installPlatform(plan, secrets, existing, dependencies) {
1495
+ 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.");
1496
+ const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1497
+ const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl ?? resolvePinnedApiBaseUrl();
1498
+ const runPinnedPlatformWrangler = (args, accountId, cwd, capture) => runPlatformWranglerAtApiBase(args, accountId, cwd, capture, cloudflareApiBaseUrl);
1499
+ const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
1500
+ const runtimeDirectory = resolvedRuntime.directory;
1501
+ const runtime = resolvedRuntime.manifest;
1502
+ if (plan.deployment.application.workersDev && runtime.applicationRoutingVersion !== 1) throw new Error("platform: This runtime does not support workers.dev application routing. Use a current platform runtime.");
1503
+ verifyRuntimeArtifacts(runtimeDirectory, runtime);
1504
+ const runtimeProvenance = {
1505
+ manifestSha256: resolvedRuntime.manifestSha256,
1506
+ source: resolvedRuntime.source,
1507
+ sourceRevision: runtime.sourceRevision
1508
+ };
1509
+ const version = runtime.platformVersion;
1510
+ const targetUsageProtocolVersion = runtime.usageProtocolVersion;
1511
+ const sourcePlatformVersion = existing?.platformVersion ?? null;
1512
+ const hadPriorWorkers = existing !== null && (existing.resources.some((resource) => resource.kind === "worker" && resource.state === "ready") || plan.actions.some((action) => action.kind === "worker" && action.action !== "create"));
1513
+ const wasDisabled = existing?.status === "disabled";
1514
+ const preserveDisabled = existing?.desiredState === "disabled";
1515
+ const previousPublicEndpoints = stablePublicEndpointState(existing);
1516
+ let workerCutoverGeneration = 0;
1517
+ let workerRollbacks = [];
1518
+ let workerDeploymentCommitted = false;
1519
+ let workerTrafficGuarded = false;
1520
+ let manifest = existing;
1521
+ if (!manifest) {
1522
+ const createdAt = now();
1523
+ manifest = createPlatformInstallationManifest({
1524
+ account: plan.account,
1525
+ createdAt,
1526
+ deployment: plan.deployment,
1527
+ id: plan.installationId,
1528
+ ownershipNonce: randomBytes(32).toString("base64url"),
1529
+ platformVersion: version
1530
+ });
1531
+ manifest = transitionInstallation(manifest, "planned", now());
1532
+ writeInstallationManifest(manifest);
1533
+ } else if (manifest.status === "uninstalled") throw new Error(`platform: Installation ${manifest.id} has already been uninstalled.`);
1534
+ const explicitWorkerSecretUpdates = sanitizeWorkerSecretUpdates(secrets.workerSecretUpdates);
1535
+ const hasExplicitWorkerSecretUpdates = Object.keys(explicitWorkerSecretUpdates).length > 0;
1536
+ const resumedWorkerSecretUpdateIntent = secrets.pendingWorkerSecretUpdateIntent && platformWorkerSecretUpdateIntentState(manifest, secrets.pendingWorkerSecretUpdateIntent) === "active" ? secrets.pendingWorkerSecretUpdateIntent : null;
1537
+ let workerSecretUpdateIntent = hasExplicitWorkerSecretUpdates ? {
1538
+ baselineGeneration: latestPlatformWorkerCutoverGeneration(manifest),
1539
+ generation: null,
1540
+ id: randomUUID(),
1541
+ schema: 1,
1542
+ updates: {
1543
+ ...resumedWorkerSecretUpdateIntent?.updates,
1544
+ ...explicitWorkerSecretUpdates
1545
+ }
1546
+ } : resumedWorkerSecretUpdateIntent;
1547
+ if (preserveDisabled && manifest.desiredState !== "disabled") {
1548
+ manifest = setInstallationDesiredState(manifest, "disabled", now());
1549
+ writeInstallationManifest(manifest);
1550
+ }
1551
+ assertRequiredPlatformSecrets(plan, manifest, secrets);
1552
+ const pendingSecrets = secretCheckpoint(secrets, workerSecretUpdateIntent);
1553
+ writePendingInstallationSecrets(manifest, {
1554
+ ...pendingSecrets,
1555
+ databaseClaimState: "claiming"
1556
+ });
1557
+ dependencies.onProgress?.("Preparing the platform database");
1558
+ try {
1559
+ await (dependencies.claimDatabase ?? claimPlatformDatabase)(secrets.databaseUrl, manifest.id, manifest.ownershipNonce);
1560
+ } catch (error) {
1561
+ writePendingInstallationSecrets(manifest, {
1562
+ ...pendingSecrets,
1563
+ databaseClaimState: "failed"
1564
+ });
1565
+ throw error;
1566
+ }
1567
+ writePendingInstallationSecrets(manifest, {
1568
+ ...pendingSecrets,
1569
+ databaseClaimState: "claimed"
1570
+ });
1571
+ dependencies.onProgress?.("Acquiring the database lock");
1572
+ const releaseDatabaseLock = await dependencies.acquireDatabaseLifecycleLock(secrets.databaseUrl, manifest.id, manifest.ownershipNonce);
1573
+ const writeRemoteInstallationCheckpoint = (client, checkpointPlan, checkpointManifest) => writeRemoteInstallationCheckpointWithoutDatabase(client, checkpointPlan, checkpointManifest, releaseDatabaseLock);
1574
+ const authoritative = releaseDatabaseLock.authoritativeManifest;
1575
+ if (authoritative && JSON.stringify(authoritative) !== JSON.stringify(manifest)) {
1576
+ writeInstallationManifest(authoritative);
1577
+ await releaseDatabaseLock();
1578
+ throw new Error("platform: This installation changed on another host. Local state was refreshed; rerun the command to review a current plan.");
1579
+ }
1580
+ try {
1581
+ assertPlatformWorkerSecretUpdateAuthority({
1582
+ explicitUpdates: explicitWorkerSecretUpdates,
1583
+ localIntent: secrets.pendingWorkerSecretUpdateIntent,
1584
+ manifest
1585
+ });
1586
+ } catch (error) {
1587
+ await releaseDatabaseLock();
1588
+ throw error;
1589
+ }
1590
+ if (workerSecretUpdateIntent && !hasExplicitWorkerSecretUpdates && platformWorkerSecretUpdateIntentState(manifest, workerSecretUpdateIntent) !== "active") workerSecretUpdateIntent = null;
1591
+ const workerDeploymentSecrets = {
1592
+ ...secrets,
1593
+ workerSecretUpdates: workerSecretUpdateIntent?.updates ?? {}
1594
+ };
1595
+ try {
1596
+ assertPlatformRuntimeSafety(runtime, manifest.safetyProtocols, releaseDatabaseLock.requiredSafetyProtocols);
1597
+ if (existing) await dependencies.verifyExistingOwnership?.(manifest, authoritative !== null && authoritative !== void 0);
1598
+ dependencies.onProgress?.("Checking and creating Cloudflare resources");
1599
+ if (JSON.stringify(manifest.deployment) !== JSON.stringify(plan.deployment)) {
1600
+ manifest = {
1601
+ ...manifest,
1602
+ deployment: plan.deployment,
1603
+ updatedAt: now()
1604
+ };
1605
+ writeInstallationManifest(manifest);
1606
+ await releaseDatabaseLock.checkpoint?.(manifest);
1607
+ }
1608
+ await releaseDatabaseLock.checkpoint?.(manifest);
1609
+ if (manifest.status !== "provisioning") {
1610
+ manifest = transitionInstallation(manifest, "provisioning", now());
1611
+ writeInstallationManifest(manifest);
1612
+ }
1613
+ const routingAction = plan.actions.find((action) => action.key === "routing-kv");
1614
+ if (routingAction.action === "adopt") throw new Error("platform: The routing checkpoint cannot be adopted.");
1615
+ if (routingAction.action === "reuse") {
1616
+ if (!manifest.resources.some((resource) => resource.key === routingAction.key) && routingAction.id) {
1617
+ manifest = recordResource(manifest, routingAction, routingAction.id, "created", now());
1618
+ writeInstallationManifest(manifest);
1619
+ }
1620
+ } else {
1621
+ manifest = recordProvisionIntent(manifest, routingAction, now());
1622
+ writeInstallationManifest(manifest);
1623
+ await releaseDatabaseLock.checkpoint?.(manifest);
1624
+ const routing = await createResource(dependencies.client, plan, routingAction, secrets.databaseUrl, "");
1625
+ manifest = recordResource(manifest, routingAction, routing.id, "created", now());
1626
+ writeInstallationManifest(manifest);
1627
+ }
1628
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1629
+ const zoneAction = plan.actions.find((action) => action.key === "zone");
1630
+ let zone = plan.zone;
1631
+ if (zoneAction) {
1632
+ if (zoneAction.action === "create") {
1633
+ manifest = recordProvisionIntent(manifest, zoneAction, now());
1634
+ writeInstallationManifest(manifest);
1635
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1636
+ zone = await dependencies.client.createZone(plan.account.id, zoneAction.name);
1637
+ }
1638
+ if (!zone) throw new Error("platform: Cloudflare did not return the platform zone.");
1639
+ if (zoneAction.action === "adopt") {
1640
+ manifest = recordAdoptionIntent(manifest, zoneAction, zone.id, now());
1641
+ writeInstallationManifest(manifest);
1642
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1643
+ }
1644
+ manifest = recordResource(manifest, zoneAction, zone.id, zoneAction.action === "adopt" ? "adopted" : zoneAction.action === "create" || hasProvisionIntent(manifest, zoneAction) ? "created" : void 0, now());
1645
+ writeInstallationManifest(manifest);
1646
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1647
+ if (zone.status !== "active") {
1648
+ manifest = transitionInstallation(manifest, "waiting-for-dns", now());
1649
+ writeInstallationManifest(manifest);
1650
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1651
+ return {
1652
+ manifest,
1653
+ waitingForDns: true,
1654
+ waitingForSandboxDrain: false,
1655
+ zoneNameServers: zone.nameServers
1656
+ };
1657
+ }
1658
+ }
1659
+ const zoneId = zone?.id ?? "";
1660
+ for (const action of plan.actions.filter((entry) => entry.key !== "routing-kv" && entry.kind !== "zone" && entry.kind !== "worker")) {
1661
+ const retainedManifest = retainReplacedInstallationResource(manifest, action, now());
1662
+ if (retainedManifest !== manifest) {
1663
+ manifest = retainedManifest;
1664
+ writeInstallationManifest(manifest);
1665
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1666
+ }
1667
+ if (action.action === "adopt") {
1668
+ if (!action.id) throw new Error(`platform: Adopted ${action.kind} ${action.name} has no id.`);
1669
+ manifest = recordAdoptionIntent(manifest, action, action.id, now());
1670
+ writeInstallationManifest(manifest);
1671
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1672
+ manifest = recordResource(manifest, action, action.id, "adopted", now(), secrets.databaseUrl);
1673
+ writeInstallationManifest(manifest);
1674
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1675
+ continue;
1676
+ }
1677
+ if (action.action === "reuse") {
1678
+ if (action.id && (action.kind === "hyperdrive" || !manifest.resources.some((resource) => resource.key === action.key))) {
1679
+ manifest = recordResource(manifest, action, action.id, void 0, now(), secrets.databaseUrl);
1680
+ writeInstallationManifest(manifest);
1681
+ await writeR2OwnershipMarkerIfNeeded(dependencies.client, plan, manifest, action);
1682
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1683
+ }
1684
+ continue;
1685
+ }
1686
+ manifest = recordProvisionIntent(manifest, action, now());
1687
+ writeInstallationManifest(manifest);
1688
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1689
+ const resource = await createResource(dependencies.client, plan, action, secrets.databaseUrl, zoneId);
1690
+ manifest = recordResource(manifest, action, resource.id, "created", now(), secrets.databaseUrl, resource.fingerprint);
1691
+ writeInstallationManifest(manifest);
1692
+ await writeR2OwnershipMarkerIfNeeded(dependencies.client, plan, manifest, action);
1693
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1694
+ }
1695
+ dependencies.onProgress?.("Checking the platform database schema");
1696
+ const rollbackDatabase = dependencies.databaseRollbackSource;
1697
+ const migrationStep = `database-schema:${runtime.database.schemaDigest}`;
1698
+ if (rollbackDatabase) {
1699
+ const verifyDatabase = releaseDatabaseLock.verifyDatabase;
1700
+ if (!verifyDatabase) throw new Error("platform: The PostgreSQL lifecycle lock cannot verify the forward-only rollback schema.");
1701
+ await verifyDatabase(rollbackDatabase.directory, rollbackDatabase.manifest.database);
1702
+ manifest = {
1703
+ ...manifest,
1704
+ databaseSchemaDigest: rollbackDatabase.manifest.database.schemaDigest,
1705
+ updatedAt: now()
1706
+ };
1707
+ writeInstallationManifest(manifest);
1708
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1709
+ } else if (!manifest.steps.some((step) => step.key === migrationStep)) {
1710
+ dependencies.onProgress?.("Applying platform database migrations");
1711
+ const legacyMigrationStep = `database-migrations:${runtime.database.journalSha256}`;
1712
+ if (dependencies.migrateDatabase) {
1713
+ if (!manifest.steps.some((step) => step.key === legacyMigrationStep)) await dependencies.migrateDatabase(secrets.databaseUrl, join(runtimeDirectory, "drizzle"));
1714
+ } else {
1715
+ const prepareDatabase = dependencies.prepareDatabase ?? releaseDatabaseLock.prepareDatabase;
1716
+ if (!prepareDatabase) throw new Error("platform: The PostgreSQL lifecycle lock cannot verify or migrate this database.");
1717
+ await prepareDatabase(runtimeDirectory, runtime.database, sourcePlatformVersion, hadPriorWorkers);
1718
+ }
1719
+ manifest = recordInstallationStep(manifest, {
1720
+ completedAt: now(),
1721
+ key: migrationStep,
1722
+ resultHash: runtime.database.schemaDigest
1723
+ }, now());
1724
+ manifest = {
1725
+ ...manifest,
1726
+ databaseSchemaDigest: runtime.database.schemaDigest,
1727
+ updatedAt: now()
1728
+ };
1729
+ writeInstallationManifest(manifest);
1730
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1731
+ } else if (releaseDatabaseLock.verifyDatabase) {
1732
+ await releaseDatabaseLock.verifyDatabase(runtimeDirectory, runtime.database);
1733
+ if (manifest.databaseSchemaDigest !== runtime.database.schemaDigest) {
1734
+ manifest = {
1735
+ ...manifest,
1736
+ databaseSchemaDigest: runtime.database.schemaDigest,
1737
+ updatedAt: now()
1738
+ };
1739
+ writeInstallationManifest(manifest);
1740
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1741
+ }
1742
+ }
1743
+ dependencies.onProgress?.("Checking platform safety");
1744
+ const targetSandboxDrainProtocol = runtime.safetyProtocols?.sandboxDrain ?? 0;
1745
+ const prepareSandboxDrain = dependencies.prepareSandboxDrain ?? releaseDatabaseLock.prepareSandboxDrain;
1746
+ if (targetSandboxDrainProtocol >= 1 && !prepareSandboxDrain) throw new Error("platform: The PostgreSQL lifecycle lock cannot establish the sandbox-drain safety checkpoint.");
1747
+ const sandboxDrainPreparation = targetSandboxDrainProtocol >= 1 ? await prepareSandboxDrain(targetSandboxDrainProtocol >= 2 ? 2 : 0) : null;
1748
+ const sandboxDrainRoutingKvId = requireId(Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id])), "routing-kv");
1749
+ const writeSandboxDrainMirror = async (preparation) => {
1750
+ await dependencies.client.putKvValue(plan.account.id, sandboxDrainRoutingKvId, PLATFORM_SANDBOX_DRAIN_KEY, preparation.mirrorValue);
1751
+ };
1752
+ if (sandboxDrainPreparation) await writeSandboxDrainMirror(sandboxDrainPreparation);
1753
+ const sandboxDrainState = sandboxDrainPreparation?.state ?? "complete";
1754
+ if (targetSandboxDrainProtocol >= 1 && (manifest.safetyProtocols?.sandboxDrain ?? 0) < targetSandboxDrainProtocol) {
1755
+ manifest = {
1756
+ ...manifest,
1757
+ safetyProtocols: { sandboxDrain: targetSandboxDrainProtocol },
1758
+ updatedAt: now()
1759
+ };
1760
+ writeInstallationManifest(manifest);
1761
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1762
+ }
1763
+ manifest = transitionInstallation(manifest, "deploying", now());
1764
+ writeInstallationManifest(manifest);
1765
+ const ids = Object.fromEntries(manifest.resources.map((resource) => [resource.key, resource.id]));
1766
+ const bootstrapStep = "bootstrap-access:v1";
1767
+ if (!manifest.steps.some((step) => step.key === bootstrapStep)) {
1768
+ const routingKv = requireId(ids, "routing-kv");
1769
+ await dependencies.client.putKvValue(plan.account.id, routingKv, "signup:enabled", "true");
1770
+ await dependencies.client.putKvValue(plan.account.id, routingKv, "signup:allowlist", JSON.stringify([]));
1771
+ manifest = recordInstallationStep(manifest, {
1772
+ completedAt: now(),
1773
+ key: bootstrapStep,
1774
+ resultHash: hash(secrets.bootstrapGitHubId)
1775
+ }, now());
1776
+ writeInstallationManifest(manifest);
1777
+ }
1778
+ const entrypoints = Object.fromEntries(Object.entries(runtime.artifacts).map(([key, artifact]) => [key, join(runtimeDirectory, artifact.path)]));
1779
+ const interruptedCutover = interruptedWorkerCutover(manifest);
1780
+ const recoveringUnsafeTraffic = (interruptedCutover?.unsafeTraffic.size ?? 0) > 0;
1781
+ const createsWorker = plan.actions.some((action) => action.kind === "worker" && action.action === "create");
1782
+ const recoveringCreatedWorker = interruptedCutover !== null && [...interruptedCutover.staged.values()].some((snapshot) => snapshot.previousVersionId === null);
1783
+ const requiresSandboxDrain = !preserveDisabled && targetSandboxDrainProtocol >= 1 && sandboxDrainState === "pending";
1784
+ const requiresTrafficGuard = !previousPublicEndpoints || recoveringUnsafeTraffic || createsWorker || recoveringCreatedWorker || manifest.usageProtocolVersion !== targetUsageProtocolVersion || requiresSandboxDrain;
1785
+ if (requiresTrafficGuard) {
1786
+ await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
1787
+ workerTrafficGuarded = true;
1788
+ }
1789
+ if (!preserveDisabled) {
1790
+ const trafficGeneration = nextTrafficGeneration(manifest);
1791
+ manifest = recordInstallationStep(manifest, {
1792
+ completedAt: now(),
1793
+ key: `traffic-enabled:${trafficGeneration}`,
1794
+ resultHash: String(trafficGeneration)
1795
+ }, now());
1796
+ writeInstallationManifest(manifest);
1797
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1798
+ }
1799
+ const configInput = {
1800
+ accountId: plan.account.id,
1801
+ apiUrl: plan.apiUrl,
1802
+ deployment: plan.deployment,
1803
+ healthProbeToken: createPlatformHealthProbeToken(manifest.ownershipNonce),
1804
+ installationId: manifest.id,
1805
+ ownershipNonce: manifest.ownershipNonce,
1806
+ platformVersion: version,
1807
+ proxyUrl: plan.proxyUrl,
1808
+ resources: {
1809
+ hyperdrive: requireId(ids, "hyperdrive"),
1810
+ isrCacheKv: requireId(ids, "isr-cache-kv"),
1811
+ legacyAssetsKv: requireId(ids, "legacy-assets-kv"),
1812
+ routingKv: requireId(ids, "routing-kv"),
1813
+ zone: zoneId
1814
+ },
1815
+ runtimeEntrypoints: entrypoints
1816
+ };
1817
+ const requiresStagedCommit = !preserveDisabled && requiresTrafficGuard;
1818
+ const enabledConfigs = createPlatformWorkerConfigs({
1819
+ ...configInput,
1820
+ publicEndpoints: !preserveDisabled,
1821
+ trafficState: preserveDisabled ? "staging" : "enabled"
1822
+ });
1823
+ const configs = requiresStagedCommit ? createPlatformWorkerConfigs({
1824
+ ...configInput,
1825
+ publicEndpoints: true,
1826
+ trafficState: requiresSandboxDrain ? "sandbox-drain" : "staging"
1827
+ }) : enabledConfigs;
1828
+ const previousConfigs = createPlatformWorkerConfigs({
1829
+ ...configInput,
1830
+ publicEndpoints: previousPublicEndpoints,
1831
+ trafficState: previousPublicEndpoints ? "enabled" : "staging"
1832
+ });
1833
+ const managedRoutes = platformManagedWorkerTraffic(plan.deployment).routes;
1834
+ let workerActions = plan.actions.filter((action) => action.kind === "worker");
1835
+ if (interruptedCutover) {
1836
+ for (const key of [...interruptedCutover.staged.keys()].reverse()) {
1837
+ const snapshot = interruptedCutover.staged.get(key);
1838
+ const preservedRoutes = dependencies.recoverWorkerCutover ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, snapshot.workerName);
1839
+ const recoveryConfig = mergePlatformWorkerTrafficConfig({
1840
+ ...configs[key],
1841
+ name: snapshot.workerName
1842
+ }, preservedRoutes);
1843
+ const previousRecoveryConfig = mergePlatformWorkerTrafficConfig({
1844
+ ...previousConfigs[key],
1845
+ name: snapshot.workerName
1846
+ }, preservedRoutes);
1847
+ if (dependencies.recoverWorkerCutover) await dependencies.recoverWorkerCutover({
1848
+ key,
1849
+ preserveCustomDomains: interruptedCutover.unsafeTraffic.has(key),
1850
+ previousVersionId: snapshot.previousVersionId,
1851
+ stagedVersionId: snapshot.stagedVersionId,
1852
+ workerName: snapshot.workerName
1853
+ });
1854
+ else {
1855
+ const recoveryTraffic = isolatePlatformWorkerCustomDomains(recoveryConfig);
1856
+ const previousRecoveryTraffic = isolatePlatformWorkerCustomDomains(previousRecoveryConfig);
1857
+ await rollbackPlatformWorker({
1858
+ accountId: plan.account.id,
1859
+ activatedDeploymentId: null,
1860
+ activatedVersionId: snapshot.stagedVersionId,
1861
+ config: recoveryConfig,
1862
+ nextTriggers: readTriggerConfigFromObject(recoveryTraffic.wranglerConfig),
1863
+ previousConfig: previousRecoveryConfig,
1864
+ previousCustomDomains: previousRecoveryTraffic.customDomains,
1865
+ previousDeployment: null,
1866
+ previousTriggers: mergePreservedTriggerRoutes(withoutPlatformCustomDomainTriggers(snapshot.previousTriggers), preservedRoutes),
1867
+ previousVersionId: snapshot.previousVersionId,
1868
+ preserveCustomDomains: interruptedCutover.unsafeTraffic.has(key),
1869
+ inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, `${key}-worker`, snapshot.workerName),
1870
+ removeCustomDomains: async () => {
1871
+ if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
1872
+ await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(manifest, /* @__PURE__ */ new Set([snapshot.workerName])));
1873
+ },
1874
+ publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
1875
+ retainNewWorker: async () => {
1876
+ await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
1877
+ },
1878
+ runner: runPinnedPlatformWrangler,
1879
+ workerExisted: snapshot.previousVersionId !== null,
1880
+ workerName: snapshot.workerName
1881
+ });
1882
+ }
1883
+ }
1884
+ manifest = recordWorkerCutoverResolution(manifest, interruptedCutover.generation, "restored", now());
1885
+ manifest = recordWorkerTrafficState(manifest, interruptedCutover.generation, previousPublicEndpoints && interruptedCutover.unsafeTraffic.size === 0, now());
1886
+ let retainedNewWorker = false;
1887
+ for (const [, snapshot] of interruptedCutover.staged) {
1888
+ if (snapshot.previousVersionId !== null) continue;
1889
+ retainedNewWorker = true;
1890
+ }
1891
+ writeInstallationManifest(manifest);
1892
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1893
+ if (retainedNewWorker) {
1894
+ if (!dependencies.replanWorkers) throw new Error("platform: Recovering retained Workers requires a fresh Cloudflare installation plan.");
1895
+ workerActions = validateReplannedWorkerActions(plan, manifest, await dependencies.replanWorkers(manifest));
1896
+ }
1897
+ }
1898
+ workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
1899
+ if (workerSecretUpdateIntent) {
1900
+ manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "pending", workerSecretUpdateIntent.id, now());
1901
+ writeInstallationManifest(manifest);
1902
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1903
+ workerSecretUpdateIntent = {
1904
+ ...workerSecretUpdateIntent,
1905
+ generation: workerCutoverGeneration
1906
+ };
1907
+ writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
1908
+ }
1909
+ for (const key of [
1910
+ "api",
1911
+ "proxy",
1912
+ "tail",
1913
+ "dispatch"
1914
+ ]) {
1915
+ dependencies.onProgress?.(`Deploying the ${key} service`);
1916
+ const action = workerActions.find((entry) => entry.key === `${key}-worker`);
1917
+ const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
1918
+ manifest = recordProvisionIntent(manifest, action, now());
1919
+ writeInstallationManifest(manifest);
1920
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1921
+ await dependencies.revalidateBeforeWorkerDeploy(manifest, action);
1922
+ const preservedRoutes = dependencies.deployWorker ? [] : action.action === "create" ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
1923
+ const workerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
1924
+ const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
1925
+ const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
1926
+ apiBaseUrl: cloudflareApiBaseUrl,
1927
+ onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
1928
+ manifest = recordWorkerCutoverStage(manifest, workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
1929
+ writeInstallationManifest(manifest);
1930
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1931
+ },
1932
+ onTrafficPublished: async () => {
1933
+ if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
1934
+ const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
1935
+ manifest = recordPlatformWorkerTraffic(manifest, action.name, {
1936
+ customDomains,
1937
+ routes
1938
+ }, now());
1939
+ writeInstallationManifest(manifest);
1940
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1941
+ },
1942
+ onUnsafeTraffic: async () => {
1943
+ manifest = recordWorkerUnsafeTraffic(manifest, workerCutoverGeneration, key, action.name, now());
1944
+ writeInstallationManifest(manifest);
1945
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1946
+ },
1947
+ managedRoutes: preserveDisabled ? [] : managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
1948
+ preservedRoutes,
1949
+ publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
1950
+ publishRoutes: async (workerName, patterns) => {
1951
+ if (!dependencies.client.publishWorkerRoutesSafely) throw new Error("platform: Safe Cloudflare Worker route publication is unavailable.");
1952
+ if (zoneId) await dependencies.client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
1953
+ else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
1954
+ },
1955
+ previousConfig: previousWorkerConfig,
1956
+ previousPublicEndpoints,
1957
+ inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, action.key, action.name),
1958
+ removeCustomDomains: async () => {
1959
+ if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
1960
+ await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(manifest, /* @__PURE__ */ new Set([action.name])));
1961
+ },
1962
+ retainNewWorker: async () => {
1963
+ await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
1964
+ },
1965
+ runner: runPinnedPlatformWrangler,
1966
+ workerExisted: action.action !== "create"
1967
+ });
1968
+ if (rollback) workerRollbacks.push(rollback);
1969
+ manifest = recordResource(manifest, action, action.name, "created", now());
1970
+ writeInstallationManifest(manifest);
1971
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1972
+ }
1973
+ manifest = setInstallationEndpoints(manifest, {
1974
+ apiUrl: plan.apiUrl,
1975
+ dashboardUrl: null,
1976
+ proxyUrl: plan.proxyUrl
1977
+ }, now());
1978
+ if (preserveDisabled) {
1979
+ workerDeploymentCommitted = true;
1980
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
1981
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
1982
+ if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
1983
+ manifest = {
1984
+ ...manifest,
1985
+ platformVersion: version,
1986
+ runtime: runtimeProvenance,
1987
+ updatedAt: now(),
1988
+ usageProtocolVersion: targetUsageProtocolVersion
1989
+ };
1990
+ manifest = transitionInstallation(manifest, "disabled", now());
1991
+ writeInstallationManifest(manifest);
1992
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
1993
+ writePendingInstallationSecrets(manifest, secretCheckpoint(secrets));
1994
+ promoteInstallationSecrets(manifest);
1995
+ saveInstalledPlatformConnection(plan, manifest);
1996
+ return {
1997
+ manifest,
1998
+ waitingForDns: false,
1999
+ waitingForSandboxDrain: false,
2000
+ zoneNameServers: zone?.nameServers ?? []
2001
+ };
2002
+ }
2003
+ manifest = transitionInstallation(manifest, "verifying", now());
2004
+ writeInstallationManifest(manifest);
2005
+ dependencies.onProgress?.("Verifying platform health");
2006
+ const healthCheckContext = {
2007
+ accountName: plan.account.name,
2008
+ workers: [
2009
+ plan.deployment.resources.apiWorker,
2010
+ plan.deployment.resources.proxyWorker,
2011
+ plan.deployment.resources.dispatchWorker
2012
+ ],
2013
+ resumeCommand: dependencies.healthCheckResumeCommand
2014
+ };
2015
+ 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);
2016
+ if (requiresSandboxDrain) {
2017
+ if (!await (dependencies.verifySandboxDrain ?? verifyPlatformSandboxDrain)(plan.apiUrl, createPlatformHealthProbeToken(manifest.ownershipNonce))) {
2018
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
2019
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
2020
+ manifest = recordInstallationStep(manifest, {
2021
+ completedAt: now(),
2022
+ key: "sandbox-drain:maintenance-runtime:v1",
2023
+ resultHash: resolvedRuntime.manifestSha256
2024
+ }, now());
2025
+ writeInstallationManifest(manifest);
2026
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2027
+ workerRollbacks = [];
2028
+ workerDeploymentCommitted = true;
2029
+ saveInstalledPlatformConnection(plan, manifest);
2030
+ return {
2031
+ manifest,
2032
+ waitingForDns: false,
2033
+ waitingForSandboxDrain: true,
2034
+ zoneNameServers: zone?.nameServers ?? []
2035
+ };
2036
+ }
2037
+ const completedDrain = await prepareSandboxDrain();
2038
+ if (completedDrain.state !== "complete") throw new Error("platform: Sandbox drain completion changed while it was being verified. Traffic remains disabled.");
2039
+ await writeSandboxDrainMirror(completedDrain);
2040
+ }
2041
+ if (requiresStagedCommit) {
2042
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
2043
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, false, now());
2044
+ writeInstallationManifest(manifest);
2045
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2046
+ workerRollbacks = [];
2047
+ workerCutoverGeneration = nextWorkerCutoverGeneration(manifest);
2048
+ if (workerSecretUpdateIntent) {
2049
+ manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "pending", workerSecretUpdateIntent.id, now());
2050
+ writeInstallationManifest(manifest);
2051
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2052
+ workerSecretUpdateIntent = {
2053
+ ...workerSecretUpdateIntent,
2054
+ generation: workerCutoverGeneration
2055
+ };
2056
+ writePendingInstallationSecrets(manifest, secretCheckpoint(secrets, workerSecretUpdateIntent));
2057
+ }
2058
+ for (const key of [
2059
+ "api",
2060
+ "proxy",
2061
+ "tail",
2062
+ "dispatch"
2063
+ ]) {
2064
+ dependencies.onProgress?.(`Enabling the ${key} service`);
2065
+ const action = workerActions.find((entry) => entry.key === `${key}-worker`);
2066
+ const workerSecrets = platformWorkerSecretPayload(key, workerDeploymentSecrets, plan.account.id, zoneId, action.action !== "create");
2067
+ const preservedRoutes = dependencies.deployWorker ? [] : await inventoryUnmanagedPlatformWorkerTraffic(dependencies.client, plan, action.name);
2068
+ const workerConfig = mergePlatformWorkerTrafficConfig(enabledConfigs[key], preservedRoutes);
2069
+ const previousWorkerConfig = mergePlatformWorkerTrafficConfig(configs[key], preservedRoutes);
2070
+ const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
2071
+ apiBaseUrl: cloudflareApiBaseUrl,
2072
+ onStaged: async (previousVersionId, stagedVersionId, previousTriggers) => {
2073
+ manifest = recordWorkerCutoverStage(manifest, workerCutoverGeneration, key, previousVersionId, stagedVersionId, action.name, previousTriggers, now());
2074
+ writeInstallationManifest(manifest);
2075
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2076
+ },
2077
+ onTrafficPublished: async () => {
2078
+ if (!dependencies.client.listWorkerDomains || !dependencies.client.listWorkerRoutes) throw new Error("platform: Cloudflare traffic inventory is unavailable.");
2079
+ const [customDomains, routes] = await Promise.all([dependencies.client.listWorkerDomains(plan.account.id), zoneId ? dependencies.client.listWorkerRoutes(zoneId) : Promise.resolve([])]);
2080
+ manifest = recordPlatformWorkerTraffic(manifest, action.name, {
2081
+ customDomains,
2082
+ routes
2083
+ }, now());
2084
+ writeInstallationManifest(manifest);
2085
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2086
+ },
2087
+ onUnsafeTraffic: async () => {
2088
+ manifest = recordWorkerUnsafeTraffic(manifest, workerCutoverGeneration, key, action.name, now());
2089
+ writeInstallationManifest(manifest);
2090
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2091
+ },
2092
+ managedRoutes: managedRoutes.filter((route) => route.script === action.name).map((route) => route.pattern),
2093
+ preservedRoutes,
2094
+ publishCustomDomains: (workerName, origins) => publishPlatformWorkerCustomDomains(dependencies.client, plan.account.id, workerName, origins),
2095
+ publishRoutes: async (workerName, patterns) => {
2096
+ if (!dependencies.client.publishWorkerRoutesSafely) throw new Error("platform: Safe Cloudflare Worker route publication is unavailable.");
2097
+ if (zoneId) await dependencies.client.publishWorkerRoutesSafely(zoneId, workerName, patterns);
2098
+ else if (patterns.length > 0) throw new Error("platform: Cannot publish zone routes in workers.dev mode.");
2099
+ },
2100
+ previousConfig: previousWorkerConfig,
2101
+ previousPublicEndpoints: true,
2102
+ inspectNewWorker: () => inspectNewPlatformWorker(dependencies.client, plan.account.id, manifest, action.key, action.name),
2103
+ removeCustomDomains: async () => {
2104
+ if (!dependencies.client.removeRoutes) throw new Error("platform: Cloudflare route removal is unavailable.");
2105
+ await dependencies.client.removeRoutes(plan.account.id, zoneId, trackedPlatformWorkerTraffic(manifest, /* @__PURE__ */ new Set([action.name])));
2106
+ },
2107
+ retainNewWorker: async () => {
2108
+ await dependencies.client.putKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE);
2109
+ },
2110
+ runner: runPinnedPlatformWrangler,
2111
+ workerExisted: true
2112
+ });
2113
+ if (rollback) workerRollbacks.push(rollback);
2114
+ }
2115
+ }
2116
+ const verifyCurrentWorkerCutover = async () => {
2117
+ if (!requiresStagedCommit) return;
2118
+ if (dependencies.verifyWorkerCutover) await dependencies.verifyWorkerCutover(manifest, workerCutoverGeneration);
2119
+ else if (!dependencies.deployWorker) await verifyPlatformWorkerCutoverVersions(manifest, workerCutoverGeneration, enabledConfigs, plan.account.id, runPinnedPlatformWrangler);
2120
+ };
2121
+ await verifyCurrentWorkerCutover();
2122
+ if (dependencies.client.deleteKvValue) await dependencies.client.deleteKvValue(plan.account.id, requireId(ids, "routing-kv"), PLATFORM_DISABLED_KEY);
2123
+ else if (wasDisabled || !previousPublicEndpoints) throw new Error("platform: Routing disablement removal is unavailable.");
2124
+ if (requiresStagedCommit) {
2125
+ dependencies.onProgress?.("Verifying the enabled platform");
2126
+ 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);
2127
+ await verifyCurrentWorkerCutover();
2128
+ }
2129
+ workerDeploymentCommitted = true;
2130
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "committed", now());
2131
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, true, now());
2132
+ if (workerSecretUpdateIntent) manifest = recordWorkerSecretUpdateState(manifest, workerCutoverGeneration, "committed", workerSecretUpdateIntent.id, now());
2133
+ manifest = {
2134
+ ...manifest,
2135
+ platformVersion: version,
2136
+ runtime: runtimeProvenance,
2137
+ safetyProtocols: runtime.safetyProtocols ?? { sandboxDrain: 0 },
2138
+ updatedAt: now(),
2139
+ usageProtocolVersion: targetUsageProtocolVersion
2140
+ };
2141
+ manifest = transitionInstallation(manifest, "installed", now());
2142
+ writeInstallationManifest(manifest);
2143
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2144
+ workerTrafficGuarded = false;
2145
+ writePendingInstallationSecrets(manifest, secretCheckpoint(secrets));
2146
+ promoteInstallationSecrets(manifest);
2147
+ saveInstalledPlatformConnection(plan, manifest);
2148
+ return {
2149
+ manifest,
2150
+ waitingForDns: false,
2151
+ waitingForSandboxDrain: false,
2152
+ zoneNameServers: zone?.nameServers ?? []
2153
+ };
2154
+ } catch (error) {
2155
+ let failure = error;
2156
+ const currentWorkerRecoveryIncomplete = error instanceof PlatformWorkerRecoveryIncompleteError;
2157
+ if (workerTrafficGuarded) try {
2158
+ 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);
2159
+ } catch (guardError) {
2160
+ failure = new Error(`platform: ${errorMessage(error)} The installation also failed to retain its routing safety gate: ${errorMessage(guardError)}`, { cause: error });
2161
+ }
2162
+ if (!workerDeploymentCommitted && workerRollbacks.length > 0) {
2163
+ const rollbackFailures = [];
2164
+ for (const rollback of workerRollbacks.reverse()) try {
2165
+ await rollback();
2166
+ } catch (rollbackError) {
2167
+ rollbackFailures.push(errorMessage(rollbackError));
2168
+ }
2169
+ workerRollbacks = [];
2170
+ 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 });
2171
+ else if (workerCutoverGeneration > 0) {
2172
+ manifest = recordWorkerCutoverResolution(manifest, workerCutoverGeneration, "restored", now());
2173
+ manifest = recordWorkerTrafficState(manifest, workerCutoverGeneration, previousPublicEndpoints, now());
2174
+ writeInstallationManifest(manifest);
2175
+ }
2176
+ }
2177
+ if (manifest.status !== "failed" && manifest.status !== "uninstalled") try {
2178
+ manifest = transitionInstallation(manifest, "failed", now());
2179
+ writeInstallationManifest(manifest);
2180
+ await writeRemoteInstallationCheckpoint(dependencies.client, plan, manifest);
2181
+ } catch {}
2182
+ throw failure;
2183
+ } finally {
2184
+ await releaseDatabaseLock();
2185
+ }
2186
+ }
2187
+ async function writeR2OwnershipMarkerIfNeeded(client, plan, manifest, action) {
2188
+ if (action.kind !== "r2-bucket") return;
2189
+ const bucket = manifest.resources.find((resource) => resource.key === action.key && resource.state === "ready");
2190
+ if (!bucket || bucket.ownership !== "created") return;
2191
+ await client.putR2Object(plan.account.id, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY, JSON.stringify(createPlatformResourceOwnershipMarker({
2192
+ accountId: plan.account.id,
2193
+ installationId: manifest.id,
2194
+ ownershipNonce: manifest.ownershipNonce,
2195
+ resourceId: bucket.id,
2196
+ resourceKey: bucket.key,
2197
+ resourceKind: bucket.kind,
2198
+ resourceName: bucket.name
2199
+ })));
2200
+ }
2201
+ function saveInstalledPlatformConnection(plan, manifest) {
2202
+ savePlatformConnection({
2203
+ accountId: plan.account.id,
2204
+ apiUrl: plan.apiUrl,
2205
+ applicationDomain: plan.deployment.application.baseDomain,
2206
+ displayName: plan.deployment.displayName,
2207
+ installationId: manifest.id,
2208
+ kind: "installed",
2209
+ proxyUrl: plan.proxyUrl
2210
+ });
2211
+ }
2212
+ async function writeRemoteInstallationCheckpointWithoutDatabase(client, plan, manifest, databaseLock) {
2213
+ await databaseLock?.checkpoint?.(manifest);
2214
+ const routing = manifest.resources.find((resource) => resource.key === "routing-kv" && resource.kind === "kv-namespace" && resource.ownership === "created" && resource.state === "ready");
2215
+ if (!routing) return;
2216
+ await client.putKvValue(plan.account.id, routing.id, REMOTE_INSTALLATION_MANIFEST_KEY, JSON.stringify(manifest));
2217
+ }
2218
+ function recordProvisionIntent(manifest, action, now) {
2219
+ const previousResourceId = manifest.resources.find((resource) => resource.key === action.key)?.id ?? null;
2220
+ const resultHash = platformProvisionIntentHash(manifest, action.key, action.kind, action.name, previousResourceId);
2221
+ if (manifest.steps.find((step) => (step.key === `provision-intent:${action.key}` || step.key.startsWith(`provision-intent:${action.key}:`)) && step.resultHash === resultHash)) return manifest;
2222
+ return recordInstallationStep(manifest, {
2223
+ completedAt: now,
2224
+ key: `provision-intent:${action.key}:${resultHash.slice(0, 16)}`,
2225
+ resultHash
2226
+ }, now);
2227
+ }
2228
+ function recordAdoptionIntent(manifest, action, id, now) {
2229
+ return recordInstallationStep(manifest, {
2230
+ completedAt: now,
2231
+ key: `adoption-intent:${action.key}`,
2232
+ resultHash: platformAdoptionIntentHash(manifest, action.key, action.kind, id, action.name)
2233
+ }, now);
2234
+ }
2235
+ function hasProvisionIntent(manifest, action) {
2236
+ const previousResourceId = manifest.resources.find((resource) => resource.key === action.key)?.id ?? null;
2237
+ 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));
2238
+ }
2239
+ async function createResource(client, plan, action, databaseUrl, zoneId) {
2240
+ switch (action.kind) {
2241
+ case "ai-gateway": return client.createAiGateway(plan.account.id, action.name);
2242
+ case "dns-record": return client.createDnsRecord(zoneId, action.name);
2243
+ case "kv-namespace": return client.createKvNamespace(plan.account.id, action.name);
2244
+ case "r2-bucket": return client.createR2Bucket(plan.account.id, action.name);
2245
+ case "queue": return client.createQueue(plan.account.id, action.name);
2246
+ case "dispatch-namespace": return client.createDispatchNamespace(plan.account.id, action.name);
2247
+ case "hyperdrive": return client.createHyperdrive(plan.account.id, action.name, databaseUrl);
2248
+ default: throw new Error(`platform: Cannot provision ${action.kind} in the resource phase.`);
2249
+ }
2250
+ }
2251
+ function recordResource(manifest, action, id, ownership, now, databaseUrl, resourceFingerprint) {
2252
+ const previous = manifest.resources.find((resource) => resource.key === action.key);
2253
+ return recordInstallationResource(manifest, {
2254
+ configHash: action.kind === "dns-record" ? dnsRecordConfigHash(action.name, "AAAA", "100::", true) : action.kind === "hyperdrive" && databaseUrl ? hyperdriveOriginConfigHash(hyperdriveOriginFromConnectionString(databaseUrl)) : action.kind === "r2-bucket" && resourceFingerprint ? r2BucketConfigHash(action.name, resourceFingerprint) : hash(JSON.stringify({
2255
+ kind: action.kind,
2256
+ name: action.name
2257
+ })),
2258
+ id,
2259
+ key: action.key,
2260
+ kind: action.kind,
2261
+ name: action.name,
2262
+ ownership: ownership ?? previous?.ownership ?? "created",
2263
+ state: "ready"
2264
+ }, now);
2265
+ }
2266
+ function r2BucketConfigHash(name, fingerprint) {
2267
+ return hash(JSON.stringify({
2268
+ fingerprint,
2269
+ kind: "r2-bucket",
2270
+ name
2271
+ }));
2272
+ }
2273
+ function retainReplacedInstallationResource(manifest, action, now) {
2274
+ const previous = manifest.resources.find((resource) => resource.key === action.key);
2275
+ if (action.action !== "create" || !previous || previous.state !== "ready" || previous.name === action.name) return manifest;
2276
+ const retainedKey = `retained:${action.key}:${hash(previous.id).slice(0, 16)}`;
2277
+ const resources = manifest.resources.filter((resource) => resource.key !== action.key && resource.key !== retainedKey).concat({
2278
+ ...previous,
2279
+ key: retainedKey,
2280
+ state: "retained"
2281
+ }).sort((left, right) => left.key.localeCompare(right.key));
2282
+ return {
2283
+ ...manifest,
2284
+ resources,
2285
+ updatedAt: now
2286
+ };
2287
+ }
2288
+ function validateReplannedWorkerActions(original, manifest, actions) {
2289
+ const expected = original.actions.filter((action) => action.kind === "worker");
2290
+ if (actions.length !== expected.length) throw new Error("platform: Fresh Cloudflare planning did not return every platform Worker.");
2291
+ for (const previous of expected) {
2292
+ const matches = actions.filter((action) => action.key === previous.key);
2293
+ const current = matches[0];
2294
+ if (matches.length !== 1 || !current || current.kind !== "worker" || current.name !== previous.name) throw new Error(`platform: Fresh Cloudflare planning changed the identity of Worker ${JSON.stringify(previous.name)}.`);
2295
+ if (manifest.resources.find((entry) => entry.key === current.key)?.state === "deleted" && current.action !== "create") throw new Error(`platform: Fresh Cloudflare planning did not recreate deleted Worker ${JSON.stringify(current.name)}.`);
2296
+ }
2297
+ return actions;
2298
+ }
2299
+ async function inventoryUnmanagedPlatformWorkerTraffic(client, plan, workerName) {
2300
+ if (!client.listWorkerDomains || !client.listWorkerServiceRoutes) throw new Error(`platform: Cloudflare traffic inventory is required before updating Worker ${JSON.stringify(workerName)}.`);
2301
+ const [routes, domains] = await Promise.all([client.listWorkerServiceRoutes(plan.account.id, workerName), client.listWorkerDomains(plan.account.id)]);
2302
+ const managed = platformManagedWorkerTraffic(plan.deployment, /* @__PURE__ */ new Set([workerName]));
2303
+ const managedRoutes = new Set(managed.routes.map((route) => route.pattern));
2304
+ const managedDomains = new Set(managed.customDomains.map((domain) => domain.hostname));
2305
+ return uniqueTriggerRoutes([...routes.filter((route) => !managedRoutes.has(route.pattern)).map((route) => ({
2306
+ pattern: route.pattern,
2307
+ zone_name: route.zoneName
2308
+ })), ...domains.filter((domain) => domain.service === workerName && !managedDomains.has(domain.hostname)).map((domain) => ({
2309
+ pattern: domain.hostname,
2310
+ custom_domain: true,
2311
+ ...domain.zoneName ? { zone_name: domain.zoneName } : {}
2312
+ }))]);
2313
+ }
2314
+ function mergePlatformWorkerTrafficConfig(config, preservedRoutes) {
2315
+ if (preservedRoutes.length === 0) return config;
2316
+ return {
2317
+ ...config,
2318
+ routes: uniqueTriggerRoutes([...Array.isArray(config.routes) ? config.routes : [], ...preservedRoutes])
2319
+ };
2320
+ }
2321
+ function mergePreservedTriggerRoutes(config, preservedRoutes) {
2322
+ return preservedRoutes.length === 0 ? config : {
2323
+ ...config,
2324
+ routes: uniqueTriggerRoutes([...config.routes, ...preservedRoutes])
2325
+ };
2326
+ }
2327
+ function isolatePlatformWorkerCustomDomains(config) {
2328
+ if (config.routes === void 0) return {
2329
+ customDomains: [],
2330
+ wranglerConfig: config
2331
+ };
2332
+ if (!Array.isArray(config.routes)) throw new Error("platform: Generated Worker routes must be an array.");
2333
+ const customDomains = [];
2334
+ const wranglerRoutes = [];
2335
+ for (const route of config.routes) {
2336
+ if (typeof route !== "object" || route === null || route.custom_domain !== true) {
2337
+ wranglerRoutes.push(route);
2338
+ continue;
2339
+ }
2340
+ const entry = route;
2341
+ const hostname = requiredNonEmptyString(entry.pattern, "Worker Custom Domain hostname");
2342
+ customDomains.push({
2343
+ hostname,
2344
+ ...entry.zone_id === void 0 ? {} : { zoneId: requiredNonEmptyString(entry.zone_id, `zone id for ${hostname}`) },
2345
+ ...entry.zone_name === void 0 ? {} : { zoneName: requiredNonEmptyString(entry.zone_name, `zone name for ${hostname}`) },
2346
+ ...entry.enabled === void 0 ? {} : { enabled: requiredBoolean(entry.enabled, `enabled for ${hostname}`) },
2347
+ ...entry.previews_enabled === void 0 ? {} : { previewsEnabled: requiredBoolean(entry.previews_enabled, `previews_enabled for ${hostname}`) }
2348
+ });
2349
+ }
2350
+ return {
2351
+ customDomains,
2352
+ wranglerConfig: {
2353
+ ...config,
2354
+ routes: wranglerRoutes
2355
+ }
2356
+ };
2357
+ }
2358
+ function withoutPlatformWorkerTrafficAttachments(config) {
2359
+ const isolated = { ...config };
2360
+ delete isolated.route;
2361
+ delete isolated.routes;
2362
+ return isolated;
2363
+ }
2364
+ function withoutPlatformCustomDomainTriggers(config) {
2365
+ return {
2366
+ ...config,
2367
+ routes: config.routes.filter((route) => typeof route !== "object" || route === null || route.custom_domain !== true)
2368
+ };
2369
+ }
2370
+ function requiredNonEmptyString(value, field) {
2371
+ if (typeof value !== "string" || value.length === 0) throw new Error(`platform: ${field} must be a non-empty string.`);
2372
+ return value;
2373
+ }
2374
+ function requiredBoolean(value, field) {
2375
+ if (typeof value !== "boolean") throw new Error(`platform: ${field} must be a boolean.`);
2376
+ return value;
2377
+ }
2378
+ async function publishPlatformWorkerCustomDomains(client, accountId, workerName, origins) {
2379
+ if (!client.publishWorkerCustomDomainsSafely) throw new Error("platform: Safe Cloudflare Custom Domain publication is unavailable.");
2380
+ await client.publishWorkerCustomDomainsSafely(accountId, workerName, origins);
2381
+ }
2382
+ function uniqueTriggerRoutes(routes) {
2383
+ const seen = /* @__PURE__ */ new Set();
2384
+ return routes.filter((route) => {
2385
+ const key = JSON.stringify(route);
2386
+ if (seen.has(key)) return false;
2387
+ seen.add(key);
2388
+ return true;
2389
+ });
2390
+ }
2391
+ function dnsRecordConfigHash(name, type, content, proxied) {
2392
+ return hash(JSON.stringify({
2393
+ content,
2394
+ kind: "dns-record",
2395
+ name,
2396
+ proxied,
2397
+ type
2398
+ }));
2399
+ }
2400
+ function hash(value) {
2401
+ return createHash("sha256").update(value).digest("hex");
2402
+ }
2403
+ function databaseInteger(value) {
2404
+ if (typeof value === "number" && Number.isSafeInteger(value)) return value;
2405
+ if (typeof value === "bigint") {
2406
+ const number = Number(value);
2407
+ return Number.isSafeInteger(number) ? number : null;
2408
+ }
2409
+ if (typeof value === "string" && /^[0-9]+$/.test(value)) {
2410
+ const number = Number(value);
2411
+ return Number.isSafeInteger(number) ? number : null;
2412
+ }
2413
+ return null;
2414
+ }
2415
+ function requireId(ids, key) {
2416
+ const id = ids[key];
2417
+ if (!id) throw new Error(`platform: Installation manifest is missing ${key}.`);
2418
+ return id;
2419
+ }
2420
+ function apiWorkerSecrets(secrets, accountId, zoneId) {
2421
+ return withoutEmptySecrets({
2422
+ CF_ACCOUNT_ID: accountId,
2423
+ CF_API_TOKEN: secrets.cloudflareApiToken,
2424
+ CF_ZONE_ID: zoneId,
2425
+ GITHUB_CLIENT_ID: secrets.githubClientId,
2426
+ GITHUB_CLIENT_SECRET: secrets.githubClientSecret,
2427
+ JWT_SECRET: secrets.jwtSecret,
2428
+ PLATFORM_BOOTSTRAP_GITHUB_ID: secrets.bootstrapGitHubId,
2429
+ ...secrets.projectSecretKeysJson && secrets.projectSecretActiveKeyVersion ? {
2430
+ PROJECT_SECRET_ACTIVE_KEY_VERSION: secrets.projectSecretActiveKeyVersion,
2431
+ PROJECT_SECRET_KEYS_JSON: secrets.projectSecretKeysJson
2432
+ } : {},
2433
+ R2_ACCESS_KEY_ID: secrets.r2AccessKeyId,
2434
+ R2_SECRET_ACCESS_KEY: secrets.r2SecretAccessKey
2435
+ });
2436
+ }
2437
+ /** Existing Workers inherit live secrets, including complete encryption keyrings. */
2438
+ function platformWorkerSecretPayload(worker, secrets, accountId, zoneId, existingWorker) {
2439
+ if (worker !== "api" && worker !== "proxy") return null;
2440
+ if (!existingWorker) return worker === "api" ? apiWorkerSecrets(secrets, accountId, zoneId) : proxyWorkerSecrets(secrets, accountId);
2441
+ const allowed = worker === "api" ? [
2442
+ "CF_API_TOKEN",
2443
+ "GITHUB_CLIENT_ID",
2444
+ "GITHUB_CLIENT_SECRET",
2445
+ "R2_ACCESS_KEY_ID",
2446
+ "R2_SECRET_ACCESS_KEY"
2447
+ ] : ["CF_API_TOKEN"];
2448
+ const updates = Object.fromEntries(Object.entries(secrets.workerSecretUpdates ?? {}).filter(([name, value]) => allowed.includes(name) && typeof value === "string" && value !== ""));
2449
+ return Object.keys(updates).length > 0 ? updates : null;
2450
+ }
2451
+ async function resolveBootstrapGitHubId(login, fetcher = fetch) {
2452
+ const normalizedLogin = login.trim();
2453
+ if (!normalizedLogin) throw new Error("platform: VOID_PLATFORM_ADMIN_GITHUB_LOGIN is required.");
2454
+ const response = await fetcher(`https://api.github.com/users/${encodeURIComponent(normalizedLogin)}`, {
2455
+ headers: {
2456
+ Accept: "application/vnd.github+json",
2457
+ "User-Agent": "void-platform-installer"
2458
+ },
2459
+ redirect: "follow",
2460
+ signal: AbortSignal.timeout(15e3)
2461
+ });
2462
+ if (!response.ok) throw new Error(`platform: Could not resolve GitHub user ${JSON.stringify(normalizedLogin)} (HTTP ${response.status}). Verify the username and retry.`);
2463
+ const body = await response.json();
2464
+ const id = typeof body.id === "number" && Number.isSafeInteger(body.id) ? String(body.id) : "";
2465
+ if (!/^[1-9][0-9]*$/.test(id)) throw new Error("platform: GitHub returned an invalid account id for the initial administrator.");
2466
+ return id;
2467
+ }
2468
+ function proxyWorkerSecrets(secrets, accountId) {
2469
+ return withoutEmptySecrets({
2470
+ CF_ACCOUNT_ID: accountId,
2471
+ CF_API_TOKEN: secrets.cloudflareApiToken,
2472
+ JWT_SECRET: secrets.jwtSecret
2473
+ });
2474
+ }
2475
+ function withoutEmptySecrets(secrets) {
2476
+ return Object.fromEntries(Object.entries(secrets).filter(([, value]) => value !== ""));
2477
+ }
2478
+ function assertRequiredPlatformSecrets(plan, manifest, secrets) {
2479
+ const missing = /* @__PURE__ */ new Set();
2480
+ const requireSecret = (value, name) => {
2481
+ if (!value) missing.add(name);
2482
+ };
2483
+ if (!manifest.steps.some((step) => step.key === "bootstrap-access:v1")) {
2484
+ requireSecret(secrets.bootstrapGitHubId, "VOID_PLATFORM_ADMIN_GITHUB_ID");
2485
+ requireSecret(secrets.bootstrapGitHubLogin, "VOID_PLATFORM_ADMIN_GITHUB_LOGIN");
2486
+ }
2487
+ if (plan.actions.some((action) => action.key === "api-worker" && action.kind === "worker" && action.action === "create")) {
2488
+ requireSecret(secrets.cloudflareApiToken, "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN");
2489
+ requireSecret(secrets.githubClientId, "VOID_PLATFORM_GITHUB_CLIENT_ID");
2490
+ requireSecret(secrets.githubClientSecret, "VOID_PLATFORM_GITHUB_CLIENT_SECRET");
2491
+ requireSecret(secrets.jwtSecret, "VOID_PLATFORM_JWT_SECRET");
2492
+ requireSecret(secrets.projectSecretKeysJson, "VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON (or VOID_PLATFORM_PROJECT_SECRET_KEY for v1)");
2493
+ requireSecret(secrets.projectSecretActiveKeyVersion, "VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION (or VOID_PLATFORM_PROJECT_SECRET_KEY for v1)");
2494
+ requireSecret(secrets.r2AccessKeyId, "VOID_PLATFORM_R2_ACCESS_KEY_ID");
2495
+ requireSecret(secrets.r2SecretAccessKey, "VOID_PLATFORM_R2_SECRET_ACCESS_KEY");
2496
+ }
2497
+ if (plan.actions.some((action) => action.key === "proxy-worker" && action.kind === "worker" && action.action === "create")) {
2498
+ requireSecret(secrets.cloudflareApiToken, "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN");
2499
+ requireSecret(secrets.jwtSecret, "VOID_PLATFORM_JWT_SECRET");
2500
+ }
2501
+ if (missing.size > 0) throw new Error(`platform: Creating a missing platform Worker requires: ${[...missing].sort().join(", ")}. Restore these values from your secret manager and retry.`);
2502
+ }
2503
+ /** Make pg's current strict TLS behavior explicit without rewriting saved connection details. */
2504
+ function platformDatabaseClientUrl(databaseUrl) {
2505
+ let url;
2506
+ try {
2507
+ url = new URL(databaseUrl);
2508
+ } catch {
2509
+ return databaseUrl;
2510
+ }
2511
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:" || url.searchParams.getAll("sslmode").at(-1) !== "require" || url.searchParams.getAll("uselibpqcompat").at(-1) === "true") return databaseUrl;
2512
+ url.searchParams.set("sslmode", "verify-full");
2513
+ return url.toString();
2514
+ }
2515
+ async function claimPlatformDatabase(databaseUrl, installationId, ownershipNonce) {
2516
+ const { Pool } = await import("pg");
2517
+ const pool = new Pool({
2518
+ connectionString: platformDatabaseClientUrl(databaseUrl),
2519
+ max: 1
2520
+ });
2521
+ const client = await pool.connect();
2522
+ try {
2523
+ await claimPlatformDatabaseConnection(client, installationId, ownershipNonce);
2524
+ } finally {
2525
+ client.release();
2526
+ await pool.end();
2527
+ }
2528
+ }
2529
+ async function acquirePlatformDatabaseLifecycleLock(databaseUrl, installationId, ownershipNonce) {
2530
+ const { Pool } = await import("pg");
2531
+ const pool = new Pool({
2532
+ connectionString: platformDatabaseClientUrl(databaseUrl),
2533
+ max: 1
2534
+ });
2535
+ const client = await pool.connect();
2536
+ try {
2537
+ const releaseConnectionLock = await acquirePlatformDatabaseLifecycleLockConnection(client, installationId, ownershipNonce);
2538
+ const release = async () => {
2539
+ try {
2540
+ await releaseConnectionLock();
2541
+ } finally {
2542
+ client.release();
2543
+ await pool.end();
2544
+ }
2545
+ };
2546
+ return Object.assign(release, releaseConnectionLock);
2547
+ } catch (error) {
2548
+ client.release();
2549
+ await pool.end();
2550
+ throw error;
2551
+ }
2552
+ }
2553
+ async function readPlatformDatabaseLifecycleManifest(databaseUrl, installationId, ownershipNonce) {
2554
+ const { Pool } = await import("pg");
2555
+ const pool = new Pool({
2556
+ connectionString: platformDatabaseClientUrl(databaseUrl),
2557
+ max: 1
2558
+ });
2559
+ try {
2560
+ return await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
2561
+ } catch (error) {
2562
+ if (error instanceof Error && error.message.startsWith("platform:")) throw error;
2563
+ throw new Error(`platform: Could not read the PostgreSQL lifecycle checkpoint: ${error instanceof Error ? error.message : String(error)}`);
2564
+ } finally {
2565
+ await pool.end();
2566
+ }
2567
+ }
2568
+ async function inspectPlatformDatabaseStateConnection(client, database) {
2569
+ const relation = await client.query(`SELECT to_regclass('"drizzle"."__drizzle_migrations"')::text AS "relation"`);
2570
+ if (relation.rows.length === 0 || relation.rows[0]?.relation === null) return {
2571
+ pendingMigrations: database.migrations,
2572
+ schemaDigest: platformDatabaseSchemaDigest(database.lineage, [])
2573
+ };
2574
+ const result = await client.query(`SELECT "id", "hash", "created_at"
2575
+ FROM "drizzle"."__drizzle_migrations"
2576
+ ORDER BY "created_at", "id"`);
2577
+ if (result.rows.length > database.migrations.length) throw new Error("platform: PostgreSQL contains migrations newer than this Void Platform package.");
2578
+ let previousTimestamp = -1;
2579
+ const seenTimestamps = /* @__PURE__ */ new Set();
2580
+ for (const [position, row] of result.rows.entries()) {
2581
+ const timestampMillis = databaseInteger(row.created_at);
2582
+ if (timestampMillis === null || timestampMillis <= previousTimestamp || seenTimestamps.has(timestampMillis)) throw new Error("platform: PostgreSQL migration history is duplicated or not strictly ordered.");
2583
+ const expected = database.migrations[position];
2584
+ 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.");
2585
+ previousTimestamp = timestampMillis;
2586
+ seenTimestamps.add(timestampMillis);
2587
+ }
2588
+ const applied = database.migrations.slice(0, result.rows.length);
2589
+ return {
2590
+ pendingMigrations: database.migrations.slice(result.rows.length),
2591
+ schemaDigest: platformDatabaseSchemaDigest(database.lineage, applied)
2592
+ };
2593
+ }
2594
+ async function preparePlatformDatabaseConnection(client, runtimeDirectory, database, sourcePlatformVersion, hasPriorWorkers) {
2595
+ verifyPlatformDatabaseArtifacts(runtimeDirectory, database);
2596
+ const before = await inspectPlatformDatabaseStateConnection(client, database);
2597
+ if (before.pendingMigrations.length === 0) {
2598
+ if (before.schemaDigest !== database.schemaDigest) throw new Error("platform: PostgreSQL schema digest does not match the packaged runtime.");
2599
+ return {
2600
+ appliedMigrations: 0,
2601
+ previousSchemaDigest: before.schemaDigest,
2602
+ schemaDigest: before.schemaDigest
2603
+ };
2604
+ }
2605
+ if (hasPriorWorkers && !database.rollbackSafeFrom.some((entry) => entry.platformVersion === sourcePlatformVersion && entry.schemaDigest === before.schemaDigest)) throw new Error(`platform: Void Platform ${sourcePlatformVersion ?? "unknown"} cannot safely roll back after migrating database schema ${before.schemaDigest} to this release. Upgrade through a supported intermediate version or install into a fresh database.`);
2606
+ await client.query("BEGIN");
2607
+ try {
2608
+ await client.query("CREATE SCHEMA IF NOT EXISTS \"drizzle\"");
2609
+ await client.query(`
2610
+ CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
2611
+ "id" serial PRIMARY KEY,
2612
+ "hash" text NOT NULL,
2613
+ "created_at" bigint
2614
+ )
2615
+ `);
2616
+ const rechecked = await inspectPlatformDatabaseStateConnection(client, database);
2617
+ if (rechecked.schemaDigest !== before.schemaDigest) throw new Error("platform: PostgreSQL migration history changed while preparing this upgrade.");
2618
+ for (const migration of rechecked.pendingMigrations) {
2619
+ const sql = readFileSync(join(runtimeDirectory, "drizzle", `${migration.tag}.sql`), "utf8");
2620
+ for (const statement of sql.split("--> statement-breakpoint").map((value) => value.trim()).filter(Boolean)) await client.query(statement);
2621
+ await client.query(`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
2622
+ VALUES ($1, $2)`, [migration.sha256, migration.timestampMillis]);
2623
+ }
2624
+ await client.query("COMMIT");
2625
+ } catch (error) {
2626
+ await client.query("ROLLBACK").catch(() => ({ rows: [] }));
2627
+ throw error;
2628
+ }
2629
+ const after = await inspectPlatformDatabaseStateConnection(client, database);
2630
+ if (after.pendingMigrations.length !== 0 || after.schemaDigest !== database.schemaDigest) throw new Error("platform: PostgreSQL migration transaction completed without reaching the packaged schema.");
2631
+ return {
2632
+ appliedMigrations: before.pendingMigrations.length,
2633
+ previousSchemaDigest: before.schemaDigest,
2634
+ schemaDigest: after.schemaDigest
2635
+ };
2636
+ }
2637
+ async function readPlatformDatabaseLifecycleManifestConnection(client, installationId, ownershipNonce) {
2638
+ const relation = await client.query(`SELECT to_regclass('"__void_platform_installation"')::text AS "relation"`);
2639
+ if (relation.rows[0]?.relation === null || relation.rows.length === 0) return null;
2640
+ const result = await client.query(`SELECT "installation_id", "ownership_nonce", "lifecycle_manifest"
2641
+ FROM "__void_platform_installation"
2642
+ WHERE "singleton" = true`);
2643
+ const row = result.rows[0];
2644
+ if (result.rows.length !== 1 || row?.installation_id !== installationId || row.ownership_nonce !== ownershipNonce) throw new Error("platform: The PostgreSQL database ownership claim does not match this installation.");
2645
+ if (row.lifecycle_manifest === null || row.lifecycle_manifest === void 0) return null;
2646
+ const manifest = parsePlatformInstallationManifest(row.lifecycle_manifest);
2647
+ if (manifest.id !== installationId || manifest.ownershipNonce !== ownershipNonce) throw new Error("platform: The PostgreSQL lifecycle checkpoint does not match this installation.");
2648
+ return manifest;
2649
+ }
2650
+ async function acquirePlatformDatabaseLifecycleLockConnection(client, installationId, ownershipNonce) {
2651
+ await client.query(`ALTER TABLE "__void_platform_installation"
2652
+ ADD COLUMN IF NOT EXISTS "lifecycle_manifest" jsonb`);
2653
+ const owner = (await client.query(`SELECT "installation_id", "ownership_nonce"
2654
+ FROM "__void_platform_installation"
2655
+ WHERE "singleton" = true`)).rows[0];
2656
+ if (!owner || owner.installation_id !== installationId || owner.ownership_nonce !== ownershipNonce) throw new Error("platform: The PostgreSQL database ownership claim does not match this installation.");
2657
+ const lockIdentity = `void-platform-lifecycle-v1:${installationId}:${ownershipNonce}`;
2658
+ if ((await client.query("SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS \"acquired\"", [lockIdentity])).rows[0]?.acquired !== true) throw new Error(`platform: Another lifecycle operation is active for installation ${installationId} on another host.`);
2659
+ let authoritativeManifest = null;
2660
+ try {
2661
+ const checkpoint = await client.query(`SELECT "lifecycle_manifest"
2662
+ FROM "__void_platform_installation"
2663
+ WHERE "singleton" = true
2664
+ AND "installation_id" = $1
2665
+ AND "ownership_nonce" = $2`, [installationId, ownershipNonce]);
2666
+ const value = checkpoint.rows[0]?.lifecycle_manifest;
2667
+ if (checkpoint.rows.length !== 1) throw new Error("platform: The PostgreSQL database ownership claim changed unexpectedly.");
2668
+ if (value !== null && value !== void 0) {
2669
+ authoritativeManifest = parsePlatformInstallationManifest(value);
2670
+ if (authoritativeManifest.id !== installationId || authoritativeManifest.ownershipNonce !== ownershipNonce) throw new Error("platform: The PostgreSQL lifecycle checkpoint does not match this installation.");
2671
+ }
2672
+ } catch (error) {
2673
+ await client.query("SELECT pg_advisory_unlock(hashtextextended($1, 0))", [lockIdentity]);
2674
+ if (error instanceof Error && error.message.startsWith("platform:")) throw error;
2675
+ throw new Error(`platform: The PostgreSQL lifecycle checkpoint is invalid: ${error instanceof Error ? error.message : String(error)}`);
2676
+ }
2677
+ let released = false;
2678
+ const release = async () => {
2679
+ if (released) return;
2680
+ released = true;
2681
+ await client.query("SELECT pg_advisory_unlock(hashtextextended($1, 0))", [lockIdentity]);
2682
+ };
2683
+ release.authoritativeManifest = authoritativeManifest;
2684
+ release.requiredSafetyProtocols = await readRequiredPlatformSafetyProtocols(client);
2685
+ release.checkpoint = async (manifest) => {
2686
+ if (released) throw new Error("platform: Cannot checkpoint lifecycle state after releasing its lock.");
2687
+ if (manifest.id !== installationId || manifest.ownershipNonce !== ownershipNonce) throw new Error("platform: Refusing to checkpoint lifecycle state for another installation.");
2688
+ if ((await client.query(`UPDATE "__void_platform_installation"
2689
+ SET "lifecycle_manifest" = $1::jsonb
2690
+ WHERE "singleton" = true
2691
+ AND "installation_id" = $2
2692
+ AND "ownership_nonce" = $3
2693
+ RETURNING "singleton"`, [
2694
+ JSON.stringify(manifest),
2695
+ installationId,
2696
+ ownershipNonce
2697
+ ])).rows.length !== 1) throw new Error("platform: The PostgreSQL database ownership claim changed unexpectedly.");
2698
+ release.authoritativeManifest = manifest;
2699
+ };
2700
+ release.prepareDatabase = (runtimeDirectory, database, sourcePlatformVersion, hasPriorWorkers) => preparePlatformDatabaseConnection(client, runtimeDirectory, database, sourcePlatformVersion, hasPriorWorkers);
2701
+ release.prepareSandboxDrain = (requiredAdmissionProtocol) => preparePlatformSandboxDrainConnection(client, requiredAdmissionProtocol);
2702
+ release.verifyDatabase = (runtimeDirectory, database) => verifyPlatformDatabaseConnection(client, runtimeDirectory, database);
2703
+ return release;
2704
+ }
2705
+ async function preparePlatformSandboxDrainConnection(client, requiredAdmissionProtocol = 0) {
2706
+ 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.");
2707
+ const hasAdmissionColumn = (await client.query(`SELECT EXISTS(
2708
+ SELECT 1
2709
+ FROM information_schema.columns
2710
+ WHERE table_schema = 'public'
2711
+ AND table_name = 'sandbox_drain_checkpoints'
2712
+ AND column_name = 'admission_protocol'
2713
+ ) AS "present"`)).rows[0]?.present === true;
2714
+ if (requiredAdmissionProtocol === 2 && !hasAdmissionColumn) throw new Error("platform: The runtime database does not include the sandbox deployment admission barrier.");
2715
+ const readCheckpoint = () => client.query(`SELECT ${hasAdmissionColumn ? "\"admission_protocol\"" : "0 AS \"admission_protocol\""}, "generation", "state", "phase", "cursor", "started_at", "updated_at", "completed_at"
2716
+ FROM "sandbox_drain_checkpoints"
2717
+ WHERE "singleton" = true`);
2718
+ const classify = (row) => {
2719
+ if (!row) return null;
2720
+ const cursor = row.cursor;
2721
+ if (!row.generation || row.admission_protocol !== 0 && row.admission_protocol !== 2 || !row.started_at || !row.updated_at || row.state !== "draining" && row.state !== "complete" || row.phase !== "deployments" && row.phase !== "resources" || !cursor || cursor.pass !== "1" && cursor.pass !== "2" || row.state === "complete" && (!row.completed_at || row.phase !== "resources" || cursor.pass !== "2") || row.state === "draining" && row.completed_at !== null) throw new Error("platform: The authoritative sandbox-drain database checkpoint is invalid. Traffic remains disabled.");
2722
+ return {
2723
+ admissionProtocol: row.admission_protocol,
2724
+ generation: row.generation,
2725
+ mirrorValue: JSON.stringify({
2726
+ version: 1,
2727
+ generation: row.generation,
2728
+ state: row.state,
2729
+ startedAt: row.started_at,
2730
+ updatedAt: row.updated_at,
2731
+ ...row.completed_at ? { completedAt: row.completed_at } : {}
2732
+ }),
2733
+ state: row.state === "complete" ? "complete" : "pending"
2734
+ };
2735
+ };
2736
+ const existingState = classify((await readCheckpoint()).rows[0]);
2737
+ if (existingState && (requiredAdmissionProtocol === 0 || existingState.admissionProtocol >= requiredAdmissionProtocol)) return existingState;
2738
+ if (requiredAdmissionProtocol === 2) {
2739
+ const generation = randomUUID();
2740
+ await client.query("BEGIN");
2741
+ try {
2742
+ await client.query("LOCK TABLE \"deployments\" IN SHARE MODE");
2743
+ await client.query(`INSERT INTO "sandbox_drain_checkpoints" (
2744
+ "singleton", "admission_protocol", "generation", "state", "phase", "cursor"
2745
+ ) VALUES (true, 2, $1, 'draining', 'deployments', '{"pass":"1"}'::jsonb)
2746
+ ON CONFLICT ("singleton") DO UPDATE
2747
+ SET "admission_protocol" = 2,
2748
+ "generation" = EXCLUDED."generation",
2749
+ "state" = 'draining',
2750
+ "phase" = 'deployments',
2751
+ "cursor" = '{"pass":"1"}'::jsonb,
2752
+ "lease_owner" = NULL,
2753
+ "lease_expires_at" = NULL,
2754
+ "started_at" = now(),
2755
+ "updated_at" = now(),
2756
+ "completed_at" = NULL`, [generation]);
2757
+ if ((await client.query(`SELECT
2758
+ NOT EXISTS(SELECT 1 FROM "deployments" LIMIT 1)
2759
+ AND NOT EXISTS(SELECT 1 FROM "resources" WHERE "type" = 'container' LIMIT 1)
2760
+ AS "empty"`)).rows[0]?.empty === true) await client.query(`UPDATE "sandbox_drain_checkpoints"
2761
+ SET "state" = 'complete',
2762
+ "phase" = 'resources',
2763
+ "cursor" = '{"pass":"2"}'::jsonb,
2764
+ "lease_owner" = NULL,
2765
+ "lease_expires_at" = NULL,
2766
+ "updated_at" = now(),
2767
+ "completed_at" = now()
2768
+ WHERE "singleton" = true
2769
+ AND "admission_protocol" = 2
2770
+ AND "generation" = $1`, [generation]);
2771
+ const armed = classify((await readCheckpoint()).rows[0]);
2772
+ if (!armed || armed.admissionProtocol !== 2) throw new Error("platform: The sandbox deployment admission barrier could not be armed.");
2773
+ await client.query("COMMIT");
2774
+ return armed;
2775
+ } catch (error) {
2776
+ await client.query("ROLLBACK").catch(() => void 0);
2777
+ throw error;
2778
+ }
2779
+ }
2780
+ const empty = (await client.query(`SELECT
2781
+ NOT EXISTS(SELECT 1 FROM "deployments" LIMIT 1)
2782
+ AND NOT EXISTS(SELECT 1 FROM "resources" WHERE "type" = 'container' LIMIT 1)
2783
+ AS "empty"`)).rows[0]?.empty === true;
2784
+ const generation = randomUUID();
2785
+ const state = empty ? "complete" : "draining";
2786
+ const phase = empty ? "resources" : "deployments";
2787
+ const cursor = JSON.stringify({ pass: empty ? "2" : "1" });
2788
+ await client.query(`INSERT INTO "sandbox_drain_checkpoints" (
2789
+ "singleton", "generation", "state", "phase", "cursor", "completed_at"
2790
+ ) VALUES (true, $1, $2, $3, $4::jsonb, CASE WHEN $2 = 'complete' THEN now() ELSE NULL END)
2791
+ ON CONFLICT ("singleton") DO NOTHING`, [
2792
+ generation,
2793
+ state,
2794
+ phase,
2795
+ cursor
2796
+ ]);
2797
+ const preparedState = classify((await readCheckpoint()).rows[0]);
2798
+ if (!preparedState) throw new Error("platform: The sandbox-drain database checkpoint could not be established.");
2799
+ return preparedState;
2800
+ }
2801
+ async function verifyPlatformDatabaseConnection(client, runtimeDirectory, database) {
2802
+ verifyPlatformDatabaseArtifacts(runtimeDirectory, database);
2803
+ const state = await inspectPlatformDatabaseStateConnection(client, database);
2804
+ 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.");
2805
+ }
2806
+ async function readRequiredPlatformSafetyProtocols(client) {
2807
+ if (!(await client.query(`SELECT to_regclass('"sandbox_drain_checkpoints"')::text AS "relation"`)).rows[0]?.relation) return { sandboxDrain: 0 };
2808
+ if ((await client.query(`SELECT EXISTS(
2809
+ SELECT 1
2810
+ FROM information_schema.columns
2811
+ WHERE table_schema = 'public'
2812
+ AND table_name = 'sandbox_drain_checkpoints'
2813
+ AND column_name = 'admission_protocol'
2814
+ ) AS "present"`)).rows[0]?.present === true) {
2815
+ if ((await client.query(`SELECT EXISTS(
2816
+ SELECT 1
2817
+ FROM "sandbox_drain_checkpoints"
2818
+ WHERE "singleton" = true
2819
+ AND "admission_protocol" >= 2
2820
+ ) AS "required"`)).rows[0]?.required === true) return { sandboxDrain: 2 };
2821
+ }
2822
+ return { sandboxDrain: (await client.query(`SELECT EXISTS(
2823
+ SELECT 1
2824
+ FROM "sandbox_drain_checkpoints"
2825
+ WHERE "singleton" = true
2826
+ ) AS "required"`)).rows[0]?.required === true ? 1 : 0 };
2827
+ }
2828
+ function assertPlatformRuntimeSafety(runtime, installed, database = { sandboxDrain: 0 }) {
2829
+ const requiredSandboxDrain = Math.max(installed?.sandboxDrain ?? 0, database?.sandboxDrain ?? 0);
2830
+ 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.`);
2831
+ }
2832
+ async function claimPlatformDatabaseConnection(client, installationId, ownershipNonce) {
2833
+ await client.query("BEGIN");
2834
+ try {
2835
+ await client.query(`SELECT pg_advisory_xact_lock(hashtext('void-platform-installation-ownership-v1'))`);
2836
+ await client.query(`
2837
+ CREATE TABLE IF NOT EXISTS "__void_platform_installation" (
2838
+ "singleton" boolean PRIMARY KEY DEFAULT true CHECK ("singleton"),
2839
+ "installation_id" text NOT NULL,
2840
+ "ownership_nonce" text NOT NULL,
2841
+ "claimed_at" timestamptz NOT NULL DEFAULT now(),
2842
+ "lifecycle_manifest" jsonb
2843
+ )
2844
+ `);
2845
+ await client.query(`ALTER TABLE "__void_platform_installation"
2846
+ ADD COLUMN IF NOT EXISTS "lifecycle_manifest" jsonb`);
2847
+ const claim = (await client.query(`SELECT "installation_id", "ownership_nonce"
2848
+ FROM "__void_platform_installation"
2849
+ WHERE "singleton" = true
2850
+ FOR UPDATE`)).rows[0];
2851
+ if (!claim) {
2852
+ const contents = await client.query(`SELECT "schemaname", "tablename"
2853
+ FROM "pg_catalog"."pg_tables"
2854
+ WHERE "schemaname" = ANY (current_schemas(false))
2855
+ AND "tablename" <> '__void_platform_installation'
2856
+ ORDER BY "schemaname", "tablename"
2857
+ LIMIT 1`);
2858
+ if (contents.rows.length > 0) {
2859
+ const table = contents.rows[0];
2860
+ throw new Error(`platform: Refusing to claim a non-empty PostgreSQL database (found ${table.schemaname}.${table.tablename}). Use a dedicated empty database for each installation.`);
2861
+ }
2862
+ await client.query(`INSERT INTO "__void_platform_installation"
2863
+ ("singleton", "installation_id", "ownership_nonce")
2864
+ VALUES (true, $1, $2)`, [installationId, ownershipNonce]);
2865
+ } else if (claim.installation_id !== installationId || claim.ownership_nonce !== ownershipNonce) throw new Error(`platform: This PostgreSQL database is already claimed by Void platform installation ${claim.installation_id}. Use a dedicated empty database for each installation.`);
2866
+ await client.query("COMMIT");
2867
+ } catch (error) {
2868
+ await client.query("ROLLBACK").catch(() => ({ rows: [] }));
2869
+ throw error;
2870
+ }
2871
+ }
2872
+ async function deployPlatformWorker(config, secrets, options) {
2873
+ const directory = mkdtempSync(join(tmpdir(), "void-platform-deploy-"));
2874
+ const workerName = String(config.name);
2875
+ const accountId = String(config.account_id);
2876
+ let activatedVersionId = null;
2877
+ let activatedDeploymentId = null;
2878
+ let uploadedVersionId = null;
2879
+ let previousDeployment = null;
2880
+ let previousDeploymentId = null;
2881
+ let previousVersionId = null;
2882
+ const nextTraffic = isolatePlatformWorkerCustomDomains(config);
2883
+ const previousTraffic = isolatePlatformWorkerCustomDomains(options.previousConfig);
2884
+ let uploadConfig = withoutPlatformWorkerTrafficAttachments(nextTraffic.wranglerConfig);
2885
+ const nextTriggers = readTriggerConfigFromObject(nextTraffic.wranglerConfig);
2886
+ let previousTriggers = readTriggerConfigFromObject(previousTraffic.wranglerConfig);
2887
+ try {
2888
+ const configPath = join(directory, "wrangler.json");
2889
+ writeFileSync(configPath, `${JSON.stringify(uploadConfig, null, 2)}\n`, { mode: 384 });
2890
+ if (options.workerExisted) {
2891
+ const previous = await readActivePlatformWorkerSnapshot(configPath, accountId, workerName, readPlatformWorkerOwnershipMarker(config), options.runner);
2892
+ previousVersionId = previous.versionId;
2893
+ const deployments = await readPlatformWorkerDeployments(configPath, accountId, workerName, options.runner);
2894
+ if (!deploymentRunsOnlyVersion(deployments[0], previousVersionId)) throw new Error(`platform: Worker ${workerName} changed concurrently while its deployment lineage was inspected.`);
2895
+ previousDeployment = deployments[0];
2896
+ previousDeploymentId = previousDeployment.id;
2897
+ uploadConfig = withoutCompletedPlatformWorkerMigrations(uploadConfig, previous.migrationTag);
2898
+ writeFileSync(configPath, `${JSON.stringify(uploadConfig, null, 2)}\n`, { mode: 384 });
2899
+ previousTriggers = resolvePlatformPreviousTriggerConfig(withoutPlatformCustomDomainTriggers(previous.triggerConfig), previousTriggers, options.previousPublicEndpoints);
2900
+ previousTriggers = withoutPlatformCustomDomainTriggers(mergePreservedTriggerRoutes(previousTriggers, options.preservedRoutes));
2901
+ }
2902
+ const argv = [
2903
+ ...options.workerExisted ? ["versions", "upload"] : ["deploy"],
2904
+ "--config",
2905
+ configPath,
2906
+ "--strict",
2907
+ "--experimental-provision=false",
2908
+ "--experimental-auto-create=false",
2909
+ ...!options.workerExisted ? ["--autoconfig=false"] : [],
2910
+ "--message",
2911
+ encodeCloudflareVersionMessage([], nextTriggers)
2912
+ ];
2913
+ if (secrets) {
2914
+ const secretsPath = join(directory, "secrets.json");
2915
+ writeFileSync(secretsPath, JSON.stringify(secrets), { mode: 384 });
2916
+ argv.push("--secrets-file", secretsPath);
2917
+ }
2918
+ const uploaded = options.workerExisted ? await options.runner(argv, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(argv, accountId, directory, true, workerName, options.apiBaseUrl);
2919
+ uploadedVersionId = parseUploadedPlatformWorkerVersion(`${uploaded.stdout}\n${uploaded.stderr}`);
2920
+ if (!options.workerExisted) activatedVersionId = uploadedVersionId;
2921
+ assertCleanPlatformWrangler(uploaded, `${options.workerExisted ? "upload" : "bootstrap"} ${workerName}`);
2922
+ if (!uploadedVersionId) throw new Error(`platform: Wrangler uploaded ${workerName} without reporting its immutable version id.`);
2923
+ await options.onStaged(previousVersionId, uploadedVersionId, previousTriggers);
2924
+ if (options.workerExisted) {
2925
+ const deploymentsBeforeActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName, options.runner);
2926
+ if (deploymentsBeforeActivation[0]?.id !== previousDeploymentId || !deploymentRunsOnlyVersion(deploymentsBeforeActivation[0], previousVersionId)) throw new Error(`platform: Worker ${workerName} changed concurrently before activation; refusing to overwrite it.`);
2927
+ activatedVersionId = uploadedVersionId;
2928
+ assertCleanPlatformWrangler(await options.runner([
2929
+ "versions",
2930
+ "deploy",
2931
+ `${uploadedVersionId}@100`,
2932
+ "--config",
2933
+ configPath,
2934
+ "--message",
2935
+ `Void platform activation ${uploadedVersionId}`,
2936
+ "--yes"
2937
+ ], accountId, directory, true), `activate ${workerName}`);
2938
+ }
2939
+ const deploymentsAfterActivation = await readPlatformWorkerDeployments(configPath, accountId, workerName, options.runner);
2940
+ const lineage = classifyPlatformWorkerDeploymentLineage(deploymentsAfterActivation, uploadedVersionId, previousDeploymentId);
2941
+ if (lineage.kind === "concurrent-after") {
2942
+ activatedVersionId = null;
2943
+ throw new Error(`platform: Worker ${workerName} changed concurrently after activation; refusing to overwrite the newer deployment.`);
2944
+ }
2945
+ activatedDeploymentId = lineage.deploymentId;
2946
+ if (lineage.kind === "intervening") {
2947
+ const interveningDeployment = deploymentsAfterActivation[1];
2948
+ activatedVersionId = null;
2949
+ activatedDeploymentId = null;
2950
+ if (!interveningDeployment) throw new PlatformWorkerRecoveryIncompleteError(`platform: Worker ${workerName} lost its previous deployment during activation. The cutover cannot be recovered automatically.`);
2951
+ try {
2952
+ await restorePlatformWorkerDeployment({
2953
+ accountId,
2954
+ configPath,
2955
+ directory,
2956
+ expectedActiveDeploymentId: lineage.deploymentId,
2957
+ expectedActiveVersionId: uploadedVersionId,
2958
+ restoration: interveningDeployment,
2959
+ runner: options.runner,
2960
+ workerName
2961
+ });
2962
+ } catch (recoveryError) {
2963
+ const recoveryFailures = await recoverFailedPlatformWorkerDeployment({
2964
+ checkpointUnsafeTraffic: options.onUnsafeTraffic,
2965
+ closeRoutingGate: options.retainNewWorker,
2966
+ rollback: null,
2967
+ unsafeTraffic: true
2968
+ });
2969
+ 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 });
2970
+ }
2971
+ throw new Error(`platform: Worker ${workerName} changed concurrently during activation. The intervening deployment was restored without deleting deployment history.`);
2972
+ }
2973
+ assertPlatformTriggerStabilization(await stabilizeCloudflareVersionTriggers({
2974
+ configPath,
2975
+ knownTriggerConfigs: {
2976
+ [uploadedVersionId]: nextTriggers,
2977
+ ...previousVersionId ? { [previousVersionId]: previousTriggers } : {}
2978
+ },
2979
+ manageRoutes: false,
2980
+ previousTriggerConfig: previousTriggers,
2981
+ publishEmptyCustomDomains: async () => {
2982
+ await options.removeCustomDomains();
2983
+ return cleanPlatformCommandResult();
2984
+ },
2985
+ runWrangler: platformWorkerCommandRunner(accountId, directory, options.runner),
2986
+ targetTriggerConfig: nextTriggers,
2987
+ targetVersionId: uploadedVersionId,
2988
+ workerName
2989
+ }), workerName, uploadedVersionId, "publish");
2990
+ if (nextTraffic.customDomains.length > 0) await options.publishCustomDomains(workerName, nextTraffic.customDomains);
2991
+ if (options.managedRoutes.length > 0) await options.publishRoutes(workerName, options.managedRoutes);
2992
+ await options.onTrafficPublished();
2993
+ const rollback = () => rollbackPlatformWorker({
2994
+ accountId,
2995
+ activatedDeploymentId,
2996
+ activatedVersionId: uploadedVersionId,
2997
+ config,
2998
+ nextTriggers,
2999
+ previousConfig: options.previousConfig,
3000
+ previousCustomDomains: previousTraffic.customDomains,
3001
+ previousDeployment,
3002
+ previousTriggers,
3003
+ previousVersionId,
3004
+ preserveCustomDomains: false,
3005
+ inspectNewWorker: options.inspectNewWorker,
3006
+ removeCustomDomains: options.removeCustomDomains,
3007
+ publishCustomDomains: options.publishCustomDomains,
3008
+ retainNewWorker: options.retainNewWorker,
3009
+ runner: options.runner,
3010
+ workerExisted: options.workerExisted,
3011
+ workerName
3012
+ });
3013
+ return rollback;
3014
+ } catch (error) {
3015
+ const unsafeTraffic = error instanceof PlatformWorkerCustomDomainsChangedError || error instanceof PlatformWorkerRoutesChangedError;
3016
+ const rollbackVersionId = activatedVersionId && uploadedVersionId ? uploadedVersionId : null;
3017
+ const recoveryFailures = await recoverFailedPlatformWorkerDeployment({
3018
+ checkpointUnsafeTraffic: options.onUnsafeTraffic,
3019
+ closeRoutingGate: options.retainNewWorker,
3020
+ rollback: rollbackVersionId ? () => rollbackPlatformWorker({
3021
+ accountId,
3022
+ activatedDeploymentId,
3023
+ activatedVersionId: rollbackVersionId,
3024
+ config,
3025
+ nextTriggers: readTriggerConfigFromObject(nextTraffic.wranglerConfig),
3026
+ previousConfig: options.previousConfig,
3027
+ previousCustomDomains: previousTraffic.customDomains,
3028
+ previousDeployment,
3029
+ previousTriggers,
3030
+ previousVersionId,
3031
+ preserveCustomDomains: unsafeTraffic,
3032
+ inspectNewWorker: options.inspectNewWorker,
3033
+ removeCustomDomains: options.removeCustomDomains,
3034
+ publishCustomDomains: options.publishCustomDomains,
3035
+ retainNewWorker: options.retainNewWorker,
3036
+ runner: options.runner,
3037
+ workerExisted: options.workerExisted,
3038
+ workerName
3039
+ }) : null,
3040
+ unsafeTraffic
3041
+ });
3042
+ if (recoveryFailures.length > 0) throw new PlatformWorkerRecoveryIncompleteError(`platform: ${errorMessage(error)} Automatic recovery for ${workerName} was incomplete: ${recoveryFailures.join("; ")}`, { cause: error });
3043
+ 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 });
3044
+ throw error;
3045
+ } finally {
3046
+ rmSync(directory, {
3047
+ force: true,
3048
+ recursive: true
3049
+ });
3050
+ }
3051
+ }
3052
+ async function recoverFailedPlatformWorkerDeployment(input) {
3053
+ const failures = [];
3054
+ const attempt = async (label, operation) => {
3055
+ try {
3056
+ await operation();
3057
+ } catch (error) {
3058
+ failures.push(`${label}: ${errorMessage(error)}`);
3059
+ }
3060
+ };
3061
+ if (input.unsafeTraffic) {
3062
+ await attempt("closing the routing gate failed", input.closeRoutingGate);
3063
+ await attempt("checkpointing unsafe traffic failed", input.checkpointUnsafeTraffic);
3064
+ }
3065
+ if (input.rollback) await attempt("Worker rollback failed", input.rollback);
3066
+ return failures;
3067
+ }
3068
+ async function rollbackPlatformWorker(input) {
3069
+ const directory = mkdtempSync(join(tmpdir(), "void-platform-rollback-"));
3070
+ try {
3071
+ const configPath = join(directory, "wrangler.json");
3072
+ const currentTraffic = isolatePlatformWorkerCustomDomains(input.config);
3073
+ writeFileSync(configPath, `${JSON.stringify(currentTraffic.wranglerConfig, null, 2)}\n`, { mode: 384 });
3074
+ if (!input.workerExisted) {
3075
+ if (await input.inspectNewWorker() === "missing") return;
3076
+ const currentVersionId = await readActivePlatformWorkerVersion(configPath, input.accountId, input.workerName, input.runner);
3077
+ 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.`);
3078
+ if (await input.inspectNewWorker() === "missing") return;
3079
+ await input.retainNewWorker();
3080
+ if (!input.preserveCustomDomains) await input.removeCustomDomains();
3081
+ return;
3082
+ }
3083
+ const currentVersionId = await readActivePlatformWorkerVersion(configPath, input.accountId, input.workerName, input.runner);
3084
+ if (currentVersionId !== input.previousVersionId && currentVersionId !== input.activatedVersionId) throw new Error(`Worker ${input.workerName} changed concurrently from installer version ${input.activatedVersionId} to ${currentVersionId ?? "no active version"}; refusing to overwrite it.`);
3085
+ if (!input.previousVersionId) throw new Error(`Worker ${input.workerName} has no previous version to restore.`);
3086
+ if (currentVersionId === input.activatedVersionId) {
3087
+ const currentDeployments = await readPlatformWorkerDeployments(configPath, input.accountId, input.workerName, input.runner);
3088
+ const activeDeploymentId = input.activatedDeploymentId ?? currentDeployments[0]?.id ?? null;
3089
+ const restoration = input.previousDeployment ?? currentDeployments[1];
3090
+ if (!activeDeploymentId || currentDeployments[0]?.id !== activeDeploymentId || !deploymentRunsOnlyVersion(currentDeployments[0], input.activatedVersionId) || !restoration || currentDeployments[1]?.id !== restoration.id || !deploymentRunsOnlyVersion(restoration, input.previousVersionId)) throw new Error(`Worker ${input.workerName} changed concurrently after installer deployment; refusing to alter its deployment history.`);
3091
+ await restorePlatformWorkerDeployment({
3092
+ accountId: input.accountId,
3093
+ configPath,
3094
+ directory,
3095
+ expectedActiveDeploymentId: activeDeploymentId,
3096
+ expectedActiveVersionId: input.activatedVersionId,
3097
+ restoration,
3098
+ runner: input.runner,
3099
+ workerName: input.workerName
3100
+ });
3101
+ if (await readActivePlatformWorkerVersion(configPath, input.accountId, input.workerName, input.runner) !== input.previousVersionId) throw new Error(`Worker ${input.workerName} did not restore immutable version ${input.previousVersionId} after rolling back installer deployment ${activeDeploymentId}.`);
3102
+ }
3103
+ const previousConfigPath = join(directory, "wrangler.previous.json");
3104
+ writeFileSync(previousConfigPath, `${JSON.stringify(input.previousConfig, null, 2)}\n`, { mode: 384 });
3105
+ assertPlatformTriggerStabilization(await stabilizeCloudflareVersionTriggers({
3106
+ configPath: previousConfigPath,
3107
+ knownTriggerConfigs: {
3108
+ [input.activatedVersionId]: input.nextTriggers,
3109
+ [input.previousVersionId]: input.previousTriggers
3110
+ },
3111
+ manageRoutes: false,
3112
+ previousTriggerConfig: input.nextTriggers,
3113
+ publishEmptyCustomDomains: async () => {
3114
+ await input.removeCustomDomains();
3115
+ return cleanPlatformCommandResult();
3116
+ },
3117
+ runWrangler: platformWorkerCommandRunner(input.accountId, directory, input.runner),
3118
+ targetTriggerConfig: input.previousTriggers,
3119
+ targetVersionId: input.previousVersionId,
3120
+ workerName: input.workerName
3121
+ }), input.workerName, input.previousVersionId, "restore");
3122
+ if (!input.preserveCustomDomains && input.previousCustomDomains.length > 0) await input.publishCustomDomains(input.workerName, input.previousCustomDomains);
3123
+ } finally {
3124
+ rmSync(directory, {
3125
+ force: true,
3126
+ recursive: true
3127
+ });
3128
+ }
3129
+ }
3130
+ function platformWorkerCommandRunner(accountId, cwd, runner) {
3131
+ return (args, options) => runner(args, accountId, cwd, options.capture);
3132
+ }
3133
+ async function runPlatformWrangler(args, accountId, cwd, capture) {
3134
+ return runPlatformWranglerAtApiBase(args, accountId, cwd, capture);
3135
+ }
3136
+ const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
3137
+ async function runPlatformWranglerAtApiBase(args, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl()) {
3138
+ const result = await createDefaultRunWrangler(process.cwd(), {
3139
+ launcher: resolveWranglerArgv(process.cwd()),
3140
+ timeoutMs: PLATFORM_WRANGLER_TIMEOUT_MS
3141
+ })([...args], {
3142
+ accountId,
3143
+ apiBaseUrl,
3144
+ captureJson: capture,
3145
+ cwd
3146
+ });
3147
+ const stdout = result.stdout || "";
3148
+ const stderr = result.stderr || "";
3149
+ if (!capture && result.status !== 0) {
3150
+ if (stdout) process.stdout.write(stdout);
3151
+ if (stderr) process.stderr.write(stderr);
3152
+ }
3153
+ return {
3154
+ status: result.status,
3155
+ stderr,
3156
+ stdout
3157
+ };
3158
+ }
3159
+ const HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
3160
+ "connection",
3161
+ "keep-alive",
3162
+ "proxy-authenticate",
3163
+ "proxy-authorization",
3164
+ "te",
3165
+ "trailer",
3166
+ "transfer-encoding",
3167
+ "upgrade"
3168
+ ]);
3169
+ function forwardHeaders(headers) {
3170
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => !HOP_BY_HOP_HEADERS.has(name.toLowerCase())));
3171
+ }
3172
+ function closeHttpServer(server) {
3173
+ return new Promise((resolvePromise, rejectPromise) => {
3174
+ server.close((error) => {
3175
+ if (error) rejectPromise(error);
3176
+ else resolvePromise();
3177
+ });
3178
+ server.closeAllConnections();
3179
+ });
3180
+ }
3181
+ async function startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName) {
3182
+ const upstreamBase = new URL(upstreamApiBaseUrl);
3183
+ if (upstreamBase.protocol !== "http:" && upstreamBase.protocol !== "https:") throw new Error("platform: Cloudflare API base URL must use HTTP or HTTPS.");
3184
+ const basePath = upstreamBase.pathname.replace(/\/+$/, "");
3185
+ const expectedPath = `${basePath}/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}`;
3186
+ const state = {
3187
+ attempted: false,
3188
+ conflictStatus: null,
3189
+ created: false,
3190
+ forwarded: false,
3191
+ rejectedMutation: null
3192
+ };
3193
+ const server = createServer((incoming, outgoing) => {
3194
+ (async () => {
3195
+ const incomingUrl = new URL(incoming.url ?? "/", "http://127.0.0.1");
3196
+ if (incomingUrl.pathname !== basePath && !incomingUrl.pathname.startsWith(`${basePath}/`)) {
3197
+ outgoing.writeHead(502, { "Content-Type": "application/json" });
3198
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Invalid API proxy path\"}]}");
3199
+ return;
3200
+ }
3201
+ const createQuery = [...incomingUrl.searchParams];
3202
+ const supportedCreateQuery = new Set(createQuery.map(([key]) => key)).size === createQuery.length && createQuery.every(([key, value]) => key === "excludeScript" && value === "true" || key === "bindings_inherit" && value === "strict");
3203
+ const isConditionalCreate = incoming.method === "PUT" && incomingUrl.pathname === expectedPath && supportedCreateQuery;
3204
+ const isTopLevelWorkerUpload = incoming.method === "PUT" && new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/accounts/[^/]+/workers/scripts/[^/]+$`).test(incomingUrl.pathname);
3205
+ const isReadOnly = incoming.method === "GET" || incoming.method === "HEAD" || incoming.method === "OPTIONS";
3206
+ if (isTopLevelWorkerUpload && !isConditionalCreate || !isReadOnly && !isConditionalCreate && !state.created) {
3207
+ state.rejectedMutation = `${incoming.method ?? "UNKNOWN"} ${incomingUrl.pathname}${incomingUrl.pathname === expectedPath && !supportedCreateQuery ? " (unsupported Worker create query)" : ""}`;
3208
+ incoming.resume();
3209
+ outgoing.writeHead(409, { "Content-Type": "application/json" });
3210
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Mutation blocked until conditional Worker creation succeeds\"}]}");
3211
+ return;
3212
+ }
3213
+ const headers = forwardHeaders(incoming.headers);
3214
+ delete headers.host;
3215
+ if (isConditionalCreate) {
3216
+ state.attempted = true;
3217
+ headers["if-none-match"] = "*";
3218
+ }
3219
+ const upstreamUrl = new URL(`${incomingUrl.pathname}${incomingUrl.search}`, upstreamBase.origin);
3220
+ const request$2 = (upstreamUrl.protocol === "https:" ? request$1 : request)(upstreamUrl, {
3221
+ headers,
3222
+ method: incoming.method
3223
+ }, (response) => {
3224
+ if (isConditionalCreate) {
3225
+ state.forwarded = true;
3226
+ if (response.statusCode === 409 || response.statusCode === 412) state.conflictStatus = response.statusCode;
3227
+ state.created = response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300;
3228
+ }
3229
+ const responseHeaders = forwardHeaders(response.headers);
3230
+ outgoing.writeHead(response.statusCode ?? 502, responseHeaders);
3231
+ response.pipe(outgoing);
3232
+ });
3233
+ request$2.on("error", () => {
3234
+ if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
3235
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy request failed\"}]}");
3236
+ });
3237
+ incoming.pipe(request$2);
3238
+ })().catch(() => {
3239
+ if (!outgoing.headersSent) outgoing.writeHead(502, { "Content-Type": "application/json" });
3240
+ outgoing.end("{\"success\":false,\"errors\":[{\"message\":\"Cloudflare API proxy failed\"}]}");
3241
+ });
3242
+ });
3243
+ await new Promise((resolvePromise, rejectPromise) => {
3244
+ server.once("error", rejectPromise);
3245
+ server.listen(0, "127.0.0.1", () => {
3246
+ server.off("error", rejectPromise);
3247
+ resolvePromise();
3248
+ });
3249
+ });
3250
+ return {
3251
+ apiBaseUrl: `http://127.0.0.1:${server.address().port}${basePath}`,
3252
+ close: () => closeHttpServer(server),
3253
+ state
3254
+ };
3255
+ }
3256
+ async function withConditionalPlatformWorkerCreate(accountId, workerName, operation, upstreamApiBaseUrl = resolvePinnedApiBaseUrl()) {
3257
+ const proxy = await startConditionalWorkerCreateProxy(upstreamApiBaseUrl, accountId, workerName);
3258
+ let result;
3259
+ try {
3260
+ result = await operation(proxy.apiBaseUrl);
3261
+ } finally {
3262
+ await proxy.close();
3263
+ }
3264
+ 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.`);
3265
+ 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.`);
3266
+ 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.`);
3267
+ return result;
3268
+ }
3269
+ async function runPlatformWranglerForConditionalCreate(args, accountId, cwd, capture, workerName, upstreamApiBaseUrl) {
3270
+ return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(args, accountId, cwd, capture, apiBaseUrl), upstreamApiBaseUrl);
3271
+ }
3272
+ async function readActivePlatformWorkerVersion(configPath, accountId, workerName, runner = runPlatformWrangler) {
3273
+ const result = await runner([
3274
+ "deployments",
3275
+ "status",
3276
+ "--config",
3277
+ configPath,
3278
+ "--name",
3279
+ workerName,
3280
+ "--json"
3281
+ ], accountId, process.cwd(), true);
3282
+ if (result.status !== 0) assertCleanPlatformWrangler(result, `read active version for ${workerName}`);
3283
+ let value;
3284
+ try {
3285
+ value = JSON.parse(result.stdout);
3286
+ } catch {
3287
+ throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
3288
+ }
3289
+ if (!value || typeof value !== "object" || !Array.isArray(value.versions)) throw new Error(`platform: Wrangler returned malformed deployment status for ${workerName}.`);
3290
+ const active = value.versions.filter((entry) => entry && typeof entry === "object" && Number(entry.percentage) === 100);
3291
+ if (active.length !== 1 || typeof active[0].version_id !== "string") return null;
3292
+ return active[0].version_id;
3293
+ }
3294
+ async function readPlatformWorkerDeployments(configPath, accountId, workerName, runner = runPlatformWrangler) {
3295
+ const result = await runner([
3296
+ "deployments",
3297
+ "list",
3298
+ "--config",
3299
+ configPath,
3300
+ "--name",
3301
+ workerName,
3302
+ "--json"
3303
+ ], accountId, process.cwd(), true);
3304
+ if (result.status !== 0) assertCleanPlatformWrangler(result, `read deployment lineage for ${workerName}`);
3305
+ let value;
3306
+ try {
3307
+ value = JSON.parse(result.stdout);
3308
+ } catch {
3309
+ throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3310
+ }
3311
+ if (!Array.isArray(value)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3312
+ return value.map((entry, index) => {
3313
+ 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}.`);
3314
+ const createdAt = Date.parse(entry.created_on);
3315
+ if (!Number.isFinite(createdAt)) throw new Error(`platform: Wrangler returned malformed deployment history for ${workerName}.`);
3316
+ const versions = entry.versions.map((version) => {
3317
+ 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}.`);
3318
+ return {
3319
+ percentage: Number(version.percentage),
3320
+ versionId: version.version_id
3321
+ };
3322
+ });
3323
+ return {
3324
+ createdAt,
3325
+ id: entry.id,
3326
+ index,
3327
+ versions
3328
+ };
3329
+ }).sort((left, right) => right.createdAt - left.createdAt || left.index - right.index).map(({ id, versions }) => ({
3330
+ id,
3331
+ versions
3332
+ }));
3333
+ }
3334
+ function samePlatformWorkerTraffic(left, right) {
3335
+ if (!left) return false;
3336
+ const normalize = (deployment) => deployment.versions.filter((version) => version.percentage > 0).map((version) => `${version.versionId}@${version.percentage}`).sort();
3337
+ return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
3338
+ }
3339
+ async function restorePlatformWorkerDeployment(input) {
3340
+ const restorationVersions = input.restoration.versions.filter((version) => version.percentage > 0);
3341
+ const totalPercentage = restorationVersions.reduce((total, version) => total + version.percentage, 0);
3342
+ if (restorationVersions.length === 0 || restorationVersions.length > 2 || restorationVersions.some((version, index) => version.percentage > 100 || restorationVersions.findIndex((entry) => entry.versionId === version.versionId) !== index) || Math.abs(totalPercentage - 100) > .001) throw new Error(`Worker ${input.workerName} deployment ${input.restoration.id} has an unsupported traffic distribution.`);
3343
+ const before = await readPlatformWorkerDeployments(input.configPath, input.accountId, input.workerName, input.runner);
3344
+ if (before[0]?.id !== input.expectedActiveDeploymentId || !deploymentRunsOnlyVersion(before[0], input.expectedActiveVersionId) || before[1]?.id !== input.restoration.id || !samePlatformWorkerTraffic(before[1], input.restoration)) throw new Error(`Worker ${input.workerName} changed concurrently before deployment restoration; refusing to overwrite the newer deployment.`);
3345
+ assertCleanPlatformWrangler(await input.runner([
3346
+ "versions",
3347
+ "deploy",
3348
+ ...restorationVersions.map((version) => `${version.versionId}@${version.percentage}`),
3349
+ "--config",
3350
+ input.configPath,
3351
+ "--message",
3352
+ `Void platform restoration ${input.restoration.id}`,
3353
+ "--yes"
3354
+ ], input.accountId, input.directory, true), `restore deployment ${input.restoration.id}`);
3355
+ const after = await readPlatformWorkerDeployments(input.configPath, input.accountId, input.workerName, input.runner);
3356
+ 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}.`);
3357
+ }
3358
+ function deploymentRunsOnlyVersion(deployment, versionId) {
3359
+ return versionId !== null && deployment?.versions.length === 1 && deployment.versions[0]?.versionId === versionId && deployment.versions[0].percentage === 100;
3360
+ }
3361
+ function classifyPlatformWorkerDeploymentLineage(deployments, installedVersionId, previousDeploymentId) {
3362
+ const installed = deployments[0];
3363
+ if (!deploymentRunsOnlyVersion(installed, installedVersionId)) return { kind: "concurrent-after" };
3364
+ return (deployments[1]?.id ?? null) === previousDeploymentId ? {
3365
+ deploymentId: installed.id,
3366
+ kind: "committed"
3367
+ } : {
3368
+ deploymentId: installed.id,
3369
+ kind: "intervening"
3370
+ };
3371
+ }
3372
+ async function inspectNewPlatformWorker(client, accountId, manifest, resourceKey, workerName) {
3373
+ if (!client.listWorkerNames || !client.getWorkerPlainTextBindings) throw new Error("platform: Cloudflare Worker ownership inspection is unavailable.");
3374
+ if (!(await client.listWorkerNames(accountId)).includes(workerName)) return "missing";
3375
+ const bindings = await client.getWorkerPlainTextBindings(accountId, workerName);
3376
+ let marker;
3377
+ try {
3378
+ marker = parsePlatformResourceOwnershipMarker(JSON.parse(bindings.PLATFORM_OWNERSHIP_MARKER ?? "null"));
3379
+ } catch {
3380
+ throw new Error(`platform: Worker ${workerName} no longer has this installation's ownership marker; refusing to treat it as installer-owned.`);
3381
+ }
3382
+ const expected = createPlatformResourceOwnershipMarker({
3383
+ accountId,
3384
+ installationId: manifest.id,
3385
+ ownershipNonce: manifest.ownershipNonce,
3386
+ resourceId: workerName,
3387
+ resourceKey,
3388
+ resourceKind: "worker",
3389
+ resourceName: workerName
3390
+ });
3391
+ if (marker.accountId !== expected.accountId || marker.installationId !== expected.installationId || marker.ownershipNonce !== expected.ownershipNonce || marker.resourceId !== expected.resourceId || marker.resourceKey !== expected.resourceKey || marker.resourceKind !== expected.resourceKind || marker.resourceName !== expected.resourceName) throw new Error(`platform: Worker ${workerName} no longer has this installation's ownership marker; refusing to treat it as installer-owned.`);
3392
+ return "owned";
3393
+ }
3394
+ function readPlatformWorkerOwnershipMarker(config) {
3395
+ const vars = config.vars;
3396
+ if (!vars || typeof vars !== "object" || Array.isArray(vars)) throw new Error("platform: Platform Worker config is missing its ownership bindings.");
3397
+ const marker = vars.PLATFORM_OWNERSHIP_MARKER;
3398
+ if (typeof marker !== "string" || marker === "") throw new Error("platform: Platform Worker config is missing its ownership marker.");
3399
+ return marker;
3400
+ }
3401
+ async function readActivePlatformWorkerSnapshot(configPath, accountId, workerName, expectedOwnershipMarker, runner = runPlatformWrangler) {
3402
+ const readActiveVersion = () => readActivePlatformWorkerVersion(configPath, accountId, workerName, runner);
3403
+ const versionId = await readActiveVersion();
3404
+ if (!versionId) throw new Error(`platform: Existing Worker ${workerName} has no single active production version.`);
3405
+ const result = await runner([
3406
+ "versions",
3407
+ "view",
3408
+ versionId,
3409
+ "--config",
3410
+ configPath,
3411
+ "--name",
3412
+ workerName,
3413
+ "--json"
3414
+ ], accountId, process.cwd(), true);
3415
+ assertCleanPlatformWrangler(result, `read rollback metadata for ${workerName}`);
3416
+ let value;
3417
+ try {
3418
+ value = JSON.parse(result.stdout);
3419
+ } catch {
3420
+ throw new Error(`platform: Wrangler returned malformed version metadata for ${workerName}.`);
3421
+ }
3422
+ if (!value || typeof value !== "object") throw new Error(`platform: Wrangler returned malformed version metadata for ${workerName}.`);
3423
+ const resources = value.resources;
3424
+ const bindings = resources && typeof resources === "object" && !Array.isArray(resources) ? resources.bindings : null;
3425
+ 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") : [];
3426
+ if (ownershipBindings.length !== 1 || ownershipBindings[0].text !== expectedOwnershipMarker) throw new Error(`platform: Active Worker version ${versionId} for ${workerName} does not belong to this installation.`);
3427
+ if (await readActiveVersion() !== versionId) throw new Error(`platform: Worker ${workerName} changed concurrently while its active version was inspected.`);
3428
+ const annotations = value.annotations;
3429
+ const message = annotations && typeof annotations === "object" && typeof annotations["workers/message"] === "string" ? annotations["workers/message"] ?? null : null;
3430
+ const metadata = parseCloudflareVersionMessage(message);
3431
+ if (metadata?.schema !== 2) throw new Error(`platform: Active Worker ${workerName} predates safe trigger snapshots. Refusing to upgrade because its routes, cron, and queue consumers could not be restored after a failed cutover.`);
3432
+ return {
3433
+ migrationTag: readCloudflareVersionMigrationTag(value),
3434
+ triggerConfig: metadata.triggerConfig,
3435
+ versionId
3436
+ };
3437
+ }
3438
+ function withoutCompletedPlatformWorkerMigrations(config, activeMigrationTag) {
3439
+ const target = (Array.isArray(config.migrations) ? config.migrations : []).at(-1);
3440
+ if (activeMigrationTag === null || !target || typeof target !== "object" || Array.isArray(target) || target.tag !== activeMigrationTag) return config;
3441
+ const result = { ...config };
3442
+ delete result.migrations;
3443
+ return result;
3444
+ }
3445
+ function parseUploadedPlatformWorkerVersion(output) {
3446
+ return /(?:Worker|Current) Version ID:\s*([0-9a-f]{8}-[0-9a-f-]{27})/i.exec(output)?.[1] ?? null;
3447
+ }
3448
+ function assertCleanPlatformWrangler(result, operation) {
3449
+ if (result.status !== 0) throw new Error(`platform: Wrangler failed to ${operation} (${result.stderr.trim() || `exit ${result.status}`}).`);
3450
+ }
3451
+ function cleanPlatformCommandResult() {
3452
+ return {
3453
+ status: 0,
3454
+ stderr: "",
3455
+ stdout: ""
3456
+ };
3457
+ }
3458
+ function assertPlatformTriggerStabilization(result, workerName, targetVersionId, operation) {
3459
+ if (!result.ok) throw new Error(`platform: Failed to ${operation} triggers for ${workerName} (${result.reason}).`);
3460
+ if (!result.targetOwnsTraffic) throw new Error(`platform: Worker ${workerName} was superseded by ${result.activeVersionId} while publishing ${targetVersionId}; its active trigger state was repaired.`);
3461
+ }
3462
+ function readTriggerConfigFromObject(config) {
3463
+ const directory = mkdtempSync(join(tmpdir(), "void-platform-triggers-"));
3464
+ try {
3465
+ const path = join(directory, "wrangler.json");
3466
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
3467
+ return readCloudflareTriggerConfig(path);
3468
+ } finally {
3469
+ rmSync(directory, {
3470
+ force: true,
3471
+ recursive: true
3472
+ });
3473
+ }
3474
+ }
3475
+ function verifyRuntimeArtifacts(runtimeDirectory, manifest) {
3476
+ for (const artifact of Object.values(manifest.artifacts)) {
3477
+ const path = join(runtimeDirectory, artifact.path);
3478
+ let contents;
3479
+ try {
3480
+ contents = readFileSync(path);
3481
+ } catch (error) {
3482
+ throw new Error(`platform: Runtime Worker artifact is missing: ${errorMessage(error)}`);
3483
+ }
3484
+ if (contents.byteLength !== artifact.size || hash(contents) !== artifact.sha256) throw new Error(`platform: Runtime Worker artifact failed integrity verification: ${path}.`);
3485
+ }
3486
+ verifyPlatformDatabaseArtifacts(runtimeDirectory, manifest.database);
3487
+ }
3488
+ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
3489
+ const journalPath = join(runtimeDirectory, "drizzle", "meta", "_journal.json");
3490
+ let journal;
3491
+ try {
3492
+ journal = readFileSync(journalPath);
3493
+ } catch (error) {
3494
+ throw new Error(`platform: Runtime database migration journal is missing: ${errorMessage(error)}`);
3495
+ }
3496
+ if (hash(journal) !== database.journalSha256) throw new Error(`platform: Runtime database migration journal failed integrity verification: ${journalPath}.`);
3497
+ for (const migration of database.migrations) {
3498
+ const path = join(runtimeDirectory, "drizzle", `${migration.tag}.sql`);
3499
+ let contents;
3500
+ try {
3501
+ contents = readFileSync(path);
3502
+ } catch (error) {
3503
+ throw new Error(`platform: Runtime database migration is missing: ${errorMessage(error)}`);
3504
+ }
3505
+ if (hash(contents) !== migration.sha256) throw new Error(`platform: Runtime database migration failed integrity verification: ${path}.`);
3506
+ }
3507
+ }
3508
+ function nextTrafficGeneration(manifest) {
3509
+ return Math.max(0, ...manifest.steps.flatMap((step) => {
3510
+ const match = /^traffic-enabled:(\d+)$/.exec(step.key);
3511
+ return match ? [Number(match[1])] : [];
3512
+ })) + 1;
3513
+ }
3514
+ function nextWorkerCutoverGeneration(manifest) {
3515
+ return latestPlatformWorkerCutoverGeneration(manifest) + 1;
3516
+ }
3517
+ function recordWorkerSecretUpdateState(manifest, generation, state, intentId, completedAt) {
3518
+ return recordInstallationStep(manifest, {
3519
+ completedAt,
3520
+ key: `worker-secret-updates:${generation}:${state}`,
3521
+ resultHash: intentId
3522
+ }, completedAt);
3523
+ }
3524
+ function recordWorkerCutoverStage(manifest, generation, key, previousVersionId, stagedVersionId, workerName, previousTriggers, completedAt) {
3525
+ return recordInstallationStep(manifest, {
3526
+ completedAt,
3527
+ key: `worker-cutover:${generation}:staged:${key}`,
3528
+ resultHash: Buffer.from(JSON.stringify({
3529
+ previousTriggers,
3530
+ previousVersionId,
3531
+ schema: 1,
3532
+ stagedVersionId,
3533
+ workerName
3534
+ })).toString("base64url")
3535
+ }, completedAt);
3536
+ }
3537
+ function recordWorkerCutoverResolution(manifest, generation, resolution, completedAt) {
3538
+ return recordInstallationStep(manifest, {
3539
+ completedAt,
3540
+ key: `worker-cutover:${generation}:${resolution}`,
3541
+ resultHash: resolution
3542
+ }, completedAt);
3543
+ }
3544
+ function recordWorkerUnsafeTraffic(manifest, generation, key, workerName, completedAt) {
3545
+ return recordInstallationStep(manifest, {
3546
+ completedAt,
3547
+ key: `worker-cutover:${generation}:unsafe-traffic:${key}`,
3548
+ resultHash: workerName
3549
+ }, completedAt);
3550
+ }
3551
+ function parseWorkerCutoverSnapshot(value) {
3552
+ try {
3553
+ const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
3554
+ const metadata = parseCloudflareVersionMessage(`void:${JSON.stringify({
3555
+ migrations: [],
3556
+ schema: 2,
3557
+ triggerConfig: parsed.previousTriggers
3558
+ })}`);
3559
+ if (parsed.schema !== 1 || parsed.previousVersionId !== null && typeof parsed.previousVersionId !== "string" || typeof parsed.stagedVersionId !== "string" || parsed.stagedVersionId === "" || typeof parsed.workerName !== "string" || parsed.workerName === "" || metadata?.schema !== 2) return null;
3560
+ return {
3561
+ previousTriggers: metadata.triggerConfig,
3562
+ previousVersionId: parsed.previousVersionId,
3563
+ stagedVersionId: parsed.stagedVersionId,
3564
+ workerName: parsed.workerName
3565
+ };
3566
+ } catch {
3567
+ return null;
3568
+ }
3569
+ }
3570
+ function interruptedWorkerCutover(manifest) {
3571
+ const generations = /* @__PURE__ */ new Map();
3572
+ for (const step of manifest.steps) {
3573
+ const resolution = /^worker-cutover:(\d+):(committed|restored)$/.exec(step.key);
3574
+ if (resolution) {
3575
+ const generation = Number(resolution[1]);
3576
+ const entry = generations.get(generation) ?? {
3577
+ resolved: false,
3578
+ staged: /* @__PURE__ */ new Map(),
3579
+ unsafeTraffic: /* @__PURE__ */ new Set()
3580
+ };
3581
+ entry.resolved = true;
3582
+ generations.set(generation, entry);
3583
+ continue;
3584
+ }
3585
+ const unsafeTraffic = /^worker-cutover:(\d+):unsafe-traffic:(api|proxy|tail|dispatch)$/.exec(step.key);
3586
+ if (unsafeTraffic) {
3587
+ const generation = Number(unsafeTraffic[1]);
3588
+ const entry = generations.get(generation) ?? {
3589
+ resolved: false,
3590
+ staged: /* @__PURE__ */ new Map(),
3591
+ unsafeTraffic: /* @__PURE__ */ new Set()
3592
+ };
3593
+ entry.unsafeTraffic.add(unsafeTraffic[2]);
3594
+ generations.set(generation, entry);
3595
+ continue;
3596
+ }
3597
+ const staged = /^worker-cutover:(\d+):staged:(api|proxy|tail|dispatch)$/.exec(step.key);
3598
+ if (!staged) continue;
3599
+ const generation = Number(staged[1]);
3600
+ const snapshot = parseWorkerCutoverSnapshot(step.resultHash);
3601
+ if (!snapshot) throw new Error(`platform: Invalid Worker cutover checkpoint ${step.key}.`);
3602
+ const entry = generations.get(generation) ?? {
3603
+ resolved: false,
3604
+ staged: /* @__PURE__ */ new Map(),
3605
+ unsafeTraffic: /* @__PURE__ */ new Set()
3606
+ };
3607
+ entry.staged.set(staged[2], snapshot);
3608
+ generations.set(generation, entry);
3609
+ }
3610
+ const unresolved = [...generations.entries()].filter(([, entry]) => !entry.resolved && entry.staged.size > 0).sort(([left], [right]) => right - left)[0];
3611
+ return unresolved ? {
3612
+ generation: unresolved[0],
3613
+ staged: unresolved[1].staged,
3614
+ unsafeTraffic: unresolved[1].unsafeTraffic
3615
+ } : null;
3616
+ }
3617
+ async function verifyPlatformWorkerCutoverVersions(manifest, generation, configs, accountId, runner = runPlatformWrangler) {
3618
+ const cutover = interruptedWorkerCutover(manifest);
3619
+ if (cutover?.generation !== generation || cutover.staged.size !== 4) throw new Error(`platform: Worker cutover generation ${generation} is missing a complete immutable-version checkpoint.`);
3620
+ const directory = mkdtempSync(join(tmpdir(), "void-platform-cutover-verify-"));
3621
+ try {
3622
+ for (const key of [
3623
+ "api",
3624
+ "proxy",
3625
+ "tail",
3626
+ "dispatch"
3627
+ ]) {
3628
+ const snapshot = cutover.staged.get(key);
3629
+ if (!snapshot) throw new Error(`platform: Worker cutover generation ${generation} is missing its ${key} checkpoint.`);
3630
+ const config = configs[key];
3631
+ const configPath = join(directory, `${key}.wrangler.json`);
3632
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
3633
+ const active = await readActivePlatformWorkerSnapshot(configPath, accountId, snapshot.workerName, readPlatformWorkerOwnershipMarker(config), runner);
3634
+ if (active.versionId !== snapshot.stagedVersionId) throw new Error(`platform: Worker ${snapshot.workerName} changed concurrently from staged version ${snapshot.stagedVersionId} to ${active.versionId}; refusing to enable platform traffic.`);
3635
+ }
3636
+ } finally {
3637
+ rmSync(directory, {
3638
+ force: true,
3639
+ recursive: true
3640
+ });
3641
+ }
3642
+ }
3643
+ function recordWorkerTrafficState(manifest, generation, enabled, completedAt) {
3644
+ return recordInstallationStep(manifest, {
3645
+ completedAt,
3646
+ key: `worker-traffic-state:${generation}`,
3647
+ resultHash: enabled ? "enabled" : "disabled"
3648
+ }, completedAt);
3649
+ }
3650
+ function stablePublicEndpointState(existing) {
3651
+ if (!existing) return false;
3652
+ if (existing.status === "disabled" || existing.desiredState === "disabled") return false;
3653
+ const latest = existing.steps.flatMap((step) => {
3654
+ const match = /^worker-traffic-state:(\d+)$/.exec(step.key);
3655
+ return match ? [{
3656
+ generation: Number(match[1]),
3657
+ state: step.resultHash
3658
+ }] : [];
3659
+ }).sort((left, right) => right.generation - left.generation)[0];
3660
+ if (latest) return latest.state === "enabled";
3661
+ return existing.status === "installed";
3662
+ }
3663
+ function resolvePlatformPreviousTriggerConfig(versionSnapshot, configuredSnapshot, publicEndpoints) {
3664
+ if (publicEndpoints) return versionSnapshot;
3665
+ return {
3666
+ ...versionSnapshot,
3667
+ preview_urls: configuredSnapshot.preview_urls,
3668
+ routes: configuredSnapshot.routes,
3669
+ workers_dev: configuredSnapshot.workers_dev
3670
+ };
3671
+ }
3672
+ function errorMessage(error) {
3673
+ return error instanceof Error ? error.message : String(error);
3674
+ }
3675
+ var PlatformAccessBlockedError = class extends Error {
3676
+ constructor(url, context) {
3677
+ super([
3678
+ "platform: Cloudflare Access blocked the platform health checks (Default-Deny, error 1050).",
3679
+ `Blocked URL: ${url}`,
3680
+ "",
3681
+ "To make this test platform publicly reachable:",
3682
+ "",
3683
+ `1. In ${context ? `account ${JSON.stringify(context.accountName)}` : "your Cloudflare account"}, open:`,
3684
+ " Zero Trust → Access controls → Applications → Create new application.",
3685
+ "2. Choose Self-hosted. When adding destinations, choose Worker and select:",
3686
+ ` ${context ? context.workers.join(", ") + "." : "This installation's API, proxy, and dispatch Workers."}`,
3687
+ "3. Add a policy: Action → Bypass; Include → Everyone.",
3688
+ 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.",
3689
+ "",
3690
+ "This removes Cloudflare Access protection for these Workers.",
3691
+ "Void’s own authentication stays enabled. Keep account-wide Default-Deny on.",
3692
+ "",
3693
+ "For platforms that must remain Access-protected:",
3694
+ "https://void.cloud/guide/self-hosted-platform#cloudflare-access"
3695
+ ].join("\n"));
3696
+ }
3697
+ };
3698
+ async function platformProbeFailure(label, url, response, context) {
3699
+ const message = `platform: ${label} failed at ${url} (HTTP ${response.status}).`;
3700
+ if (response.status === 403 && response.headers.get("server")?.toLowerCase() === "cloudflare" && response.headers.get("content-type")?.toLowerCase().includes("text/html")) {
3701
+ const body = await response.text().catch(() => "");
3702
+ if (body.includes("Default-Deny") && /errorCode:\s*1050\b/.test(body)) return new PlatformAccessBlockedError(url, context);
3703
+ }
3704
+ return new Error(message);
3705
+ }
3706
+ async function verifyPlatformHealth(apiUrl, installationId, proxyUrl, applicationBaseDomain, platformVersion, usageProtocolVersion, healthProbeToken, fetcher = fetch, dispatchOrigin, context) {
3707
+ const request = (url) => fetcher(url, {
3708
+ headers: {
3709
+ "X-Void-Platform-Probe": healthProbeToken,
3710
+ ...cfAccessHeaders(process.env, url)
3711
+ },
3712
+ redirect: "manual",
3713
+ signal: AbortSignal.timeout(3e4)
3714
+ });
3715
+ const dispatchUrl = dispatchOrigin ?? `https://test.${applicationBaseDomain}`;
3716
+ const [health, discovery, proxy, dispatch] = await Promise.all([
3717
+ request(`${apiUrl}/health`),
3718
+ request(`${apiUrl}/.well-known/void-platform`),
3719
+ request(`${proxyUrl}/health`),
3720
+ request(`${dispatchUrl}/health`)
3721
+ ]);
3722
+ if (!health.ok) throw await platformProbeFailure("Health check", apiUrl, health, context);
3723
+ if (!discovery.ok) throw await platformProbeFailure("Discovery check", apiUrl, discovery, context);
3724
+ const body = await discovery.json();
3725
+ if (body.installationId !== installationId) throw new Error("platform: Deployed API discovery identity does not match this installation.");
3726
+ if (body.platformVersion !== platformVersion) throw new Error("platform: Deployed API discovery version does not match this release.");
3727
+ if ((body.usageProtocolVersion ?? 0) !== usageProtocolVersion) throw new Error("platform: Deployed API usage protocol does not match this runtime. Traffic remains disabled.");
3728
+ if (!proxy.ok) throw await platformProbeFailure("Proxy health check", proxyUrl, proxy, context);
3729
+ if (!dispatch.ok) throw await platformProbeFailure("Dispatch health check", dispatchUrl, dispatch, context);
3730
+ }
3731
+ async function verifyPlatformSandboxDrain(apiUrl, healthProbeToken, fetcher = fetch) {
3732
+ const response = await fetcher(`${apiUrl}/internal/platform/sandbox-drain`, {
3733
+ headers: {
3734
+ "X-Void-Platform-Probe": healthProbeToken,
3735
+ ...cfAccessHeaders(process.env, apiUrl)
3736
+ },
3737
+ signal: AbortSignal.timeout(3e4)
3738
+ });
3739
+ if (!response.ok) throw new Error(`platform: Sandbox drain probe failed at ${apiUrl} (HTTP ${response.status}). Traffic remains disabled.`);
3740
+ let value;
3741
+ try {
3742
+ value = await response.json();
3743
+ } catch {
3744
+ throw new Error("platform: Sandbox drain probe did not return its DB-backed protocol v1 document. Traffic remains disabled.");
3745
+ }
3746
+ 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.");
3747
+ const status = value;
3748
+ const validState = status.state === "required" || status.state === "draining" || status.state === "complete";
3749
+ const validPhase = status.phase === null || status.phase === "deployments" || status.phase === "resources";
3750
+ 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.");
3751
+ return status.state === "complete";
3752
+ }
3753
+ function createPlatformHealthProbeToken(ownershipNonce) {
3754
+ return createHash("sha256").update("void-platform-health-probe:v1\0").update(ownershipNonce).digest("base64url");
3755
+ }
3756
+ //#endregion
3757
+ //#region src/cli/platform-setup-draft.ts
3758
+ const STARTED = "credential-setup:v1";
3759
+ const COMPLETED = "credential-setup-complete:v1";
3760
+ /** Includes interrupted provisioning, but not maintenance of an already deployed platform. */
3761
+ function isPlatformInstallInProgress(manifest) {
3762
+ return manifest.runtime === null && manifest.desiredState === "enabled" && !manifest.resources.some((resource) => resource.state === "deleted") && [
3763
+ "planned",
3764
+ "provisioning",
3765
+ "waiting-for-dns",
3766
+ "waiting-for-oauth",
3767
+ "deploying",
3768
+ "verifying",
3769
+ "failed"
3770
+ ].includes(manifest.status);
3771
+ }
3772
+ function isPlatformSetupDraft(manifest) {
3773
+ return manifest.status === "planned" && manifest.resources.length === 0 && manifest.endpoints.apiUrl === null && manifest.steps.some((step) => step.key === STARTED) && !manifest.steps.some((step) => step.key === COMPLETED);
3774
+ }
3775
+ function savePlatformSetupDraft(plan, version) {
3776
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3777
+ let manifest = createPlatformInstallationManifest({
3778
+ account: plan.account,
3779
+ deployment: plan.deployment,
3780
+ id: plan.installationId,
3781
+ createdAt: now,
3782
+ platformVersion: version,
3783
+ ownershipNonce: randomBytes(32).toString("base64url")
3784
+ });
3785
+ manifest = transitionInstallation(manifest, "planned", now);
3786
+ manifest = recordInstallationStep(manifest, {
3787
+ key: STARTED,
3788
+ completedAt: now,
3789
+ resultHash: plan.installationId
3790
+ }, now);
3791
+ writeInstallationManifest(manifest);
3792
+ writePendingInstallationSecrets(manifest, {});
3793
+ return manifest;
3794
+ }
3795
+ function completePlatformSetupDraft(manifest) {
3796
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3797
+ const completed = recordInstallationStep(manifest, {
3798
+ key: COMPLETED,
3799
+ completedAt: now,
3800
+ resultHash: manifest.id
3801
+ }, now);
3802
+ writeInstallationManifest(completed);
3803
+ return completed;
3804
+ }
3805
+ //#endregion
3806
+ //#region src/cli/platform-setup.ts
3807
+ function platformSetupLink(page, hyperlinks = supportsSetupLinks()) {
3808
+ return hyperlinks ? "\x1B]8;;" + page.url + "\x1B\\" + page.title + "\x1B]8;;\x1B\\" : page.title + ": " + page.url;
3809
+ }
3810
+ function platformSetupFieldHelp(key) {
3811
+ const docs = (section, title) => platformSetupLink({
3812
+ title,
3813
+ url: "https://void.cloud/guide/self-hosted-platform#" + section
3814
+ });
3815
+ switch (key) {
3816
+ case "databaseUrl": return {
3817
+ placeholder: "postgresql://user:pw@…/database",
3818
+ description: "Stores platform users, projects, and deployments; use an empty hosted database, e.g. " + platformSetupLink({
3819
+ title: "Neon",
3820
+ url: "https://neon.com/"
3821
+ }) + ". " + docs("platform-database", "Database setup")
3822
+ };
3823
+ case "jwtSecret": return { description: "Signs platform login tokens. Run openssl rand -base64 32; save the key in your password manager. " + docs("signing-and-encryption-keys", "Key setup") };
3824
+ case "projectSecretKey": return { description: "Encrypts app secrets. Run openssl rand -base64 32 separately; save this key for recovery. " + docs("signing-and-encryption-keys", "Key setup") };
3825
+ default: return;
3826
+ }
3827
+ }
3828
+ function supportsSetupLinks() {
3829
+ return !!process.stdout.isTTY && ([
3830
+ "ghostty",
3831
+ "iTerm.app",
3832
+ "WezTerm",
3833
+ "vscode",
3834
+ "WarpTerminal"
3835
+ ].includes(process.env.TERM_PROGRAM ?? "") || !!process.env.WT_SESSION || !!process.env.VTE_VERSION || !!process.env.KONSOLE_VERSION || process.env.TERM === "xterm-kitty");
3836
+ }
3837
+ /** Dashboard keys use permission-catalog labels, which can differ from product names. */
3838
+ function platformSetupPages(plan) {
3839
+ const needsGateway = plan.actions.some((action) => action.kind === "ai-gateway" && action.action === "inspect");
3840
+ const permissions = [
3841
+ {
3842
+ key: "account_settings",
3843
+ type: "read"
3844
+ },
3845
+ {
3846
+ key: "workers_scripts",
3847
+ type: "edit"
3848
+ },
3849
+ {
3850
+ key: "workers_kv_storage",
3851
+ type: "edit"
3852
+ },
3853
+ {
3854
+ key: "workers_r2",
3855
+ type: "edit"
3856
+ },
3857
+ {
3858
+ key: "d1",
3859
+ type: "edit"
3860
+ },
3861
+ {
3862
+ key: "queues",
3863
+ type: "edit"
3864
+ },
3865
+ {
3866
+ key: "account_analytics",
3867
+ type: "read"
3868
+ },
3869
+ {
3870
+ key: "workers_tail",
3871
+ type: "read"
3872
+ },
3873
+ {
3874
+ key: "query_cache",
3875
+ type: "edit"
3876
+ },
3877
+ ...needsGateway ? [{
3878
+ key: "aig",
3879
+ type: "edit"
3880
+ }] : [],
3881
+ ...plan.deployment.application.zoneName ? [{
3882
+ key: "cache",
3883
+ type: "purge"
3884
+ }] : [],
3885
+ ...plan.deployment.capabilities.customDomains ? [{
3886
+ key: "ssl_and_certificates",
3887
+ type: "edit"
3888
+ }] : []
3889
+ ];
3890
+ return {
3891
+ runtime: {
3892
+ title: "Cloudflare runtime token",
3893
+ url: "https://dash.cloudflare.com/?" + new URLSearchParams({
3894
+ to: "/" + plan.account.id + "/api-tokens",
3895
+ name: plan.deployment.displayName + " Runtime",
3896
+ permissionGroupKeys: JSON.stringify(permissions)
3897
+ }),
3898
+ instructions: [
3899
+ "Review the preselected permissions for account " + plan.account.name + ".",
3900
+ "Edit: Workers Scripts, KV, R2, D1, Queues, Hyperdrive" + (needsGateway ? ", AI Gateway." : "."),
3901
+ "Read: Account Settings, Account Analytics, Workers Tail.",
3902
+ ...plan.deployment.application.zoneName ? ["Zone " + plan.deployment.application.zoneName + ": Cache Purge: Purge."] : [],
3903
+ ...plan.deployment.capabilities.customDomains ? ["Custom project domains also need SSL and Certificates: Edit on that zone."] : [],
3904
+ "If the form differs, match the permissions above. Create the token, then paste it here."
3905
+ ].join("\n")
3906
+ },
3907
+ github: {
3908
+ title: "GitHub OAuth App",
3909
+ url: "https://github.com/settings/applications/new",
3910
+ instructions: "GitHub Auth will be used by the users of your Void Platform instance. Register a GitHub app and generate a client secret.\nHomepage: " + plan.apiUrl + "\nRedirect URI: " + plan.apiUrl + "/auth/callback"
3911
+ },
3912
+ r2: {
3913
+ title: "R2 upload token",
3914
+ url: "https://dash.cloudflare.com/?" + new URLSearchParams({
3915
+ to: "/" + plan.account.id + "/r2/api-tokens/create",
3916
+ type: "account"
3917
+ }),
3918
+ instructions: [
3919
+ "Select Object Read & Write (the form defaults to read-only).",
3920
+ "Keep Apply to all buckets in this account (including newly created buckets) selected.",
3921
+ "Create the token, then copy the Access Key ID and Secret Access Key into the CLI."
3922
+ ].join("\n")
3923
+ }
3924
+ };
3925
+ }
3926
+ function formatPlatformSetupPlan(plan) {
3927
+ const pages = platformSetupPages(plan);
3928
+ return [
3929
+ "Credentials needed (paste them into the installer; keep a password-manager copy):",
3930
+ platformSetupLink(pages.runtime),
3931
+ pages.runtime.instructions,
3932
+ "",
3933
+ platformSetupLink(pages.r2),
3934
+ pages.r2.instructions,
3935
+ "",
3936
+ platformSetupLink(pages.github),
3937
+ pages.github.instructions
3938
+ ].join("\n");
3939
+ }
3940
+ function openPlatformSetupPage(page, dependencies = {}) {
3941
+ log.info(page.title + "\n" + page.instructions);
3942
+ console.log(platformSetupLink({
3943
+ ...page,
3944
+ title: "Open " + page.title
3945
+ }));
3946
+ if (dependencies.interactive ?? process.stdin.isTTY === true) try {
3947
+ (dependencies.open ?? ((url) => openBrowser(url, execFile)))(page.url);
3948
+ } catch {
3949
+ log.info("Could not open the browser. Use the link above.");
3950
+ }
3951
+ }
3952
+ //#endregion
3953
+ //#region src/cli/prompt-password.ts
3954
+ var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
3955
+ /** A visual example for an empty secret field; never a default value or an unmasked input. */
3956
+ async function passwordWithPlaceholder(options) {
3957
+ return await new u$1({
3958
+ input: options.input,
3959
+ output: options.output,
3960
+ signal: options.signal,
3961
+ mask: options.mask ?? S_PASSWORD_MASK,
3962
+ validate: options.validate,
3963
+ render() {
3964
+ const guide = options.withGuide ?? settings.withGuide;
3965
+ const prefix = `${guide ? `${import_picocolors.default.gray(S_BAR)}\n` : ""}${symbol(this.state)} ${options.message}\n`;
3966
+ const bar = guide ? `${import_picocolors.default.gray(S_BAR)} ` : "";
3967
+ if (this.state === "submit" || this.state === "cancel") return `${prefix}${bar}${import_picocolors.default.dim(this.masked)}`;
3968
+ if (this.state === "error") {
3969
+ const masked = this.masked;
3970
+ if (options.clearOnError) this.clear();
3971
+ return `${prefix}${bar}${import_picocolors.default.dim(masked)}\n${guide ? `${import_picocolors.default.yellow(S_BAR_END)} ` : ""}${import_picocolors.default.yellow(this.error)}\n`;
3972
+ }
3973
+ const input = this.userInput.length === 0 ? import_picocolors.default.dim(options.placeholder) : this.userInputWithCursor;
3974
+ return `${prefix}${guide ? `${import_picocolors.default.cyan(S_BAR)} ` : ""}${input}\n${guide ? import_picocolors.default.cyan(S_BAR_END) : ""}\n`;
3975
+ }
3976
+ }).prompt() ?? CANCEL_SYMBOL;
3977
+ }
3978
+ //#endregion
3979
+ //#region src/cli/platform-lifecycle.ts
3980
+ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
3981
+ const explicit = process.env.VOID_PLATFORM_DATABASE_URL || process.env.DATABASE_URL || "";
3982
+ const recorded = checkpoint?.databaseUrl || "";
3983
+ if (checkpoint?.databaseClaimState === "claimed" || manifest?.resources.some((resource) => resource.state === "ready" && (resource.kind === "hyperdrive" || resource.kind === "worker")) || manifest?.steps.some((step) => step.key.startsWith("database-schema:") || step.key.startsWith("database-migrations:"))) {
3984
+ if (!recorded) {
3985
+ if (explicit) return explicit;
3986
+ throw new Error("platform: The claimed PostgreSQL database URL is missing from local recovery state. Export VOID_PLATFORM_DATABASE_URL with the original installation database before continuing.");
3987
+ }
3988
+ if (explicit && explicit !== recorded) throw new Error("platform: This installation is already bound to its recorded PostgreSQL database. Unset DATABASE_URL and VOID_PLATFORM_DATABASE_URL before resuming; changing databases requires an explicit migration flow.");
3989
+ return recorded;
3990
+ }
3991
+ return explicit || recorded;
3992
+ }
3993
+ async function runPlatformLifecycleCommand(args) {
3994
+ if (args.subcommand === "domain") {
3995
+ const { runPlatformDomainCommand } = await import("./platform-domain-ChvbJkdy.mjs");
3996
+ await runPlatformDomainCommand(args);
3997
+ return;
3998
+ }
3999
+ if (args.subcommand === "discover") {
4000
+ const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
4001
+ const accountId = await selectAccount(await readAccounts(client), args.account);
4002
+ const { recoverPlatformInstallations } = await import("./platform-recovery-C_YO-tIs.mjs");
4003
+ const recovered = await recoverPlatformInstallations(client, accountId, void 0, args.selector);
4004
+ if (recovered.length === 0) {
4005
+ log.info(`No recoverable Void platform installations were found in ${accountId}.`);
4006
+ return;
4007
+ }
4008
+ for (const manifest of recovered) log.success(`Recovered ${manifest.deployment.displayName} (${manifest.id}) at ${manifest.endpoints.apiUrl}.`);
4009
+ return;
4010
+ }
4011
+ if (args.subcommand !== "install") {
4012
+ const { runInstalledPlatformCommand } = await import("./platform-management-Db2PXw0B.mjs");
4013
+ await runInstalledPlatformCommand(args);
4014
+ return;
4015
+ }
4016
+ const runtime = loadPlatformRuntime(args.runtime);
4017
+ assertNoninteractivePlatformMutationAcknowledged(args, process.stdin.isTTY === true);
4018
+ console.log();
4019
+ intro(cliTitle("platform install"));
4020
+ let existing = args.resume ? resolveInstallationManifest(args.name) : null;
4021
+ let selectedInstall = false;
4022
+ let selectedDraft = false;
4023
+ const { input: installationSlug, credential: operatorCredential } = await resolvePlatformCloudflareCredentialWithInput(async () => {
4024
+ let requestedName = args.name;
4025
+ const resumableInstalls = () => listInstallationManifests().filter((manifest) => isPlatformInstallInProgress(manifest) && (!args.account || manifest.account.id === args.account || manifest.account.name.toLowerCase() === args.account.toLowerCase()));
4026
+ const selectInstall = (manifest) => {
4027
+ existing = manifest;
4028
+ selectedInstall = true;
4029
+ selectedDraft = isPlatformSetupDraft(manifest);
4030
+ };
4031
+ const assertNewNameAvailable = (slug) => {
4032
+ const collision = listInstallationManifests().find((manifest) => manifest.status !== "uninstalled" && manifest.deployment.installationSlug === slug);
4033
+ if (collision && !isPlatformInstallInProgress(collision)) throw new Error(`platform: ${collision.deployment.displayName} (${slug}) already exists (${collision.status}). Run \`void platform status ${collision.id}\` to inspect it.`);
4034
+ assertPlatformInstallationSlugAvailable(slug);
4035
+ };
4036
+ if (!args.resume && process.stdin.isTTY) {
4037
+ const installs = resumableInstalls().filter((manifest) => !args.name || manifest.deployment.installationSlug === normalizeInstallationSlug(args.name));
4038
+ if (installs.length > 0) {
4039
+ const choice = await select({
4040
+ message: "Continue a previous platform install?",
4041
+ options: [...installs.map((install) => ({
4042
+ value: install.id,
4043
+ label: `Continue setup for ${install.deployment.displayName} (${install.deployment.installationSlug})`,
4044
+ hint: install.account.name
4045
+ })), {
4046
+ value: "new",
4047
+ label: "Start a new platform install"
4048
+ }]
4049
+ });
4050
+ if (isCancel(choice) || typeof choice === "symbol") throw new Error("platform: Cancelled.");
4051
+ if (choice === "new") requestedName = "";
4052
+ else {
4053
+ const selected = installs.find((install) => install.id === choice);
4054
+ if (!selected) throw new Error("platform: The selected setup is no longer available.");
4055
+ selectInstall(selected);
4056
+ }
4057
+ }
4058
+ }
4059
+ while (!existing) {
4060
+ const name = requestedName || await promptText("Installation name:", { placeholder: "company" }, false, (value) => {
4061
+ try {
4062
+ const slug = normalizeInstallationSlug(value);
4063
+ if (!resumableInstalls().some((install) => install.deployment.installationSlug === slug)) assertNewNameAvailable(slug);
4064
+ } catch (error) {
4065
+ return error instanceof Error ? error.message : "Choose another installation name.";
4066
+ }
4067
+ });
4068
+ const slug = normalizeInstallationSlug(name);
4069
+ const matches = resumableInstalls().filter((install) => install.deployment.installationSlug === slug);
4070
+ if (process.stdin.isTTY && matches.length > 0) {
4071
+ let selected = matches[0];
4072
+ if (matches.length > 1) {
4073
+ const choice = await select({
4074
+ message: `Which ${slug} installation do you want to resume?`,
4075
+ options: matches.map((install) => ({
4076
+ value: install.id,
4077
+ label: install.deployment.displayName,
4078
+ hint: install.account.name
4079
+ }))
4080
+ });
4081
+ if (isCancel(choice) || typeof choice === "symbol") throw new Error("platform: Cancelled.");
4082
+ const matching = matches.find((install) => install.id === choice);
4083
+ if (!matching) throw new Error("platform: The selected setup is no longer available.");
4084
+ selected = matching;
4085
+ }
4086
+ const resume = await confirm({
4087
+ message: `Resume installation for ${selected.deployment.displayName} (${slug})?`,
4088
+ initialValue: true
4089
+ });
4090
+ if (isCancel(resume) || typeof resume === "symbol") throw new Error("platform: Cancelled.");
4091
+ if (resume) selectInstall(selected);
4092
+ else requestedName = "";
4093
+ continue;
4094
+ }
4095
+ assertNewNameAvailable(slug);
4096
+ return slug;
4097
+ }
4098
+ if (existing.endpoints.apiUrl && !readPendingInstallationSecrets(existing)) throw new Error(`platform: Installation ${existing.id} has already been deployed. Use \`void platform repair ${existing.id}\` or \`void platform upgrade ${existing.id}\` instead.`);
4099
+ return existing.deployment.installationSlug;
4100
+ });
4101
+ let client = new PlatformCloudflareClient(operatorCredential.token);
4102
+ const accounts = await readAccounts(client);
4103
+ const accountId = existing ? existing.account.id : await selectAccount(accounts, args.account);
4104
+ if (existing && args.account && await selectAccount(accounts, args.account) !== existing.account.id) throw new Error("platform: The installation belongs to a different Cloudflare account.");
4105
+ const recorded = existing?.deployment;
4106
+ if (existing && isPlatformSetupDraft(existing) && (args.applicationDomain && normalizeApplicationDomain(args.applicationDomain) !== recorded.application.baseDomain || args.zone && normalizeApplicationDomain(args.zone) !== recorded.application.zoneName || args.workersDev && !!recorded.application.zoneName || args.controlPlaneDomain && normalizeApplicationDomain(args.controlPlaneDomain) !== recorded.controlPlane.apiHostname || args.dedicatedZone && !recorded.application.dedicatedZone)) throw new Error("platform: These options differ from the saved setup. Resume its recorded configuration or choose another installation name.");
4107
+ let workersDev = !!recorded?.application.workersDev && !recorded.application.zoneName || args.workersDev === true;
4108
+ if (!recorded && !args.workersDev && !args.applicationDomain && !args.zone && process.stdin.isTTY) {
4109
+ const mode = await select({
4110
+ message: "Where should your apps live?",
4111
+ options: [{
4112
+ value: "domain",
4113
+ label: "Use a domain",
4114
+ hint: "recommended"
4115
+ }, {
4116
+ value: "workers-dev",
4117
+ label: "Use workers.dev for testing",
4118
+ hint: "add a domain later"
4119
+ }]
4120
+ });
4121
+ if (isCancel(mode)) throw new Error("platform: Cancelled.");
4122
+ workersDev = mode === "workers-dev";
4123
+ }
4124
+ const applicationDomain = recorded?.application.baseDomain ?? (workersDev ? "" : normalizeApplicationDomain(args.applicationDomain || await promptText("Application base domain:", "example.app")));
4125
+ const zoneName = recorded?.application.zoneName ?? (workersDev ? "" : normalizeApplicationDomain(args.zone || await promptText("Cloudflare zone:", applicationDomain)));
4126
+ const displayName = recorded?.displayName ?? (args.displayName || await promptText("Platform display name:", `${installationSlug} Void`));
4127
+ const controlPlaneDomain = recorded ? recorded.controlPlane.apiHostname ?? "" : args.controlPlaneDomain || (process.stdin.isTTY ? await promptText("Optional custom API hostname (leave empty for workers.dev):", "", true) : "");
4128
+ let dedicatedZone = recorded?.application.dedicatedZone ?? args.dedicatedZone;
4129
+ if (!workersDev && !recorded && !args.dedicatedZone && applicationDomain === zoneName && process.stdin.isTTY) {
4130
+ const catchAll = await confirm({
4131
+ message: `Dedicate all unmatched ${zoneName} traffic to Void apps?`,
4132
+ initialValue: false
4133
+ });
4134
+ if (isCancel(catchAll) || typeof catchAll === "symbol") throw new Error("platform: Cancelled.");
4135
+ dedicatedZone = catchAll;
4136
+ }
4137
+ const release = args.plan ? null : acquirePlatformLifecycleLock(accountId, installationSlug);
4138
+ let progress;
4139
+ const startProgress = (message) => {
4140
+ const active = createSpinner({ onCancel: () => {
4141
+ if (!process.emit("SIGINT")) process.exit(130);
4142
+ } });
4143
+ active.start(message);
4144
+ return active;
4145
+ };
4146
+ try {
4147
+ if (!existing && !args.plan) assertPlatformInstallationSlugAvailable(installationSlug);
4148
+ if (existing && !args.plan) {
4149
+ existing = resolveInstallationManifest(existing.id);
4150
+ if (selectedInstall && (!isPlatformInstallInProgress(existing) || selectedDraft && !isPlatformSetupDraft(existing))) throw new Error("platform: Setup advanced in another process. Rerun with --resume to review its current state.");
4151
+ }
4152
+ const checkpoint = existing ? readPendingInstallationSecrets(existing) : null;
4153
+ const databaseUrl = resolvePlatformInstallDatabaseUrl(checkpoint, existing);
4154
+ let authoritativeProvisionIntents = false;
4155
+ if (existing && !isPlatformSetupDraft(existing)) {
4156
+ if (!databaseUrl) throw new Error("platform: DATABASE_URL is required to verify an interrupted installation before resuming it.");
4157
+ progress = startProgress("Reading the saved installation");
4158
+ const authoritative = await readPlatformDatabaseLifecycleManifest(databaseUrl, existing.id, existing.ownershipNonce);
4159
+ progress?.stop("Read the saved installation.");
4160
+ progress = void 0;
4161
+ if (authoritative) {
4162
+ const { selectAuthoritativePlatformManifest } = await import("./platform-management-Db2PXw0B.mjs");
4163
+ const selected = selectAuthoritativePlatformManifest(existing, authoritative);
4164
+ authoritativeProvisionIntents = true;
4165
+ if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
4166
+ existing = selected;
4167
+ if (selectedInstall && !isPlatformInstallInProgress(existing)) throw new Error(`platform: Installation ${existing.id} is no longer unfinished. Run \`void platform status ${existing.id}\` to inspect it.`);
4168
+ }
4169
+ }
4170
+ const planInput = {
4171
+ accountId,
4172
+ applicationDomain,
4173
+ controlPlaneDomain,
4174
+ dedicatedZone,
4175
+ displayName,
4176
+ installationSlug,
4177
+ zoneName,
4178
+ ...workersDev ? { workersDev: true } : {}
4179
+ };
4180
+ let plan = await createPlatformInstallPlan(client, planInput, existing, {
4181
+ authoritativeProvisionIntents,
4182
+ allowUnverifiedAiGateway: true
4183
+ });
4184
+ note(formatPlatformInstallPlan(plan), args.plan ? "Read-only installation plan" : "Installation plan");
4185
+ note(formatPlatformRuntime(runtime), "Platform runtime");
4186
+ if (args.plan) {
4187
+ console.log(formatPlatformSetupPlan(plan));
4188
+ outro("No Cloudflare or local installation state was changed.");
4189
+ return;
4190
+ }
4191
+ assertPlatformOperatorCredentialCanApplyPlan(operatorCredential, plan);
4192
+ if (process.stdin.isTTY && !args.yes) {
4193
+ const proceed = await confirm({
4194
+ message: "Apply this plan?",
4195
+ initialValue: false
4196
+ });
4197
+ if (isCancel(proceed) || !proceed) {
4198
+ outro("No changes made.");
4199
+ return;
4200
+ }
4201
+ }
4202
+ if (!existing) existing = savePlatformSetupDraft(plan, runtime.manifest.platformVersion);
4203
+ const setupManifest = existing;
4204
+ const credentialSetup = isPlatformSetupDraft(existing);
4205
+ const values = { ...checkpoint };
4206
+ const saveValues = (updates) => {
4207
+ Object.assign(values, updates);
4208
+ if (credentialSetup) writePendingInstallationSecrets(setupManifest, values);
4209
+ };
4210
+ const configured = (key, environment) => credentialSetup ? process.env[environment] || values[key] || "" : values[key] || process.env[environment] || "";
4211
+ const collect = async (key, environment, message, masked = true, persist = true, validate) => {
4212
+ let value = configured(key, environment);
4213
+ if (!value) {
4214
+ const help = platformSetupFieldHelp(key);
4215
+ if (help && process.stdin.isTTY) log.info(help.description);
4216
+ value = await promptSecret(message, masked, validate, help?.placeholder);
4217
+ }
4218
+ const error = validate?.(value);
4219
+ if (error) throw new Error("platform: " + error);
4220
+ if (persist) saveValues({ [key]: value });
4221
+ return value;
4222
+ };
4223
+ const pages = platformSetupPages(plan);
4224
+ if (!configured("cloudflareApiToken", "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN")) openPlatformSetupPage(pages.runtime);
4225
+ const usingSavedRuntimeToken = credentialSetup && !process.env.VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN && Boolean(values.cloudflareApiToken);
4226
+ let runtimeCloudflareApiToken = await collect("cloudflareApiToken", "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN", "Paste runtime API token:", true, false);
4227
+ let rejectedSavedRuntimeToken = false;
4228
+ const verifyRuntimeToken = async (token) => {
4229
+ let checkingRuntimePermissions = true;
4230
+ try {
4231
+ await verifyPlatformRuntimeTokenPermissions(new PlatformCloudflareClient(token), {
4232
+ accountId,
4233
+ applicationDomain,
4234
+ customDomains: plan.deployment.capabilities.customDomains,
4235
+ zoneId: plan.zone?.id ?? null
4236
+ });
4237
+ checkingRuntimePermissions = false;
4238
+ if (plan.actions.some((action) => action.action === "inspect")) {
4239
+ client = client.withAiGatewayToken(token);
4240
+ plan = await verifyPlatformAiGatewayPlan(client, planInput, existing, plan, authoritativeProvisionIntents);
4241
+ }
4242
+ } catch (error) {
4243
+ if (credentialSetup && token === values.cloudflareApiToken && isRuntimeTokenAuthorizationError(error, checkingRuntimePermissions ? void 0 : "/ai-gateway/")) {
4244
+ delete values.cloudflareApiToken;
4245
+ writePendingInstallationSecrets(setupManifest, values);
4246
+ rejectedSavedRuntimeToken = true;
4247
+ }
4248
+ throw error;
4249
+ }
4250
+ };
4251
+ try {
4252
+ await verifyRuntimeToken(runtimeCloudflareApiToken);
4253
+ } catch (error) {
4254
+ if (!usingSavedRuntimeToken || !process.stdin.isTTY || !rejectedSavedRuntimeToken) throw error;
4255
+ log.warn("Cloudflare rejected the saved runtime token. Create a replacement to continue setup.");
4256
+ openPlatformSetupPage(pages.runtime);
4257
+ runtimeCloudflareApiToken = await collect("cloudflareApiToken", "VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN", "Paste runtime API token:", true, false);
4258
+ await verifyRuntimeToken(runtimeCloudflareApiToken);
4259
+ }
4260
+ saveValues({ cloudflareApiToken: runtimeCloudflareApiToken });
4261
+ const previousLogin = values.bootstrapGitHubLogin;
4262
+ const bootstrapGitHubLogin = await collect("bootstrapGitHubLogin", "VOID_PLATFORM_ADMIN_GITHUB_LOGIN", "Initial administrator GitHub username:", false);
4263
+ const bootstrapGitHubId = (previousLogin === bootstrapGitHubLogin ? values.bootstrapGitHubId : "") || process.env.VOID_PLATFORM_ADMIN_GITHUB_ID || await resolveBootstrapGitHubId(bootstrapGitHubLogin);
4264
+ saveValues({ bootstrapGitHubId });
4265
+ const installationDatabaseUrl = databaseUrl || await collect("databaseUrl", "VOID_PLATFORM_DATABASE_URL", "PostgreSQL DATABASE_URL:");
4266
+ saveValues({ databaseUrl: installationDatabaseUrl });
4267
+ if (!configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") || !configured("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET")) openPlatformSetupPage(configured("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID") ? {
4268
+ ...pages.github,
4269
+ url: "https://github.com/settings/developers"
4270
+ } : pages.github);
4271
+ const githubClientId = await collect("githubClientId", "VOID_PLATFORM_GITHUB_CLIENT_ID", "GitHub OAuth client ID:", false, false);
4272
+ const githubClientSecret = await collect("githubClientSecret", "VOID_PLATFORM_GITHUB_CLIENT_SECRET", "GitHub OAuth client secret:", true, false);
4273
+ saveValues({
4274
+ githubClientId,
4275
+ githubClientSecret
4276
+ });
4277
+ if (!configured("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID") || !configured("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY")) openPlatformSetupPage(pages.r2);
4278
+ const r2AccessKeyId = await collect("r2AccessKeyId", "VOID_PLATFORM_R2_ACCESS_KEY_ID", "Access Key ID:", false, false);
4279
+ const r2SecretAccessKey = await collect("r2SecretAccessKey", "VOID_PLATFORM_R2_SECRET_ACCESS_KEY", "Secret Access Key:", true, false);
4280
+ saveValues({
4281
+ r2AccessKeyId,
4282
+ r2SecretAccessKey
4283
+ });
4284
+ const hasFullProjectSecretKeyring = Boolean(checkpoint?.projectSecretKeysJson || checkpoint?.projectSecretActiveKeyVersion || process.env.VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON || process.env.VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION);
4285
+ const secrets = createPlatformInstallationSecrets({
4286
+ bootstrapGitHubId,
4287
+ bootstrapGitHubLogin,
4288
+ cloudflareApiToken: runtimeCloudflareApiToken,
4289
+ checkpoint: values,
4290
+ databaseUrl: installationDatabaseUrl,
4291
+ githubClientId,
4292
+ githubClientSecret,
4293
+ r2AccessKeyId,
4294
+ r2SecretAccessKey,
4295
+ existingInstallation: !credentialSetup,
4296
+ jwtSecret: await collect("jwtSecret", "VOID_PLATFORM_JWT_SECRET", "JWT signing key:", true, true, credentialSetup ? (value) => Buffer.byteLength(value, "utf8") >= 32 ? void 0 : "The JWT signing secret must contain at least 32 random bytes." : void 0),
4297
+ projectSecretKey: configured("projectSecretKey", "VOID_PLATFORM_PROJECT_SECRET_KEY") || (hasFullProjectSecretKeyring ? "" : await collect("projectSecretKey", "VOID_PLATFORM_PROJECT_SECRET_KEY", "Project encryption key:", true, true, credentialSetup ? (value) => {
4298
+ const key = Buffer.from(value, "base64");
4299
+ return key.length === 32 && key.toString("base64") === value ? void 0 : "The project encryption key must be canonical base64 encoding of exactly 32 bytes.";
4300
+ } : void 0)),
4301
+ projectSecretKeysJson: process.env.VOID_PLATFORM_PROJECT_SECRET_KEYS_JSON || "",
4302
+ projectSecretActiveKeyVersion: process.env.VOID_PLATFORM_PROJECT_SECRET_ACTIVE_KEY_VERSION || ""
4303
+ });
4304
+ progress = startProgress("Installing the platform");
4305
+ if (credentialSetup) {
4306
+ saveValues({ projectSecretKey: secrets.projectSecretKey });
4307
+ existing = completePlatformSetupDraft(existing);
4308
+ }
4309
+ const result = await installPlatform(plan, secrets, existing, {
4310
+ acquireDatabaseLifecycleLock: acquirePlatformDatabaseLifecycleLock,
4311
+ client,
4312
+ runtime,
4313
+ healthCheckResumeCommand: `${cliCommandName()} platform install --resume --name ${existing.id}`,
4314
+ onProgress: (message) => progress?.message(message),
4315
+ replanWorkers: async (manifest) => (await createPlatformInstallPlan(client, planInput, manifest, {
4316
+ authoritativeProvisionIntents: true,
4317
+ repairMissing: true
4318
+ })).actions.filter((action) => action.kind === "worker"),
4319
+ revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
4320
+ verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
4321
+ const { verifyPlatformInstallationOwnership } = await import("./platform-management-Db2PXw0B.mjs");
4322
+ const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
4323
+ const previous = manifest.resources.find((resource) => resource.key === action.key);
4324
+ return action.action === "create" && previous && previous.name !== action.name;
4325
+ }).map((action) => action.key));
4326
+ await verifyPlatformInstallationOwnership(client, manifest, {
4327
+ allowUninitializedRoutingCheckpoint: databaseCheckpointExists,
4328
+ databaseUrl: secrets.databaseUrl,
4329
+ retainedReplacementKeys
4330
+ });
4331
+ }
4332
+ });
4333
+ progress?.stop(result.waitingForDns || result.waitingForSandboxDrain ? "Installation paused; progress is saved." : "Platform installed.");
4334
+ progress = void 0;
4335
+ if (result.waitingForDns) {
4336
+ note(result.zoneNameServers.length > 0 ? `Update the domain at your registrar to use:\n${result.zoneNameServers.join("\n")}\n\nThen rerun: void platform install --resume --name ${result.manifest.id}` : `Wait for ${zoneName} to become active, then rerun:\nvoid platform install --resume --name ${result.manifest.id}`, "Waiting for nameservers");
4337
+ outro(`Installation ${result.manifest.id} is safely checkpointed.`);
4338
+ return;
4339
+ }
4340
+ if (result.waitingForSandboxDrain) {
4341
+ note("The compatible candidate runtime is live in sandbox-drain maintenance mode and application traffic remains disabled. Preview bounded work with `void platform system sandbox-drain --plan`; follow any returned `nextCursor` with `--cursor '<value>'`. Then apply with `void platform system sandbox-drain --yes` and rerun until it reports DB-backed `complete: true`. Rerun this install command afterward to enable traffic.", "Waiting for managed Sandbox drain");
4342
+ outro(`Installation ${result.manifest.id} is safely checkpointed.`);
4343
+ return;
4344
+ }
4345
+ log.success(`${result.manifest.deployment.displayName} is installed at ${new URL("/admin", plan.apiUrl).href}.`);
4346
+ log.info(`API URL: ${plan.apiUrl}`);
4347
+ if (!plan.deployment.application.zoneName) log.info("Apps will use workers.dev addresses. When your domain is ready: void platform domain set <domain>");
4348
+ outro("Run `void auth login`, then deploy an app with `void deploy --platform void`.");
4349
+ } catch (error) {
4350
+ progress?.error("Platform installation failed.");
4351
+ progress = void 0;
4352
+ if (existing && !args.plan && !(error instanceof PlatformAccessBlockedError)) log.info(`Continue with: ${cliCommandName()} platform install --resume --name ${existing.id}`);
4353
+ throw error;
4354
+ } finally {
4355
+ try {
4356
+ progress?.clear();
4357
+ } finally {
4358
+ release?.();
4359
+ }
4360
+ }
4361
+ }
4362
+ function isRuntimeTokenAuthorizationError(error, requestPathIncludes) {
4363
+ for (let depth = 0; depth < 5 && error instanceof Error; depth++, error = error.cause) if (error instanceof PlatformCloudflareApiError && [401, 403].includes(error.status) && (!requestPathIncludes || error.requestPath?.includes(requestPathIncludes))) return true;
4364
+ return false;
4365
+ }
4366
+ function formatPlatformRuntime(runtime) {
4367
+ return [
4368
+ `${import_picocolors$1.default.bold("Version:")} ${runtime.manifest.platformVersion}`,
4369
+ `${import_picocolors$1.default.bold("Source:")} ${runtime.source}`,
4370
+ `${import_picocolors$1.default.bold("Revision:")} ${runtime.manifest.sourceRevision ?? "not recorded"}`,
4371
+ `${import_picocolors$1.default.bold("Manifest SHA-256:")} ${runtime.manifestSha256}`
4372
+ ].join("\n");
4373
+ }
4374
+ async function verifyPlatformAiGatewayPlan(client, input, existing, preview, authoritativeProvisionIntents) {
4375
+ let verified;
4376
+ try {
4377
+ verified = await createPlatformInstallPlan(client, input, existing, { authoritativeProvisionIntents });
4378
+ } catch (error) {
4379
+ 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 });
4380
+ throw error;
4381
+ }
4382
+ const infrastructure = (plan) => plan.actions.filter((action) => action.kind !== "ai-gateway").map(({ action, id, key, kind, name }) => ({
4383
+ action,
4384
+ id,
4385
+ key,
4386
+ kind,
4387
+ name
4388
+ }));
4389
+ if (verified.installationId !== preview.installationId || JSON.stringify(verified.deployment) !== JSON.stringify(preview.deployment) || JSON.stringify(infrastructure(verified)) !== JSON.stringify(infrastructure(preview))) throw new Error("platform: Resources changed while checking AI Gateway access. Rerun the command to review the updated plan. No platform resources were created.");
4390
+ return verified;
4391
+ }
4392
+ function assertNoninteractivePlatformMutationAcknowledged(args, interactive) {
4393
+ if (args.subcommand === "list" || args.subcommand === "use" || args.subcommand === "status" || args.subcommand === "discover" || args.plan || interactive || args.yes) return;
4394
+ throw new Error(`platform: Non-interactive \`void platform ${args.subcommand}\` requires --yes to acknowledge Cloudflare changes. Run with --plan first to inspect them.`);
4395
+ }
4396
+ async function readAccounts(client) {
4397
+ const accounts = await client.listAccounts();
4398
+ if (accounts.length === 0) throw new Error("platform: This Cloudflare token has no accessible accounts.");
4399
+ return accounts;
4400
+ }
4401
+ async function selectAccount(accounts, selector) {
4402
+ if (selector) {
4403
+ const matches = accounts.filter((account) => account.id === selector || account.name.toLowerCase() === selector.toLowerCase());
4404
+ if (matches.length !== 1) throw new Error(`platform: Cloudflare account ${JSON.stringify(selector)} is ambiguous or unavailable.`);
4405
+ return matches[0].id;
4406
+ }
4407
+ if (accounts.length === 1) return accounts[0].id;
4408
+ if (!process.stdin.isTTY) throw new Error("platform: --account <id> is required when the token can access multiple accounts.");
4409
+ const value = await select({
4410
+ message: "Cloudflare account:",
4411
+ options: accounts.map((account) => ({
4412
+ label: account.name,
4413
+ value: account.id,
4414
+ hint: account.id
4415
+ }))
4416
+ });
4417
+ if (isCancel(value)) throw new Error("platform: Cancelled.");
4418
+ return String(value);
4419
+ }
4420
+ async function promptText(message, value, optional = false, validate) {
4421
+ if (!process.stdin.isTTY) {
4422
+ if (optional) return "";
4423
+ throw new Error(`platform: ${message.replace(/:$/, "")} is required in non-interactive mode.`);
4424
+ }
4425
+ const input = await text({
4426
+ message,
4427
+ ...typeof value === "string" ? { initialValue: value } : value,
4428
+ validate: (input) => input ? validate?.(input) : optional ? void 0 : "A value is required."
4429
+ });
4430
+ if (isCancel(input) || typeof input === "symbol") throw new Error("platform: Cancelled.");
4431
+ return input;
4432
+ }
4433
+ async function promptSecret(message, masked = true, validate, placeholder) {
4434
+ if (!process.stdin.isTTY) throw new Error(`platform: ${message.replace(/:$/, "")} is required in non-interactive mode. Set the documented VOID_PLATFORM_* environment variable.`);
4435
+ const options = {
4436
+ message,
4437
+ validate: (input) => input ? validate?.(input) : "A value is required."
4438
+ };
4439
+ const value = masked ? placeholder ? await passwordWithPlaceholder({
4440
+ ...options,
4441
+ placeholder
4442
+ }) : await password(options) : await text({
4443
+ ...options,
4444
+ placeholder
4445
+ });
4446
+ if (isCancel(value) || typeof value === "symbol") throw new Error("platform: Cancelled.");
4447
+ return value;
4448
+ }
4449
+ //#endregion
4450
+ export { resolvePlatformCloudflareCredential as S, hyperdriveOriginConfigHash as _, installPlatform as a, assertNoninteractivePlatformMutationAcknowledged, verifyPlatformRuntimeTokenPermissions as b, resolveBootstrapGitHubId as c, createPlatformInstallPlan as d, formatPlatformInstallPlan as f, PlatformCloudflareClient as g, revalidatePlatformInstallAction as h, createPlatformInstallationSecrets as i, recordPlatformWorkerTraffic as l, platformProvisionIntentHash as m, acquirePlatformDatabaseLifecycleLock as n, loadPlatformRuntime as o, platformAdoptionIntentHash as p, createPlatformHealthProbeToken as r, resolvePlatformInstallDatabaseUrl, runPlatformLifecycleCommand, recordResource as s, REMOTE_INSTALLATION_MANIFEST_KEY as t, trackedPlatformWorkerTraffic as u, hyperdriveOriginFromConnectionString as v, assertPlatformOperatorCredentialCanApplyPlan as x, hyperdriveOriginMatches as y };