1import type * as Effect from "effect/Effect";
2import * as Redacted from "effect/Redacted";
3import type { RuntimeContext } from "../../RuntimeContext.ts";
4import * as Binding from "./Binding.ts";
5import type { SecretKeyBinding } from "./SecretKeyBinding.ts";
6
7const TypeId = "Cloudflare.Workers.SecretKey" as const;
8type TypeId = typeof TypeId;
9
10/**
11 * Data format of the key material, mirroring
12 * [`SubtleCrypto.importKey`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#format).
13 */
14export type SecretKeyFormat = "raw" | "pkcs8" | "spki" | "jwk";
15
16/**
17 * Operations the imported key may be used for, mirroring
18 * [`SubtleCrypto.importKey`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#keyUsages).
19 */
20export type SecretKeyUsage =
21 | "encrypt"
22 | "decrypt"
23 | "sign"
24 | "verify"
25 | "deriveKey"
26 | "deriveBits"
27 | "wrapKey"
28 | "unwrapKey";
29
30/**
31 * Algorithm-specific import parameters, mirroring
32 * [`SubtleCrypto.importKey`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#algorithm)
33 * (e.g. `{ name: "HMAC", hash: "SHA-256" }`, `{ name: "AES-GCM" }`).
34 */
35export interface SecretKeyAlgorithm {
36 /** WebCrypto algorithm name, e.g. `"HMAC"`, `"AES-GCM"`, `"RSA-PSS"`. */
37 readonly name: string;
38 /** Algorithm-specific parameters (`hash`, `length`, `namedCurve`, …). */
39 readonly [param: string]: unknown;
40}
41
42/**
43 * Key material in [JSON Web Key](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#json_web_key)
44 * format, e.g. `{ kty: "oct", k: "<base64url>", alg: "HS256" }`.
45 */
46export interface SecretKeyJwk {
47 /** JWK key type, e.g. `"oct"`, `"RSA"`, `"EC"`. */
48 readonly kty: string;
49 /** Remaining JWK members (`k`, `n`, `e`, `crv`, `alg`, …). */
50 readonly [member: string]: unknown;
51}
52
53export type SecretKeyProps = {
54 /**
55 * Algorithm-specific key parameters passed to WebCrypto's `importKey`,
56 * e.g. `{ name: "HMAC", hash: "SHA-256" }`.
57 */
58 algorithm: SecretKeyAlgorithm;
59 /**
60 * Allowed operations with the key (`"sign"`, `"verify"`, `"encrypt"`, …).
61 */
62 usages: ReadonlyArray<SecretKeyUsage>;
63} & (
64 | {
65 /**
66 * Data format of the key material. `raw` / `pkcs8` / `spki` formats
67 * carry the key as base64 in `keyBase64`.
68 */
69 format: "raw" | "pkcs8" | "spki";
70 /**
71 * Base64-encoded key data (raw bytes, or DER for `pkcs8`/`spki`).
72 * Accepts a `Redacted` value — it is unwrapped when the binding is
73 * registered, since the Cloudflare API accepts only the plain value.
74 */
75 keyBase64: string | Redacted.Redacted<string>;
76 keyJwk?: never;
77 }
78 | {
79 /** Data format of the key material — a JSON Web Key in `keyJwk`. */
80 format: "jwk";
81 /**
82 * Key data in JSON Web Key format. Accepts a `Redacted` value — it is
83 * unwrapped when the binding is registered, since the Cloudflare API
84 * accepts only the plain value.
85 */
86 keyJwk: SecretKeyJwk | Redacted.Redacted<SecretKeyJwk>;
87 keyBase64?: never;
88 }
89);
90
91/**
92 * Deferred accessor for the bound key. The env binding only exists at the
93 * *exec* phase on the deployed Worker, so reading it is behind an Effect
94 * requiring {@link RuntimeContext}. Yield it inside a handler to obtain the
95 * native `CryptoKey`, then use `crypto.subtle` directly.
96 */
97export type SecretKeyAccessor = Effect.Effect<
98 // The ambient global type, matching InferEnv's SecretKeyBinding mapping:
99 // resolves to @cloudflare/workers-types' CryptoKey inside a Worker program
100 // and to the platform CryptoKey elsewhere.
101 CryptoKey,
102 never,
103 RuntimeContext
104>;
105
106/** The resolved (plain, non-Redacted) binding payload carried on a {@link SecretKeyBinding}. */
107export interface SecretKeyPayload {
108 format: SecretKeyFormat;
109 algorithm: SecretKeyAlgorithm;
110 usages: SecretKeyUsage[];
111 keyBase64?: string;
112 keyJwk?: SecretKeyJwk;
113}
114
115const unwrap = <A>(
116 value: A | Redacted.Redacted<A> | undefined,
117): A | undefined =>
118 value === undefined
119 ? undefined
120 : Redacted.isRedacted(value)
121 ? Redacted.value(value)
122 : value;
123
124/**
125 * A Cloudflare Workers Secret Key binding — key material uploaded once and
126 * exposed to the Worker as a native, non-extractable
127 * [`CryptoKey`](https://developers.cloudflare.com/workers/runtime-apis/bindings/secret-key/).
128 * The Worker can sign, verify, encrypt, or decrypt with the key via
129 * `crypto.subtle`, but can never read the raw key material back out.
130 *
131 * `SecretKey` is a single value that is at once the `Binding.Service` tag, the
132 * callable that produces a {@link SecretKeyBinding}, and the type. Declare it
133 * on a Worker's `env` (it flows through `InferEnv` → the native `CryptoKey`)
134 * or `yield*` it inside an Effect-native Worker to attach the binding and
135 * obtain a deferred {@link SecretKeyAccessor}.
136 *
137 * ### Binding inside an Effect-native Worker
138 * **Example:** HMAC sign and verify
139 * ```typescript
140 * Cloudflare.Worker(
141 * "SignerWorker",
142 * { main: import.meta.url },
143 * Effect.gen(function* () {
144 * // Attaches the binding to this Worker AND returns a deferred accessor.
145 * const hmacKey = yield* Cloudflare.Workers.SecretKey("HMAC_KEY", {
146 * format: "raw",
147 * algorithm: { name: "HMAC", hash: "SHA-256" },
148 * usages: ["sign", "verify"],
149 * keyBase64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
150 * });
151 *
152 * return {
153 * fetch: Effect.gen(function* () {
154 * const key = yield* hmacKey;
155 * const data = new TextEncoder().encode("hello");
156 * const signature = yield* Effect.promise(() =>
157 * crypto.subtle.sign("HMAC", key, data),
158 * );
159 * const valid = yield* Effect.promise(() =>
160 * crypto.subtle.verify("HMAC", key, signature, data),
161 * );
162 * return HttpServerResponse.json({ valid });
163 * }),
164 * };
165 * }).pipe(Effect.provide(Cloudflare.Workers.SecretKeyBinding)),
166 * );
167 * ```
168 *
169 * **Example:** JSON Web Key format
170 * ```typescript
171 * const jwkKey = yield* Cloudflare.Workers.SecretKey("HMAC_KEY_JWK", {
172 * format: "jwk",
173 * algorithm: { name: "HMAC", hash: "SHA-256" },
174 * usages: ["sign", "verify"],
175 * keyJwk: {
176 * kty: "oct",
177 * k: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8",
178 * alg: "HS256",
179 * },
180 * });
181 * ```
182 *
183 * ### Declaring on a Worker's env
184 * **Example:** Async (non-Effect) Worker
185 * ```typescript
186 * export const Worker = Cloudflare.Worker("Worker", {
187 * main: "./src/worker.ts",
188 * env: {
189 * HMAC_KEY: Cloudflare.Workers.SecretKey("HMAC_KEY", {
190 * format: "raw",
191 * algorithm: { name: "HMAC", hash: "SHA-256" },
192 * usages: ["sign", "verify"],
193 * keyBase64: Redacted.make("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="),
194 * }),
195 * },
196 * });
197 *
198 * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
199 * // { HMAC_KEY: CryptoKey } — the native runtime binding
200 *
201 * // worker.ts
202 * export default {
203 * fetch: async (req: Request, env: WorkerEnv) => {
204 * const signature = await crypto.subtle.sign(
205 * "HMAC",
206 * env.HMAC_KEY,
207 * new TextEncoder().encode("hello"),
208 * );
209 * return new Response(btoa(String.fromCharCode(...new Uint8Array(signature))));
210 * },
211 * };
212 * ```
213 *
214 * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/secret-key/
215 *
216 * @binding
217 * @product Workers
218 * @category Workers & Compute
219 */
220export interface SecretKey extends Binding.Service<
221 SecretKey,
222 TypeId,
223 SecretKeyAccessor
224> {
225 /**
226 * @param name Binding name (logical id) — the `env` key it resolves to.
227 * @param props Key format, algorithm, usages, and the key material.
228 */
229 (name: string, props: SecretKeyProps): SecretKeyBinding;
230}
231
232export const SecretKey = Binding.Service<SecretKey, SecretKeyPayload>({
233 id: TypeId,
234 defaultName: "SECRET_KEY",
235 parse: (name: string, props: SecretKeyProps) => ({
236 name,
237 format: props.format,
238 algorithm: props.algorithm,
239 usages: [...props.usages],
240 // The wire shape is plain — Redacted key material is unwrapped at bind
241 // time (same convention as Redacted env values → `secret_text`).
242 keyBase64: unwrap(props.keyBase64),
243 keyJwk: unwrap(props.keyJwk),
244 }),
245 toWorkerBinding: (binding) => ({
246 type: "secret_key",
247 name: binding.name,
248 format: binding.format,
249 algorithm: binding.algorithm,
250 usages: binding.usages,
251 keyBase64: binding.keyBase64,
252 keyJwk: binding.keyJwk,
253 }),
254});
255
256export const isSecretKey = (value: unknown): value is SecretKeyBinding =>
257 Binding.isBinding(value) && value.kind === TypeId;