@alchemy.run/fly 2.0.0-beta.85 → 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 (271) hide show
  1. package/lib/App.d.ts +26 -4
  2. package/lib/App.d.ts.map +1 -1
  3. package/lib/App.js +25 -2
  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 +20 -27
  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 +36 -2
  16. package/lib/Bucket.d.ts.map +1 -1
  17. package/lib/Bucket.js +181 -28
  18. package/lib/Bucket.js.map +1 -1
  19. package/lib/BucketBinding.d.ts.map +1 -1
  20. package/lib/BucketBinding.js.map +1 -1
  21. package/lib/Catalog.d.ts.map +1 -1
  22. package/lib/Catalog.js.map +1 -1
  23. package/lib/Certificate.d.ts +25 -1
  24. package/lib/Certificate.d.ts.map +1 -1
  25. package/lib/Certificate.js +18 -0
  26. package/lib/Certificate.js.map +1 -1
  27. package/lib/Checkpoint.d.ts.map +1 -1
  28. package/lib/Checkpoint.js.map +1 -1
  29. package/lib/CheckpointHttp.d.ts.map +1 -1
  30. package/lib/CheckpointHttp.js.map +1 -1
  31. package/lib/ConnectPostgres.d.ts.map +1 -1
  32. package/lib/ConnectPostgres.js.map +1 -1
  33. package/lib/ConnectPostgresHttp.d.ts.map +1 -1
  34. package/lib/ConnectPostgresHttp.js.map +1 -1
  35. package/lib/Contributions.d.ts +1 -1
  36. package/lib/Contributions.d.ts.map +1 -1
  37. package/lib/Contributions.js.map +1 -1
  38. package/lib/Credentials.d.ts +4 -3
  39. package/lib/Credentials.d.ts.map +1 -1
  40. package/lib/Credentials.js +9 -5
  41. package/lib/Credentials.js.map +1 -1
  42. package/lib/Decrypt.d.ts.map +1 -1
  43. package/lib/Decrypt.js.map +1 -1
  44. package/lib/DecryptHttp.d.ts.map +1 -1
  45. package/lib/DecryptHttp.js.map +1 -1
  46. package/lib/DeleteObject.d.ts.map +1 -1
  47. package/lib/DeleteObject.js.map +1 -1
  48. package/lib/DeleteObjectHttp.d.ts.map +1 -1
  49. package/lib/DeleteObjectHttp.js.map +1 -1
  50. package/lib/Deployment.d.ts.map +1 -1
  51. package/lib/Deployment.js.map +1 -1
  52. package/lib/DeploymentImages.d.ts.map +1 -1
  53. package/lib/DeploymentImages.js.map +1 -1
  54. package/lib/DeploymentState.d.ts.map +1 -1
  55. package/lib/DeploymentState.js.map +1 -1
  56. package/lib/Encrypt.d.ts.map +1 -1
  57. package/lib/Encrypt.js.map +1 -1
  58. package/lib/EncryptHttp.d.ts.map +1 -1
  59. package/lib/EncryptHttp.js.map +1 -1
  60. package/lib/Environment.d.ts.map +1 -1
  61. package/lib/Environment.js.map +1 -1
  62. package/lib/Errors.d.ts.map +1 -1
  63. package/lib/Errors.js.map +1 -1
  64. package/lib/Exec.d.ts.map +1 -1
  65. package/lib/Exec.js.map +1 -1
  66. package/lib/ExecHttp.d.ts.map +1 -1
  67. package/lib/ExecHttp.js.map +1 -1
  68. package/lib/GetObject.d.ts.map +1 -1
  69. package/lib/GetObject.js.map +1 -1
  70. package/lib/GetObjectHttp.d.ts.map +1 -1
  71. package/lib/GetObjectHttp.js.map +1 -1
  72. package/lib/GetSecret.d.ts.map +1 -1
  73. package/lib/GetSecret.js.map +1 -1
  74. package/lib/GetSecretHttp.d.ts.map +1 -1
  75. package/lib/GetSecretHttp.js.map +1 -1
  76. package/lib/HeadObject.d.ts.map +1 -1
  77. package/lib/HeadObject.js.map +1 -1
  78. package/lib/HeadObjectHttp.d.ts.map +1 -1
  79. package/lib/HeadObjectHttp.js.map +1 -1
  80. package/lib/IpAssignment.d.ts +24 -1
  81. package/lib/IpAssignment.d.ts.map +1 -1
  82. package/lib/IpAssignment.js +17 -0
  83. package/lib/IpAssignment.js.map +1 -1
  84. package/lib/ListObjectsV2.d.ts.map +1 -1
  85. package/lib/ListObjectsV2.js.map +1 -1
  86. package/lib/ListObjectsV2Http.d.ts.map +1 -1
  87. package/lib/ListObjectsV2Http.js.map +1 -1
  88. package/lib/ListSecrets.d.ts.map +1 -1
  89. package/lib/ListSecrets.js.map +1 -1
  90. package/lib/ListSecretsHttp.d.ts.map +1 -1
  91. package/lib/ListSecretsHttp.js.map +1 -1
  92. package/lib/Machine.d.ts +26 -1
  93. package/lib/Machine.d.ts.map +1 -1
  94. package/lib/Machine.js +16 -1
  95. package/lib/Machine.js.map +1 -1
  96. package/lib/MachineContainers.d.ts.map +1 -1
  97. package/lib/MachineContainers.js.map +1 -1
  98. package/lib/Metadata.d.ts.map +1 -1
  99. package/lib/Metadata.js.map +1 -1
  100. package/lib/MountVolume.d.ts +7 -2
  101. package/lib/MountVolume.d.ts.map +1 -1
  102. package/lib/MountVolume.js.map +1 -1
  103. package/lib/Postgres.d.ts +33 -1
  104. package/lib/Postgres.d.ts.map +1 -1
  105. package/lib/Postgres.js +20 -0
  106. package/lib/Postgres.js.map +1 -1
  107. package/lib/PostgresMigrations.d.ts.map +1 -1
  108. package/lib/PostgresMigrations.js.map +1 -1
  109. package/lib/Providers.d.ts +1 -1
  110. package/lib/Providers.d.ts.map +1 -1
  111. package/lib/Providers.js.map +1 -1
  112. package/lib/PutObject.d.ts.map +1 -1
  113. package/lib/PutObject.js.map +1 -1
  114. package/lib/PutObjectHttp.d.ts.map +1 -1
  115. package/lib/PutObjectHttp.js.map +1 -1
  116. package/lib/ReadRedis.d.ts.map +1 -1
  117. package/lib/ReadRedis.js.map +1 -1
  118. package/lib/ReadRedisHttp.d.ts.map +1 -1
  119. package/lib/ReadRedisHttp.js.map +1 -1
  120. package/lib/ReadWriteRedis.d.ts.map +1 -1
  121. package/lib/ReadWriteRedis.js.map +1 -1
  122. package/lib/ReadWriteRedisHttp.d.ts.map +1 -1
  123. package/lib/ReadWriteRedisHttp.js.map +1 -1
  124. package/lib/Redis.d.ts +21 -1
  125. package/lib/Redis.d.ts.map +1 -1
  126. package/lib/Redis.js +28 -0
  127. package/lib/Redis.js.map +1 -1
  128. package/lib/RedisBinding.d.ts.map +1 -1
  129. package/lib/RedisBinding.js.map +1 -1
  130. package/lib/RedisHttp.d.ts.map +1 -1
  131. package/lib/RedisHttp.js.map +1 -1
  132. package/lib/Region.d.ts.map +1 -1
  133. package/lib/Region.js.map +1 -1
  134. package/lib/Secret.d.ts +22 -1
  135. package/lib/Secret.d.ts.map +1 -1
  136. package/lib/Secret.js +15 -0
  137. package/lib/Secret.js.map +1 -1
  138. package/lib/SecretHttp.d.ts.map +1 -1
  139. package/lib/SecretHttp.js.map +1 -1
  140. package/lib/SecretKey.d.ts +22 -1
  141. package/lib/SecretKey.d.ts.map +1 -1
  142. package/lib/SecretKey.js +15 -0
  143. package/lib/SecretKey.js.map +1 -1
  144. package/lib/SecretKeyHttp.d.ts.map +1 -1
  145. package/lib/SecretKeyHttp.js.map +1 -1
  146. package/lib/Service.d.ts +96 -21
  147. package/lib/Service.d.ts.map +1 -1
  148. package/lib/Service.js +251 -37
  149. package/lib/Service.js.map +1 -1
  150. package/lib/Sign.d.ts.map +1 -1
  151. package/lib/Sign.js.map +1 -1
  152. package/lib/SignHttp.d.ts.map +1 -1
  153. package/lib/SignHttp.js.map +1 -1
  154. package/lib/Sprite.d.ts +22 -3
  155. package/lib/Sprite.d.ts.map +1 -1
  156. package/lib/Sprite.js +15 -0
  157. package/lib/Sprite.js.map +1 -1
  158. package/lib/SpriteHttp.d.ts.map +1 -1
  159. package/lib/SpriteHttp.js.map +1 -1
  160. package/lib/Verify.d.ts.map +1 -1
  161. package/lib/Verify.js.map +1 -1
  162. package/lib/VerifyHttp.d.ts.map +1 -1
  163. package/lib/VerifyHttp.js.map +1 -1
  164. package/lib/Volume.d.ts +3 -3
  165. package/lib/Volume.d.ts.map +1 -1
  166. package/lib/Volume.js +17 -1
  167. package/lib/Volume.js.map +1 -1
  168. package/lib/VolumeSnapshot.d.ts +32 -1
  169. package/lib/VolumeSnapshot.d.ts.map +1 -1
  170. package/lib/VolumeSnapshot.js +23 -0
  171. package/lib/VolumeSnapshot.js.map +1 -1
  172. package/lib/Website/AssetDeployment.d.ts.map +1 -1
  173. package/lib/Website/AssetDeployment.js.map +1 -1
  174. package/lib/Website/Astro.d.ts.map +1 -1
  175. package/lib/Website/Astro.js.map +1 -1
  176. package/lib/Website/Foldkit.d.ts.map +1 -1
  177. package/lib/Website/Foldkit.js.map +1 -1
  178. package/lib/Website/FrameworkSite.d.ts.map +1 -1
  179. package/lib/Website/FrameworkSite.js.map +1 -1
  180. package/lib/Website/Nextjs.d.ts.map +1 -1
  181. package/lib/Website/Nextjs.js.map +1 -1
  182. package/lib/Website/Nuxt.d.ts.map +1 -1
  183. package/lib/Website/Nuxt.js.map +1 -1
  184. package/lib/Website/Octane.d.ts.map +1 -1
  185. package/lib/Website/Octane.js.map +1 -1
  186. package/lib/Website/ReactRouter.d.ts.map +1 -1
  187. package/lib/Website/ReactRouter.js.map +1 -1
  188. package/lib/Website/SolidStart.d.ts.map +1 -1
  189. package/lib/Website/SolidStart.js.map +1 -1
  190. package/lib/Website/StaticSite.d.ts.map +1 -1
  191. package/lib/Website/StaticSite.js.map +1 -1
  192. package/lib/Website/SvelteKit.d.ts.map +1 -1
  193. package/lib/Website/SvelteKit.js.map +1 -1
  194. package/lib/Website/TanStackStart.d.ts.map +1 -1
  195. package/lib/Website/TanStackStart.js.map +1 -1
  196. package/lib/Website/Vinext.d.ts.map +1 -1
  197. package/lib/Website/Vinext.js.map +1 -1
  198. package/lib/Website/Vite.d.ts.map +1 -1
  199. package/lib/Website/Vite.js.map +1 -1
  200. package/lib/Website/Vocs.d.ts.map +1 -1
  201. package/lib/Website/Vocs.js.map +1 -1
  202. package/lib/Website/Waku.d.ts.map +1 -1
  203. package/lib/Website/Waku.js.map +1 -1
  204. package/lib/Website/index.d.ts.map +1 -1
  205. package/lib/Website/index.js.map +1 -1
  206. package/lib/WriteCertificates.d.ts.map +1 -1
  207. package/lib/WriteCertificates.js.map +1 -1
  208. package/lib/WriteCertificatesHttp.d.ts.map +1 -1
  209. package/lib/WriteCertificatesHttp.js.map +1 -1
  210. package/lib/WriteRedis.d.ts.map +1 -1
  211. package/lib/WriteRedis.js.map +1 -1
  212. package/lib/WriteRedisHttp.d.ts.map +1 -1
  213. package/lib/WriteRedisHttp.js.map +1 -1
  214. package/lib/WriteSecret.d.ts.map +1 -1
  215. package/lib/WriteSecret.js.map +1 -1
  216. package/lib/WriteSecretHttp.d.ts.map +1 -1
  217. package/lib/WriteSecretHttp.js.map +1 -1
  218. package/lib/bluegreen.d.ts +5 -4
  219. package/lib/bluegreen.d.ts.map +1 -1
  220. package/lib/bluegreen.js +1 -1
  221. package/lib/bluegreen.js.map +1 -1
  222. package/lib/hosted.d.ts +16 -0
  223. package/lib/hosted.d.ts.map +1 -1
  224. package/lib/hosted.js +101 -11
  225. package/lib/hosted.js.map +1 -1
  226. package/lib/index.d.ts.map +1 -1
  227. package/lib/index.js.map +1 -1
  228. package/lib/internal/Oidc.d.ts +15 -0
  229. package/lib/internal/Oidc.d.ts.map +1 -0
  230. package/lib/internal/Oidc.js +73 -0
  231. package/lib/internal/Oidc.js.map +1 -0
  232. package/lib/internal/RemoteBuilder.d.ts.map +1 -1
  233. package/lib/internal/RemoteBuilder.js.map +1 -1
  234. package/lib/leases.d.ts +6 -2
  235. package/lib/leases.d.ts.map +1 -1
  236. package/lib/leases.js +25 -5
  237. package/lib/leases.js.map +1 -1
  238. package/lib/ports.d.ts.map +1 -1
  239. package/lib/ports.js.map +1 -1
  240. package/lib/replicas.d.ts +38 -26
  241. package/lib/replicas.d.ts.map +1 -1
  242. package/lib/replicas.js +57 -2
  243. package/lib/replicas.js.map +1 -1
  244. package/lib/rpc.d.ts +14 -12
  245. package/lib/rpc.d.ts.map +1 -1
  246. package/lib/rpc.js +38 -40
  247. package/lib/rpc.js.map +1 -1
  248. package/package.json +15 -15
  249. package/src/App.ts +36 -3
  250. package/src/BindService.ts +27 -34
  251. package/src/Bootstrap.ts +2 -0
  252. package/src/Bucket.ts +264 -33
  253. package/src/Certificate.ts +32 -1
  254. package/src/Credentials.ts +9 -5
  255. package/src/IpAssignment.ts +31 -1
  256. package/src/Machine.ts +34 -1
  257. package/src/MountVolume.ts +5 -2
  258. package/src/Postgres.ts +43 -1
  259. package/src/Redis.ts +43 -1
  260. package/src/Secret.ts +29 -1
  261. package/src/SecretKey.ts +29 -1
  262. package/src/Service.ts +389 -70
  263. package/src/Sprite.ts +34 -8
  264. package/src/Volume.ts +25 -1
  265. package/src/VolumeSnapshot.ts +39 -1
  266. package/src/bluegreen.ts +4 -1
  267. package/src/hosted.ts +118 -13
  268. package/src/internal/Oidc.ts +99 -0
  269. package/src/leases.ts +35 -7
  270. package/src/replicas.ts +63 -2
  271. 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";
