1import {
2 apiKeyCredentials,
3 apiTokenCredentials,
4 Credentials,
5 oauthCredentials,
6 type ResolvedCredentials,
7} from "@distilled.cloud/cloudflare/Credentials";
8import { ConfigError } from "@distilled.cloud/core/errors";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Match from "effect/Match";
12import * as Redacted from "effect/Redacted";
13import * as CredentialsCache from "../Auth/CredentialsCache.ts";
14import { deferUntilFirstUse, resolveProviderConfig } from "../Auth/Resolve.ts";
15import {
16 CLOUDFLARE_AUTH_PROVIDER_NAME,
17 type CloudflareAuthConfig,
18 type CloudflareResolvedCredentials,
19} from "./Auth/AuthConfig.ts";
20
21export { Credentials, fromEnv } from "@distilled.cloud/cloudflare/Credentials";
22
23declare 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 */
35export 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 */
47export 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 );