@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/Service.ts CHANGED
@@ -14,11 +14,11 @@ import { deepEqual, isResolved } from "alchemy/Diff";
14
14
  import type { Input } from "alchemy/Input";
15
15
  import { Platform, type Main, type MainRpc, type PlatformProps } from "alchemy/Platform";
16
16
  import * as Provider from "alchemy/Provider";
17
- import { makeRandom } from "alchemy/Random";
18
- import type { Resource } from "alchemy/Resource";
17
+ import type { LookupMethod, Resource } from "alchemy/Resource";
19
18
  import type { ServerHost } from "alchemy/Runtime/Process";
20
19
  import { packEnvValue } from "alchemy/RuntimeContext";
21
20
  import { Stack } from "alchemy/Stack";
21
+ import { sha256 } from "alchemy/Util/sha256";
22
22
  import * as Data from "effect/Data";
23
23
  import * as Effect from "effect/Effect";
24
24
  import * as Layer from "effect/Layer";
@@ -52,6 +52,7 @@ import {
52
52
  createFlyAppName,
53
53
  createFlyResourceName,
54
54
  diffMachineMetadata,
55
+ matchesAlchemyPhysicalName,
55
56
  sanitizeFlyAppName,
56
57
  } from "./Metadata.ts";
57
58
  import type { BoundTarget, MountedDisk, ServiceBinding } from "./MountVolume.ts";
@@ -64,6 +65,7 @@ import {
64
65
  deleteReplicaSet,
65
66
  hasPublishedService,
66
67
  listReplicaSets,
68
+ lookupReplicaSet,
67
69
  observeReplicaSet,
68
70
  reconcileReplicas,
69
71
  resolveCount,
@@ -73,7 +75,7 @@ import {
73
75
  type Replica,
74
76
  type ReplicaSet,
75
77
  } from "./replicas.ts";
76
- import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_ORG_ENV, RPC_TOKEN_ENV } from "./rpc.ts";
78
+ import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_CALLERS_ENV, RPC_ORG_ENV } from "./rpc.ts";
77
79
 
78
80
  /**
79
81
  * A resource-valued prop: the resource itself, or an Effect that produces
@@ -139,18 +141,21 @@ export interface ServiceProps extends PlatformProps {
139
141
  * @default 7780
140
142
  */
141
143
  bindingPort?: number;
142
- /**
143
- * Caller token for bound callers. Set automatically from an
144
- * `Alchemy.Random` child resource; do not pass it.
145
- * @internal
146
- */
147
- rpcToken?: Redacted.Redacted<string>;
148
144
  /**
149
145
  * Module entrypoint bundled with rolldown and baked into a Docker
150
146
  * image pushed to `registry.fly.io`. Typically `import.meta.url`.
151
147
  * A content-hash change updates the workload using the selected deployment strategy.
148
+ * Set this or {@link context}.
149
+ */
150
+ main?: string;
151
+ /**
152
+ * A Docker build context whose `Dockerfile` builds the Service's image
153
+ * as it is, on a Fly remote builder (or locally with
154
+ * `build: { remote: false }`), for `linux/amd64`. The image's own
155
+ * `CMD` runs. A change to any file in it updates the workload. Set this
156
+ * or {@link main}.
152
157
  */
153
- main: string;
158
+ context?: string;
154
159
  /**
155
160
  * Region or regions to run in (`"iad"`, `["iad", "lhr"]`). The
156
161
  * Service runs `count` Machines in each region behind one hostname;
@@ -271,6 +276,19 @@ export interface ServiceEndpoint {
271
276
  url: string | undefined;
272
277
  }
273
278
 
279
+ /** An App secret a {@link Service} wrote. Never the plaintext. */
280
+ export interface BoundSecret {
281
+ /**
282
+ * Name of the App secret, scoped to the Service
283
+ * (`ALCHEMY_<hash>_<ENV_VAR>`). The Machine maps it back to its env var.
284
+ */
285
+ name: string;
286
+ /** Fly's digest of the value after the write. */
287
+ digest: string;
288
+ /** sha256 of `<name>:<value>` for the value written. Never the plaintext. */
289
+ sha256: string;
290
+ }
291
+
274
292
  export type Service = Resource<
275
293
  "Fly.Service",
276
294
  ServiceProps,
@@ -286,8 +304,19 @@ export type Service = Resource<
286
304
  * shared App's network. `undefined` for the organization's default.
287
305
  */
288
306
  network?: string;
