@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/lib/Service.js CHANGED
@@ -5,9 +5,9 @@ import * as Bundle from "alchemy/Bundle/Bundle";
5
5
  import { deepEqual, isResolved } from "alchemy/Diff";
6
6
  import { Platform } from "alchemy/Platform";
7
7
  import * as Provider from "alchemy/Provider";
8
- import { makeRandom } from "alchemy/Random";
9
8
  import { packEnvValue } from "alchemy/RuntimeContext";
10
9
  import { Stack } from "alchemy/Stack";
10
+ import { sha256 } from "alchemy/Util/sha256";
11
11
  import * as Data from "effect/Data";
12
12
  import * as Effect from "effect/Effect";
13
13
  import * as Layer from "effect/Layer";
@@ -19,13 +19,13 @@ import { deploymentPolicy, validateDeployment, } from "./Deployment.js";
19
19
  import { resolveOrgSlug } from "./Environment.js";
20
20
  import { collectBindingState, createFlyHostedSupport, createFlyHostRuntimeContext, defaultHttpServices, DEFAULT_PORT, toEnvRecord, } from "./hosted.js";
21
21
  import { ensureFlycastAddress, hasPublicAddress, syncOwnedAppAddresses } from "./IpAssignment.js";
22
- import { alchemyMetadataKeys, createFlyAppName, createFlyResourceName, diffMachineMetadata, sanitizeFlyAppName, } from "./Metadata.js";
22
+ import { alchemyMetadataKeys, createFlyAppName, createFlyResourceName, diffMachineMetadata, matchesAlchemyPhysicalName, sanitizeFlyAppName, } from "./Metadata.js";
23
23
  import { bindingPortOf, findPortConflict, portsOfFly } from "./ports.js";
24
24
  import { attachPostgresSecrets } from "./Postgres.js";
25
25
  import { attachRedisSecrets } from "./Redis.js";
26
26
  import { regionList } from "./Region.js";
27
- import { deleteReplicaSet, hasPublishedService, listReplicaSets, observeReplicaSet, reconcileReplicas, resolveCount, sameServices, toFlyService, volumeIdsOf, } from "./replicas.js";
28
- import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_ORG_ENV, RPC_TOKEN_ENV } from "./rpc.js";
27
+ import { deleteReplicaSet, hasPublishedService, listReplicaSets, lookupReplicaSet, observeReplicaSet, reconcileReplicas, resolveCount, sameServices, toFlyService, volumeIdsOf, } from "./replicas.js";
28
+ import { BINDING_PORT_ENV, DEFAULT_BINDING_PORT, RPC_CALLERS_ENV, RPC_ORG_ENV } from "./rpc.js";
29
29
  const DEFAULT_REGION = "iad";
30
30
  const DEFAULT_CPU_KIND = "shared";
31
31
  const DEFAULT_CPUS = 1;
@@ -212,9 +212,9 @@ export const isService = (value) => typeof value === "object" &&
212
212
  * ### Call another Service
213
213
  * A Service can return methods next to `fetch`. Another Service calls
214
214
  * them by binding it with {@link bindService}, which returns a typed
215
- * client. Binding makes the caller deploy after the Service, and only
216
- * callers that bind a Service receive its caller token, so only they
217
- * can call its methods.
215
+ * client. Binding makes the caller deploy after the Service and records
216
+ * the caller's App on it, so only callers that bind a Service can call
217
+ * its methods.
218
218
  *
219
219
  * **Example:** A private Service with methods
220
220
  * ```typescript
@@ -248,9 +248,10 @@ export const isService = (value) => typeof value === "object" &&
248
248
  * ```
249
249
  *
250
250
  * Calls use plain HTTP to `privateUrl` inside Fly's WireGuard-encrypted
