feat: support prisma mTLS database connections
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user