1import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Redacted from "effect/Redacted";
5import * as Stream from "effect/Stream";
6
7import type {
8 IdentityProviderAttributes,
9 IdentityProviderType,
10} from "./IdentityProvider.ts";
11
12// Shared observation scaffolding for the IdentityProvider resource provider
13// and the GetIdentityProvider data source. NOT exported from `index.ts`.
14
15export interface ObservedIdp {
16 readonly id?: string | null;
17 readonly name: string;
18 readonly type: string;
19 readonly scimConfig?: {
20 readonly enabled?: boolean | null;
21 readonly identityUpdateBehavior?: string | null;
22 readonly scimBaseUrl?: string | null;
23 readonly seatDeprovision?: boolean | null;
24 readonly secret?: string | null;
25 readonly userDeprovision?: boolean | null;
26 } | null;
27}
28
29/**
30 * IdP types that can exist at most once per scope (the managed Cloudflare
31 * WARP login method and the built-in one-time PIN). They are located by
32 * type — their display name is user-irrelevant and often empty (`""` for
33 * the dashboard-provisioned `cloudflare` IdP).
34 */
35export const isSingletonType = (
36 type: IdentityProviderType | undefined,
37): type is "onetimepin" | "cloudflare" =>
38 type === "onetimepin" || type === "cloudflare";
39
40/**
41 * Read an identity provider by id, mapping "gone"
42 * (`AccessIdentityProviderNotFound`, Cloudflare error code 12135 —
43 * `access.api.error.not_found`) to `undefined`. Zone-level when `zoneId`
44 * is set, account-level otherwise.
45 */
46export const getIdp = (
47 zoneId: string | undefined,
48 accountId: string,
49 identityProviderId: string,
50) =>
51 (zoneId !== undefined
52 ? zeroTrust.getIdentityProviderForZone({ zoneId, identityProviderId })
53 : zeroTrust.getIdentityProviderForAccount({ accountId, identityProviderId })
54 ).pipe(
55 Effect.map((idp): ObservedIdp | undefined => idp as ObservedIdp),
56 Effect.catchTag("AccessIdentityProviderNotFound", () =>
57 Effect.succeed(undefined),
58 ),
59 );
60
61/**
62 * Find the first identity provider in the scope matching a predicate.
63 */
64export const findFirst = (
65 zoneId: string | undefined,
66 accountId: string,
67 predicate: (idp: { name: string; type: string }) => boolean,
68) =>
69 (zoneId !== undefined
70 ? zeroTrust.listIdentityProvidersForZone.items({ zoneId })
71 : zeroTrust.listIdentityProvidersForAccount.items({ accountId })
72 ).pipe(
73 Stream.filter(predicate),
74 Stream.runHead,
75 Effect.map(Option.getOrUndefined),
76 Effect.map(
77 (idp): ObservedIdp | undefined => idp as ObservedIdp | undefined,
78 ),
79 );
80
81/**
82 * Find an identity provider by exact name within the scope. Names are
83 * not unique on Cloudflare's side; pick the first match.
84 */
85export const findByName = (
86 zoneId: string | undefined,
87 accountId: string,
88 name: string,
89) => findFirst(zoneId, accountId, (idp) => idp.name === name);
90
91/**
92 * Find an identity provider by type within the scope — the identity of
93 * the {@link isSingletonType singleton} types, whose display name carries
94 * no information.
95 */
96export const findByType = (
97 zoneId: string | undefined,
98 accountId: string,
99 type: IdentityProviderType,
100) => findFirst(zoneId, accountId, (idp) => idp.type === type);
101
102export const toAttributes = (
103 idp: ObservedIdp,
104 zoneId: string | undefined,
105 accountId: string,
106 priorSecret: Redacted.Redacted<string> | undefined,
107): IdentityProviderAttributes => ({
108 identityProviderId: idp.id ?? "",
109 accountId,
110 zoneId,
111 name: idp.name,
112 type: idp.type as IdentityProviderType,
113 scimBaseUrl: idp.scimConfig?.scimBaseUrl ?? undefined,
114 // The SCIM secret is returned once when SCIM is enabled; afterwards the
115 // API masks it — carry the prior value forward.
116 scimSecret: idp.scimConfig?.secret
117 ? Redacted.make(idp.scimConfig.secret)
118 : priorSecret,
119 scimEnabled: idp.scimConfig?.enabled ?? false,
120});