1import { describe, expect, it } from "@effect/vitest";
2import { DpopPublicJwk, verifyDpopProof } from "@t3tools/shared/dpop";
3import * as Digest from "effect/crypto/Digest";
4import * as SecureRandom from "effect/crypto/SecureRandom";
5import * as Signature from "effect/crypto/Signature";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Schema from "effect/Schema";
9import * as TestClock from "effect/testing/TestClock";
10
11import * as DpopProof from "./dpopProof.ts";
12
13const decodePublicJwk = Schema.decodeUnknownEffect(DpopPublicJwk);
14
15const layer = Layer.mergeAll(Digest.layerWebCrypto, Signature.layerWebCrypto).pipe(
16 Layer.provideMerge(SecureRandom.layerWebCrypto),
17);
18
19// Mirrors the web adapter: a non-extractable WebCrypto key signed through Signature.
20const webCryptoProofKey = Effect.gen(function* () {
21 const signature = yield* Signature.Signature;
22 const pair = yield* Effect.promise(
23 () =>
24 crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, [
25 "sign",
26 "verify",
27 ]) as Promise<CryptoKeyPair>,
28 );
29 const publicJwk = yield* Effect.promise(() =>
30 crypto.subtle.exportKey("jwk", pair.publicKey),
31 ).pipe(Effect.flatMap(decodePublicJwk), Effect.orDie);
32 const privateKey = yield* Signature.privateKeyFromCryptoKey(
33 Signature.ecdsaP256,
34 pair.privateKey,
35 ).pipe(Effect.orDie);
36 return {
37 publicJwk,
38 sign: (data) => signature.sign(privateKey, data).pipe(Effect.orDie),
39 } satisfies DpopProof.DpopProofKey;
40});
41
42describe("DpopProof", () => {
43 it.effect("creates proofs the relay verifier accepts, with htu normalized and ath bound", () =>
44 Effect.gen(function* () {
45 yield* TestClock.setTime(1_700_000_000_000);
46 const key = yield* webCryptoProofKey;
47 const thumbprint = yield* DpopProof.thumbprint(key.publicJwk);
48 const url = "https://relay.example.test/v1/environments/env-1/connect?debug=1#ignored";
49 const proof = yield* DpopProof.createProof({
50 method: "post",
51 url,
52 accessToken: "relay-access-token",
53 key,
54 });
55
56 expect(
57 verifyDpopProof({
58 proof,
59 method: "POST",
60 url,
61 expectedThumbprint: thumbprint,
62 expectedAccessToken: "relay-access-token",
63 nowEpochSeconds: 1_700_000_000,
64 }),
65 ).toMatchObject({ ok: true, thumbprint });
66 }).pipe(Effect.provide(layer)),
67 );
68
69 it.effect("rejects an unparseable URL before signing", () =>
70 Effect.gen(function* () {
71 const key = yield* webCryptoProofKey;
72 const error = yield* Effect.flip(
73 DpopProof.createProof({ method: "GET", url: "not a url", key }),
74 );
75 expect(error.message).toBe("DPoP URL is invalid.");
76 }).pipe(Effect.provide(layer)),
77 );
78});