1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as rulesets from "@distilled.cloud/cloudflare/rulesets";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Result from "effect/Result";
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
19// A freshly minted scoped token propagates eventually-consistently across
20// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
21const forbiddenRetrySchedule = Schedule.exponential("500 millis");
22
23const phase = "http_request_firewall_custom";
24
25const resolveAccountId = Effect.gen(function* () {
26 const { accountId } = yield* yield* CloudflareEnvironment;
27 return accountId;
28});
29
30const getEntrypointRules = (accountId: string) =>
31 rulesets.getPhasForAccount({ accountId, rulesetPhase: phase }).pipe(
32 Effect.retry({
33 while: (e) => e._tag === "Forbidden",
34 schedule: forbiddenRetrySchedule,
35 times: 8,
36 }),
37 Effect.map((entrypoint) => entrypoint.rules ?? []),
38 Effect.catchTag("RulesetNotFound", () => Effect.succeed([])),
39 );
40
41// Account-level phase entrypoints require an Enterprise plan. On the
42// standard testing account every PUT fails with the typed `PhaseNotEntitled`
43// error (code 50002). The test probes once: unentitled accounts assert the
44// typed tag; entitled accounts run the full lifecycle.
45test.provider(
46 "account entrypoint lifecycle (typed PhaseNotEntitled on unentitled accounts)",
47 (stack) =>
48 Effect.gen(function* () {
49 const accountId = yield* resolveAccountId;
50
51 yield* stack.destroy();
52
53 // Probing with an empty rules PUT is a harmless baseline on entitled
54 // accounts (the entrypoint is a singleton; empty = "no rules").
55 const probe = yield* rulesets
56 .putPhasForAccount({
57 accountId,
58 rulesetPhase: phase,
59 rules: [],
60 })
61 .pipe(
62 Effect.retry({
63 while: (e) => e._tag === "Forbidden",
64 schedule: forbiddenRetrySchedule,
65 times: 8,
66 }),
67 Effect.result,
68 );
69
70 if (Result.isFailure(probe)) {
71 // Unentitled — the distilled call must fail with the typed
72 // entitlement tag, never an untyped catch-all.
73 expect(probe.failure._tag).toEqual("PhaseNotEntitled");
74 yield* stack.destroy();
75 return;
76 }
77
78 // Entitled — deploy a custom ruleset plus the entrypoint that
79 // executes it (the Enterprise WAF deployment workflow).
80 const deployed = yield* stack.deploy(
81 Effect.gen(function* () {
82 const ruleset = yield* Cloudflare.Ruleset.CustomRuleset(
83 "SharedRules",
84 {
85 phase,
86 rules: [
87 {
88 description: "Block exploit probes",
89 expression: `lower(http.request.uri.path) contains "/.env"`,
90 action: "block",
91 },
92 ],
93 },
94 );
95 const entrypoint = yield* Cloudflare.Ruleset.AccountEntrypoint(
96 "WafDeployment",
97 {
98 phase,
99 description: "alchemy account entrypoint v1",
100 rules: [
101 {
102 description: "Deploy shared WAF rules",
103 expression: "true",
104 action: "execute",
105 actionParameters: { id: ruleset.rulesetId },
106 },
107 ],
108 },
109 );
110 return { entrypoint, ruleset };
111 }),
112 );
113 expect(deployed.entrypoint.accountId).toEqual(accountId);
114 expect(deployed.entrypoint.phase).toEqual(phase);
115 expect(deployed.entrypoint.rules).toHaveLength(1);
116
117 // Out-of-band verification via the distilled API.
118 const liveRules = yield* getEntrypointRules(accountId);
119 expect(liveRules).toHaveLength(1);
120 expect(liveRules[0]).toMatchObject({ action: "execute" });
121
122 // Update the description in place — same singleton entrypoint.
123 const updated = yield* stack.deploy(
124 Effect.gen(function* () {
125 const ruleset = yield* Cloudflare.Ruleset.CustomRuleset(
126 "SharedRules",
127 {
128 phase,
129 rules: [
130 {
131 description: "Block exploit probes",
132 expression: `lower(http.request.uri.path) contains "/.env"`,
133 action: "block",
134 },
135 ],
136 },
137 );
138 return yield* Cloudflare.Ruleset.AccountEntrypoint("WafDeployment", {
139 phase,
140 description: "alchemy account entrypoint v2",
141 rules: [
142 {
143 description: "Deploy shared WAF rules",
144 expression: "true",
145 action: "execute",
146 actionParameters: { id: ruleset.rulesetId },
147 },
148 ],
149 });
150 }),
151 );
152 expect(updated.rulesetId).toEqual(deployed.entrypoint.rulesetId);
153 expect(updated.description).toEqual("alchemy account entrypoint v2");
154
155 // Destroy and verify the entrypoint was emptied in Cloudflare.
156 yield* stack.destroy();
157 const remaining = yield* getEntrypointRules(accountId);
158 expect(remaining).toEqual([]);
159 }).pipe(logLevel),
160 {
161 tags: ["provider:cloudflare", "provider:cloudflare:ruleset", "live"],
162 timeout: 120_000,
163 },
164);
165
166test.provider(
167 "list enumerates the account phase entrypoints",
168 (stack) =>
169 Effect.gen(function* () {
170 const accountId = yield* resolveAccountId;
171
172 yield* stack.destroy();
173
174 // Probe entitlement: an empty-rules PUT is a harmless baseline on
175 // entitled accounts, but fails with the typed `PhaseNotEntitled` tag
176 // on the standard testing account.
177 const probe = yield* rulesets
178 .putPhasForAccount({ accountId, rulesetPhase: phase, rules: [] })
179 .pipe(
180 Effect.retry({
181 while: (e) => e._tag === "Forbidden",
182 schedule: forbiddenRetrySchedule,
183 times: 8,
184 }),
185 Effect.result,
186 );
187
188 const provider = yield* Provider.findProvider(
189 Cloudflare.Ruleset.AccountEntrypoint,
190 );
191
192 if (Result.isFailure(probe)) {
193 // Unentitled — list must still succeed (typed-skip gated phases) and
194 // return a well-typed array rather than throwing.
195 expect(probe.failure._tag).toEqual("PhaseNotEntitled");
196 const all = yield* provider.list();
197 expect(Array.isArray(all)).toBe(true);
198 yield* stack.destroy();
199 return;
200 }
201
202 // Entitled — deploy the entrypoint and assert list() surfaces it in the
203 // exhaustively-enumerated result.
204 const deployed = yield* stack.deploy(
205 Effect.gen(function* () {
206 const ruleset = yield* Cloudflare.Ruleset.CustomRuleset(
207 "SharedRules",
208 {
209 phase,
210 rules: [
211 {
212 description: "Block exploit probes",
213 expression: `lower(http.request.uri.path) contains "/.env"`,
214 action: "block",
215 },
216 ],
217 },
218 );
219 return yield* Cloudflare.Ruleset.AccountEntrypoint("WafDeployment", {
220 phase,
221 description: "alchemy account entrypoint list",
222 rules: [
223 {
224 description: "Deploy shared WAF rules",
225 expression: "true",
226 action: "execute",
227 actionParameters: { id: ruleset.rulesetId },
228 },
229 ],
230 });
231 }),
232 );
233
234 const all = yield* provider.list();
235 expect(all.some((e) => e.rulesetId === deployed.rulesetId)).toBe(true);
236 const found = all.find((e) => e.rulesetId === deployed.rulesetId)!;
237 expect(found.accountId).toEqual(accountId);
238 expect(found.phase).toEqual(phase);
239
240 yield* stack.destroy();
241 }).pipe(logLevel),
242 {
243 tags: ["provider:cloudflare", "provider:cloudflare:ruleset", "live"],
244 timeout: 120_000,
245 },
246);