289
- /** Token bound callers send with RPC calls. */
290
- rpcToken?: Redacted.Redacted<string>;
307
+ /**
308
+ * App secrets the Service wrote from bound `Config` and Redacted
309
+ * values, by the env var they set: the scoped secret name, Fly's
310
+ * digest after the write and a sha256 of the value.
311
+ */
312
+ secrets?: Record<string, BoundSecret>;
313
+ /** App-secrets version that holds {@link secrets}. */
314
+ secretsVersion?: number;
315
+ /**
316
+ * Slug of the organization the App is in. Its Machines' OIDC tokens are
317
+ * issued by `https://oidc.fly.io/{orgSlug}`.
318
+ */
319
+ orgSlug?: string;
291
320
  /** Fly Machine id of replica 0. */
292
321
  machineId: string;
293
322
  /** Fly Machine ids of every replica. */
@@ -338,9 +367,24 @@ export type Service = Resource<
338
367
  code: { hash: string };
339
368
  },
340
369
  ServiceBinding,
341
- Providers
370
+ Providers,
371
+ ServiceLookup
342
372
  >;
343
373
 
374
+ /**
375
+ * Props for {@link Service.lookup}: find a Service this stack doesn't
376
+ * manage by its App, and its name when the App holds several.
377
+ */
378
+ export interface ServiceLookup {
379
+ /** Fly App name the Service's Machines run in. */
380
+ appName: string;
381
+ /**
382
+ * Machine name of the Service, for an App that runs several. Omit it to
383
+ * take every Machine in the App.
384
+ */
385
+ name?: string;
386
+ }
387
+
344
388
  export const isService = (value: unknown): value is Service =>
345
389
  typeof value === "object" &&
346
390
  value !== null &&
@@ -538,9 +582,9 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
538
582
  * ### Call another Service
539
583
  * A Service can return methods next to `fetch`. Another Service calls
540
584
  * them by binding it with {@link bindService}, which returns a typed
541
- * client. Binding makes the caller deploy after the Service, and only
542
- * callers that bind a Service receive its caller token, so only they
543
- * can call its methods.
585
+ * client. Binding makes the caller deploy after the Service and records
586
+ * the caller's App on it, so only callers that bind a Service can call
587
+ * its methods.
544
588
  *
545
589
  * **Example:** A private Service with methods
546
590
  * ```typescript
@@ -574,9 +618,10 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
574
618
  * ```
575
619
  *
576
620
  * Calls use plain HTTP to `privateUrl` inside Fly's WireGuard-encrypted
577
- * private network. The Service accepts a method call only with its
578
- * caller token and a `Fly-Src` signature from Fly's proxy for the same
579
- * organization, so the public internet can never call it. A public
621
+ * private network. The Service accepts a method call only with a
622
+ * `Fly-Src` signature from Fly's proxy naming the App of a Service that
623
+ * binds it, so neither the public internet nor any other App can call
624
+ * it, and no caller holds a key. A public
580
625
  * Service publishes an extra plain-HTTP port for bound callers,
581
626
  * `bindingPort` (default 7780), since its own ports are an HTTPS
582
627
  * redirect and HTTPS. `client.fetch` sends a request to the Service's
@@ -727,8 +772,18 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
727
772
  *
728
773
  * ### Config
729
774
  * Yield `Config` in init. Alchemy reads the value from the env of
730
- * whoever deploys and writes it onto the Machine, so there is no need
731
- * to copy it into `env`.
775
+ * whoever deploys and stores it as a secret on the Service's App, so
776
+ * there is no need to copy it into `env` and it never appears in the
777
+ * Machine config. Redacted outputs of bound resources are stored the
778
+ * same way. The secret's name is scoped to the Service
779
+ * (`ALCHEMY_<hash>_API_KEY`) and the Machine maps it back to `API_KEY`,
780
+ * so it never collides with `Fly.Secret`, `fly secrets set` or an
781
+ * attachment. The bound value wins over an App secret already named
782
+ * `API_KEY`, which stays in the App untouched. The Machine sees the
783
+ * App's other secrets as they were at deploy; one set later with
784
+ * `fly secrets set` reaches it on the next deploy. A Service in a shared
785
+ * App (`app`) writes the values into the Machine env instead, because
786
+ * Fly gives every App secret to every Machine in the App.
732
787
  *
733
788
  * `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
734
789
  * `Redacted.value` only where you need the raw string.
@@ -866,6 +921,20 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
866
921
  * ) {}
867
922
  * ```
868
923
  *
924
+ * ### Run your own image
925
+ * `context` builds a directory's `Dockerfile` as it is, on a Fly remote
926
+ * builder, and runs the image's own `CMD`. A change to any file in the
927
+ * directory updates the Machines.
928
+ *
929
+ * **Example:** A Dockerfile build
930
+ * ```typescript
931
+ * const worker = yield* Fly.Service("Worker", {
932
+ * context: "./worker",
933
+ * public: false,
934
+ * services: [],
935
+ * });
936
+ * ```
937
+ *
869
938
  * ### Custom proxy services
870
939
  * `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
871
940
  * toward `port`, or plain HTTP 80 for a private Service. Pass a custom
@@ -928,28 +997,23 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
928
997
  * ) {}
929
998
  * ```
