1import * as Mac from "effect/crypto/Mac";
2import * as Effect from "effect/Effect";
3import * as Base64Url from "effect/encoding/Base64Url";
4import * as Result from "effect/Result";
5
6import * as ServerSecretStore from "./ServerSecretStore.ts";
7
8/** HMAC-SHA256 signatures over UTF-8 payloads, as unpadded base64url. */
9export interface PayloadSigner<E = never> {
10 readonly sign: (payload: string) => Effect.Effect<string, E>;
11 readonly verify: (payload: string, signature: string) => Effect.Effect<boolean, E>;
12}
13
14const utf8 = (value: string) => new TextEncoder().encode(value);
15
16const fromKey = <E>(mac: Mac.Mac["Service"], key: Effect.Effect<Mac.Key, E>): PayloadSigner<E> => ({
17 sign: (payload) =>
18 key.pipe(
19 Effect.flatMap((loaded) => mac.sign(loaded, utf8(payload)).pipe(Effect.orDie)),
20 Effect.map(Base64Url.encode),
21 ),
22 verify: (payload, signature) => {
23 const tag = Base64Url.decode(signature);
24 return Result.isFailure(tag)
25 ? Effect.succeed(false)
26 : key.pipe(
27 Effect.flatMap((loaded) =>
28 mac.verify(loaded, tag.success, utf8(payload)).pipe(Effect.orDie),
29 ),
30 );
31 },
32});
33
34/** A signer for secret bytes the caller already holds. */
35export const fromSecret = Effect.fn("PayloadSigner.fromSecret")(function* (secret: Uint8Array) {
36 const mac = yield* Mac.Mac;
37 const key = yield* Mac.key(Mac.hmacSha256, secret).pipe(Effect.orDie);
38 return fromKey(mac, Effect.succeed(key));
39});
40
41/** A signer keyed by a named server secret, loaded on first use so store failures reach callers. */
42export const make = Effect.fn("PayloadSigner.make")(function* (secretName: string) {
43 const secrets = yield* ServerSecretStore.ServerSecretStore;
44 const mac = yield* Mac.Mac;
45 let key: Mac.Key | undefined;
46 const loadKey = Effect.suspend(() =>
47 key !== undefined
48 ? Effect.succeed(key)
49 : secrets.getOrCreateRandom(secretName, 32).pipe(
50 Effect.flatMap((bytes) => Mac.key(Mac.hmacSha256, bytes).pipe(Effect.orDie)),
51 Effect.tap((loaded) =>
52 Effect.sync(() => {
53 key = loaded;
54 }),
55 ),
56 ),
57 );
58 return fromKey(mac, loadKey);
59});