From f219dd71a0cff105772ee3fac3bcf13cc8e9b6f4 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 18 Feb 2026 18:49:16 -0600 Subject: [PATCH 1/2] 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) { -- 2.49.1 From aeac188d40bf14b5d51698526c7fe5dfa5d6d134 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 18 Feb 2026 18:53:25 -0600 Subject: [PATCH 2/2] chore(deps): override minimatch to 10.2.1 for audit fix --- package.json | 1 + pnpm-lock.yaml | 94 +++++++++++++------------------------------------- 2 files changed, 25 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index bc5f6d4..d8fc1e5 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ ], "overrides": { "@isaacs/brace-expansion": ">=5.0.1", + "minimatch": ">=10.2.1", "form-data": ">=2.5.4", "lodash": ">=4.17.23", "lodash-es": ">=4.17.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b9c2a8..68cefe9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: '@isaacs/brace-expansion': '>=5.0.1' + minimatch: '>=10.2.1' form-data: '>=2.5.4' lodash: '>=4.17.23' lodash-es: '>=4.17.23' @@ -1462,14 +1463,6 @@ packages: '@ioredis/commands@1.5.0': resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.1': - resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1522,6 +1515,7 @@ packages: '@mosaicstack/telemetry-client@0.1.1': resolution: {integrity: sha512-1udg6p4cs8rhQgQ2pKCfi7EpRlJieRRhA5CIqthRQ6HQZLgQ0wH+632jEulov3rlHSM1iplIQ+AAe5DWrvSkEA==, tarball: https://git.mosaicstack.dev/api/packages/mosaic/npm/%40mosaicstack%2Ftelemetry-client/-/0.1.1/telemetry-client-0.1.1.tgz} + engines: {node: '>=18'} '@mrleebo/prisma-ast@0.13.1': resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} @@ -3434,8 +3428,9 @@ packages: react-native-b4a: optional: true - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} + engines: {node: 20 || >=22} bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} @@ -3557,11 +3552,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.2: + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -3800,9 +3793,6 @@ packages: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -5372,21 +5362,10 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + minimatch@10.2.1: + resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} engines: {node: 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -7729,7 +7708,7 @@ snapshots: dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 10.2.1 transitivePeerDependencies: - supports-color @@ -7750,7 +7729,7 @@ snapshots: ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 10.2.1 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -8041,12 +8020,6 @@ snapshots: '@ioredis/commands@1.5.0': {} - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.1': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -9942,7 +9915,7 @@ snapshots: '@typescript-eslint/types': 8.54.0 '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 - minimatch: 9.0.5 + minimatch: 10.2.1 semver: 7.7.3 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -10386,7 +10359,7 @@ snapshots: b4a@1.7.3: {} - balanced-match@1.0.2: {} + balanced-match@4.0.3: {} bare-events@2.8.2: {} @@ -10538,14 +10511,9 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.12: + brace-expansion@5.0.2: dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 + balanced-match: 4.0.3 braces@3.0.3: dependencies: @@ -10801,8 +10769,6 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 - concat-map@0.0.1: {} - concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -11455,7 +11421,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 10.2.1 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -11684,7 +11650,7 @@ snapshots: deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 - minimatch: 3.1.2 + minimatch: 10.2.1 node-abort-controller: 3.1.1 schema-utils: 3.3.0 semver: 7.7.3 @@ -11804,14 +11770,14 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 10.2.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 glob@13.0.0: dependencies: - minimatch: 10.1.1 + minimatch: 10.2.1 minipass: 7.1.2 path-scurry: 2.0.1 @@ -12412,21 +12378,9 @@ snapshots: minimalistic-assert@1.0.1: {} - minimatch@10.1.1: + minimatch@10.2.1: dependencies: - '@isaacs/brace-expansion': 5.0.1 - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.2 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.2 minimist@1.2.8: {} @@ -12996,7 +12950,7 @@ snapshots: readdir-glob@1.1.3: dependencies: - minimatch: 5.1.6 + minimatch: 10.2.1 readdirp@4.1.2: {} @@ -13633,7 +13587,7 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 10.5.0 - minimatch: 9.0.5 + minimatch: 10.2.1 text-decoder@1.2.3: dependencies: -- 2.49.1