930
999
  *
931
- * **Example:** Install `pg` unbundled
932
- * `pg` is CommonJS. Rolldown's interop turns `Client` into a namespace.
933
- * Install it into the image so `@effect/sql-pg` / `Drizzle.Postgres` load
934
- * it with Node's CJS semantics — same `build.install` as Lambda.
1000
+ * **Example:** Install a native package unbundled
1001
+ * `build.install` runs `npm install` in the image for packages that
1002
+ * cannot be bundled, such as native addons — same `build.install` as
1003
+ * Lambda.
935
1004
  * ```typescript
936
1005
  * export default class Api extends Fly.Service<Api>()(
937
1006
  * "Api",
938
1007
  * {
939
1008
  * main: import.meta.url,
940
1009
  * port: 3000,
941
- * build: { install: ["pg"] },
1010
+ * build: { install: ["sharp"] },
942
1011
  * },
943
1012
  * Effect.gen(function* () {
944
- * const conn = yield* Fly.ConnectPostgres(Db);
945
- * const db = yield* Drizzle.Postgres(conn.connectionString);
946
1013
  * return {
947
- * fetch: Effect.gen(function* () {
948
- * const rows = yield* db.execute("select 1 as ok");
949
- * return HttpServerResponse.json({ rows });
950
- * }),
1014
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
951
1015
  * };
952
- * }).pipe(Effect.provide(Fly.ConnectPostgresHttp)),
1016
+ * }),
953
1017
  * ) {}
954
1018
  * ```
955
1019
  *
@@ -1033,38 +1097,48 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
1033
1097
  * See the [deployment guide](/fly/compute/deployments) for recovery,
1034
1098
  * idle capacity, application responsibilities, and verification limits.
1035
1099
  *
1100
+ * ### Looking up an existing Service
1101
+ * `Service.lookup` references a Service Alchemy doesn't manage (deployed
1102
+ * with `fly deploy` or by another stack). It is read during plan/deploy,
1103
+ * never created, updated or deleted, and `url`, `endpoints` and
1104
+ * `privateUrl` come from the Machines and addresses Fly reports.
1105
+ *
1106
+ * **Example:** Call a Service another stack deployed
1107
+ * ```typescript
1108
+ * const api = Fly.Service.lookup({ appName: "my-api" });
1109
+ * const url = api.privateUrl;
1110
+ * ```
1111
+ *
1036
1112
  * @resource
1037
1113
  * @product Service
1038
1114
  */
1039
- export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRuntimeContext> =
1040
- Platform("Fly.Service", {
1041
- createRuntimeContext: createFlyHostRuntimeContext("Fly.Service"),
1042
- // `{ app: Site }` at module scope is an Effect. Yield it here so the
1043
- // App is registered and `news.app` is resolved attributes at
1044
- // reconcile (same DX as `yield* App(...)` inside Effect.gen).
1045
- transformProps: (id, props) =>
1046
- Effect.gen(function* () {
1047
- if (globalThis.__ALCHEMY_RUNTIME__) return props;
1048
- const app = Effect.isEffect(props.app)
1049
- ? yield* props.app as Effect.Effect<App, never, Providers>
1050
- : props.app;
1051
- // Only callers that bind this Service receive the token.
1052
- const rpcToken = yield* makeRandom(`${id}RpcToken`);
1053
- if (app !== undefined) {
1054
- // Report the ports this Service publishes so the App can reject
1055
- // two Services on the same port before either one is deployed.
1056
- yield* app.bind(id, {
1057
- service: id,
1058
- services: props.services,
1059
- bindingPort: props.bindingPort,
1060
- });
1061
- }
1062
- return { ...props, app, rpcToken } as typeof props;
1063
- }),
1064
- });
1115
+ export const Service: Platform<Service, ServiceServices, ServiceShape, ServiceRuntimeContext> &
1116
+ LookupMethod<Service> = Platform("Fly.Service", {
1117
+ createRuntimeContext: createFlyHostRuntimeContext("Fly.Service"),
1118
+ // `{ app: Site }` at module scope is an Effect. Yield it here so the
1119
+ // App is registered and `news.app` is resolved attributes at
1120
+ // reconcile (same DX as `yield* App(...)` inside Effect.gen).
1121
+ transformProps: (id, props) =>
1122
+ Effect.gen(function* () {
1123
+ if (globalThis.__ALCHEMY_RUNTIME__) return props;
1124
+ const app = Effect.isEffect(props.app)
1125
+ ? yield* props.app as Effect.Effect<App, never, Providers>
1126
+ : props.app;
1127
+ if (app !== undefined) {
1128
+ // Report the ports this Service publishes so the App can reject
1129
+ // two Services on the same port before either one is deployed.
1130
+ yield* app.bind(id, {
1131
+ service: id,
1132
+ services: props.services,
1133
+ bindingPort: props.bindingPort,
1134
+ });
1135
+ }
1136
+ return { ...props, app } as typeof props;
1137
+ }),
1138
+ });
1065
1139
 