@@ -65,6 +65,7 @@ import {
65
65
  deleteReplicaSet,
66
66
  hasPublishedService,
67
67
  listReplicaSets,
68
+ lookupReplicaSet,
68
69
  observeReplicaSet,
69
70
  reconcileReplicas,
70
71
  resolveCount,
@@ -74,7 +75,7 @@ import {
74
75
  type Replica,
75
76
  type ReplicaSet,
76
77
  } from "./replicas.ts";
77
- 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";
78
79
 
79
80
  /**
80
81
  * A resource-valued prop: the resource itself, or an Effect that produces
@@ -140,18 +141,21 @@ export interface ServiceProps extends PlatformProps {
140
141
  * @default 7780
141
142
  */
142
143
  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
144
  /**
150
145
  * Module entrypoint bundled with rolldown and baked into a Docker
151
146
  * image pushed to `registry.fly.io`. Typically `import.meta.url`.
152
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}.
153
157
  */
154
- main: string;
158
+ context?: string;
155
159
  /**
156
160
  * Region or regions to run in (`"iad"`, `["iad", "lhr"]`). The
157
161
  * Service runs `count` Machines in each region behind one hostname;
@@ -272,6 +276,19 @@ export interface ServiceEndpoint {
272
276
  url: string | undefined;
273
277
  }
274
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
+
275
292
  export type Service = Resource<
276
293
  "Fly.Service",
277
294
  ServiceProps,
@@ -287,8 +304,19 @@ export type Service = Resource<
287
304
  * shared App's network. `undefined` for the organization's default.
288
305
  */
289
306
  network?: string;
290
- /** Token bound callers send with RPC calls. */
291
- 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;
292
320
  /** Fly Machine id of replica 0. */
293
321
  machineId: string;
294
322
  /** Fly Machine ids of every replica. */
@@ -339,9 +367,24 @@ export type Service = Resource<
339
367
  code: { hash: string };
340
368
  },
