pg-admin-mcp 0.0.0-stage → 0.1.0
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/README.md +608 -2
- package/dist/config/env.js +67 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/profiles.js +257 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/core/audit-context.js +21 -0
- package/dist/core/audit-context.js.map +1 -0
- package/dist/core/audit.js +91 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/brands.js +24 -0
- package/dist/core/brands.js.map +1 -0
- package/dist/core/classifier.js +279 -0
- package/dist/core/classifier.js.map +1 -0
- package/dist/core/errors.js +197 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/format.js +258 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/guards.js +513 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/identifiers.js +431 -0
- package/dist/core/identifiers.js.map +1 -0
- package/dist/core/limits.js +79 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/logger.js +34 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paginate.js +195 -0
- package/dist/core/paginate.js.map +1 -0
- package/dist/core/paths.js +392 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/policy.js +98 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/shutdown.js +10 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/stdout-guard.js +25 -0
- package/dist/core/stdout-guard.js.map +1 -0
- package/dist/db/catalog.js +1006 -0
- package/dist/db/catalog.js.map +1 -0
- package/dist/db/pool.js +158 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/session.js +258 -0
- package/dist/db/session.js.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/index.js +88 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/runtime/binaries.js +356 -0
- package/dist/runtime/binaries.js.map +1 -0
- package/dist/runtime/env-file.js +62 -0
- package/dist/runtime/env-file.js.map +1 -0
- package/dist/runtime/environment.js +34 -0
- package/dist/runtime/environment.js.map +1 -0
- package/dist/server.js +106 -0
- package/dist/server.js.map +1 -0
- package/dist/snapshot/capture.js +189 -0
- package/dist/snapshot/capture.js.map +1 -0
- package/dist/snapshot/diff.js +254 -0
- package/dist/snapshot/diff.js.map +1 -0
- package/dist/snapshot/migrate.js +1596 -0
- package/dist/snapshot/migrate.js.map +1 -0
- package/dist/snapshot/model.js +80 -0
- package/dist/snapshot/model.js.map +1 -0
- package/dist/tools/_archive_meta.js +138 -0
- package/dist/tools/_archive_meta.js.map +1 -0
- package/dist/tools/_batch_shared.js +73 -0
- package/dist/tools/_batch_shared.js.map +1 -0
- package/dist/tools/_export_format.js +107 -0
- package/dist/tools/_export_format.js.map +1 -0
- package/dist/tools/_object_kinds.js +97 -0
- package/dist/tools/_object_kinds.js.map +1 -0
- package/dist/tools/_relation_detail.js +55 -0
- package/dist/tools/_relation_detail.js.map +1 -0
- package/dist/tools/_shared.js +17 -0
- package/dist/tools/_shared.js.map +1 -0
- package/dist/tools/_snapshot_io.js +238 -0
- package/dist/tools/_snapshot_io.js.map +1 -0
- package/dist/tools/admin/_shared.js +18 -0
- package/dist/tools/admin/_shared.js.map +1 -0
- package/dist/tools/admin/extensions.js +80 -0
- package/dist/tools/admin/extensions.js.map +1 -0
- package/dist/tools/admin/grants.js +207 -0
- package/dist/tools/admin/grants.js.map +1 -0
- package/dist/tools/admin/index.js +138 -0
- package/dist/tools/admin/index.js.map +1 -0
- package/dist/tools/admin/maintenance.js +106 -0
- package/dist/tools/admin/maintenance.js.map +1 -0
- package/dist/tools/admin/roles.js +144 -0
- package/dist/tools/admin/roles.js.map +1 -0
- package/dist/tools/admin/sessions.js +104 -0
- package/dist/tools/admin/sessions.js.map +1 -0
- package/dist/tools/admin/stats.js +63 -0
- package/dist/tools/admin/stats.js.map +1 -0
- package/dist/tools/backup.js +288 -0
- package/dist/tools/backup.js.map +1 -0
- package/dist/tools/connection_status.js +138 -0
- package/dist/tools/connection_status.js.map +1 -0
- package/dist/tools/copy.js +355 -0
- package/dist/tools/copy.js.map +1 -0
- package/dist/tools/delete_batch.js +73 -0
- package/dist/tools/delete_batch.js.map +1 -0
- package/dist/tools/describe_object.js +87 -0
- package/dist/tools/describe_object.js.map +1 -0
- package/dist/tools/execute_sql.js +395 -0
- package/dist/tools/execute_sql.js.map +1 -0
- package/dist/tools/explain.js +134 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/export.js +215 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/get_schema.js +148 -0
- package/dist/tools/get_schema.js.map +1 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insert_batch.js +212 -0
- package/dist/tools/insert_batch.js.map +1 -0
- package/dist/tools/list_objects.js +160 -0
- package/dist/tools/list_objects.js.map +1 -0
- package/dist/tools/prepare_migration.js +112 -0
- package/dist/tools/prepare_migration.js.map +1 -0
- package/dist/tools/restore.js +301 -0
- package/dist/tools/restore.js.map +1 -0
- package/dist/tools/schema_diff.js +79 -0
- package/dist/tools/schema_diff.js.map +1 -0
- package/dist/tools/schema_snapshot.js +57 -0
- package/dist/tools/schema_snapshot.js.map +1 -0
- package/dist/tools/select.js +162 -0
- package/dist/tools/select.js.map +1 -0
- package/dist/tools/update_batch.js +154 -0
- package/dist/tools/update_batch.js.map +1 -0
- package/package.json +47 -5
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PgMcpError } from '../core/errors.js';
|
|
2
|
+
/** Returns `fallback` (default `undefined`) when unset or empty. */
|
|
3
|
+
export function readString(env, name, fallback) {
|
|
4
|
+
const raw = env[name];
|
|
5
|
+
return raw === undefined || raw === '' ? fallback : raw;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parses an integer, clamping to `[min, max]` when given. A value that is not
|
|
9
|
+
* a finite number is a hard `PgMcpError` naming the variable — clamping only
|
|
10
|
+
* applies to numbers that parsed fine but landed out of range.
|
|
11
|
+
*/
|
|
12
|
+
export function readInt(env, name, opts) {
|
|
13
|
+
const raw = env[name];
|
|
14
|
+
if (raw === undefined || raw === '')
|
|
15
|
+
return opts.default;
|
|
16
|
+
const parsed = Number(raw);
|
|
17
|
+
if (!Number.isFinite(parsed)) {
|
|
18
|
+
throw new PgMcpError({
|
|
19
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
20
|
+
message: `${name} must be a number, got "${raw}"`,
|
|
21
|
+
hint: `set ${name} to a whole number, e.g. ${name}=${opts.default}`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
let value = parsed;
|
|
25
|
+
if (opts.min !== undefined && value < opts.min)
|
|
26
|
+
value = opts.min;
|
|
27
|
+
if (opts.max !== undefined && value > opts.max)
|
|
28
|
+
value = opts.max;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
const TRUE_VALUES = new Set(['1', 'true', 'yes']);
|
|
32
|
+
const FALSE_VALUES = new Set(['0', 'false', 'no', '']);
|
|
33
|
+
/**
|
|
34
|
+
* `1`/`true`/`yes` (any case) -> `true`. `0`/`false`/`no`/unset/empty -> `false`.
|
|
35
|
+
* Anything else is a hard `PgMcpError` naming the variable — an unrecognized
|
|
36
|
+
* bool spelling is a typo, not a value to guess at.
|
|
37
|
+
*/
|
|
38
|
+
export function readBool(env, name) {
|
|
39
|
+
const raw = env[name] ?? '';
|
|
40
|
+
const normalized = raw.trim().toLowerCase();
|
|
41
|
+
if (TRUE_VALUES.has(normalized))
|
|
42
|
+
return true;
|
|
43
|
+
if (FALSE_VALUES.has(normalized))
|
|
44
|
+
return false;
|
|
45
|
+
throw new PgMcpError({
|
|
46
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
47
|
+
message: `${name} must be a boolean, got "${raw}"`,
|
|
48
|
+
hint: `set ${name} to one of 1/true/yes or 0/false/no`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function splitList(raw) {
|
|
52
|
+
if (raw === undefined)
|
|
53
|
+
return [];
|
|
54
|
+
return raw
|
|
55
|
+
.split(',')
|
|
56
|
+
.map((entry) => entry.trim())
|
|
57
|
+
.filter((entry) => entry.length > 0);
|
|
58
|
+
}
|
|
59
|
+
/** Comma-separated, trimmed, empties dropped. `[]` when unset. */
|
|
60
|
+
export function readCsv(env, name) {
|
|
61
|
+
return splitList(env[name]);
|
|
62
|
+
}
|
|
63
|
+
/** Same rules as {@link readCsv} — path lists use the comma separator too, see the file header. */
|
|
64
|
+
export function readPathList(env, name) {
|
|
65
|
+
return splitList(env[name]);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { PgMcpError, envPrefix, isLoopbackHost } from '../core/errors.js';
|
|
3
|
+
import { logger } from '../core/logger.js';
|
|
4
|
+
import { inContainer } from '../runtime/environment.js';
|
|
5
|
+
import { readBool, readCsv, readInt, readPathList, readString } from './env.js';
|
|
6
|
+
/**
|
|
7
|
+
* The profile registry — env-vars-only multi-database configuration.
|
|
8
|
+
*
|
|
9
|
+
* `PG_MCP_PROFILES` is a required CSV of aliases; every other per-profile
|
|
10
|
+
* setting lives under `PG_<ALIAS>_*`, where `<ALIAS>` is the alias uppercased
|
|
11
|
+
* with every non-alphanumeric character replaced by `_` (see `envPrefix` in
|
|
12
|
+
* `core/errors.ts` — the one place that mapping is defined, reused here so it
|
|
13
|
+
* cannot drift from the hints `errors.ts` renders).
|
|
14
|
+
*
|
|
15
|
+
* **Failure isolation.** A broken profile does not stop the server: its error
|
|
16
|
+
* is recorded in `errors` and the other profiles stay usable. Only a missing
|
|
17
|
+
* `PG_MCP_PROFILES` or a prefix collision between two distinct aliases is
|
|
18
|
+
* fatal — the latter would otherwise silently share credentials between two
|
|
19
|
+
* profiles the caller believes are different.
|
|
20
|
+
*/
|
|
21
|
+
export const CAPABILITIES = ['write', 'ddl', 'admin', 'explainAnalyze', 'backup', 'restore'];
|
|
22
|
+
/**
|
|
23
|
+
* Per-profile env var suffix for each capability, constrained to the `ALLOW_*`
|
|
24
|
+
* shape by the template literal type. Exported so `core/policy.ts` names the
|
|
25
|
+
* exact variable in its refusal hints from this one table rather than a second
|
|
26
|
+
* copy that could drift.
|
|
27
|
+
*/
|
|
28
|
+
export const CAPABILITY_ENV_SUFFIX = {
|
|
29
|
+
write: 'ALLOW_WRITE',
|
|
30
|
+
ddl: 'ALLOW_DDL',
|
|
31
|
+
admin: 'ALLOW_ADMIN',
|
|
32
|
+
explainAnalyze: 'ALLOW_EXPLAIN_ANALYZE',
|
|
33
|
+
backup: 'ALLOW_BACKUP',
|
|
34
|
+
restore: 'ALLOW_RESTORE',
|
|
35
|
+
};
|
|
36
|
+
const PROFILE_ENVS = new Set(['development', 'staging', 'production']);
|
|
37
|
+
/** Unrecognized or missing `ENV` is always `production` — this server never fails open. */
|
|
38
|
+
function normalizeProfileEnv(raw) {
|
|
39
|
+
if (raw !== undefined && PROFILE_ENVS.has(raw))
|
|
40
|
+
return raw;
|
|
41
|
+
return 'production';
|
|
42
|
+
}
|
|
43
|
+
function readCapabilities(prefix, env) {
|
|
44
|
+
const result = {};
|
|
45
|
+
for (const cap of CAPABILITIES) {
|
|
46
|
+
result[cap] = readBool(env, `${prefix}_${CAPABILITY_ENV_SUFFIX[cap]}`);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
function inheritList(values, fallback) {
|
|
51
|
+
return values.length > 0 ? values : fallback;
|
|
52
|
+
}
|
|
53
|
+
function buildProfile(alias, env) {
|
|
54
|
+
const prefix = envPrefix(alias);
|
|
55
|
+
const rawUrl = readString(env, `${prefix}_URL`);
|
|
56
|
+
if (rawUrl === undefined) {
|
|
57
|
+
throw new PgMcpError({
|
|
58
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
59
|
+
message: `${prefix}_URL is not set`,
|
|
60
|
+
hint: `set ${prefix}_URL to a postgres:// connection string for profile "${alias}"`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(rawUrl);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Deliberately no `cause` here: Node's URL parse error can echo the raw
|
|
69
|
+
// (password-bearing) input back in its own `.message`, and this
|
|
70
|
+
// PgMcpError lives on for the lifetime of the process on the registry's
|
|
71
|
+
// `errors` map — attaching that as `cause` would be exactly the kind of
|
|
72
|
+
// password storage this module exists to prevent.
|
|
73
|
+
throw new PgMcpError({
|
|
74
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
75
|
+
message: `${prefix}_URL could not be parsed as a URL`,
|
|
76
|
+
hint: "Docker's --env-file does no quote stripping or interpolation and handles a mid-line # inconsistently, " +
|
|
77
|
+
`so a password containing # or a URL containing a space silently truncates — percent-encode the password ` +
|
|
78
|
+
`inside ${prefix}_URL and retry`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const envClass = normalizeProfileEnv(readString(env, `${prefix}_ENV`));
|
|
82
|
+
const capabilities = readCapabilities(prefix, env);
|
|
83
|
+
// `min: 1` keeps a negative or zero MAX_ROWS from reaching core/format.ts,
|
|
84
|
+
// which uses this as a slice/truncation bound. `max: 100_000` is a sane
|
|
85
|
+
// operator ceiling — large enough for any legitimate query preview, small
|
|
86
|
+
// enough that a typo (an extra zero) can't make the server buffer an
|
|
87
|
+
// unbounded result set in memory.
|
|
88
|
+
const globalAllowedDirs = readPathList(env, 'PG_MCP_ALLOWED_DIRS');
|
|
89
|
+
const globalMaxRows = readInt(env, 'PG_MCP_MAX_ROWS', { default: 500, min: 1, max: 100_000 });
|
|
90
|
+
const allowedDirs = inheritList(readPathList(env, `${prefix}_ALLOWED_DIRS`), globalAllowedDirs);
|
|
91
|
+
const maxRows = readInt(env, `${prefix}_MAX_ROWS`, { default: globalMaxRows, min: 1, max: 100_000 });
|
|
92
|
+
const schemasCsv = readCsv(env, `${prefix}_SCHEMAS`);
|
|
93
|
+
const schemas = schemasCsv.length > 0 ? schemasCsv : undefined;
|
|
94
|
+
const host = parsed.hostname;
|
|
95
|
+
const port = parsed.port === '' ? 5432 : Number(parsed.port);
|
|
96
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ''));
|
|
97
|
+
const user = decodeURIComponent(parsed.username);
|
|
98
|
+
// Empty stays `undefined` so `pg`'s own PGPASSWORD/.pgpass fallback still
|
|
99
|
+
// applies, exactly as it did when the URL was handed to pg whole.
|
|
100
|
+
const password = parsed.password === '' ? undefined : decodeURIComponent(parsed.password);
|
|
101
|
+
// Every other query parameter is dropped on the floor — see the `Profile`
|
|
102
|
+
// doc comment for why the URL is not allowed to be authoritative. Dropping
|
|
103
|
+
// one silently would be its own support nightmare, so name them.
|
|
104
|
+
const ignoredParams = Array.from(new Set(parsed.searchParams.keys())).filter((key) => key !== 'sslmode');
|
|
105
|
+
if (ignoredParams.length > 0) {
|
|
106
|
+
logger.warn(`profile "${alias}": ignoring URL parameter(s) ${ignoredParams.join(', ')} — ` +
|
|
107
|
+
`they would silently outrank the ${prefix}_* settings; use those instead`);
|
|
108
|
+
}
|
|
109
|
+
const containerLoopback = inContainer(env) && isLoopbackHost(host);
|
|
110
|
+
const hostRewrite = readString(env, 'PG_MCP_HOST_REWRITE');
|
|
111
|
+
let rewrittenFrom;
|
|
112
|
+
let effectiveHost = host;
|
|
113
|
+
if (containerLoopback && hostRewrite !== undefined) {
|
|
114
|
+
rewrittenFrom = host;
|
|
115
|
+
effectiveHost = hostRewrite;
|
|
116
|
+
logger.warn(`profile "${alias}" host rewritten from ${host} to ${hostRewrite} (PG_MCP_HOST_REWRITE)`);
|
|
117
|
+
}
|
|
118
|
+
const redacted = () => ({
|
|
119
|
+
alias,
|
|
120
|
+
envClass,
|
|
121
|
+
host: effectiveHost,
|
|
122
|
+
port,
|
|
123
|
+
database,
|
|
124
|
+
user,
|
|
125
|
+
containerLoopback,
|
|
126
|
+
rewrittenFrom,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
alias,
|
|
130
|
+
envClass,
|
|
131
|
+
capabilities,
|
|
132
|
+
statementTimeoutMs: readInt(env, `${prefix}_STATEMENT_TIMEOUT_MS`, { default: 30000, min: 1 }),
|
|
133
|
+
lockTimeoutMs: readInt(env, `${prefix}_LOCK_TIMEOUT_MS`, { default: 5000, min: 1 }),
|
|
134
|
+
// 10 minutes: generous enough for a large export/copy, small enough that a
|
|
135
|
+
// misbehaving query cannot pin a connection open indefinitely.
|
|
136
|
+
exportTimeoutMaxMs: readInt(env, `${prefix}_EXPORT_TIMEOUT_MAX_MS`, { default: 600_000, min: 1 }),
|
|
137
|
+
// 1 hour. A backup is a child process, not a statement: the profile's
|
|
138
|
+
// statement_timeout is never applied to it (design.md risk 6), so this is
|
|
139
|
+
// the only thing bounding it — generous enough for a real database, finite
|
|
140
|
+
// enough that a wedged pg_dump cannot hold a server connection forever.
|
|
141
|
+
backupTimeoutMaxMs: readInt(env, `${prefix}_BACKUP_TIMEOUT_MAX_MS`, { default: 3_600_000, min: 1 }),
|
|
142
|
+
idleTxTimeoutMs: readInt(env, `${prefix}_IDLE_TX_TIMEOUT_MS`, { default: 60000, min: 1 }),
|
|
143
|
+
// `max: 50` — Postgres' own default `max_connections` is 100; capping one
|
|
144
|
+
// profile's pool at half that leaves headroom for every other profile
|
|
145
|
+
// plus non-MCP clients, even if an operator fat-fingers an extra zero.
|
|
146
|
+
poolMax: readInt(env, `${prefix}_POOL_MAX`, { default: 4, min: 1, max: 50 }),
|
|
147
|
+
// Global-only, deliberately: this bounds how long a *pool checkout* may
|
|
148
|
+
// wait, and the failure it turns from a silent hang into a loud error —
|
|
149
|
+
// a second checkout taken while the first is still held — is a code bug,
|
|
150
|
+
// not a per-database tuning knob. 10s is comfortably above a healthy
|
|
151
|
+
// TCP+TLS+auth handshake and above any queueing a sane pool does, while
|
|
152
|
+
// still surfacing a wedged profile inside one tool call.
|
|
153
|
+
connectTimeoutMs: readInt(env, 'PG_MCP_CONNECT_TIMEOUT_MS', { default: 10_000, min: 1 }),
|
|
154
|
+
schemas,
|
|
155
|
+
allowedDirs,
|
|
156
|
+
maxRows,
|
|
157
|
+
// The URL's `sslmode` is the fallback, not the winner: an operator who only
|
|
158
|
+
// ever configured TLS in the URL keeps it, and `PG_<ALIAS>_SSL_MODE`
|
|
159
|
+
// overrides it. Either way `db/pool.ts` validates the value — an
|
|
160
|
+
// unrecognized mode is a hard error there, never a quiet drop to plaintext.
|
|
161
|
+
sslMode: readString(env, `${prefix}_SSL_MODE`) ?? (parsed.searchParams.get('sslmode') ?? undefined),
|
|
162
|
+
sslRootCert: readString(env, `${prefix}_SSL_ROOT_CERT`),
|
|
163
|
+
host: effectiveHost,
|
|
164
|
+
port,
|
|
165
|
+
database,
|
|
166
|
+
user,
|
|
167
|
+
containerLoopback,
|
|
168
|
+
rewrittenFrom,
|
|
169
|
+
password: () => password,
|
|
170
|
+
redacted,
|
|
171
|
+
toString: () => `Profile(${alias})`,
|
|
172
|
+
toJSON: redacted,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Pure function of its `env` argument — no module-level singleton, so tests never fight shared state. */
|
|
176
|
+
export function loadProfiles(env = process.env) {
|
|
177
|
+
const aliases = readCsv(env, 'PG_MCP_PROFILES');
|
|
178
|
+
if (aliases.length === 0) {
|
|
179
|
+
throw new PgMcpError({
|
|
180
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
181
|
+
message: 'PG_MCP_PROFILES is not set',
|
|
182
|
+
hint: 'set PG_MCP_PROFILES to a comma-separated list of profile aliases, e.g. PG_MCP_PROFILES=dev,prod',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Seeded with the reserved global prefix so an alias that normalizes to it
|
|
186
|
+
// (e.g. `PG_MCP_PROFILES=MCP`) hits the same fatal collision path as an
|
|
187
|
+
// alias-vs-alias collision below — otherwise its `${prefix}_ALLOWED_DIRS` /
|
|
188
|
+
// `${prefix}_MAX_ROWS` would resolve to the real `PG_MCP_*` globals and the
|
|
189
|
+
// profile would silently read global config as if it were its own.
|
|
190
|
+
const aliasByPrefix = new Map([['PG_MCP', '(reserved for global settings)']]);
|
|
191
|
+
for (const alias of aliases) {
|
|
192
|
+
const prefix = envPrefix(alias);
|
|
193
|
+
const collidingAlias = aliasByPrefix.get(prefix);
|
|
194
|
+
if (collidingAlias !== undefined) {
|
|
195
|
+
throw new PgMcpError({
|
|
196
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
197
|
+
message: prefix === 'PG_MCP'
|
|
198
|
+
? `profile "${alias}" normalizes to the env prefix PG_MCP_, which is reserved for global settings`
|
|
199
|
+
: `profiles "${collidingAlias}" and "${alias}" both normalize to the env prefix ${prefix}_`,
|
|
200
|
+
hint: prefix === 'PG_MCP'
|
|
201
|
+
? `pick a different alias for "${alias}" — the PG_MCP_ prefix is reserved for global settings, not a profile`
|
|
202
|
+
: 'rename one of the aliases so their uppercased, alphanumeric-only forms differ — ' +
|
|
203
|
+
'sharing a prefix would silently share one profile\'s credentials with the other',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
aliasByPrefix.set(prefix, alias);
|
|
207
|
+
}
|
|
208
|
+
const firstAlias = aliases[0];
|
|
209
|
+
if (firstAlias === undefined) {
|
|
210
|
+
// Unreachable: the empty-CSV case above already returned. Guarding anyway
|
|
211
|
+
// keeps `defaultAlias` typed as `string`, not `string | undefined`.
|
|
212
|
+
throw new PgMcpError({
|
|
213
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
214
|
+
message: 'PG_MCP_PROFILES is not set',
|
|
215
|
+
hint: 'set PG_MCP_PROFILES to a comma-separated list of profile aliases, e.g. PG_MCP_PROFILES=dev,prod',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const defaultAlias = readString(env, 'PG_MCP_DEFAULT_PROFILE') ?? firstAlias;
|
|
219
|
+
const profiles = new Map();
|
|
220
|
+
const errors = new Map();
|
|
221
|
+
for (const alias of aliases) {
|
|
222
|
+
try {
|
|
223
|
+
profiles.set(alias, buildProfile(alias, env));
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const pgErr = err instanceof PgMcpError
|
|
227
|
+
? err
|
|
228
|
+
: new PgMcpError({
|
|
229
|
+
code: 'PROFILE_MISCONFIGURED',
|
|
230
|
+
message: err instanceof Error ? err.message : String(err),
|
|
231
|
+
hint: 'fix the profile configuration and restart the server',
|
|
232
|
+
});
|
|
233
|
+
errors.set(alias, pgErr);
|
|
234
|
+
logger.warn(`profile "${alias}" failed to load: ${pgErr.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
defaultAlias,
|
|
239
|
+
errors,
|
|
240
|
+
list: () => Array.from(profiles.values()),
|
|
241
|
+
get: (alias) => {
|
|
242
|
+
const target = alias ?? defaultAlias;
|
|
243
|
+
const profile = profiles.get(target);
|
|
244
|
+
if (profile !== undefined)
|
|
245
|
+
return profile;
|
|
246
|
+
const known = Array.from(profiles.keys());
|
|
247
|
+
throw new PgMcpError({
|
|
248
|
+
code: 'PROFILE_UNKNOWN',
|
|
249
|
+
message: `no profile named "${target}"`,
|
|
250
|
+
hint: known.length > 0
|
|
251
|
+
? `known profiles: ${known.join(', ')}`
|
|
252
|
+
: 'no profiles loaded successfully — check PG_MCP_PROFILES and the startup warnings on stderr',
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../src/config/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,CAAU,CAAC;AAItG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,KAAK,EAAE,aAAa;IACpB,GAAG,EAAE,WAAW;IAChB,KAAK,EAAE,aAAa;IACpB,cAAc,EAAE,uBAAuB;IACvC,MAAM,EAAE,cAAc;IACtB,OAAO,EAAE,eAAe;CACgC,CAAC;AAG3D,MAAM,YAAY,GAAG,IAAI,GAAG,CAAa,CAAC,aAAa,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEnF,2FAA2F;AAC3F,SAAS,mBAAmB,CAAC,GAAuB;IAClD,IAAI,GAAG,KAAK,SAAS,IAAI,YAAY,CAAC,GAAG,CAAC,GAAiB,CAAC;QAAE,OAAO,GAAiB,CAAC;IACvF,OAAO,YAAY,CAAC;AACtB,CAAC;AA4ED,SAAS,gBAAgB,CAAC,MAAc,EAAE,GAAsB;IAC9D,MAAM,MAAM,GAAG,EAAiC,CAAC;IACjD,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,MAAM,IAAI,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,MAAyB,EAAE,QAA2B;IACzE,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC/C,CAAC;AAED,SAAS,YAAY,CAAC,KAAa,EAAE,GAAsB;IACzD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAEhC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,CAAC;IAChD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,GAAG,MAAM,iBAAiB;YACnC,IAAI,EAAE,OAAO,MAAM,wDAAwD,KAAK,GAAG;SACpF,CAAC,CAAC;IACL,CAAC;IAED,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,gEAAgE;QAChE,wEAAwE;QACxE,wEAAwE;QACxE,kDAAkD;QAClD,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,GAAG,MAAM,mCAAmC;YACrD,IAAI,EACF,wGAAwG;gBACxG,0GAA0G;gBAC1G,UAAU,MAAM,gBAAgB;SACnC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,QAAQ,GAAG,mBAAmB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC;IACvE,MAAM,YAAY,GAAG,gBAAgB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEnD,2EAA2E;IAC3E,wEAAwE;IACxE,0EAA0E;IAC1E,qEAAqE;IACrE,kCAAkC;IAClC,MAAM,iBAAiB,GAAG,YAAY,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;IACnE,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAE9F,MAAM,WAAW,GAAG,WAAW,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,MAAM,eAAe,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAChG,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,WAAW,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IAErG,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,UAAU,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAE/D,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IACxE,MAAM,IAAI,GAAG,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjD,0EAA0E;IAC1E,kEAAkE;IAClE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE1F,0EAA0E;IAC1E,2EAA2E;IAC3E,iEAAiE;IACjE,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;IACzG,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,CACT,YAAY,KAAK,gCAAgC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YAC5E,mCAAmC,MAAM,gCAAgC,CAC5E,CAAC;IACJ,CAAC;IAED,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACnE,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;IAE3D,IAAI,aAAiC,CAAC;IACtC,IAAI,aAAa,GAAG,IAAI,CAAC;IACzB,IAAI,iBAAiB,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QACnD,aAAa,GAAG,IAAI,CAAC;QACrB,aAAa,GAAG,WAAW,CAAC;QAC5B,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,yBAAyB,IAAI,OAAO,WAAW,wBAAwB,CAAC,CAAC;IACxG,CAAC;IAED,MAAM,QAAQ,GAAG,GAAoB,EAAE,CAAC,CAAC;QACvC,KAAK;QACL,QAAQ;QACR,IAAI,EAAE,aAAa;QACnB,IAAI;QACJ,QAAQ;QACR,IAAI;QACJ,iBAAiB;QACjB,aAAa;KACd,CAAC,CAAC;IAEH,OAAO;QACL,KAAK;QACL,QAAQ;QACR,YAAY;QACZ,kBAAkB,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,uBAAuB,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QAC9F,aAAa,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,kBAAkB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QACnF,2EAA2E;QAC3E,+DAA+D;QAC/D,kBAAkB,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,wBAAwB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QACjG,sEAAsE;QACtE,0EAA0E;QAC1E,2EAA2E;QAC3E,wEAAwE;QACxE,kBAAkB,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,wBAAwB,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QACnG,eAAe,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,qBAAqB,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QACzF,0EAA0E;QAC1E,sEAAsE;QACtE,uEAAuE;QACvE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;QAC5E,wEAAwE;QACxE,wEAAwE;QACxE,yEAAyE;QACzE,qEAAqE;QACrE,wEAAwE;QACxE,yDAAyD;QACzD,gBAAgB,EAAE,OAAO,CAAC,GAAG,EAAE,2BAA2B,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QACxF,OAAO;QACP,WAAW;QACX,OAAO;QACP,4EAA4E;QAC5E,qEAAqE;QACrE,iEAAiE;QACjE,4EAA4E;QAC5E,OAAO,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC;QACnG,WAAW,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,MAAM,gBAAgB,CAAC;QACvD,IAAI,EAAE,aAAa;QACnB,IAAI;QACJ,QAAQ;QACR,IAAI;QACJ,iBAAiB;QACjB,aAAa;QACb,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ;QACxB,QAAQ;QACR,QAAQ,EAAE,GAAG,EAAE,CAAC,WAAW,KAAK,GAAG;QACnC,MAAM,EAAE,QAAQ;KACjB,CAAC;AACJ,CAAC;AAWD,0GAA0G;AAC1G,MAAM,UAAU,YAAY,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,4BAA4B;YACrC,IAAI,EAAE,iGAAiG;SACxG,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,wEAAwE;IACxE,4EAA4E;IAC5E,4EAA4E;IAC5E,mEAAmE;IACnE,MAAM,aAAa,GAAG,IAAI,GAAG,CAAiB,CAAC,CAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC,CAAC,CAAC;IAC9F,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,cAAc,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,uBAAuB;gBAC7B,OAAO,EACL,MAAM,KAAK,QAAQ;oBACjB,CAAC,CAAC,YAAY,KAAK,+EAA+E;oBAClG,CAAC,CAAC,aAAa,cAAc,UAAU,KAAK,sCAAsC,MAAM,GAAG;gBAC/F,IAAI,EACF,MAAM,KAAK,QAAQ;oBACjB,CAAC,CAAC,+BAA+B,KAAK,uEAAuE;oBAC7G,CAAC,CAAC,kFAAkF;wBAClF,iFAAiF;aACxF,CAAC,CAAC;QACL,CAAC;QACD,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,0EAA0E;QAC1E,oEAAoE;QACpE,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,4BAA4B;YACrC,IAAI,EAAE,iGAAiG;SACxG,CAAC,CAAC;IACL,CAAC;IACD,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE,wBAAwB,CAAC,IAAI,UAAU,CAAC;IAE7E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC7C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,KAAK,GACT,GAAG,YAAY,UAAU;gBACvB,CAAC,CAAC,GAAG;gBACL,CAAC,CAAC,IAAI,UAAU,CAAC;oBACb,IAAI,EAAE,uBAAuB;oBAC7B,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;oBACzD,IAAI,EAAE,sDAAsD;iBAC7D,CAAC,CAAC;YACT,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,qBAAqB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,OAAO;QACL,YAAY;QACZ,MAAM;QACN,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACzC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE;YACb,MAAM,MAAM,GAAG,KAAK,IAAI,YAAY,CAAC;YACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrC,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC;YAE1C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1C,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,qBAAqB,MAAM,GAAG;gBACvC,IAAI,EACF,KAAK,CAAC,MAAM,GAAG,CAAC;oBACd,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBACvC,CAAC,CAAC,4FAA4F;aACnG,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const als = new AsyncLocalStorage();
|
|
3
|
+
/** Runs `fn` inside a fresh contribution slot and returns what was contributed. */
|
|
4
|
+
export async function withAuditContext(fn) {
|
|
5
|
+
const store = {};
|
|
6
|
+
const result = await als.run(store, fn);
|
|
7
|
+
return { result, contributed: store };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Merges `c` into the currently open slot — last write wins per field.
|
|
11
|
+
* A silent no-op outside a context: a unit test that calls a handler directly
|
|
12
|
+
* (no wrapper, no ALS) must not crash, and this is the only call site
|
|
13
|
+
* `runGuardedMutation` has, so it cannot special-case "am I under a wrapper".
|
|
14
|
+
*/
|
|
15
|
+
export function noteAudit(c) {
|
|
16
|
+
const store = als.getStore();
|
|
17
|
+
if (store === undefined)
|
|
18
|
+
return;
|
|
19
|
+
Object.assign(store, c);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=audit-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-context.js","sourceRoot":"","sources":["../../src/core/audit-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAoBrD,MAAM,GAAG,GAAG,IAAI,iBAAiB,EAAqB,CAAC;AAEvD,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAI,EAAoB;IAC5D,MAAM,KAAK,GAAsB,EAAE,CAAC;IACpC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,CAAoB;IAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAChC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { appendFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { readBool, readString } from '../config/env.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
import { assertNoTraversalSegment } from './paths.js';
|
|
7
|
+
import { onShutdown } from './shutdown.js';
|
|
8
|
+
const SQL_MAX_BYTES = 2048;
|
|
9
|
+
function clipSql(sql) {
|
|
10
|
+
const buf = Buffer.from(sql, 'utf8');
|
|
11
|
+
if (buf.byteLength <= SQL_MAX_BYTES)
|
|
12
|
+
return sql;
|
|
13
|
+
return `${buf.subarray(0, SQL_MAX_BYTES).toString('utf8')}…[truncated]`;
|
|
14
|
+
}
|
|
15
|
+
/** A malformed `PG_MCP_AUDIT_PARAMS` degrades to the safe default (redacted) instead of throwing on every audited call — this runs on every tool call, so it must never be the thing that breaks one. */
|
|
16
|
+
function auditParamsEnabled(env) {
|
|
17
|
+
try {
|
|
18
|
+
return readBool(env, 'PG_MCP_AUDIT_PARAMS');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function redactParams(params, env) {
|
|
25
|
+
if (params === undefined)
|
|
26
|
+
return null;
|
|
27
|
+
if (auditParamsEnabled(env))
|
|
28
|
+
return [...params];
|
|
29
|
+
return [`<redacted:${params.length}>`];
|
|
30
|
+
}
|
|
31
|
+
function buildRecord(input, env) {
|
|
32
|
+
return {
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
profile: input.profile,
|
|
35
|
+
envClass: input.envClass,
|
|
36
|
+
tool: input.tool,
|
|
37
|
+
sqlHash: input.sqlHash ?? null,
|
|
38
|
+
sql: input.sql === undefined ? null : clipSql(input.sql),
|
|
39
|
+
params: redactParams(input.params, env),
|
|
40
|
+
rowCount: input.rowCount ?? null,
|
|
41
|
+
durationMs: input.durationMs,
|
|
42
|
+
outcome: input.outcome,
|
|
43
|
+
errorCode: input.errorCode ?? null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Canonicalizes `raw` and rejects an explicit `..` path segment. See the module doc for why this is deliberately not allow-list-scoped. */
|
|
47
|
+
function resolveAuditPath(raw) {
|
|
48
|
+
assertNoTraversalSegment(raw);
|
|
49
|
+
return path.resolve(raw);
|
|
50
|
+
}
|
|
51
|
+
const queues = new Map();
|
|
52
|
+
const warnedPaths = new Set();
|
|
53
|
+
function warnOncePerPath(key, message) {
|
|
54
|
+
if (warnedPaths.has(key))
|
|
55
|
+
return;
|
|
56
|
+
warnedPaths.add(key);
|
|
57
|
+
logger.warn(message);
|
|
58
|
+
}
|
|
59
|
+
function enqueue(rawPath, line) {
|
|
60
|
+
let resolved;
|
|
61
|
+
try {
|
|
62
|
+
resolved = resolveAuditPath(rawPath);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
warnOncePerPath(rawPath, `audit: ${err instanceof Error ? err.message : String(err)} — audit logging disabled for this path`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const prior = queues.get(resolved) ?? Promise.resolve();
|
|
69
|
+
const next = prior
|
|
70
|
+
.then(() => appendFile(resolved, line, 'utf8'))
|
|
71
|
+
.catch((err) => {
|
|
72
|
+
// Degrade to a warn and keep serving — never let a bad audit path fail a tool call.
|
|
73
|
+
logger.warn(`audit: failed to write ${resolved} — ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
});
|
|
75
|
+
queues.set(resolved, next);
|
|
76
|
+
}
|
|
77
|
+
/** Records one tool call. No-op (opens nothing) when `PG_MCP_AUDIT_PATH` is unset. */
|
|
78
|
+
export function auditLog(input, env = process.env) {
|
|
79
|
+
const rawPath = readString(env, 'PG_MCP_AUDIT_PATH');
|
|
80
|
+
if (rawPath === undefined)
|
|
81
|
+
return;
|
|
82
|
+
const line = `${JSON.stringify(buildRecord(input, env))}\n`;
|
|
83
|
+
enqueue(rawPath, line);
|
|
84
|
+
}
|
|
85
|
+
/** Resolves once every write enqueued so far has settled — used by tests and by the shutdown hook. */
|
|
86
|
+
export async function flush() {
|
|
87
|
+
await Promise.allSettled(Array.from(queues.values()));
|
|
88
|
+
}
|
|
89
|
+
// Without this, buffered audit writes are lost when the container stops.
|
|
90
|
+
onShutdown(flush);
|
|
91
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/core/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAmD3C,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACrC,IAAI,GAAG,CAAC,UAAU,IAAI,aAAa;QAAE,OAAO,GAAG,CAAC;IAChD,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC;AAC1E,CAAC;AAED,yMAAyM;AACzM,SAAS,kBAAkB,CAAC,GAAsB;IAChD,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,MAAsC,EAAE,GAAsB;IAClF,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,kBAAkB,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;IAChD,OAAO,CAAC,aAAa,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,WAAW,CAAC,KAAiB,EAAE,GAAsB;IAC5D,OAAO;QACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;QAC9B,GAAG,EAAE,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;QACxD,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC;QACvC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;QAChC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;KACnC,CAAC;AACJ,CAAC;AAED,4IAA4I;AAC5I,SAAS,gBAAgB,CAAC,GAAW;IACnC,wBAAwB,CAAC,GAAG,CAAC,CAAC;IAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;AAChD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;AAEtC,SAAS,eAAe,CAAC,GAAW,EAAE,OAAe;IACnD,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IACjC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,OAAO,CAAC,OAAe,EAAE,IAAY;IAC5C,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAe,CACb,OAAO,EACP,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,yCAAyC,CACpG,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,KAAK;SACf,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;SAC9C,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,oFAAoF;QACpF,MAAM,CAAC,IAAI,CAAC,0BAA0B,QAAQ,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1G,CAAC,CAAC,CAAC;IACL,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,KAAiB,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC9E,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAElC,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC;IAC5D,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,sGAAsG;AACtG,MAAM,CAAC,KAAK,UAAU,KAAK;IACzB,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,yEAAyE;AACzE,UAAU,CAAC,KAAK,CAAC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded identifier types.
|
|
3
|
+
*
|
|
4
|
+
* Every SQL-building signature in this codebase takes `SafeIdent`, never
|
|
5
|
+
* `string`. A raw user-supplied string does not satisfy `SafeIdent`, so
|
|
6
|
+
* passing one is a compile error at the call site of any function typed to
|
|
7
|
+
* take `SafeIdent` — the type system, not review discipline, is what keeps
|
|
8
|
+
* identifiers out of SQL text.
|
|
9
|
+
*
|
|
10
|
+
* The only legitimate way to obtain one of these values is a catalog lookup in
|
|
11
|
+
* `core/identifiers.ts` (added in a later phase), which resolves the name against
|
|
12
|
+
* `pg_catalog`, quotes it, and then brands the result.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Applies a brand to a plain string.
|
|
16
|
+
*
|
|
17
|
+
* @internal Callable ONLY from `core/identifiers.ts`, and only on a string that
|
|
18
|
+
* has just been returned by a `pg_catalog` lookup and quoted. Calling it
|
|
19
|
+
* anywhere else defeats the entire point of the brand.
|
|
20
|
+
*/
|
|
21
|
+
export function unsafeBrand(value) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=brands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brands.js","sourceRoot":"","sources":["../../src/core/brands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,KAA6B,CAAC;AACvC,CAAC"}
|