@alchemy.run/fly 0.0.0 → 2.0.0-beta.85

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 (499) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +57 -4
  3. package/lib/App.d.ts +230 -0
  4. package/lib/App.d.ts.map +1 -0
  5. package/lib/App.js +338 -0
  6. package/lib/App.js.map +1 -0
  7. package/lib/AuthProvider.d.ts +22 -0
  8. package/lib/AuthProvider.d.ts.map +1 -0
  9. package/lib/AuthProvider.js +48 -0
  10. package/lib/AuthProvider.js.map +1 -0
  11. package/lib/BindService.d.ts +159 -0
  12. package/lib/BindService.d.ts.map +1 -0
  13. package/lib/BindService.js +222 -0
  14. package/lib/BindService.js.map +1 -0
  15. package/lib/Bootstrap.d.ts +8 -0
  16. package/lib/Bootstrap.d.ts.map +1 -0
  17. package/lib/Bootstrap.js +41 -0
  18. package/lib/Bootstrap.js.map +1 -0
  19. package/lib/Bucket.d.ts +278 -0
  20. package/lib/Bucket.d.ts.map +1 -0
  21. package/lib/Bucket.js +658 -0
  22. package/lib/Bucket.js.map +1 -0
  23. package/lib/BucketBinding.d.ts +33 -0
  24. package/lib/BucketBinding.d.ts.map +1 -0
  25. package/lib/BucketBinding.js +70 -0
  26. package/lib/BucketBinding.js.map +1 -0
  27. package/lib/Catalog.d.ts +28 -0
  28. package/lib/Catalog.d.ts.map +1 -0
  29. package/lib/Catalog.js +31 -0
  30. package/lib/Catalog.js.map +1 -0
  31. package/lib/Certificate.d.ts +189 -0
  32. package/lib/Certificate.d.ts.map +1 -0
  33. package/lib/Certificate.js +380 -0
  34. package/lib/Certificate.js.map +1 -0
  35. package/lib/Checkpoint.d.ts +55 -0
  36. package/lib/Checkpoint.d.ts.map +1 -0
  37. package/lib/Checkpoint.js +3 -0
  38. package/lib/Checkpoint.js.map +1 -0
  39. package/lib/CheckpointHttp.d.ts +22 -0
  40. package/lib/CheckpointHttp.d.ts.map +1 -0
  41. package/lib/CheckpointHttp.js +53 -0
  42. package/lib/CheckpointHttp.js.map +1 -0
  43. package/lib/ConnectPostgres.d.ts +58 -0
  44. package/lib/ConnectPostgres.d.ts.map +1 -0
  45. package/lib/ConnectPostgres.js +13 -0
  46. package/lib/ConnectPostgres.js.map +1 -0
  47. package/lib/ConnectPostgresHttp.d.ts +26 -0
  48. package/lib/ConnectPostgresHttp.d.ts.map +1 -0
  49. package/lib/ConnectPostgresHttp.js +57 -0
  50. package/lib/ConnectPostgresHttp.js.map +1 -0
  51. package/lib/Contributions.d.ts +7 -0
  52. package/lib/Contributions.d.ts.map +1 -0
  53. package/lib/Contributions.js +6 -0
  54. package/lib/Contributions.js.map +1 -0
  55. package/lib/Credentials.d.ts +33 -0
  56. package/lib/Credentials.d.ts.map +1 -0
  57. package/lib/Credentials.js +55 -0
  58. package/lib/Credentials.js.map +1 -0
  59. package/lib/Decrypt.d.ts +52 -0
  60. package/lib/Decrypt.d.ts.map +1 -0
  61. package/lib/Decrypt.js +3 -0
  62. package/lib/Decrypt.js.map +1 -0
  63. package/lib/DecryptHttp.d.ts +22 -0
  64. package/lib/DecryptHttp.d.ts.map +1 -0
  65. package/lib/DecryptHttp.js +43 -0
  66. package/lib/DecryptHttp.js.map +1 -0
  67. package/lib/DeleteObject.d.ts +29 -0
  68. package/lib/DeleteObject.d.ts.map +1 -0
  69. package/lib/DeleteObject.js +3 -0
  70. package/lib/DeleteObject.js.map +1 -0
  71. package/lib/DeleteObjectHttp.d.ts +12 -0
  72. package/lib/DeleteObjectHttp.d.ts.map +1 -0
  73. package/lib/DeleteObjectHttp.js +17 -0
  74. package/lib/DeleteObjectHttp.js.map +1 -0
  75. package/lib/Deployment.d.ts +51 -0
  76. package/lib/Deployment.d.ts.map +1 -0
  77. package/lib/Deployment.js +62 -0
  78. package/lib/Deployment.js.map +1 -0
  79. package/lib/DeploymentImages.d.ts +18 -0
  80. package/lib/DeploymentImages.d.ts.map +1 -0
  81. package/lib/DeploymentImages.js +61 -0
  82. package/lib/DeploymentImages.js.map +1 -0
  83. package/lib/DeploymentState.d.ts +17 -0
  84. package/lib/DeploymentState.d.ts.map +1 -0
  85. package/lib/DeploymentState.js +87 -0
  86. package/lib/DeploymentState.js.map +1 -0
  87. package/lib/Encrypt.d.ts +50 -0
  88. package/lib/Encrypt.d.ts.map +1 -0
  89. package/lib/Encrypt.js +3 -0
  90. package/lib/Encrypt.js.map +1 -0
  91. package/lib/EncryptHttp.d.ts +22 -0
  92. package/lib/EncryptHttp.d.ts.map +1 -0
  93. package/lib/EncryptHttp.js +39 -0
  94. package/lib/EncryptHttp.js.map +1 -0
  95. package/lib/Environment.d.ts +45 -0
  96. package/lib/Environment.d.ts.map +1 -0
  97. package/lib/Environment.js +43 -0
  98. package/lib/Environment.js.map +1 -0
  99. package/lib/Errors.d.ts +18 -0
  100. package/lib/Errors.d.ts.map +1 -0
  101. package/lib/Errors.js +13 -0
  102. package/lib/Errors.js.map +1 -0
  103. package/lib/Exec.d.ts +41 -0
  104. package/lib/Exec.d.ts.map +1 -0
  105. package/lib/Exec.js +3 -0
  106. package/lib/Exec.js.map +1 -0
  107. package/lib/ExecHttp.d.ts +22 -0
  108. package/lib/ExecHttp.d.ts.map +1 -0
  109. package/lib/ExecHttp.js +39 -0
  110. package/lib/ExecHttp.js.map +1 -0
  111. package/lib/GetObject.d.ts +34 -0
  112. package/lib/GetObject.d.ts.map +1 -0
  113. package/lib/GetObject.js +3 -0
  114. package/lib/GetObject.js.map +1 -0
  115. package/lib/GetObjectHttp.d.ts +12 -0
  116. package/lib/GetObjectHttp.d.ts.map +1 -0
  117. package/lib/GetObjectHttp.js +17 -0
  118. package/lib/GetObjectHttp.js.map +1 -0
  119. package/lib/GetSecret.d.ts +44 -0
  120. package/lib/GetSecret.d.ts.map +1 -0
  121. package/lib/GetSecret.js +3 -0
  122. package/lib/GetSecret.js.map +1 -0
  123. package/lib/GetSecretHttp.d.ts +22 -0
  124. package/lib/GetSecretHttp.d.ts.map +1 -0
  125. package/lib/GetSecretHttp.js +50 -0
  126. package/lib/GetSecretHttp.js.map +1 -0
  127. package/lib/HeadObject.d.ts +29 -0
  128. package/lib/HeadObject.d.ts.map +1 -0
  129. package/lib/HeadObject.js +3 -0
  130. package/lib/HeadObject.js.map +1 -0
  131. package/lib/HeadObjectHttp.d.ts +12 -0
  132. package/lib/HeadObjectHttp.d.ts.map +1 -0
  133. package/lib/HeadObjectHttp.js +17 -0
  134. package/lib/HeadObjectHttp.js.map +1 -0
  135. package/lib/IpAssignment.d.ts +258 -0
  136. package/lib/IpAssignment.d.ts.map +1 -0
  137. package/lib/IpAssignment.js +376 -0
  138. package/lib/IpAssignment.js.map +1 -0
  139. package/lib/ListObjectsV2.d.ts +29 -0
  140. package/lib/ListObjectsV2.d.ts.map +1 -0
  141. package/lib/ListObjectsV2.js +3 -0
  142. package/lib/ListObjectsV2.js.map +1 -0
  143. package/lib/ListObjectsV2Http.d.ts +12 -0
  144. package/lib/ListObjectsV2Http.d.ts.map +1 -0
  145. package/lib/ListObjectsV2Http.js +17 -0
  146. package/lib/ListObjectsV2Http.js.map +1 -0
  147. package/lib/ListSecrets.d.ts +53 -0
  148. package/lib/ListSecrets.d.ts.map +1 -0
  149. package/lib/ListSecrets.js +3 -0
  150. package/lib/ListSecrets.js.map +1 -0
  151. package/lib/ListSecretsHttp.d.ts +22 -0
  152. package/lib/ListSecretsHttp.d.ts.map +1 -0
  153. package/lib/ListSecretsHttp.js +35 -0
  154. package/lib/ListSecretsHttp.js.map +1 -0
  155. package/lib/Machine.d.ts +802 -0
  156. package/lib/Machine.d.ts.map +1 -0
  157. package/lib/Machine.js +773 -0
  158. package/lib/Machine.js.map +1 -0
  159. package/lib/MachineContainers.d.ts +44 -0
  160. package/lib/MachineContainers.d.ts.map +1 -0
  161. package/lib/MachineContainers.js +115 -0
  162. package/lib/MachineContainers.js.map +1 -0
  163. package/lib/Metadata.d.ts +109 -0
  164. package/lib/Metadata.d.ts.map +1 -0
  165. package/lib/Metadata.js +159 -0
  166. package/lib/Metadata.js.map +1 -0
  167. package/lib/MountVolume.d.ts +295 -0
  168. package/lib/MountVolume.d.ts.map +1 -0
  169. package/lib/MountVolume.js +20 -0
  170. package/lib/MountVolume.js.map +1 -0
  171. package/lib/Postgres.d.ts +298 -0
  172. package/lib/Postgres.d.ts.map +1 -0
  173. package/lib/Postgres.js +508 -0
  174. package/lib/Postgres.js.map +1 -0
  175. package/lib/PostgresMigrations.d.ts +20 -0
  176. package/lib/PostgresMigrations.d.ts.map +1 -0
  177. package/lib/PostgresMigrations.js +58 -0
  178. package/lib/PostgresMigrations.js.map +1 -0
  179. package/lib/Providers.d.ts +38 -0
  180. package/lib/Providers.d.ts.map +1 -0
  181. package/lib/Providers.js +95 -0
  182. package/lib/Providers.js.map +1 -0
  183. package/lib/PutObject.d.ts +35 -0
  184. package/lib/PutObject.d.ts.map +1 -0
  185. package/lib/PutObject.js +3 -0
  186. package/lib/PutObject.js.map +1 -0
  187. package/lib/PutObjectHttp.d.ts +12 -0
  188. package/lib/PutObjectHttp.d.ts.map +1 -0
  189. package/lib/PutObjectHttp.js +17 -0
  190. package/lib/PutObjectHttp.js.map +1 -0
  191. package/lib/ReadRedis.d.ts +26 -0
  192. package/lib/ReadRedis.d.ts.map +1 -0
  193. package/lib/ReadRedis.js +3 -0
  194. package/lib/ReadRedis.js.map +1 -0
  195. package/lib/ReadRedisHttp.d.ts +11 -0
  196. package/lib/ReadRedisHttp.d.ts.map +1 -0
  197. package/lib/ReadRedisHttp.js +15 -0
  198. package/lib/ReadRedisHttp.js.map +1 -0
  199. package/lib/ReadWriteRedis.d.ts +29 -0
  200. package/lib/ReadWriteRedis.d.ts.map +1 -0
  201. package/lib/ReadWriteRedis.js +3 -0
  202. package/lib/ReadWriteRedis.js.map +1 -0
  203. package/lib/ReadWriteRedisHttp.d.ts +11 -0
  204. package/lib/ReadWriteRedisHttp.d.ts.map +1 -0
  205. package/lib/ReadWriteRedisHttp.js +15 -0
  206. package/lib/ReadWriteRedisHttp.js.map +1 -0
  207. package/lib/Redis.d.ts +252 -0
  208. package/lib/Redis.d.ts.map +1 -0
  209. package/lib/Redis.js +611 -0
  210. package/lib/Redis.js.map +1 -0
  211. package/lib/RedisBinding.d.ts +7 -0
  212. package/lib/RedisBinding.d.ts.map +1 -0
  213. package/lib/RedisBinding.js +53 -0
  214. package/lib/RedisBinding.js.map +1 -0
  215. package/lib/RedisHttp.d.ts +8 -0
  216. package/lib/RedisHttp.d.ts.map +1 -0
  217. package/lib/RedisHttp.js +11 -0
  218. package/lib/RedisHttp.js.map +1 -0
  219. package/lib/Region.d.ts +16 -0
  220. package/lib/Region.d.ts.map +1 -0
  221. package/lib/Region.js +13 -0
  222. package/lib/Region.js.map +1 -0
  223. package/lib/Secret.d.ts +190 -0
  224. package/lib/Secret.d.ts.map +1 -0
  225. package/lib/Secret.js +310 -0
  226. package/lib/Secret.js.map +1 -0
  227. package/lib/SecretHttp.d.ts +54 -0
  228. package/lib/SecretHttp.d.ts.map +1 -0
  229. package/lib/SecretHttp.js +66 -0
  230. package/lib/SecretHttp.js.map +1 -0
  231. package/lib/SecretKey.d.ts +164 -0
  232. package/lib/SecretKey.d.ts.map +1 -0
  233. package/lib/SecretKey.js +277 -0
  234. package/lib/SecretKey.js.map +1 -0
  235. package/lib/SecretKeyHttp.d.ts +14 -0
  236. package/lib/SecretKeyHttp.d.ts.map +1 -0
  237. package/lib/SecretKeyHttp.js +5 -0
  238. package/lib/SecretKeyHttp.js.map +1 -0
  239. package/lib/Service.d.ts +1017 -0
  240. package/lib/Service.d.ts.map +1 -0
  241. package/lib/Service.js +1430 -0
  242. package/lib/Service.js.map +1 -0
  243. package/lib/Sign.d.ts +36 -0
  244. package/lib/Sign.d.ts.map +1 -0
  245. package/lib/Sign.js +3 -0
  246. package/lib/Sign.js.map +1 -0
  247. package/lib/SignHttp.d.ts +22 -0
  248. package/lib/SignHttp.d.ts.map +1 -0
  249. package/lib/SignHttp.js +36 -0
  250. package/lib/SignHttp.js.map +1 -0
  251. package/lib/Sprite.d.ts +283 -0
  252. package/lib/Sprite.d.ts.map +1 -0
  253. package/lib/Sprite.js +471 -0
  254. package/lib/Sprite.js.map +1 -0
  255. package/lib/SpriteHttp.d.ts +25 -0
  256. package/lib/SpriteHttp.d.ts.map +1 -0
  257. package/lib/SpriteHttp.js +33 -0
  258. package/lib/SpriteHttp.js.map +1 -0
  259. package/lib/Verify.d.ts +37 -0
  260. package/lib/Verify.d.ts.map +1 -0
  261. package/lib/Verify.js +3 -0
  262. package/lib/Verify.js.map +1 -0
  263. package/lib/VerifyHttp.d.ts +22 -0
  264. package/lib/VerifyHttp.d.ts.map +1 -0
  265. package/lib/VerifyHttp.js +38 -0
  266. package/lib/VerifyHttp.js.map +1 -0
  267. package/lib/Volume.d.ts +53 -0
  268. package/lib/Volume.d.ts.map +1 -0
  269. package/lib/Volume.js +188 -0
  270. package/lib/Volume.js.map +1 -0
  271. package/lib/VolumeSnapshot.d.ts +135 -0
  272. package/lib/VolumeSnapshot.d.ts.map +1 -0
  273. package/lib/VolumeSnapshot.js +213 -0
  274. package/lib/VolumeSnapshot.js.map +1 -0
  275. package/lib/Website/AssetDeployment.d.ts +43 -0
  276. package/lib/Website/AssetDeployment.d.ts.map +1 -0
  277. package/lib/Website/AssetDeployment.js +162 -0
  278. package/lib/Website/AssetDeployment.js.map +1 -0
  279. package/lib/Website/Astro.d.ts +89 -0
  280. package/lib/Website/Astro.d.ts.map +1 -0
  281. package/lib/Website/Astro.js +54 -0
  282. package/lib/Website/Astro.js.map +1 -0
  283. package/lib/Website/Foldkit.d.ts +52 -0
  284. package/lib/Website/Foldkit.d.ts.map +1 -0
  285. package/lib/Website/Foldkit.js +45 -0
  286. package/lib/Website/Foldkit.js.map +1 -0
  287. package/lib/Website/FrameworkSite.d.ts +212 -0
  288. package/lib/Website/FrameworkSite.d.ts.map +1 -0
  289. package/lib/Website/FrameworkSite.js +205 -0
  290. package/lib/Website/FrameworkSite.js.map +1 -0
  291. package/lib/Website/Nextjs.d.ts +54 -0
  292. package/lib/Website/Nextjs.d.ts.map +1 -0
  293. package/lib/Website/Nextjs.js +46 -0
  294. package/lib/Website/Nextjs.js.map +1 -0
  295. package/lib/Website/Nuxt.d.ts +51 -0
  296. package/lib/Website/Nuxt.d.ts.map +1 -0
  297. package/lib/Website/Nuxt.js +36 -0
  298. package/lib/Website/Nuxt.js.map +1 -0
  299. package/lib/Website/Octane.d.ts +49 -0
  300. package/lib/Website/Octane.d.ts.map +1 -0
  301. package/lib/Website/Octane.js +39 -0
  302. package/lib/Website/Octane.js.map +1 -0
  303. package/lib/Website/ReactRouter.d.ts +80 -0
  304. package/lib/Website/ReactRouter.d.ts.map +1 -0
  305. package/lib/Website/ReactRouter.js +70 -0
  306. package/lib/Website/ReactRouter.js.map +1 -0
  307. package/lib/Website/SolidStart.d.ts +93 -0
  308. package/lib/Website/SolidStart.d.ts.map +1 -0
  309. package/lib/Website/SolidStart.js +71 -0
  310. package/lib/Website/SolidStart.js.map +1 -0
  311. package/lib/Website/StaticSite.d.ts +162 -0
  312. package/lib/Website/StaticSite.d.ts.map +1 -0
  313. package/lib/Website/StaticSite.js +180 -0
  314. package/lib/Website/StaticSite.js.map +1 -0
  315. package/lib/Website/SvelteKit.d.ts +52 -0
  316. package/lib/Website/SvelteKit.d.ts.map +1 -0
  317. package/lib/Website/SvelteKit.js +37 -0
  318. package/lib/Website/SvelteKit.js.map +1 -0
  319. package/lib/Website/TanStackStart.d.ts +76 -0
  320. package/lib/Website/TanStackStart.d.ts.map +1 -0
  321. package/lib/Website/TanStackStart.js +66 -0
  322. package/lib/Website/TanStackStart.js.map +1 -0
  323. package/lib/Website/Vinext.d.ts +73 -0
  324. package/lib/Website/Vinext.d.ts.map +1 -0
  325. package/lib/Website/Vinext.js +67 -0
  326. package/lib/Website/Vinext.js.map +1 -0
  327. package/lib/Website/Vite.d.ts +112 -0
  328. package/lib/Website/Vite.d.ts.map +1 -0
  329. package/lib/Website/Vite.js +100 -0
  330. package/lib/Website/Vite.js.map +1 -0
  331. package/lib/Website/Vocs.d.ts +52 -0
  332. package/lib/Website/Vocs.d.ts.map +1 -0
  333. package/lib/Website/Vocs.js +38 -0
  334. package/lib/Website/Vocs.js.map +1 -0
  335. package/lib/Website/Waku.d.ts +64 -0
  336. package/lib/Website/Waku.d.ts.map +1 -0
  337. package/lib/Website/Waku.js +39 -0
  338. package/lib/Website/Waku.js.map +1 -0
  339. package/lib/Website/index.d.ts +17 -0
  340. package/lib/Website/index.d.ts.map +1 -0
  341. package/lib/Website/index.js +17 -0
  342. package/lib/Website/index.js.map +1 -0
  343. package/lib/WriteCertificates.d.ts +63 -0
  344. package/lib/WriteCertificates.d.ts.map +1 -0
  345. package/lib/WriteCertificates.js +3 -0
  346. package/lib/WriteCertificates.js.map +1 -0
  347. package/lib/WriteCertificatesHttp.d.ts +25 -0
  348. package/lib/WriteCertificatesHttp.d.ts.map +1 -0
  349. package/lib/WriteCertificatesHttp.js +67 -0
  350. package/lib/WriteCertificatesHttp.js.map +1 -0
  351. package/lib/WriteRedis.d.ts +26 -0
  352. package/lib/WriteRedis.d.ts.map +1 -0
  353. package/lib/WriteRedis.js +3 -0
  354. package/lib/WriteRedis.js.map +1 -0
  355. package/lib/WriteRedisHttp.d.ts +11 -0
  356. package/lib/WriteRedisHttp.d.ts.map +1 -0
  357. package/lib/WriteRedisHttp.js +15 -0
  358. package/lib/WriteRedisHttp.js.map +1 -0
  359. package/lib/WriteSecret.d.ts +68 -0
  360. package/lib/WriteSecret.d.ts.map +1 -0
  361. package/lib/WriteSecret.js +4 -0
  362. package/lib/WriteSecret.js.map +1 -0
  363. package/lib/WriteSecretHttp.d.ts +26 -0
  364. package/lib/WriteSecretHttp.d.ts.map +1 -0
  365. package/lib/WriteSecretHttp.js +54 -0
  366. package/lib/WriteSecretHttp.js.map +1 -0
  367. package/lib/bluegreen.d.ts +36 -0
  368. package/lib/bluegreen.d.ts.map +1 -0
  369. package/lib/bluegreen.js +460 -0
  370. package/lib/bluegreen.js.map +1 -0
  371. package/lib/hosted.d.ts +171 -0
  372. package/lib/hosted.d.ts.map +1 -0
  373. package/lib/hosted.js +458 -0
  374. package/lib/hosted.js.map +1 -0
  375. package/lib/index.d.ts +65 -0
  376. package/lib/index.d.ts.map +1 -0
  377. package/lib/index.js +63 -0
  378. package/lib/index.js.map +1 -0
  379. package/lib/internal/RemoteBuilder.d.ts +28 -0
  380. package/lib/internal/RemoteBuilder.d.ts.map +1 -0
  381. package/lib/internal/RemoteBuilder.js +158 -0
  382. package/lib/internal/RemoteBuilder.js.map +1 -0
  383. package/lib/leases.d.ts +53 -0
  384. package/lib/leases.d.ts.map +1 -0
  385. package/lib/leases.js +279 -0
  386. package/lib/leases.js.map +1 -0
  387. package/lib/ports.d.ts +51 -0
  388. package/lib/ports.d.ts.map +1 -0
  389. package/lib/ports.js +123 -0
  390. package/lib/ports.js.map +1 -0
  391. package/lib/replicas.d.ts +270 -0
  392. package/lib/replicas.d.ts.map +1 -0
  393. package/lib/replicas.js +969 -0
  394. package/lib/replicas.js.map +1 -0
  395. package/lib/rpc.d.ts +35 -0
  396. package/lib/rpc.d.ts.map +1 -0
  397. package/lib/rpc.js +113 -0
  398. package/lib/rpc.js.map +1 -0
  399. package/package.json +83 -3
  400. package/src/App.ts +514 -0
  401. package/src/AuthProvider.ts +68 -0
  402. package/src/BindService.ts +303 -0
  403. package/src/Bootstrap.ts +66 -0
  404. package/src/Bucket.ts +918 -0
  405. package/src/BucketBinding.ts +128 -0
  406. package/src/Catalog.ts +43 -0
  407. package/src/Certificate.ts +548 -0
  408. package/src/Checkpoint.ts +69 -0
  409. package/src/CheckpointHttp.ts +66 -0
  410. package/src/ConnectPostgres.ts +68 -0
  411. package/src/ConnectPostgresHttp.ts +77 -0
  412. package/src/Contributions.ts +9 -0
  413. package/src/Credentials.ts +75 -0
  414. package/src/Decrypt.ts +62 -0
  415. package/src/DecryptHttp.ts +52 -0
  416. package/src/DeleteObject.ts +43 -0
  417. package/src/DeleteObjectHttp.ts +20 -0
  418. package/src/Deployment.ts +121 -0
  419. package/src/DeploymentImages.ts +84 -0
  420. package/src/DeploymentState.ts +104 -0
  421. package/src/Encrypt.ts +60 -0
  422. package/src/EncryptHttp.ts +48 -0
  423. package/src/Environment.ts +81 -0
  424. package/src/Errors.ts +14 -0
  425. package/src/Exec.ts +50 -0
  426. package/src/ExecHttp.ts +47 -0
  427. package/src/GetObject.ts +48 -0
  428. package/src/GetObjectHttp.ts +20 -0
  429. package/src/GetSecret.ts +48 -0
  430. package/src/GetSecretHttp.ts +60 -0
  431. package/src/HeadObject.ts +43 -0
  432. package/src/HeadObjectHttp.ts +20 -0
  433. package/src/IpAssignment.ts +522 -0
  434. package/src/ListObjectsV2.ts +43 -0
  435. package/src/ListObjectsV2Http.ts +20 -0
  436. package/src/ListSecrets.ts +57 -0
  437. package/src/ListSecretsHttp.ts +43 -0
  438. package/src/Machine.ts +1270 -0
  439. package/src/MachineContainers.ts +152 -0
  440. package/src/Metadata.ts +195 -0
  441. package/src/MountVolume.ts +315 -0
  442. package/src/Postgres.ts +777 -0
  443. package/src/PostgresMigrations.ts +93 -0
  444. package/src/Providers.ts +155 -0
  445. package/src/PutObject.ts +49 -0
  446. package/src/PutObjectHttp.ts +20 -0
  447. package/src/ReadRedis.ts +31 -0
  448. package/src/ReadRedisHttp.ts +18 -0
  449. package/src/ReadWriteRedis.ts +33 -0
  450. package/src/ReadWriteRedisHttp.ts +18 -0
  451. package/src/Redis.ts +823 -0
  452. package/src/RedisBinding.ts +64 -0
  453. package/src/RedisHttp.ts +14 -0
  454. package/src/Region.ts +44 -0
  455. package/src/Secret.ts +405 -0
  456. package/src/SecretHttp.ts +149 -0
  457. package/src/SecretKey.ts +389 -0
  458. package/src/SecretKeyHttp.ts +23 -0
  459. package/src/Service.ts +1968 -0
  460. package/src/Sign.ts +46 -0
  461. package/src/SignHttp.ts +44 -0
  462. package/src/Sprite.ts +673 -0
  463. package/src/SpriteHttp.ts +50 -0
  464. package/src/Verify.ts +47 -0
  465. package/src/VerifyHttp.ts +46 -0
  466. package/src/Volume.ts +264 -0
  467. package/src/VolumeSnapshot.ts +321 -0
  468. package/src/Website/AssetDeployment.ts +292 -0
  469. package/src/Website/Astro.ts +92 -0
  470. package/src/Website/Foldkit.ts +54 -0
  471. package/src/Website/FrameworkSite.ts +449 -0
  472. package/src/Website/Nextjs.ts +50 -0
  473. package/src/Website/Nuxt.ts +48 -0
  474. package/src/Website/Octane.ts +44 -0
  475. package/src/Website/ReactRouter.ts +76 -0
  476. package/src/Website/SolidStart.ts +90 -0
  477. package/src/Website/StaticSite.ts +295 -0
  478. package/src/Website/SvelteKit.ts +49 -0
  479. package/src/Website/TanStackStart.ts +72 -0
  480. package/src/Website/Vinext.ts +79 -0
  481. package/src/Website/Vite.ts +121 -0
  482. package/src/Website/Vocs.ts +50 -0
  483. package/src/Website/Waku.ts +65 -0
  484. package/src/Website/index.ts +16 -0
  485. package/src/WriteCertificates.ts +90 -0
  486. package/src/WriteCertificatesHttp.ts +93 -0
  487. package/src/WriteRedis.ts +31 -0
  488. package/src/WriteRedisHttp.ts +18 -0
  489. package/src/WriteSecret.ts +86 -0
  490. package/src/WriteSecretHttp.ts +71 -0
  491. package/src/bluegreen.ts +711 -0
  492. package/src/globals.d.ts +2 -0
  493. package/src/hosted.ts +667 -0
  494. package/src/index.ts +64 -0
  495. package/src/internal/RemoteBuilder.ts +242 -0
  496. package/src/leases.ts +437 -0
  497. package/src/ports.ts +177 -0
  498. package/src/replicas.ts +1480 -0
  499. package/src/rpc.ts +136 -0