341
369
  ServiceBinding,
342
- Providers
370
+ Providers,
371
+ ServiceLookup
343
372
  >;
344
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
+
345
388
  export const isService = (value: unknown): value is Service =>
346
389
  typeof value === "object" &&
347
390
  value !== null &&
@@ -539,9 +582,9 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
539
582
  * ### Call another Service
540
583
  * A Service can return methods next to `fetch`. Another Service calls
541
584
  * 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.
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.
545
588
  *
546
589
  * **Example:** A private Service with methods
547
590
  * ```typescript
@@ -575,9 +618,10 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
575
618
  * ```
576
619
  *
577
620
  * 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
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
581
625
  * Service publishes an extra plain-HTTP port for bound callers,
582
626
  * `bindingPort` (default 7780), since its own ports are an HTTPS
583
627
  * redirect and HTTPS. `client.fetch` sends a request to the Service's
@@ -728,8 +772,18 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
728
772
  *
729
773
  * ### Config
730
774
  * 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`.
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.
733
787
  *
734
788
  * `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
735
789
  * `Redacted.value` only where you need the raw string.
@@ -867,6 +921,20 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
867
921
  * ) {}
868
922
  * ```
869
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
+ *
870
938
  * ### Custom proxy services
871
939
  * `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
872
940
  * toward `port`, or plain HTTP 80 for a private Service. Pass a custom
