1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import { findZoneByName } from "@/Cloudflare/Zone/lookup";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as pageShield from "@distilled.cloud/cloudflare/page-shield";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const zoneName =
20 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
21
22// Page Shield CSP policies are an Enterprise add-on. On the testing
23// account's zone every create fails with "exceeded the maximum number of
24// rules in the phase http_response_page_shield: 1 out of 0", surfaced as
25// the typed `PolicyQuotaExceeded` error. The full lifecycle test below is
26// gated behind an entitled zone id supplied via env.
27const entitledZoneId = process.env.CLOUDFLARE_TEST_PAGE_SHIELD_POLICY_ZONE_ID;
28
29const resolveZoneId = Effect.gen(function* () {
30 const { accountId } = yield* yield* CloudflareEnvironment;
31 const zone = yield* findZoneByName({ accountId, name: zoneName });
32 if (!zone) {
33 return yield* Effect.die(
34 new Error(`zone "${zoneName}" not found in account`),
35 );
36 }
37 return zone.id;
38});
39
40// Fresh scoped tokens propagate eventually-consistently across Cloudflare's
41// edge — retry the typed `Forbidden` error on out-of-band calls.
42const forbiddenRetrySchedule = Schedule.exponential("500 millis");
43
44const getPolicy = (zoneId: string, policyId: string) =>
45 pageShield.getPolicy({ zoneId, policyId }).pipe(
46 Effect.retry({
47 while: (e) => e._tag === "Forbidden",
48 schedule: forbiddenRetrySchedule,
49 times: 8,
50 }),
51 );
52
53const findPolicyByDescription = (zoneId: string, description: string) =>
54 pageShield.listPolicies({ zoneId }).pipe(
55 Effect.map((list) =>
56 list.result.find((p) => p.description === description),
57 ),
58 Effect.retry({
59 while: (e) => e._tag === "Forbidden",
60 schedule: forbiddenRetrySchedule,
61 times: 8,
62 }),
63 );
64
65test.provider(
66 "surfaces the typed PolicyQuotaExceeded error on unentitled zones",
67 (stack) =>
68 Effect.gen(function* () {
69 const zoneId = yield* resolveZoneId;
70
71 yield* stack.destroy();
72
73 // The standard testing zone lacks the Enterprise CSP entitlement —
74 // the distilled call must fail with the typed quota tag.
75 const error = yield* pageShield
76 .createPolicy({
77 zoneId,
78 action: "log",
79 description: "alchemy-pageshield-quota-probe",
80 enabled: true,
81 expression: `http.host eq "${zoneName}"`,
82 value: "script-src 'self'",
83 })
84 .pipe(
85 Effect.retry({
86 while: (e) => e._tag === "Forbidden",
87 schedule: forbiddenRetrySchedule,
88 times: 8,
89 }),
90 Effect.flip,
91 );
92 expect(error._tag).toEqual("PolicyQuotaExceeded");
93
94 yield* stack.destroy();
95 }).pipe(logLevel),
96 {
97 tags: [
98 "provider:cloudflare",
99 "provider:cloudflare:pageshield",
100 "provider:cloudflare:zone",
101 "live",
102 ],
103 timeout: 120_000,
104 },
105);
106
107test.provider(
108 "list enumerates Page Shield policies across all zones",
109 (stack) =>
110 Effect.gen(function* () {
111 yield* stack.destroy();
112
113 const provider = yield* Provider.findProvider(
114 Cloudflare.PageShield.Policy,
115 );
116
117 // Page Shield CSP policies are an Enterprise add-on; the testing
118 // account has a zero rule quota, so we cannot deploy a policy to
119 // observe. Enumeration must still succeed (fanning out across every
120 // zone, skipping unentitled `Forbidden` zones) and return a
121 // well-typed array — empty when no policies exist.
122 if (entitledZoneId) {
123 const description = "alchemy-pageshield-list";
124 const deployed = yield* stack.deploy(
125 Effect.gen(function* () {
126 yield* Cloudflare.PageShield.Settings("PageShield", {
127 zoneId: entitledZoneId,
128 });
129 return yield* Cloudflare.PageShield.Policy("ListPolicy", {
130 zoneId: entitledZoneId,
131 description,
132 action: "log",
133 expression: `http.host eq "${zoneName}"`,
134 value: "script-src 'self'",
135 });
136 }),
137 );
138
139 const all = yield* provider.list();
140 expect(Array.isArray(all)).toBe(true);
141 expect(all.some((p) => p.policyId === deployed.policyId)).toBe(true);
142 } else {
143 const all = yield* provider.list();
144 expect(Array.isArray(all)).toBe(true);
145 }
146
147 yield* stack.destroy();
148 }).pipe(logLevel),
149 {
150 tags: ["provider:cloudflare", "provider:cloudflare:pageshield", "live"],
151 timeout: 180_000,
152 },
153);
154
155// Requires the Enterprise Page Shield CSP entitlement — unentitled zones have a zero
156// rule quota and fail with the typed PolicyQuotaExceeded. Unlock with CLOUDFLARE_TEST_PAGE_SHIELD_POLICY_ZONE_ID=<zone id>.
157test.provider.skipIf(!entitledZoneId)(
158 "creates, updates in place, and deletes a CSP policy",
159 (stack) =>
160 Effect.gen(function* () {
161 const zoneId = entitledZoneId!;
162 const description = "alchemy-pageshield-policy-lifecycle";
163
164 yield* stack.destroy();
165
166 // Create — log-only CSP policy (Page Shield itself is enabled by
167 // the Settings singleton in the same stack).
168 const created = yield* stack.deploy(
169 Effect.gen(function* () {
170 yield* Cloudflare.PageShield.Settings("PageShield", {
171 zoneId,
172 });
173 return yield* Cloudflare.PageShield.Policy("CspPolicy", {
174 zoneId,
175 description,
176 action: "log",
177 expression: `http.host eq "${zoneName}"`,
178 value: "script-src 'self'",
179 });
180 }),
181 );
182
183 expect(created.policyId).toBeDefined();
184 expect(created.zoneId).toEqual(zoneId);
185 expect(created.action).toEqual("log");
186 expect(created.description).toEqual(description);
187 expect(created.enabled).toEqual(true);
188
189 // Out-of-band verification via the distilled API.
190 const live = yield* getPolicy(zoneId, created.policyId);
191 expect(live.action).toEqual("log");
192 expect(live.value).toEqual("script-src 'self'");
193
194 // Update in place — action and value are mutable, same policy id.
195 const updated = yield* stack.deploy(
196 Effect.gen(function* () {
197 yield* Cloudflare.PageShield.Settings("PageShield", {
198 zoneId,
199 });
200 return yield* Cloudflare.PageShield.Policy("CspPolicy", {
201 zoneId,
202 description,
203 action: "allow",
204 expression: `http.host eq "${zoneName}"`,
205 value: "script-src 'self' 'unsafe-inline'",
206 });
207 }),
208 );
209 expect(updated.policyId).toEqual(created.policyId);
210 expect(updated.action).toEqual("allow");
211 expect(updated.value).toEqual("script-src 'self' 'unsafe-inline'");
212
213 const liveUpdated = yield* getPolicy(zoneId, updated.policyId);
214 expect(liveUpdated.action).toEqual("allow");
215
216 yield* stack.destroy();
217
218 // The policy is gone (delete is idempotent — Cloudflare answers a
219 // 204 even for already-deleted policies).
220 const gone = yield* findPolicyByDescription(zoneId, description);
221 expect(gone).toBeUndefined();
222 }).pipe(logLevel),
223 {
224 tags: ["provider:cloudflare", "provider:cloudflare:pageshield", "live"],
225 timeout: 120_000,
226 },
227);