package/src/Service.ts ADDED
@@ -0,0 +1,1968 @@
1
+ import { DockerLive, Docker } from "@alchemy.run/docker/Docker";
2
+ import type {
3
+ FlyMachineConfig,
4
+ FlyMachineGuest,
5
+ FlyMachineMount,
6
+ FlyMachineService,
7
+ FlyStatic,
8
+ Machine as FlyMachine,
9
+ } from "@distilled.cloud/fly-io/machines";
10
+ import * as machines from "@distilled.cloud/fly-io/machines";
11
+ import { AlchemyContext } from "alchemy/AlchemyContext";
12
+ import * as Bundle from "alchemy/Bundle/Bundle";
13
+ import { deepEqual, isResolved } from "alchemy/Diff";
14
+ import type { Input } from "alchemy/Input";
15
+ import { Platform, type Main, type MainRpc, type PlatformProps } from "alchemy/Platform";
16
+ import * as Provider from "alchemy/Provider";
17
+ import { makeRandom } from "alchemy/Random";
18
+ import type { Resource } from "alchemy/Resource";
19
+ import type { ServerHost } from "alchemy/Runtime/Process";
20
+ import { packEnvValue } from "alchemy/RuntimeContext";
21
+ import { Stack } from "alchemy/Stack";
22
+ import * as Data from "effect/Data";
23
+ import * as Effect from "effect/Effect";
24
+ import * as Layer from "effect/Layer";
25
+ import * as Redacted from "effect/Redacted";
26
+ import { App, deleteApp, ensureApp } from "./App.ts";
27
+ import { boundTargetEnvKeys } from "./BindService.ts";
28
+ import { attachBucketSecrets } from "./Bucket.ts";
29
+ import {
30
+ deploymentPolicy,
31
+ validateDeployment,
32
+ type MachineDeploy,
33
+ type MachineShutdown,
34
+ type MachineCheck,
35
+ } from "./Deployment.ts";
36
+ import { resolveOrgSlug } from "./Environment.ts";
37
+ import {
38
+ collectBindingState,
39
+ createFlyHostedSupport,
40
+ createFlyHostRuntimeContext,
41
+ defaultHttpServices,
42
+ DEFAULT_PORT,
43
+ toEnvRecord,
44
+ type FlyBuildOptions,
45
+ type FlyHostRuntimeContext,
46
+ type HostedProgramProps,
47
+ } from "./hosted.ts";
48
+ import { ensureFlycastAddress, hasPublicAddress, syncOwnedAppAddresses } from "./IpAssignment.ts";
49
+ import type { MachineGuest, MachineImageRef, MachineService } from "./Machine.ts";
50
+ import {
51
+ alchemyMetadataKeys,
52
+ createFlyAppName,
53
+ createFlyResourceName,
54
+ diffMachineMetadata,
55
+ matchesAlchemyPhysicalName,
56
+ sanitizeFlyAppName,
57
+ } from "./Metadata.ts";
58
+ import type { BoundTarget, MountedDisk, ServiceBinding } from "./MountVolume.ts";
59
+ import { bindingPortOf, findPortConflict, portsOfFly } from "./ports.ts";
60
+ import { attachPostgresSecrets } from "./Postgres.ts";
61
+ import type { Providers } from "./Providers.ts";
62
+ import { attachRedisSecrets } from "./Redis.ts";
63
+ import { type Region, regionList } from "./Region.ts";
64
+ import {
65
+ deleteReplicaSet,
66
+ hasPublishedService,
67
+ listReplicaSets,
68
+ observeReplicaSet,
69
+ reconcileReplicas,
70
+ resolveCount,
71
+ sameServices,
72
+ toFlyService,
73
+ volumeIdsOf,
74
+ type Replica,
75
+ type ReplicaSet,
76
+ } from "./replicas.ts";
77
+ import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_ORG_ENV, RPC_TOKEN_ENV } from "./rpc.ts";
78
+
79
+ /**
80
+ * A resource-valued prop: the resource itself, or an Effect that produces
81
+ * it (so `yield* App(...)` and `App(...)` both type-check).
82
+ */
83
+ type Ref<T> = T | Effect.Effect<T, never, Providers>;
84
+
85
+ const DEFAULT_REGION = "iad";
86
+ const DEFAULT_CPU_KIND = "shared";
87
+ const DEFAULT_CPUS = 1;
88
+ const DEFAULT_MEMORY_MB = 256;
89
+
90
+ export interface ServiceProps extends PlatformProps {
91
+ /** Deployment strategy and readiness deadline. Defaults to in-place rolling updates. */
92
+ deploy?: MachineDeploy;
93
+ /** Graceful process shutdown. Defaults to SIGTERM / 30 seconds when blue/green is enabled. */
94
+ shutdown?: MachineShutdown;
95
+ /** Named readiness checks for workers without public services. */
96
+ checks?: Record<string, MachineCheck>;
97
+ /**
98
+ * Run inside an existing {@link App} instead of the Service's own App.
99
+ * The Service then shares that App's hostname, addresses, and secrets,
100
+ * and Alchemy does not manage addresses for it. Accepts a `Fly.App` or
101
+ * an Effect that produces one. Changing it, or adding or removing it,
102
+ * replaces the Service.
103
+ *
104
+ * By default each Service owns its App, so it gets its own
105
+ * `{name}.fly.dev` hostname, addresses, logs, and metrics.
106
+ */
107
+ app?: Ref<App>;
108
+ /**
109
+ * Whether the Service is reachable from the internet. A public Service
110
+ * gets a shared IPv4 and an IPv6 and serves `https://{name}.fly.dev`. A
111
+ * private Service is reachable only from other Apps in the organization,
112
+ * at `http://{name}.flycast`. Every address is free. Only applies when
113
+ * the Service owns its App (no `app`).
114
+ *
115
+ * @default true
116
+ */
117
+ public?: boolean;
118
+ /**
119
+ * Private network the Service's App joins. Only Apps on the same
120
+ * network can reach it, over `privateUrl` or `.internal`; every other
121
+ * App in the organization, including those on the default network,
122
+ * cannot resolve it. Services on one network reach each other freely.
123
+ * Use {@link stackNetwork} for a network unique to the stack and stage.
124
+ * Fly creates the network with the first App that names it. Changing
125
+ * it replaces the Service. Only applies when the Service owns its App
126
+ * (no `app`).
127
+ *
128
+ * @default the organization's default network, shared by every App
129
+ */
130
+ network?: string;
131
+ /**
132
+ * Plain-HTTP port Alchemy publishes for callers that bind this Service
133
+ * with {@link bindService}, when the Service publishes no plain-HTTP port
134
+ * of its own (a public Service's defaults are an HTTPS redirect on 80 and
135
+ * HTTPS on 443). Fly issues no certificate for `.flycast`, so bound
136
+ * callers use plain HTTP inside Fly's WireGuard-encrypted private network.
137
+ * Requests on this port must come from Fly's private network. Change it
138
+ * when the port collides with another Service in the same App.
139
+ *
140
+ * @default 7780
141
+ */
142
+ bindingPort?: number;
143
+ /**
144
+ * Caller token for bound callers. Set automatically from an
145
+ * `Alchemy.Random` child resource; do not pass it.
146
+ * @internal
147
+ */
148
+ rpcToken?: Redacted.Redacted<string>;
149
+ /**
150
+ * Module entrypoint bundled with rolldown and baked into a Docker
151
+ * image pushed to `registry.fly.io`. Typically `import.meta.url`.
152
+ * A content-hash change updates the workload using the selected deployment strategy.
153
+ */
154
+ main: string;
155
+ /**
156
+ * Region or regions to run in (`"iad"`, `["iad", "lhr"]`). The
157
+ * Service runs `count` Machines in each region behind one hostname;
158
+ * Fly's proxy sends each request to the nearest healthy Machine.
159
+ * Adding or removing a region updates the Service in place: Machines
160
+ * in a removed region are deleted, new regions get new Machines.
161
+ *
162
+ * @default "iad"
163
+ */
164
+ region?: Region | Region[];
165
+ /**
166
+ * Number of Machines to provision in each region, including
167
+ * stopped/suspended idle capacity. Fly's proxy load-balances
168
+ * `{app}.fly.dev` across available replicas.
169
+ * Blue/green checks a representative and the required running floor while
170
+ * preserving idle nonrepresentatives. Each replica gets its own Volume
171
+ * from every `MountVolume` binding; attached volumes require rolling updates.
172
+ *
173
+ * @default 1
174
+ */
175
+ count?: number;
176
+ /**
177
+ * Guest size. Defaults to shared-cpu-1x 256 MB.
178
+ */
179
+ guest?: MachineGuest;
180
+ /**
181
+ * Port the hosted HTTP server listens on. Written to `PORT` and used
182
+ * as the Fly proxy `internal_port`.
183
+ *
184
+ * @default 3000
185
+ */
186
+ port?: number;
187
+ /**
188
+ * Named export to load from `main`.
189
+ *
190
+ * @default "default"
191
+ */
192
+ handler?: string;
193
+ /**
194
+ * Additional environment variables for the hosted process. Merged
195
+ * after binding-injected `env`.
196
+ */
197
+ env?: Record<string, any>;
198
+ /**
199
+ * Bundler configuration for `main`: rolldown `input`/`output`
200
+ * overrides, pure-annotation options (`pure`), and `install` for
201
+ * packages that must ship as real `node_modules` (see {@link FlyBuildOptions}).
202
+ */
203
+ build?: FlyBuildOptions;
204
+ /**
205
+ * Environment image used as the generated Dockerfile's `FROM`. Must
206
+ * be able to run Node (websites and Effect-native Services use Node
207
+ * in production).
208
+ *
209
+ * @default "node:26-slim"
210
+ */
211
+ image?: string;
212
+ /**
213
+ * Fly proxy services. Defaults to HTTP 80 + HTTPS 443 → {@link port}.
214
+ */
215
+ services?: MachineService[];
216
+ /**
217
+ * Name of the Service's App, which is its `{name}.fly.dev` and
218
+ * `{name}.flycast` hostname. Globally unique across Fly. If omitted, a
219
+ * unique name is generated from the stack, stage and logical ID.
220
+ * Changing it replaces the Service.
221
+ *
222
+ * When `app` is set, this is instead the Machine name (unique per App;
223
+ * in blue/green mode a base for generation-qualified physical names).
224
+ */
225
+ name?: string;
226
+ /**
227
+ * Extra host directories copied into the Machine image next to the
228
+ * bundled entry (e.g. a website `clientDirectory` at `/app/dist`).
229
+ * Hashed into `code.hash` so asset changes update the image.
230
+ * Destination is relative to `/app`.
231
+ */
232
+ extraFiles?: ReadonlyArray<{ source: string; dest: string }>;
233
+ /**
234
+ * Fly proxy static-file maps. Matching GET paths skip the process and
235
+ * are served from the image (`guestPath`) or a Tigris bucket
236
+ * (`tigrisBucket`). Website composites publish hashed client assets
237
+ * this way.
238
+ */
239
+ statics?: ReadonlyArray<{
240
+ /** Path inside the image, or key prefix in {@link tigrisBucket}. */
241
+ guestPath: string;
242
+ /** URL prefix, e.g. `"/"`. */
243
+ urlPrefix: string;
244
+ /** Tigris bucket name. When set, files come from the bucket. */
245
+ tigrisBucket?: string;
246
+ /** Directory index file (`index.html`) for Tigris statics. */
247
+ indexDocument?: string;
248
+ }>;
249
+ }
250
+
251
+ /** A port the Service publishes on Fly's proxy. */
252
+ export interface ServiceEndpoint {
253
+ /**
254
+ * Hostname the port is published on: `{appName}.fly.dev`, or
255
+ * `{appName}.flycast` for a private Service.
256
+ */
257
+ host: string;
258
+ /** Port on Fly's proxy. */
259
+ port: number;
260
+ /** Port the process listens on inside the Machine. */
261
+ internalPort: number | undefined;
262
+ /** `tcp` or `udp`. */
263
+ protocol: string;
264
+ /** Fly proxy handlers (`tls`, `http`, `pg_tls`, …). Empty for raw TCP or UDP. */
265
+ handlers: string[];
266
+ /**
267
+ * URL when the port speaks HTTP: `https://` for `tls` + `http`, `http://`
268
+ * for plain `http`, with `:{port}` unless it is 443 or 80. `undefined`
269
+ * for other protocols, and for TLS ports on `.flycast` (Fly issues no
270
+ * certificate there).
271
+ */
272
+ url: string | undefined;
273
+ }
274
+
275
+ export type Service = Resource<
276
+ "Fly.Service",
277
+ ServiceProps,
278
+ {
279
+ /** Whether recovery must finish an interrupted deployment. */
280
+ rolloutPending?: boolean;
281
+ /** Fly App name the Machines run in. */
282
+ appName: string;
283
+ /** Whether the Service created and manages {@link appName}. */
284
+ ownsApp?: boolean;
285
+ /**
286
+ * Private network the Service's Machines are on: `network`, or the
287
+ * shared App's network. `undefined` for the organization's default.
288
+ */
289
+ network?: string;
290
+ /** Token bound callers send with RPC calls. */
291
+ rpcToken?: Redacted.Redacted<string>;
292
+ /** Fly Machine id of replica 0. */
293
+ machineId: string;
294
+ /** Fly Machine ids of every replica. */
295
+ machineIds: string[];
296
+ /** Logical base for generation-qualified Machine names. */
297
+ baseName?: string;
298
+ /** Machine name of replica 0 (unique per App). Changes during blue/green deployment. */
299
+ name: string;
300
+ /** Region of the first Machine. */
301
+ region: string;
302
+ /** Every region the Service's Machines run in. */
303
+ regions: string[];
304
+ /** Observed state of replica 0 (`created`, `started`, `stopped`, …). */
305
+ state: string;
306
+ /**
307
+ * Main public HTTP endpoint: the first published `tls` + `http` port
308
+ * as `https://{appName}.fly.dev[:port]`, else the first plain `http`
309
+ * port as `http://{appName}.fly.dev[:port]`. `https://{appName}.fly.dev`
310
+ * for the default services. `undefined` for a private Service or when
311
+ * no port speaks HTTP. When `app` is set, the App's addresses are
312
+ * managed elsewhere, so this is where the Service answers once the App
313
+ * is reachable. See {@link endpoints} for every published port.
314
+ */
315
+ url: string | undefined;
316
+ /**
317
+ * Every port the Service publishes, in declaration order. Ports that
318
+ * only redirect HTTP to HTTPS (`forceHttps`) and port ranges are
319
+ * omitted. Empty when nothing is published.
320
+ */
321
+ endpoints: ServiceEndpoint[];
322
+ /**
323
+ * Address bound callers use over Fly's private network:
324
+ * `http://{appName}.flycast`, with the port when it is not 80. It is
325
+ * the Service's first plain-HTTP port, or {@link ServiceProps.bindingPort}
326
+ * when it has none. Reachable only from Apps on the Service's network.
327
+ * `undefined` when nothing is published.
328
+ */
329
+ privateUrl: string | undefined;
330
+ /** Parsed image reference from Fly. */
331
+ imageRef: MachineImageRef | undefined;
332
+ /** Number of Machines in each region. */
333
+ count: number;
334
+ /** Disks mounted on replica 0. */
335
+ mounts: MountedDisk[];
336
+ /** Every replica in the set. */
337
+ replicas: Replica[];
338
+ /** Content hash of the bundled program's image. */
339
+ code: { hash: string };
340
+ },
341
+ ServiceBinding,
342
+ Providers
343
+ >;
344
+
345
+ export const isService = (value: unknown): value is Service =>
346
+ typeof value === "object" &&
347
+ value !== null &&
348
+ (value as { Type?: string }).Type === "Fly.Service";
349
+
350
+ export type ServiceServices = ServerHost;
351
+
352
+ /**
353
+ * What a Service's program returns: `fetch` for HTTP, `run` for a
354
+ * background loop, and any other methods, which bound callers invoke
355
+ * with {@link bindService}.
356
+ */
357
+ export type ServiceShape =
358
+ | Main<ServiceServices>
359
+ | (Exclude<Main<ServiceServices>, void> & MainRpc<ServiceServices>);
360
+
361
+ export type ServiceRuntimeContext = FlyHostRuntimeContext;
362
+
363
+ /**
364
+ * A Service is an Effect program running on Fly.io Machines in its own
365
+ * Fly App. It gets its own `{name}.fly.dev` hostname, addresses, logs,
366
+ * and metrics, the way each Cloudflare Worker is its own endpoint. Set
367
+ * `count` to scale it up or down.
368
+ *
369
+ * @see https://fly.io/docs/machines/api/machines-resource/
370
+ *
371
+ * ### Declare a Service
372
+ * A Service is a class. Props describe the Machine. The Effect is the
373
+ * program that runs on it.
374
+ *
375
+ * The Service creates its Fly App, and deleting the Service deletes it.
376
+ * `main: import.meta.url` is the bundle entrypoint. Alchemy bundles this
377
+ * file with Rolldown, builds a Docker image (default `node:26-slim`), and
378
+ * pushes it to `registry.fly.io/{app}:{id}-{hash}`.
379
+ *
380
+ * **Example:** Class + main
381
+ * ```typescript
382
+ * // src/api.ts
383
+ * import * as Fly from "@alchemy.run/fly";
384
+ * import * as Effect from "effect/Effect";
385
+ *
386
+ * export default class Api extends Fly.Service<Api>()(
387
+ * "Api",
388
+ * { main: import.meta.url },
389
+ * Effect.gen(function* () {
390
+ * return {};
391
+ * }),
392
+ * ) {}
393
+ * ```
394
+ *
395
+ * ### Serve HTTP with fetch
396
+ * Return `fetch` from the init Effect to boot an HTTP server. Omit
397
+ * `fetch` for a background service.
398
+ *
399
+ * **Example:** Hello
400
+ * ```typescript
401
+ * export default class Api extends Fly.Service<Api>()(
402
+ * "Api",
403
+ * { main: import.meta.url },
404
+ * Effect.gen(function* () {
405
+ * return {
406
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
407
+ * };
408
+ * }),
409
+ * ) {}
410
+ * ```
411
+ *
412
+ * ### Pin a region
413
+ * Fly Machines live in a region. Default is `iad`. See
414
+ * [Regions](/fly/compute/regions) for the list of codes.
415
+ *
416
+ * **Example:** Region
417
+ * ```typescript
418
+ * export default class Api extends Fly.Service<Api>()(
419
+ * "Api",
420
+ * { main: import.meta.url, region: "iad" },
421
+ * Effect.gen(function* () {
422
+ * return {
423
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
424
+ * };
425
+ * }),
426
+ * ) {}
427
+ * ```
428
+ *
429
+ * ### Run in several regions
430
+ * Pass a list to run the same program in several regions behind one
431
+ * hostname. `count` Machines run in each region, and Fly's proxy sends
432
+ * each request to the nearest healthy one. `regions` on the Service
433
+ * lists where it runs.
434
+ *
435
+ * **Example:** Two regions, two Machines each
436
+ * ```typescript
437
+ * export default class Api extends Fly.Service<Api>()(
438
+ * "Api",
439
+ * { main: import.meta.url, region: ["iad", "lhr"], count: 2 },
440
+ * Effect.gen(function* () {
441
+ * return {
442
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
443
+ * };
444
+ * }),
445
+ * ) {}
446
+ * ```
447
+ *
448
+ * Adding or removing a region updates the Service in place and keeps
449
+ * its App and hostname. Machines already in a kept region stay; a
450
+ * removed region's Machines are deleted. Each Machine's Volume from
451
+ * `MountVolume` is created in that Machine's region.
452
+ *
453
+ * ### Set the port
454
+ * `port` is the port the process listens on inside the Machine.
455
+ * Alchemy writes it to `PORT`. Default is `3000`.
456
+ *
457
+ * **Example:** Port 3000
458
+ * ```typescript
459
+ * export default class Api extends Fly.Service<Api>()(
460
+ * "Api",
461
+ * { main: import.meta.url, region: "iad", port: 3000 },
462
+ * Effect.gen(function* () {
463
+ * return {
464
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
465
+ * };
466
+ * }),
467
+ * ) {}
468
+ * ```
469
+ *
470
+ * ### The public URL
471
+ * A Service is public by default. Alchemy allocates a shared IPv4 and
472
+ * an IPv6 on its App (both free) and `api.url` is
473
+ * `https://{appName}.fly.dev`.
474
+ *
475
+ * **Example:** Stack output
476
+ * ```typescript
477
+ * export default Alchemy.Stack(
478
+ * "MyApp",
479
+ * { providers: Fly.providers(), state: Alchemy.localState() },
480
+ * Effect.gen(function* () {
481
+ * const api = yield* Api;
482
+ * return { url: api.url };
483
+ * }),
484
+ * );
485
+ * ```
486
+ *
487
+ * `url` is `undefined` when you pass `services: []` (nothing is
488
+ * published).
489
+ *
490
+ * ### Every published port
491
+ * `url` is one endpoint. A Service that publishes several ports has one
492
+ * endpoint per port at the same hostname, listed in `endpoints` with
493
+ * its port, handlers, and a URL when the port speaks HTTP. Ports that
494
+ * only redirect to HTTPS are left out.
495
+ *
496
+ * **Example:** An admin UI on 8443 and raw TCP on 7000
497
+ * ```typescript
498
+ * export default class Api extends Fly.Service<Api>()(
499
+ * "Api",
500
+ * {
501
+ * main: import.meta.url,
502
+ * services: [
503
+ * {
504
+ * internalPort: 3000,
505
+ * ports: [
506
+ * { port: 80, handlers: ["http"], forceHttps: true },
507
+ * { port: 443, handlers: ["tls", "http"] },
508
+ * ],
509
+ * },
510
+ * { internalPort: 9000, ports: [{ port: 8443, handlers: ["tls", "http"] }] },
511
+ * { internalPort: 7000, ports: [{ port: 7000 }] },
512
+ * ],
513
+ * },
514
+ * Effect.gen(function* () {
515
+ * return {};
516
+ * }),
517
+ * ) {}
518
+ *
519
+ * api.url; // "https://{appName}.fly.dev"
520
+ * api.endpoints.map((endpoint) => endpoint.url);
521
+ * // ["https://{appName}.fly.dev", "https://{appName}.fly.dev:8443", undefined]
522
+ * ```
523
+ *
524
+ * :::note[Plain HTTP on other ports needs IPv6]
525
+ * The free shared IPv4 serves TLS on any port but plain HTTP only on
526
+ * port 80. A plain-HTTP port such as 8080 answers over IPv6 only.
527
+ * :::
528
+ *
529
+ * ### Private Services
530
+ * `public: false` keeps a Service off the internet. It gets only a
531
+ * Flycast address and publishes plain HTTP on port 80 (Fly issues no
532
+ * certificate for `.flycast`). `url` is `undefined`, and `privateUrl` is
533
+ * `http://{appName}.flycast`. Calls go through Fly's proxy, so service
534
+ * checks, `autostart`, and blue/green cutover still apply.
535
+ *
536
+ * Turning `public` on or off updates the Service in place. Its App and
537
+ * hostname stay the same.
538
+ *
539
+ * ### Call another Service
540
+ * A Service can return methods next to `fetch`. Another Service calls
541
+ * them by binding it with {@link bindService}, which returns a typed
542
+ * client. Binding makes the caller deploy after the Service, and only
543
+ * callers that bind a Service receive its caller token, so only they
544
+ * can call its methods.
545
+ *
546
+ * **Example:** A private Service with methods
547
+ * ```typescript
548
+ * export default class Users extends Fly.Service<Users>()(
549
+ * "Users",
550
+ * { main: import.meta.url, public: false },
551
+ * Effect.gen(function* () {
552
+ * return {
553
+ * list: () => Effect.succeed(USERS),
554
+ * get: (id: string) =>
555
+ * Effect.succeed(USERS.find((user) => user.id === id)),
556
+ * };
557
+ * }),
558
+ * ) {}
559
+ * ```
560
+ *
561
+ * **Example:** Bind it from another Service
562
+ * ```typescript
563
+ * export default class Gateway extends Fly.Service<Gateway>()(
564
+ * "Gateway",
565
+ * { main: import.meta.url },
566
+ * Effect.gen(function* () {
567
+ * const users = yield* Fly.bindService(Users);
568
+ * return {
569
+ * fetch: Effect.gen(function* () {
570
+ * return yield* HttpServerResponse.json(yield* users.list());
571
+ * }).pipe(Effect.orDie),
572
+ * };
573
+ * }),
574
+ * ) {}
575
+ * ```
576
+ *
577
+ * Calls use plain HTTP to `privateUrl` inside Fly's WireGuard-encrypted
578
+ * private network. The Service accepts a method call only with its
579
+ * caller token and a `Fly-Src` signature from Fly's proxy for the same
580
+ * organization, so the public internet can never call it. A public
581
+ * Service publishes an extra plain-HTTP port for bound callers,
582
+ * `bindingPort` (default 7780), since its own ports are an HTTPS
583
+ * redirect and HTTPS. `client.fetch` sends a request to the Service's
584
+ * `fetch` routes, and {@link bindEndpoint} targets one published port.
585
+ * Two Services can bind each other; declare them as tag classes and
586
+ * provide their `.make` layers (see the
587
+ * [Connect Services guide](/fly/compute/connecting-services)).
588
+ *
589
+ * ### Isolate Services on a private network
590
+ * Fly's default private network spans the whole organization, so any
591
+ * App in it can reach a private Service's address. Put a stack's
592
+ * Services on their own network with `network`. {@link stackNetwork}
593
+ * names one per stack and stage. Apps on other networks, including the
594
+ * default one, cannot resolve them, and binding a Service on another
595
+ * network fails the caller's deploy with `Fly.ServiceUnreachable`
596
+ * before anything is created. A public Service on the network still
597
+ * serves `url`, which makes it the stack's single entry point.
598
+ *
599
+ * **Example:** Private backend behind a public gateway
600
+ * ```typescript
601
+ * export class Users extends Fly.Service<Users>()(
602
+ * "Users",
603
+ * Effect.gen(function* () {
604
+ * return {
605
+ * main: import.meta.url,
606
+ * public: false,
607
+ * network: yield* Fly.stackNetwork,
608
+ * };
609
+ * }),
610
+ * Effect.gen(function* () {
611
+ * return { list: () => Effect.succeed(USERS) };
612
+ * }),
613
+ * ) {}
614
+ *
615
+ * export class Gateway extends Fly.Service<Gateway>()(
616
+ * "Gateway",
617
+ * Effect.gen(function* () {
618
+ * return { main: import.meta.url, network: yield* Fly.stackNetwork };
619
+ * }),
620
+ * Effect.gen(function* () {
621
+ * const users = yield* Fly.bindService(Users);
622
+ * return {
623
+ * fetch: Effect.gen(function* () {
624
+ * return yield* HttpServerResponse.json(yield* users.list());
625
+ * }).pipe(Effect.orDie),
626
+ * };
627
+ * }),
628
+ * ) {}
629
+ * ```
630
+ *
631
+ * :::caution[Changing `network` replaces the Service]
632
+ * Fly cannot move an App between networks. Alchemy creates the Service
633
+ * on the new network, then deletes the old App.
634
+ * :::
635
+ *
636
+ * ### Fly's proxy is the load balancer
637
+ * There is no LoadBalancer resource. Fly runs an Anycast proxy at
638
+ * the edge.
639
+ *
640
+ * **Example:** Published ports
641
+ * ```typescript
642
+ * export default class Api extends Fly.Service<Api>()(
643
+ * "Api",
644
+ * { main: import.meta.url, region: "iad", port: 3000 },
645
+ * Effect.gen(function* () {
646
+ * return {
647
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
648
+ * };
649
+ * }),
650
+ * ) {}
651
+ * ```
652
+ *
653
+ * Unless you override `services`, Alchemy publishes HTTP 80 and
654
+ * HTTPS 443 on that proxy and points them at `port` inside each
655
+ * Machine (`internal_port`). A request to
656
+ * `https://{appName}.fly.dev` lands on Fly's edge. Fly terminates
657
+ * TLS on 443, picks one started Machine that published this service,
658
+ * and forwards to `port` where `fetch` runs.
659
+ *
660
+ * ### Configure routing health checks
661
+ * The generated service includes a TCP check on `port`. To customize
662
+ * it, provide `services` and configure each service's `checks` property.
663
+ * With rolling updates, reconcile waits for each started replica's checks
664
+ * before updating the next replica. Missing or non-passing results are
665
+ * polled within `deploy.healthTimeout` (60 seconds by default), then fail
666
+ * deployment with `Fly.ReplicaChecksNotPassing`. Later replicas remain
667
+ * unchanged; earlier updates are not rolled back. A single rolling replica
668
+ * can be unavailable. Blue/green checks replacements before retiring the
669
+ * old set, with representative/floor readiness for idle capacity.
670
+ *
671
+ * **Example:** HTTP readiness check
672
+ * ```typescript
673
+ * export default class Api extends Fly.Service<Api>()(
674
+ * "Api",
675
+ * {
676
+ * main: import.meta.url,
677
+ * port: 3000,
678
+ * services: [
679
+ * {
680
+ * protocol: "tcp",
681
+ * internalPort: 3000,
682
+ * ports: [
683
+ * { port: 80, handlers: ["http"], forceHttps: true },
684
+ * { port: 443, handlers: ["tls", "http"] },
685
+ * ],
686
+ * checks: [
687
+ * {
688
+ * type: "http",
689
+ * port: 3000,
690
+ * method: "GET",
691
+ * path: "/health",
692
+ * protocol: "http",
693
+ * interval: "15s",
694
+ * timeout: "2s",
695
+ * gracePeriod: "30s",
696
+ * },
697
+ * ],
698
+ * },
699
+ * ],
700
+ * },
701
+ * Effect.gen(function* () {
702
+ * return {
703
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
704
+ * };
705
+ * }),
706
+ * ) {}
707
+ * ```
708
+ *
709
+ * ### Scale with count
710
+ * `count` is how many Machines to provision, including idle capacity.
711
+ * Default is `1`. Replicas publish the same proxy service behind
712
+ * `{appName}.fly.dev`; Fly's proxy picks an available Machine per request.
713
+ * Each replica gets its own Volume from every {@link MountVolume} binding;
714
+ * attached volumes require rolling updates.
715
+ *
716
+ * **Example:** Three replicas
717
+ * ```typescript
718
+ * export default class Api extends Fly.Service<Api>()(
719
+ * "Api",
720
+ * { main: import.meta.url, region: "iad", count: 3, port: 3000 },
721
+ * Effect.gen(function* () {
722
+ * return {
723
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
724
+ * };
725
+ * }),
726
+ * ) {}
727
+ * ```
728
+ *
729
+ * ### Config
730
+ * Yield `Config` in init. Alchemy reads the value from the env of
731
+ * whoever deploys and writes it onto the Machine, so there is no need
732
+ * to copy it into `env`.
733
+ *
734
+ * `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
735
+ * `Redacted.value` only where you need the raw string.
736
+ *
737
+ * Alchemy also injects `PORT` (when `port` is set) and stack metadata.
738
+ * For a secret Fly should own and inject into every Machine on an App,
739
+ * run the Service in an {@link App} (`app`) and use {@link Secret}.
740
+ *
741
+ * **Example:** Config.Redacted
742
+ * ```typescript
743
+ * import * as Config from "effect/Config";
744
+ * import * as Redacted from "effect/Redacted";
745
+ *
746
+ * export default class Api extends Fly.Service<Api>()(
747
+ * "Api",
748
+ * { main: import.meta.url, port: 3000 },
749
+ * Effect.gen(function* () {
750
+ * const apiKey = yield* Config.Redacted("API_KEY");
751
+ *
752
+ * return {
753
+ * fetch: Effect.gen(function* () {
754
+ * const token = Redacted.value(apiKey);
755
+ * return HttpServerResponse.text("ok");
756
+ * }),
757
+ * };
758
+ * }),
759
+ * ) {}
760
+ * ```
761
+ *
762
+ * ### Mount a disk
763
+ * Bind {@link MountVolume} inside init. App and region come from the
764
+ * Service. `count: 3` creates three Volumes, one per replica. Provide
765
+ * {@link MountVolumeLive}.
766
+ *
767
+ * **Example:** Per-replica disk
768
+ * ```typescript
769
+ * export default class Api extends Fly.Service<Api>()(
770
+ * "Api",
771
+ * { main: import.meta.url, region: "iad", count: 3, port: 3000 },
772
+ * Effect.gen(function* () {
773
+ * const disk = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 });
774
+ * const fs = yield* FileSystem.FileSystem;
775
+ * return {
776
+ * fetch: Effect.gen(function* () {
777
+ * const text = yield* fs.readFileString(`${disk.path}/hello.txt`);
778
+ * return HttpServerResponse.text(text);
779
+ * }),
780
+ * };
781
+ * }).pipe(Effect.provide(Fly.MountVolumeLive)),
782
+ * ) {}
783
+ * ```
784
+ *
785
+ * ### Guest size
786
+ * `guest` is CPU kind, CPU count, and memory. Default is shared-cpu,
787
+ * 1 CPU, 256 MB. Set `gpuKind` and `gpus` for a GPU. Guest updates in
788
+ * place.
789
+ *
790
+ * **Example:** Bigger guest
791
+ * ```typescript
792
+ * export default class Api extends Fly.Service<Api>()(
793
+ * "Api",
794
+ * {
795
+ * main: import.meta.url,
796
+ * region: "iad",
797
+ * port: 3000,
798
+ * guest: { cpuKind: "shared", cpus: 2, memoryMb: 512 },
799
+ * },
800
+ * Effect.gen(function* () {
801
+ * return {
802
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
803
+ * };
804
+ * }),
805
+ * ) {}
806
+ * ```
807
+ *
808
+ * ### A stable hostname
809
+ * `name` names the Service's App, so it is the `{name}.fly.dev`
810
+ * hostname. App names are globally unique across Fly. Omit `name` and
811
+ * Alchemy generates one from the stack, stage, and logical ID.
812
+ *
813
+ * **Example:** Explicit name
814
+ * ```typescript
815
+ * export default class Api extends Fly.Service<Api>()(
816
+ * "Api",
817
+ * { main: import.meta.url, name: "api", port: 3000 },
818
+ * Effect.gen(function* () {
819
+ * return {
820
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
821
+ * };
822
+ * }),
823
+ * ) {}
824
+ * ```
825
+ *
826
+ * :::caution[Changing `name` replaces the Service]
827
+ * Fly cannot rename an App. Alchemy creates the Service under the new
828
+ * name, then deletes the old App.
829
+ * :::
830
+ *
831
+ * ### Named export
832
+ * `handler` is the named export to load from `main`. Default is
833
+ * `"default"`.
834
+ *
835
+ * **Example:** Custom handler
836
+ * ```typescript
837
+ * export default class Api extends Fly.Service<Api>()(
838
+ * "Api",
839
+ * { main: import.meta.url, handler: "api", port: 3000 },
840
+ * Effect.gen(function* () {
841
+ * return {
842
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
843
+ * };
844
+ * }),
845
+ * ) {}
846
+ * ```
847
+ *
848
+ * ### Base image
849
+ * `image` is the generated Dockerfile's `FROM`. Default is
850
+ * `node:26-slim`. A content-hash change of `main` updates the
851
+ * Machine in place.
852
+ *
853
+ * **Example:** Override FROM
854
+ * ```typescript
855
+ * export default class Api extends Fly.Service<Api>()(
856
+ * "Api",
857
+ * {
858
+ * main: import.meta.url,
859
+ * image: "node:26",
860
+ * port: 3000,
861
+ * },
862
+ * Effect.gen(function* () {
863
+ * return {
864
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
865
+ * };
866
+ * }),
867
+ * ) {}
868
+ * ```
869
+ *
870
+ * ### Custom proxy services
871
+ * `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
872
+ * toward `port`, or plain HTTP 80 for a private Service. Pass a custom
873
+ * list to change handlers or autostop. Pass `[]` so Fly does not
874
+ * publish a proxy.
875
+ *
876
+ * **Example:** Unpublished process
877
+ * ```typescript
878
+ * export default class Worker extends Fly.Service<Worker>()(
879
+ * "Worker",
880
+ * { main: import.meta.url, region: "iad", services: [] },
881
+ * Effect.gen(function* () {
882
+ * return {};
883
+ * }),
884
+ * ) {}
885
+ * ```
886
+ *
887
+ * ### Background services
888
+ * Omit `port` and `fetch`. Pass `services: []`. Use `ServerHost.run`
889
+ * for a long-running loop. If the process exits, Fly restarts it.
890
+ *
891
+ * **Example:** ServerHost.run
892
+ * ```typescript
893
+ * import { ServerHost } from "alchemy/Runtime/Process";
894
+ *
895
+ * export default class Worker extends Fly.Service<Worker>()(
896
+ * "Worker",
897
+ * { main: import.meta.url, region: "iad", services: [] },
898
+ * Effect.gen(function* () {
899
+ * const host = yield* ServerHost;
900
+ *
901
+ * yield* host.run(
902
+ * Effect.gen(function* () {
903
+ * return yield* Effect.never;
904
+ * }).pipe(Effect.orDie),
905
+ * );
906
+ * }),
907
+ * ) {}
908
+ * ```
909
+ *
910
+ * ### Bundle config
911
+ * `build` is Rolldown `input` / `output` overrides plus
912
+ * pure-annotation options. Use it when `main` needs extra entry
913
+ * points or externals.
914
+ *
915
+ * **Example:** Externals
916
+ * ```typescript
917
+ * export default class Api extends Fly.Service<Api>()(
918
+ * "Api",
919
+ * {
920
+ * main: import.meta.url,
921
+ * port: 3000,
922
+ * build: { input: { external: ["sharp"] } },
923
+ * },
924
+ * Effect.gen(function* () {
925
+ * return {
926
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
927
+ * };
928
+ * }),
929
+ * ) {}
930
+ * ```
931
+ *
932
+ * **Example:** Install a native package unbundled
933
+ * `build.install` runs `npm install` in the image for packages that
934
+ * cannot be bundled, such as native addons — same `build.install` as
935
+ * Lambda.
936
+ * ```typescript
937
+ * export default class Api extends Fly.Service<Api>()(
938
+ * "Api",
939
+ * {
940
+ * main: import.meta.url,
941
+ * port: 3000,
942
+ * build: { install: ["sharp"] },
943
+ * },
944
+ * Effect.gen(function* () {
945
+ * return {
946
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
947
+ * };
948
+ * }),
949
+ * ) {}
950
+ * ```
951
+ *
952
+ * ### Group Services in one App
953
+ * Pass `app` to run a Service inside an existing {@link App} instead of
954
+ * its own. The Services share the App's hostname, addresses, and
955
+ * {@link Secret}s, and a {@link Certificate} on the App covers them.
956
+ * Alchemy manages no addresses for a Service in a shared App: allocate
957
+ * an {@link IpAssignment} on the App. `url` and `endpoints` follow the
958
+ * Service's own ports, so Services in one App tell themselves apart by
959
+ * port (`https://{appName}.fly.dev` and `https://{appName}.fly.dev:8443`).
960
+ * `public` does not apply.
961
+ *
962
+ * **Example:** API and worker sharing a secret
963
+ * ```typescript
964
+ * export const Site = Fly.App("Site");
965
+ *
966
+ * class Api extends Fly.Service<Api>()(
967
+ * "Api",
968
+ * { app: Site, main: import.meta.url, port: 3000 },
969
+ * Effect.gen(function* () {
970
+ * return {
971
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
972
+ * };
973
+ * }),
974
+ * ) {}
975
+ *
976
+ * class Worker extends Fly.Service<Worker>()(
977
+ * "Worker",
978
+ * { app: Site, main: import.meta.url, services: [] },
979
+ * Effect.gen(function* () {
980
+ * return {};
981
+ * }),
982
+ * ) {}
983
+ * ```
984
+ *
985
+ * Fly's proxy routes an App's traffic by port only, so each Service in
986
+ * the App needs its own ports, including its own `bindingPort` when
987
+ * Alchemy adds one. Two Services on the same port and protocol fail with
988
+ * `Fly.ServicePortConflict` before any Machine is created: at plan time
989
+ * when the App already exists, otherwise when the App deploys. Grouped
990
+ * Services can be bound like any other; they cannot bind each other in
991
+ * a cycle.
992
+ *
993
+ * ### Blue/green deployments
994
+ * Opt into healthy replacement Machines instead of in-place updates.
995
+ * The default TCP service check proves the server is listening; supply
996
+ * service HTTP checks when readiness also depends on application state.
997
+ *
998
+ * **Example:** Single-replica HTTP replacement
999
+ * ```typescript
1000
+ * export default class Api extends Fly.Service<Api>()(
1001
+ * "Api",
1002
+ * {
1003
+ * main: import.meta.url,
1004
+ * deploy: { strategy: "bluegreen" },
1005
+ * shutdown: { timeout: "30 seconds" },
1006
+ * },
1007
+ * Effect.succeed({ fetch: Effect.succeed(HttpServerResponse.text("ready")) }),
1008
+ * ) {}
1009
+ * ```
1010
+ *
1011
+ * Keep one Service declaration. The old process retains its own shutdown
1012
+ * signal and deadline when the replacement's policy changes. Managed
1013
+ * SIGTERM/SIGINT shutdown drains HTTP while runtime resource finalizers run;
1014
+ * shared dependencies remain alive until both settle or the deadline expires.
1015
+ * Applications own stop-acquisition barriers, separately scoped jobs, and
1016
+ * bounded drain or checkpoint logic using ordinary finalizers, not a new
1017
+ * shutdown hook. External servers own their signal handling. An old bootstrap
1018
+ * cannot gain handlers retroactively. Volumes are incompatible with blue/green;
1019
+ * physical IDs and names change. Service-bound secret versions are floors,
1020
+ * not vault snapshots, and native Machine leases are not deployment-wide locks.
1021
+ * Leases do not serialize vault writers or every simultaneous first deployment;
1022
+ * serialize CI invocations for the same resource.
1023
+ *
1024
+ * Stop and suspend autostop policies preserve idle nonrepresentatives while
1025
+ * a representative and the required running floor pass readiness. Requested
1026
+ * idle policy is restored before old retirement; a new instance needs fresh
1027
+ * checks. Suspension is not SIGTERM shutdown and does not run ordinary
1028
+ * shutdown finalizers. Replacements do not inherit suspended process memory.
1029
+ * See the [deployment guide](/fly/compute/deployments) for recovery,
1030
+ * idle capacity, application responsibilities, and verification limits.
1031
+ *
1032
+ * @resource
1033
+ * @product Service
1034
+ */
1035
+ export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRuntimeContext> =
1036
+ Platform("Fly.Service", {
1037
+ createRuntimeContext: createFlyHostRuntimeContext("Fly.Service"),
1038
+ // `{ app: Site }` at module scope is an Effect. Yield it here so the
1039
+ // App is registered and `news.app` is resolved attributes at
1040
+ // reconcile (same DX as `yield* App(...)` inside Effect.gen).
1041
+ transformProps: (id, props) =>
1042
+ Effect.gen(function* () {
1043
+ if (globalThis.__ALCHEMY_RUNTIME__) return props;
1044
+ const app = Effect.isEffect(props.app)
1045
+ ? yield* props.app as Effect.Effect<App, never, Providers>
1046
+ : props.app;
1047
+ // Only callers that bind this Service receive the token.
1048
+ const rpcToken = yield* makeRandom(`${id}RpcToken`);
1049
+ if (app !== undefined) {
1050
+ // Report the ports this Service publishes so the App can reject
1051
+ // two Services on the same port before either one is deployed.
1052
+ yield* app.bind(id, {
1053
+ service: id,
1054
+ services: props.services,
1055
+ bindingPort: props.bindingPort,
1056
+ });
1057
+ }
1058
+ return { ...props, app, rpcToken } as typeof props;
1059
+ }),
1060
+ });
1061
+
1062
+ export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated")<{
1063
+ resourceName: string;
1064
+ appName: string;
1065
+ }> {}
1066
+
1067
+ export class InvalidServiceProps extends Data.TaggedError("Fly.InvalidServiceProps")<{
1068
+ message: string;
1069
+ }> {}
1070
+
1071
+ export class ServiceAppNotResolved extends Data.TaggedError("Fly.ServiceAppNotResolved")<{
1072
+ message: string;
1073
+ }> {}
1074
+
1075
+ const appNameOf = (value: unknown): string | undefined => {
1076
+ if (value == null || typeof value !== "object") return undefined;
1077
+ const name = (value as { appName?: unknown }).appName;
1078
+ return typeof name === "string" && name.length > 0 ? name : undefined;
1079
+ };
1080
+
1081
+ const compactRecord = (
1082
+ record: Record<string, string | undefined> | null | undefined,
1083
+ ): Record<string, string> =>
1084
+ Object.fromEntries(
1085
+ Object.entries(record ?? {}).flatMap(([key, value]) =>
1086
+ value === undefined ? [] : [[key, value]],
1087
+ ),
1088
+ );
1089
+
1090
+ const toEnv = toEnvRecord;
1091
+
1092
+ const resolveMachineName = (id: string, name: string | undefined, existing?: string) =>
1093
+ Effect.gen(function* () {
1094
+ if (name !== undefined) return sanitizeFlyAppName(name);
1095
+ if (existing !== undefined) return existing;
1096
+ return yield* createFlyResourceName(id);
1097
+ });
1098
+
1099
+ const toFlyGuest = (guest: MachineGuest | undefined): FlyMachineGuest => {
1100
+ const fly: FlyMachineGuest = {
1101
+ cpu_kind: guest?.cpuKind ?? DEFAULT_CPU_KIND,
1102
+ cpus: guest?.cpus ?? DEFAULT_CPUS,
1103
+ memory_mb: guest?.memoryMb ?? DEFAULT_MEMORY_MB,
1104
+ };
1105
+ if (guest?.gpuKind !== undefined) fly.gpu_kind = guest.gpuKind;
1106
+ if (guest?.gpus !== undefined) fly.gpus = guest.gpus;
1107
+ return fly;
1108
+ };
1109
+
1110
+ const desiredEnv = (
1111
+ props: ServiceProps,
1112
+ bindingEnv: Record<string, any>,
1113
+ alchemyEnv: Record<string, string>,
1114
+ port: number,
1115
+ ): Record<string, string> => ({
1116
+ ...toEnv(bindingEnv),
1117
+ ...alchemyEnv,
1118
+ PORT: String(port),
1119
+ ...toEnv(props.env),
1120
+ });
1121
+
1122
+ const toFlyStatics = (
1123
+ statics:
1124
+ | ReadonlyArray<{
1125
+ guestPath: string;
1126
+ urlPrefix: string;
1127
+ tigrisBucket?: string;
1128
+ indexDocument?: string;
1129
+ }>
1130
+ | undefined,
1131
+ ): FlyStatic[] | undefined => {
1132
+ if (statics === undefined || statics.length === 0) return undefined;
1133
+ return statics.map((entry) => ({
1134
+ guest_path: entry.guestPath,
1135
+ url_prefix: entry.urlPrefix,
1136
+ ...(entry.tigrisBucket !== undefined ? { tigris_bucket: entry.tigrisBucket } : {}),
1137
+ ...(entry.indexDocument !== undefined ? { index_document: entry.indexDocument } : {}),
1138
+ }));
1139
+ };
1140
+
1141
+ const buildConfig = (input: {
1142
+ image: string;
1143
+ guest: FlyMachineGuest;
1144
+ env: Record<string, string>;
1145
+ services: FlyMachineService[];
1146
+ mounts: FlyMachineMount[];
1147
+ metadata: Record<string, string>;
1148
+ statics?: FlyStatic[];
1149
+ }): FlyMachineConfig => ({
1150
+ image: input.image,
1151
+ guest: input.guest,
1152
+ env: Object.keys(input.env).length > 0 ? input.env : undefined,
1153
+ services: input.services.length > 0 ? input.services : undefined,
1154
+ mounts: input.mounts.length > 0 ? input.mounts : undefined,
1155
+ metadata: input.metadata,
1156
+ statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
1157
+ });
1158
+
1159
+ const sameImage = (machine: FlyMachine, image: string) => {
1160
+ const configImage = machine.config?.image;
1161
+ if (configImage === image) return true;
1162
+ const ref = machine.image_ref;
1163
+ const colon = image.lastIndexOf(":");
1164
+ const slash = image.lastIndexOf("/");
1165
+ const split = colon > slash ? colon : -1;
1166
+ const repo = split === -1 ? image : image.slice(0, split);
1167
+ const tag = split === -1 ? "latest" : image.slice(split + 1);
1168
+ const observedRepo = ref?.repository;
1169
+ if (observedRepo === undefined || ref?.tag !== tag) return false;
1170
+ return observedRepo === repo || observedRepo.endsWith(`/${repo}`);
1171
+ };
1172
+
1173
+ const sameGuest = (observed: FlyMachineGuest | undefined, desired: FlyMachineGuest) =>
1174
+ (observed?.cpu_kind ?? DEFAULT_CPU_KIND) === desired.cpu_kind &&
1175
+ (observed?.cpus ?? DEFAULT_CPUS) === desired.cpus &&
1176
+ (observed?.memory_mb ?? DEFAULT_MEMORY_MB) === desired.memory_mb &&
1177
+ observed?.gpu_kind === desired.gpu_kind &&
1178
+ observed?.gpus === desired.gpus;
1179
+
1180
+ const sameEnv = (
1181
+ observed: Record<string, string | undefined> | undefined,
1182
+ desired: Record<string, string>,
1183
+ ) => deepEqual(compactRecord(observed), desired);
1184
+
1185
+ const sameMounts = (observed: FlyMachineMount[] | undefined, desired: FlyMachineMount[]) => {
1186
+ const key = (mount: FlyMachineMount) => `${mount.volume ?? ""}:${mount.path ?? ""}`;
1187
+ const left = [...(observed ?? [])].map(key).sort();
1188
+ const right = desired.map(key).sort();
1189
+ return deepEqual(left, right);
1190
+ };
1191
+
1192
+ const metadataChanged = (
1193
+ observed: Record<string, string | undefined> | undefined,
1194
+ desired: Record<string, string>,
1195
+ ) => {
1196
+ const { removed, added, updated } = diffMachineMetadata(compactRecord(observed), desired);
1197
+ return removed.length > 0 || Object.keys(added).length > 0 || Object.keys(updated).length > 0;
1198
+ };
1199
+
1200
+ const sameStatics = (observed: FlyStatic[] | undefined, desired: FlyStatic[] | undefined) => {
1201
+ const normalize = (statics: FlyStatic[] | undefined) =>
1202
+ (statics ?? []).map((entry) => ({
1203
+ ...entry,
1204
+ // Empty and omitted index documents both disable directory indexes.
1205
+ index_document: entry.index_document === "" ? undefined : entry.index_document,
1206
+ }));
1207
+ return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
1208
+ };
1209
+
1210
+ const configDrifted = (
1211
+ machine: FlyMachine,
1212
+ desired: {
1213
+ image: string;
1214
+ guest: FlyMachineGuest;
1215
+ env: Record<string, string>;
1216
+ services: FlyMachineService[];
1217
+ mounts: FlyMachineMount[];
1218
+ metadata: Record<string, string>;
1219
+ statics?: FlyStatic[];
1220
+ },
1221
+ ) => {
1222
+ const config = machine.config;
1223
+ return (
1224
+ !sameImage(machine, desired.image) ||
1225
+ !sameGuest(config?.guest, desired.guest) ||
1226
+ !sameEnv(config?.env, desired.env) ||
1227
+ !sameServices(config?.services, desired.services) ||
1228
+ !sameMounts(config?.mounts, desired.mounts) ||
1229
+ metadataChanged(config?.metadata, desired.metadata) ||
1230
+ !sameStatics(config?.statics, desired.statics)
1231
+ );
1232
+ };
1233
+
1234
+ const withPort = (base: string, port: number, standard: number) =>
1235
+ port === standard ? base : `${base}:${port}`;
1236
+
1237
+ const isRedirect = (handlers: string[], forceHttps: boolean | undefined) =>
1238
+ forceHttps === true && handlers.includes("http") && !handlers.includes("tls");
1239
+
1240
+ /** Every published port on `host`, excluding HTTPS redirects and ranges. */
1241
+ const endpointListOf = (
1242
+ host: string,
1243
+ services: FlyMachineService[] | undefined,
1244
+ ): ServiceEndpoint[] => {
1245
+ const flycast = host.endsWith(".flycast");
1246
+ return (services ?? []).flatMap((service) =>
1247
+ (service.ports ?? []).flatMap((entry) => {
1248
+ const handlers = entry.handlers ?? [];
1249
+ if (entry.port === undefined || isRedirect(handlers, entry.force_https)) return [];
1250
+ const http = handlers.includes("http");
1251
+ const tls = handlers.includes("tls");
1252
+ const url = !http
1253
+ ? undefined
1254
+ : tls
1255
+ ? flycast
1256
+ ? undefined
1257
+ : withPort(`https://${host}`, entry.port, 443)
1258
+ : withPort(`http://${host}`, entry.port, 80);
1259
+ return [
1260
+ {
1261
+ host,
1262
+ port: entry.port,
1263
+ internalPort: service.internal_port,
1264
+ protocol: service.protocol ?? "tcp",
1265
+ handlers,
1266
+ url,
1267
+ },
1268
+ ];
1269
+ }),
1270
+ );
1271
+ };
1272
+
1273
+ /** HTTPS on the first TLS port, else HTTP on the first plain HTTP port. */
1274
+ const mainUrlOf = (endpoints: ServiceEndpoint[]) =>
1275
+ endpoints.find((endpoint) => endpoint.url?.startsWith("https://"))?.url ??
1276
+ endpoints.find((endpoint) => endpoint.url !== undefined)?.url;
1277
+
1278
+ /** `public` and `network` configure the Service's own App. */
1279
+ const validateOwnership = (props: Partial<Pick<ServiceProps, "app" | "public" | "network">>) => {
1280
+ if (props.app === undefined) return Effect.void;
1281
+ const conflicting = (["public", "network"] as const).filter((key) => props[key] !== undefined);
1282
+ return conflicting.length === 0
1283
+ ? Effect.void
1284
+ : Effect.fail(
1285
+ new InvalidServiceProps({
1286
+ message: `Fly.Service ${conflicting.map((key) => `\`${key}\``).join(" and ")} only apply when the Service owns its App; remove \`app\` or ${conflicting.map((key) => `\`${key}\``).join(" and ")}.`,
1287
+ }),
1288
+ );
1289
+ };
1290
+
1291
+ /**
1292
+ * A private network name unique to the current stack and stage, for the
1293
+ * Service `network` prop. Services in one stage reach each other; other
1294
+ * stages and every other App in the organization cannot.
1295
+ */
1296
+ export const stackNetwork = Effect.gen(function* () {
1297
+ const stack = yield* Stack;
1298
+ const name = `${stack.name}-${stack.stage}`
1299
+ .toLowerCase()
1300
+ .replace(/[^a-z0-9-]+/g, "-")
1301
+ .replace(/-+/g, "-")
1302
+ .replace(/^-|-$/g, "")
1303
+ .slice(0, 63)
1304
+ .replace(/-$/, "");
1305
+ return /^[a-z]/.test(name) ? name : `n-${name}`.slice(0, 63);
1306
+ });
1307
+
1308
+ interface Access {
1309
+ ownsApp: boolean;
1310
+ isPublic: boolean;
1311
+ network: string | undefined;
1312
+ bindingPort: number;
1313
+ rpcToken: Redacted.Redacted<string> | undefined;
1314
+ }
1315
+
1316
+ /** Drop the port Alchemy added for bindings from a services list. */
1317
+ const withoutBindingPort = (services: FlyMachineService[] | undefined, bindingPort: number) =>
1318
+ (services ?? []).flatMap((service) => {
1319
+ const ports = (service.ports ?? []).filter(
1320
+ (entry) =>
1321
+ !(
1322
+ entry.port === bindingPort &&
1323
+ (entry.handlers ?? []).includes("http") &&
1324
+ !(entry.handlers ?? []).includes("tls")
1325
+ ),
1326
+ );
1327
+ return ports.length === 0 ? [] : [{ ...service, ports }];
1328
+ });
1329
+
1330
+ const endpointsOf = (set: ReplicaSet, access: Access) => {
1331
+ if (!hasPublishedService(set.services))
1332
+ return { url: undefined, privateUrl: undefined, endpoints: [] };
1333
+ const isPublic = !access.ownsApp || access.isPublic;
1334
+ const binding = bindingPortOf(portsOfFly(set.services), access.bindingPort);
1335
+ const visible = binding?.added ? withoutBindingPort(set.services, binding.port) : set.services;
1336
+ const endpoints = endpointListOf(`${set.appName}.${isPublic ? "fly.dev" : "flycast"}`, visible);
1337
+ return {
1338
+ url: isPublic ? mainUrlOf(endpoints) : undefined,
1339
+ privateUrl:
1340
+ binding === undefined
1341
+ ? undefined
1342
+ : withPort(`http://${set.appName}.flycast`, binding.port, 80),
1343
+ endpoints,
1344
+ };
1345
+ };
1346
+
1347
+ /**
1348
+ * The Service's Fly services plus the plain-HTTP port for bound callers,
1349
+ * when none of its published ports serves plain HTTP.
1350
+ */
1351
+ const withBindingService = (
1352
+ services: FlyMachineService[],
1353
+ internalPort: number,
1354
+ bindingPort: number,
1355
+ ): { services: FlyMachineService[]; added: boolean } => {
1356
+ const binding = bindingPortOf(portsOfFly(services), bindingPort);
1357
+ if (binding?.added !== true) return { services, added: false };
1358
+ if (portsOfFly(services).some((entry) => entry.start === bindingPort))
1359
+ return { services, added: true };
1360
+ // Add the port to the first TCP service so it shares that service's
1361
+ // internal port and health checks (blue/green requires checks on every
1362
+ // published service).
1363
+ const index = services.findIndex((service) => (service.protocol ?? "tcp") === "tcp");
1364
+ if (index === -1) {
1365
+ return {
1366
+ added: true,
1367
+ services: [
1368
+ ...services,
1369
+ {
1370
+ protocol: "tcp",
1371
+ internal_port: internalPort,
1372
+ ports: [{ port: bindingPort, handlers: ["http"] }],
1373
+ },
1374
+ ],
1375
+ };
1376
+ }
1377
+ return {
1378
+ added: true,
1379
+ services: services.map((service, i) =>
1380
+ i === index
1381
+ ? {
1382
+ ...service,
1383
+ ports: [...(service.ports ?? []), { port: bindingPort, handlers: ["http"] }],
1384
+ }
1385
+ : service,
1386
+ ),
1387
+ };
1388
+ };
1389
+
1390
+ /** Fly reports the organization's default network as `default`. */
1391
+ const normalizeNetwork = (network: string | undefined | null) =>
1392
+ network === undefined || network === null || network === "" || network === "default"
1393
+ ? undefined
1394
+ : network;
1395
+
1396
+ export class ServiceUnreachable extends Data.TaggedError("Fly.ServiceUnreachable")<{
1397
+ service: string;
1398
+ target: string;
1399
+ network: string | undefined;
1400
+ targetNetwork: string | undefined;
1401
+ }> {
1402
+ get message() {
1403
+ return `Fly Service ${this.service} binds ${this.target}, but ${this.service} is on the ${this.network ?? "default"} network and ${this.target} is on the ${this.targetNetwork ?? "default"} network. Put both on the same \`network\`.`;
1404
+ }
1405
+ }
1406
+
1407
+ export class ServiceNotBindable extends Data.TaggedError("Fly.ServiceNotBindable")<{
1408
+ service: string;
1409
+ target: string;
1410
+ }> {
1411
+ get message() {
1412
+ return `Fly Service ${this.service} binds ${this.target}, which publishes no ports (\`services: []\`), so it has no private address.`;
1413
+ }
1414
+ }
1415
+
1416
+ export class EndpointNotPublished extends Data.TaggedError("Fly.EndpointNotPublished")<{
1417
+ service: string;
1418
+ target: string;
1419
+ port: number;
1420
+ }> {
1421
+ get message() {
1422
+ return `Fly Service ${this.service} binds port ${this.port} of ${this.target}, which does not publish it.`;
1423
+ }
1424
+ }
1425
+
1426
+ /** Check every Service this one binds is reachable from its network. */
1427
+ const validateTargets = (
1428
+ service: string,
1429
+ network: string | undefined,
1430
+ targets: readonly BoundTarget[],
1431
+ ) =>
1432
+ Effect.gen(function* () {
1433
+ for (const target of targets) {
1434
+ const targetNetwork = normalizeNetwork(target.network);
1435
+ if (targetNetwork !== normalizeNetwork(network)) {
1436
+ return yield* new ServiceUnreachable({
1437
+ service,
1438
+ target: target.service,
1439
+ network: normalizeNetwork(network),
1440
+ targetNetwork,
1441
+ });
1442
+ }
1443
+ if (target.privateUrl === undefined) {
1444
+ return yield* new ServiceNotBindable({ service, target: target.service });
1445
+ }
1446
+ if (
1447
+ target.port !== undefined &&
1448
+ !(target.endpoints ?? []).some((entry) => entry.port === target.port)
1449
+ ) {
1450
+ return yield* new EndpointNotPublished({
1451
+ service,
1452
+ target: target.service,
1453
+ port: target.port,
1454
+ });
1455
+ }
1456
+ }
1457
+ });
1458
+
1459
+ /**
1460
+ * Fail when a Machine outside this Service already publishes one of its
1461
+ * ports in a shared App. Catches Machines the App's bindings do not list:
1462
+ * other stacks, the `fly` CLI, or plain `Fly.Machine`s.
1463
+ */
1464
+ const validateObservedPorts = (
1465
+ appName: string,
1466
+ fqn: string,
1467
+ id: string,
1468
+ services: FlyMachineService[],
1469
+ ) =>
1470
+ Effect.gen(function* () {
1471
+ const listed = yield* machines
1472
+ .listMachines({ app_name: appName })
1473
+ .pipe(Effect.catchTag(["NotFound"], () => Effect.succeed([])));
1474
+ const others = new Map<string, FlyMachineService[] | undefined>();
1475
+ for (const machine of listed) {
1476
+ if (machine.state === "destroyed") continue;
1477
+ const owner = machine.config?.metadata?.[alchemyMetadataKeys.fqn] ?? machine.id ?? "";
1478
+ if (owner === fqn || others.has(owner)) continue;
1479
+ others.set(owner, machine.config?.services);
1480
+ }
1481
+ const mine = portsOfFly(services);
1482
+ for (const [owner, theirs] of others) {
1483
+ const conflict = findPortConflict(appName, [
1484
+ { id, ports: mine },
1485
+ { id: owner, ports: portsOfFly(theirs) },
1486
+ ]);
1487
+ if (conflict !== undefined && conflict.publishers[0] !== conflict.publishers[1])
1488
+ return yield* conflict;
1489
+ }
1490
+ });
1491
+
1492
+ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["Attributes"] => ({
1493
+ appName: set.appName,
1494
+ ownsApp: access.ownsApp,
1495
+ network: normalizeNetwork(access.network),
1496
+ rpcToken: access.rpcToken,
1497
+ ...endpointsOf(set, access),
1498
+ rolloutPending: set.rolloutPending,
1499
+ machineId: set.machineId,
1500
+ machineIds: set.machineIds,
1501
+ name: set.name,
1502
+ baseName: set.baseName,
1503
+ region: set.region,
1504
+ regions: set.regions,
1505
+ state: set.state,
1506
+ imageRef: set.imageRef,
1507
+ count: Math.round(set.count / Math.max(1, set.regions.length)),
1508
+ mounts: set.mounts,
1509
+ replicas: set.replicas,
1510
+ code: { hash: codeHash },
1511
+ });
1512
+
1513
+ /** The replica set of a Service whose App exists but runs no Machines yet. */
1514
+ const emptyReplicaSet = (appName: string, name: string): ReplicaSet => ({
1515
+ appName,
1516
+ machineId: "",
1517
+ machineIds: [],
1518
+ name,
1519
+ baseName: name,
1520
+ region: "",
1521
+ regions: [],
1522
+ state: "created",
1523
+ instanceId: undefined,
1524
+ privateIp: undefined,
1525
+ imageRef: undefined,
1526
+ guest: undefined,
1527
+ services: undefined,
1528
+ count: 0,
1529
+ mounts: [],
1530
+ replicas: [],
1531
+ });
1532
+
1533
+ const machineIdsOf = (output: Service["Attributes"] | undefined) =>
1534
+ output?.machineIds ??
1535
+ (output?.machineId !== undefined && output.machineId.length > 0 ? [output.machineId] : []);
1536
+
1537
+ /** The private network the Service's Machines join. */
1538
+ const networkOf = (props: ServiceProps) =>
1539
+ normalizeNetwork(
1540
+ props.app === undefined ? props.network : (props.app as { network?: string }).network,
1541
+ );
1542
+
1543
+ /**
1544
+ * Resolve the App the Service runs in. A Service that owns its App creates
1545
+ * it with its addresses; one placed in an App gets a Flycast address on it
1546
+ * so bound callers can reach it.
1547
+ */
1548
+ const ensureServiceApp = Effect.fn(function* (
1549
+ id: string,
1550
+ props: ServiceProps,
1551
+ output: Service["Attributes"] | undefined,
1552
+ ) {
1553
+ yield* validateOwnership(props);
1554
+ const ownsApp = props.app === undefined;
1555
+ const isPublic = !ownsApp || props.public !== false;
1556
+ if (ownsApp) {
1557
+ const desired =
1558
+ props.name !== undefined
1559
+ ? sanitizeFlyAppName(props.name)
1560
+ : output?.ownsApp === true
1561
+ ? output.appName
1562
+ : yield* createFlyAppName(id);
1563
+ const app = yield* ensureApp({
1564
+ name: desired,
1565
+ network: props.network,
1566
+ previousName: output?.ownsApp === true ? output.appName : undefined,
1567
+ });
1568
+ const appName = app.name ?? desired;
1569
+ yield* syncOwnedAppAddresses(appName, isPublic, props.network);
1570
+ return { appName, ownsApp, isPublic, network: props.network };
1571
+ }
1572
+ const appName = appNameOf(props.app) ?? output?.appName;
1573
+ if (appName === undefined) {
1574
+ return yield* new ServiceAppNotResolved({
1575
+ message: "Fly.Service requires a resolved App with appName.",
1576
+ });
1577
+ }
1578
+ const network = networkOf(props);
1579
+ yield* ensureFlycastAddress(appName, network);
1580
+ return { appName, ownsApp, isPublic, network };
1581
+ });
1582
+
1583
+ export const ServiceProvider = () =>
1584
+ Provider.effect(
1585
+ Service,
1586
+ Effect.gen(function* () {
1587
+ const stack = yield* Stack;
1588
+ const docker = yield* Docker;
1589
+ const { dotAlchemy } = yield* AlchemyContext;
1590
+ const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
1591
+ const hosted = createFlyHostedSupport({
1592
+ stackName: stack.name,
1593
+ stage: stack.stage,
1594
+ virtualEntryPlugin,
1595
+ docker,
1596
+ dotAlchemy,
1597
+ });
1598
+
1599
+ return Service.Provider.of({
1600
+ stables: ["appName"],
1601
+ nuke: { dependsOn: ["Fly.App"] },
1602
+
1603
+ diff: Effect.fn(function* ({ news, output }) {
1604
+ if (news === undefined) return;
1605
+ const shape = news as Partial<Pick<ServiceProps, "app" | "public" | "network">>;
1606
+ const ownsApp = shape.app === undefined;
1607
+ yield* validateOwnership(shape);
1608
+ if ("main" in news) {
1609
+ const settings: Input<
1610
+ Pick<
1611
+ ServiceProps,
1612
+ "deploy" | "shutdown" | "checks" | "services" | "port" | "isExternal" | "public"
1613
+ >
1614
+ > = {
1615
+ deploy: news.deploy,
1616
+ shutdown: news.shutdown,
1617
+ services: news.services,
1618
+ checks: news.checks,
1619
+ port: news.port,
1620
+ isExternal: news.isExternal,
1621
+ public: news.public,
1622
+ };
1623
+ if (
1624
+ isResolved<
1625
+ Pick<
1626
+ ServiceProps,
1627
+ "deploy" | "shutdown" | "checks" | "services" | "port" | "isExternal" | "public"
1628
+ >
1629
+ >(settings)
1630
+ ) {
1631
+ yield* validateDeployment(
1632
+ yield* deploymentPolicy(settings.deploy, settings.shutdown, !settings.isExternal),
1633
+ {
1634
+ services:
1635
+ settings.services?.map(toFlyService) ??
1636
+ defaultHttpServices(
1637
+ settings.port ?? DEFAULT_PORT,
1638
+ 1,
1639
+ !ownsApp || settings.public !== false,
1640
+ ),
1641
+ checks: settings.checks,
1642
+ },
1643
+ false,
1644
+ );
1645
+ }
1646
+ }
1647
+ if (output === undefined) return;
1648
+ // Moving into or out of a shared App changes the hostname.
1649
+ if (ownsApp !== (output.ownsApp === true)) {
1650
+ return { action: "replace" as const, deleteFirst: false };
1651
+ }
1652
+ if (ownsApp && isResolved(news)) {
1653
+ const desiredAppName =
1654
+ news.name !== undefined ? sanitizeFlyAppName(news.name) : output.appName;
1655
+ const nameChanged = desiredAppName !== output.appName;
1656
+ // Fly cannot move an App to another network.
1657
+ const networkChanged =
1658
+ normalizeNetwork(news.network) !== normalizeNetwork(output.network);
1659
+ if (nameChanged || networkChanged) {
1660
+ return {
1661
+ action: "replace" as const,
1662
+ // A pinned App name cannot exist twice.
1663
+ deleteFirst: !nameChanged && news.name !== undefined,
1664
+ };
1665
+ }
1666
+ }
1667
+ if (!ownsApp && isResolved(news)) {
1668
+ const desiredAppName = appNameOf(news.app);
1669
+ const appChanged = desiredAppName !== undefined && desiredAppName !== output.appName;
1670
+ const desiredName =
1671
+ news.name !== undefined
1672
+ ? sanitizeFlyAppName(news.name)
1673
+ : (output.baseName ?? output.name);
1674
+ const nameChanged = desiredName !== (output.baseName ?? output.name);
1675
+ if (appChanged || nameChanged) {
1676
+ return {
1677
+ action: "replace" as const,
1678
+ deleteFirst: nameChanged === false && appChanged === false,
1679
+ };
1680
+ }
1681
+ }
1682
+ // The code hash depends only on statically-known props (`main`,
1683
+ // `build`, `image`, `port`, `extraFiles`, `isExternal`). Never
1684
+ // gate it on the WHOLE props being resolved: `app` is a resource
1685
+ // reference that stays unresolved at diff time, so a whole-props
1686
+ // guard makes code-only changes silently noop. By diff time the
1687
+ // effect-config form has been evaluated, so the object view is
1688
+ // safe to read.
1689
+ const statics = news as Partial<
1690
+ Pick<ServiceProps, "main" | "build" | "image" | "port" | "extraFiles" | "isExternal">
1691
+ >;
1692
+ if (
1693
+ isResolved({
1694
+ main: statics.main,
1695
+ build: statics.build,
1696
+ image: statics.image,
1697
+ port: statics.port,
1698
+ extraFiles: statics.extraFiles,
1699
+ isExternal: statics.isExternal,
1700
+ }) &&
1701
+ statics.main !== undefined
1702
+ ) {
1703
+ const hash = yield* hosted.hash(statics as HostedProgramProps);
1704
+ if (hash !== output.code.hash) {
1705
+ return { action: "update" as const };
1706
+ }
1707
+ }
1708
+ return output.rolloutPending ? { action: "update" as const } : undefined;
1709
+ }),
1710
+
1711
+ read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
1712
+ const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
1713
+ const appName = ownsApp
1714
+ ? (output?.appName ??
1715
+ (olds?.name !== undefined
1716
+ ? sanitizeFlyAppName(olds.name)
1717
+ : yield* createFlyAppName(id)))
1718
+ : (appNameOf(olds?.app) ?? output?.appName);
1719
+ const name = yield* resolveMachineName(
1720
+ id,
1721
+ ownsApp ? undefined : olds?.name,
1722
+ output?.baseName ?? output?.name,
1723
+ );
1724
+ const found = yield* observeReplicaSet({
1725
+ appName,
1726
+ id,
1727
+ type: "Fly.Service",
1728
+ fqn,
1729
+ resourceInstanceId: instanceId,
1730
+ machineIds: machineIdsOf(output),
1731
+ baseName: name,
1732
+ });
1733
+ if (found === undefined) {
1734
+ // A create interrupted between creating the Service's own App and
1735
+ // starting its Machines (an image build in between can take
1736
+ // minutes) leaves the App with nothing in it. Report it, so the
1737
+ // engine's recovery of the interrupted create deletes it instead
1738
+ // of leaking it. The App's name is derived from this resource's
1739
+ // instance, so an App with that name is this resource's.
1740
+ if (!ownsApp || appName === undefined) return undefined;
1741
+ const app = yield* machines
1742
+ .getApp({ app_name: appName })
1743
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
1744
+ if (app === undefined || !matchesAlchemyPhysicalName(app.name)) return undefined;
1745
+ return toAttrs(emptyReplicaSet(appName, name), output?.code.hash ?? "", {
1746
+ ownsApp,
1747
+ isPublic: false,
1748
+ network: output?.network ?? olds?.network,
1749
+ bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1750
+ rpcToken: output?.rpcToken,
1751
+ });
1752
+ }
1753
+ return toAttrs(found, output?.code.hash ?? "", {
1754
+ ownsApp,
1755
+ isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
1756
+ network: output?.network ?? (ownsApp ? olds?.network : undefined),
1757
+ bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1758
+ rpcToken: output?.rpcToken,
1759
+ });
1760
+ }),
1761
+
1762
+ list: Effect.fn(function* () {
1763
+ const sets = yield* listReplicaSets("Fly.Service");
1764
+ // Owned Apps are also listed (and nuked) as Fly.App rows.
1765
+ return sets.map((set) =>
1766
+ toAttrs(set, "", {
1767
+ ownsApp: false,
1768
+ isPublic: true,
1769
+ network: undefined,
1770
+ bindingPort: DEFAULT_BINDING_PORT,
1771
+ rpcToken: undefined,
1772
+ }),
1773
+ );
1774
+ }),
1775
+
1776
+ reconcile: Effect.fn(function* ({ id, fqn, instanceId, news, output, bindings, session }) {
1777
+ const props = news;
1778
+ const policy = yield* deploymentPolicy(props.deploy, props.shutdown, !props.isExternal);
1779
+ yield* validateOwnership(props);
1780
+ const bound = collectBindingState(bindings ?? []);
1781
+ // Before creating anything: every bound Service must be reachable.
1782
+ yield* validateTargets(id, networkOf(props), bound.targets);
1783
+ const { appName, ownsApp, isPublic, network } = yield* ensureServiceApp(
1784
+ id,
1785
+ props,
1786
+ output,
1787
+ );
1788
+ const name = yield* resolveMachineName(
1789
+ id,
1790
+ ownsApp ? undefined : props.name,
1791
+ output?.baseName ?? output?.name,
1792
+ );
1793
+ const regions = regionList(props.region, DEFAULT_REGION);
1794
+ const count = resolveCount(props.count);
1795
+ const port = props.port ?? DEFAULT_PORT;
1796
+ const bindingPort = props.bindingPort ?? DEFAULT_BINDING_PORT;
1797
+ const secretVersions: number[] = [];
1798
+ const redisVersion = yield* attachRedisSecrets(appName, bound.redis);
1799
+ if (redisVersion !== undefined) secretVersions.push(redisVersion);
1800
+ const bucketVersion = yield* attachBucketSecrets(appName, bound.buckets, {
1801
+ ...bound.env,
1802
+ ...toEnv(props.env),
1803
+ });
1804
+ if (bucketVersion !== undefined) secretVersions.push(bucketVersion);
1805
+ for (const pg of bound.postgres) {
1806
+ const version = yield* attachPostgresSecrets(appName, pg.clusterId, pg.variableName);
1807
+ if (version !== undefined) secretVersions.push(version);
1808
+ }
1809
+ const minSecretsVersion =
1810
+ secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
1811
+ const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
1812
+ if (policy.shutdown && !props.isExternal) {
1813
+ env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
1814
+ }
1815
+ const guest = toFlyGuest(props.guest);
1816
+ const published = withBindingService(
1817
+ props.services !== undefined
1818
+ ? props.services.map(toFlyService)
1819
+ : defaultHttpServices(port, count, isPublic),
1820
+ port,
1821
+ bindingPort,
1822
+ );
1823
+ const services = published.services;
1824
+ if (!ownsApp) yield* validateObservedPorts(appName, fqn, id, services);
1825
+ if (props.rpcToken !== undefined) env[RPC_TOKEN_ENV] = Redacted.value(props.rpcToken);
1826
+ for (const target of bound.targets) {
1827
+ const keys = boundTargetEnvKeys(target.service);
1828
+ env[keys.appName] = packEnvValue(target.appName);
1829
+ if (target.privateUrl !== undefined) env[keys.url] = packEnvValue(target.privateUrl);
1830
+ if (target.rpcToken !== undefined) env[keys.token] = packEnvValue(target.rpcToken);
1831
+ }
1832
+ env[RPC_ORG_ENV] = yield* resolveOrgSlug();
1833
+ if (published.added) env[BINDING_PORT_ENV] = String(bindingPort);
1834
+ const statics = toFlyStatics(props.statics);
1835
+
1836
+ const { imageRef, codeHash } = yield* hosted.resolveImage({
1837
+ id,
1838
+ appName,
1839
+ props,
1840
+ previousHash: output?.code.hash,
1841
+ session,
1842
+ });
1843
+
1844
+ const set = yield* reconcileReplicas({
1845
+ id,
1846
+ type: "Fly.Service",
1847
+ fqn,
1848
+ resourceInstanceId: instanceId,
1849
+ policy,
1850
+ checks: props.checks,
1851
+ appName,
1852
+ baseName: name,
1853
+ regions,
1854
+ // `count` is per region.
1855
+ count: count * regions.length,
1856
+ disks: bound.mounts,
1857
+ minSecretsVersion,
1858
+ outputMachineIds: machineIdsOf(output),
1859
+ preferVolumeIds: (output?.replicas ?? []).map((replica) =>
1860
+ replica.mounts.map((mount) => mount.volumeId),
1861
+ ),
1862
+ configDrifted: (machine, desired) =>
1863
+ configDrifted(machine, {
1864
+ image: imageRef,
1865
+ guest,
1866
+ env,
1867
+ services,
1868
+ mounts: desired.mounts,
1869
+ metadata: desired.metadata,
1870
+ statics,
1871
+ }),
1872
+ buildConfig: ({ mounts, metadata }) =>
1873
+ buildConfig({ image: imageRef, guest, env, services, mounts, metadata, statics }),
1874
+ }).pipe(
1875
+ Effect.catchTag("Fly.ReplicaNotCreated", (error) =>
1876
+ Effect.fail(
1877
+ new ServiceNotCreated({ resourceName: error.resourceName, appName: error.appName }),
1878
+ ),
1879
+ ),
1880
+ );
1881
+ return toAttrs(set, codeHash, {
1882
+ ownsApp,
1883
+ isPublic,
1884
+ network,
1885
+ bindingPort,
1886
+ rpcToken: props.rpcToken,
1887
+ });
1888
+ }),
1889
+
1890
+ // Create the App, its addresses, and the caller token before any
1891
+ // Machine, so two Services that bind each other can both resolve
1892
+ // the other's private address and token.
1893
+ precreate: Effect.fn(function* ({ id, news }) {
1894
+ const props = news;
1895
+ // Precreate runs before dependencies resolve. A Service placed in
1896
+ // an App cannot see the App yet, so it has no stub to offer;
1897
+ // cycles are supported between Services that own their App.
1898
+ const inert =
1899
+ props.app !== undefined ||
1900
+ !isResolved(props.name) ||
1901
+ !isResolved(props.network) ||
1902
+ !isResolved(props.services) ||
1903
+ !isResolved(props.bindingPort);
1904
+ // No stub: a failed create then recovers through `read`, which
1905
+ // finds the App from the persisted props.
1906
+ if (inert) return undefined as unknown as Service["Attributes"];
1907
+ const { appName, ownsApp, isPublic, network } = yield* ensureServiceApp(
1908
+ id,
1909
+ props,
1910
+ undefined,
1911
+ );
1912
+ const port = props.port ?? DEFAULT_PORT;
1913
+ const count = resolveCount(props.count);
1914
+ const bindingPort = props.bindingPort ?? DEFAULT_BINDING_PORT;
1915
+ const { services } = withBindingService(
1916
+ props.services !== undefined
1917
+ ? props.services.map(toFlyService)
1918
+ : defaultHttpServices(port, count, isPublic),
1919
+ port,
1920
+ bindingPort,
1921
+ );
1922
+ const name = yield* resolveMachineName(id, ownsApp ? undefined : props.name);
1923
+ const stub: ReplicaSet = {
1924
+ appName,
1925
+ machineId: "",
1926
+ machineIds: [],
1927
+ name,
1928
+ baseName: name,
1929
+ region: regionList(props.region, DEFAULT_REGION)[0]!,
1930
+ regions: regionList(props.region, DEFAULT_REGION),
1931
+ state: "created",
1932
+ instanceId: undefined,
1933
+ privateIp: undefined,
1934
+ imageRef: undefined,
1935
+ guest: undefined,
1936
+ services,
1937
+ count: 0,
1938
+ mounts: [],
1939
+ replicas: [],
1940
+ };
1941
+ return toAttrs(stub, "", {
1942
+ ownsApp,
1943
+ isPublic,
1944
+ network,
1945
+ bindingPort,
1946
+ rpcToken: isResolved(props.rpcToken) ? props.rpcToken : undefined,
1947
+ });
1948
+ }),
1949
+
1950
+ delete: Effect.fn(function* ({ id, fqn, instanceId, olds, output, force }) {
1951
+ const appName = output.appName || appNameOf(olds.app);
1952
+ // A precreate stub that never reached its App has nothing to delete.
1953
+ if (appName === undefined || appName.length === 0) return;
1954
+ yield* deleteReplicaSet({
1955
+ appName,
1956
+ id,
1957
+ type: "Fly.Service",
1958
+ fqn,
1959
+ resourceInstanceId: instanceId,
1960
+ machineIds: machineIdsOf(output),
1961
+ volumeIds: volumeIdsOf(output),
1962
+ force,
1963
+ });
1964
+ if (output.ownsApp === true) yield* deleteApp(appName);
1965
+ }),
1966
+ });
1967
+ }),
1968
+ ).pipe(Layer.provide(DockerLive));