feat: support prisma mTLS database connections

This commit is contained in:
2026-04-18 21:18:43 +08:00
parent f4ffd4abfb
commit 8dbc18c392
4 changed files with 143 additions and 5 deletions
+5
View File
@@ -1,5 +1,10 @@
# Database
DATABASE_URL="postgresql://postgres:password@localhost:5432/agent_park"
PG_SSL_ROOT_CERT_B64=""
PG_SSL_IDENTITY_P12_B64=""
PG_SSL_IDENTITY_PASSWORD=""
# Optional override when your proxy hostname or certificate policy differs
# PG_SSL_MODE="verify-full"
# Webhook API - Generate a secure key for production
WEBHOOK_API_KEY="sk_live_your_secure_api_key_min_32_chars"
+64
View File
@@ -0,0 +1,64 @@
import fs from "fs";
import os from "os";
import path from "path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildPrismaDataSourceUrl } from "./prisma-url";
const ENV_KEYS = [
"DATABASE_URL",
"PG_SSL_ROOT_CERT_B64",
"PG_SSL_IDENTITY_P12_B64",
"PG_SSL_IDENTITY_PASSWORD",
"PG_SSL_MODE",
"PG_SSL_CERT_DIR",
] as const;
const envSnapshot = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
afterEach(() => {
vi.restoreAllMocks();
for (const key of ENV_KEYS) {
const value = envSnapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
describe("buildPrismaDataSourceUrl", () => {
it("returns the original url when TLS env vars are absent", () => {
const url = "postgresql://root:rootroot@103.112.185.248:6432/agent_park";
delete process.env.PG_SSL_ROOT_CERT_B64;
delete process.env.PG_SSL_IDENTITY_P12_B64;
expect(buildPrismaDataSourceUrl(url)).toBe(url);
});
it("writes decoded TLS artifacts and appends Prisma SSL params", () => {
const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-park-prisma-test-"));
const baseUrl = "postgresql://root:rootroot@103.112.185.248:6432/agent_park";
process.env.PG_SSL_CERT_DIR = certDir;
process.env.PG_SSL_ROOT_CERT_B64 = Buffer.from("root-cert").toString("base64");
process.env.PG_SSL_IDENTITY_P12_B64 = Buffer.from("identity-p12").toString("base64");
process.env.PG_SSL_IDENTITY_PASSWORD = "topsecret";
const builtUrl = buildPrismaDataSourceUrl(baseUrl);
const parsed = new URL(builtUrl!);
const rootCertPath = path.join(certDir, "ca.crt");
const identityPath = path.join(certDir, "client-identity.p12");
expect(fs.readFileSync(rootCertPath, "utf8")).toBe("root-cert");
expect(fs.readFileSync(identityPath, "utf8")).toBe("identity-p12");
expect(parsed.searchParams.get("sslmode")).toBe("verify-full");
expect(parsed.searchParams.get("sslrootcert")).toBe(rootCertPath);
expect(parsed.searchParams.get("sslidentity")).toBe(identityPath);
expect(parsed.searchParams.get("sslpassword")).toBe("topsecret");
});
});
+53
View File
@@ -0,0 +1,53 @@
import fs from "fs";
import os from "os";
import path from "path";
const DEFAULT_CERT_DIR = path.join(os.tmpdir(), "agent-park-db-mtls");
function writeFileIfChanged(filePath: string, content: Buffer | string, mode?: number) {
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
const nextContent = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
const currentContent = fs.existsSync(filePath) ? fs.readFileSync(filePath) : null;
if (!currentContent || !currentContent.equals(nextContent)) {
fs.writeFileSync(filePath, nextContent);
}
if (mode !== undefined) {
fs.chmodSync(filePath, mode);
}
}
export function buildPrismaDataSourceUrl(baseUrl = process.env.DATABASE_URL): string | undefined {
if (!baseUrl) {
return undefined;
}
const rootCertB64 = process.env.PG_SSL_ROOT_CERT_B64;
const identityP12B64 = process.env.PG_SSL_IDENTITY_P12_B64;
if (!rootCertB64 || !identityP12B64) {
return baseUrl;
}
const certDir = process.env.PG_SSL_CERT_DIR || DEFAULT_CERT_DIR;
const rootCertPath = path.join(certDir, "ca.crt");
const identityPath = path.join(certDir, "client-identity.p12");
writeFileIfChanged(rootCertPath, Buffer.from(rootCertB64, "base64"), 0o600);
writeFileIfChanged(identityPath, Buffer.from(identityP12B64, "base64"), 0o600);
const url = new URL(baseUrl);
url.searchParams.set("sslmode", process.env.PG_SSL_MODE || "verify-full");
url.searchParams.set("sslrootcert", rootCertPath);
url.searchParams.set("sslidentity", identityPath);
const identityPassword = process.env.PG_SSL_IDENTITY_PASSWORD;
if (identityPassword) {
url.searchParams.set("sslpassword", identityPassword);
}
return url.toString();
}
+21 -5
View File
@@ -1,9 +1,25 @@
import { PrismaClient } from '@prisma/client'
import { PrismaClient } from "@prisma/client";
import { buildPrismaDataSourceUrl } from "@/lib/prisma-url";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
const datasourceUrl = buildPrismaDataSourceUrl();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
export const prisma =
globalForPrisma.prisma ??
new PrismaClient(
datasourceUrl
? {
datasources: {
db: {
url: datasourceUrl,
},
},
}
: undefined,
);
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;