@@ -1029,35 +1097,45 @@ export type ServiceRuntimeContext = FlyHostRuntimeContext;
1029
1097
  * See the [deployment guide](/fly/compute/deployments) for recovery,
1030
1098
  * idle capacity, application responsibilities, and verification limits.
1031
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
+ *
1032
1112
  * @resource
1033
1113
  * @product Service
1034
1114
  */
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
- });
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
+ });
1061
1139
 
1062
1140
  export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated")<{
1063
1141
  resourceName: string;
@@ -1107,6 +1185,147 @@ const toFlyGuest = (guest: MachineGuest | undefined): FlyMachineGuest => {
1107
1185
  return fly;
1108
1186
  };
1109
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
+
1110
1329
  const desiredEnv = (
1111
1330
  props: ServiceProps,
1112
1331
  bindingEnv: Record<string, any>,
@@ -1146,6 +1365,7 @@ const buildConfig = (input: {
1146
1365
  mounts: FlyMachineMount[];
1147
1366
  metadata: Record<string, string>;
1148
1367
  statics?: FlyStatic[];
1368
+ processes?: FlyMachineConfig["processes"];
1149
1369
  }): FlyMachineConfig => ({
1150
1370
  image: input.image,
1151
1371
  guest: input.guest,
@@ -1154,6 +1374,7 @@ const buildConfig = (input: {
1154
1374
  mounts: input.mounts.length > 0 ? input.mounts : undefined,
1155
1375
  metadata: input.metadata,
1156
1376
  statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
1377
+ processes: input.processes,
1157
1378
  });
1158
1379
 
1159
1380
  const sameImage = (machine: FlyMachine, image: string) => {
@@ -1207,6 +1428,22 @@ const sameStatics = (observed: FlyStatic[] | undefined, desired: FlyStatic[] | u
1207
1428
  return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
1208
1429
  };
1209
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
+
1210
1447
  const configDrifted = (
1211
1448
  machine: FlyMachine,
1212
1449
  desired: {
@@ -1217,6 +1454,7 @@ const configDrifted = (
1217
1454
  mounts: FlyMachineMount[];
1218
1455
  metadata: Record<string, string>;
1219
1456
  statics?: FlyStatic[];
1457
+ processes?: FlyMachineConfig["processes"];
1220
1458
  },
1221
1459
  ) => {
1222
1460
  const config = machine.config;
@@ -1227,7 +1465,8 @@ const configDrifted = (
1227
1465
  !sameServices(config?.services, desired.services) ||
1228
1466
  !sameMounts(config?.mounts, desired.mounts) ||
1229
1467
  metadataChanged(config?.metadata, desired.metadata) ||
1230
- !sameStatics(config?.statics, desired.statics)
1468
+ !sameStatics(config?.statics, desired.statics) ||
1469
+ !sameProcesses(config?.processes, desired.processes)
1231
1470
  );
1232
1471
  };
1233
1472
 
@@ -1310,7 +1549,9 @@ interface Access {
1310
1549
  isPublic: boolean;
1311
1550
  network: string | undefined;
1312
1551
  bindingPort: number;
1313
- rpcToken: Redacted.Redacted<string> | undefined;
1552
+ secrets?: Record<string, BoundSecret>;
1553
+ secretsVersion?: number;
1554
+ orgSlug: string | undefined;
1314
1555
  }
1315
1556
 
1316
1557
  /** Drop the port Alchemy added for bindings from a services list. */
@@ -1493,7 +1734,9 @@ const toAttrs = (set: ReplicaSet, codeHash: string, access: Access): Service["At
1493
1734
  appName: set.appName,
1494
1735
  ownsApp: access.ownsApp,
1495
1736
  network: normalizeNetwork(access.network),
1496
- rpcToken: access.rpcToken,
1737
+ secrets: access.secrets,
1738
+ secretsVersion: access.secretsVersion,
1739
+ orgSlug: access.orgSlug,
1497
1740
  ...endpointsOf(set, access),
1498
1741
  rolloutPending: set.rolloutPending,
1499
1742
  machineId: set.machineId,
@@ -1687,8 +1930,15 @@ export const ServiceProvider = () =>
1687
1930
  // effect-config form has been evaluated, so the object view is
1688
1931
  // safe to read.
1689
1932
  const statics = news as Partial<
1690
- Pick<ServiceProps, "main" | "build" | "image" | "port" | "extraFiles" | "isExternal">
1933
+ Pick<
1934
+ ServiceProps,
1935
+ "main" | "context" | "build" | "image" | "port" | "extraFiles" | "isExternal"
1936
+ >
1691
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
+ }
1692
1942
  if (
1693
1943
  isResolved({
1694
1944
  main: statics.main,
@@ -1708,6 +1958,24 @@ export const ServiceProvider = () =>
1708
1958
  return output.rolloutPending ? { action: "update" as const } : undefined;
1709
1959
  }),
1710
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
+
1711
1979
  read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
1712
1980
  const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
1713
1981
  const appName = ownsApp
@@ -1747,7 +2015,7 @@ export const ServiceProvider = () =>
1747
2015
  isPublic: false,
1748
2016
  network: output?.network ?? olds?.network,
1749
2017
  bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1750
- rpcToken: output?.rpcToken,
2018
+ orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
1751
2019
  });
1752
2020
  }
1753
2021
  return toAttrs(found, output?.code.hash ?? "", {
@@ -1755,7 +2023,9 @@ export const ServiceProvider = () =>
1755
2023
  isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
1756
2024
  network: output?.network ?? (ownsApp ? olds?.network : undefined),
1757
2025
  bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1758
- rpcToken: output?.rpcToken,
2026
+ secrets: output?.secrets,
2027
+ secretsVersion: output?.secretsVersion,
2028
+ orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
1759
2029
  });
1760
2030
  }),
1761
2031
 
@@ -1768,7 +2038,7 @@ export const ServiceProvider = () =>
1768
2038
  isPublic: true,
1769
2039
  network: undefined,
1770
2040
  bindingPort: DEFAULT_BINDING_PORT,
1771
- rpcToken: undefined,
2041
+ orgSlug: undefined,
1772
2042
  }),
