1import type { Config } from "@distilled.cloud/fly-io";
2import { Credentials } from "@distilled.cloud/fly-io";
3import * as machines from "@distilled.cloud/fly-io/machines";
4import * as Context from "effect/Context";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as HttpClient from "effect/http/HttpClient";
9
10export class FlyOrgNotFound extends Data.TaggedError("Fly.OrgNotFound")<{
11 message: string;
12}> {}
13
14/**
15 * Fully-resolved Fly.io environment for a stack.
16 *
17 * `{ apiKey, apiBaseUrl }` comes from distilled `Credentials`. `orgSlug` is
18 * the current token's organization (`getCurrentToken` → `tokens[0].org_slug`),
19 * cached for the process. Resolve it inside lifecycle operations with
20 * `FlyEnvironment.current`.
21 */
22export type FlyEnvironmentShape = Config & {
23 readonly orgSlug: string;
24};
25
26export class FlyEnvironment extends Context.Service<
27 FlyEnvironment,
28 Effect.Effect<FlyEnvironmentShape>
29>()("Fly::Environment") {
30 static current = FlyEnvironment.use((env) => env);
31 readonly kind = "Environment" as const;
32}
33
34/**
35 * Discover the current token's organization slug via `getCurrentToken`.
36 * Used by {@link fromCredentials} and by {@link Catalog.currentOrgSlug}.
37 */
38export const resolveOrgSlug = Effect.fn(function* () {
39 const info = yield* machines.getCurrentToken({});
40 const orgSlug = info.tokens?.[0]?.org_slug;
41 if (orgSlug === undefined || orgSlug.length === 0) {
42 return yield* new FlyOrgNotFound({
43 message: "Fly current token did not include an organization slug.",
44 });
45 }
46 return orgSlug;
47});
48
49/**
50 * Build a `FlyEnvironment` layer from the distilled `Credentials`
51 * service. Provide this after `Credentials.fromAuthProvider()`.
52 *
53 * `orgSlug` is resolved from `getCurrentToken` and cached so App.list /
54 * Catalog do not re-hit `/tokens/current` on every call.
55 */
56export const fromCredentials = () =>
57 Layer.effect(
58 FlyEnvironment,
59 Effect.gen(function* () {
60 const creds = yield* Credentials;
61 const http = yield* HttpClient.HttpClient;
62 return yield* creds.pipe(
63 Effect.flatMap((resolved) =>
64 resolveOrgSlug().pipe(
65 Effect.provide(
66 Layer.mergeAll(
67 Layer.succeed(Credentials, creds),
68 Layer.succeed(HttpClient.HttpClient, http),
69 ),
70 ),
71 Effect.map((orgSlug) => ({
72 ...resolved,
73 orgSlug,
74 })),
75 ),
76 ),
77 Effect.orDie,
78 Effect.cached,
79 );
80 }),
81 );