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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/node/postgres/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@saga-ed/soa-postgres",
"type": "module",
"version": "0.1.1",
"version": "0.1.2",
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,26 @@ describe('PostgresProvider config sources', () => {
expect(state.options?.ssl).toBe(true);
expect(state.options?.max).toBe(7);
});

it('passes a CA-bundle ssl object straight through to pg (PostgresPoolConfig path)', async () => {
const ssl = { ca: '-----BEGIN CERTIFICATE-----\nMIIB...', rejectUnauthorized: true };
await new PostgresProvider({
instanceName: 'RdsDB',
host: 'rds.example',
port: 5432,
database: 'iam_db',
user: 'iam_api_app',
password: async () => 'iam-token',
ssl,
}).connect();

// pg forwards the object verbatim to tls.connect — the CA pin survives.
expect(state.options?.ssl).toEqual(ssl);
});

it('accepts and passes through an ssl object on the static schema path', async () => {
const ssl = { ca: 'PEM', rejectUnauthorized: true };
await new PostgresProvider(baseConfig({ ssl })).connect();
expect(state.options?.ssl).toEqual(ssl);
});
});
36 changes: 35 additions & 1 deletion packages/node/postgres/src/aws-postgres-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,45 @@ export interface LoadPostgresConfigParams {
region?: string;
}

/**
* TLS options for a Postgres connection — a structural subset of Node's
* ``tls.ConnectionOptions`` that ``pg`` forwards verbatim to ``tls.connect``.
*
* Use this object form (instead of a bare ``true``) when the server cert
* chains to a root that is **not** in Node's default trust store — most
* notably to pin the Amazon RDS CA bundle for managed RDS:
*
* {
* ca: readFileSync(process.env.PG_RDS_CA_BUNDLE_PATH, 'utf8'),
* rejectUnauthorized: true,
* }
*
* ``ssl: true`` keeps full verification against Node's default CA bundle;
* ``ssl: false`` disables TLS (local dev).
*/
export interface PostgresSslConfig {
/** CA cert(s) to trust (PEM). Pin the RDS CA bundle here. */
ca?: string | Buffer | Array<string | Buffer>;
/** Verify the server cert against `ca` / the default store. Defaults true. */
rejectUnauthorized?: boolean;
/** Client cert (PEM) for mutual TLS. */
cert?: string | Buffer | Array<string | Buffer>;
/** Client private key (PEM) for mutual TLS. */
key?: string | Buffer | Array<string | Buffer>;
/** SNI / cert-identity hostname override. */
servername?: string;
}

/**
* `pg.Pool`-compatible config returned by the loader. The `password`
* field is a union: a static string for dev (parity secret) or an
* async callback for mirror/prod (mints IAM token per new pool
* connection). `pg.Pool` natively accepts both shapes.
*
* `ssl` is `boolean | PostgresSslConfig`: the loader returns a bare
* `true`/`false`, but a consumer may override it with a {@link
* PostgresSslConfig} (e.g. pinning the RDS CA bundle) before handing the
* config to {@link PostgresProvider}.
*/
export interface PostgresPoolConfig {
instanceName: string;
Expand All @@ -103,7 +137,7 @@ export interface PostgresPoolConfig {
database: string;
user: string;
password: string | (() => Promise<string>);
ssl: boolean;
ssl: boolean | PostgresSslConfig;

// Optional pool tuning. `PostgresProvider` applies conservative defaults
// (matching `PostgresProviderSchema`) when these are omitted, so the
Expand Down
1 change: 1 addition & 0 deletions packages/node/postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
export type {
LoadPostgresConfigParams,
PostgresPoolConfig,
PostgresSslConfig,
} from './aws-postgres-loader.js';

export const POSTGRES_PROVIDER = Symbol.for('PostgresProvider');
22 changes: 19 additions & 3 deletions packages/node/postgres/src/postgres-provider-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { z } from 'zod';

/**
* TLS options for a static {@link PostgresProviderConfig}. JSON-friendly
* (PEM strings, not Buffers) since this path is parsed from a Secrets
* Manager payload. Mirrors {@link PostgresSslConfig} from the loader so
* both config shapes can pin a CA bundle (e.g. the Amazon RDS roots).
*/
export const PostgresSslSchema = z.object({
ca: z.string().optional(),
rejectUnauthorized: z.boolean().optional(),
cert: z.string().optional(),
key: z.string().optional(),
servername: z.string().optional(),
});

/**
* Config schema for {@link PostgresProvider}.
*
Expand All @@ -24,9 +38,11 @@ export const PostgresProviderSchema = z.object({
username: z.string().min(1),
password: z.string().min(1),

// TLS toggle. Required for managed RDS / RDS Proxy connections.
// Dev db-host containers can leave this off.
ssl: z.boolean().default(false),
// TLS: `true`/`false` toggle, or a CA-pinning object (see
// PostgresSslSchema) when the server cert chains to a root outside
// Node's default trust store. Required for managed RDS / RDS Proxy;
// dev db-host containers can leave this off.
ssl: z.union([z.boolean(), PostgresSslSchema]).default(false),

// Optional pool tuning. Sensible defaults for an API service.
poolSize: z.number().int().positive().default(10),
Expand Down
16 changes: 11 additions & 5 deletions packages/node/postgres/src/postgres-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import 'reflect-metadata';
import { injectable } from 'inversify';
import { Pool, type PoolClient } from 'pg';
import type { PostgresProviderConfig } from './postgres-provider-config.js';
import type { PostgresPoolConfig } from './aws-postgres-loader.js';
import type {
PostgresPoolConfig,
PostgresSslConfig,
} from './aws-postgres-loader.js';

/**
* Manages a single ``pg.Pool`` for one logical database. Provider is
Expand Down Expand Up @@ -63,7 +66,7 @@ interface NormalizedConfig {
database: string;
user: string;
password: string | (() => Promise<string>);
ssl: boolean;
ssl: boolean | PostgresSslConfig;
poolSize: number;
idleTimeoutMs: number;
connectionTimeoutMs: number;
Expand Down Expand Up @@ -105,9 +108,12 @@ export class PostgresProvider {
// specifically because pg ignores a discrete password when a
// connectionString is also present — the callback would be lost.
password: this.cfg.password,
// ssl:true => full cert verification, equivalent to the previous
// `sslmode=require` (which pg maps to {}). RDS certs chain to the
// Amazon roots in Node's CA bundle. ssl:false for local dev.
// ssl:true => full cert verification against Node's default CA
// bundle (equivalent to the previous `sslmode=require`, which pg
// maps to {}). Pass a PostgresSslConfig object instead to pin a CA
// bundle (e.g. the Amazon RDS roots, which aren't always in Node's
// default trust store) — pg forwards it verbatim to tls.connect.
// ssl:false for local dev.
ssl: this.cfg.ssl,
max: this.cfg.poolSize,
idleTimeoutMillis: this.cfg.idleTimeoutMs,
Expand Down
Loading