1773
2043
  );
1774
2044
  }),
@@ -1806,9 +2076,35 @@ export const ServiceProvider = () =>
1806
2076
  const version = yield* attachPostgresSecrets(appName, pg.clusterId, pg.variableName);
1807
2077
  if (version !== undefined) secretVersions.push(version);
1808
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);
1809
2105
  const minSecretsVersion =
1810
2106
  secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
1811
- const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
2107
+ const processes = secretMappings(boundSecrets);
1812
2108
  if (policy.shutdown && !props.isExternal) {
1813
2109
  env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
1814
2110
  }
@@ -1822,24 +2118,35 @@ export const ServiceProvider = () =>
1822
2118
  );
1823
2119
  const services = published.services;
1824
2120
  if (!ownsApp) yield* validateObservedPorts(appName, fqn, id, services);
1825
- 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(",");
1826
2124
  for (const target of bound.targets) {
1827
2125
  const keys = boundTargetEnvKeys(target.service);
1828
2126
  env[keys.appName] = packEnvValue(target.appName);
1829
2127
  if (target.privateUrl !== undefined) env[keys.url] = packEnvValue(target.privateUrl);
1830
- if (target.rpcToken !== undefined) env[keys.token] = packEnvValue(target.rpcToken);
1831
2128
  }
