1import { ManagedRelay } from "@t3tools/client-runtime/relay";
2import { RelayWebClientId } from "@t3tools/contracts/relay";
3import * as Crypto from "effect/Crypto";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Semaphore from "effect/Semaphore";
7
8import {
9 createBrowserDpopProof,
10 generateBrowserDpopKey,
11 readStoredBrowserDpopKey,
12 writeStoredBrowserDpopKey,
13 type BrowserDpopKey,
14} from "./dpop";
15
16const layerRelayDpopSigner = Layer.effect(
17 ManagedRelay.ManagedRelayDpopSigner,
18 Effect.gen(function* () {
19 const crypto = yield* Crypto.Crypto;
20 const keyLoadSemaphore = yield* Semaphore.make(1);
21 let loadedKey: BrowserDpopKey | null = null;
22 const loadOrCreateBrowserDpopKey = keyLoadSemaphore.withPermit(
23 Effect.gen(function* () {
24 if (loadedKey) {
25 return loadedKey;
26 }
27 const stored = yield* readStoredBrowserDpopKey();
28 if (stored) {
29 loadedKey = stored;
30 return stored;
31 }
32 const generated = yield* generateBrowserDpopKey;
33 yield* writeStoredBrowserDpopKey(generated);
34 loadedKey = generated;
35 return generated;
36 }),
37 );
38
39 return ManagedRelay.ManagedRelayDpopSigner.of({
40 thumbprint: loadOrCreateBrowserDpopKey.pipe(
41 Effect.map((proofKey) => proofKey.thumbprint),
42 Effect.mapError(
43 (error) =>
44 new ManagedRelay.ManagedRelayDpopKeyLoadError({
45 keyStore: "indexed-db",
46 cause: error,
47 }),
48 ),
49 Effect.withSpan("web.managedRelayDpopSigner.loadThumbprint"),
50 ),
51 createProof: Effect.fn("web.managedRelayDpopSigner.createProof")(function* (input) {
52 const proofKey = yield* loadOrCreateBrowserDpopKey.pipe(
53 Effect.mapError(
54 (error) =>
55 new ManagedRelay.ManagedRelayDpopProofCreationError({
56 method: input.method,
57 url: input.url,
58 cause: error,
59 }),
60 ),
61 );
62 return yield* createBrowserDpopProof({ ...input, proofKey }).pipe(
63 Effect.provideService(Crypto.Crypto, crypto),
64 Effect.map((proof) => proof.proof),
65 Effect.mapError(
66 (error) =>
67 new ManagedRelay.ManagedRelayDpopProofCreationError({
68 method: input.method,
69 url: input.url,
70 cause: error,
71 }),
72 ),
73 );
74 }),
75 });
76 }),
77);
78
79export const layer = (relayUrl: string) =>
80 ManagedRelay.layer({ relayUrl, clientId: RelayWebClientId }).pipe(
81 Layer.provideMerge(layerRelayDpopSigner),
82 );