1import { Credentials } from "@distilled.cloud/cloudflare/Credentials";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as FetchHttpClient from "effect/http/FetchHttpClient";
6import type * as HttpClient from "effect/http/HttpClient";
7import * as Binding from "../../Binding.ts";
8import type { RuntimeContext } from "../../RuntimeContext.ts";
9import { AccountApiToken } from "../ApiToken/AccountApiToken.ts";
10import type { PermissionGroupRef } from "../ApiToken/Common.ts";
11import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
12import { authorizeWith } from "../HttpClientUtils.ts";
13import { isWorker } from "../Workers/Worker.ts";
14import {
15 type FindIdentityProviderOptions,
16 GetIdentityProvider,
17} from "./GetIdentityProvider.ts";
18import { findFirst, toAttributes } from "./IdentityProviderLookup.ts";
19
20const PERMISSION_GROUPS: PermissionGroupRef[] = [
21 "Access: Organizations, Identity Providers, and Groups Read",
22];
23
24/**
25 * Injectable auth for the lookup client: `authorize` provides
26 * `Credentials` + `HttpClient` to the raw zero-trust list op, so the
27 * client is agnostic to whether creds come from a bound scoped token
28 * (Worker host) or the ambient plan-time credentials (data source).
29 */
30interface AccessIdpAuth {
31 authorize: <A, E>(
32 eff: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
33 ) => Effect.Effect<A, E, RuntimeContext>;
34 accountId: Effect.Effect<string>;
35}
36
37/**
38 * HTTP implementation of the {@link GetIdentityProvider} binding.
39 *
40 * When bound inside a Worker, it mints a scoped {@link AccountApiToken}
41 * with the `Access: Organizations, Identity Providers, and Groups Read`
42 * permission and binds its outputs into the Worker so runtime code can
43 * call the Access API. Hostless — the plan-time `execute` data source
44 * ({@link getIdentityProvider}) or an Action — it runs with the ambient
45 * current credentials instead; no token is minted.
46 */
47export const GetIdentityProviderHttp = Layer.effect(
48 GetIdentityProvider,
49 Effect.gen(function* () {
50 const Token = yield* AccountApiToken;
51 const env = yield* CloudflareEnvironment;
52 const ambientCredentials = yield* Effect.serviceOption(Credentials).pipe(
53 Effect.map(Option.getOrUndefined),
54 );
55
56 return Effect.fn(function* (options: FindIdentityProviderOptions) {
57 // Resolved per call, NOT at layer build: the callable runs in the
58 // caller's context, so a Worker host is visible here during the
59 // Worker's init phase (plan and runtime) even though the layer
60 // itself may have been built (and memoized) hostless by
61 // `providers()`. Hostless callers — the plan-time `execute` data
62 // source — resolve `undefined`.
63 const host = yield* Binding.Host;
64 if (isWorker(host)) {
65 // Worker-hosted: mint the scoped token, attach the read policy
66 // (a no-op once deployed), and bind the token's outputs into the
67 // Worker so the client can authenticate at runtime.
68 const token = yield* Token(`${host.LogicalId}Token`);
69 if (!globalThis.__ALCHEMY_RUNTIME__) {
70 const { accountId } = yield* env;
71 yield* token.bind("Cloudflare.Access.GetIdentityProvider", {
72 policies: [
73 {
74 effect: "allow",
75 permissionGroups: PERMISSION_GROUPS,
76 resources: {
77 [`com.cloudflare.api.account.${accountId}`]: "*",
78 },
79 },
80 ],
81 });
82 }
83 const bound = {
84 value: yield* token.value,
85 accountId: yield* token.accountId,
86 };
87 return makeGetIdentityProviderClient(
88 { authorize: authorizeWith(bound), accountId: bound.accountId },
89 options,
90 );
91 }
92
93 // Hostless — plan-time data source: run with the ambient credentials
94 // (refresh-aware, provided by the stack's providers layer).
95 if (ambientCredentials === undefined) {
96 return yield* Effect.die(
97 new Error(
98 "Cloudflare.Access.GetIdentityProvider requires either a Worker " +
99 "host (runtime binding) or ambient Cloudflare credentials " +
100 "(plan-time data source).",
101 ),
102 );
103 }
104 const { accountId } = yield* env;
105 return makeGetIdentityProviderClient(
106 {
107 authorize: (eff) =>
108 eff.pipe(
109 Effect.provideService(Credentials, ambientCredentials),
110 Effect.provide(FetchHttpClient.layer),
111 ),
112 accountId: Effect.succeed(accountId),
113 },
114 options,
115 );
116 });
117 }),
118);
119
120const makeGetIdentityProviderClient = (
121 auth: AccessIdpAuth,
122 options: FindIdentityProviderOptions,
123) =>
124 Effect.fn("Cloudflare.Access.GetIdentityProvider")(function* () {
125 const accountId = yield* auth.accountId;
126 const match = yield* auth.authorize(
127 findFirst(
128 options.zoneId,
129 accountId,
130 (idp) =>
131 (options.name === undefined || idp.name === options.name) &&
132 (options.type === undefined || idp.type === options.type),
133 ),
134 );
135 return match
136 ? toAttributes(match, options.zoneId, accountId, undefined)
137 : undefined;
138 });