1import type { Config } from "@distilled.cloud/gcp/Credentials";
2import { Credentials } from "@distilled.cloud/gcp/Credentials";
3import * as Region from "@distilled.cloud/gcp/Region";
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 Option from "effect/Option";
9import * as Redacted from "effect/Redacted";
10import { DEFAULT_GCP_REGION } from "./AuthProvider.ts";
11
12export class GcpProjectMissing extends Data.TaggedError("GCP.ProjectMissing")<{
13 message: string;
14}> {}
15
16/**
17 * Fully-resolved GCP environment for a stack.
18 *
19 * Distilled GCP authenticates with a bearer access token; Alchemy always
20 * pairs it with a project id so lifecycle operations can fill
21 * `projects/{project}/...` resource names.
22 */
23export interface GcpEnvironmentShape {
24 accessToken: Redacted.Redacted<string>;
25 project: string;
26 /**
27 * Default region for regional resources created without an explicit
28 * location: a `GCP.Region` override, else the profile / environment
29 * region, else `us-central1`.
30 */
31 region: string;
32}
33
34export class GcpEnvironment extends Context.Service<
35 GcpEnvironment,
36 Effect.Effect<GcpEnvironmentShape, GcpProjectMissing>
37>()("GCP::Environment") {
38 static current = GcpEnvironment.use((env) => env);
39 readonly kind = "Environment" as const;
40}
41
42const requireProject = (
43 config: Config,
44 region: string,
45): Effect.Effect<GcpEnvironmentShape, GcpProjectMissing> => {
46 if (!config.project) {
47 return Effect.fail(
48 new GcpProjectMissing({
49 message:
50 "GCP project id is required. Set GOOGLE_PROJECT_ID or configure the Alchemy GCP profile.",
51 }),
52 );
53 }
54 return Effect.succeed({
55 accessToken: config.accessToken,
56 project: config.project,
57 region,
58 });
59};
60
61/**
62 * Build a `GcpEnvironment` layer from the distilled `Credentials`
63 * service. Provide this after `Credentials.fromAuthProvider()`.
64 */
65export const fromCredentials = () =>
66 Layer.effect(
67 GcpEnvironment,
68 Effect.gen(function* () {
69 const credentials = yield* Credentials;
70 // Like AWS, the region override is part of the providers layer:
71 // capture it when the layer is built, since provider lifecycles run
72 // in the providers' context, not the caller's.
73 const override = yield* Effect.serviceOption(Region.Region);
74 return Effect.gen(function* () {
75 const config = yield* credentials;
76 const region =
77 (Option.isSome(override) ? yield* override.value : undefined) ??
78 config.region ??
79 DEFAULT_GCP_REGION;
80 return yield* requireProject(config, region);
81 });
82 }),
83 );