1066
1140
  export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated")<{
1067
- name: string;
1141
+ resourceName: string;
1068
1142
  appName: string;
1069
1143
  }> {}
1070
1144
 
@@ -1111,6 +1185,147 @@ const toFlyGuest = (guest: MachineGuest | undefined): FlyMachineGuest => {
1111
1185
  return fly;
1112
1186
  };
1113
1187
 
1188
+ /**
1189
+ * Prefix of the App secrets a Service writes. Scoped to its FQN so they
1190
+ * never share a name with `Fly.Secret`, `fly secrets set`, the
1191
+ * Redis/Postgres/Bucket attachments, or another Service.
1192
+ */
1193
+ const boundSecretPrefix = Effect.fn(function* (fqn: string) {
1194
+ const hash = yield* sha256(fqn);
1195
+ return `ALCHEMY_${hash.slice(0, 8).toUpperCase()}_`;
1196
+ });
1197
+
1198
+ /** A secret some Service wrote under its {@link boundSecretPrefix}. */
1199
+ const SCOPED_SECRET = /^ALCHEMY_[0-9A-F]{8}_/;
1200
+
1201
+ /**
1202
+ * Store bound `Config` and Redacted values as App secrets under names
1203
+ * scoped to the Service; the Machine config maps each back to its env var
1204
+ * (see {@link secretMappings}). Fly's digest is not a hash Alchemy can
1205
+ * compute, so a value is unchanged only when its sha256 matches the last
1206
+ * write and Fly still reports that write's digest. Deletes secrets under
1207
+ * the Service's prefix that are no longer bound, including ones a crash
1208
+ * left out of state; never touches any other secret. Returns what it now
1209
+ * owns, the App's other secrets the Machine should still see, and the
1210
+ * App-secrets version Machines must reach.
1211
+ */
1212
+ const syncBoundSecrets = Effect.fn(function* (
1213
+ appName: string,
1214
+ fqn: string,
1215
+ desired: Record<string, Redacted.Redacted<string>>,
1216
+ previous: { secrets: Record<string, BoundSecret>; version: number | undefined },
1217
+ ) {
1218
+ if (Object.keys(desired).length === 0 && Object.keys(previous.secrets).length === 0) {
1219
+ return { secrets: {}, others: [], version: previous.version };
1220
+ }
1221
+ const prefix = yield* boundSecretPrefix(fqn);
1222
+ const listDigests = machines
1223
+ .listSecrets({ app_name: appName, show_secrets: false })
1224
+ .pipe(
1225
+ Effect.map(
1226
+ (listed) =>
1227
+ new Map(
1228
+ (listed.secrets ?? []).flatMap((secret) =>
1229
+ secret.name === undefined ? [] : [[secret.name, secret.digest ?? ""] as const],
1230
+ ),
1231
+ ),
1232
+ ),
1233
+ );
1234
+ const observed = yield* listDigests;
1235
+ const planned = yield* Effect.forEach(
1236
+ Object.entries(desired),
1237
+ ([envVar, value]) =>
1238
+ // Salted with the scoped name, so equal values on different Services
1239
+ // or Apps never share a hash in state.
1240
+ sha256(`${prefix}${envVar}:${Redacted.value(value)}`).pipe(
1241
+ Effect.map((hash) => ({ envVar, name: `${prefix}${envVar}`, value, hash })),
1242
+ ),
1243
+ { concurrency: "unbounded" },
1244
+ );
1245
+ const isUnchanged = ({ envVar, name, hash }: (typeof planned)[number]) => {
1246
+ const last = previous.secrets[envVar];
1247
+ return (
1248
+ last !== undefined &&
1249
+ last.name === name &&
1250
+ last.sha256 === hash &&
1251
+ observed.get(name) === last.digest
1252
+ );
1253
+ };
1254
+ const kept = planned.filter(isUnchanged);
1255
+ const changed = planned.filter((entry) => !isUnchanged(entry));
1256
+ const unbound = [...observed.keys()].filter(
1257
+ (name) => name.startsWith(prefix) && !(name.slice(prefix.length) in desired),
1258
+ );
1259
+ const write =
1260
+ changed.length === 0
1261
+ ? Effect.succeed({ version: undefined, written: [] as [string, BoundSecret][] })
1262
+ : Effect.gen(function* () {
1263
+ const response = yield* machines.updateSecrets({
1264
+ app_name: appName,
1265
+ values: Object.fromEntries(changed.map(({ name, value }) => [name, value])),
1266
+ });
1267
+ const digests = yield* listDigests;
1268
+ return {
1269
+ version: response.version ?? response.Version,
1270
+ written: changed.map(({ envVar, name, hash }): [string, BoundSecret] => [
1271
+ envVar,
1272
+ { name, digest: digests.get(name) ?? "", sha256: hash },
1273
+ ]),
1274
+ };
1275
+ });
1276
+ const remove = Effect.forEach(
1277
+ unbound,
1278
+ (name) =>
1279
+ machines.deleteSecret({ app_name: appName, secret_name: name }).pipe(
1280
+ Effect.map((response) => response.version ?? response.Version),
1281
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
1282
+ ),
1283
+ { concurrency: "unbounded" },
1284
+ );
1285
+ const [{ version: writeVersion, written }, deleteVersions] = yield* Effect.all([write, remove], {
1286
+ concurrency: "unbounded",
1287
+ });
1288
+ const secrets: Record<string, BoundSecret> = Object.fromEntries([
1289
+ ...kept.map(({ envVar }): [string, BoundSecret] => [envVar, previous.secrets[envVar]!]),
1290
+ ...written,
1291
+ ]);
1292
+ const versions = [writeVersion, ...deleteVersions].filter(
1293
+ (version): version is number => version !== undefined && Number.isSafeInteger(version),
1294
+ );
1295
+ // A bound value wins over an App secret with the same env var name, as
1296
+ // a Machine env var did; that secret stays in the App untouched.
1297
+ const others = [...observed.keys()]
1298
+ .filter((name) => !SCOPED_SECRET.test(name) && !(name in desired))
1299
+ .sort();
1300
+ return {
1301
+ secrets,
1302
+ others,
1303
+ version: versions.length > 0 ? Math.max(...versions) : previous.version,
1304
+ };
1305
+ });
1306
+
1307
+ /**
1308
+ * Fly maps a process's `secrets` entries from a scoped App secret to the
1309
+ * env var the program reads, but only with `ignore_app_secrets`, which
1310
+ * hides every App secret the process does not list. So the App's other
1311
+ * secrets (attachments, `fly secrets set`) are listed under their own
1312
+ * names, as they were at deploy.
1313
+ */
1314
+ const secretMappings = (bound: {
1315
+ secrets: Record<string, BoundSecret>;
1316
+ others: string[];
1317
+ }): FlyMachineConfig["processes"] => {
1318
+ const mapped = Object.entries(bound.secrets).map(([envVar, secret]) => ({
1319
+ env_var: envVar,
1320
+ name: secret.name,
1321
+ }));
1322
+ if (mapped.length === 0) return undefined;
1323
+ const entries = [...mapped, ...bound.others.map((name) => ({ env_var: name, name }))].sort(
1324
+ (left, right) => left.env_var.localeCompare(right.env_var),
1325
+ );
1326
+ return [{ secrets: entries, ignore_app_secrets: true }];
1327
+ };
1328
+
1114
1329
  const desiredEnv = (
1115
1330
  props: ServiceProps,
1116
1331
  bindingEnv: Record<string, any>,
@@ -1150,6 +1365,7 @@ const buildConfig = (input: {
1150
1365
  mounts: FlyMachineMount[];
1151
1366
  metadata: Record<string, string>;
1152
1367
  statics?: FlyStatic[];
1368
+ processes?: FlyMachineConfig["processes"];
1153
1369
  }): FlyMachineConfig => ({
1154
1370
  image: input.image,
1155
1371
  guest: input.guest,
@@ -1158,6 +1374,7 @@ const buildConfig = (input: {
1158
1374
  mounts: input.mounts.length > 0 ? input.mounts : undefined,
1159
1375
  metadata: input.metadata,
1160
1376
  statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
1377
+ processes: input.processes,
1161
1378
  });
1162
1379
 
1163
1380
  const sameImage = (machine: FlyMachine, image: string) => {
@@ -1211,6 +1428,22 @@ const sameStatics = (observed: FlyStatic[] | undefined, desired: FlyStatic[] | u
1211
1428
  return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
1212
1429
  };
1213
1430
 
1431
+ /** Fly fills an omitted secret `name` with its `env_var`. */
1432
+ const sameProcesses = (
1433
+ observed: FlyMachineConfig["processes"],
1434
+ desired: FlyMachineConfig["processes"],
1435
+ ) => {
1436
+ const normalize = (processes: FlyMachineConfig["processes"]) =>
1437
+ (processes ?? []).map((process) => ({
1438
+ ...process,
1439
+ secrets: process.secrets?.map((secret) => ({
1440
+ env_var: secret.env_var,
1441
+ name: secret.name || secret.env_var,
1442
+ })),
1443
+ }));
1444
+ return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
1445
+ };
1446
+
1214
1447
  const configDrifted = (
1215
1448
  machine: FlyMachine,
1216
1449
  desired: {
@@ -1221,6 +1454,7 @@ const configDrifted = (
1221
1454
  mounts: FlyMachineMount[];
1222
1455
  metadata: Record<string, string>;
1223
1456
  statics?: FlyStatic[];
1457
+ processes?: FlyMachineConfig["processes"];
1224
1458
  },
1225
1459
  ) => {
1226
1460
  const config = machine.config;
@@ -1231,13 +1465,11 @@ const configDrifted = (
1231
1465
  !sameServices(config?.services, desired.services) ||
1232
1466
  !sameMounts(config?.mounts, desired.mounts) ||
1233
1467
  metadataChanged(config?.metadata, desired.metadata) ||
1234
- !sameStatics(config?.statics, desired.statics)
1468
+ !sameStatics(config?.statics, desired.statics) ||
1469
+ !sameProcesses(config?.processes, desired.processes)
1235
1470
  );
1236
1471
  };
1237
1472
 
1238
- const portsOf = (services: FlyMachineService[] | undefined) =>
1239
- (services ?? []).flatMap((service) => service.ports ?? []);
1240
-
1241
1473
  const withPort = (base: string, port: number, standard: number) =>
1242
1474
  port === standard ? base : `${base}:${port}`;
1243
1475
 
@@ -1317,7 +1549,9 @@ interface Access {
1317
1549
  isPublic: boolean;
1318
1550
  network: string | undefined;
1319
1551
  bindingPort: number;
1320
- rpcToken: Redacted.Redacted<string> | undefined;
1552
+ secrets?: Record<string, BoundSecret>;
1553
+ secretsVersion?: number;
1554
+ orgSlug: string | undefined;
1321
1555
  }
1322
1556
 
1323
1557
  /** Drop the port Alchemy added for bindings from a services list. */
@@ -1500,7 +1734,9 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
1500
1734
  appName: set.appName,
1501
1735
  ownsApp: access.ownsApp,
1502
1736
  network: normalizeNetwork(access.network),
1503
- rpcToken: access.rpcToken,
1737
+ secrets: access.secrets,
1738
+ secretsVersion: access.secretsVersion,
1739
+ orgSlug: access.orgSlug,
1504
1740
  ...endpointsOf(set, access),
1505
1741
  rolloutPending: set.rolloutPending,
1506
1742
  machineId: set.machineId,
@@ -1517,6 +1753,26 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
1517
1753
  code: { hash: codeHash },
1518
1754
  });
1519
1755
 
1756
+ /** The replica set of a Service whose App exists but runs no Machines yet. */
1757
+ const emptyReplicaSet = (appName: string, name: string): ReplicaSet => ({
1758
+ appName,
1759
+ machineId: "",
1760
+ machineIds: [],
1761
+ name,
1762
+ baseName: name,
1763
+ region: "",
1764
+ regions: [],
1765
+ state: "created",
1766
+ instanceId: undefined,
1767
+ privateIp: undefined,
1768
+ imageRef: undefined,
1769
+ guest: undefined,
1770
+ services: undefined,
1771
+ count: 0,
1772
+ mounts: [],
1773
+ replicas: [],
1774
+ });
1775
+
1520
1776
  const machineIdsOf = (output: Service["Attributes"] | undefined) =>
1521
1777
  output?.machineIds ??
1522
1778
  (output?.machineId !== undefined && output.machineId.length > 0 ? [output.machineId] : []);
@@ -1587,7 +1843,7 @@ export const ServiceProvider = () =>
1587
1843
  stables: ["appName"],
1588
1844
  nuke: { dependsOn: ["Fly.App"] },
1589
1845
 
1590
- diff: Effect.fn(function* ({ id, news, output }) {
1846
+ diff: Effect.fn(function* ({ news, output }) {
1591
1847
  if (news === undefined) return;
1592
1848
  const shape = news as Partial<Pick<ServiceProps, "app" | "public" | "network">>;
1593
1849
  const ownsApp = shape.app === undefined;
@@ -1674,8 +1930,15 @@ export const ServiceProvider = () =>
1674
1930
  // effect-config form has been evaluated, so the object view is
1675
1931
  // safe to read.
1676
1932
  const statics = news as Partial<
1677
- Pick<ServiceProps, "main" | "build" | "image" | "port" | "extraFiles" | "isExternal">
1933
+ Pick<
1934
+ ServiceProps,
1935
+ "main" | "context" | "build" | "image" | "port" | "extraFiles" | "isExternal"
1936
+ >
1678
1937
  >;
1938
+ if (typeof statics.context === "string") {
1939
+ const hash = yield* hosted.contextHash(statics.context);
1940
+ if (hash !== output.code.hash) return { action: "update" as const };
1941
+ }
1679
1942
  if (
1680
1943
  isResolved({
1681
1944
  main: statics.main,
@@ -1695,6 +1958,24 @@ export const ServiceProvider = () =>
1695
1958
  return output.rolloutPending ? { action: "update" as const } : undefined;
1696
1959
  }),
1697
1960
 
1961
+ lookup: Effect.fn(function* ({ appName, name }) {
1962
+ const found = yield* lookupReplicaSet({ appName, name });
1963
+ if (found === undefined) return undefined;
1964
+ const app = yield* machines
1965
+ .getApp({ app_name: appName })
1966
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
1967
+ const attrs = toAttrs(found, "", {
1968
+ // Reported as Alchemy-owned only so the endpoints follow the App's
1969
+ // observed addresses, as a declared Service's own App's do.
1970
+ ownsApp: true,
1971
+ isPublic: yield* hasPublicAddress(appName),
1972
+ network: app?.network,
1973
+ bindingPort: DEFAULT_BINDING_PORT,
1974
+ orgSlug: app?.organization?.slug,
1975
+ });
1976
+ return { ...attrs, ownsApp: false };
1977
+ }),
1978
+
1698
1979
  read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
1699
1980
  const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
1700
1981
  const appName = ownsApp
@@ -1717,13 +1998,34 @@ export const ServiceProvider = () =>
1717
1998
  machineIds: machineIdsOf(output),
1718
1999
  baseName: name,
1719
2000
  });
1720
- if (found === undefined) return undefined;
2001
+ if (found === undefined) {
2002
+ // A create interrupted between creating the Service's own App and
2003
+ // starting its Machines (an image build in between can take
2004
+ // minutes) leaves the App with nothing in it. Report it, so the
2005
+ // engine's recovery of the interrupted create deletes it instead
2006
+ // of leaking it. The App's name is derived from this resource's
2007
+ // instance, so an App with that name is this resource's.
2008
+ if (!ownsApp || appName === undefined) return undefined;
2009
+ const app = yield* machines
2010
+ .getApp({ app_name: appName })
2011
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
2012
+ if (app === undefined || !matchesAlchemyPhysicalName(app.name)) return undefined;
2013
+ return toAttrs(emptyReplicaSet(appName, name), output?.code.hash ?? "", {
2014
+ ownsApp,
2015
+ isPublic: false,
2016
+ network: output?.network ?? olds?.network,
2017
+ bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
2018
+ orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
2019
+ });
2020
+ }
1721
2021
  return toAttrs(found, output?.code.hash ?? "", {
1722
2022
  ownsApp,
1723
2023
  isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
1724
2024
  network: output?.network ?? (ownsApp ? olds?.network : undefined),
1725
2025
  bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1726
- rpcToken: output?.rpcToken,
2026
+ secrets: output?.secrets,
2027
+ secretsVersion: output?.secretsVersion,
2028
+ orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
1727
2029
  });
1728
2030
  }),
1729
2031
 
@@ -1736,7 +2038,7 @@ export const ServiceProvider = () =>
1736
2038
  isPublic: true,
1737
2039
  network: undefined,
1738
2040
  bindingPort: DEFAULT_BINDING_PORT,
1739
- rpcToken: undefined,
2041
+ orgSlug: undefined,
1740
2042
  }),
1741
2043
  );
1742
2044
  }),
