From f219dd71a0cff105772ee3fac3bcf13cc8e9b6f4 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 18 Feb 2026 18:49:16 -0600 Subject: [PATCH] fix(auth): use UUID id generation for BetterAuth DB models --- apps/api/src/auth/auth.config.spec.ts | 15 +++++++++ apps/api/src/auth/auth.config.ts | 6 ++-- .../telemetry/telemetry.interceptor.spec.ts | 31 +++++++++++++++++++ .../src/telemetry/telemetry.interceptor.ts | 2 +- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/apps/api/src/auth/auth.config.spec.ts b/apps/api/src/auth/auth.config.spec.ts index 0485eb6..892a18a 100644 --- a/apps/api/src/auth/auth.config.spec.ts +++ b/apps/api/src/auth/auth.config.spec.ts @@ -524,6 +524,21 @@ describe("auth.config", () => { expect(config.session.updateAge).toBe(7200); }); + it("should configure BetterAuth database ID generation as UUID", () => { + const mockPrisma = {} as PrismaClient; + createAuth(mockPrisma); + + expect(mockBetterAuth).toHaveBeenCalledOnce(); + const config = mockBetterAuth.mock.calls[0][0] as { + advanced: { + database: { + generateId: string; + }; + }; + }; + expect(config.advanced.database.generateId).toBe("uuid"); + }); + it("should set httpOnly cookie attribute to true", () => { const mockPrisma = {} as PrismaClient; createAuth(mockPrisma); diff --git a/apps/api/src/auth/auth.config.ts b/apps/api/src/auth/auth.config.ts index d8597fa..d1c1554 100644 --- a/apps/api/src/auth/auth.config.ts +++ b/apps/api/src/auth/auth.config.ts @@ -1,4 +1,3 @@ -import { randomUUID } from "node:crypto"; import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { genericOAuth } from "better-auth/plugins"; @@ -260,7 +259,10 @@ export function createAuth(prisma: PrismaClient) { updateAge: 60 * 60 * 2, // 2 hours — minimum session age before BetterAuth refreshes the expiry on next request }, advanced: { - generateId: () => randomUUID(), + database: { + // BetterAuth's default ID generator emits opaque strings; our auth tables use UUID PKs. + generateId: "uuid", + }, defaultCookieAttributes: { httpOnly: true, secure: process.env.NODE_ENV === "production", diff --git a/apps/api/src/telemetry/telemetry.interceptor.spec.ts b/apps/api/src/telemetry/telemetry.interceptor.spec.ts index 8aadeac..504bb8d 100644 --- a/apps/api/src/telemetry/telemetry.interceptor.spec.ts +++ b/apps/api/src/telemetry/telemetry.interceptor.spec.ts @@ -50,6 +50,8 @@ describe("TelemetryInterceptor", () => { getResponse: vi.fn().mockReturnValue({ statusCode: 200, setHeader: vi.fn(), + headersSent: false, + writableEnded: false, }), }), getClass: vi.fn().mockReturnValue({ name: "TestController" }), @@ -101,6 +103,35 @@ describe("TelemetryInterceptor", () => { expect(mockResponse.setHeader).toHaveBeenCalledWith("x-trace-id", "test-trace-id"); }); + it("should not set trace header when response is already committed", async () => { + const committedResponseContext = { + ...mockContext, + switchToHttp: vi.fn().mockReturnValue({ + getRequest: vi.fn().mockReturnValue({ + method: "GET", + url: "/api/test", + path: "/api/test", + }), + getResponse: vi.fn().mockReturnValue({ + statusCode: 200, + setHeader: vi.fn(), + headersSent: true, + writableEnded: true, + }), + }), + } as unknown as ExecutionContext; + + mockHandler = { + handle: vi.fn().mockReturnValue(of({ data: "test" })), + } as unknown as CallHandler; + + const committedResponse = committedResponseContext.switchToHttp().getResponse(); + + await lastValueFrom(interceptor.intercept(committedResponseContext, mockHandler)); + + expect(committedResponse.setHeader).not.toHaveBeenCalled(); + }); + it("should record exception on error", async () => { const error = new Error("Test error"); mockHandler = { diff --git a/apps/api/src/telemetry/telemetry.interceptor.ts b/apps/api/src/telemetry/telemetry.interceptor.ts index 5f9449e..d85a544 100644 --- a/apps/api/src/telemetry/telemetry.interceptor.ts +++ b/apps/api/src/telemetry/telemetry.interceptor.ts @@ -88,7 +88,7 @@ export class TelemetryInterceptor implements NestInterceptor { // Add trace context to response headers for distributed tracing const spanContext = span.spanContext(); - if (spanContext.traceId) { + if (spanContext.traceId && !response.headersSent && !response.writableEnded) { response.setHeader("x-trace-id", spanContext.traceId); } } catch (error) {