@igstack/app-catalog-backend-core 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/client.d.mts.map +1 -1
- package/dist/db/client.mjs +6 -1
- package/dist/db/client.mjs.map +1 -1
- package/dist/db/index.d.mts +1 -0
- package/dist/db/sslConfig.d.mts +28 -0
- package/dist/db/sslConfig.d.mts.map +1 -0
- package/dist/db/sslConfig.mjs +71 -0
- package/dist/db/sslConfig.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/middleware/database.d.mts.map +1 -1
- package/dist/middleware/database.mjs +6 -1
- package/dist/middleware/database.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/sslConfig.test.ts +90 -0
- package/src/db/client.ts +10 -2
- package/src/db/index.ts +1 -0
- package/src/db/sslConfig.ts +94 -0
- package/src/index.ts +1 -0
- package/src/middleware/database.ts +9 -2
package/dist/db/client.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.mts","names":[],"sources":["../../src/db/client.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../../src/db/client.ts"],"mappings":";;;;;AAYA;;iBAAgB,WAAA,CAAA,GAAe,YAAA;;;AA8B/B;;iBAAgB,WAAA,CAAY,MAAA,EAAQ,YAAA;;;AAQpC;;iBAAsB,SAAA,CAAA,GAAa,OAAA;;;AASnC;;iBAAsB,YAAA,CAAA,GAAgB,OAAA"}
|
package/dist/db/client.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PrismaClient } from "../generated/prisma/client.mjs";
|
|
2
|
+
import { buildPgSslConfig } from "./sslConfig.mjs";
|
|
2
3
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
3
4
|
import pg from "pg";
|
|
4
5
|
|
|
@@ -13,7 +14,11 @@ function getDbClient() {
|
|
|
13
14
|
if (!prismaClient) {
|
|
14
15
|
const databaseUrl = process.env.AC_CORE_DATABASE_URL;
|
|
15
16
|
if (!databaseUrl) throw new Error("PrismaClient not initialized. You must call createAcMiddleware() before using database functions, or set AC_CORE_DATABASE_URL environment variable for standalone usage.");
|
|
16
|
-
|
|
17
|
+
const ssl = buildPgSslConfig();
|
|
18
|
+
pool = new pg.Pool({
|
|
19
|
+
connectionString: databaseUrl,
|
|
20
|
+
...ssl === void 0 ? {} : { ssl }
|
|
21
|
+
});
|
|
17
22
|
prismaClient = new PrismaClient({ adapter: new PrismaPg(pool) });
|
|
18
23
|
}
|
|
19
24
|
return prismaClient;
|
package/dist/db/client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../../src/db/client.ts"],"sourcesContent":["import { PrismaClient } from '../generated/prisma/client'\nimport { PrismaPg } from '@prisma/adapter-pg'\nimport pg from 'pg'\n\nlet prismaClient: PrismaClient | null = null\nlet pool: pg.Pool | null = null\n\n/**\n * Gets the internal Prisma client instance.\n * Creates one if it doesn't exist.\n */\nexport function getDbClient(): PrismaClient {\n if (!prismaClient) {\n const databaseUrl = process.env.AC_CORE_DATABASE_URL\n if (!databaseUrl) {\n throw new Error(\n 'PrismaClient not initialized. You must call createAcMiddleware() before using database functions, ' +\n 'or set AC_CORE_DATABASE_URL environment variable for standalone usage.',\n )\n }\n\n // Prisma 7 with adapter: Create pg pool and wrap with adapter\n pool = new pg.Pool({
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../../src/db/client.ts"],"sourcesContent":["import { PrismaClient } from '../generated/prisma/client'\nimport { PrismaPg } from '@prisma/adapter-pg'\nimport pg from 'pg'\nimport { buildPgSslConfig } from './sslConfig'\n\nlet prismaClient: PrismaClient | null = null\nlet pool: pg.Pool | null = null\n\n/**\n * Gets the internal Prisma client instance.\n * Creates one if it doesn't exist.\n */\nexport function getDbClient(): PrismaClient {\n if (!prismaClient) {\n const databaseUrl = process.env.AC_CORE_DATABASE_URL\n if (!databaseUrl) {\n throw new Error(\n 'PrismaClient not initialized. You must call createAcMiddleware() before using database functions, ' +\n 'or set AC_CORE_DATABASE_URL environment variable for standalone usage.',\n )\n }\n\n // Prisma 7 with adapter: Create pg pool and wrap with adapter.\n // SSL comes from PGSSLMODE/PGSSLROOTCERT via our helper (node-postgres\n // doesn't honor those correctly for a connection-string pool — see\n // buildPgSslConfig).\n const ssl = buildPgSslConfig()\n pool = new pg.Pool({\n connectionString: databaseUrl,\n ...(ssl === undefined ? {} : { ssl }),\n })\n const adapter = new PrismaPg(pool)\n\n prismaClient = new PrismaClient({ adapter })\n }\n return prismaClient\n}\n\n/**\n * Sets the internal Prisma client instance.\n * Used by middleware to bridge with existing getDbClient() usage.\n */\nexport function setDbClient(client: PrismaClient): void {\n prismaClient = client\n}\n\n/**\n * Connects to the database.\n * Call this before performing database operations.\n */\nexport async function connectDb(): Promise<void> {\n const client = getDbClient()\n await client.$connect()\n}\n\n/**\n * Disconnects from the database.\n * Call this when done with database operations (e.g., in scripts).\n */\nexport async function disconnectDb(): Promise<void> {\n if (prismaClient) {\n await prismaClient.$disconnect()\n prismaClient = null\n }\n if (pool) {\n await pool.end()\n pool = null\n }\n}\n"],"mappings":";;;;;;AAKA,IAAI,eAAoC;AACxC,IAAI,OAAuB;;;;;AAM3B,SAAgB,cAA4B;AAC1C,KAAI,CAAC,cAAc;EACjB,MAAM,cAAc,QAAQ,IAAI;AAChC,MAAI,CAAC,YACH,OAAM,IAAI,MACR,2KAED;EAOH,MAAM,MAAM,kBAAkB;AAC9B,SAAO,IAAI,GAAG,KAAK;GACjB,kBAAkB;GAClB,GAAI,QAAQ,SAAY,EAAE,GAAG,EAAE,KAAK;GACrC,CAAC;AAGF,iBAAe,IAAI,aAAa,EAAE,SAFlB,IAAI,SAAS,KAAK,EAES,CAAC;;AAE9C,QAAO;;;;;;AAOT,SAAgB,YAAY,QAA4B;AACtD,gBAAe;;;;;;AAOjB,eAAsB,YAA2B;AAE/C,OADe,aAAa,CACf,UAAU;;;;;;AAOzB,eAAsB,eAA8B;AAClD,KAAI,cAAc;AAChB,QAAM,aAAa,aAAa;AAChC,iBAAe;;AAEjB,KAAI,MAAM;AACR,QAAM,KAAK,KAAK;AAChB,SAAO"}
|
package/dist/db/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectDb, disconnectDb, getDbClient, setDbClient } from "./client.mjs";
|
|
2
|
+
import { buildPgSslConfig } from "./sslConfig.mjs";
|
|
2
3
|
import { MakeTFromPrismaModel, ObjectKeys, ScalarFilter, ScalarKeys, TableSyncParamsPrisma, tableSyncPrisma } from "./tableSyncPrismaAdapter.mjs";
|
|
3
4
|
import { TABLE_SYNC_MAGAZINE, TableSyncMagazine, TableSyncMagazineModelNameKey } from "./tableSyncMagazine.mjs";
|
|
4
5
|
import { SyncAppCatalogResult, syncAppCatalog } from "./syncAppCatalog.mjs";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ConnectionOptions } from "node:tls";
|
|
2
|
+
|
|
3
|
+
//#region src/db/sslConfig.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the node-postgres `ssl` PoolConfig option from libpq-style env vars,
|
|
6
|
+
* because node-postgres does NOT do this correctly on its own for a
|
|
7
|
+
* connection-string pool: its built-in env handling maps `PGSSLMODE=verify-full`
|
|
8
|
+
* to a bare `ssl: true` (Node's default trust store) and IGNORES `PGSSLROOTCERT`
|
|
9
|
+
* — so an RDS server cert signed by the Amazon RDS CA would fail to verify.
|
|
10
|
+
*
|
|
11
|
+
* We therefore construct the `ssl` object explicitly and pass it to `new
|
|
12
|
+
* pg.Pool({ connectionString, ssl })`. Semantics mirror libpq / pg-connection-string:
|
|
13
|
+
* - verify-full → validate CA chain AND hostname (Node defaults; no overrides)
|
|
14
|
+
* - verify-ca → validate CA chain, skip hostname (checkServerIdentity no-op)
|
|
15
|
+
* - require/prefer → encrypt but don't validate (unless a CA is given → verify-ca-like)
|
|
16
|
+
* - no-verify → encrypt, don't validate (rejectUnauthorized:false)
|
|
17
|
+
* - disable / unset → no TLS
|
|
18
|
+
*
|
|
19
|
+
* `PGSSLROOTCERT` points at a CA bundle on disk (e.g. the Amazon RDS
|
|
20
|
+
* global-bundle.pem baked into the image).
|
|
21
|
+
*
|
|
22
|
+
* @returns a `ConnectionOptions` object, `false` for disabled, or `undefined`
|
|
23
|
+
* to leave SSL unset (let node-postgres fall back to its defaults).
|
|
24
|
+
*/
|
|
25
|
+
declare function buildPgSslConfig(env?: NodeJS.ProcessEnv): ConnectionOptions | boolean | undefined;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { buildPgSslConfig };
|
|
28
|
+
//# sourceMappingURL=sslConfig.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sslConfig.d.mts","names":[],"sources":["../../src/db/sslConfig.ts"],"mappings":";;;;;AAwBA;;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,CACd,GAAA,GAAK,MAAA,CAAO,UAAA,GACX,iBAAA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
//#region src/db/sslConfig.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the node-postgres `ssl` PoolConfig option from libpq-style env vars,
|
|
6
|
+
* because node-postgres does NOT do this correctly on its own for a
|
|
7
|
+
* connection-string pool: its built-in env handling maps `PGSSLMODE=verify-full`
|
|
8
|
+
* to a bare `ssl: true` (Node's default trust store) and IGNORES `PGSSLROOTCERT`
|
|
9
|
+
* — so an RDS server cert signed by the Amazon RDS CA would fail to verify.
|
|
10
|
+
*
|
|
11
|
+
* We therefore construct the `ssl` object explicitly and pass it to `new
|
|
12
|
+
* pg.Pool({ connectionString, ssl })`. Semantics mirror libpq / pg-connection-string:
|
|
13
|
+
* - verify-full → validate CA chain AND hostname (Node defaults; no overrides)
|
|
14
|
+
* - verify-ca → validate CA chain, skip hostname (checkServerIdentity no-op)
|
|
15
|
+
* - require/prefer → encrypt but don't validate (unless a CA is given → verify-ca-like)
|
|
16
|
+
* - no-verify → encrypt, don't validate (rejectUnauthorized:false)
|
|
17
|
+
* - disable / unset → no TLS
|
|
18
|
+
*
|
|
19
|
+
* `PGSSLROOTCERT` points at a CA bundle on disk (e.g. the Amazon RDS
|
|
20
|
+
* global-bundle.pem baked into the image).
|
|
21
|
+
*
|
|
22
|
+
* @returns a `ConnectionOptions` object, `false` for disabled, or `undefined`
|
|
23
|
+
* to leave SSL unset (let node-postgres fall back to its defaults).
|
|
24
|
+
*/
|
|
25
|
+
function buildPgSslConfig(env = process.env) {
|
|
26
|
+
const mode = env.PGSSLMODE;
|
|
27
|
+
const rootCertPath = env.PGSSLROOTCERT;
|
|
28
|
+
const readCa = () => {
|
|
29
|
+
if (!rootCertPath) return void 0;
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(rootCertPath, "utf8");
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new Error(`PGSSLROOTCERT is set to "${rootCertPath}" but the CA bundle could not be read: ${err instanceof Error ? err.message : String(err)}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
switch (mode) {
|
|
37
|
+
case "disable": return false;
|
|
38
|
+
case "verify-full": {
|
|
39
|
+
const ca = readCa();
|
|
40
|
+
if (!ca) throw new Error("PGSSLMODE=verify-full requires PGSSLROOTCERT to point at a CA bundle (node-postgres would otherwise use the system trust store, which does not include the Amazon RDS CA). Set PGSSLROOTCERT.");
|
|
41
|
+
return {
|
|
42
|
+
ca,
|
|
43
|
+
rejectUnauthorized: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
case "verify-ca": {
|
|
47
|
+
const ca = readCa();
|
|
48
|
+
if (!ca) throw new Error("PGSSLMODE=verify-ca requires PGSSLROOTCERT to point at a CA bundle.");
|
|
49
|
+
return {
|
|
50
|
+
ca,
|
|
51
|
+
rejectUnauthorized: true,
|
|
52
|
+
checkServerIdentity: () => void 0
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
case "require":
|
|
56
|
+
case "prefer": {
|
|
57
|
+
const ca = readCa();
|
|
58
|
+
return ca ? {
|
|
59
|
+
ca,
|
|
60
|
+
rejectUnauthorized: true,
|
|
61
|
+
checkServerIdentity: () => void 0
|
|
62
|
+
} : { rejectUnauthorized: false };
|
|
63
|
+
}
|
|
64
|
+
case "no-verify": return { rejectUnauthorized: false };
|
|
65
|
+
default: return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { buildPgSslConfig };
|
|
71
|
+
//# sourceMappingURL=sslConfig.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sslConfig.mjs","names":[],"sources":["../../src/db/sslConfig.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport type { ConnectionOptions } from 'node:tls'\n\n/**\n * Build the node-postgres `ssl` PoolConfig option from libpq-style env vars,\n * because node-postgres does NOT do this correctly on its own for a\n * connection-string pool: its built-in env handling maps `PGSSLMODE=verify-full`\n * to a bare `ssl: true` (Node's default trust store) and IGNORES `PGSSLROOTCERT`\n * — so an RDS server cert signed by the Amazon RDS CA would fail to verify.\n *\n * We therefore construct the `ssl` object explicitly and pass it to `new\n * pg.Pool({ connectionString, ssl })`. Semantics mirror libpq / pg-connection-string:\n * - verify-full → validate CA chain AND hostname (Node defaults; no overrides)\n * - verify-ca → validate CA chain, skip hostname (checkServerIdentity no-op)\n * - require/prefer → encrypt but don't validate (unless a CA is given → verify-ca-like)\n * - no-verify → encrypt, don't validate (rejectUnauthorized:false)\n * - disable / unset → no TLS\n *\n * `PGSSLROOTCERT` points at a CA bundle on disk (e.g. the Amazon RDS\n * global-bundle.pem baked into the image).\n *\n * @returns a `ConnectionOptions` object, `false` for disabled, or `undefined`\n * to leave SSL unset (let node-postgres fall back to its defaults).\n */\nexport function buildPgSslConfig(\n env: NodeJS.ProcessEnv = process.env,\n): ConnectionOptions | boolean | undefined {\n const mode = env.PGSSLMODE\n const rootCertPath = env.PGSSLROOTCERT\n\n const readCa = (): string | undefined => {\n if (!rootCertPath) return undefined\n try {\n return readFileSync(rootCertPath, 'utf8')\n } catch (err) {\n throw new Error(\n `PGSSLROOTCERT is set to \"${rootCertPath}\" but the CA bundle could not be read: ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n }\n\n switch (mode) {\n case 'disable':\n return false\n\n case 'verify-full': {\n const ca = readCa()\n if (!ca) {\n throw new Error(\n 'PGSSLMODE=verify-full requires PGSSLROOTCERT to point at a CA bundle ' +\n '(node-postgres would otherwise use the system trust store, which does ' +\n 'not include the Amazon RDS CA). Set PGSSLROOTCERT.',\n )\n }\n // Node defaults: rejectUnauthorized=true (CA chain) + checkServerIdentity\n // (hostname). That IS verify-full.\n return { ca, rejectUnauthorized: true }\n }\n\n case 'verify-ca': {\n const ca = readCa()\n if (!ca) {\n throw new Error(\n 'PGSSLMODE=verify-ca requires PGSSLROOTCERT to point at a CA bundle.',\n )\n }\n // Validate the CA chain but skip hostname matching.\n return {\n ca,\n rejectUnauthorized: true,\n checkServerIdentity: () => undefined,\n }\n }\n\n case 'require':\n case 'prefer': {\n const ca = readCa()\n // With a CA, behave like verify-ca (validate chain, skip hostname);\n // without one, encrypt but don't validate.\n return ca\n ? { ca, rejectUnauthorized: true, checkServerIdentity: () => undefined }\n : { rejectUnauthorized: false }\n }\n\n case 'no-verify':\n return { rejectUnauthorized: false }\n\n default:\n // Unset / unknown: leave SSL unset so callers/pg keep prior behavior.\n return undefined\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,iBACd,MAAyB,QAAQ,KACQ;CACzC,MAAM,OAAO,IAAI;CACjB,MAAM,eAAe,IAAI;CAEzB,MAAM,eAAmC;AACvC,MAAI,CAAC,aAAc,QAAO;AAC1B,MAAI;AACF,UAAO,aAAa,cAAc,OAAO;WAClC,KAAK;AACZ,SAAM,IAAI,MACR,4BAA4B,aAAa,yCACvC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAEnD;;;AAIL,SAAQ,MAAR;EACE,KAAK,UACH,QAAO;EAET,KAAK,eAAe;GAClB,MAAM,KAAK,QAAQ;AACnB,OAAI,CAAC,GACH,OAAM,IAAI,MACR,gMAGD;AAIH,UAAO;IAAE;IAAI,oBAAoB;IAAM;;EAGzC,KAAK,aAAa;GAChB,MAAM,KAAK,QAAQ;AACnB,OAAI,CAAC,GACH,OAAM,IAAI,MACR,sEACD;AAGH,UAAO;IACL;IACA,oBAAoB;IACpB,2BAA2B;IAC5B;;EAGH,KAAK;EACL,KAAK,UAAU;GACb,MAAM,KAAK,QAAQ;AAGnB,UAAO,KACH;IAAE;IAAI,oBAAoB;IAAM,2BAA2B;IAAW,GACtE,EAAE,oBAAoB,OAAO;;EAGnC,KAAK,YACH,QAAO,EAAE,oBAAoB,OAAO;EAEtC,QAEE"}
|
package/dist/index.d.mts
CHANGED
|
@@ -24,6 +24,7 @@ import { ScreenshotRestControllerConfig, registerScreenshotRestController } from
|
|
|
24
24
|
import { SyncAssetsConfig, syncAssets } from "./modules/assets/syncAssets.mjs";
|
|
25
25
|
import { checkAllLinks, printLinkCheckReport } from "./modules/appCatalog/checkLinks.mjs";
|
|
26
26
|
import { connectDb, disconnectDb, getDbClient, setDbClient } from "./db/client.mjs";
|
|
27
|
+
import { buildPgSslConfig } from "./db/sslConfig.mjs";
|
|
27
28
|
import { MakeTFromPrismaModel, ObjectKeys, ScalarFilter, ScalarKeys, TableSyncParamsPrisma, tableSyncPrisma } from "./db/tableSyncPrismaAdapter.mjs";
|
|
28
29
|
import { TABLE_SYNC_MAGAZINE, TableSyncMagazine, TableSyncMagazineModelNameKey } from "./db/tableSyncMagazine.mjs";
|
|
29
30
|
import { SyncAppCatalogResult, syncAppCatalog } from "./db/syncAppCatalog.mjs";
|
|
@@ -34,4 +35,4 @@ import { injectCustomScripts } from "./middleware/htmlInjection.mjs";
|
|
|
34
35
|
import { runLighthouseKeeperDemo } from "./modules/lighthouseKeeper/demo.mjs";
|
|
35
36
|
import { APP_CATALOG_AI_SYSTEM_PROMPT, createAppCatalogAITools } from "./modules/lighthouseKeeper/tools.mjs";
|
|
36
37
|
import { getBuildPipelineId, getFrontendPackageVersion, getVersionInfo } from "./utils/versionUtils.mjs";
|
|
37
|
-
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcAppIndexed, AcAppPageIndexed, AcAppUiIndexed, AcAppsMeta, type AcAuthConfig, AcBackendAppDto, AcBackendAppInput, AcBackendAppUIBaseInput, AcBackendAppUIInput, AcBackendCredentialInput, AcBackendDataFreshness, AcBackendDataSourceInput, AcBackendDataSourceInputCommon, AcBackendDataSourceInputDb, AcBackendDataSourceInputKafka, AcBackendDataVersion, AcBackendDeployableInput, AcBackendDeployment, AcBackendDeploymentInput, AcBackendEnvironmentInput, AcBackendPageInput, type AcBackendProvider, AcBackendTagDescriptionDataIndexed, AcBackendTagFixedTagValue, AcBackendTagsDescriptionDataIndexed, AcBackendUiDefaultsInput, AcBackendVersionsRequestParams, AcBackendVersionsReturn, AcContextIndexed, type AcDatabaseConfig, AcDatabaseManager, AcEnvIndexed, type AcFeatureToggles, type AcLifecycleHooks, type AcLighthouseKeeperConfig, type AcMcpServerConfig, AcMetaDictionary, type AcMiddlewareOptions, type AcMiddlewareResult, AcResourceIndexed, type AcStaticControllerContract, type AcTrpcContext, type AcTrpcContextOptions, AccessRequest, AppAccessRequest, AppApprovalMethod, AppCatalogCompanySpecificBackend, AppCatalogData, AppCategory, AppRole, AppVersionInfo, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, type AssetRestControllerConfig, type AuthRouter, type BetterAuth, CustomConfig, Freshness, Group, GroupingTagDefinition, GroupingTagValue, type IconRestControllerConfig, type MakeTFromPrismaModel, type MiddlewareContext, type ObjectKeys, Person, PersonTeamConfig, type Resource, Role, type ScalarFilter, type ScalarKeys, type ScreenshotRestControllerConfig, ServiceConfig, SourceReference, type SyncAppCatalogResult, type SyncAssetsConfig, TABLE_SYNC_MAGAZINE, type TRPCRouter, type TableSyncMagazine, type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma, Tag, TierVariant, type UpsertIconInput, VersionInfo, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getVersionInfo, injectCustomScripts, printLinkCheckReport, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, runLighthouseKeeperDemo, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, upsertIcon, upsertIcons };
|
|
38
|
+
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcAppIndexed, AcAppPageIndexed, AcAppUiIndexed, AcAppsMeta, type AcAuthConfig, AcBackendAppDto, AcBackendAppInput, AcBackendAppUIBaseInput, AcBackendAppUIInput, AcBackendCredentialInput, AcBackendDataFreshness, AcBackendDataSourceInput, AcBackendDataSourceInputCommon, AcBackendDataSourceInputDb, AcBackendDataSourceInputKafka, AcBackendDataVersion, AcBackendDeployableInput, AcBackendDeployment, AcBackendDeploymentInput, AcBackendEnvironmentInput, AcBackendPageInput, type AcBackendProvider, AcBackendTagDescriptionDataIndexed, AcBackendTagFixedTagValue, AcBackendTagsDescriptionDataIndexed, AcBackendUiDefaultsInput, AcBackendVersionsRequestParams, AcBackendVersionsReturn, AcContextIndexed, type AcDatabaseConfig, AcDatabaseManager, AcEnvIndexed, type AcFeatureToggles, type AcLifecycleHooks, type AcLighthouseKeeperConfig, type AcMcpServerConfig, AcMetaDictionary, type AcMiddlewareOptions, type AcMiddlewareResult, AcResourceIndexed, type AcStaticControllerContract, type AcTrpcContext, type AcTrpcContextOptions, AccessRequest, AppAccessRequest, AppApprovalMethod, AppCatalogCompanySpecificBackend, AppCatalogData, AppCategory, AppRole, AppVersionInfo, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, type AssetRestControllerConfig, type AuthRouter, type BetterAuth, CustomConfig, Freshness, Group, GroupingTagDefinition, GroupingTagValue, type IconRestControllerConfig, type MakeTFromPrismaModel, type MiddlewareContext, type ObjectKeys, Person, PersonTeamConfig, type Resource, Role, type ScalarFilter, type ScalarKeys, type ScreenshotRestControllerConfig, ServiceConfig, SourceReference, type SyncAppCatalogResult, type SyncAssetsConfig, TABLE_SYNC_MAGAZINE, type TRPCRouter, type TableSyncMagazine, type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma, Tag, TierVariant, type UpsertIconInput, VersionInfo, buildPgSslConfig, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getVersionInfo, injectCustomScripts, printLinkCheckReport, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, runLighthouseKeeperDemo, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, upsertIcon, upsertIcons };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPgSslConfig } from "./db/sslConfig.mjs";
|
|
1
2
|
import { connectDb, disconnectDb, getDbClient, setDbClient } from "./db/client.mjs";
|
|
2
3
|
import { createAuthRouter } from "./modules/auth/authRouter.mjs";
|
|
3
4
|
import { createTrpcRouter } from "./server/controller.mjs";
|
|
@@ -21,4 +22,4 @@ import { APP_CATALOG_AI_SYSTEM_PROMPT, createAppCatalogAITools } from "./modules
|
|
|
21
22
|
import { runLighthouseKeeperDemo } from "./modules/lighthouseKeeper/demo.mjs";
|
|
22
23
|
import { getBuildPipelineId, getFrontendPackageVersion, getVersionInfo } from "./utils/versionUtils.mjs";
|
|
23
24
|
|
|
24
|
-
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcDatabaseManager, TABLE_SYNC_MAGAZINE, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getVersionInfo, injectCustomScripts, printLinkCheckReport, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, runLighthouseKeeperDemo, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, upsertIcon, upsertIcons };
|
|
25
|
+
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcDatabaseManager, TABLE_SYNC_MAGAZINE, buildPgSslConfig, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getVersionInfo, injectCustomScripts, printLinkCheckReport, registerAssetRestController, registerAuthRoutes, registerIconRestController, registerScreenshotRestController, runLighthouseKeeperDemo, setDbClient, staticControllerContract, syncAppCatalog, syncAssets, tableSyncPrisma, upsertIcon, upsertIcons };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.mts","names":[],"sources":["../../src/middleware/database.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"database.d.mts","names":[],"sources":["../../src/middleware/database.ts"],"mappings":";;;;;;AAuBA;;cAAa,iBAAA;EAAA,QACH,MAAA;EAAA,QACA,IAAA;EAAA,QACA,MAAA;cAEI,MAAA,EAAQ,gBAAA;EAyCO;;;;EAjC3B,SAAA,CAAA,GAAa,YAAA;EA4BP,OAAA,CAAA,GAAW,OAAA;EAKX,UAAA,CAAA,GAAc,OAAA;AAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PrismaClient } from "../generated/prisma/client.mjs";
|
|
2
|
+
import { buildPgSslConfig } from "../db/sslConfig.mjs";
|
|
2
3
|
import { setDbClient } from "../db/client.mjs";
|
|
3
4
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
4
5
|
import pg from "pg";
|
|
@@ -29,7 +30,11 @@ var AcDatabaseManager = class {
|
|
|
29
30
|
getClient() {
|
|
30
31
|
if (!this.client) {
|
|
31
32
|
const datasourceUrl = formatConnectionUrl(this.config);
|
|
32
|
-
|
|
33
|
+
const ssl = buildPgSslConfig();
|
|
34
|
+
this.pool = new pg.Pool({
|
|
35
|
+
connectionString: datasourceUrl,
|
|
36
|
+
...ssl === void 0 ? {} : { ssl }
|
|
37
|
+
});
|
|
33
38
|
this.client = new PrismaClient({
|
|
34
39
|
adapter: new PrismaPg(this.pool),
|
|
35
40
|
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["warn", "error"]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.mjs","names":[],"sources":["../../src/middleware/database.ts"],"sourcesContent":["import { PrismaClient } from '../generated/prisma/client'\nimport { PrismaPg } from '@prisma/adapter-pg'\nimport pg from 'pg'\nimport type { AcDatabaseConfig } from './types'\nimport { setDbClient } from '../db/client'\n\n/**\n * Formats a database connection URL from structured config.\n */\nfunction formatConnectionUrl(config: AcDatabaseConfig): string {\n if ('url' in config) {\n return config.url\n }\n\n const { host, port, database, username, password, schema = 'public' } = config\n return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`\n}\n\n/**\n * Internal database manager used by the middleware.\n * Handles connection URL formatting and lifecycle.\n */\nexport class AcDatabaseManager {\n private client: PrismaClient | null = null\n private pool: pg.Pool | null = null\n private config: AcDatabaseConfig\n\n constructor(config: AcDatabaseConfig) {\n this.config = config\n }\n\n /**\n * Get or create the Prisma client instance.\n * Uses lazy initialization for flexibility.\n */\n getClient(): PrismaClient {\n if (!this.client) {\n const datasourceUrl = formatConnectionUrl(this.config)\n\n // Prisma 7 with adapter: Create pg pool and wrap with adapter\n this.pool = new pg.Pool({
|
|
1
|
+
{"version":3,"file":"database.mjs","names":[],"sources":["../../src/middleware/database.ts"],"sourcesContent":["import { PrismaClient } from '../generated/prisma/client'\nimport { PrismaPg } from '@prisma/adapter-pg'\nimport pg from 'pg'\nimport type { AcDatabaseConfig } from './types'\nimport { setDbClient } from '../db/client'\nimport { buildPgSslConfig } from '../db/sslConfig'\n\n/**\n * Formats a database connection URL from structured config.\n */\nfunction formatConnectionUrl(config: AcDatabaseConfig): string {\n if ('url' in config) {\n return config.url\n }\n\n const { host, port, database, username, password, schema = 'public' } = config\n return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`\n}\n\n/**\n * Internal database manager used by the middleware.\n * Handles connection URL formatting and lifecycle.\n */\nexport class AcDatabaseManager {\n private client: PrismaClient | null = null\n private pool: pg.Pool | null = null\n private config: AcDatabaseConfig\n\n constructor(config: AcDatabaseConfig) {\n this.config = config\n }\n\n /**\n * Get or create the Prisma client instance.\n * Uses lazy initialization for flexibility.\n */\n getClient(): PrismaClient {\n if (!this.client) {\n const datasourceUrl = formatConnectionUrl(this.config)\n\n // Prisma 7 with adapter: Create pg pool and wrap with adapter.\n // SSL from PGSSLMODE/PGSSLROOTCERT via buildPgSslConfig (node-postgres\n // doesn't apply those correctly to a connection-string pool).\n const ssl = buildPgSslConfig()\n this.pool = new pg.Pool({\n connectionString: datasourceUrl,\n ...(ssl === undefined ? {} : { ssl }),\n })\n const adapter = new PrismaPg(this.pool)\n\n this.client = new PrismaClient({\n adapter,\n log:\n process.env.NODE_ENV === 'development'\n ? ['warn', 'error']\n : ['warn', 'error'],\n })\n\n // Bridge with existing backend-core getDbClient() usage\n setDbClient(this.client)\n }\n return this.client\n }\n\n async connect(): Promise<void> {\n const client = this.getClient()\n await client.$connect()\n }\n\n async disconnect(): Promise<void> {\n if (this.client) {\n await this.client.$disconnect()\n this.client = null\n }\n if (this.pool) {\n await this.pool.end()\n this.pool = null\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAS,oBAAoB,QAAkC;AAC7D,KAAI,SAAS,OACX,QAAO,OAAO;CAGhB,MAAM,EAAE,MAAM,MAAM,UAAU,UAAU,UAAU,SAAS,aAAa;AACxE,QAAO,gBAAgB,SAAS,GAAG,mBAAmB,SAAS,CAAC,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,UAAU;;;;;;AAOxG,IAAa,oBAAb,MAA+B;CAK7B,YAAY,QAA0B;gBAJA;cACP;AAI7B,OAAK,SAAS;;;;;;CAOhB,YAA0B;AACxB,MAAI,CAAC,KAAK,QAAQ;GAChB,MAAM,gBAAgB,oBAAoB,KAAK,OAAO;GAKtD,MAAM,MAAM,kBAAkB;AAC9B,QAAK,OAAO,IAAI,GAAG,KAAK;IACtB,kBAAkB;IAClB,GAAI,QAAQ,SAAY,EAAE,GAAG,EAAE,KAAK;IACrC,CAAC;AAGF,QAAK,SAAS,IAAI,aAAa;IAC7B,SAHc,IAAI,SAAS,KAAK,KAAK;IAIrC,KACE,QAAQ,IAAI,aAAa,gBACrB,CAAC,QAAQ,QAAQ,GACjB,CAAC,QAAQ,QAAQ;IACxB,CAAC;AAGF,eAAY,KAAK,OAAO;;AAE1B,SAAO,KAAK;;CAGd,MAAM,UAAyB;AAE7B,QADe,KAAK,WAAW,CAClB,UAAU;;CAGzB,MAAM,aAA4B;AAChC,MAAI,KAAK,QAAQ;AACf,SAAM,KAAK,OAAO,aAAa;AAC/B,QAAK,SAAS;;AAEhB,MAAI,KAAK,MAAM;AACb,SAAM,KAAK,KAAK,KAAK;AACrB,QAAK,OAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-backend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Backend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"tsyringe": "^4.10.0",
|
|
46
46
|
"yaml": "^2.8.0",
|
|
47
47
|
"zod": "^4.3.5",
|
|
48
|
-
"@igstack/app-catalog-shared-core": "0.
|
|
49
|
-
"@igstack/app-catalog-table-sync": "0.
|
|
48
|
+
"@igstack/app-catalog-shared-core": "0.13.1",
|
|
49
|
+
"@igstack/app-catalog-table-sync": "0.13.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tanstack/vite-config": "^0.4.3",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"engines": {
|
|
74
74
|
"node": ">=24"
|
|
75
75
|
},
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "1a7efd91a8e67bea65020e30913ffbbf593c7412",
|
|
77
77
|
"scripts": {
|
|
78
78
|
"build": "tsdown",
|
|
79
79
|
"build:lenient": "tsdown",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { buildPgSslConfig } from '../db/sslConfig'
|
|
6
|
+
|
|
7
|
+
const CA_PEM =
|
|
8
|
+
'-----BEGIN CERTIFICATE-----\nMIIB...fake...\n-----END CERTIFICATE-----\n'
|
|
9
|
+
|
|
10
|
+
describe('buildPgSslConfig', () => {
|
|
11
|
+
let dir: string
|
|
12
|
+
let caPath: string
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
dir = mkdtempSync(join(tmpdir(), 'sslcfg-'))
|
|
16
|
+
caPath = join(dir, 'ca.pem')
|
|
17
|
+
writeFileSync(caPath, CA_PEM)
|
|
18
|
+
})
|
|
19
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }))
|
|
20
|
+
|
|
21
|
+
const env = (over: Record<string, string | undefined>): NodeJS.ProcessEnv =>
|
|
22
|
+
over as NodeJS.ProcessEnv
|
|
23
|
+
|
|
24
|
+
it('returns undefined when PGSSLMODE is unset (leave pg defaults)', () => {
|
|
25
|
+
expect(buildPgSslConfig(env({}))).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns false for disable', () => {
|
|
29
|
+
expect(buildPgSslConfig(env({ PGSSLMODE: 'disable' }))).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('verify-full loads the CA and keeps full validation', () => {
|
|
33
|
+
const ssl = buildPgSslConfig(
|
|
34
|
+
env({ PGSSLMODE: 'verify-full', PGSSLROOTCERT: caPath }),
|
|
35
|
+
)
|
|
36
|
+
expect(ssl).toMatchObject({ ca: CA_PEM, rejectUnauthorized: true })
|
|
37
|
+
// verify-full must NOT disable hostname checking
|
|
38
|
+
expect(
|
|
39
|
+
(ssl as { checkServerIdentity?: unknown }).checkServerIdentity,
|
|
40
|
+
).toBeUndefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('verify-full throws without a CA (would silently use system store otherwise)', () => {
|
|
44
|
+
expect(() => buildPgSslConfig(env({ PGSSLMODE: 'verify-full' }))).toThrow(
|
|
45
|
+
/PGSSLROOTCERT/,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('verify-ca validates the chain but skips hostname', () => {
|
|
50
|
+
const ssl = buildPgSslConfig(
|
|
51
|
+
env({ PGSSLMODE: 'verify-ca', PGSSLROOTCERT: caPath }),
|
|
52
|
+
) as {
|
|
53
|
+
ca: string
|
|
54
|
+
rejectUnauthorized: boolean
|
|
55
|
+
checkServerIdentity: () => unknown
|
|
56
|
+
}
|
|
57
|
+
expect(ssl.ca).toBe(CA_PEM)
|
|
58
|
+
expect(ssl.rejectUnauthorized).toBe(true)
|
|
59
|
+
expect(typeof ssl.checkServerIdentity).toBe('function')
|
|
60
|
+
expect(ssl.checkServerIdentity()).toBeUndefined()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('no-verify encrypts without validation', () => {
|
|
64
|
+
expect(buildPgSslConfig(env({ PGSSLMODE: 'no-verify' }))).toEqual({
|
|
65
|
+
rejectUnauthorized: false,
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('require without a CA does not validate', () => {
|
|
70
|
+
expect(buildPgSslConfig(env({ PGSSLMODE: 'require' }))).toEqual({
|
|
71
|
+
rejectUnauthorized: false,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('require WITH a CA validates the chain (verify-ca-like)', () => {
|
|
76
|
+
const ssl = buildPgSslConfig(
|
|
77
|
+
env({ PGSSLMODE: 'require', PGSSLROOTCERT: caPath }),
|
|
78
|
+
) as { ca: string; rejectUnauthorized: boolean }
|
|
79
|
+
expect(ssl.ca).toBe(CA_PEM)
|
|
80
|
+
expect(ssl.rejectUnauthorized).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('throws a clear error when the CA path is unreadable', () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
buildPgSslConfig(
|
|
86
|
+
env({ PGSSLMODE: 'verify-full', PGSSLROOTCERT: '/no/such/ca.pem' }),
|
|
87
|
+
),
|
|
88
|
+
).toThrow(/could not be read/)
|
|
89
|
+
})
|
|
90
|
+
})
|
package/src/db/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PrismaClient } from '../generated/prisma/client'
|
|
2
2
|
import { PrismaPg } from '@prisma/adapter-pg'
|
|
3
3
|
import pg from 'pg'
|
|
4
|
+
import { buildPgSslConfig } from './sslConfig'
|
|
4
5
|
|
|
5
6
|
let prismaClient: PrismaClient | null = null
|
|
6
7
|
let pool: pg.Pool | null = null
|
|
@@ -19,8 +20,15 @@ export function getDbClient(): PrismaClient {
|
|
|
19
20
|
)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
// Prisma 7 with adapter: Create pg pool and wrap with adapter
|
|
23
|
-
|
|
23
|
+
// Prisma 7 with adapter: Create pg pool and wrap with adapter.
|
|
24
|
+
// SSL comes from PGSSLMODE/PGSSLROOTCERT via our helper (node-postgres
|
|
25
|
+
// doesn't honor those correctly for a connection-string pool — see
|
|
26
|
+
// buildPgSslConfig).
|
|
27
|
+
const ssl = buildPgSslConfig()
|
|
28
|
+
pool = new pg.Pool({
|
|
29
|
+
connectionString: databaseUrl,
|
|
30
|
+
...(ssl === undefined ? {} : { ssl }),
|
|
31
|
+
})
|
|
24
32
|
const adapter = new PrismaPg(pool)
|
|
25
33
|
|
|
26
34
|
prismaClient = new PrismaClient({ adapter })
|
package/src/db/index.ts
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import type { ConnectionOptions } from 'node:tls'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the node-postgres `ssl` PoolConfig option from libpq-style env vars,
|
|
6
|
+
* because node-postgres does NOT do this correctly on its own for a
|
|
7
|
+
* connection-string pool: its built-in env handling maps `PGSSLMODE=verify-full`
|
|
8
|
+
* to a bare `ssl: true` (Node's default trust store) and IGNORES `PGSSLROOTCERT`
|
|
9
|
+
* — so an RDS server cert signed by the Amazon RDS CA would fail to verify.
|
|
10
|
+
*
|
|
11
|
+
* We therefore construct the `ssl` object explicitly and pass it to `new
|
|
12
|
+
* pg.Pool({ connectionString, ssl })`. Semantics mirror libpq / pg-connection-string:
|
|
13
|
+
* - verify-full → validate CA chain AND hostname (Node defaults; no overrides)
|
|
14
|
+
* - verify-ca → validate CA chain, skip hostname (checkServerIdentity no-op)
|
|
15
|
+
* - require/prefer → encrypt but don't validate (unless a CA is given → verify-ca-like)
|
|
16
|
+
* - no-verify → encrypt, don't validate (rejectUnauthorized:false)
|
|
17
|
+
* - disable / unset → no TLS
|
|
18
|
+
*
|
|
19
|
+
* `PGSSLROOTCERT` points at a CA bundle on disk (e.g. the Amazon RDS
|
|
20
|
+
* global-bundle.pem baked into the image).
|
|
21
|
+
*
|
|
22
|
+
* @returns a `ConnectionOptions` object, `false` for disabled, or `undefined`
|
|
23
|
+
* to leave SSL unset (let node-postgres fall back to its defaults).
|
|
24
|
+
*/
|
|
25
|
+
export function buildPgSslConfig(
|
|
26
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
27
|
+
): ConnectionOptions | boolean | undefined {
|
|
28
|
+
const mode = env.PGSSLMODE
|
|
29
|
+
const rootCertPath = env.PGSSLROOTCERT
|
|
30
|
+
|
|
31
|
+
const readCa = (): string | undefined => {
|
|
32
|
+
if (!rootCertPath) return undefined
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(rootCertPath, 'utf8')
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`PGSSLROOTCERT is set to "${rootCertPath}" but the CA bundle could not be read: ${
|
|
38
|
+
err instanceof Error ? err.message : String(err)
|
|
39
|
+
}`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
switch (mode) {
|
|
45
|
+
case 'disable':
|
|
46
|
+
return false
|
|
47
|
+
|
|
48
|
+
case 'verify-full': {
|
|
49
|
+
const ca = readCa()
|
|
50
|
+
if (!ca) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'PGSSLMODE=verify-full requires PGSSLROOTCERT to point at a CA bundle ' +
|
|
53
|
+
'(node-postgres would otherwise use the system trust store, which does ' +
|
|
54
|
+
'not include the Amazon RDS CA). Set PGSSLROOTCERT.',
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
// Node defaults: rejectUnauthorized=true (CA chain) + checkServerIdentity
|
|
58
|
+
// (hostname). That IS verify-full.
|
|
59
|
+
return { ca, rejectUnauthorized: true }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'verify-ca': {
|
|
63
|
+
const ca = readCa()
|
|
64
|
+
if (!ca) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'PGSSLMODE=verify-ca requires PGSSLROOTCERT to point at a CA bundle.',
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
// Validate the CA chain but skip hostname matching.
|
|
70
|
+
return {
|
|
71
|
+
ca,
|
|
72
|
+
rejectUnauthorized: true,
|
|
73
|
+
checkServerIdentity: () => undefined,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case 'require':
|
|
78
|
+
case 'prefer': {
|
|
79
|
+
const ca = readCa()
|
|
80
|
+
// With a CA, behave like verify-ca (validate chain, skip hostname);
|
|
81
|
+
// without one, encrypt but don't validate.
|
|
82
|
+
return ca
|
|
83
|
+
? { ca, rejectUnauthorized: true, checkServerIdentity: () => undefined }
|
|
84
|
+
: { rejectUnauthorized: false }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'no-verify':
|
|
88
|
+
return { rejectUnauthorized: false }
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
// Unset / unknown: leave SSL unset so callers/pg keep prior behavior.
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { PrismaPg } from '@prisma/adapter-pg'
|
|
|
3
3
|
import pg from 'pg'
|
|
4
4
|
import type { AcDatabaseConfig } from './types'
|
|
5
5
|
import { setDbClient } from '../db/client'
|
|
6
|
+
import { buildPgSslConfig } from '../db/sslConfig'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Formats a database connection URL from structured config.
|
|
@@ -37,8 +38,14 @@ export class AcDatabaseManager {
|
|
|
37
38
|
if (!this.client) {
|
|
38
39
|
const datasourceUrl = formatConnectionUrl(this.config)
|
|
39
40
|
|
|
40
|
-
// Prisma 7 with adapter: Create pg pool and wrap with adapter
|
|
41
|
-
|
|
41
|
+
// Prisma 7 with adapter: Create pg pool and wrap with adapter.
|
|
42
|
+
// SSL from PGSSLMODE/PGSSLROOTCERT via buildPgSslConfig (node-postgres
|
|
43
|
+
// doesn't apply those correctly to a connection-string pool).
|
|
44
|
+
const ssl = buildPgSslConfig()
|
|
45
|
+
this.pool = new pg.Pool({
|
|
46
|
+
connectionString: datasourceUrl,
|
|
47
|
+
...(ssl === undefined ? {} : { ssl }),
|
|
48
|
+
})
|
|
42
49
|
const adapter = new PrismaPg(this.pool)
|
|
43
50
|
|
|
44
51
|
this.client = new PrismaClient({
|