1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Output from "@/Output";
4import { findZoneByName } from "@/Cloudflare/Zone/lookup";
5import * as Provider from "@/Provider";
6import * as Test from "@/Test/Alchemy";
7import * as tokenValidation from "@distilled.cloud/cloudflare/token-validation";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import { MinimumLogLevel } from "effect/References";
11import * as Schedule from "effect/Schedule";
12import { JWKS_KEY_1, JWKS_KEY_2 } from "./fixtures/jwks.ts";
13
14const { test } = Test.make({ providers: Cloudflare.providers() });
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21const zoneName =
22 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
23
24// JWT validation (API Shield) is entitlement-gated — on the standard testing
25// account every token_validation call fails with "You are not entitled for
26// this service" (code 10403), surfaced as the typed
27// `TokenValidationNotEntitled` error. The full lifecycle test below is gated
28// behind an entitled zone id supplied via env.
29const entitledZoneId = process.env.CLOUDFLARE_TEST_TOKEN_VALIDATION_ZONE_ID;
30
31const resolveZoneId = Effect.gen(function* () {
32 const { accountId } = yield* yield* CloudflareEnvironment;
33 const zone = yield* findZoneByName({ accountId, name: zoneName });
34 if (!zone) {
35 return yield* Effect.die(
36 new Error(`zone "${zoneName}" not found in account`),
37 );
38 }
39 return zone.id;
40});
41
42// A freshly minted scoped token propagates eventually-consistently across
43// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
44const forbiddenRetrySchedule = Schedule.exponential("500 millis");
45
46const listConfigurations = (zoneId: string) =>
47 tokenValidation.listConfigurations({ zoneId }).pipe(
48 Effect.retry({
49 while: (e) => e._tag === "Forbidden",
50 schedule: forbiddenRetrySchedule,
51 times: 8,
52 }),
53 );
54
55const getConfiguration = (zoneId: string, configId: string) =>
56 tokenValidation.getConfiguration({ zoneId, configId }).pipe(
57 Effect.retry({
58 while: (e) => e._tag === "Forbidden",
59 schedule: forbiddenRetrySchedule,
60 times: 8,
61 }),
62 );
63
64const getRule = (zoneId: string, ruleId: string) =>
65 tokenValidation.getRule({ zoneId, ruleId }).pipe(
66 Effect.retry({
67 while: (e) => e._tag === "Forbidden",
68 schedule: forbiddenRetrySchedule,
69 times: 8,
70 }),
71 );
72
73test.provider(
74 "surfaces the typed TokenValidationNotEntitled error on unentitled accounts",
75 (stack) =>
76 Effect.gen(function* () {
77 const zoneId = yield* resolveZoneId;
78
79 yield* stack.destroy();
80
81 // The testing account lacks the API Shield JWT validation
82 // entitlement — every distilled call must fail with the typed
83 // entitlement tag (never the catch-all
84 // `Forbidden`/`UnknownCloudflareError`).
85 const listError = yield* listConfigurations(zoneId).pipe(Effect.flip);
86 expect(listError._tag).toEqual("TokenValidationNotEntitled");
87
88 const getError = yield* getConfiguration(
89 zoneId,
90 "00000000-0000-0000-0000-000000000000",
91 ).pipe(Effect.flip);
92 expect(getError._tag).toEqual("TokenValidationNotEntitled");
93
94 const ruleError = yield* getRule(
95 zoneId,
96 "00000000-0000-0000-0000-000000000000",
97 ).pipe(Effect.flip);
98 expect(ruleError._tag).toEqual("TokenValidationNotEntitled");
99
100 yield* stack.destroy();
101 }).pipe(logLevel),
102 {
103 tags: [
104 "provider:cloudflare",
105 "provider:cloudflare:tokenvalidation",
106 "provider:cloudflare:zone",
107 "live",
108 ],
109 timeout: 120_000,
110 },
111);
112
113// Canonical `list()` test (zone-scoped collection): configurations live
114// inside a zone and the list op is keyed per zone, so `list()` enumerates
115// every zone via `listAllZones` and fans out the per-zone list, skipping
116// zones that reject with the typed `TokenValidationNotEntitled` / `Forbidden`
117// tags. On the unentitled testing account every zone is skipped, so the
118// result is an empty array — the assertion is that `list()` resolves to an
119// array (proving the typed skip path) rather than throwing. Presence of a
120// deployed configuration is asserted only on an entitled account (env-gated).
121test.provider(
122 "list enumerates configurations across all zones",
123 (stack) =>
124 Effect.gen(function* () {
125 const provider = yield* Provider.findProvider(
126 Cloudflare.TokenValidation.TokenConfiguration,
127 );
128
129 if (!entitledZoneId) {
130 const all = yield* provider.list();
131 expect(Array.isArray(all)).toBe(true);
132 yield* stack.destroy();
133 return;
134 }
135
136 const zoneId = entitledZoneId;
137 yield* stack.destroy();
138
139 const deployed = yield* stack.deploy(
140 Effect.gen(function* () {
141 return yield* Cloudflare.TokenValidation.TokenConfiguration(
142 "JwtConfigList",
143 {
144 zoneId,
145 tokenSources: ['http.request.headers["authorization"][0]'],
146 keys: [JWKS_KEY_1],
147 },
148 );
149 }),
150 );
151
152 const all = yield* provider.list();
153 expect(all.some((c) => c.configId === deployed.configId)).toBe(true);
154
155 yield* stack.destroy();
156 }).pipe(logLevel),
157 {
158 tags: [
159 "provider:cloudflare",
160 "provider:cloudflare:tokenvalidation",
161 "live",
162 ],
163 timeout: 120_000,
164 },
165);
166
167test.provider.skipIf(!entitledZoneId)(
168 "configuration + rule lifecycle: create, update in place, rotate keys, destroy",
169 (stack) =>
170 Effect.gen(function* () {
171 const zoneId = entitledZoneId!;
172
173 yield* stack.destroy();
174
175 // -- Create: configuration with one key, rule that logs ------------
176 const created = yield* stack.deploy(
177 Effect.gen(function* () {
178 const config = yield* Cloudflare.TokenValidation.TokenConfiguration(
179 "JwtConfig",
180 {
181 zoneId,
182 description: "v1",
183 tokenSources: ['http.request.headers["authorization"][0]'],
184 keys: [JWKS_KEY_1],
185 },
186 );
187 const rule = yield* Cloudflare.TokenValidation.Rule("JwtRule", {
188 zoneId,
189 description: "v1",
190 action: "log",
191 expression: Output.interpolate`is_jwt_valid("${config.configId}")`,
192 selector: { include: [{ host: [zoneName] }] },
193 });
194 return { config, rule };
195 }),
196 );
197
198 expect(created.config.configId).toBeDefined();
199 expect(created.config.zoneId).toEqual(zoneId);
200 expect(created.config.tokenType).toEqual("JWT");
201 expect(created.config.keys.map((k) => k.kid)).toEqual([JWKS_KEY_1.kid]);
202 expect(created.rule.ruleId).toBeDefined();
203 expect(created.rule.action).toEqual("log");
204 expect(created.rule.enabled).toEqual(true);
205 expect(created.rule.expression).toContain(created.config.configId);
206
207 // Out-of-band verification via the distilled API.
208 const liveConfig = yield* getConfiguration(
209 zoneId,
210 created.config.configId,
211 );
212 expect(liveConfig.credentials.keys.map((k) => k.kid)).toEqual([
213 JWKS_KEY_1.kid,
214 ]);
215 const liveRule = yield* getRule(zoneId, created.rule.ruleId);
216 expect(liveRule.action).toEqual("log");
217
218 // -- Update in place: patch metadata, rotate keys, flip the rule ---
219 const updated = yield* stack.deploy(
220 Effect.gen(function* () {
221 const config = yield* Cloudflare.TokenValidation.TokenConfiguration(
222 "JwtConfig",
223 {
224 zoneId,
225 description: "v2",
226 tokenSources: [
227 'http.request.headers["authorization"][0]',
228 'http.request.uri.args["token"][0]',
229 ],
230 keys: [JWKS_KEY_1, JWKS_KEY_2],
231 },
232 );
233 const rule = yield* Cloudflare.TokenValidation.Rule("JwtRule", {
234 zoneId,
235 description: "v2",
236 enabled: false,
237 action: "block",
238 expression: Output.interpolate`is_jwt_valid("${config.configId}")`,
239 selector: { include: [{ host: [zoneName] }] },
240 });
241 return { config, rule };
242 }),
243 );
244
245 // Same physical resources — patched in place, not replaced.
246 expect(updated.config.configId).toEqual(created.config.configId);
247 expect(updated.config.description).toEqual("v2");
248 expect(updated.config.tokenSources).toHaveLength(2);
249 expect(updated.config.keys.map((k) => k.kid).sort()).toEqual([
250 JWKS_KEY_1.kid,
251 JWKS_KEY_2.kid,
252 ]);
253 expect(updated.rule.ruleId).toEqual(created.rule.ruleId);
254 expect(updated.rule.action).toEqual("block");
255 expect(updated.rule.enabled).toEqual(false);
256
257 const rotated = yield* getConfiguration(zoneId, updated.config.configId);
258 expect(rotated.credentials.keys).toHaveLength(2);
259
260 // -- Destroy: rule first (it references the config), then config ---
261 yield* stack.destroy();
262
263 const configGone = yield* getConfiguration(
264 zoneId,
265 updated.config.configId,
266 ).pipe(Effect.flip);
267 expect(configGone._tag).toEqual("TokenConfigurationNotFound");
268 const ruleGone = yield* getRule(zoneId, updated.rule.ruleId).pipe(
269 Effect.flip,
270 );
271 expect(ruleGone._tag).toEqual("TokenValidationRuleNotFound");
272
273 // Destroy is idempotent.
274 yield* stack.destroy();
275 }).pipe(logLevel),
276 {
277 tags: [
278 "provider:cloudflare",
279 "provider:cloudflare:tokenvalidation",
280 "live",
281 ],
282 timeout: 120_000,
283 },
284);