| 1 | import type { |
| 2 | CredentialsError, |
| 3 | ResolvedCredentials, |
| 4 | } from "@distilled.cloud/aws/Credentials"; |
| 5 | import * as Config from "effect/Config"; |
| 6 | import * as Context from "effect/Context"; |
| 7 | import * as Data from "effect/Data"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Layer from "effect/Layer"; |
| 10 | import { |
| 11 | deferUntilFirstUse, |
| 12 | orDieCredentialsUnavailable, |
| 13 | resolveProviderConfig, |
| 14 | } from "../Auth/Resolve.ts"; |
| 15 | import { |
| 16 | AWS_AUTH_PROVIDER_NAME, |
| 17 | LOCAL_ACCOUNT_ID, |
| 18 | type AwsAuthConfig, |
| 19 | type AwsResolvedCredentials, |
| 20 | } from "./AuthProvider.ts"; |
| 21 | |
| 22 | export const AWS_PROFILE = Config.String("AWS_PROFILE").pipe( |
| 23 | Config.withDefault("default"), |
| 24 | ); |
| 25 | |
| 26 | export const AWS_REGION = Config.String("AWS_REGION"); |
| 27 | export const AWS_ACCOUNT_ID = Config.String("AWS_ACCOUNT_ID"); |
| 28 | export const AWS_ACCESS_KEY_ID = Config.String("AWS_ACCESS_KEY_ID"); |
| 29 | export const AWS_SECRET_ACCESS_KEY = Config.Redacted("AWS_SECRET_ACCESS_KEY"); |
| 30 | export const AWS_SESSION_TOKEN = Config.Redacted("AWS_SESSION_TOKEN"); |
| 31 | |
| 32 | export type AccountID = string; |
| 33 | export type RegionID = string; |
| 34 | |
| 35 | export class FailedToGetAccount extends Data.TaggedError( |
| 36 | "AWS::Environment::FailedToGetAccount", |
| 37 | )<{ |
| 38 | message: string; |
| 39 | cause: Error; |
| 40 | }> {} |
| 41 | |
| 42 | /** |
| 43 | * Fully-resolved AWS environment for a stack. Mirrors `CloudflareEnvironment`: |
| 44 | * one Context.Service that holds account, region, credentials, endpoint, and |
| 45 | * (optionally) the SSO profile name. |
| 46 | * |
| 47 | * `credentials` is held as an Effect so callers can refresh on each access |
| 48 | * (SSO sessions expire). The Effect itself is constructed once when this |
| 49 | * service is built; resolving it lazily preserves @distilled.cloud/aws's |
| 50 | * existing `Credentials` semantics. |
| 51 | */ |
| 52 | export interface AWSEnvironmentShape { |
| 53 | accountId: AccountID; |
| 54 | region: RegionID; |
| 55 | credentials: Effect.Effect<ResolvedCredentials, CredentialsError>; |
| 56 | endpoint?: string; |
| 57 | profile?: string; |
| 58 | } |
| 59 | |
| 60 | export class AWSEnvironment extends Context.Service< |
| 61 | AWSEnvironment, |
| 62 | Effect.Effect<AWSEnvironmentShape> |
| 63 | >()("AWS::Environment") { |
| 64 | static current = AWSEnvironment.use((env) => env); |
| 65 | /** |
| 66 | * Whether this environment is the floci emulator |
| 67 | * (dummy account {@link LOCAL_ACCOUNT_ID}). A set `endpoint` is not |
| 68 | * enough: `AWS_ENDPOINT_URL` and explicit endpoint overrides also |
| 69 | * populate it on real-account credentials. |
| 70 | */ |
| 71 | static isLocalEmulator = Effect.map( |
| 72 | AWSEnvironment.current, |
| 73 | (env) => env.accountId === LOCAL_ACCOUNT_ID, |
| 74 | ); |
| 75 | readonly kind = "Environment" as const; |
| 76 | } |
| 77 | |
| 78 | /** @see {@link AWSEnvironment.isLocalEmulator} */ |
| 79 | export const isLocalEmulator = AWSEnvironment.isLocalEmulator; |
| 80 | |
| 81 | export const Default = Layer.effect( |
| 82 | AWSEnvironment, |
| 83 | Effect.gen(function* () { |
| 84 | // Provider layers are built on every run — before any profile may be |
| 85 | // configured — so nothing may resolve at construction. A dev run with |
| 86 | // zero AWS credentials builds this layer too (the ambient environment |
| 87 | // is the emulator; only `Alchemy.remote()` rows ever need it), so the |
| 88 | // profile/CI precedence is captured here and evaluated on first use, |
| 89 | // exactly once. |
| 90 | const resolve = yield* resolveProviderConfig< |
| 91 | AwsAuthConfig, |
| 92 | AwsResolvedCredentials |
| 93 | >(AWS_AUTH_PROVIDER_NAME).pipe( |
| 94 | Effect.flatMap(({ resolve }) => resolve), |
| 95 | deferUntilFirstUse, |
| 96 | ); |
| 97 | return yield* resolve.pipe( |
| 98 | orDieCredentialsUnavailable(AWS_AUTH_PROVIDER_NAME), |
| 99 | Effect.cached, |
| 100 | ); |
| 101 | }), |
| 102 | ).pipe(Layer.orDie); |