1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as stream from "@distilled.cloud/cloudflare/stream";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
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
19/**
20 * True when the key with the given id is listed on the account. The list
21 * endpoint returns `result: null` (not `[]`) when there are no keys.
22 * Rides out 403 blips (`Forbidden`) from token propagation.
23 */
24const keyListed = (accountId: string, keyId: string) =>
25 stream.getKey({ accountId }).pipe(
26 Effect.map((res) => (res.result ?? []).some((k) => k.id === keyId)),
27 Effect.retry({
28 while: (e) => e._tag === "Forbidden",
29 schedule: Schedule.exponential("500 millis"),
30 times: 8,
31 }),
32 );
33
34const expectGone = (accountId: string, keyId: string) =>
35 keyListed(accountId, keyId).pipe(
36 Effect.flatMap((listed) =>
37 listed
38 ? Effect.fail({ _tag: "SigningKeyNotDeleted" } as const)
39 : Effect.void,
40 ),
41 Effect.retry({
42 while: (e) => e._tag === "SigningKeyNotDeleted",
43 schedule: Schedule.max([
44 Schedule.exponential("500 millis"),
45 Schedule.recurs(10),
46 ]),
47 }),
48 );
49
50test.provider(
51 "create and delete a signing key",
52 (stack) =>
53 Effect.gen(function* () {
54 const { accountId } = yield* yield* CloudflareEnvironment;
55
56 yield* stack.destroy();
57
58 const key = yield* stack.deploy(
59 Cloudflare.Stream.SigningKey("PlaybackKey", {}),
60 );
61
62 expect(key.keyId).toBeTruthy();
63 expect(key.accountId).toEqual(accountId);
64 // The key material is only returned at creation time — it must be
65 // captured in the attributes.
66 expect(Redacted.value(key.pem)).toBeTruthy();
67 expect(Redacted.value(key.jwk)).toBeTruthy();
68
69 expect(yield* keyListed(accountId, key.keyId)).toBe(true);
70
71 // Redeploying is a no-op — same key, same material preserved.
72 const noop = yield* stack.deploy(
73 Cloudflare.Stream.SigningKey("PlaybackKey", {}),
74 );
75 expect(noop.keyId).toEqual(key.keyId);
76 expect(Redacted.value(noop.pem)).toEqual(Redacted.value(key.pem));
77
78 yield* stack.destroy();
79
80 yield* expectGone(accountId, key.keyId);
81 }).pipe(logLevel),
82 {
83 tags: ["provider:cloudflare", "provider:cloudflare:stream", "live"],
84 timeout: 120_000,
85 },
86);
87
88test.provider(
89 "list enumerates the deployed signing key",
90 (stack) =>
91 Effect.gen(function* () {
92 yield* stack.destroy();
93
94 const key = yield* stack.deploy(
95 Cloudflare.Stream.SigningKey("ListKey", {}),
96 );
97
98 const provider = yield* Provider.findProvider(
99 Cloudflare.Stream.SigningKey,
100 );
101 const all = yield* provider.list();
102
103 expect(all.some((k) => k.keyId === key.keyId)).toBe(true);
104
105 yield* stack.destroy();
106 }).pipe(logLevel),
107 {
108 tags: ["provider:cloudflare", "provider:cloudflare:stream", "live"],
109 timeout: 120_000,
110 },
111);