| 1 | import { |
| 2 | apiKeyCredentials, |
| 3 | apiTokenCredentials, |
| 4 | Credentials, |
| 5 | oauthCredentials, |
| 6 | type ResolvedCredentials, |
| 7 | } from "@distilled.cloud/cloudflare/Credentials"; |
| 8 | import { ConfigError } from "@distilled.cloud/core/errors"; |
| 9 | import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; |
| 11 | import * as Match from "effect/Match"; |
| 12 | import * as Redacted from "effect/Redacted"; |
| 13 | import * as CredentialsCache from "../Auth/CredentialsCache.ts"; |
| 14 | import { deferUntilFirstUse, resolveProviderConfig } from "../Auth/Resolve.ts"; |
| 15 | import { |
| 16 | CLOUDFLARE_AUTH_PROVIDER_NAME, |
| 17 | type CloudflareAuthConfig, |
| 18 | type CloudflareResolvedCredentials, |
| 19 | } from "./Auth/AuthConfig.ts"; |
| 20 | |
| 21 | export { Credentials, fromEnv } from "@distilled.cloud/cloudflare/Credentials"; |
| 22 | |
| 23 | declare module "@distilled.cloud/cloudflare/Credentials" { |
| 24 | interface Credentials { |
| 25 | readonly kind: "Credentials"; |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | /** |
| 30 | * Memoize a credentials-resolution effect until shortly before the resolved |
| 31 | * credentials expire — see {@link CredentialsCache.cacheUntilExpiry} for the |
| 32 | * caching rules. Non-OAuth credentials (API token / global key) never expire |
| 33 | * and cache forever. |
| 34 | */ |
| 35 | export const cacheUntilExpiry = <E>( |
| 36 | resolve: Effect.Effect<ResolvedCredentials, E>, |
| 37 | ) => |
| 38 | CredentialsCache.cacheUntilExpiry(resolve, (credentials) => |
| 39 | credentials.type === "oauth" ? credentials.expiresAt : undefined, |
| 40 | ); |
| 41 | |
| 42 | /** |
| 43 | * Build a `Credentials` layer that resolves Cloudflare credentials via the |
| 44 | * Alchemy AuthProvider using the configured profile (defaults to "default", |
| 45 | * selected by the current Alchemy profile). |
| 46 | */ |
| 47 | export const fromAuthProvider = () => |
| 48 | Layer.effect( |
| 49 | Credentials, |
| 50 | Effect.gen(function* () { |
| 51 | // Defer both profile lookup and credential resolution. Local-only dev |
| 52 | // builds this layer too, but never evaluates its credential recipe. |
| 53 | const resolve = yield* resolveProviderConfig< |
| 54 | CloudflareAuthConfig, |
| 55 | CloudflareResolvedCredentials |
| 56 | >(CLOUDFLARE_AUTH_PROVIDER_NAME).pipe( |
| 57 | Effect.flatMap(({ resolve }) => resolve), |
| 58 | Effect.map((creds) => |
| 59 | Match.value(creds).pipe( |
| 60 | Match.when({ type: "apiToken" }, (c) => |
| 61 | apiTokenCredentials({ |
| 62 | apiToken: Redacted.value(c.apiToken), |
| 63 | }), |
| 64 | ), |
| 65 | Match.when({ type: "apiKey" }, (c) => |
| 66 | apiKeyCredentials({ |
| 67 | apiKey: Redacted.value(c.apiKey), |
| 68 | email: Redacted.value(c.email), |
| 69 | }), |
| 70 | ), |
| 71 | Match.when({ type: "oauth" }, (c) => |
| 72 | oauthCredentials({ |
| 73 | accessToken: Redacted.value(c.accessToken), |
| 74 | expiresAt: c.expires, |
| 75 | }), |
| 76 | ), |
| 77 | Match.exhaustive, |
| 78 | ), |
| 79 | ), |
| 80 | Effect.mapError( |
| 81 | (e) => |
| 82 | new ConfigError({ |
| 83 | message: `Failed to resolve Cloudflare credentials: ${e.message}`, |
| 84 | }), |
| 85 | ), |
| 86 | deferUntilFirstUse, |
| 87 | ); |
| 88 | |
| 89 | // `auth.read` refreshes and persists expired OAuth tokens when it is |
| 90 | // re-run, so expiry-aware caching (instead of caching the first |
| 91 | // resolution forever) is what keeps long-lived dev sessions |
| 92 | // authenticated across the ~1h access-token lifetime. |
| 93 | return yield* cacheUntilExpiry(resolve); |
| 94 | }), |
| 95 | ); |