@@ -1774,9 +2076,35 @@ export const ServiceProvider = () =>
1774
2076
  const version = yield* attachPostgresSecrets(appName, pg.clusterId, pg.variableName);
1775
2077
  if (version !== undefined) secretVersions.push(version);
1776
2078
  }
2079
+ const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
2080
+ // Redacted values (every bound `Config`) become App secrets, but
2081
+ // only in an App the Service owns: Fly gives every App secret to
2082
+ // every Machine in the App. A plain `env` entry wins over a binding.
2083
+ const redacted = Object.keys(env).filter((key) =>
2084
+ props.env?.[key] !== undefined
2085
+ ? Redacted.isRedacted(props.env[key])
2086
+ : bound.secretNames.includes(key),
2087
+ );
2088
+ // Move them out of the plain env record before anything else reads it.
2089
+ const desiredSecrets: Record<string, Redacted.Redacted<string>> = {};
2090
+ if (ownsApp) {
2091
+ for (const key of redacted) {
2092
+ desiredSecrets[key] = Redacted.make(env[key]!);
2093
+ delete env[key];
2094
+ }
2095
+ }
2096
+ const boundSecrets = yield* syncBoundSecrets(
2097
+ appName,
2098
+ fqn,
2099
+ desiredSecrets,
2100
+ output?.appName === appName
2101
+ ? { secrets: output.secrets ?? {}, version: output.secretsVersion }
2102
+ : { secrets: {}, version: undefined },
2103
+ );
2104
+ if (boundSecrets.version !== undefined) secretVersions.push(boundSecrets.version);
1777
2105
  const minSecretsVersion =
