@alchemy.run/fly 2.0.0-beta.84 → 2.0.0-beta.86

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 (307) hide show
  1. package/lib/App.d.ts +27 -5
  2. package/lib/App.d.ts.map +1 -1
  3. package/lib/App.js +29 -8
  4. package/lib/App.js.map +1 -1
  5. package/lib/AuthProvider.d.ts +1 -1
  6. package/lib/AuthProvider.d.ts.map +1 -1
  7. package/lib/AuthProvider.js.map +1 -1
  8. package/lib/BindService.d.ts +7 -7
  9. package/lib/BindService.d.ts.map +1 -1
  10. package/lib/BindService.js +21 -28
  11. package/lib/BindService.js.map +1 -1
  12. package/lib/Bootstrap.d.ts.map +1 -1
  13. package/lib/Bootstrap.js +2 -1
  14. package/lib/Bootstrap.js.map +1 -1
  15. package/lib/Bucket.d.ts +38 -4
  16. package/lib/Bucket.d.ts.map +1 -1
  17. package/lib/Bucket.js +190 -37
  18. package/lib/Bucket.js.map +1 -1
  19. package/lib/BucketBinding.d.ts.map +1 -1
  20. package/lib/BucketBinding.js +1 -1
  21. package/lib/BucketBinding.js.map +1 -1
  22. package/lib/Catalog.d.ts.map +1 -1
  23. package/lib/Catalog.js.map +1 -1
  24. package/lib/Certificate.d.ts +25 -1
  25. package/lib/Certificate.d.ts.map +1 -1
  26. package/lib/Certificate.js +22 -4
  27. package/lib/Certificate.js.map +1 -1
  28. package/lib/Checkpoint.d.ts.map +1 -1
  29. package/lib/Checkpoint.js.map +1 -1
  30. package/lib/CheckpointHttp.d.ts.map +1 -1
  31. package/lib/CheckpointHttp.js.map +1 -1
  32. package/lib/ConnectPostgres.d.ts +1 -1
  33. package/lib/ConnectPostgres.d.ts.map +1 -1
  34. package/lib/ConnectPostgres.js.map +1 -1
  35. package/lib/ConnectPostgresHttp.d.ts.map +1 -1
  36. package/lib/ConnectPostgresHttp.js +1 -1
  37. package/lib/ConnectPostgresHttp.js.map +1 -1
  38. package/lib/Contributions.d.ts +1 -1
  39. package/lib/Contributions.d.ts.map +1 -1
  40. package/lib/Contributions.js.map +1 -1
  41. package/lib/Credentials.d.ts +5 -4
  42. package/lib/Credentials.d.ts.map +1 -1
  43. package/lib/Credentials.js +18 -18
  44. package/lib/Credentials.js.map +1 -1
  45. package/lib/Decrypt.d.ts.map +1 -1
  46. package/lib/Decrypt.js.map +1 -1
  47. package/lib/DecryptHttp.d.ts.map +1 -1
  48. package/lib/DecryptHttp.js.map +1 -1
  49. package/lib/DeleteObject.d.ts.map +1 -1
  50. package/lib/DeleteObject.js.map +1 -1
  51. package/lib/DeleteObjectHttp.d.ts.map +1 -1
  52. package/lib/DeleteObjectHttp.js.map +1 -1
  53. package/lib/Deployment.d.ts.map +1 -1
  54. package/lib/Deployment.js.map +1 -1
  55. package/lib/DeploymentImages.d.ts.map +1 -1
  56. package/lib/DeploymentImages.js +1 -1
  57. package/lib/DeploymentImages.js.map +1 -1
  58. package/lib/DeploymentState.d.ts.map +1 -1
  59. package/lib/DeploymentState.js.map +1 -1
  60. package/lib/Encrypt.d.ts.map +1 -1
  61. package/lib/Encrypt.js.map +1 -1
  62. package/lib/EncryptHttp.d.ts.map +1 -1
  63. package/lib/EncryptHttp.js.map +1 -1
  64. package/lib/Environment.d.ts.map +1 -1
  65. package/lib/Environment.js.map +1 -1
  66. package/lib/Errors.d.ts +1 -1
  67. package/lib/Errors.d.ts.map +1 -1
  68. package/lib/Errors.js.map +1 -1
  69. package/lib/Exec.d.ts.map +1 -1
  70. package/lib/Exec.js.map +1 -1
  71. package/lib/ExecHttp.d.ts.map +1 -1
  72. package/lib/ExecHttp.js.map +1 -1
  73. package/lib/GetObject.d.ts.map +1 -1
  74. package/lib/GetObject.js.map +1 -1
  75. package/lib/GetObjectHttp.d.ts.map +1 -1
  76. package/lib/GetObjectHttp.js.map +1 -1
  77. package/lib/GetSecret.d.ts.map +1 -1
  78. package/lib/GetSecret.js.map +1 -1
  79. package/lib/GetSecretHttp.d.ts.map +1 -1
  80. package/lib/GetSecretHttp.js.map +1 -1
  81. package/lib/HeadObject.d.ts.map +1 -1
  82. package/lib/HeadObject.js.map +1 -1
  83. package/lib/HeadObjectHttp.d.ts.map +1 -1
  84. package/lib/HeadObjectHttp.js.map +1 -1
  85. package/lib/IpAssignment.d.ts +24 -1
  86. package/lib/IpAssignment.d.ts.map +1 -1
  87. package/lib/IpAssignment.js +21 -4
  88. package/lib/IpAssignment.js.map +1 -1
  89. package/lib/ListObjectsV2.d.ts.map +1 -1
  90. package/lib/ListObjectsV2.js.map +1 -1
  91. package/lib/ListObjectsV2Http.d.ts.map +1 -1
  92. package/lib/ListObjectsV2Http.js.map +1 -1
  93. package/lib/ListSecrets.d.ts.map +1 -1
  94. package/lib/ListSecrets.js.map +1 -1
  95. package/lib/ListSecretsHttp.d.ts.map +1 -1
  96. package/lib/ListSecretsHttp.js.map +1 -1
  97. package/lib/Machine.d.ts +27 -2
  98. package/lib/Machine.d.ts.map +1 -1
  99. package/lib/Machine.js +18 -3
  100. package/lib/Machine.js.map +1 -1
  101. package/lib/MachineContainers.d.ts.map +1 -1
  102. package/lib/MachineContainers.js.map +1 -1
  103. package/lib/Metadata.d.ts.map +1 -1
  104. package/lib/Metadata.js.map +1 -1
  105. package/lib/MountVolume.d.ts +7 -2
  106. package/lib/MountVolume.d.ts.map +1 -1
  107. package/lib/MountVolume.js.map +1 -1
  108. package/lib/Postgres.d.ts +34 -2
  109. package/lib/Postgres.d.ts.map +1 -1
  110. package/lib/Postgres.js +26 -6
  111. package/lib/Postgres.js.map +1 -1
  112. package/lib/PostgresMigrations.d.ts +7 -22
  113. package/lib/PostgresMigrations.d.ts.map +1 -1
  114. package/lib/PostgresMigrations.js +37 -89
  115. package/lib/PostgresMigrations.js.map +1 -1
  116. package/lib/Providers.d.ts +1 -1
  117. package/lib/Providers.d.ts.map +1 -1
  118. package/lib/Providers.js.map +1 -1
  119. package/lib/PutObject.d.ts.map +1 -1
  120. package/lib/PutObject.js.map +1 -1
  121. package/lib/PutObjectHttp.d.ts.map +1 -1
  122. package/lib/PutObjectHttp.js.map +1 -1
  123. package/lib/ReadRedis.d.ts.map +1 -1
  124. package/lib/ReadRedis.js.map +1 -1
  125. package/lib/ReadRedisHttp.d.ts.map +1 -1
  126. package/lib/ReadRedisHttp.js.map +1 -1
  127. package/lib/ReadWriteRedis.d.ts.map +1 -1
  128. package/lib/ReadWriteRedis.js.map +1 -1
  129. package/lib/ReadWriteRedisHttp.d.ts.map +1 -1
  130. package/lib/ReadWriteRedisHttp.js.map +1 -1
  131. package/lib/Redis.d.ts +23 -3
  132. package/lib/Redis.d.ts.map +1 -1
  133. package/lib/Redis.js +37 -6
  134. package/lib/Redis.js.map +1 -1
  135. package/lib/RedisBinding.d.ts.map +1 -1
  136. package/lib/RedisBinding.js +1 -1
  137. package/lib/RedisBinding.js.map +1 -1
  138. package/lib/RedisHttp.d.ts +1 -1
  139. package/lib/RedisHttp.d.ts.map +1 -1
  140. package/lib/RedisHttp.js +4 -1
  141. package/lib/RedisHttp.js.map +1 -1
  142. package/lib/Region.d.ts.map +1 -1
  143. package/lib/Region.js.map +1 -1
  144. package/lib/Secret.d.ts +23 -2
  145. package/lib/Secret.d.ts.map +1 -1
  146. package/lib/Secret.js +17 -2
  147. package/lib/Secret.js.map +1 -1
  148. package/lib/SecretHttp.d.ts.map +1 -1
  149. package/lib/SecretHttp.js.map +1 -1
  150. package/lib/SecretKey.d.ts +23 -2
  151. package/lib/SecretKey.d.ts.map +1 -1
  152. package/lib/SecretKey.js +17 -2
  153. package/lib/SecretKey.js.map +1 -1
  154. package/lib/SecretKeyHttp.d.ts.map +1 -1
  155. package/lib/SecretKeyHttp.js.map +1 -1
  156. package/lib/Service.d.ts +104 -34
  157. package/lib/Service.d.ts.map +1 -1
  158. package/lib/Service.js +301 -54
  159. package/lib/Service.js.map +1 -1
  160. package/lib/Sign.d.ts.map +1 -1
  161. package/lib/Sign.js.map +1 -1
  162. package/lib/SignHttp.d.ts.map +1 -1
  163. package/lib/SignHttp.js.map +1 -1
  164. package/lib/Sprite.d.ts +24 -5
  165. package/lib/Sprite.d.ts.map +1 -1
  166. package/lib/Sprite.js +22 -5
  167. package/lib/Sprite.js.map +1 -1
  168. package/lib/SpriteHttp.d.ts.map +1 -1
  169. package/lib/SpriteHttp.js.map +1 -1
  170. package/lib/Verify.d.ts.map +1 -1
  171. package/lib/Verify.js.map +1 -1
  172. package/lib/VerifyHttp.d.ts.map +1 -1
  173. package/lib/VerifyHttp.js.map +1 -1
  174. package/lib/Volume.d.ts +4 -4
  175. package/lib/Volume.d.ts.map +1 -1
  176. package/lib/Volume.js +21 -5
  177. package/lib/Volume.js.map +1 -1
  178. package/lib/VolumeSnapshot.d.ts +32 -1
  179. package/lib/VolumeSnapshot.d.ts.map +1 -1
  180. package/lib/VolumeSnapshot.js +24 -1
  181. package/lib/VolumeSnapshot.js.map +1 -1
  182. package/lib/Website/AssetDeployment.d.ts.map +1 -1
  183. package/lib/Website/AssetDeployment.js +9 -9
  184. package/lib/Website/AssetDeployment.js.map +1 -1
  185. package/lib/Website/Astro.d.ts +8 -6
  186. package/lib/Website/Astro.d.ts.map +1 -1
  187. package/lib/Website/Astro.js +12 -13
  188. package/lib/Website/Astro.js.map +1 -1
  189. package/lib/Website/Foldkit.d.ts.map +1 -1
  190. package/lib/Website/Foldkit.js.map +1 -1
  191. package/lib/Website/FrameworkSite.d.ts +39 -2
  192. package/lib/Website/FrameworkSite.d.ts.map +1 -1
  193. package/lib/Website/FrameworkSite.js +46 -3
  194. package/lib/Website/FrameworkSite.js.map +1 -1
  195. package/lib/Website/Nextjs.d.ts.map +1 -1
  196. package/lib/Website/Nextjs.js.map +1 -1
  197. package/lib/Website/Nuxt.d.ts.map +1 -1
  198. package/lib/Website/Nuxt.js.map +1 -1
  199. package/lib/Website/Octane.d.ts.map +1 -1
  200. package/lib/Website/Octane.js.map +1 -1
  201. package/lib/Website/ReactRouter.d.ts.map +1 -1
  202. package/lib/Website/ReactRouter.js.map +1 -1
  203. package/lib/Website/SolidStart.d.ts.map +1 -1
  204. package/lib/Website/SolidStart.js.map +1 -1
  205. package/lib/Website/StaticSite.d.ts +10 -3
  206. package/lib/Website/StaticSite.d.ts.map +1 -1
  207. package/lib/Website/StaticSite.js +16 -4
  208. package/lib/Website/StaticSite.js.map +1 -1
  209. package/lib/Website/SvelteKit.d.ts.map +1 -1
  210. package/lib/Website/SvelteKit.js.map +1 -1
  211. package/lib/Website/TanStackStart.d.ts.map +1 -1
  212. package/lib/Website/TanStackStart.js.map +1 -1
  213. package/lib/Website/Vinext.d.ts.map +1 -1
  214. package/lib/Website/Vinext.js.map +1 -1
  215. package/lib/Website/Vite.d.ts +9 -0
  216. package/lib/Website/Vite.d.ts.map +1 -1
  217. package/lib/Website/Vite.js +9 -0
  218. package/lib/Website/Vite.js.map +1 -1
  219. package/lib/Website/Vocs.d.ts.map +1 -1
  220. package/lib/Website/Vocs.js.map +1 -1
  221. package/lib/Website/Waku.d.ts.map +1 -1
  222. package/lib/Website/Waku.js.map +1 -1
  223. package/lib/Website/index.d.ts.map +1 -1
  224. package/lib/Website/index.js.map +1 -1
  225. package/lib/WriteCertificates.d.ts.map +1 -1
  226. package/lib/WriteCertificates.js.map +1 -1
  227. package/lib/WriteCertificatesHttp.d.ts.map +1 -1
  228. package/lib/WriteCertificatesHttp.js +2 -2
  229. package/lib/WriteCertificatesHttp.js.map +1 -1
  230. package/lib/WriteRedis.d.ts.map +1 -1
  231. package/lib/WriteRedis.js.map +1 -1
  232. package/lib/WriteRedisHttp.d.ts.map +1 -1
  233. package/lib/WriteRedisHttp.js.map +1 -1
  234. package/lib/WriteSecret.d.ts.map +1 -1
  235. package/lib/WriteSecret.js.map +1 -1
  236. package/lib/WriteSecretHttp.d.ts.map +1 -1
  237. package/lib/WriteSecretHttp.js.map +1 -1
  238. package/lib/bluegreen.d.ts +5 -4
  239. package/lib/bluegreen.d.ts.map +1 -1
  240. package/lib/bluegreen.js +7 -5
  241. package/lib/bluegreen.js.map +1 -1
  242. package/lib/hosted.d.ts +19 -4
  243. package/lib/hosted.d.ts.map +1 -1
  244. package/lib/hosted.js +101 -11
  245. package/lib/hosted.js.map +1 -1
  246. package/lib/index.d.ts.map +1 -1
  247. package/lib/index.js.map +1 -1
  248. package/lib/internal/Oidc.d.ts +15 -0
  249. package/lib/internal/Oidc.d.ts.map +1 -0
  250. package/lib/internal/Oidc.js +73 -0
  251. package/lib/internal/Oidc.js.map +1 -0
  252. package/lib/internal/RemoteBuilder.d.ts.map +1 -1
  253. package/lib/internal/RemoteBuilder.js +21 -8
  254. package/lib/internal/RemoteBuilder.js.map +1 -1
  255. package/lib/leases.d.ts +6 -2
  256. package/lib/leases.d.ts.map +1 -1
  257. package/lib/leases.js +25 -5
  258. package/lib/leases.js.map +1 -1
  259. package/lib/ports.d.ts.map +1 -1
  260. package/lib/ports.js.map +1 -1
  261. package/lib/replicas.d.ts +39 -27
  262. package/lib/replicas.d.ts.map +1 -1
  263. package/lib/replicas.js +63 -8
  264. package/lib/replicas.js.map +1 -1
  265. package/lib/rpc.d.ts +14 -12
  266. package/lib/rpc.d.ts.map +1 -1
  267. package/lib/rpc.js +38 -40
  268. package/lib/rpc.js.map +1 -1
  269. package/package.json +15 -15
  270. package/src/App.ts +41 -10
  271. package/src/BindService.ts +28 -36
  272. package/src/Bootstrap.ts +2 -0
  273. package/src/Bucket.ts +278 -45
  274. package/src/BucketBinding.ts +1 -1
  275. package/src/Certificate.ts +36 -5
  276. package/src/ConnectPostgres.ts +1 -1
  277. package/src/ConnectPostgresHttp.ts +1 -1
  278. package/src/Credentials.ts +18 -41
  279. package/src/DeploymentImages.ts +1 -1
  280. package/src/Errors.ts +1 -1
  281. package/src/IpAssignment.ts +35 -5
  282. package/src/Machine.ts +39 -4
  283. package/src/MountVolume.ts +5 -2
  284. package/src/Postgres.ts +50 -8
  285. package/src/PostgresMigrations.ts +39 -99
  286. package/src/Redis.ts +54 -9
  287. package/src/RedisBinding.ts +1 -1
  288. package/src/RedisHttp.ts +4 -1
  289. package/src/Secret.ts +32 -4
  290. package/src/SecretKey.ts +32 -4
  291. package/src/Service.ts +441 -88
  292. package/src/Sprite.ts +45 -17
  293. package/src/Volume.ts +30 -6
  294. package/src/VolumeSnapshot.ts +40 -2
  295. package/src/Website/AssetDeployment.ts +13 -15
  296. package/src/Website/Astro.ts +20 -15
  297. package/src/Website/FrameworkSite.ts +92 -6
  298. package/src/Website/StaticSite.ts +27 -7
  299. package/src/Website/Vite.ts +9 -0
  300. package/src/WriteCertificatesHttp.ts +2 -2
  301. package/src/bluegreen.ts +11 -5
  302. package/src/hosted.ts +121 -17
  303. package/src/internal/Oidc.ts +99 -0
  304. package/src/internal/RemoteBuilder.ts +37 -19
  305. package/src/leases.ts +35 -7
  306. package/src/replicas.ts +70 -9
  307. package/src/rpc.ts +37 -40
