@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/Sprite.ts CHANGED
@@ -5,7 +5,7 @@ import * as Bundle from "alchemy/Bundle/Bundle";
5
5
  import { isResolved } from "alchemy/Diff";
6
6
  import { Platform, type Main, type PlatformProps } from "alchemy/Platform";
7
7
  import * as Provider from "alchemy/Provider";
8
- import type { Resource, ResourceBinding } from "alchemy/Resource";
8
+ import type { LookupMethod, Resource, ResourceBinding } from "alchemy/Resource";
9
9
  import type { ServerHost } from "alchemy/Runtime/Process";
10
10
  import { Stack } from "alchemy/Stack";
11
11
  import { createInternalTags } from "alchemy/Tags";
@@ -119,9 +119,19 @@ export type Sprite = Resource<
119
119
  };
120
120
  },
121
121
  SpriteBinding,
122
- Providers
122
+ Providers,
123
+ SpriteLookup
123
124
  >;
124
125
 
126
+ /**
127
+ * Props for {@link Sprite.lookup}: find a Sprite this stack doesn't
128
+ * manage by its name.
129
+ */
130
+ export interface SpriteLookup {
131
+ /** Sprite name (unique per org). */
132
+ name: string;
133
+ }
134
+
125
135
  export const isSprite = (value: unknown): value is Sprite =>
126
136
  typeof value === "object" && value !== null && (value as { Type?: string }).Type === "Fly.Sprite";
127
137
 
@@ -307,29 +317,38 @@ export type SpriteRuntimeContext = FlyHostRuntimeContext;
307
317
  * ) {}