1778
2106
  secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
1779
- const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
2107
+ const processes = secretMappings(boundSecrets);
1780
2108
  if (policy.shutdown && !props.isExternal) {
1781
2109
  env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
1782
2110
  }
@@ -1790,24 +2118,35 @@ export const ServiceProvider = () =>
1790
2118
  );
1791
2119
  const services = published.services;
1792
2120
  if (!ownsApp) yield* validateObservedPorts(appName, fqn, id, services);
1793
- if (props.rpcToken !== undefined) env[RPC_TOKEN_ENV] = Redacted.value(props.rpcToken);
2121
+ // The Apps of the Services that bind this one; only they may call
2122
+ // its methods (see `serveFlyRpc`).
2123
+ if (bound.callers.length > 0) env[RPC_CALLERS_ENV] = bound.callers.join(",");
1794
2124
  for (const target of bound.targets) {
1795
2125
  const keys = boundTargetEnvKeys(target.service);
1796
2126
  env[keys.appName] = packEnvValue(target.appName);
1797
2127
  if (target.privateUrl !== undefined) env[keys.url] = packEnvValue(target.privateUrl);
1798
- if (target.rpcToken !== undefined) env[keys.token] = packEnvValue(target.rpcToken);
1799
2128
  }
1800
2129
  env[RPC_ORG_ENV] = yield* resolveOrgSlug();
