import { Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { JwksClient } from 'jwks-rsa'; import { PrismaService } from '../prisma/prisma.service'; import type { ClientRegistryEntry, ClientRegistryPort, NonceStorePort, PublicKeyResolverPort, } from './context-attestation'; /** Client registry backed by Postgres (IiosClientRegistry). */ @Injectable() export class PrismaClientRegistry implements ClientRegistryPort { constructor(private readonly prisma: PrismaService) {} async find(clientId: string): Promise { const r = await this.prisma.iiosClientRegistry.findUnique({ where: { clientId } }); if (!r) return null; return { clientId: r.clientId, clientType: r.clientType, allowedAppIds: r.allowedAppIds, attestSecret: r.attestSecret ?? undefined, jwksUri: r.jwksUri ?? undefined, status: r.status === 'ACTIVE' ? 'ACTIVE' : 'DISABLED', }; } } /** * Nonce ledger backed by Postgres (IiosAttestationNonce). Reserve-if-absent is atomic via the * unique primary key: a duplicate insert (P2002) means the nonce was already used → replay. */ @Injectable() export class PrismaNonceStore implements NonceStorePort { constructor(private readonly prisma: PrismaService) {} async reserve(input: { nonce: string; clientId: string; expiresAt: Date }): Promise { try { await this.prisma.iiosAttestationNonce.create({ data: input }); return true; } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') return false; // already seen throw e; } } } /** * Resolves a client's public signing key from its JWKS (prod ES256 path). Keeps ONE JwksClient * per jwksUri — the client caches keys and refetches on a cache miss (so key rotation is picked up * without a restart). Returns null on any failure so the verifier fails closed with NO_KEY. */ @Injectable() export class JwksPublicKeyResolver implements PublicKeyResolverPort { private readonly clients = new Map(); private clientFor(jwksUri: string): JwksClient { let c = this.clients.get(jwksUri); if (!c) { c = new JwksClient({ jwksUri, cache: true, cacheMaxEntries: 8, cacheMaxAge: 10 * 60_000, rateLimit: true, jwksRequestsPerMinute: 12 }); this.clients.set(jwksUri, c); } return c; } async resolve(jwksUri: string, kid: string): Promise { try { const key = await this.clientFor(jwksUri).getSigningKey(kid); return key.getPublicKey(); // PEM (SPKI) — works for EC (ES256) and RSA keys } catch { return null; // unknown kid / unreachable JWKS / malformed key → fail closed } } }