1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as emailSecurity from "@distilled.cloud/cloudflare/email-security";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as Stream from "effect/Stream";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// Cloudflare Email Security (Area 1) is an enterprise add-on — the standard
20// testing account has no entitlement and every settings call fails with the
21// typed `EmailSecurityNotEntitled` error ("Email Security API access is not
22// available in the current subscription"). The full lifecycle tests are
23// gated behind an entitled account flagged via env.
24const entitled = !!process.env.CLOUDFLARE_EMAIL_SECURITY;
25
26// Entitlement and reachability are different axes. `entitled` says the
27// account has bought the add-on; `reachable` says the *credential* carries
28// the Email Security scope at all. Cloudflare OAuth carries neither, and an
29// unreachable credential is refused at the edge with a bare `Forbidden`
30// before any entitlement check happens.
31const reachable = !!process.env.CLOUDFLARE_TEST_EMAIL_SECURITY;
32
33// A freshly minted scoped token propagates eventually-consistently across
34// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
35const forbiddenRetrySchedule = Schedule.exponential("500 millis");
36
37const pattern = "alchemy-allow-policy@alchemy-test-2.us";
38
39const findByPattern = (accountId: string) =>
40 emailSecurity.listSettingAllowPolicies.items({ accountId, pattern }).pipe(
41 Stream.runCollect,
42 Effect.map((chunk) => Array.from(chunk).find((p) => p.pattern === pattern)),
43 Effect.retry({
44 while: (e) => e._tag === "Forbidden",
45 schedule: forbiddenRetrySchedule,
46 times: 8,
47 }),
48 );
49
50// Unentitlement probe — pins the typed EmailSecurityNotEntitled rejection and
51// skips on entitled accounts, where the list call would succeed.
52//
53// Gated: the probe's premise is that the credential can *reach* Email
54// Security and be told the account is not entitled. A credential lacking
55// the Email Security scope outright (Cloudflare OAuth) never gets that far
56// and is refused at the edge instead:
57//
58// Forbidden: Authentication error
59// at matchTypedError (submodules/distilled/packages/core/src/protocol-http.ts)
60//
61// Set `CLOUDFLARE_TEST_EMAIL_SECURITY=1` with an API-token credential
62// carrying the Email Security read scope to run it.
63test.provider.skipIf(entitled || !reachable)(
64 "surfaces the typed EmailSecurityNotEntitled error on unentitled accounts",
65 (stack) =>
66 Effect.gen(function* () {
67 const { accountId } = yield* yield* CloudflareEnvironment;
68
69 yield* stack.destroy();
70
71 // The testing account lacks the Email Security entitlement — the
72 // distilled list call must fail with the typed entitlement tag.
73 const error = yield* emailSecurity.listSettingAllowPolicies
74 .items({ accountId })
75 .pipe(
76 Stream.runCollect,
77 Effect.retry({
78 while: (e) => e._tag === "Forbidden",
79 schedule: forbiddenRetrySchedule,
80 times: 8,
81 }),
82 Effect.flip,
83 );
84 expect(error._tag).toEqual("EmailSecurityNotEntitled");
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 {
89 tags: ["provider:cloudflare", "provider:cloudflare:email", "live"],
90 timeout: 120_000,
91 },
92);
93
94// Read-only list assertion that runs on every account. On unentitled
95// accounts the provider's list() maps the typed EmailSecurityNotEntitled
96// rejection to a well-typed empty array; entitled accounts return real rows.
97test.provider(
98 "list enumerates allow policies (read-only)",
99 (stack) =>
100 Effect.gen(function* () {
101 yield* stack.destroy();
102
103 const provider = yield* Provider.findProvider(
104 Cloudflare.Email.AllowPolicy,
105 );
106 const all = yield* provider.list();
107 expect(Array.isArray(all)).toBe(true);
108
109 yield* stack.destroy();
110 }).pipe(logLevel),
111 {
112 tags: ["provider:cloudflare", "provider:cloudflare:email", "live"],
113 timeout: 120_000,
114 },
115);
116
117// Requires the Email Security (Area 1) enterprise add-on — unentitled accounts fail
118// with the typed EmailSecurityNotEntitled. Unlock with CLOUDFLARE_EMAIL_SECURITY=1.
119test.provider.skipIf(!entitled)(
120 "list includes the deployed allow policy",
121 (stack) =>
122 Effect.gen(function* () {
123 yield* stack.destroy();
124
125 const deployed = yield* stack.deploy(
126 Effect.gen(function* () {
127 return yield* Cloudflare.Email.AllowPolicy("ListPolicy", {
128 pattern,
129 patternType: "EMAIL",
130 isAcceptableSender: true,
131 });
132 }),
133 );
134
135 const provider = yield* Provider.findProvider(
136 Cloudflare.Email.AllowPolicy,
137 );
138 const all = yield* provider.list();
139 expect(all.some((p) => p.policyId === deployed.policyId)).toBe(true);
140
141 yield* stack.destroy();
142 }).pipe(logLevel),
143 {
144 tags: ["provider:cloudflare", "provider:cloudflare:email", "live"],
145 timeout: 120_000,
146 },
147);
148
149// Requires the Email Security (Area 1) enterprise add-on — unentitled accounts fail
150// with the typed EmailSecurityNotEntitled. Unlock with CLOUDFLARE_EMAIL_SECURITY=1.
151test.provider.skipIf(!entitled)(
152 "create, update in place, destroy",
153 (stack) =>
154 Effect.gen(function* () {
155 const { accountId } = yield* yield* CloudflareEnvironment;
156
157 yield* stack.destroy();
158
159 // Create an acceptable-sender allow policy.
160 const created = yield* stack.deploy(
161 Effect.gen(function* () {
162 return yield* Cloudflare.Email.AllowPolicy("Policy", {
163 pattern,
164 patternType: "EMAIL",
165 isAcceptableSender: true,
166 comments: "v1",
167 });
168 }),
169 );
170 expect(created.policyId).toBeDefined();
171 expect(created.accountId).toEqual(accountId);
172 expect(created.pattern).toEqual(pattern);
173 expect(created.patternType).toEqual("EMAIL");
174 expect(created.isAcceptableSender).toEqual(true);
175 expect(created.isTrustedSender).toEqual(false);
176 expect(created.verifySender).toEqual(true);
177 expect(created.comments).toEqual("v1");
178
179 // Out-of-band verification via the distilled API.
180 const live = yield* findByPattern(accountId);
181 expect(live?.id).toEqual(created.policyId);
182
183 // Update mutable fields in place — same physical policy.
184 const updated = yield* stack.deploy(
185 Effect.gen(function* () {
186 return yield* Cloudflare.Email.AllowPolicy("Policy", {
187 pattern,
188 patternType: "EMAIL",
189 isAcceptableSender: true,
190 isExemptRecipient: true,
191 verifySender: false,
192 comments: "v2",
193 });
194 }),
195 );
196 expect(updated.policyId).toEqual(created.policyId);
197 expect(updated.isExemptRecipient).toEqual(true);
198 expect(updated.verifySender).toEqual(false);
199 expect(updated.comments).toEqual("v2");
200
201 yield* stack.destroy();
202
203 // The policy is gone after destroy.
204 const gone = yield* findByPattern(accountId).pipe(
205 Effect.repeat({
206 schedule: Schedule.spaced("2 seconds"),
207 until: (p) => p === undefined,
208 times: 10,
209 }),
210 );
211 expect(gone).toBeUndefined();
212 }).pipe(logLevel),
213 {
214 tags: ["provider:cloudflare", "provider:cloudflare:email", "live"],
215 timeout: 120_000,
216 },
217);