1832
2129
  env[RPC_ORG_ENV] = yield* resolveOrgSlug();
1833
2130
  if (published.added) env[BINDING_PORT_ENV] = String(bindingPort);
1834
2131
  const statics = toFlyStatics(props.statics);
1835
2132
 
1836
- const { imageRef, codeHash } = yield* hosted.resolveImage({
1837
- id,
1838
- appName,
1839
- props,
1840
- previousHash: output?.code.hash,
1841
- session,
1842
- });
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
+ });
1843
2150
 
1844
2151
  const set = yield* reconcileReplicas({
1845
2152
  id,
@@ -1868,9 +2175,19 @@ export const ServiceProvider = () =>
1868
2175
  mounts: desired.mounts,
1869
2176
  metadata: desired.metadata,
1870
2177
  statics,
2178
+ processes,
1871
2179
  }),
1872
2180
  buildConfig: ({ mounts, metadata }) =>
1873
- 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
+ }),
1874
2191
  }).pipe(
1875
2192
  Effect.catchTag("Fly.ReplicaNotCreated", (error) =>
1876
2193
  Effect.fail(
@@ -1883,13 +2200,15 @@ export const ServiceProvider = () =>
1883
2200
  isPublic,
1884
2201
  network,
1885
2202
  bindingPort,
1886
- rpcToken: props.rpcToken,
2203
+ secrets: boundSecrets.secrets,
2204
+ secretsVersion: boundSecrets.version,
2205
+ orgSlug: yield* resolveOrgSlug(),
1887
2206
  });
1888
2207
  }),
1889
2208
 
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.
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.
1893
2212
  precreate: Effect.fn(function* ({ id, news }) {
1894
2213
  const props = news;
1895
2214
  // Precreate runs before dependencies resolve. A Service placed in
@@ -1943,7 +2262,7 @@ export const ServiceProvider = () =>
1943
2262
  isPublic,
1944
2263
  network,
1945
2264
  bindingPort,
1946
- rpcToken: isResolved(props.rpcToken) ? props.rpcToken : undefined,
2265
+ orgSlug: yield* resolveOrgSlug(),
1947
2266
  });
1948
2267
  }),
1949
2268