1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import { MinimumLogLevel } from "effect/References";
10
11const { test } = Test.make({
12 providers: Cloudflare.providers(),
13 state: Cloudflare.state(),
14});
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21test.provider(
22 "create, update duration, and delete service token",
23 (stack) =>
24 Effect.gen(function* () {
25 const { accountId } = yield* yield* CloudflareEnvironment;
26
27 yield* stack.destroy();
28
29 const token = yield* stack.deploy(
30 Effect.gen(function* () {
31 return yield* Cloudflare.Access.ServiceToken("BasicToken", {});
32 }),
33 );
34
35 expect(token.serviceTokenId).toBeDefined();
36 expect(token.accountId).toEqual(accountId);
37 expect(token.clientId).toBeDefined();
38 // The client secret is only revealed on create — it must be captured.
39 expect(token.clientSecret).toBeDefined();
40 expect(Redacted.value(token.clientSecret!).length).toBeGreaterThan(0);
41
42 const actual = yield* zeroTrust.getAccessServiceTokenForAccount({
43 accountId,
44 serviceTokenId: token.serviceTokenId,
45 });
46 expect(actual.id).toEqual(token.serviceTokenId);
47 expect(actual.clientId).toEqual(token.clientId);
48 expect(actual.name).toEqual(token.name);
49
50 // Update — change the validity duration in place (same id) and keep
51 // the previously captured secret.
52 const updated = yield* stack.deploy(
53 Effect.gen(function* () {
54 return yield* Cloudflare.Access.ServiceToken("BasicToken", {
55 duration: "17520h",
56 });
57 }),
58 );
59 expect(updated.serviceTokenId).toEqual(token.serviceTokenId);
60 expect(updated.duration).toEqual("17520h");
61 expect(updated.clientSecret).toBeDefined();
62 expect(Redacted.value(updated.clientSecret!)).toEqual(
63 Redacted.value(token.clientSecret!),
64 );
65
66 const afterUpdate = yield* zeroTrust.getAccessServiceTokenForAccount({
67 accountId,
68 serviceTokenId: token.serviceTokenId,
69 });
70 expect(afterUpdate.duration).toEqual("17520h");
71
72 yield* stack.destroy();
73
74 const afterDestroy = yield* zeroTrust
75 .getAccessServiceTokenForAccount({
76 accountId,
77 serviceTokenId: token.serviceTokenId,
78 })
79 .pipe(
80 Effect.catchTag("AccessServiceTokenNotFound", () =>
81 Effect.succeed(undefined),
82 ),
83 );
84 expect(afterDestroy?.id ?? undefined).toBeUndefined();
85 }).pipe(logLevel),
86 { tags: ["provider:cloudflare", "provider:cloudflare:access", "live"] },
87);
88
89test.provider(
90 "list enumerates the deployed service token",
91 (stack) =>
92 Effect.gen(function* () {
93 yield* stack.destroy();
94
95 const token = yield* stack.deploy(
96 Effect.gen(function* () {
97 return yield* Cloudflare.Access.ServiceToken("ListToken", {});
98 }),
99 );
100
101 const provider = yield* Provider.findProvider(
102 Cloudflare.Access.ServiceToken,
103 );
104 const all = yield* provider.list();
105
106 expect(all.some((t) => t.serviceTokenId === token.serviceTokenId)).toBe(
107 true,
108 );
109 // Enumeration never exposes the one-time secret — it matches read.
110 const found = all.find((t) => t.serviceTokenId === token.serviceTokenId);
111 expect(found?.clientId).toEqual(token.clientId);
112 expect(found?.clientSecret).toBeUndefined();
113
114 yield* stack.destroy();
115 }).pipe(logLevel),
116 { tags: ["provider:cloudflare", "provider:cloudflare:access", "live"] },
117);
118
119test.provider(
120 "incrementing clientSecretVersion rotates the secret",
121 (stack) =>
122 Effect.gen(function* () {
123 const { accountId } = yield* yield* CloudflareEnvironment;
124
125 yield* stack.destroy();
126
127 const token = yield* stack.deploy(
128 Effect.gen(function* () {
129 return yield* Cloudflare.Access.ServiceToken("RotateToken", {});
130 }),
131 );
132 expect(token.clientSecret).toBeDefined();
133 expect(token.clientSecretVersion).toEqual(1);
134
135 const rotated = yield* stack.deploy(
136 Effect.gen(function* () {
137 return yield* Cloudflare.Access.ServiceToken("RotateToken", {
138 clientSecretVersion: 2,
139 });
140 }),
141 );
142 expect(rotated.serviceTokenId).toEqual(token.serviceTokenId);
143 expect(rotated.clientId).toEqual(token.clientId);
144 expect(rotated.clientSecretVersion).toEqual(2);
145 expect(rotated.clientSecret).toBeDefined();
146 expect(Redacted.value(rotated.clientSecret!)).not.toEqual(
147 Redacted.value(token.clientSecret!),
148 );
149
150 // Re-deploying the same version must NOT rotate again.
151 const stable = yield* stack.deploy(
152 Effect.gen(function* () {
153 return yield* Cloudflare.Access.ServiceToken("RotateToken", {
154 clientSecretVersion: 2,
155 });
156 }),
157 );
158 expect(stable.serviceTokenId).toEqual(token.serviceTokenId);
159 expect(Redacted.value(stable.clientSecret!)).toEqual(
160 Redacted.value(rotated.clientSecret!),
161 );
162
163 const actual = yield* zeroTrust.getAccessServiceTokenForAccount({
164 accountId,
165 serviceTokenId: token.serviceTokenId,
166 });
167 expect(actual.id).toEqual(token.serviceTokenId);
168
169 yield* stack.destroy();
170 }).pipe(logLevel),
171 { tags: ["provider:cloudflare", "provider:cloudflare:access", "live"] },
172);