308
318
  * ```
309
319
  *
320
+ * ### Looking up an existing Sprite
321
+ * `Sprite.lookup` references a Sprite Alchemy doesn't manage (created with
322
+ * `sprite create` or by another stack). It is read during plan/deploy,
323
+ * never created, updated or deleted. `code.hash` is empty: the Sprite's
324
+ * program isn't Alchemy's.
325
+ *
326
+ * **Example:** Reference an existing Sprite
327
+ * ```typescript
328
+ * const box = Fly.Sprite.lookup({ name: "my-box" });
329
+ * ```
330
+ *
310
331
  * @resource
311
332
  * @product Sprite
312
333
  */
313
- export const Sprite: Platform<Sprite, SpriteServices, SpriteShape, SpriteRuntimeContext> = Platform(
314
- "Fly.Sprite",
315
- {
316
- createRuntimeContext: createFlyHostRuntimeContext("Fly.Sprite"),
317
- },
318
- );
334
+ export const Sprite: Platform<Sprite, SpriteServices, SpriteShape, SpriteRuntimeContext> &
335
+ LookupMethod<Sprite> = Platform("Fly.Sprite", {
336
+ createRuntimeContext: createFlyHostRuntimeContext("Fly.Sprite"),
337
+ });
319
338
 
320
339
  export class SpriteNotCreated extends Data.TaggedError("Fly.SpriteNotCreated")<{
321
- name: string;
340
+ resourceName: string;
322
341
  }> {}
323
342
 
324
343
  export class SpriteExecFailed extends Data.TaggedError("Fly.SpriteExecFailed")<{
325
- name: string;
344
+ resourceName: string;
326
345
  cmd: string;
327
346
  exitCode: number | undefined;
328
347
  stderr: string | undefined;
329
348
  }> {}
330
349
 
331
350
  class SpritePending extends Data.TaggedError("Fly.SpritePending")<{
332
- name: string;
351
+ resourceName: string;
333
352
  }> {}
334
353
 
335
354
  const toStatus = (status: string | undefined): SpriteStatus =>
@@ -368,12 +387,14 @@ const ownershipLabels = (id: string) =>
368
387
  });
369
388
 
370
389
  const getByName = (name: string) =>
371
- sprites.getSprite({ name }).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
390
+ sprites.getSprite({ name }).pipe(Effect.catchTag("NotFound", () => Effect.undefined));
372
391
 
373
392
  const waitForSprite = (name: string) =>
374
393
  getByName(name).pipe(
375
394
  Effect.flatMap((found) =>
376
- found !== undefined ? Effect.succeed(found) : Effect.fail(new SpritePending({ name })),
395
+ found !== undefined
396
+ ? Effect.succeed(found)
397
+ : Effect.fail(new SpritePending({ resourceName: name })),
377
398
  ),
378
399
  Effect.retry({
379
400
  while: (e) => e._tag === "Fly.SpritePending",
@@ -458,7 +479,7 @@ const runExec = (input: { name: string; cmd: string[]; env?: string[] }) =>
458
479
  );
459
480
  if ((result.exit_code ?? 0) !== 0) {
460
481
  return yield* new SpriteExecFailed({
461
- name: input.name,
482
+ resourceName: input.name,
462
483
  cmd: input.cmd.join(" "),
463
484
  exitCode: result.exit_code,
464
485
  stderr: result.stderr,
@@ -573,15 +594,22 @@ export const SpriteProvider = () =>
573
594
  return undefined;
574
595
  }),
575
596
 
597
+ lookup: Effect.fn(function* ({ name }) {
598
+ const found = yield* getByName(name).pipe(
599
+ Effect.catchTag("SpritesNotEnabled", () => Effect.undefined),
600
+ );
601
+ return found === undefined ? undefined : toAttrs(found, name, "");
602
+ }),
603
+
576
604
  read: Effect.fn(function* ({ id, olds, output }) {
577
605
  const name = yield* resolveSpriteName(id, olds?.name, output?.name);
578
606
  const found = yield* (
579
- output?.name !== undefined ? getByName(output.name) : Effect.succeed(undefined)
607
+ output?.name !== undefined ? getByName(output.name) : Effect.undefined
580
608
  ).pipe(
581
609
  Effect.flatMap((existing) =>
582
610
  existing !== undefined ? Effect.succeed(existing) : getByName(name),
583
611
  ),
584
- Effect.catchTag("SpritesNotEnabled", () => Effect.succeed(undefined)),
612
+ Effect.catchTag("SpritesNotEnabled", () => Effect.undefined),
585
613
  );
586
614
  if (found === undefined) return undefined;
587
615
  const attrs = toAttrs(found, name, output?.code.hash ?? "");
@@ -628,7 +656,7 @@ export const SpriteProvider = () =>
628
656
  }
629
657
 
630
658
  if (current === undefined) {
631
- return yield* new SpriteNotCreated({ name });
659
+ return yield* new SpriteNotCreated({ resourceName: name });
632
660
  }
633
661
  // Narrowed once so the `NotFound` fallbacks below keep the
634
662
  // non-optional type instead of widening back to `| undefined`.
package/src/Volume.ts CHANGED
@@ -16,7 +16,7 @@ export const DEFAULT_VOLUME_REGION = "iad";
16
16
  export const MIN_SIZE_GB = 1;
17
17
 
18
18
  export class VolumeNotCreated extends Data.TaggedError("Fly.VolumeNotCreated")<{
19
- name: string;
19
+ resourceName: string;
20
20
  appName: string;
21
21
  }> {}
22
22
 
@@ -39,7 +39,7 @@ const transientState = (state: string | undefined) =>
39
39
  export const getVolumeById = (appName: string, volumeId: string) =>
40
40
  machines.getVolumeById({ app_name: appName, volume_id: volumeId }).pipe(
41
41
  Effect.map((volume) => (destroying(volume.state) ? undefined : volume)),
42
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
42
+ Effect.catchTag("NotFound", () => Effect.undefined),
43
43
  );
44
44
 
45
45
  export const listVolumesByApp = (appName: string) =>
@@ -66,7 +66,7 @@ export const listVolumeGroup = (appName: string, name: string, region: string) =
66
66
  export const waitUntilVolumeReady = (appName: string, volumeId: string) =>
67
67
  getVolumeById(appName, volumeId).pipe(
68
68
  Effect.flatMap((volume) => {
69
- if (volume === undefined) return Effect.succeed(undefined);
69
+ if (volume === undefined) return Effect.undefined;
70
70
  if (transientState(volume.state)) {
71
71
  return Effect.fail(
72
72
  new VolumePending({
@@ -116,6 +116,11 @@ export const createVolume = Effect.fn(function* (input: {
116
116
  disk: DiskSpec;
117
117
  }) {
118
118
  const sizeGb = Math.max(input.disk.sizeGb, MIN_SIZE_GB);
119
+ // The volumes this group held before the create: a storage host that fails
120
+ // with an unknown outcome may still have made one.
121
+ const before = new Set(
122
+ (yield* listVolumeGroup(input.appName, input.name, input.region)).map((volume) => volume.id),
123
+ );
119
124
  const created = yield* machines
120
125
  .createVolume({
121
126
  app_name: input.appName,
@@ -130,7 +135,26 @@ export const createVolume = Effect.fn(function* (input: {
130
135
  source_volume_id: input.disk.sourceVolumeId,
131
136
  require_unique_zone: input.disk.requireUniqueZone,
132
137
  })
133
- .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
138
+ .pipe(
139
+ Effect.catchTags({
140
+ Conflict: () => Effect.undefined,
141
+ StorageHostCapacity: (error) =>
142
+ Effect.gen(function* () {
143
+ // Its outcome is unknown: a single new volume in the group is the
144
+ // one this call made. Otherwise create again after a pause.
145
+ yield* Effect.sleep(Duration.seconds(5));
146
+ const fresh = (yield* listVolumeGroup(input.appName, input.name, input.region)).filter(
147
+ (volume) => volume.id !== undefined && !before.has(volume.id),
148
+ );
149
+ return fresh.length === 1 ? fresh[0] : yield* error;
150
+ }),
151
+ }),
152
+ Effect.retry({
153
+ while: (error) => error._tag === "StorageHostCapacity",
154
+ schedule: Schedule.spaced(Duration.seconds(5)),
155
+ times: 6,
156
+ }),
157
+ );
134
158
  if (created?.id !== undefined && created.id.length > 0) {
135
159
  return (yield* waitUntilVolumeReady(input.appName, created.id)) ?? created;
136
160
  }
@@ -138,7 +162,7 @@ export const createVolume = Effect.fn(function* (input: {
138
162
  const hit = group.at(-1);
139
163
  if (hit === undefined || hit.id === undefined) {
140
164
  return yield* new VolumeNotCreated({
141
- name: input.name,
165
+ resourceName: input.name,
142
166
  appName: input.appName,
143
167
  });
144
168
  }
@@ -211,7 +235,7 @@ export const ensureVolumeGroup = Effect.fn(function* (input: {
211
235
  }
212
236
  if (group.length < input.count) {
213
237
  return yield* new VolumeNotCreated({
214
- name: input.name,
238
+ resourceName: input.name,
215
239
  appName: input.appName,
216
240
  });
217
241
  }
@@ -56,9 +56,23 @@ export type VolumeSnapshot = Resource<
56
56
  createdAt: string | undefined;
57
57
  },
58
58
  never,
59
- Providers
59
+ Providers,
60
+ VolumeSnapshotLookup
60
61
  >;
61
62
 
63
+ /**
64
+ * Props for {@link VolumeSnapshot.lookup}: find a snapshot this stack
65
+ * doesn't manage by its App, Volume and id.
66
+ */
67
+ export interface VolumeSnapshotLookup {
68
+ /** Fly App name the Volume belongs to. */
69
+ appName: string;
70
+ /** Fly Volume id (`vol_…`). */
71
+ volumeId: string;
72
+ /** Fly snapshot id (`vs_…`). */
73
+ snapshotId: string;
74
+ }
75
+
62
76
  /**
63
77
  * A Fly.VolumeSnapshot is an on-demand snapshot of a mounted disk.
64
78
  *
@@ -127,6 +141,25 @@ export type VolumeSnapshot = Resource<
127
141
  * ) {}
128
142
  * ```
