From 731c3e00677c998851fcbb40e3e5eb6ff3b871c5 Mon Sep 17 00:00:00 2001 From: itinsecurity <98172852+itinsecurity@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:26:25 +0100 Subject: [PATCH 1/3] chore: upgrade CI to Node 24 and drop stub @types/bcryptjs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node 24 is Active LTS (since Oct 2025) and matches local dev environment. @types/bcryptjs@3 is a no-op stub — bcryptjs ships its own types. Also aligns @types/node to ^24 to match the runtime. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 10 +++++----- package-lock.json | 27 ++++++++------------------- package.json | 3 +-- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e048cec..78e61c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 20 + node-version: 24 cache: npm - name: Install dependencies run: npm ci @@ -60,7 +60,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 20 + node-version: 24 cache: npm - name: Install dependencies run: npm ci @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 20 + node-version: 24 cache: npm - name: Install dependencies run: npm ci @@ -112,7 +112,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 20 + node-version: 24 cache: npm - name: Install dependencies run: npm ci @@ -154,7 +154,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 20 + node-version: 24 cache: npm - name: Install dependencies run: npm ci diff --git a/package-lock.json b/package-lock.json index ba77556..ca44d03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@prisma/adapter-pg": "^7.5.0", "@prisma/client": "7.5.0", - "@types/bcryptjs": "^3.0.0", "bcryptjs": "^3.0.3", "cheerio": "^1.0.0", "next": "^16.1.7", @@ -28,7 +27,7 @@ "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^16.0.0", - "@types/node": "^20.0.0", + "@types/node": "^24.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", @@ -3715,16 +3714,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/bcryptjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz", - "integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==", - "deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.", - "license": "MIT", - "dependencies": { - "bcryptjs": "*" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3828,12 +3817,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/react": { @@ -11449,9 +11438,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/unpdf": { diff --git a/package.json b/package.json index b08b00a..6faf67a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "dependencies": { "@prisma/adapter-pg": "^7.5.0", "@prisma/client": "7.5.0", - "@types/bcryptjs": "^3.0.0", "bcryptjs": "^3.0.3", "cheerio": "^1.0.0", "next": "^16.1.7", @@ -40,7 +39,7 @@ "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^16.0.0", - "@types/node": "^20.0.0", + "@types/node": "^24.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", From a90f8cbd8c39e6efce29b429be069ee9dbb4fd79 Mon Sep 17 00:00:00 2001 From: itinsecurity <98172852+itinsecurity@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:43:18 +0100 Subject: [PATCH 2/3] feat(db): add instrumentation hook for SST resource binding and lazy Prisma init Add Next.js instrumentation.ts to resolve DATABASE_URL from SST resource bindings at server startup. Refactor db.ts to lazily initialise the Prisma client via a Proxy so it is not created until after instrumentation runs, and throw a descriptive error when DATABASE_URL is unset. Co-Authored-By: Claude Opus 4.6 --- src/instrumentation.ts | 34 +++++++++++++++++++++++++++++++ src/lib/db.ts | 45 ++++++++++++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 src/instrumentation.ts diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 0000000..5f2ba6e --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,34 @@ +/** + * Next.js instrumentation hook — runs once at server startup, before any + * route modules are loaded. + * + * This is the single platform-adapter seam for the app. All platform-specific + * wiring (e.g. resolving SST resource bindings) lives here so the rest of the + * codebase stays platform-agnostic and only reads standard env vars. + */ +export async function register() { + // Only run in the Node.js runtime (not the Edge runtime, which cannot hold + // long-lived database connections anyway). + if (process.env.NEXT_RUNTIME !== "nodejs") return; + + // If DATABASE_URL is already present — set by .env.local, CI, or any platform + // that injects env vars directly — there is nothing to do. + if (process.env.DATABASE_URL) return; + + // SST resource binding: when the app is deployed via SST with a linked + // Database resource the connection URL is available through the Resource API + // rather than as a plain environment variable. We import dynamically so the + // app compiles and starts correctly in environments where the `sst` package + // is not installed (local dev, CI, non-SST platforms). + try { + const { Resource } = await import("sst"); + const url = (Resource as { Database?: { url?: string } }).Database?.url; + if (url) { + process.env.DATABASE_URL = url; + } + } catch { + // `sst` is not available in this environment. DATABASE_URL was not + // resolved here; db.ts will throw a descriptive error on first use if it + // remains unset. + } +} diff --git a/src/lib/db.ts b/src/lib/db.ts index 9fb616f..3db4477 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,9 +1,17 @@ import { PrismaClient } from "@prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; -// Create a new Prisma client reading DATABASE_URL from environment function createPrismaClient(): PrismaClient { - const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); + const url = process.env.DATABASE_URL; + if (!url) { + throw new Error( + "DATABASE_URL is not set. " + + "For local development add it to .env.local. " + + "For other platforms set the environment variable directly. " + + "For SST deployments link a Database resource — instrumentation.ts resolves it automatically.", + ); + } + const adapter = new PrismaPg({ connectionString: url }); return new PrismaClient({ adapter, log: @@ -15,11 +23,28 @@ declare global { var __prisma: PrismaClient | undefined; } -// In test environments: always create fresh client (avoids DATABASE_URL caching issues across tests) -// In development: use global singleton to survive Next.js hot-reload -// In production: create once per process -export const prisma: PrismaClient = - process.env.VITEST !== undefined - ? createPrismaClient() - : (globalThis.__prisma ?? - (globalThis.__prisma = createPrismaClient())); +// Returns the singleton client, creating it on first call. +// +// Using a factory rather than a module-level assignment ensures the client is +// not created until after Next.js instrumentation has run (instrumentation.ts +// resolves platform-specific config such as SST resource bindings before any +// route module is evaluated). +function getClient(): PrismaClient { + // Tests always get a fresh client to avoid DATABASE_URL being cached across + // test files that set different values. + if (process.env.VITEST !== undefined) { + return createPrismaClient(); + } + return (globalThis.__prisma ??= createPrismaClient()); +} + +// Proxy so callers can write `prisma.holding.findMany()` etc. without needing +// to call a getter explicitly. The underlying client is initialised on the +// first property access, guaranteeing instrumentation has already run. +export const prisma = new Proxy({} as PrismaClient, { + get(_target, prop) { + const client = getClient(); + const value = (client as unknown as Record)[prop]; + return typeof value === "function" ? value.bind(client) : value; + }, +}); From a3d6032702d609f8c21259869275f4753b48ae0d Mon Sep 17 00:00:00 2001 From: itinsecurity <98172852+itinsecurity@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:47:19 +0100 Subject: [PATCH 3/3] Revert "feat(db): add instrumentation hook for SST resource binding and lazy Prisma init" This reverts commit a90f8cbd8c39e6efce29b429be069ee9dbb4fd79. --- src/instrumentation.ts | 34 ------------------------------- src/lib/db.ts | 45 ++++++++++-------------------------------- 2 files changed, 10 insertions(+), 69 deletions(-) delete mode 100644 src/instrumentation.ts diff --git a/src/instrumentation.ts b/src/instrumentation.ts deleted file mode 100644 index 5f2ba6e..0000000 --- a/src/instrumentation.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Next.js instrumentation hook — runs once at server startup, before any - * route modules are loaded. - * - * This is the single platform-adapter seam for the app. All platform-specific - * wiring (e.g. resolving SST resource bindings) lives here so the rest of the - * codebase stays platform-agnostic and only reads standard env vars. - */ -export async function register() { - // Only run in the Node.js runtime (not the Edge runtime, which cannot hold - // long-lived database connections anyway). - if (process.env.NEXT_RUNTIME !== "nodejs") return; - - // If DATABASE_URL is already present — set by .env.local, CI, or any platform - // that injects env vars directly — there is nothing to do. - if (process.env.DATABASE_URL) return; - - // SST resource binding: when the app is deployed via SST with a linked - // Database resource the connection URL is available through the Resource API - // rather than as a plain environment variable. We import dynamically so the - // app compiles and starts correctly in environments where the `sst` package - // is not installed (local dev, CI, non-SST platforms). - try { - const { Resource } = await import("sst"); - const url = (Resource as { Database?: { url?: string } }).Database?.url; - if (url) { - process.env.DATABASE_URL = url; - } - } catch { - // `sst` is not available in this environment. DATABASE_URL was not - // resolved here; db.ts will throw a descriptive error on first use if it - // remains unset. - } -} diff --git a/src/lib/db.ts b/src/lib/db.ts index 3db4477..9fb616f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,17 +1,9 @@ import { PrismaClient } from "@prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; +// Create a new Prisma client reading DATABASE_URL from environment function createPrismaClient(): PrismaClient { - const url = process.env.DATABASE_URL; - if (!url) { - throw new Error( - "DATABASE_URL is not set. " + - "For local development add it to .env.local. " + - "For other platforms set the environment variable directly. " + - "For SST deployments link a Database resource — instrumentation.ts resolves it automatically.", - ); - } - const adapter = new PrismaPg({ connectionString: url }); + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); return new PrismaClient({ adapter, log: @@ -23,28 +15,11 @@ declare global { var __prisma: PrismaClient | undefined; } -// Returns the singleton client, creating it on first call. -// -// Using a factory rather than a module-level assignment ensures the client is -// not created until after Next.js instrumentation has run (instrumentation.ts -// resolves platform-specific config such as SST resource bindings before any -// route module is evaluated). -function getClient(): PrismaClient { - // Tests always get a fresh client to avoid DATABASE_URL being cached across - // test files that set different values. - if (process.env.VITEST !== undefined) { - return createPrismaClient(); - } - return (globalThis.__prisma ??= createPrismaClient()); -} - -// Proxy so callers can write `prisma.holding.findMany()` etc. without needing -// to call a getter explicitly. The underlying client is initialised on the -// first property access, guaranteeing instrumentation has already run. -export const prisma = new Proxy({} as PrismaClient, { - get(_target, prop) { - const client = getClient(); - const value = (client as unknown as Record)[prop]; - return typeof value === "function" ? value.bind(client) : value; - }, -}); +// In test environments: always create fresh client (avoids DATABASE_URL caching issues across tests) +// In development: use global singleton to survive Next.js hot-reload +// In production: create once per process +export const prisma: PrismaClient = + process.env.VITEST !== undefined + ? createPrismaClient() + : (globalThis.__prisma ?? + (globalThis.__prisma = createPrismaClient()));