251
- * private network. The Service accepts a method call only with its
252
- * caller token and a `Fly-Src` signature from Fly's proxy for the same
253
- * organization, so the public internet can never call it. A public
251
+ * private network. The Service accepts a method call only with a
252
+ * `Fly-Src` signature from Fly's proxy naming the App of a Service that
253
+ * binds it, so neither the public internet nor any other App can call
254
+ * it, and no caller holds a key. A public
254
255
  * Service publishes an extra plain-HTTP port for bound callers,
255
256
  * `bindingPort` (default 7780), since its own ports are an HTTPS
256
257
  * redirect and HTTPS. `client.fetch` sends a request to the Service's
@@ -401,8 +402,18 @@ export const isService = (value) => typeof value === "object" &&
401
402
  *
402
403
  * ### Config
403
404
  * Yield `Config` in init. Alchemy reads the value from the env of
404
- * whoever deploys and writes it onto the Machine, so there is no need
405
- * to copy it into `env`.
405
+ * whoever deploys and stores it as a secret on the Service's App, so
406
+ * there is no need to copy it into `env` and it never appears in the
407
+ * Machine config. Redacted outputs of bound resources are stored the
408
+ * same way. The secret's name is scoped to the Service
409
+ * (`ALCHEMY_<hash>_API_KEY`) and the Machine maps it back to `API_KEY`,
410
+ * so it never collides with `Fly.Secret`, `fly secrets set` or an
411
+ * attachment. The bound value wins over an App secret already named
412
+ * `API_KEY`, which stays in the App untouched. The Machine sees the
413
+ * App's other secrets as they were at deploy; one set later with
414
+ * `fly secrets set` reaches it on the next deploy. A Service in a shared
415
+ * App (`app`) writes the values into the Machine env instead, because
416
+ * Fly gives every App secret to every Machine in the App.
406
417
  *
407
418
  * `Config.Redacted("API_KEY")` is `Redacted<string>`. Unwrap with
408
419
  * `Redacted.value` only where you need the raw string.
@@ -540,6 +551,20 @@ export const isService = (value) => typeof value === "object" &&
540
551
  * ) {}
541
552
  * ```
542
553
  *
554
+ * ### Run your own image
555
+ * `context` builds a directory's `Dockerfile` as it is, on a Fly remote
556
+ * builder, and runs the image's own `CMD`. A change to any file in the
557
+ * directory updates the Machines.
558
+ *
559
+ * **Example:** A Dockerfile build
560
+ * ```typescript
561
+ * const worker = yield* Fly.Service("Worker", {
562
+ * context: "./worker",
563
+ * public: false,
564
+ * services: [],
565
+ * });
566
+ * ```
567
+ *
543
568
  * ### Custom proxy services
544
569
  * `services` defaults to HTTP 80 (redirecting to HTTPS) + HTTPS 443
545
570
  * toward `port`, or plain HTTP 80 for a private Service. Pass a custom
@@ -602,28 +627,23 @@ export const isService = (value) => typeof value === "object" &&
602
627
  * ) {}
603
628
  * ```
604
629
  *
605
- * **Example:** Install `pg` unbundled
606
- * `pg` is CommonJS. Rolldown's interop turns `Client` into a namespace.
607
- * Install it into the image so `@effect/sql-pg` / `Drizzle.Postgres` load
608
- * it with Node's CJS semantics — same `build.install` as Lambda.
630
+ * **Example:** Install a native package unbundled
631
+ * `build.install` runs `npm install` in the image for packages that
632
+ * cannot be bundled, such as native addons — same `build.install` as
633
+ * Lambda.
609
634
  * ```typescript
610
635
  * export default class Api extends Fly.Service<Api>()(
611
636
  * "Api",
612
637
  * {
613
638
  * main: import.meta.url,
614
639
  * port: 3000,
615
- * build: { install: ["pg"] },
640
+ * build: { install: ["sharp"] },
616
641
  * },
617
642
  * Effect.gen(function* () {
618
- * const conn = yield* Fly.ConnectPostgres(Db);
619
- * const db = yield* Drizzle.Postgres(conn.connectionString);
620
643
  * return {
621
- * fetch: Effect.gen(function* () {
622
- * const rows = yield* db.execute("select 1 as ok");
623
- * return HttpServerResponse.json({ rows });
624
- * }),
644
+ * fetch: Effect.succeed(HttpServerResponse.text("hello")),
625
645
  * };
626
- * }).pipe(Effect.provide(Fly.ConnectPostgresHttp)),
646
+ * }),
627
647
  * ) {}
628
648
  * ```
