1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import SecretKeyWorker from "./fixtures/secret-key/worker.ts";
10
11// `dev: true` runs local providers behind the RPC sidecar proxy by default,
12// matching the process topology of the real `alchemy dev` command.
13const { test } = Test.make({
14 providers: Cloudflare.providers(),
15 dev: true,
16});
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
24 status: number;
25}> {}
26
27/**
28 * `secret_key` bindings are lowered to workerd's native
29 * `Worker_Binding.cryptoKey` config under the local runtime, so the dev
30 * worker sees real non-extractable `CryptoKey`s in `env`. The fixture
31 * carries the same HMAC material as a `raw` binding and a `jwk` binding
32 * and cross-verifies a signature between them.
33 */
34test.provider(
35 "secret_key bindings become native CryptoKeys under the local runtime",
36 (stack) =>
37 Effect.gen(function* () {
38 yield* stack.destroy();
39
40 const deployed = yield* stack.deploy(
41 Effect.gen(function* () {
42 const worker = yield* SecretKeyWorker;
43 return { worker };
44 }),
45 );
46
47 // The local provider serves from the dev proxy — proof no cloud call
48 // ran.
49 expect(deployed.worker.url).toMatch(/^http:\/\/localhost:\d+$/);
50
51 const url = deployed.worker.url;
52 const client = yield* HttpClient.HttpClient;
53
54 const res = yield* Effect.gen(function* () {
55 const res = yield* client.get(`${url}/`);
56 if (res.status !== 200) {
57 return yield* Effect.fail(new WorkerNotReady({ status: res.status }));
58 }
59 return res;
60 }).pipe(
61 Effect.retry({
62 schedule: Schedule.exponential("500 millis"),
63 times: 10,
64 }),
65 );
66
67 const body = (yield* res.json) as {
68 crossVerified: boolean;
69 rawIsCryptoKey: boolean;
70 jwkIsCryptoKey: boolean;
71 algorithm: string;
72 usages: string[];
73 extractable: boolean;
74 };
75 // A signature from the raw-format key verifies with the jwk-format
76 // key — both formats imported the same material correctly.
77 expect(body.crossVerified).toBe(true);
78 expect(body.rawIsCryptoKey).toBe(true);
79 expect(body.jwkIsCryptoKey).toBe(true);
80 expect(body.algorithm).toBe("HMAC");
81 expect(body.usages).toEqual(["sign", "verify"]);
82 // The Cloudflare API never marks bound keys extractable; the local
83 // lowering matches.
84 expect(body.extractable).toBe(false);
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 {
89 tags: ["provider:cloudflare", "provider:cloudflare:worker", "local"],
90 timeout: 120_000,
91 },
92);