import { Credentials } from "@distilled.cloud/cloudflare/Credentials"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; import type * as HttpClient from "effect/unstable/http/HttpClient"; import * as Binding from "../../Binding.ts"; import type { RuntimeContext } from "../../RuntimeContext.ts"; import { AccountApiToken } from "../ApiToken/AccountApiToken.ts"; import type { PermissionGroupRef } from "../ApiToken/Common.ts"; import { CloudflareEnvironment } from "../CloudflareEnvironment.ts"; import { authorizeWith } from "../HttpClientUtils.ts"; import { isWorker } from "../Workers/Worker.ts"; import { type FindIdentityProviderOptions, GetIdentityProvider, } from "./GetIdentityProvider.ts"; import { findFirst, toAttributes } from "./IdentityProviderLookup.ts"; const PERMISSION_GROUPS: PermissionGroupRef[] = [ "Access: Organizations, Identity Providers, and Groups Read", ]; /** * Injectable auth for the lookup client: `authorize` provides * `Credentials` + `HttpClient` to the raw zero-trust list op, so the * client is agnostic to whether creds come from a bound scoped token * (Worker host) or the ambient plan-time credentials (data source). */ interface AccessIdpAuth { authorize: ( eff: Effect.Effect, ) => Effect.Effect; accountId: Effect.Effect; } /** * HTTP implementation of the {@link GetIdentityProvider} binding. * * When bound inside a Worker, it mints a scoped {@link AccountApiToken} * with the `Access: Organizations, Identity Providers, and Groups Read` * permission and binds its outputs into the Worker so runtime code can * call the Access API. Hostless — the plan-time `execute` data source * ({@link getIdentityProvider}) or an Action — it runs with the ambient * current credentials instead; no token is minted. */ export const GetIdentityProviderHttp = Layer.effect( GetIdentityProvider, Effect.gen(function* () { const Token = yield* AccountApiToken; const env = yield* CloudflareEnvironment; const ambientCredentials = yield* Effect.serviceOption(Credentials).pipe( Effect.map(Option.getOrUndefined), ); return Effect.fn(function* (options: FindIdentityProviderOptions) { // Resolved per call, NOT at layer build: the callable runs in the // caller's context, so a Worker host is visible here during the // Worker's init phase (plan and runtime) even though the layer // itself may have been built (and memoized) hostless by // `providers()`. Hostless callers — the plan-time `execute` data // source — resolve `undefined`. const host = yield* Binding.Host; if (isWorker(host)) { // Worker-hosted: mint the scoped token, attach the read policy // (a no-op once deployed), and bind the token's outputs into the // Worker so the client can authenticate at runtime. const token = yield* Token(`${host.LogicalId}Token`); if (!globalThis.__ALCHEMY_RUNTIME__) { const { accountId } = yield* env; yield* token.bind("Cloudflare.Access.GetIdentityProvider", { policies: [ { effect: "allow", permissionGroups: PERMISSION_GROUPS, resources: { [`com.cloudflare.api.account.${accountId}`]: "*", }, }, ], }); } const bound = { value: yield* token.value, accountId: yield* token.accountId, }; return makeGetIdentityProviderClient( { authorize: authorizeWith(bound), accountId: bound.accountId }, options, ); } // Hostless — plan-time data source: run with the ambient credentials // (refresh-aware, provided by the stack's providers layer). if (ambientCredentials === undefined) { return yield* Effect.die( new Error( "Cloudflare.Access.GetIdentityProvider requires either a Worker " + "host (runtime binding) or ambient Cloudflare credentials " + "(plan-time data source).", ), ); } const { accountId } = yield* env; return makeGetIdentityProviderClient( { authorize: (eff) => eff.pipe( Effect.provideService(Credentials, ambientCredentials), Effect.provide(FetchHttpClient.layer), ), accountId: Effect.succeed(accountId), }, options, ); }); }), ); const makeGetIdentityProviderClient = ( auth: AccessIdpAuth, options: FindIdentityProviderOptions, ) => Effect.fn("Cloudflare.Access.GetIdentityProvider")(function* () { const accountId = yield* auth.accountId; const match = yield* auth.authorize( findFirst( options.zoneId, accountId, (idp) => (options.name === undefined || idp.name === options.name) && (options.type === undefined || idp.type === options.type), ), ); return match ? toAttributes(match, options.zoneId, accountId, undefined) : undefined; });