129
143
  *
144
+ * ### Looking up an existing snapshot
145
+ * `VolumeSnapshot.lookup` references a snapshot Alchemy doesn't manage
146
+ * (taken by Fly's daily schedule or by another stack). It is read during
147
+ * plan/deploy and never created or deleted.
148
+ *
149
+ * **Example:** Restore a disk from an existing snapshot
150
+ * ```typescript
151
+ * const snapshot = Fly.VolumeSnapshot.lookup({
152
+ * appName: "my-app",
153
+ * volumeId: "vol_123",
154
+ * snapshotId: "vs_456",
155
+ * });
156
+ * const disk = yield* Fly.MountVolume({
157
+ * path: "/data",
158
+ * sizeGb: 1,
159
+ * snapshotId: snapshot.snapshotId,
160
+ * });
161
+ * ```
162
+ *
130
163
  * @resource
131
164
  * @product Volume
132
165
  */
@@ -214,7 +247,7 @@ const waitForNewSnapshot = (appName: string, volumeId: string, knownIds: Readonl
214
247
  times: 8,
215
248
  schedule: backoff,
216
249
  }),
217
- Effect.catchTag("Fly.VolumeSnapshotPending", () => Effect.succeed(undefined)),
250
+ Effect.catchTag("Fly.VolumeSnapshotPending", () => Effect.undefined),
218
251
  );