629
649
  *
@@ -707,6 +727,18 @@ export const isService = (value) => typeof value === "object" &&
707
727
  * See the [deployment guide](/fly/compute/deployments) for recovery,
708
728
  * idle capacity, application responsibilities, and verification limits.
709
729
  *
730
+ * ### Looking up an existing Service
731
+ * `Service.lookup` references a Service Alchemy doesn't manage (deployed
732
+ * with `fly deploy` or by another stack). It is read during plan/deploy,
733
+ * never created, updated or deleted, and `url`, `endpoints` and
734
+ * `privateUrl` come from the Machines and addresses Fly reports.
735
+ *
736
+ * **Example:** Call a Service another stack deployed
737
+ * ```typescript
738
+ * const api = Fly.Service.lookup({ appName: "my-api" });
739
+ * const url = api.privateUrl;
740
+ * ```
741
+ *
710
742
  * @resource
711
743
  * @product Service
712
744
  */
@@ -721,8 +753,6 @@ export const Service = Platform("Fly.Service", {
721
753
  const app = Effect.isEffect(props.app)
722
754
  ? yield* props.app
723
755
  : props.app;
724
- // Only callers that bind this Service receive the token.
725
- const rpcToken = yield* makeRandom(`${id}RpcToken`);
726
756
  if (app !== undefined) {
727
757
  // Report the ports this Service publishes so the App can reject
728
758
  // two Services on the same port before either one is deployed.
@@ -732,7 +762,7 @@ export const Service = Platform("Fly.Service", {
732
762
  bindingPort: props.bindingPort,
733
763
  });
734
764
  }
735
- return { ...props, app, rpcToken };
765
+ return { ...props, app };
736
766
  }),
737
767
  });