1801
2130
  if (published.added) env[BINDING_PORT_ENV] = String(bindingPort);
1802
2131
  const statics = toFlyStatics(props.statics);
1803
2132
 
1804
- const { imageRef, codeHash } = yield* hosted.resolveImage({
1805
- id,
1806
- appName,
1807
- props,
1808
- previousHash: output?.code.hash,
1809
- session,
1810
- });
2133
+ const { imageRef, codeHash } =
2134
+ props.context !== undefined
2135
+ ? yield* hosted.resolveContextImage({
2136
+ id,
2137
+ appName,
2138
+ context: props.context,
2139
+ remote: props.build?.remote,
2140
+ previousHash: output?.code.hash,
2141
+ session,
2142
+ })
2143
+ : yield* hosted.resolveImage({
2144
+ id,
2145
+ appName,
2146
+ props: props as HostedProgramProps,
2147
+ previousHash: output?.code.hash,
2148
+ session,
2149
+ });
1811
2150
 
1812
2151
  const set = yield* reconcileReplicas({
1813
2152
  id,
@@ -1836,12 +2175,24 @@ export const ServiceProvider = () =>
1836
2175
  mounts: desired.mounts,
1837
2176
  metadata: desired.metadata,
1838
2177
  statics,
2178
+ processes,
1839
2179
  }),
