@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.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../../src/db/client.ts"],"mappings":";;;;;AAWA;;iBAAgB,WAAA,CAAA,GAAe,YAAA;;;AAuB/B;;iBAAgB,WAAA,CAAY,MAAA,EAAQ,YAAA;;;AAQpC;;iBAAsB,SAAA,CAAA,GAAa,OAAA;;;AASnC;;iBAAsB,YAAA,CAAA,GAAgB,OAAA"}
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"}
@@ -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
- pool = new pg.Pool({ connectionString: databaseUrl });
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;
@@ -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({ connectionString: databaseUrl })\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":";;;;;AAIA,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;AAIH,SAAO,IAAI,GAAG,KAAK,EAAE,kBAAkB,aAAa,CAAC;AAGrD,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"}
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"}
@@ -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":";;;;;;AAsBA;;cAAa,iBAAA;EAAA,QACH,MAAA;EAAA,QACA,IAAA;EAAA,QACA,MAAA;cAEI,MAAA,EAAQ,gBAAA;EAmCO;;;;EA3B3B,SAAA,CAAA,GAAa,YAAA;EAsBP,OAAA,CAAA,GAAW,OAAA;EAKX,UAAA,CAAA,GAAc,OAAA;AAAA"}
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
- this.pool = new pg.Pool({ connectionString: datasourceUrl });
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({ connectionString: datasourceUrl })\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":";;;;;;;;;AASA,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;AAGtD,QAAK,OAAO,IAAI,GAAG,KAAK,EAAE,kBAAkB,eAAe,CAAC;AAG5D,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"}
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.12.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.12.0",
49
- "@igstack/app-catalog-table-sync": "0.12.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": "34de03d23f3923a85c8a3b4d0bc6ff2d83c799d3",
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
- pool = new pg.Pool({ connectionString: databaseUrl })
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
@@ -1,5 +1,6 @@
1
1
  // Database connection
2
2
  export { connectDb, disconnectDb, getDbClient, setDbClient } from './client'
3
+ export { buildPgSslConfig } from './sslConfig'
3
4
 
4
5
  // Table sync utilities
5
6
  export {
@@ -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
@@ -59,6 +59,7 @@ export {
59
59
 
60
60
  // Database utilities
61
61
  export {
62
+ buildPgSslConfig,
62
63
  connectDb,
63
64
  disconnectDb,
64
65
  getDbClient,
@@ -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
- this.pool = new pg.Pool({ connectionString: datasourceUrl })
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({