219
252
 
220
253
  export const VolumeSnapshotProvider = () =>
@@ -235,6 +268,11 @@ export const VolumeSnapshotProvider = () =>
235
268
  return undefined;
236
269
  }),
237
270
 
271
+ lookup: Effect.fn(function* ({ appName, volumeId, snapshotId }) {
272
+ const found = yield* findById(appName, volumeId, snapshotId);
273
+ return found === undefined ? undefined : toAttrs(appName, volumeId, found);
274
+ }),
275
+
238
276
  read: Effect.fn(function* ({ olds, output }) {
239
277
  const appName = output?.appName ?? appNameOf(olds?.app);
240
278
  const volumeId = output?.volumeId ?? volumeIdOf(olds?.volumeId);
@@ -95,7 +95,7 @@ const scopeOf = (bucket: Bucket) =>
95
95
  endpoint === undefined
96
96
  ) {
97
97
  return yield* new TigrisCredentialsMissing({
98
- name: bucketName ?? bucket.LogicalId,
98
+ resourceName: bucketName ?? bucket.LogicalId,
99
99
  });
100
100
  }
101
101
  return { bucketName, accessKeyId, secretAccessKey, endpoint, region };
@@ -131,14 +131,14 @@ const walkFiles = Effect.fn(function* (root: string) {
131
131
  const path = yield* Path.Path;
132
132
  if (!(yield* fs.exists(root))) return [] as string[];
133
133
  const names = yield* fs.readDirectory(root, { recursive: true });
134
- const files = yield* Effect.all(
135
- names.map((name) =>
134
+ const files = yield* Effect.forEach(
135
+ names,
136
+ (name) =>
136
137
  Effect.gen(function* () {
137
138
  const full = path.join(root, name);
138
139
  const stat = yield* fs.stat(full);
139
140
  return stat.type === "File" ? name.replaceAll("\\", "/") : undefined;
140
141
  }),
141
- ),
142
142
  { concurrency: "unbounded" },
143
143
  );
144
144
  return files
@@ -213,8 +213,9 @@ export const AssetDeploymentProvider = () =>
213
213
  : path.resolve(initialCwd, news.sourcePath);
214
214
  const files = yield* walkFiles(root);
215
215
  const observed = yield* listObserved(scope, prefix);
216
- const prepared = yield* Effect.all(
217
- files.map((relative) =>
216
+ const prepared = yield* Effect.forEach(
217
+ files,
218
+ (relative) =>
218
219
  Effect.gen(function* () {
219
220
  const body = yield* fs.readFile(path.join(root, relative));
220
221
  const key = prefix.length > 0 ? `${prefix}/${relative}` : relative;
@@ -222,7 +223,6 @@ export const AssetDeploymentProvider = () =>
222
223
  const cacheControl = cacheControlOf(relative);
223
224
  return { relative, key, body, contentType, cacheControl };
224
225
  }),
225
- ),
226
226
  { concurrency: "unbounded" },
227
227
  );
228
228
  const version = yield* Effect.sync(() => {
@@ -258,10 +258,9 @@ export const AssetDeploymentProvider = () =>
258
258
  );
259
259
 
260
260
  if (news.purge ?? true) {
261
- yield* Effect.all(
262
- [...observed.keys()]
263
- .filter((key) => !desired.has(key))
264
- .map((key) => deleteObject(scope, key)),
261
+ yield* Effect.forEach(
262
+ [...observed.keys()].filter((key) => !desired.has(key)),
263
+ (key) => deleteObject(scope, key),
265
264
  { concurrency: s3Concurrency },
266
265
  );
267
266
  }
@@ -283,10 +282,9 @@ export const AssetDeploymentProvider = () =>
283
282
  if (!(olds.purge ?? true)) return;
284
283
  const scope = yield* scopeOf(olds.bucket);
285
284
  const observed = yield* listObserved(scope, output.prefix);
286
- yield* Effect.all(
287
- [...observed.keys()].map((key) => deleteObject(scope, key)),
288
- { concurrency: s3Concurrency },
289
- );
285
+ yield* Effect.forEach([...observed.keys()], (key) => deleteObject(scope, key), {
286
+ concurrency: s3Concurrency,
287
+ });
290
288
  },
291
289
  Effect.catchTag("NoSuchBucket", () => Effect.void),
292
290
  ),
@@ -1,3 +1,5 @@
1
+ import { resolveAstroOutput } from "alchemy/Website/AstroOutput";
2
+ import * as Effect from "effect/Effect";
1
3
  import { frameworkSite, staticConfigFromAssets, type FrameworkSiteProps } from "./FrameworkSite.ts";
2
4
 
3
5
  /** The framework-integration package that drives the Astro build. */
@@ -21,8 +23,9 @@ export interface AstroProps extends FrameworkSiteProps {
21
23
  * Astro output target. `"server"` renders pages on demand; individual
22
24
  * pages opt into prerendering with `export const prerender = true`.
23
25
  * `"static"` prerenders every page at build time and deploys
24
- * assets-only (no SSR server).
25
- * @default "server"
26
+ * assets-only (no SSR server). Overrides the `output` in your
27
+ * `astro.config.*`; leave it unset to use the file's value.
28
+ * @default the config file's `output`, else `"server"`
26
29
  */
27
30
  output?: "server" | "static";
28
31
  /** Source directory, relative to `rootDir`. @default "./src" */
@@ -41,9 +44,9 @@ export interface AstroProps extends FrameworkSiteProps {
41
44
  * running the Node adapter's serve entry (static files first, then the
42
45
  * framework handler) on port 3000.
43
46
  *
44
- * Pages render on demand by default (`output: "server"`). With
45
- * `astro: { output: "static" }` every page is prerendered and the deploy
46
- * is assets-only.
47
+ * Pages render on demand by default (`output: "server"`). When your
48
+ * `astro.config.*` or `astro: { output: "static" }` declares a static
49
+ * site, every page is prerendered and the deploy is assets-only.
47
50
  *
48
51
  *
49
52
  * ### Creating Astro Sites
@@ -75,13 +78,15 @@ export interface AstroProps extends FrameworkSiteProps {
75
78
  * @resource
76
79
  * @product Website
77
80
  */
78
- export const Astro = (id: string, props: AstroProps = {}) => {
79
- const output = props.astro?.output ?? "server";
80
- return frameworkSite(id, props, {
81
- name: "Astro",
82
- framework: ASTRO_FRAMEWORK_SPECIFIER,
83
- target: ASTRO_NODE_TARGET_SPECIFIER,
84
- options: { astro: { ...props.astro, output } },
85
- static: output === "static" ? staticConfigFromAssets(props.assets) : undefined,
86
- });
87
- };
81
+ export const Astro = (id: string, props: AstroProps = {}) =>
82
+ resolveAstroOutput({ rootDir: props.rootDir, output: props.astro?.output }).pipe(
83
+ Effect.flatMap((output) =>
84
+ frameworkSite(id, props, {
85
+ name: "Astro",
86
+ framework: ASTRO_FRAMEWORK_SPECIFIER,
87
+ target: ASTRO_NODE_TARGET_SPECIFIER,
88
+ options: { astro: { ...props.astro, output } },
89
+ static: output === "static" ? staticConfigFromAssets(props.assets) : undefined,
90
+ }),
91
+ ),
92
+ );
@@ -1,5 +1,7 @@
1
1
  import { AlchemyContext } from "alchemy/AlchemyContext";
2
2
  import type { MemoOptions } from "alchemy/Command/Memo";
3
+ import * as DNS from "alchemy/DNS/Adapter";
4
+ import type { Input } from "alchemy/Input";
3
5
  import * as Namespace from "alchemy/Namespace";
4
6
  import * as Output from "alchemy/Output";
5
7
  import { ProviderModePolicy } from "alchemy/ProviderMode";
@@ -18,7 +20,7 @@ import * as Path from "effect/Path";
18
20
  import * as Redacted from "effect/Redacted";
19
21
  import { App } from "../App.ts";
20
22
  import { Bucket } from "../Bucket.ts";
21
- import { Certificate } from "../Certificate.ts";
23
+ import { Certificate, type CertificateDnsRequirements } from "../Certificate.ts";
22
24
  import { IpAssignment } from "../IpAssignment.ts";
23
25
  import type { Providers } from "../Providers.ts";
24
26
  import { Service, type ServiceProps } from "../Service.ts";
@@ -33,6 +35,80 @@ export type Ref<T> = T | Effect.Effect<T, never, Providers>;
33
35
  export type { ServerDevProps, WebsiteAssetsProps, WebsiteNotFoundHandling };
34
36
  export { staticConfigFromAssets };
35
37
 
38
+ /**
39
+ * Custom hostname of a Fly website: the hostname itself, or
40
+ * `{ name, dns }` to also publish its DNS records.
41
+ */
42
+ export type WebsiteDomain =
43
+ | string
44
+ | {
45
+ /** Hostname, e.g. `www.example.com`. */
46
+ name: string;
47
+ /**
48
+ * DNS host that publishes the hostname's records (see
49
+ * [DNS Adapters](/infrastructure-as-code/dns-adapters)), e.g.
50
+ * `Cloudflare.DNS.Adapter()`: the ACME challenge `CNAME` Fly lists in
51
+ * the Certificate's `dnsRequirements`, and `A` / `AAAA` records at
52
+ * the site's addresses. Omitted: publish the records yourself.
53
+ */
54
+ dns?: DNS.DnsConfig;
55
+ };
56
+
57
+ /** The hostname of a {@link WebsiteDomain}. */
58
+ export const websiteDomainName = (domain: WebsiteDomain | undefined): string | undefined =>
59
+ domain === undefined || typeof domain === "string" ? domain : domain.name;
60
+
61
+ const acmeChallengeRecords = (
62
+ hostname: string,
63
+ requirements: CertificateDnsRequirements | undefined,
64
+ ): DNS.DnsRecord[] => {
65
+ const challenge = requirements?.acmeChallenge;
66
+ if (challenge?.target === undefined || challenge.target.length === 0) {
67
+ return [];
68
+ }
69
+ const expected = `_acme-challenge.${hostname}`;
70
+ const name =
71
+ challenge.name !== undefined &&
72
+ DNS.normalizeDnsName(challenge.name).endsWith(DNS.normalizeDnsName(hostname))
73
+ ? challenge.name
74
+ : expected;
75
+ return [{ name, type: "CNAME", value: challenge.target }];
76
+ };
77
+
78
+ /**
79
+ * Publish a Fly website's records through `domain.dns`: the Certificate's
80
+ * ACME challenge `CNAME` (`CertificateValidation`) and `A` / `AAAA`
81
+ * records at the site's shared IPv4 plus any IPv6 Fly lists for the
82
+ * hostname (`Domain`). No-op for a plain hostname or an object without
83
+ * `dns`.
84
+ */
85
+ export const publishWebsiteDomainDns = Effect.fn("Fly.Website.publishDomainDns")(function* (args: {
86
+ readonly domain: WebsiteDomain;
87
+ readonly certificate: Certificate;
88
+ readonly ip: IpAssignment;
89
+ }) {
90
+ if (typeof args.domain === "string" || args.domain.dns === undefined) {
91
+ return;
92
+ }
93
+ const dns = yield* DNS.resolve(args.domain.dns);
94
+ const name = args.domain.name;
95
+ yield* dns.records("CertificateValidation", {
96
+ records: Output.map(args.certificate.dnsRequirements, (requirements) =>
97
+ acmeChallengeRecords(name, requirements),
98
+ ) as unknown as Input<DNS.DnsRecord[]>,
99
+ });
100
+ yield* dns.alias("Domain", {
101
+ name,
102
+ target: {
103
+ ipv4: Output.map(args.ip.ip, (ip) => [ip]) as unknown as Input<string[]>,
104
+ ipv6: Output.map(
105
+ args.certificate.dnsRequirements,
106
+ (requirements) => requirements?.aaaa ?? [],
107
+ ) as unknown as Input<string[]>,
108
+ },
109
+ });
110
+ });
111
+
36
112
  /**
37
113
  * Props shared by every Fly framework website composite.
38
114
  */
@@ -78,9 +154,15 @@ export interface FrameworkSiteProps {
78
154
  dev?: ServerDevProps;
79
155
  /**
80
156
  * Optional custom hostname. Requests ACME (`Fly.Certificate`) on the
81
- * App. `url` becomes `https://<domain>` (existing DNS only for v1).
157
+ * App and `url` becomes `https://<domain>`.
158
+ *
159
+ * A string (or `{ name }`) leaves DNS to you. Set `dns` (e.g.
160
+ * `{ name: "www.example.com", dns: Cloudflare.DNS.Adapter() }`) to
161
+ * publish the ACME challenge `CNAME` and the `A` / `AAAA` records
162
+ * through that DNS host. See
163
+ * [DNS Adapters](/infrastructure-as-code/dns-adapters).
82
164
  */
83
- domain?: string;
165
+ domain?: WebsiteDomain;
84
166
  /**
85
167
  * User-defined tags. Accepted for API parity; Fly Services do not
86
168
  * surface resource tags.
@@ -327,16 +409,20 @@ const runFrameworkSite = Effect.fn("Fly.Website.FrameworkSite")(function* (
327
409
  : undefined,
328
410
  });
329
411
 
412
+ const domainName = websiteDomainName(props.domain);
330
413
  const certificate =
331
- props.domain !== undefined
414
+ domainName !== undefined
332
415
  ? yield* Certificate("Certificate", {
333
416
  app,
334
- hostname: props.domain,
417
+ hostname: domainName,
335
418
  kind: "acme",
336
419
  })
337
420
  : undefined;
421
+ if (props.domain !== undefined && certificate !== undefined) {
422
+ yield* publishWebsiteDomainDns({ domain: props.domain, certificate, ip });
423
+ }
338
424
 
339
- const url = props.domain !== undefined ? `https://${props.domain}` : app.url;
425
+ const url = domainName !== undefined ? `https://${domainName}` : app.url;
340
426
 
341
427
  return { url, app, service, ip, certificate };
342
428
  });
@@ -15,9 +15,12 @@ import { IpAssignment } from "../IpAssignment.ts";
15
15
  import { Service, type ServiceProps } from "../Service.ts";
16
16
  import {
17
17
  type FrameworkSite,
18
+ publishWebsiteDomainDns,
18
19
  type Ref,
19
20
  type WebsiteAssetsProps,
21
+ type WebsiteDomain,
20
22
  staticConfigFromAssets,
23
+ websiteDomainName,
21
24
  } from "./FrameworkSite.ts";
22
25
 
23
26
  const DEFAULT_PORT = 3000;
@@ -69,9 +72,16 @@ export interface StaticSiteProps {
69
72
  */
70
73
  app?: Ref<App>;
71
74
  /**
72
- * Optional custom hostname. Requests ACME (`Fly.Certificate`) on the App.
75
+ * Optional custom hostname. Requests ACME (`Fly.Certificate`) on the App
76
+ * and `url` becomes `https://<domain>`.
77
+ *
78
+ * A string (or `{ name }`) leaves DNS to you. Set `dns` (e.g.
79
+ * `{ name: "www.example.com", dns: Cloudflare.DNS.Adapter() }`) to
80
+ * publish the ACME challenge `CNAME` and the `A` / `AAAA` records
81
+ * through that DNS host. See
82
+ * [DNS Adapters](/infrastructure-as-code/dns-adapters).
73
83
  */
74
- domain?: string;
84
+ domain?: WebsiteDomain;
75
85
  /**
76
86
  * User-defined tags. Accepted for API parity; Fly Services do not
77
87
  * surface resource tags.
@@ -262,14 +272,24 @@ export const StaticSite = (id: string, props: StaticSiteProps) =>
262
272
  ],
263
273
  });
264
274
 
275
+ const domainName = websiteDomainName(props.domain);
265
276
  const certificate =
266
- props.domain !== undefined
267
- ? yield* Certificate("Certificate", { app, hostname: props.domain, kind: "acme" }).pipe(
268
- Namespace.push(id),
269
- )
277
+ domainName !== undefined
278
+ ? yield* Certificate("Certificate", {
279
+ app,
280
+ hostname: domainName,
281
+ kind: "acme",
282
+ }).pipe(Namespace.push(id))
270
283
  : undefined;
284
+ if (props.domain !== undefined && certificate !== undefined) {
285
+ yield* publishWebsiteDomainDns({
286
+ domain: props.domain,
287
+ certificate,
288
+ ip,
289
+ }).pipe(Namespace.push(id));
290
+ }
271
291
 
272
- const url = props.domain !== undefined ? `https://${props.domain}` : app.url;
292
+ const url = domainName !== undefined ? `https://${domainName}` : app.url;
273
293
 
274
294
  return { url, app, service, ip, certificate };
275
295
  }).pipe(Effect.orDie);
@@ -77,6 +77,15 @@ export interface ViteProps extends FrameworkSiteProps {
77
77
  * });
78
78
  * ```
79
79
  *
80
+ * **Example:** Publish DNS in Cloudflare
81
+ * ```typescript
82
+ * // The ACME challenge CNAME and A/AAAA records are published in the
83
+ * // Cloudflare zone. Requires Cloudflare.providers() in the stack.
84
+ * const site = yield* Fly.Website.Vite("Web", {
85
+ * domain: { name: "app.example.com", dns: Cloudflare.DNS.Adapter() },
86
+ * });
87
+ * ```
88
+ *
80
89
  * ### Build Configuration
81
90
  * **Example:** Custom Output Directory and Base Path
82
91
  * ```typescript
@@ -43,7 +43,7 @@ export const certificatesWriteClient = (
43
43
  return yield* authorize(
44
44
  machines
45
45
  .createAppAcmeCertificate({ app_name: yield* appName, hostname })
46
- .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined))),
46
+ .pipe(Effect.catchTag("Conflict", () => Effect.undefined)),
47
47
  );
48
48
  }),
49
49
  upload: Effect.fn("Fly.Certificates.upload")(function* (request) {
@@ -78,7 +78,7 @@ export const certificatesWriteClient = (
78
78
  return yield* authorize(
79
79
  machines
80
80
  .getAppCertificate({ app_name: yield* appName, hostname })
81
- .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))),
81
+ .pipe(Effect.catchTag("NotFound", () => Effect.undefined)),
82
82
  );
83
83
  }),
84
84
  remove: Effect.fn("Fly.Certificates.remove")(function* (hostname) {