diff --git a/src/config.ts b/src/config.ts index fba65159..e01cd21a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -342,6 +342,13 @@ class Conf { ttl: Number(Deno.env.get('DITTO_CACHE_TRANSLATION_TTL') || 6 * 60 * 60 * 1000), }; }, + /** Signer cache settings. */ + get signer(): { max: number; ttl: number } { + return { + max: Number(Deno.env.get('DITTO_CACHE_SIGNER_MAX') || 1000), + ttl: Number(Deno.env.get('DITTO_CACHE_SIGNER_TTL') || 1 * 60 * 60 * 1000), + }; + }, }; } diff --git a/src/metrics.ts b/src/metrics.ts index 7fe75a8f..07759f8a 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -126,6 +126,11 @@ export const cachedTranslationsSizeGauge = new Gauge({ help: 'Number of translated statuses in cache', }); +export const cachedSignersSizeGauge = new Gauge({ + name: 'ditto_cached_signers_size', + help: 'Number of signers in cache', +}); + export const internalSubscriptionsSizeGauge = new Gauge({ name: 'ditto_internal_subscriptions_size', help: "Number of active subscriptions to Ditto's internal relay", diff --git a/src/middleware/signerMiddleware.ts b/src/middleware/signerMiddleware.ts index 61507173..610839c4 100644 --- a/src/middleware/signerMiddleware.ts +++ b/src/middleware/signerMiddleware.ts @@ -1,20 +1,28 @@ import { HTTPException } from '@hono/hono/http-exception'; -import { NSecSigner } from '@nostrify/nostrify'; +import { type NostrSigner, NSecSigner } from '@nostrify/nostrify'; +import { LRUCache } from 'lru-cache'; import { nip19 } from 'nostr-tools'; import { AppMiddleware } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { cachedSignersSizeGauge } from '@/metrics.ts'; import { ConnectSigner } from '@/signers/ConnectSigner.ts'; import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts'; import { Storages } from '@/storages.ts'; import { aesDecrypt } from '@/utils/aes.ts'; import { getTokenHash } from '@/utils/auth.ts'; +const signerCache = new LRUCache(Conf.caches.signer); + /** Make a `signer` object available to all controllers, or unset if the user isn't logged in. */ export const signerMiddleware: AppMiddleware = async (c, next) => { const accessToken = getAccessToken(c.req.raw); - if (accessToken?.startsWith('token1')) { + const cached = accessToken ? signerCache.get(accessToken) : undefined; + + if (cached) { + c.set('signer', cached); + } else if (accessToken?.startsWith('token1')) { try { const kysely = await Storages.kysely(); const tokenHash = await getTokenHash(accessToken as `token1${string}`); @@ -26,8 +34,12 @@ export const signerMiddleware: AppMiddleware = async (c, next) => { .executeTakeFirstOrThrow(); const nep46Seckey = await aesDecrypt(Conf.seckey, nip46_sk_enc); + const signer = new ConnectSigner(pubkey, new NSecSigner(nep46Seckey), nip46_relays); - c.set('signer', new ConnectSigner(pubkey, new NSecSigner(nep46Seckey), nip46_relays)); + signerCache.set(accessToken, signer); + cachedSignersSizeGauge.set(signerCache.size); + + c.set('signer', signer); } catch { throw new HTTPException(401); } diff --git a/src/signers/ConnectSigner.ts b/src/signers/ConnectSigner.ts index 782fdc2a..20b9a11c 100644 --- a/src/signers/ConnectSigner.ts +++ b/src/signers/ConnectSigner.ts @@ -11,14 +11,15 @@ import { Storages } from '@/storages.ts'; */ export class ConnectSigner implements NostrSigner { private signer: Promise; + private cachedPubkey: Promise | undefined; - constructor(private pubkey: string, signer: NostrSigner, private relays?: string[]) { - this.signer = this.init(signer); + constructor(pubkey: string, signer: NostrSigner, private relays?: string[]) { + this.signer = this.init(pubkey, signer); } - async init(signer: NostrSigner): Promise { + async init(pubkey: string, signer: NostrSigner): Promise { return new NConnectSigner({ - pubkey: this.pubkey, + pubkey, // TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list) relay: await Storages.pubsub(), signer, @@ -105,9 +106,11 @@ export class ConnectSigner implements NostrSigner { async getPublicKey(): Promise { const signer = await this.signer; + this.cachedPubkey = this.cachedPubkey ?? signer.getPublicKey(); try { - return await signer.getPublicKey(); + return await this.cachedPubkey; } catch (e) { + this.cachedPubkey = undefined; if (e instanceof Error && e.name === 'AbortError') { throw new HTTPException(408, { message: 'Public key not received quickly enough' }); } else {