| 1 | import * as Cloudflare from "@/Cloudflare"; |
| 2 | import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment"; |
| 3 | import { findZoneByName } from "@/Cloudflare/Zone/lookup"; |
| 4 | import * as Provider from "@/Provider"; |
| 5 | import * as Test from "@/Test/Alchemy"; |
| 6 | import * as customCertificates from "@distilled.cloud/cloudflare/custom-certificates"; |
| 7 | import { expect } from "alchemy-test"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Redacted from "effect/Redacted"; |
| 10 | import { MinimumLogLevel } from "effect/References"; |
| 11 | import * as Schedule from "effect/Schedule"; |
| 12 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Path from "effect/Path"; |
| 14 | |
| 15 | const { test } = Test.make({ providers: Cloudflare.providers() }); |
| 16 | |
| 17 | const logLevel = Effect.provideService( |
| 18 | MinimumLogLevel, |
| 19 | process.env.DEBUG ? "Debug" : "Info", |
| 20 | ); |
| 21 | |
| 22 | const zoneName = |
| 23 | process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us"; |
| 24 | |
| 25 | // Custom (BYO) edge certificates are a Business/Enterprise feature. On the |
| 26 | // testing account's zone every custom_certificates call fails with |
| 27 | // "Plan level does not allow custom certificates ..." (Cloudflare error |
| 28 | // code 1011), surfaced as the typed `PlanLevelNotAllowed` error. The full |
| 29 | // lifecycle test below is gated behind an entitled zone id supplied via env. |
| 30 | const entitledZoneId = process.env.CLOUDFLARE_TEST_CUSTOM_CERT_ZONE_ID; |
| 31 | |
| 32 | const resolveZoneId = Effect.gen(function* () { |
| 33 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 34 | const zone = yield* findZoneByName({ accountId, name: zoneName }); |
| 35 | if (!zone) { |
| 36 | return yield* Effect.die( |
| 37 | new Error(`zone "${zoneName}" not found in account`), |
| 38 | ); |
| 39 | } |
| 40 | return zone.id; |
| 41 | }); |
| 42 | |
| 43 | // Checked-in self-signed PEM fixtures (CN=alchemy-test-2.us, 10y validity). |
| 44 | // The upload API accepts self-signed certificates — validation only requires |
| 45 | // that the key matches the certificate — keeping the test hermetic. |
| 46 | const readFixture = (name: string) => |
| 47 | Effect.gen(function* () { |
| 48 | const fs = yield* FileSystem.FileSystem; |
| 49 | const path = yield* Path.Path; |
| 50 | return yield* fs.readFileString( |
| 51 | path.join(import.meta.dirname, "fixtures", name), |
| 52 | ); |
| 53 | }); |
| 54 | |
| 55 | // A freshly minted scoped token propagates eventually-consistently across |
| 56 | // Cloudflare's edge; ride out 403 blips on out-of-band verification calls. |
| 57 | const forbiddenRetrySchedule = Schedule.exponential("500 millis"); |
| 58 | |
| 59 | const getCertificate = (zoneId: string, customCertificateId: string) => |
| 60 | customCertificates.getCustomCertificate({ zoneId, customCertificateId }).pipe( |
| 61 | Effect.retry({ |
| 62 | while: (e) => e._tag === "Forbidden", |
| 63 | schedule: forbiddenRetrySchedule, |
| 64 | times: 8, |
| 65 | }), |
| 66 | ); |
| 67 | |
| 68 | test.provider( |
| 69 | "surfaces the typed PlanLevelNotAllowed error on unentitled zones", |
| 70 | (stack) => |
| 71 | Effect.gen(function* () { |
| 72 | const zoneId = yield* resolveZoneId; |
| 73 | const certificate = yield* readFixture("cert1.pem"); |
| 74 | const privateKey = yield* readFixture("key1.pem"); |
| 75 | |
| 76 | yield* stack.destroy(); |
| 77 | |
| 78 | // The standard testing zone is not on a Business/Enterprise plan — |
| 79 | // the distilled call must fail with the typed entitlement tag. |
| 80 | const error = yield* customCertificates |
| 81 | .createCustomCertificate({ |
| 82 | zoneId, |
| 83 | certificate, |
| 84 | privateKey, |
| 85 | type: "sni_custom", |
| 86 | }) |
| 87 | .pipe( |
| 88 | Effect.retry({ |
| 89 | while: (e) => e._tag === "Forbidden", |
| 90 | schedule: forbiddenRetrySchedule, |
| 91 | times: 8, |
| 92 | }), |
| 93 | Effect.flip, |
| 94 | ); |
| 95 | expect(error._tag).toEqual("PlanLevelNotAllowed"); |
| 96 | |
| 97 | yield* stack.destroy(); |
| 98 | }).pipe(logLevel), |
| 99 | { |
| 100 | tags: [ |
| 101 | "provider:cloudflare", |
| 102 | "provider:cloudflare:customcertificate", |
| 103 | "provider:cloudflare:zone", |
| 104 | "live", |
| 105 | ], |
| 106 | }, |
| 107 | ); |
| 108 | |
| 109 | // `list()` fans out over every zone in the account, paginates each zone's |
| 110 | // custom certificates, and skips plan-gated zones via the typed |
| 111 | // `PlanLevelNotAllowed`/`Forbidden` tags. On the testing account (no |
| 112 | // Business/Enterprise zones) every zone is skipped, so the call must still |
| 113 | // succeed and return a well-typed array (empty here) rather than throwing. |
| 114 | test.provider( |
| 115 | "list enumerates custom certificates across zones", |
| 116 | (stack) => |
| 117 | Effect.gen(function* () { |
| 118 | yield* stack.destroy(); |
| 119 | |
| 120 | const provider = yield* Provider.findProvider( |
| 121 | Cloudflare.CustomCertificate.CustomCertificate, |
| 122 | ); |
| 123 | const all = yield* provider.list(); |
| 124 | |
| 125 | expect(Array.isArray(all)).toBe(true); |
| 126 | // Every item carries the full read Attributes shape. |
| 127 | for (const cert of all) { |
| 128 | expect(typeof cert.certificateId).toBe("string"); |
| 129 | expect(typeof cert.zoneId).toBe("string"); |
| 130 | } |
| 131 | |
| 132 | // On an entitled zone, the certificate we deploy must appear in the |
| 133 | // exhaustively-paginated result. |
| 134 | if (entitledZoneId) { |
| 135 | const cert1 = yield* readFixture("cert1.pem"); |
| 136 | const key1 = yield* readFixture("key1.pem"); |
| 137 | const deployed = yield* stack.deploy( |
| 138 | Effect.gen(function* () { |
| 139 | return yield* Cloudflare.CustomCertificate.CustomCertificate( |
| 140 | "ListEdgeCert", |
| 141 | { |
| 142 | zoneId: entitledZoneId, |
| 143 | certificate: cert1, |
| 144 | privateKey: Redacted.make(key1), |
| 145 | type: "sni_custom", |
| 146 | bundleMethod: "force", |
| 147 | }, |
| 148 | ); |
| 149 | }), |
| 150 | ); |
| 151 | const after = yield* provider.list(); |
| 152 | expect( |
| 153 | after.some((c) => c.certificateId === deployed.certificateId), |
| 154 | ).toBe(true); |
| 155 | } |
| 156 | |
| 157 | yield* stack.destroy(); |
| 158 | }).pipe(logLevel), |
| 159 | { |
| 160 | tags: [ |
| 161 | "provider:cloudflare", |
| 162 | "provider:cloudflare:customcertificate", |
| 163 | "live", |
| 164 | ], |
| 165 | timeout: 120_000, |
| 166 | }, |
| 167 | ); |
| 168 | |
| 169 | test.provider.skipIf(!entitledZoneId)( |
| 170 | "uploads, rotates in place, replaces on type change, and deletes", |
| 171 | (stack) => |
| 172 | Effect.gen(function* () { |
| 173 | const zoneId = entitledZoneId!; |
| 174 | const cert1 = yield* readFixture("cert1.pem"); |
| 175 | const key1 = yield* readFixture("key1.pem"); |
| 176 | const cert2 = yield* readFixture("cert2.pem"); |
| 177 | const key2 = yield* readFixture("key2.pem"); |
| 178 | |
| 179 | yield* stack.destroy(); |
| 180 | |
| 181 | // Create — upload the first certificate/key pair. |
| 182 | const created = yield* stack.deploy( |
| 183 | Effect.gen(function* () { |
| 184 | return yield* Cloudflare.CustomCertificate.CustomCertificate( |
| 185 | "EdgeCert", |
| 186 | { |
| 187 | zoneId, |
| 188 | certificate: cert1, |
| 189 | privateKey: Redacted.make(key1), |
| 190 | type: "sni_custom", |
| 191 | bundleMethod: "force", |
| 192 | }, |
| 193 | ); |
| 194 | }), |
| 195 | ); |
| 196 | |
| 197 | expect(created.certificateId).toBeDefined(); |
| 198 | expect(created.zoneId).toEqual(zoneId); |
| 199 | expect(created.type).toEqual("sni_custom"); |
| 200 | expect(created.hosts).toContain("alchemy-test-2.us"); |
| 201 | expect(created.contentHash).toHaveLength(64); |
| 202 | |
| 203 | // Out-of-band verification via the distilled API. |
| 204 | const live = yield* getCertificate(zoneId, created.certificateId); |
| 205 | expect(live.id).toEqual(created.certificateId); |
| 206 | |
| 207 | // Rotate in place — a new cert/key pair PATCHes the same id. |
| 208 | const rotated = yield* stack.deploy( |
| 209 | Effect.gen(function* () { |
| 210 | return yield* Cloudflare.CustomCertificate.CustomCertificate( |
| 211 | "EdgeCert", |
| 212 | { |
| 213 | zoneId, |
| 214 | certificate: cert2, |
| 215 | privateKey: Redacted.make(key2), |
| 216 | type: "sni_custom", |
| 217 | bundleMethod: "force", |
| 218 | }, |
| 219 | ); |
| 220 | }), |
| 221 | ); |
| 222 | expect(rotated.certificateId).toEqual(created.certificateId); |
| 223 | expect(rotated.contentHash).not.toEqual(created.contentHash); |
| 224 | // cert2 has a different validity window than cert1. |
| 225 | expect(rotated.expiresOn).not.toEqual(created.expiresOn); |
| 226 | |
| 227 | // Changing the immutable `type` triggers a replacement. |
| 228 | const replaced = yield* stack.deploy( |
| 229 | Effect.gen(function* () { |
| 230 | return yield* Cloudflare.CustomCertificate.CustomCertificate( |
| 231 | "EdgeCert", |
| 232 | { |
| 233 | zoneId, |
| 234 | certificate: cert2, |
| 235 | privateKey: Redacted.make(key2), |
| 236 | type: "legacy_custom", |
| 237 | bundleMethod: "force", |
| 238 | }, |
| 239 | ); |
| 240 | }), |
| 241 | ); |
| 242 | expect(replaced.certificateId).not.toEqual(rotated.certificateId); |
| 243 | expect(replaced.type).toEqual("legacy_custom"); |
| 244 | |
| 245 | yield* stack.destroy(); |
| 246 | |
| 247 | // Destroy deleted the certificate — the typed NotFound tag confirms. |
| 248 | const gone = yield* customCertificates |
| 249 | .getCustomCertificate({ |
| 250 | zoneId, |
| 251 | customCertificateId: replaced.certificateId, |
| 252 | }) |
| 253 | .pipe( |
| 254 | Effect.retry({ |
| 255 | while: (e) => e._tag === "Forbidden", |
| 256 | schedule: forbiddenRetrySchedule, |
| 257 | times: 8, |
| 258 | }), |
| 259 | Effect.flip, |
| 260 | ); |
| 261 | expect(gone._tag).toEqual("CustomCertificateNotFound"); |
| 262 | }).pipe(logLevel), |
| 263 | { |
| 264 | tags: [ |
| 265 | "provider:cloudflare", |
| 266 | "provider:cloudflare:customcertificate", |
| 267 | "live", |
| 268 | ], |
| 269 | timeout: 120_000, |
| 270 | }, |
| 271 | ); |