1840
2180
  buildConfig: ({ mounts, metadata }) =>
1841
- buildConfig({ image: imageRef, guest, env, services, mounts, metadata, statics }),
2181
+ buildConfig({
2182
+ image: imageRef,
2183
+ guest,
2184
+ env,
2185
+ services,
2186
+ mounts,
2187
+ metadata,
2188
+ statics,
2189
+ processes,
2190
+ }),
1842
2191
  }).pipe(
1843
2192
  Effect.catchTag("Fly.ReplicaNotCreated", (error) =>
1844
- Effect.fail(new ServiceNotCreated({ name: error.name, appName: error.appName })),
2193
+ Effect.fail(
2194
+ new ServiceNotCreated({ resourceName: error.resourceName, appName: error.appName }),
2195
+ ),
1845
2196
  ),
1846
2197
  );
1847
2198
  return toAttrs(set, codeHash, {
@@ -1849,13 +2200,15 @@ export const ServiceProvider = () =>
1849
2200
  isPublic,
1850
2201
  network,
1851
2202
  bindingPort,
1852
- rpcToken: props.rpcToken,
2203
+ secrets: boundSecrets.secrets,
2204
+ secretsVersion: boundSecrets.version,
2205
+ orgSlug: yield* resolveOrgSlug(),
1853
2206
  });
1854
2207
  }),
1855
2208
 
1856
- // Create the App, its addresses, and the caller token before any
1857
- // Machine, so two Services that bind each other can both resolve
1858
- // the other's private address and token.
2209
+ // Create the App and its addresses before any Machine, so two
2210
+ // Services that bind each other can both resolve the other's App
2211
+ // and private address.
1859
2212
  precreate: Effect.fn(function* ({ id, news }) {
1860
2213
  const props = news;
1861
2214
  // Precreate runs before dependencies resolve. A Service placed in
@@ -1909,7 +2262,7 @@ export const ServiceProvider = () =>
1909
2262
  isPublic,
1910
2263
  network,
1911
2264
  bindingPort,
1912
- rpcToken: isResolved(props.rpcToken) ? props.rpcToken : undefined,
2265
+ orgSlug: yield* resolveOrgSlug(),
1913
2266
  });
1914
2267
  }),
1915
2268