package/src/bluegreen.ts CHANGED
@@ -102,7 +102,7 @@ export const setRouting = (
102
102
  yield* getMachineById(appName, machineId).pipe(
103
103
  Effect.filterOrFail(
104
104
  (machine) => machine !== undefined && machine.cordoned === cordoned,
105
- () => new ReplicaNotCreated({ appName, name: machineId }),
105
+ () => new ReplicaNotCreated({ appName, resourceName: machineId }),
106
106
  ),
107
107
  Effect.retry({
108
108
  times: 8,
@@ -293,7 +293,9 @@ export const reconcileBlueGreen = Effect.fn(function* (
293
293
  reusable?.[0] ??
294
294
  (yield* sha256Object({
295
295
  workload,
296
- predecessors: owned.map((machine) => machine.id).sort(),
296
+ predecessors: owned
297
+ .map((machine) => machine.id)
298
+ .sort((a, b) => ((a ?? "") < (b ?? "") ? -1 : (a ?? "") > (b ?? "") ? 1 : 0)),
297
299
  })).slice(0, 20);
298
300
  const desired = reusable?.[1] ?? [];
299
301
  if (
@@ -417,7 +419,7 @@ export const reconcileBlueGreen = Effect.fn(function* (
417
419
  ),
418
420
  Effect.filterOrFail(
419
421
  (machine) => machine !== undefined,
420
- () => new ReplicaNotCreated({ appName: input.appName, name }),
422
+ () => new ReplicaNotCreated({ appName: input.appName, resourceName: name }),
421
423
  ),
422
424
  Effect.retry({
423
425
  times: 8,
@@ -442,11 +444,15 @@ export const reconcileBlueGreen = Effect.fn(function* (
442
444
  .pipe(
443
445
  Retry.none,
444
446
  Effect.timeout("30 seconds"),
445
- Effect.catchTag(["Conflict", "GatewayTimeout", "TimeoutError"], () => readback),
447
+ Effect.catchTag(
448
+ ["Conflict", "GatewayTimeout", "TimeoutError", "StorageHostCapacity"],
449
+ () => readback,
450
+ ),
446
451
  Effect.catchReason("HttpClientError", "TransportError", () => readback),
447
452
  );
448
453
  }
449
- if (!current?.id) return yield* new ReplicaNotCreated({ appName: input.appName, name });
454
+ if (!current?.id)
455
+ return yield* new ReplicaNotCreated({ appName: input.appName, resourceName: name });
450
456
  const leased = (yield* leaseSnapshot(input.appName, [current], leases))[0];
451
457
  if (!leased?.id)
452
458
  return yield* ambiguous(
package/src/hosted.ts CHANGED
@@ -9,9 +9,12 @@ import {
9
9
  type PackageInstall,
10
10
  } from "alchemy/Bundle/InstalledPackages";
11
11
  import { findCwdForBundle, getStableContextDir, resolveMainPath } from "alchemy/Bundle/TempRoot";
12
+ import { hashDirectory } from "alchemy/Command/Memo";
12
13
  import { safeHttpEffect } from "alchemy/Http";
14
+ import * as Output from "alchemy/Output";
13
15
  import type { ResourceBinding } from "alchemy/Resource";
14
16
  import { createContainerRuntimeContext, type HostRuntimeContext } from "alchemy/Runtime/Process";
17
+ import { packEnvValueKeepRedacted } from "alchemy/RuntimeContext";
15
18
  import {
16
19
  copyExtraFiles,
17
20
  contextRootOf,
@@ -30,6 +33,7 @@ import * as Path from "effect/Path";
30
33
  import * as Redacted from "effect/Redacted";
31
34
  import * as Schedule from "effect/Schedule";
32
35
  import type * as rolldown from "rolldown";
36
+ import { flyHostBundle, flyOidcToken } from "./internal/Oidc.ts";
33
37
  import { buildOnRemoteBuilder } from "./internal/RemoteBuilder.ts";
34
38
  import type { BoundTarget, DiskSpec, ServiceBinding } from "./MountVolume.ts";
35
39
  import { rpcMethodsOf, serveFlyRpc } from "./rpc.ts";
@@ -59,7 +63,20 @@ export const createFlyHostRuntimeContext =
59
63
  boots ? { shape: { ...shape, fetch: shape?.fetch ?? wrapped } } : options,
60
64
  ) as Effect.Effect<void, never, any>;
61
65
  };
62
- return Object.assign(base, { serve });
66
+ // Keep the `Redacted` wrapper outside the packed string so the Service
67
+ // reconcile can store those values as App secrets instead of Machine env.
68
+ const set: HostRuntimeContext["set"] = (bindingId, output) =>
69
+ Effect.sync(() => {
70
+ const key = bindingId.replaceAll(/[^a-zA-Z0-9]/g, "_");
71
+ base.env[key] = output.pipe(Output.map(packEnvValueKeepRedacted));
72
+ return key;
73
+ });
74
+ // A Service runs on Fly Machines, which mint OIDC tokens for their
75
+ // bindings to exchange with AWS and GCP (see `internal/Oidc.ts`).
76
+ return Object.assign(
77
+ base,
78
+ type === "Fly.Service" ? { serve, set, oidcToken: flyOidcToken } : { serve, set },
79
+ );
63
80
  };
64
81
 
65
82
  export const FLY_REGISTRY = "registry.fly.io";
@@ -70,13 +87,12 @@ export const MACHINE_PLATFORM = "linux/amd64";
70
87
  export interface FlyBuildOptions extends Bundle.BundleConfig {
71
88
  /**
72
89
  * Native or Node-only packages to install into the Machine image with
73
- * `npm install` instead of bundling them. `pg` is CommonJS: Rolldown's
74
- * interop turns `Client` into a namespace (`The superclass is not a
75
- * constructor`). Same `build.install` shape as Lambda.
90
+ * `npm install` instead of bundling them. Same `build.install` shape as
91
+ * Lambda.
76
92
  *
77
93
  * @example
78
94
  * ```typescript
79
- * build: { install: ["pg"] }
95
+ * build: { install: ["sharp"] }
80
96
  * ```
81
97
  */
82
98
  readonly install?: PackageInstall;
@@ -190,10 +206,14 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
190
206
  const active = bindings.filter(
191
207
  (binding: ResourceBinding<ServiceBinding> & { action?: string }) => binding.action !== "delete",
192
208
  );
193
- const env = toEnvRecord(
194
- active
195
- .map((binding) => binding?.data?.env)
196
- .reduce<Record<string, any>>((acc, value) => ({ ...acc, ...value }), {}),
209
+ const merged = active
210
+ .map((binding) => binding?.data?.env)
211
+ .reduce<Record<string, any>>((acc, value) => ({ ...acc, ...value }), {});
212
+ const env = toEnvRecord(merged);
213
+ // Names of bound `Config` values and Redacted outputs. Only the names:
214
+ // the values stay in `env` until the Service moves them into App secrets.
215
+ const secretNames = Object.keys(toEnvRecord(merged)).filter((key) =>
216
+ Redacted.isRedacted(merged[key]),
197
217
  );
198
218
  const mounts: DiskSpec[] = [];
199
219
  const seen = new Set<string>();
@@ -201,9 +221,12 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
201
221
  const buckets: { name: string; id?: string; values?: Record<string, string> }[] = [];
202
222
  const postgres: { clusterId: string; variableName?: string }[] = [];
203
223
  const targets: BoundTarget[] = [];
224
+ const callers = new Set<string>();
204
225
  for (const binding of active) {
205
226
  const target = binding?.data?.target;
206
227
  if (target !== undefined) targets.push(target);
228
+ const caller = coerceBindingName(binding?.data?.caller?.appName);
229
+ if (caller !== undefined) callers.add(caller);
207
230
  for (const mount of binding?.data?.mounts ?? []) {
208
231
  if (seen.has(mount.path)) continue;
209
232
  seen.add(mount.path);
@@ -257,7 +280,16 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
257
280
  });
258
281
  }
259
282
  }
260
- return { env, mounts, redis, buckets, postgres, targets };
283
+ return {
284
+ env,
285
+ secretNames,
286
+ mounts,
287
+ redis,
288
+ buckets,
289
+ postgres,
290
+ targets,
291
+ callers: [...callers].sort(),
292
+ };
261
293
  };
262
294
 
263
295
  /**
@@ -378,7 +410,7 @@ export const createFlyHostedSupport = ({
378
410
  plugins?: rolldown.RolldownPluginOption,
379
411
  ) {
380
412
  return yield* Bundle.build(
381
- {
413
+ flyHostBundle({
382
414
  ...props.build?.input,
383
415
  input: entry,
384
416
  cwd,
@@ -395,7 +427,7 @@ export const createFlyHostedSupport = ({
395
427
  ...props.build?.input?.resolve,
396
428
  },
397
429
  plugins: [props.build?.input?.plugins, plugins],
398
- },
430
+ }),
399
431
  {
400
432
  ...props.build?.output,
401
433
  format: "esm",
@@ -540,10 +572,10 @@ export const createFlyHostedSupport = ({
540
572
  );
541
573
  const entryRel = posixRelUnder(root, realMain, path) ?? path.basename(realMain);
542
574
  const dest = path.join(contextDir, entryRel);
543
- if (!(yield* fs.exists(dest).pipe(Effect.orElseSucceed(() => false)))) {
544
- yield* fs.makeDirectory(path.dirname(dest), { recursive: true });
545
- yield* fs.copy(realMain, dest, { overwrite: true });
546
- }
575
+ // The context directory is reused across deploys: always copy the
576
+ // entry, or a changed entry ships the previous deploy's copy.
577
+ yield* fs.makeDirectory(path.dirname(dest), { recursive: true });
578
+ yield* fs.copy(realMain, dest, { overwrite: true });
547
579
  }
548
580
  return contextDir;
549
581
  });
@@ -584,7 +616,79 @@ export const createFlyHostedSupport = ({
584
616
  return codeHash;
585
617
  });
586
618
 
587
- return { alchemyEnv, bundleProgram, computeCodeHash, resolveImage, hash };
619
+ /** A Docker build context's content hash: its image changes when any file in it does. */
620
+ const contextHash = Effect.fn(function* (context: string) {
621
+ return (yield* hashDirectory({ cwd: context })).slice(0, 16);
622
+ });
623
+
624
+ /**
625
+ * Build the image a `context` directory's `Dockerfile` describes, as it
626
+ * is, and push it to `registry.fly.io/{appName}:{logicalId}-{hash}`.
627
+ * When `previousHash` matches, skip build and push.
628
+ */
629
+ const resolveContextImage = Effect.fn(function* (input: {
630
+ id: string;
631
+ appName: string;
632
+ context: string;
633
+ remote: boolean | undefined;
634
+ previousHash?: string;
635
+ session?: { note: (message: string) => Effect.Effect<void> };
636
+ }) {
637
+ const note = input.session?.note ?? ((_message: string) => Effect.void);
638
+ const codeHash = yield* contextHash(input.context);
639
+ const imageRef = `${FLY_REGISTRY}/${input.appName}:${sanitizeImageRepo(input.id)}-${codeHash}`;
640
+ if (input.previousHash === codeHash) return { imageRef, codeHash };
641
+
642
+ const minted = yield* machines.createAppDeployToken({ app_name: input.appName });
643
+ const token =
644
+ minted.token === undefined || Redacted.isRedacted(minted.token)
645
+ ? minted.token
646
+ : Redacted.make(minted.token);
647
+ if (token === undefined || Redacted.value(token).length === 0) {
648
+ return yield* new DeployTokenMissing({ appName: input.appName });
649
+ }
650
+ const registry = { server: FLY_REGISTRY, username: "x", password: token };
651
+
652
+ if (input.remote !== false) {
653
+ yield* note(`Building ${imageRef} on a Fly remote builder...`);
654
+ yield* buildOnRemoteBuilder({
655
+ docker,
656
+ appName: input.appName,
657
+ contextDir: input.context,
658
+ imageRef,
659
+ platform: MACHINE_PLATFORM,
660
+ registry,
661
+ note,
662
+ });
663
+ yield* note(`Pushed ${imageRef}`);
664
+ return { imageRef, codeHash };
665
+ }
666
+
667
+ if (!(yield* imageExists(imageRef))) {
668
+ yield* note(`Building container image ${imageRef}...`);
669
+ yield* docker.image.build({
670
+ context: input.context,
671
+ tag: imageRef,
672
+ platform: MACHINE_PLATFORM,
673
+ });
674
+ }
675
+ yield* note(`Pushing ${imageRef}...`);
676
+ yield* docker.image
677
+ .push(imageRef, registry)
678
+ .pipe(Effect.retry({ times: 3, schedule: pushBackoff }));
679
+ yield* note(`Pushed ${imageRef}`);
680
+ return { imageRef, codeHash };
681
+ });
682
+
683
+ return {
684
+ alchemyEnv,
685
+ bundleProgram,
686
+ computeCodeHash,
687
+ resolveImage,
688
+ hash,
689
+ contextHash,
690
+ resolveContextImage,
691
+ };
588
692
  };
589
693
 
590
694
  /**
@@ -0,0 +1,99 @@
1
+ import * as http from "node:http";
2
+ import { OidcTokenError } from "alchemy/RuntimeContext";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Redacted from "effect/Redacted";
5
+ import type * as rolldown from "rolldown";
6
+
7
+ /**
8
+ * Fly Machine OIDC tokens. Every Machine serves part of the Machines API on
9
+ * the `/.fly/api` socket, authenticated as that Machine, so a process can
10
+ * mint a token without holding one. A token is signed by
11
+ * `https://oidc.fly.io/<org>`, lives 10 minutes, and names its caller as
12
+ * `sub = <org>:<app>:<machine>`. AWS and GCP trust it through an OIDC
13
+ * provider, so a Fly host reaches them without a key Alchemy created.
14
+ *
15
+ * See https://fly.io/docs/security/openid-connect/.
16
+ */
17
+
18
+ /** The socket a Machine serves its Machines API on. */
19
+ const FLY_API_SOCKET = "/.fly/api";
20
+
21
+ /** Fly's OIDC issuer for an organization. */
22
+ export const flyOidcIssuer = (orgSlug: string) => `https://oidc.fly.io/${orgSlug}`;
23
+
24
+ /**
25
+ * Bundle options for a Fly Service: defines `__ALCHEMY_HOST_IDENTITY__` as
26
+ * `"Fly.OIDC"` so bindings keep only the code that exchanges the Machine's
27
+ * OIDC token, and drop the created-key path.
28
+ */
29
+ export const flyHostBundle = (input: rolldown.InputOptions): rolldown.InputOptions => ({
30
+ ...input,
31
+ transform: {
32
+ ...input.transform,
33
+ define: {
34
+ ...input.transform?.define,
35
+ "globalThis.__ALCHEMY_HOST_IDENTITY__": JSON.stringify("Fly.OIDC"),
36
+ },
37
+ },
38
+ });
39
+
40
+ /** Mint an OIDC token for `audience` from the Machine's `/.fly/api` socket. */
41
+ export const flyOidcToken = (
42
+ audience: string,
43
+ ): Effect.Effect<Redacted.Redacted<string>, OidcTokenError> =>
44
+ Effect.callback<Redacted.Redacted<string>, OidcTokenError>((resume) => {
45
+ const body = JSON.stringify({ aud: audience });
46
+ const request = http.request(
47
+ {
48
+ socketPath: FLY_API_SOCKET,
49
+ path: "/v1/tokens/oidc",
50
+ method: "POST",
51
+ headers: {
52
+ "content-type": "application/json",
53
+ "content-length": Buffer.byteLength(body),
54
+ },
55
+ },
56
+ (response) => {
57
+ let text = "";
58
+ response.setEncoding("utf8");
59
+ response.on("data", (chunk: string) => {
60
+ text += chunk;
61
+ });
62
+ response.on("end", () => {
63
+ if (response.statusCode === 200 && text.trim().length > 0) {
64
+ resume(Effect.succeed(Redacted.make(text.trim())));
65
+ } else {
66
+ resume(
67
+ Effect.fail(
68
+ new OidcTokenError({
69
+ message: `${FLY_API_SOCKET} returned HTTP ${response.statusCode} for an OIDC token: ${text.slice(0, 200)}`,
70
+ }),
71
+ ),
72
+ );
73
+ }
74
+ });
75
+ response.on("error", (cause) =>
76
+ resume(
77
+ Effect.fail(
78
+ new OidcTokenError({
79
+ message: `Reading the OIDC token from ${FLY_API_SOCKET}`,
80
+ cause,
81
+ }),
82
+ ),
83
+ ),
84
+ );
85
+ },
86
+ );
87
+ request.on("error", (cause) =>
88
+ resume(
89
+ Effect.fail(
90
+ new OidcTokenError({
91
+ message: `Cannot reach ${FLY_API_SOCKET} for an OIDC token. It is served only on Fly Machines, to root.`,
92
+ cause,
93
+ }),
94
+ ),
95
+ ),
96
+ );
97
+ request.end(body);
98
+ return Effect.sync(() => request.destroy());
99
+ });
@@ -37,7 +37,9 @@ const HEALTH_INTERVAL = Duration.seconds(5);
37
37
  const MAX_PENDING_POLLS = 240;
38
38
  /** How often, and how long, to wait for the builder's push to reach `registry.fly.io`. */
39
39
  const UPSTREAM_POLL = Duration.seconds(2);
40
- const MAX_UPSTREAM_POLLS = 90;
40
+ const MAX_UPSTREAM_POLLS = 30;
41
+ /** Pushes of one image before giving up on it reaching the registry. */
42
+ const MAX_PUSHES = 3;
41
43
 
42
44
  export class RemoteBuilderError extends Data.TaggedError("Fly.RemoteBuilderError")<{
43
45
  operation: string;
@@ -170,7 +172,13 @@ export const buildOnRemoteBuilder = Effect.fn(function* (input: {
170
172
  () => input.docker.run(["buildx", "rm", builder]).pipe(Effect.ignore),
171
173
  );
172
174
 
173
- yield* input.docker.image
175
+ // The builder pushes through its host's registry mirror, which forwards
176
+ // to registry.fly.io. Machines pull from there, so confirm the tag landed
177
+ // (usually within seconds) while the builder is still held. The mirror
178
+ // occasionally drops a push without an error, so a tag that hasn't
179
+ // landed is pushed again: the builder has every layer cached, so a
180
+ // repeat build only re-sends the image.
181
+ const build = input.docker.image
174
182
  .build(
175
183
  {
176
184
  context: input.contextDir,
@@ -193,25 +201,35 @@ export const buildOnRemoteBuilder = Effect.fn(function* (input: {
193
201
  }),
194
202
  }),
195
203
  );
196
-
197
- // The builder pushes through its host's registry mirror, which forwards
198
- // to registry.fly.io. Machines pull from there, so confirm the tag landed
199
- // (usually within seconds) while the builder is still held.
200
- const started = yield* Clock.currentTimeMillis;
201
- yield* input.docker.run(["buildx", "imagetools", "inspect", "--raw", input.imageRef]).pipe(
202
- Effect.provideService(DockerEnvironment, {
203
- DOCKER_AUTH_CONFIG: JSON.stringify({
204
- auths: { [input.registry.server]: { auth: basicAuth(input.registry) } },
205
- }),
206
- }),
207
- Effect.retry({ schedule: Schedule.spaced(UPSTREAM_POLL), times: MAX_UPSTREAM_POLLS }),
208
- Effect.mapError(
209
- () =>
210
- new RemoteBuilderError({
211
- operation: "upload",
212
- message: `${input.imageRef} did not reach ${input.registry.server}`,
204
+ const landed = input.docker
205
+ .run(["buildx", "imagetools", "inspect", "--raw", input.imageRef])
206
+ .pipe(
207
+ Effect.provideService(DockerEnvironment, {
208
+ DOCKER_AUTH_CONFIG: JSON.stringify({
209
+ auths: { [input.registry.server]: { auth: basicAuth(input.registry) } },
213
210
  }),
211
+ }),
212
+ Effect.retry({ schedule: Schedule.spaced(UPSTREAM_POLL), times: MAX_UPSTREAM_POLLS }),
213
+ Effect.mapError(
214
+ () =>
215
+ new RemoteBuilderError({
216
+ operation: "upload",
217
+ message: `${input.imageRef} did not reach ${input.registry.server} after ${MAX_PUSHES} pushes`,
218
+ }),
219
+ ),
220
+ );
221
+ const started = yield* Clock.currentTimeMillis;
222
+ yield* build.pipe(
223
+ Effect.andThen(landed),
224
+ Effect.tapError((error) =>
225
+ error._tag === "Fly.RemoteBuilderError" && error.operation === "upload"
226
+ ? note(`${input.imageRef} hasn't reached ${input.registry.server} yet`)
227
+ : Effect.void,
214
228
  ),
229
+ Effect.retry({
230
+ while: (error) => error._tag === "Fly.RemoteBuilderError" && error.operation === "upload",
231
+ times: MAX_PUSHES - 1,
232
+ }),
215
233
  );
216
234
  const waited = (yield* Clock.currentTimeMillis) - started;
217
235
  yield* note(`${input.imageRef} available on ${input.registry.server} after ${waited}ms`);
package/src/leases.ts CHANGED
@@ -17,6 +17,7 @@ import * as FetchHttpClient from "effect/http/FetchHttpClient";
17
17
  import * as HttpClient from "effect/http/HttpClient";
18
18
  import * as HttpClientRequest from "effect/http/HttpClientRequest";
19
19
  import * as Option from "effect/Option";
20
+ import * as Random from "effect/Random";
20
21
  import * as Schedule from "effect/Schedule";
21
22
 
22
23
  const TTL_SECONDS = 120;
@@ -37,14 +38,20 @@ const retrySchedule = <E>(): Schedule.Schedule<Duration.Duration, E> =>
37
38
  ),
38
39
  ),
39
40
  Schedule.addDelay<Duration.Duration, E>(() =>
40
- Effect.sync(() => Duration.millis(Math.random() * 50)),
41
+ Effect.map(Random.next, (random) => Duration.millis(random * 50)),
41
42
  ),
42
43
  );
43
44
 
44
45
  export class MachineLeaseBusy extends Data.TaggedError("Fly.MachineLeaseBusy")<{
45
46
  appName: string;
46
47
  machineId: string;
47
- }> {}
48
+ /** Fly's conflict message: the lease holder and its expiry. */
49
+ reason?: string;
50
+ }> {
51
+ get message() {
52
+ return `Machine lease for ${this.appName}/${this.machineId} is held by another operation${this.reason ? `: ${this.reason}` : ""}`;
53
+ }
54
+ }
48
55
 
49
56
  export class MachineLeaseLost extends Data.TaggedError("Fly.MachineLeaseLost")<{
50
57
  appName: string;
@@ -71,6 +78,9 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
71
78
  const scope = yield* Effect.scope;
72
79
  const held = new Map<string, HeldLease>();
73
80
  const deleting = new Set<string>();
81
+ // Machines this lease set deleted; a later snapshot of the same operation
82
+ // must not wait for them to accept a lease.
83
+ const removed = new Set<string>();
74
84
  const renewals = new Set<Fiber.Fiber<void, never>>();
75
85
  const uncertain = new Set<string>();
76
86
  const lost = yield* Deferred.make<never, MachineLeaseLost>();
@@ -131,8 +141,10 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
131
141
  while: (error) => error._tag === "Conflict" || error._tag === "TooManyRequests",
132
142
  }),
133
143
  Effect.timeout("30 seconds"),
134
- Effect.catchTag("Conflict", () =>
135
- Effect.fail(new MachineLeaseBusy({ appName, machineId })),
144
+ Effect.catchTag("Conflict", (error) =>
145
+ Effect.fail(
146
+ new MachineLeaseBusy({ appName, machineId, reason: error.message }),
147
+ ),
136
148
  ),
137
149
  Effect.catchTag(["HttpClientError", "GatewayTimeout", "TimeoutError"], (error) =>
138
150
  lose(machineId, `acquisition uncertain: ${error._tag}`),
@@ -274,7 +286,10 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
274
286
  Effect.asVoid,
275
287
  Effect.catch((error) =>
276
288
  Effect.gen(function* () {
277
- if (error._tag === "NotFound") return yield* forget(machineId);
289
+ if (error._tag === "NotFound") {
290
+ removed.add(machineId);
291
+ return yield* forget(machineId);
292
+ }
278
293
  if (
279
294
  error._tag === "Fly.MachineMutationUncertain" ||
280
295
  error._tag === "InternalServerError" ||
@@ -282,12 +297,16 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
282
297
  error._tag === "ServiceUnavailable"
283
298
  ) {
284
299
  // Keep renewal aware of the removal until its exact target has been observed.
285
- if (yield* confirmRemoved(machineId)) return;
300
+ if (yield* confirmRemoved(machineId)) {
301
+ removed.add(machineId);
302
+ return;
303
+ }
286
304
  }
287
305
  return yield* Effect.fail(error);
288
306
  }),
289
307
  ),
290
308
  );
309
+ removed.add(machineId);
291
310
  yield* forget(machineId);
292
311
  }).pipe(
293
312
  Effect.ensuring(
@@ -360,7 +379,14 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
360
379
  Effect.retry({
361
380
  times: 8,
362
381
  schedule: retrySchedule<machines.CreateMachineLeaseError | MachineLeaseLost>(),
363
- while: (error) => error._tag === "TooManyRequests",
382
+ // A renewal re-sends the nonce we hold, so replaying it after a
383
+ // transient 5xx extends the same lease instead of taking a new one.
384
+ while: (error) =>
385
+ error._tag === "TooManyRequests" ||
386
+ error._tag === "InternalServerError" ||
387
+ error._tag === "BadGateway" ||
388
+ error._tag === "ServiceUnavailable" ||
389
+ error._tag === "GatewayTimeout",
364
390
  }),
365
391
  Effect.timeout("75 seconds"),
366
392
  Effect.catch((error) =>
@@ -407,6 +433,7 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
407
433
  Effect.forkScoped,
408
434
  );
409
435
  const checkTarget = (machineId: string) => nonce(machineId).pipe(Effect.asVoid);
436
+ const wasRemoved = (machineId: string) => removed.has(machineId);
410
437
  return {
411
438
  acquire,
412
439
  check,
@@ -416,6 +443,7 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
416
443
  remove,
417
444
  forget,
418
445
  nonceIfHeld,
446
+ wasRemoved,
419
447
  };
420
448
  });
421
449