Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions apps/api/caBundleExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { BuildContext, BuildExtension, BuildManifest } from '@trigger.dev/build';
import { existsSync } from 'node:fs';
import { cp, mkdir } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';

// Path relative to the monorepo root (apps/api or apps/app → ../../packages/db/certs/...)
const BUNDLE_RELATIVE_FROM_APP = '../../packages/db/certs/rds-global-bundle.pem';
const BUNDLE_DEST_REL = 'certs/rds-global-bundle.pem';

function findBundleSrc(workingDir: string): string | undefined {
// Walk up from workingDir to find the cert — handles both normal checkouts and git worktrees
// where workspaceDir points to the main worktree root (wrong for us).
const candidates = [
resolve(workingDir, BUNDLE_RELATIVE_FROM_APP),
resolve(workingDir, '../packages/db/certs/rds-global-bundle.pem'),
resolve(workingDir, 'packages/db/certs/rds-global-bundle.pem'),
];

return candidates.find((c) => existsSync(c));
}

export function caBundleExtension(): BuildExtension {
return {
name: 'CABundleExtension',
onBuildStart: (context) => {
// Real OS env var at task spawn time — verified flow:
// addLayer.deploy.env → manifest.deploy.sync.env → syncEnvVarsWithServer →
// taskRunProcessProvider injects into worker env before Node TLS init.
context.addLayer({
id: 'ca-bundle-env',
deploy: {
env: { NODE_EXTRA_CA_CERTS: `/app/${BUNDLE_DEST_REL}` },
override: true,
},
});
},
onBuildComplete: async (context: BuildContext, manifest: BuildManifest) => {
const src = findBundleSrc(context.workingDir);
if (!src) {
throw new Error(
`CABundleExtension: rds-global-bundle.pem not found. Searched relative to ${context.workingDir}`,
);
}
const dest = join(manifest.outputPath, BUNDLE_DEST_REL);
await mkdir(dirname(dest), { recursive: true });
await cp(src, dest);
context.logger.log(`Copied RDS CA bundle to ${BUNDLE_DEST_REL}`);
},
};
}
32 changes: 27 additions & 5 deletions apps/api/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const globalForPrisma = global as unknown as { prisma: PrismaClient };
const globalForPrisma = global as unknown as { prisma?: PrismaClient };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);

Expand Down Expand Up @@ -40,11 +40,18 @@ function createPrismaClient(): PrismaClient {
// silently downgrading.
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
const allowInsecure = process.env.PRISMA_ALLOW_INSECURE_TLS === '1';
let ssl: undefined | true | { rejectUnauthorized: false };
let ssl:
| undefined
| { checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };
if (isLocalhost) {
ssl = undefined;
} else if (hasCABundle) {
ssl = true;
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
ssl = { checkServerIdentity: () => undefined };
} else if (allowInsecure) {
ssl = { rejectUnauthorized: false };
} else {
Expand All @@ -63,6 +70,21 @@ function createPrismaClient(): PrismaClient {
});
}