738
768
  export class ServiceNotCreated extends Data.TaggedError("Fly.ServiceNotCreated") {
@@ -768,6 +798,104 @@ const toFlyGuest = (guest) => {
768
798
  fly.gpus = guest.gpus;
769
799
  return fly;
770
800
  };
801
+ /**
802
+ * Prefix of the App secrets a Service writes. Scoped to its FQN so they
803
+ * never share a name with `Fly.Secret`, `fly secrets set`, the
804
+ * Redis/Postgres/Bucket attachments, or another Service.
805
+ */
806
+ const boundSecretPrefix = Effect.fn(function* (fqn) {
807
+ const hash = yield* sha256(fqn);
808
+ return `ALCHEMY_${hash.slice(0, 8).toUpperCase()}_`;
809
+ });
810
+ /** A secret some Service wrote under its {@link boundSecretPrefix}. */
811
+ const SCOPED_SECRET = /^ALCHEMY_[0-9A-F]{8}_/;
812
+ /**
813
+ * Store bound `Config` and Redacted values as App secrets under names
814
+ * scoped to the Service; the Machine config maps each back to its env var
815
+ * (see {@link secretMappings}). Fly's digest is not a hash Alchemy can
816
+ * compute, so a value is unchanged only when its sha256 matches the last
817
+ * write and Fly still reports that write's digest. Deletes secrets under
818
+ * the Service's prefix that are no longer bound, including ones a crash
819
+ * left out of state; never touches any other secret. Returns what it now
820
+ * owns, the App's other secrets the Machine should still see, and the
821
+ * App-secrets version Machines must reach.
822
+ */
823
+ const syncBoundSecrets = Effect.fn(function* (appName, fqn, desired, previous) {
824
+ if (Object.keys(desired).length === 0 && Object.keys(previous.secrets).length === 0) {
825
+ return { secrets: {}, others: [], version: previous.version };
826
+ }
827
+ const prefix = yield* boundSecretPrefix(fqn);
828
+ const listDigests = machines
829
+ .listSecrets({ app_name: appName, show_secrets: false })
830
+ .pipe(Effect.map((listed) => new Map((listed.secrets ?? []).flatMap((secret) => secret.name === undefined ? [] : [[secret.name, secret.digest ?? ""]]))));
831
+ const observed = yield* listDigests;
832
+ const planned = yield* Effect.forEach(Object.entries(desired), ([envVar, value]) =>
833
+ // Salted with the scoped name, so equal values on different Services
834
+ // or Apps never share a hash in state.
835
+ sha256(`${prefix}${envVar}:${Redacted.value(value)}`).pipe(Effect.map((hash) => ({ envVar, name: `${prefix}${envVar}`, value, hash }))), { concurrency: "unbounded" });
836
+ const isUnchanged = ({ envVar, name, hash }) => {
837
+ const last = previous.secrets[envVar];
838
+ return (last !== undefined &&
839
+ last.name === name &&
840
+ last.sha256 === hash &&
841
+ observed.get(name) === last.digest);
842
+ };
843
+ const kept = planned.filter(isUnchanged);
844
+ const changed = planned.filter((entry) => !isUnchanged(entry));
845
+ const unbound = [...observed.keys()].filter((name) => name.startsWith(prefix) && !(name.slice(prefix.length) in desired));
846
+ const write = changed.length === 0
847
+ ? Effect.succeed({ version: undefined, written: [] })
848
+ : Effect.gen(function* () {
849
+ const response = yield* machines.updateSecrets({
850
+ app_name: appName,
851
+ values: Object.fromEntries(changed.map(({ name, value }) => [name, value])),
852
+ });
853
+ const digests = yield* listDigests;
854
+ return {
855
+ version: response.version ?? response.Version,
856
+ written: changed.map(({ envVar, name, hash }) => [
857
+ envVar,
858
+ { name, digest: digests.get(name) ?? "", sha256: hash },
859
+ ]),
860
+ };
861
+ });
862
+ const remove = Effect.forEach(unbound, (name) => machines.deleteSecret({ app_name: appName, secret_name: name }).pipe(Effect.map((response) => response.version ?? response.Version), Effect.catchTag("NotFound", () => Effect.succeed(undefined))), { concurrency: "unbounded" });
863
+ const [{ version: writeVersion, written }, deleteVersions] = yield* Effect.all([write, remove], {
864
+ concurrency: "unbounded",
865
+ });
866
+ const secrets = Object.fromEntries([
867
+ ...kept.map(({ envVar }) => [envVar, previous.secrets[envVar]]),
868
+ ...written,
869
+ ]);
870
+ const versions = [writeVersion, ...deleteVersions].filter((version) => version !== undefined && Number.isSafeInteger(version));
871
+ // A bound value wins over an App secret with the same env var name, as
872
+ // a Machine env var did; that secret stays in the App untouched.
873
+ const others = [...observed.keys()]
874
+ .filter((name) => !SCOPED_SECRET.test(name) && !(name in desired))
875
+ .sort();
876
+ return {
877
+ secrets,
878
+ others,
879
+ version: versions.length > 0 ? Math.max(...versions) : previous.version,
880
+ };
881
+ });
882
+ /**
883
+ * Fly maps a process's `secrets` entries from a scoped App secret to the
884
+ * env var the program reads, but only with `ignore_app_secrets`, which
885
+ * hides every App secret the process does not list. So the App's other
886
+ * secrets (attachments, `fly secrets set`) are listed under their own
887
+ * names, as they were at deploy.
888
+ */
889
+ const secretMappings = (bound) => {
890
+ const mapped = Object.entries(bound.secrets).map(([envVar, secret]) => ({
891
+ env_var: envVar,
892
+ name: secret.name,
893
+ }));
894
+ if (mapped.length === 0)
895
+ return undefined;
896
+ const entries = [...mapped, ...bound.others.map((name) => ({ env_var: name, name }))].sort((left, right) => left.env_var.localeCompare(right.env_var));
897
+ return [{ secrets: entries, ignore_app_secrets: true }];
898
+ };
771
899
  const desiredEnv = (props, bindingEnv, alchemyEnv, port) => ({
772
900
  ...toEnv(bindingEnv),
773
901
  ...alchemyEnv,
@@ -792,6 +920,7 @@ const buildConfig = (input) => ({
792
920
  mounts: input.mounts.length > 0 ? input.mounts : undefined,
793
921
  metadata: input.metadata,
794
922
  statics: input.statics !== undefined && input.statics.length > 0 ? input.statics : undefined,
923
+ processes: input.processes,
795
924
  });
796
925
  const sameImage = (machine, image) => {
797
926
  const configImage = machine.config?.image;
@@ -832,6 +961,17 @@ const sameStatics = (observed, desired) => {
832
961
  }));
833
962
  return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
834
963
  };
964
+ /** Fly fills an omitted secret `name` with its `env_var`. */
965
+ const sameProcesses = (observed, desired) => {
966
+ const normalize = (processes) => (processes ?? []).map((process) => ({
967
+ ...process,
968
+ secrets: process.secrets?.map((secret) => ({
969
+ env_var: secret.env_var,
970
+ name: secret.name || secret.env_var,
971
+ })),
972
+ }));
973
+ return deepEqual(normalize(observed), normalize(desired), { stripNullish: true });
974
+ };
835
975
  const configDrifted = (machine, desired) => {
836
976
  const config = machine.config;
837
977
  return (!sameImage(machine, desired.image) ||
@@ -840,9 +980,9 @@ const configDrifted = (machine, desired) => {
840
980
  !sameServices(config?.services, desired.services) ||
841
981
  !sameMounts(config?.mounts, desired.mounts) ||
842
982
  metadataChanged(config?.metadata, desired.metadata) ||
843
- !sameStatics(config?.statics, desired.statics));
983
+ !sameStatics(config?.statics, desired.statics) ||
984
+ !sameProcesses(config?.processes, desired.processes));
844
985
  };
845
- const portsOf = (services) => (services ?? []).flatMap((service) => service.ports ?? []);
846
986
  const withPort = (base, port, standard) => port === standard ? base : `${base}:${port}`;
847
987
  const isRedirect = (handlers, forceHttps) => forceHttps === true && handlers.includes("http") && !handlers.includes("tls");
848
988
  /** Every published port on `host`, excluding HTTPS redirects and ranges. */
@@ -1038,7 +1178,9 @@ const toAttrs = (set, codeHash, access) => ({
1038
1178
  appName: set.appName,
1039
1179
  ownsApp: access.ownsApp,
1040
1180
  network: normalizeNetwork(access.network),
1041
- rpcToken: access.rpcToken,
1181
+ secrets: access.secrets,
1182
+ secretsVersion: access.secretsVersion,
1183
+ orgSlug: access.orgSlug,
1042
1184
  ...endpointsOf(set, access),
1043
1185
  rolloutPending: set.rolloutPending,
1044
1186
  machineId: set.machineId,
@@ -1054,6 +1196,25 @@ const toAttrs = (set, codeHash, access) => ({
1054
1196
  replicas: set.replicas,
1055
1197
  code: { hash: codeHash },
1056
1198
  });
1199
+ /** The replica set of a Service whose App exists but runs no Machines yet. */
1200
+ const emptyReplicaSet = (appName, name) => ({
1201
+ appName,
1202
+ machineId: "",
1203
+ machineIds: [],
1204
+ name,
1205
+ baseName: name,
1206
+ region: "",
1207
+ regions: [],
1208
+ state: "created",
1209
+ instanceId: undefined,
1210
+ privateIp: undefined,
1211
+ imageRef: undefined,
1212
+ guest: undefined,
1213
+ services: undefined,
1214
+ count: 0,
1215
+ mounts: [],
1216
+ replicas: [],
1217
+ });
1057
1218
  const machineIdsOf = (output) => output?.machineIds ??
1058
1219
  (output?.machineId !== undefined && output.machineId.length > 0 ? [output.machineId] : []);
1059
1220
  /** The private network the Service's Machines join. */
@@ -1107,7 +1268,7 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1107
1268
  return Service.Provider.of({
1108
1269
  stables: ["appName"],
1109
1270
  nuke: { dependsOn: ["Fly.App"] },
1110
- diff: Effect.fn(function* ({ id, news, output }) {
1271
+ diff: Effect.fn(function* ({ news, output }) {
1111
1272
  if (news === undefined)
1112
1273
  return;
1113
1274
  const shape = news;
@@ -1172,6 +1333,11 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1172
1333
  // effect-config form has been evaluated, so the object view is
1173
1334
  // safe to read.
1174
1335
  const statics = news;
1336
+ if (typeof statics.context === "string") {
1337
+ const hash = yield* hosted.contextHash(statics.context);
1338
+ if (hash !== output.code.hash)
1339
+ return { action: "update" };
1340
+ }
1175
1341
  if (isResolved({
1176
1342
  main: statics.main,
1177
1343
  build: statics.build,
@@ -1188,6 +1354,24 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1188
1354
  }
1189
1355
  return output.rolloutPending ? { action: "update" } : undefined;
1190
1356
  }),
1357
+ lookup: Effect.fn(function* ({ appName, name }) {
1358
+ const found = yield* lookupReplicaSet({ appName, name });
1359
+ if (found === undefined)
1360
+ return undefined;
1361
+ const app = yield* machines
1362
+ .getApp({ app_name: appName })
1363
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
1364
+ const attrs = toAttrs(found, "", {
1365
+ // Reported as Alchemy-owned only so the endpoints follow the App's
1366
+ // observed addresses, as a declared Service's own App's do.
1367
+ ownsApp: true,
1368
+ isPublic: yield* hasPublicAddress(appName),
1369
+ network: app?.network,
1370
+ bindingPort: DEFAULT_BINDING_PORT,
1371
+ orgSlug: app?.organization?.slug,
1372
+ });
1373
+ return { ...attrs, ownsApp: false };
1374
+ }),
1191
1375
  read: Effect.fn(function* ({ id, fqn, instanceId, olds, output }) {
1192
1376
  const ownsApp = output?.ownsApp ?? (olds !== undefined && olds.app === undefined);
1193
1377
  const appName = ownsApp
@@ -1206,14 +1390,36 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1206
1390
  machineIds: machineIdsOf(output),
1207
1391
  baseName: name,
1208
1392
  });
1209
- if (found === undefined)
1210
- return undefined;
1393
+ if (found === undefined) {
1394
+ // A create interrupted between creating the Service's own App and
1395
+ // starting its Machines (an image build in between can take
1396
+ // minutes) leaves the App with nothing in it. Report it, so the
1397
+ // engine's recovery of the interrupted create deletes it instead
1398
+ // of leaking it. The App's name is derived from this resource's
1399
+ // instance, so an App with that name is this resource's.
1400
+ if (!ownsApp || appName === undefined)
1401
+ return undefined;
1402
+ const app = yield* machines
1403
+ .getApp({ app_name: appName })
1404
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined));
1405
+ if (app === undefined || !matchesAlchemyPhysicalName(app.name))
1406
+ return undefined;
1407
+ return toAttrs(emptyReplicaSet(appName, name), output?.code.hash ?? "", {
1408
+ ownsApp,
1409
+ isPublic: false,
1410
+ network: output?.network ?? olds?.network,
1411
+ bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1412
+ orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
1413
+ });
1414
+ }
1211
1415
  return toAttrs(found, output?.code.hash ?? "", {
1212
1416
  ownsApp,
1213
1417
  isPublic: ownsApp && (yield* hasPublicAddress(found.appName)),
1214
1418
  network: output?.network ?? (ownsApp ? olds?.network : undefined),
1215
1419
  bindingPort: olds?.bindingPort ?? DEFAULT_BINDING_PORT,
1216
- rpcToken: output?.rpcToken,
1420
+ secrets: output?.secrets,
1421
+ secretsVersion: output?.secretsVersion,
1422
+ orgSlug: output?.orgSlug ?? (yield* resolveOrgSlug()),
1217
1423
  });
1218
1424
  }),
1219
1425
  list: Effect.fn(function* () {
@@ -1224,7 +1430,7 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1224
1430
  isPublic: true,
1225
1431
  network: undefined,
1226
1432
  bindingPort: DEFAULT_BINDING_PORT,
1227
- rpcToken: undefined,
1433
+ orgSlug: undefined,
1228
1434
  }));
1229
1435
  }),
1230
1436
  reconcile: Effect.fn(function* ({ id, fqn, instanceId, news, output, bindings, session }) {
@@ -1255,8 +1461,28 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1255
1461
  if (version !== undefined)
1256
1462
  secretVersions.push(version);
1257
1463
  }
1258
- const minSecretsVersion = secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
1259
1464
  const env = desiredEnv(props, bound.env, hosted.alchemyEnv, port);
1465
+ // Redacted values (every bound `Config`) become App secrets, but
1466
+ // only in an App the Service owns: Fly gives every App secret to
1467
+ // every Machine in the App. A plain `env` entry wins over a binding.
1468
+ const redacted = Object.keys(env).filter((key) => props.env?.[key] !== undefined
1469
+ ? Redacted.isRedacted(props.env[key])
1470
+ : bound.secretNames.includes(key));
1471
+ // Move them out of the plain env record before anything else reads it.
1472
+ const desiredSecrets = {};
1473
+ if (ownsApp) {
1474
+ for (const key of redacted) {
1475
+ desiredSecrets[key] = Redacted.make(env[key]);
1476
+ delete env[key];
1477
+ }
1478
+ }
1479
+ const boundSecrets = yield* syncBoundSecrets(appName, fqn, desiredSecrets, output?.appName === appName
1480
+ ? { secrets: output.secrets ?? {}, version: output.secretsVersion }
1481
+ : { secrets: {}, version: undefined });
1482
+ if (boundSecrets.version !== undefined)
1483
+ secretVersions.push(boundSecrets.version);
1484
+ const minSecretsVersion = secretVersions.length > 0 ? Math.max(...secretVersions) : undefined;
1485
+ const processes = secretMappings(boundSecrets);
1260
1486
  if (policy.shutdown && !props.isExternal) {
1261
1487
  env.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS = String(policy.shutdown.timeoutMs);
1262
1488
  }
@@ -1267,27 +1493,36 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1267
1493
  const services = published.services;
1268
1494
  if (!ownsApp)
1269
1495
  yield* validateObservedPorts(appName, fqn, id, services);
1270
- if (props.rpcToken !== undefined)
1271
- env[RPC_TOKEN_ENV] = Redacted.value(props.rpcToken);
1496
+ // The Apps of the Services that bind this one; only they may call
1497
+ // its methods (see `serveFlyRpc`).
1498
+ if (bound.callers.length > 0)
1499
+ env[RPC_CALLERS_ENV] = bound.callers.join(",");
1272
1500
  for (const target of bound.targets) {
1273
1501
  const keys = boundTargetEnvKeys(target.service);
1274
1502
  env[keys.appName] = packEnvValue(target.appName);
1275
1503
  if (target.privateUrl !== undefined)
1276
1504
  env[keys.url] = packEnvValue(target.privateUrl);
1277
- if (target.rpcToken !== undefined)
1278
- env[keys.token] = packEnvValue(target.rpcToken);
1279
1505
  }
1280
1506
  env[RPC_ORG_ENV] = yield* resolveOrgSlug();
1281
1507
  if (published.added)
1282
1508
  env[BINDING_PORT_ENV] = String(bindingPort);
1283
1509
  const statics = toFlyStatics(props.statics);
1284
- const { imageRef, codeHash } = yield* hosted.resolveImage({
1285
- id,
1286
- appName,
1287
- props,
1288
- previousHash: output?.code.hash,
1289
- session,
1290
- });
1510
+ const { imageRef, codeHash } = props.context !== undefined
1511
+ ? yield* hosted.resolveContextImage({
1512
+ id,
1513
+ appName,
1514
+ context: props.context,
1515
+ remote: props.build?.remote,
1516
+ previousHash: output?.code.hash,
1517
+ session,
1518
+ })
1519
+ : yield* hosted.resolveImage({
1520
+ id,
1521
+ appName,
1522
+ props: props,
1523
+ previousHash: output?.code.hash,
1524
+ session,
1525
+ });
1291
1526
  const set = yield* reconcileReplicas({
1292
1527
  id,
1293
1528
  type: "Fly.Service",
@@ -1312,20 +1547,32 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1312
1547
  mounts: desired.mounts,
1313
1548
  metadata: desired.metadata,
1314
1549
  statics,
1550
+ processes,
1551
+ }),
1552
+ buildConfig: ({ mounts, metadata }) => buildConfig({
1553
+ image: imageRef,
1554
+ guest,
1555
+ env,
1556
+ services,
1557
+ mounts,
1558
+ metadata,
1559
+ statics,
1560
+ processes,
1315
1561
  }),
1316
- buildConfig: ({ mounts, metadata }) => buildConfig({ image: imageRef, guest, env, services, mounts, metadata, statics }),
1317
- }).pipe(Effect.catchTag("Fly.ReplicaNotCreated", (error) => Effect.fail(new ServiceNotCreated({ name: error.name, appName: error.appName }))));
1562
+ }).pipe(Effect.catchTag("Fly.ReplicaNotCreated", (error) => Effect.fail(new ServiceNotCreated({ resourceName: error.resourceName, appName: error.appName }))));
1318
1563
  return toAttrs(set, codeHash, {
1319
1564
  ownsApp,
1320
1565
  isPublic,
1321
1566
  network,
1322
1567
  bindingPort,
1323
- rpcToken: props.rpcToken,
1568
+ secrets: boundSecrets.secrets,
1569
+ secretsVersion: boundSecrets.version,
1570
+ orgSlug: yield* resolveOrgSlug(),
1324
1571
  });
1325
1572
  }),
1326
- // Create the App, its addresses, and the caller token before any
1327
- // Machine, so two Services that bind each other can both resolve
1328
- // the other's private address and token.
1573
+ // Create the App and its addresses before any Machine, so two
1574
+ // Services that bind each other can both resolve the other's App
1575
+ // and private address.
1329
1576
  precreate: Effect.fn(function* ({ id, news }) {
1330
1577
  const props = news;
1331
1578
  // Precreate runs before dependencies resolve. A Service placed in
@@ -1371,7 +1618,7 @@ export const ServiceProvider = () => Provider.effect(Service, Effect.gen(functio
1371
1618
  isPublic,
1372
1619
  network,
1373
1620
  bindingPort,
1374
- rpcToken: isResolved(props.rpcToken) ? props.rpcToken : undefined,
1621
+ orgSlug: yield* resolveOrgSlug(),
1375
1622
  });
1376
1623
  }),
1377
1624
  delete: Effect.fn(function* ({ id, fqn, instanceId, olds, output, force }) {