export const db = globalForPrisma.prisma || createPrismaClient();
// Lazy initialization. Importing this module does NOT construct a Prisma client
// — that only happens on first property access on `db`. Critical so that
// Next.js `next build` (which imports every route handler to analyze it) does
// not trigger the strict TLS check at build time when no actual queries run.
function getClient(): PrismaClient {
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = createPrismaClient();
}
return globalForPrisma.prisma;
}

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
export const db = new Proxy({} as PrismaClient, {
get(_target, prop, _receiver) {
const client = getClient();
const value = Reflect.get(client, prop, client);
return typeof value === 'function' ? value.bind(client) : value;
},
});
2 changes: 2 additions & 0 deletions apps/api/trigger.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from '@trigger.dev/sdk';
import { caBundleExtension } from './caBundleExtension';
import { prismaExtension } from './customPrismaExtension';
import { emailExtension } from './emailExtension';
import { integrationPlatformExtension } from './integrationPlatformExtension';
Expand All @@ -10,6 +11,7 @@ export default defineConfig({
maxDuration: 300, // 5 minutes
build: {
extensions: [
caBundleExtension(),
prismaExtension({
version: '7.6.0',
dbPackageVersion: '^2.0.0',
Expand Down
50 changes: 50 additions & 0 deletions apps/app/caBundleExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { BuildContext, BuildExtension, BuildManifest } from '@trigger.dev/build';
import { existsSync } from 'node:fs';
import { cp, mkdir } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';

// Path relative to the monorepo root (apps/api or apps/app → ../../packages/db/certs/...)
const BUNDLE_RELATIVE_FROM_APP = '../../packages/db/certs/rds-global-bundle.pem';
const BUNDLE_DEST_REL = 'certs/rds-global-bundle.pem';

function findBundleSrc(workingDir: string): string | undefined {
// Walk up from workingDir to find the cert — handles both normal checkouts and git worktrees
// where workspaceDir points to the main worktree root (wrong for us).
const candidates = [
resolve(workingDir, BUNDLE_RELATIVE_FROM_APP),
resolve(workingDir, '../packages/db/certs/rds-global-bundle.pem'),
resolve(workingDir, 'packages/db/certs/rds-global-bundle.pem'),
];

return candidates.find((c) => existsSync(c));
}

export function caBundleExtension(): BuildExtension {
return {
name: 'CABundleExtension',
onBuildStart: (context) => {
// Real OS env var at task spawn time — verified flow:
// addLayer.deploy.env → manifest.deploy.sync.env → syncEnvVarsWithServer →
// taskRunProcessProvider injects into worker env before Node TLS init.
context.addLayer({
id: 'ca-bundle-env',
deploy: {
env: { NODE_EXTRA_CA_CERTS: `/app/${BUNDLE_DEST_REL}` },
override: true,
},
});
},
onBuildComplete: async (context: BuildContext, manifest: BuildManifest) => {
const src = findBundleSrc(context.workingDir);
if (!src) {
throw new Error(
`CABundleExtension: rds-global-bundle.pem not found. Searched relative to ${context.workingDir}`,
);
}
const dest = join(manifest.outputPath, BUNDLE_DEST_REL);
await mkdir(dirname(dest), { recursive: true });
await cp(src, dest);
context.logger.log(`Copied RDS CA bundle to ${BUNDLE_DEST_REL}`);
},
};
}
3 changes: 3 additions & 0 deletions apps/app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ const config: NextConfig = {
webpackMemoryOptimizations: true,
},
outputFileTracingRoot: workspaceRoot,
outputFileTracingIncludes: {
'/**/*': ['../../packages/db/certs/rds-global-bundle.pem'],
},

// Reduce memory usage during production build
productionBrowserSourceMaps: false,
Expand Down
61 changes: 53 additions & 8 deletions apps/app/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,52 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const globalForPrisma = global as unknown as { prisma: PrismaClient };
const globalForPrisma = global as unknown as { prisma?: PrismaClient };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);

function stripSslMode(connectionString: string): string {
const url = new URL(connectionString);
url.searchParams.delete('sslmode');
return url.toString();
}

function isLocalhostUrl(connectionString: string): boolean {
try {
const { hostname } = new URL(connectionString);
const stripped = hostname.replace(/^\[/, '').replace(/\]$/, '');
return LOCAL_HOSTNAMES.has(stripped);
} catch {
return false;
}
}

function createPrismaClient(): PrismaClient {
const rawUrl = process.env.DATABASE_URL!;
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
const isLocalhost = isLocalhostUrl(rawUrl);
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
const allowInsecure = process.env.PRISMA_ALLOW_INSECURE_TLS === '1';

let ssl:
| undefined
| { checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };
if (isLocalhost) {
ssl = undefined;
} else if (hasCABundle) {
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
ssl = { checkServerIdentity: () => undefined };
} else if (allowInsecure) {
ssl = { rejectUnauthorized: false };
} else {
throw new Error(
'Refusing to connect to a non-local Postgres without TLS verification. Set NODE_EXTRA_CA_CERTS to a CA bundle, or set PRISMA_ALLOW_INSECURE_TLS=1 if you intentionally want unverified TLS.',
);
}

const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
const adapter = new PrismaPg({ connectionString: url, ssl });
return new PrismaClient({
Expand All @@ -27,6 +57,21 @@ function createPrismaClient(): PrismaClient {
});
}

export const db = globalForPrisma.prisma || createPrismaClient();
// Lazy initialization. Importing this module does NOT construct a Prisma client
// — that only happens on first property access on `db`. Critical so that
// Next.js `next build` (which imports every route handler to analyze it) does
// not trigger the strict TLS check at build time when no actual queries run.
function getClient(): PrismaClient {
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = createPrismaClient();
}
return globalForPrisma.prisma;
}

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
export const db = new Proxy({} as PrismaClient, {
get(_target, prop, _receiver) {
const client = getClient();
const value = Reflect.get(client, prop, client);
return typeof value === 'function' ? value.bind(client) : value;
},
});
2 changes: 2 additions & 0 deletions apps/app/trigger.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { puppeteer } from '@trigger.dev/build/extensions/puppeteer';
import { defineConfig } from '@trigger.dev/sdk';
import { caBundleExtension } from './caBundleExtension';
import { prismaExtension } from './customPrismaExtension';

export default defineConfig({
Expand All @@ -14,6 +15,7 @@ export default defineConfig({
maxDuration: 300, // 5 minutes
build: {
extensions: [
caBundleExtension(),
prismaExtension({
version: '7.6.0',
dbPackageVersion: '^2.0.0',
Expand Down
5 changes: 4 additions & 1 deletion apps/app/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test-utils/setup.ts'],
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
include: [
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'prisma/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
],
exclude: ['node_modules', 'dist', '.next', 'e2e'],
coverage: {
reporter: ['text', 'json', 'html'],
Expand Down
61 changes: 53 additions & 8 deletions apps/framework-editor/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,52 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const globalForPrisma = global as unknown as { prisma: PrismaClient };
const globalForPrisma = global as unknown as { prisma?: PrismaClient };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);

function stripSslMode(connectionString: string): string {
const url = new URL(connectionString);
url.searchParams.delete('sslmode');
return url.toString();
}

function isLocalhostUrl(connectionString: string): boolean {
try {
const { hostname } = new URL(connectionString);
const stripped = hostname.replace(/^\[/, '').replace(/\]$/, '');
return LOCAL_HOSTNAMES.has(stripped);
} catch {
return false;
}
}

function createPrismaClient(): PrismaClient {
const rawUrl = process.env.DATABASE_URL!;
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
const isLocalhost = isLocalhostUrl(rawUrl);
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
const allowInsecure = process.env.PRISMA_ALLOW_INSECURE_TLS === '1';

let ssl:
| undefined
| { checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };
if (isLocalhost) {
ssl = undefined;
} else if (hasCABundle) {
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
ssl = { checkServerIdentity: () => undefined };
} else if (allowInsecure) {
ssl = { rejectUnauthorized: false };
} else {
throw new Error(
'Refusing to connect to a non-local Postgres without TLS verification. Set NODE_EXTRA_CA_CERTS to a CA bundle, or set PRISMA_ALLOW_INSECURE_TLS=1 if you intentionally want unverified TLS.',
);
}

const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
const adapter = new PrismaPg({ connectionString: url, ssl });
return new PrismaClient({
Expand All @@ -27,6 +57,21 @@ function createPrismaClient(): PrismaClient {
});
}

export const db = globalForPrisma.prisma || createPrismaClient();
// Lazy initialization. Importing this module does NOT construct a Prisma client
// — that only happens on first property access on `db`. Critical so that
// Next.js `next build` (which imports every route handler to analyze it) does
// not trigger the strict TLS check at build time when no actual queries run.
function getClient(): PrismaClient {
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = createPrismaClient();
}
return globalForPrisma.prisma;
}

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
export const db = new Proxy({} as PrismaClient, {
get(_target, prop, _receiver) {
const client = getClient();
const value = Reflect.get(client, prop, client);
return typeof value === 'function' ? value.bind(client) : value;
},
});
3 changes: 3 additions & 0 deletions apps/portal/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const config = {
},
skipTrailingSlashRedirect: true,
outputFileTracingRoot: path.join(__dirname, '../../'),
outputFileTracingIncludes: {
'/**/*': ['../../packages/db/certs/rds-global-bundle.pem'],
},
...(isStandalone
? {
output: 'standalone' as const,
Expand Down
Loading
Loading