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.
Files changed (130) hide show
  1. package/README.md +608 -2
  2. package/dist/config/env.js +67 -0
  3. package/dist/config/env.js.map +1 -0
  4. package/dist/config/profiles.js +257 -0
  5. package/dist/config/profiles.js.map +1 -0
  6. package/dist/core/audit-context.js +21 -0
  7. package/dist/core/audit-context.js.map +1 -0
  8. package/dist/core/audit.js +91 -0
  9. package/dist/core/audit.js.map +1 -0
  10. package/dist/core/brands.js +24 -0
  11. package/dist/core/brands.js.map +1 -0
  12. package/dist/core/classifier.js +279 -0
  13. package/dist/core/classifier.js.map +1 -0
  14. package/dist/core/errors.js +197 -0
  15. package/dist/core/errors.js.map +1 -0
  16. package/dist/core/format.js +258 -0
  17. package/dist/core/format.js.map +1 -0
  18. package/dist/core/guards.js +513 -0
  19. package/dist/core/guards.js.map +1 -0
  20. package/dist/core/identifiers.js +431 -0
  21. package/dist/core/identifiers.js.map +1 -0
  22. package/dist/core/limits.js +79 -0
  23. package/dist/core/limits.js.map +1 -0
  24. package/dist/core/logger.js +34 -0
  25. package/dist/core/logger.js.map +1 -0
  26. package/dist/core/paginate.js +195 -0
  27. package/dist/core/paginate.js.map +1 -0
  28. package/dist/core/paths.js +392 -0
  29. package/dist/core/paths.js.map +1 -0
  30. package/dist/core/policy.js +98 -0
  31. package/dist/core/policy.js.map +1 -0
  32. package/dist/core/shutdown.js +10 -0
  33. package/dist/core/shutdown.js.map +1 -0
  34. package/dist/core/stdout-guard.js +25 -0
  35. package/dist/core/stdout-guard.js.map +1 -0
  36. package/dist/db/catalog.js +1006 -0
  37. package/dist/db/catalog.js.map +1 -0
  38. package/dist/db/pool.js +158 -0
  39. package/dist/db/pool.js.map +1 -0
  40. package/dist/db/session.js +258 -0
  41. package/dist/db/session.js.map +1 -0
  42. package/dist/db/types.js +2 -0
  43. package/dist/db/types.js.map +1 -0
  44. package/dist/index.js +91 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/resources/index.js +88 -0
  47. package/dist/resources/index.js.map +1 -0
  48. package/dist/runtime/binaries.js +356 -0
  49. package/dist/runtime/binaries.js.map +1 -0
  50. package/dist/runtime/env-file.js +62 -0
  51. package/dist/runtime/env-file.js.map +1 -0
  52. package/dist/runtime/environment.js +34 -0
  53. package/dist/runtime/environment.js.map +1 -0
  54. package/dist/server.js +106 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/snapshot/capture.js +189 -0
  57. package/dist/snapshot/capture.js.map +1 -0
  58. package/dist/snapshot/diff.js +254 -0
  59. package/dist/snapshot/diff.js.map +1 -0
  60. package/dist/snapshot/migrate.js +1596 -0
  61. package/dist/snapshot/migrate.js.map +1 -0
  62. package/dist/snapshot/model.js +80 -0
  63. package/dist/snapshot/model.js.map +1 -0
  64. package/dist/tools/_archive_meta.js +138 -0
  65. package/dist/tools/_archive_meta.js.map +1 -0
  66. package/dist/tools/_batch_shared.js +73 -0
  67. package/dist/tools/_batch_shared.js.map +1 -0
  68. package/dist/tools/_export_format.js +107 -0
  69. package/dist/tools/_export_format.js.map +1 -0
  70. package/dist/tools/_object_kinds.js +97 -0
  71. package/dist/tools/_object_kinds.js.map +1 -0
  72. package/dist/tools/_relation_detail.js +55 -0
  73. package/dist/tools/_relation_detail.js.map +1 -0
  74. package/dist/tools/_shared.js +17 -0
  75. package/dist/tools/_shared.js.map +1 -0
  76. package/dist/tools/_snapshot_io.js +238 -0
  77. package/dist/tools/_snapshot_io.js.map +1 -0
  78. package/dist/tools/admin/_shared.js +18 -0
  79. package/dist/tools/admin/_shared.js.map +1 -0
  80. package/dist/tools/admin/extensions.js +80 -0
  81. package/dist/tools/admin/extensions.js.map +1 -0
  82. package/dist/tools/admin/grants.js +207 -0
  83. package/dist/tools/admin/grants.js.map +1 -0
  84. package/dist/tools/admin/index.js +138 -0
  85. package/dist/tools/admin/index.js.map +1 -0
  86. package/dist/tools/admin/maintenance.js +106 -0
  87. package/dist/tools/admin/maintenance.js.map +1 -0
  88. package/dist/tools/admin/roles.js +144 -0
  89. package/dist/tools/admin/roles.js.map +1 -0
  90. package/dist/tools/admin/sessions.js +104 -0
  91. package/dist/tools/admin/sessions.js.map +1 -0
  92. package/dist/tools/admin/stats.js +63 -0
  93. package/dist/tools/admin/stats.js.map +1 -0
  94. package/dist/tools/backup.js +288 -0
  95. package/dist/tools/backup.js.map +1 -0
  96. package/dist/tools/connection_status.js +138 -0
  97. package/dist/tools/connection_status.js.map +1 -0
  98. package/dist/tools/copy.js +355 -0
  99. package/dist/tools/copy.js.map +1 -0
  100. package/dist/tools/delete_batch.js +73 -0
  101. package/dist/tools/delete_batch.js.map +1 -0
  102. package/dist/tools/describe_object.js +87 -0
  103. package/dist/tools/describe_object.js.map +1 -0
  104. package/dist/tools/execute_sql.js +395 -0
  105. package/dist/tools/execute_sql.js.map +1 -0
  106. package/dist/tools/explain.js +134 -0
  107. package/dist/tools/explain.js.map +1 -0
  108. package/dist/tools/export.js +215 -0
  109. package/dist/tools/export.js.map +1 -0
  110. package/dist/tools/get_schema.js +148 -0
  111. package/dist/tools/get_schema.js.map +1 -0
  112. package/dist/tools/index.js +40 -0
  113. package/dist/tools/index.js.map +1 -0
  114. package/dist/tools/insert_batch.js +212 -0
  115. package/dist/tools/insert_batch.js.map +1 -0
  116. package/dist/tools/list_objects.js +160 -0
  117. package/dist/tools/list_objects.js.map +1 -0
  118. package/dist/tools/prepare_migration.js +112 -0
  119. package/dist/tools/prepare_migration.js.map +1 -0
  120. package/dist/tools/restore.js +301 -0
  121. package/dist/tools/restore.js.map +1 -0
  122. package/dist/tools/schema_diff.js +79 -0
  123. package/dist/tools/schema_diff.js.map +1 -0
  124. package/dist/tools/schema_snapshot.js +57 -0
  125. package/dist/tools/schema_snapshot.js.map +1 -0
  126. package/dist/tools/select.js +162 -0
  127. package/dist/tools/select.js.map +1 -0
  128. package/dist/tools/update_batch.js +154 -0
  129. package/dist/tools/update_batch.js.map +1 -0
  130. package/package.json +47 -5
@@ -0,0 +1,195 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { PgMcpError } from './errors.js';
3
+ function effectiveNulls(col) {
4
+ return col.nulls ?? (col.direction === 'ASC' ? 'LAST' : 'FIRST');
5
+ }
6
+ const MAX_CURSOR_BASE64_LENGTH = 4000;
7
+ function invalidCursor(reason) {
8
+ return new PgMcpError({
9
+ code: 'CURSOR_INVALID',
10
+ message: `pagination cursor is invalid: ${reason}`,
11
+ hint: 'omit the cursor to start from the first page, or pass back the exact nextCursor value from the previous call',
12
+ });
13
+ }
14
+ /**
15
+ * Hashes the query shape (table, filter, sort columns/directions — whatever
16
+ * the caller considers load-bearing) so a cursor minted for one query can
17
+ * never be replayed against a differently-shaped one. Not a security
18
+ * boundary, a correctness one: 16 hex chars (64 bits) is plenty to catch
19
+ * accidental/adversarial replay across queries.
20
+ */
21
+ export function computeShapeHash(shape) {
22
+ return createHash('sha256').update(JSON.stringify(shape)).digest('hex').slice(0, 16);
23
+ }
24
+ /** Base64url, JSON envelope: `{ v: sort values, h: shape hash }`. */
25
+ export function encodeCursor(sortValues, queryShapeHash) {
26
+ const payload = { v: sortValues, h: queryShapeHash };
27
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
28
+ }
29
+ function isCursorScalar(x) {
30
+ return x === null || typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean';
31
+ }
32
+ function isSortValueArray(x) {
33
+ return (Array.isArray(x) &&
34
+ x.every((v) => typeof v === 'object' && v !== null && typeof v.column === 'string' && isCursorScalar(v.value)));
35
+ }
36
+ /**
37
+ * Decodes and validates a cursor, rejecting anything malformed, oversized, or
38
+ * tampered — including a shape hash that does not match `queryShapeHash` for
39
+ * the request currently being served (a cursor minted for a different filter,
40
+ * sort, or table).
41
+ */
42
+ export function decodeCursor(cursor, queryShapeHash) {
43
+ if (cursor.length === 0 || cursor.length > MAX_CURSOR_BASE64_LENGTH) {
44
+ throw invalidCursor('empty or too large');
45
+ }
46
+ let json;
47
+ try {
48
+ json = Buffer.from(cursor, 'base64url').toString('utf8');
49
+ }
50
+ catch {
51
+ throw invalidCursor('not valid base64url');
52
+ }
53
+ let payload;
54
+ try {
55
+ payload = JSON.parse(json);
56
+ }
57
+ catch {
58
+ throw invalidCursor('not valid JSON');
59
+ }
60
+ if (typeof payload !== 'object' || payload === null)
61
+ throw invalidCursor('malformed payload');
62
+ const { v, h } = payload;
63
+ if (typeof h !== 'string' || !isSortValueArray(v))
64
+ throw invalidCursor('malformed payload');
65
+ if (h !== queryShapeHash)
66
+ throw invalidCursor('was minted for a different query (filter, sort, or table changed)');
67
+ return v;
68
+ }
69
+ /**
70
+ * Appends a tiebreaker column so keyset comparisons are always over a unique
71
+ * key, even when the caller's own sort columns are not unique on their own
72
+ * (e.g. `ORDER BY created_at` with duplicate timestamps would otherwise skip
73
+ * or repeat rows across pages).
74
+ *
75
+ * Prefers the relation's primary key (already unique, already indexed, so the
76
+ * keyset predicate stays sargable). Falls back to `ctid` — PostgreSQL's
77
+ * physical row identifier — only when there is no primary key to use; `ctid`
78
+ * has a btree-comparable type and is always present, but it is stable only
79
+ * within a transaction (a concurrent `VACUUM`/`UPDATE` can move a row), so it
80
+ * is the last resort, not the default.
81
+ */
82
+ /**
83
+ * Reverses `quoteIdent`: every `SafeIdent` reaching this module was produced
84
+ * there, so stripping the wrapping quotes and un-doubling embedded ones is a
85
+ * total, safe inverse — not a new identifier-trust boundary. Falls through
86
+ * unchanged for the one hardcoded, never-quoted literal this module also
87
+ * treats as a `SafeIdent`: `'ctid' as SafeIdent` below.
88
+ */
89
+ function unquoteIdent(ident) {
90
+ const raw = ident;
91
+ return raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1).replace(/""/g, '"') : raw;
92
+ }
93
+ export function withTiebreaker(columns, pkColumns) {
94
+ const already = new Set(columns.map((c) => c.name));
95
+ const tiebreakers = pkColumns.length > 0
96
+ ? pkColumns
97
+ .map((ident) => ({ name: unquoteIdent(ident), ident, direction: 'ASC' }))
98
+ .filter((c) => !already.has(c.name))
99
+ : already.has('ctid')
100
+ ? []
101
+ : [{ name: 'ctid', ident: 'ctid', direction: 'ASC' }];
102
+ return [...columns, ...tiebreakers];
103
+ }
104
+ export function orderByClause(columns) {
105
+ return columns.map((c) => `${c.ident} ${c.direction} NULLS ${effectiveNulls(c)}`).join(', ');
106
+ }
107
+ /**
108
+ * Reads the sort values off the last row of a page — the seed for the next
109
+ * page's cursor. A non-scalar sort-column value (e.g. a sort column that
110
+ * turned out to be an array/object type) means this page cannot be
111
+ * continued — that is a server-side condition, not a malformed *caller*
112
+ * cursor, but it still must surface through the same `PgMcpError` taxonomy
113
+ * every other tool failure does (never a bare `Error`, which `toToolResult`
114
+ * would render as an unclassified `PG_ERROR`-shaped message anyway, but
115
+ * without a `code`/`hint` a caller can act on).
116
+ */
117
+ export function cursorValuesFromRow(row, columns) {
118
+ return columns.map((c) => {
119
+ const raw = row[c.name];
120
+ const value = raw instanceof Date ? raw.toISOString() : raw;
121
+ if (!isCursorScalar(value)) {
122
+ throw new PgMcpError({
123
+ code: 'PG_ERROR',
124
+ message: `sort column "${c.name}" has a non-scalar value and cannot seed a keyset cursor`,
125
+ hint: 'choose a different sort column (scalar, non-array/object) or drop pagination for this query',
126
+ });
127
+ }
128
+ return { column: c.name, value };
129
+ });
130
+ }
131
+ /**
132
+ * Builds a parameterized `WHERE` fragment for "rows after this cursor",
133
+ * correct under mixed ASC/DESC columns: a plain row-wise `(a, b) > (x, y)`
134
+ * comparison assumes every column compares the same direction, so this
135
+ * expands to the general OR-chain instead —
136
+ * `(c1 ⊘ v1) OR (c1 = v1 AND c2 ⊘ v2) OR (c1 = v1 AND c2 = v2 AND c3 ⊘ v3) …`
137
+ * where `⊘` is `>` for an ASC column and `<` for a DESC one. Every value is a
138
+ * bound parameter (`$N`), starting at `startParamIndex` so this can be spliced
139
+ * into a WHERE clause that already has other parameterized filters ahead of it.
140
+ *
141
+ * Equality-prefix branches use `IS NOT DISTINCT FROM` rather than `=` — plain
142
+ * `=` is never true against a NULL prior-column value, which would silently
143
+ * drop every later branch whenever an earlier sort column is nullable.
144
+ *
145
+ * Nullable sort columns are the sharp edge here: `effectiveNulls(col)` must
146
+ * report the exact same placement `orderByClause` emits for that column, or
147
+ * the keyset predicate and the `ORDER BY` disagree and rows get skipped or
148
+ * repeated across pages. Per column, "strictly after cursor value `v`" is:
149
+ * - `v` non-null, `NULLS LAST` -> `(col ⊘ v OR col IS NULL)` (NULL sorts
150
+ * after every non-null value, so it is always "after" a non-null cursor).
151
+ * - `v` non-null, `NULLS FIRST` -> `col ⊘ v` (NULLs already sort before any
152
+ * non-null value, so none of them are "after" a non-null cursor).
153
+ * - `v` null, `NULLS FIRST` -> `col IS NOT NULL` (every non-null value
154
+ * sorts after every NULL; other NULLs are equal, deferred to the next
155
+ * branch's equality prefix).
156
+ * - `v` null, `NULLS LAST` -> `FALSE` (NULL is already the maximum
157
+ * element; nothing sorts after it).
158
+ */
159
+ export function buildKeysetWhere(columns, cursorValues, startParamIndex) {
160
+ const params = [];
161
+ let paramIndex = startParamIndex;
162
+ const byColumn = new Map(cursorValues.map((v) => [v.column, v.value]));
163
+ const branches = [];
164
+ for (let i = 0; i < columns.length; i++) {
165
+ const col = columns[i];
166
+ if (col === undefined)
167
+ continue;
168
+ if (!byColumn.has(col.name))
169
+ continue; // no cursor value for this column -> first page, caller passes an empty cursorValues
170
+ const eqParts = [];
171
+ for (let j = 0; j < i; j++) {
172
+ const prior = columns[j];
173
+ if (prior === undefined)
174
+ continue;
175
+ const value = byColumn.get(prior.name) ?? null;
176
+ params.push(value);
177
+ eqParts.push(`${prior.ident} IS NOT DISTINCT FROM $${paramIndex++}`);
178
+ }
179
+ const value = byColumn.get(col.name) ?? null;
180
+ const op = col.direction === 'ASC' ? '>' : '<';
181
+ const nulls = effectiveNulls(col);
182
+ let colCondition;
183
+ if (value === null) {
184
+ colCondition = nulls === 'FIRST' ? `${col.ident} IS NOT NULL` : 'FALSE';
185
+ }
186
+ else {
187
+ params.push(value);
188
+ const base = `${col.ident} ${op} $${paramIndex++}`;
189
+ colCondition = nulls === 'LAST' ? `(${base} OR ${col.ident} IS NULL)` : base;
190
+ }
191
+ branches.push(eqParts.length === 0 ? colCondition : `(${[...eqParts, colCondition].join(' AND ')})`);
192
+ }
193
+ return { clause: branches.length === 0 ? 'TRUE' : branches.join(' OR '), params };
194
+ }
195
+ //# sourceMappingURL=paginate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginate.js","sourceRoot":"","sources":["../../src/core/paginate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,392 @@
1
+ import { once } from 'node:events';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { access, link, open, realpath, rename, rm, stat, unlink } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { finished } from 'node:stream/promises';
7
+ import { readString } from '../config/env.js';
8
+ import { PgMcpError } from './errors.js';
9
+ /**
10
+ * The one path policy for every file-producing tool (`export`, `copy`, and
11
+ * later `backup`/`schema_snapshot`) — see design.md "Workspace and file
12
+ * paths". One resolution rule, identical in container and host mode:
13
+ *
14
+ * - A **relative** input resolves against `allowedDirs[0]` (`dumps/x.csv` ->
15
+ * `<allowedDirs[0]>/dumps/x.csv`).
16
+ * - An **absolute** input is normalized as-is.
17
+ * - Either way, the candidate's **parent directory is `realpath`'d** — which
18
+ * catches a symlink escape and still works for a file that does not exist
19
+ * yet, since only the parent (not the file itself) has to exist — and the
20
+ * result must land inside one of `allowedDirs`, each realpath'd the same
21
+ * way so a symlinked allow-listed directory does not falsely reject
22
+ * everything inside it.
23
+ * - An explicit `..` segment anywhere in the raw input (or in the resolved
24
+ * candidate) is refused outright, before any filesystem call. An empty
25
+ * `allowedDirs` refuses everything — the correct default for a host run
26
+ * that never set `PG_MCP_ALLOWED_DIRS`.
27
+ *
28
+ * **The containment check is segment-wise, never `startsWith`.** `startsWith`
29
+ * is the classic bug here: `/workspace-evil` starts with `/workspace` as a
30
+ * string but is not inside it as a directory. {@link resolveWorkspacePath}
31
+ * compares path *segments* instead, so `/workspace-evil/x` is correctly
32
+ * refused against an allow-list of `/workspace`. Comparison is
33
+ * case-insensitive on `win32` (NTFS/ReFS default) and case-sensitive
34
+ * everywhere else, decided by `opts.platform` (defaults to `process.platform`,
35
+ * overridable so a Windows test run can still exercise the POSIX branch).
36
+ */
37
+ const PARTIAL_SUFFIX = '.partial';
38
+ function hasErrnoCode(err, code) {
39
+ return typeof err === 'object' && err !== null && 'code' in err && err.code === code;
40
+ }
41
+ function isEnoent(err) {
42
+ return hasErrnoCode(err, 'ENOENT');
43
+ }
44
+ /** Every path segment, `/` and `\` both treated as separators regardless of platform (an operator could type either into an env var). */
45
+ function rawSegments(p) {
46
+ return p.split(/[\\/]/);
47
+ }
48
+ function pathSegments(p) {
49
+ return rawSegments(p).filter((s) => s.length > 0);
50
+ }
51
+ /**
52
+ * Refuses an explicit `..` path segment, and a NUL byte. Exported so
53
+ * `core/audit.ts` (a different, allow-list-independent path setting) shares
54
+ * this one check instead of carrying its own copy that could drift from this
55
+ * one.
56
+ *
57
+ * The NUL check is here rather than at the caller because without it a `\0`
58
+ * sails past every containment check and only blows up inside `node:fs` as a
59
+ * raw `TypeError`/`ERR_INVALID_ARG_VALUE` — an internal-looking crash where
60
+ * the caller deserves `PATH_DENIED`.
61
+ */
62
+ export function assertNoTraversalSegment(raw) {
63
+ if (raw.includes('\0')) {
64
+ throw new PgMcpError({
65
+ code: 'PATH_DENIED',
66
+ message: `"${raw.replace(/\0/g, '\\0')}" contains a NUL byte`,
67
+ hint: 'remove the NUL byte — a path is plain text, and node:fs rejects embedded NULs outright',
68
+ });
69
+ }
70
+ if (rawSegments(raw).includes('..')) {
71
+ throw new PgMcpError({
72
+ code: 'PATH_DENIED',
73
+ message: `"${raw}" contains a '..' path segment`,
74
+ hint: 'remove the .. segment — paths resolve relative to an allowed directory and are never allowed to climb above it',
75
+ });
76
+ }
77
+ }
78
+ /**
79
+ * `realpath`s the nearest existing ancestor of `p` and re-appends whatever
80
+ * trailing segments do not exist yet, so a not-yet-created export
81
+ * destination (or an allow-listed directory in a from-scratch test) still
82
+ * gets every *real* symlink along the way resolved, without requiring the
83
+ * full path to already exist on disk.
84
+ */
85
+ async function nearestExistingRealDir(p) {
86
+ try {
87
+ return await realpath(p);
88
+ }
89
+ catch (err) {
90
+ if (!isEnoent(err))
91
+ throw err;
92
+ const parent = path.dirname(p);
93
+ if (parent === p)
94
+ return p; // reached the filesystem root; nothing left to resolve
95
+ const realParent = await nearestExistingRealDir(parent);
96
+ return path.join(realParent, path.basename(p));
97
+ }
98
+ }
99
+ function normalizeForCompare(segment, platform) {
100
+ return platform === 'win32' ? segment.toLowerCase() : segment;
101
+ }
102
+ /**
103
+ * True when `dir` is `candidate` itself or a proper ancestor of it, compared
104
+ * segment-by-segment — never `candidate.startsWith(dir)` (see the module doc
105
+ * for why that is the classic bug).
106
+ */
107
+ function isWithinOrEqual(candidate, dir, platform) {
108
+ const candidateSegs = pathSegments(candidate).map((s) => normalizeForCompare(s, platform));
109
+ const dirSegs = pathSegments(dir).map((s) => normalizeForCompare(s, platform));
110
+ if (candidateSegs.length < dirSegs.length)
111
+ return false;
112
+ return dirSegs.every((seg, i) => seg === candidateSegs[i]);
113
+ }
114
+ /**
115
+ * Resolves a caller-supplied file path against `allowedDirs`, per the module
116
+ * doc above. Returns the real, absolute, symlink-resolved path a tool should
117
+ * actually open — or throws `PgMcpError` with code `PATH_DENIED`.
118
+ */
119
+ export async function resolveWorkspacePath(input, allowedDirs, opts = {}) {
120
+ const platform = opts.platform ?? process.platform;
121
+ if (allowedDirs.length === 0) {
122
+ throw new PgMcpError({
123
+ code: 'PATH_DENIED',
124
+ message: `"${input}" was refused: no allowed directory is configured`,
125
+ hint: 'set PG_MCP_ALLOWED_DIRS (or this profile\'s ALLOWED_DIRS) to a mounted directory to enable file I/O',
126
+ });
127
+ }
128
+ assertNoTraversalSegment(input);
129
+ const anchor = allowedDirs[0]; // non-empty checked above
130
+ const candidate = path.isAbsolute(input) ? path.normalize(input) : path.resolve(anchor, input);
131
+ assertNoTraversalSegment(candidate);
132
+ const parentDir = path.dirname(candidate);
133
+ const baseName = path.basename(candidate);
134
+ let realParent;
135
+ try {
136
+ realParent = await nearestExistingRealDir(parentDir);
137
+ }
138
+ catch (err) {
139
+ throw new PgMcpError({
140
+ code: 'PATH_DENIED',
141
+ message: `"${input}" could not be resolved: ${err instanceof Error ? err.message : String(err)}`,
142
+ hint: 'check that the destination directory (or its nearest existing ancestor) is readable',
143
+ cause: err,
144
+ });
145
+ }
146
+ const resolvedAllowedDirs = await Promise.all(allowedDirs.map((dir) => nearestExistingRealDir(path.resolve(dir)).catch(() => path.resolve(dir))));
147
+ const resolved = path.join(realParent, baseName);
148
+ const withinAllowList = resolvedAllowedDirs.some((dir) => isWithinOrEqual(realParent, dir, platform));
149
+ if (!withinAllowList) {
150
+ // Containment is checked on the PARENT, so an input that *is* an allowed
151
+ // directory fails it — its parent is above the allow-list. Refusing is
152
+ // right (a file is what every caller wants, and an export would put its
153
+ // `.partial` sibling outside the allow-list), but "outside every allowed
154
+ // directory" is a lie about the one path the operator explicitly allowed.
155
+ const isAllowedDirItself = resolvedAllowedDirs.some((dir) => isWithinOrEqual(resolved, dir, platform));
156
+ throw new PgMcpError({
157
+ code: 'PATH_DENIED',
158
+ message: isAllowedDirItself
159
+ ? `"${input}" is an allowed directory itself, not a file inside one`
160
+ : `"${input}" resolves to ${resolved}, which is outside every allowed directory`,
161
+ hint: isAllowedDirItself
162
+ ? 'append a file name — every tool here reads or writes a file, never a directory'
163
+ : `use a path under one of: ${allowedDirs.join(', ')}`,
164
+ details: { input, allowedDirs: [...allowedDirs] },
165
+ });
166
+ }
167
+ return resolved;
168
+ }
169
+ /**
170
+ * Renders the host-visible form of a resolved workspace path, from
171
+ * `PG_MCP_WORKSPACE_HOST_PATH` — set by an operator to the real host
172
+ * directory that is bind-mounted onto `allowedDirs[0]` inside the container,
173
+ * so a response can tell a human where the file actually landed on their
174
+ * machine. `undefined` when the env var is unset, or when `resolvedPath`
175
+ * is not under `allowedDirs[0]` at all (nothing sane to render).
176
+ */
177
+ export function renderHostPath(resolvedPath, allowedDirs, env = process.env) {
178
+ const hostRoot = readString(env, 'PG_MCP_WORKSPACE_HOST_PATH');
179
+ const anchor = allowedDirs[0];
180
+ if (hostRoot === undefined || anchor === undefined)
181
+ return undefined;
182
+ const rel = path.relative(path.resolve(anchor), resolvedPath);
183
+ if (rel.startsWith('..') || path.isAbsolute(rel))
184
+ return undefined;
185
+ if (rel === '')
186
+ return hostRoot;
187
+ const trimmedHostRoot = hostRoot.replace(/[\\/]+$/, '');
188
+ return `${trimmedHostRoot}/${rel.split(path.sep).join('/')}`;
189
+ }
190
+ /**
191
+ * The size refusal, exported because `tools/copy.ts` re-counts bytes as they
192
+ * stream past and must raise the identical error mid-flight — a file that grew
193
+ * after the `fstat` cannot slip through by being late.
194
+ */
195
+ export function fileTooLarge(filePath, bytes, opts) {
196
+ return new PgMcpError({
197
+ code: 'LIMIT_EXCEEDED',
198
+ message: `"${filePath}" is ${bytes} bytes, over the ${opts.maxBytes}-byte ${opts.limitLabel} limit`,
199
+ hint: opts.limitHint,
200
+ details: { path: filePath, bytes, maxBytes: opts.maxBytes },
201
+ });
202
+ }
203
+ /**
204
+ * **The one read-side path policy.** Resolves an operator-supplied path,
205
+ * refuses it — for containment, for kind and for size — *before a single byte
206
+ * is read*, and hands back the open handle the caller streams or reads from.
207
+ * The caller owns that handle and must close it on every path.
208
+ *
209
+ * This existed three times before Phase 6 (`tools/copy.ts#resolveImportSource`,
210
+ * `tools/_snapshot_io.ts#readSnapshotFile`, and would have been a fourth in
211
+ * `postgres_restore` — the one that feeds an archive to `pg_restore`). Three
212
+ * independent copies of a security check are three chances for one of them to
213
+ * drift; this is the single copy, and each caller supplies only its wording.
214
+ *
215
+ * Four things happen here, and each closes a specific hole:
216
+ *
217
+ * 1. **Double {@link resolveWorkspacePath}.** The first call realpaths the
218
+ * *parent* only (deliberately — a write destination need not exist yet), so
219
+ * a symlink planted inside an allowed directory would be followed straight
220
+ * out of the allow-list. `realpath` then follows the file itself and the
221
+ * second call proves where it really landed is still inside.
222
+ * 2. **Open once, check the handle.** Every check above is on a *name*, and a
223
+ * name can be re-pointed between the last check and the read. `handle.stat()`
224
+ * is an fstat on the open inode; the bytes the caller gets are the bytes of
225
+ * the inode this function approved.
226
+ * 3. **`ino` comparison** against a path-based `stat` of the resolved path,
227
+ * which refuses a swap that happened after the open. `ino` only, deliberately:
228
+ * on Windows a path-based `stat` reports `dev: 0` while an `fstat` on the same
229
+ * file reports the real volume, so comparing `dev` would refuse every read
230
+ * there.
231
+ * 4. **Size cap before the read**, from the fstat — not after a `readFile` has
232
+ * already pulled 564 MB into the heap.
233
+ */
234
+ export async function resolveWorkspaceFileForRead(input, allowedDirs, opts) {
235
+ const candidate = await resolveWorkspacePath(input, allowedDirs);
236
+ let handle;
237
+ try {
238
+ handle = await open(candidate, 'r');
239
+ }
240
+ catch (err) {
241
+ throw new PgMcpError({
242
+ code: 'PATH_DENIED',
243
+ message: `"${input}" does not exist or cannot be read`,
244
+ hint: opts.missingHint,
245
+ details: { input },
246
+ cause: err,
247
+ });
248
+ }
249
+ try {
250
+ const info = await handle.stat();
251
+ if (!info.isFile()) {
252
+ throw new PgMcpError({
253
+ code: 'PATH_DENIED',
254
+ message: `"${input}" is not a regular file`,
255
+ hint: opts.notFileHint,
256
+ details: { input },
257
+ });
258
+ }
259
+ const resolvedPath = await resolveWorkspacePath(await realpath(candidate), allowedDirs);
260
+ const onDisk = await stat(resolvedPath);
261
+ if (onDisk.ino !== info.ino) {
262
+ throw new PgMcpError({
263
+ code: 'PATH_DENIED',
264
+ message: `"${input}" was replaced while it was being checked — the open file is not the one at that path`,
265
+ hint: 'nothing was read; re-run once the path is stable',
266
+ details: { input },
267
+ });
268
+ }
269
+ if (info.size > opts.maxBytes)
270
+ throw fileTooLarge(resolvedPath, info.size, opts);
271
+ return { path: resolvedPath, bytes: info.size, handle };
272
+ }
273
+ catch (err) {
274
+ await handle.close();
275
+ throw err;
276
+ }
277
+ }
278
+ function alreadyExists(finalPath, cause) {
279
+ return new PgMcpError({
280
+ code: 'PATH_DENIED',
281
+ message: `"${finalPath}" already exists`,
282
+ hint: 'pass overwrite: true to replace it, or choose a different destination path',
283
+ ...(cause === undefined ? {} : { cause }),
284
+ });
285
+ }
286
+ /**
287
+ * Publishes the finished partial file under `finalPath`.
288
+ *
289
+ * `overwrite: false` links-then-unlinks rather than renaming: `link` fails
290
+ * `EEXIST` when the destination appeared *after* the up-front `access` check,
291
+ * so the refusal is atomic with the publish instead of racing it (`rename`
292
+ * silently replaces whatever is there). `overwrite: true` renames, which is
293
+ * the atomic replace it asked for.
294
+ *
295
+ * ponytail: a filesystem with no hard links (some network shares / bind
296
+ * mounts) falls back to `rename`, which restores the old check-then-act race.
297
+ * Acceptable because the refusal is a courtesy against clobbering, not a
298
+ * security boundary — containment is `resolveWorkspacePath`'s job. Drop the
299
+ * fallback if a target filesystem ever makes the race reachable in practice.
300
+ */
301
+ async function publish(partialPath, finalPath, overwrite) {
302
+ if (overwrite) {
303
+ await rename(partialPath, finalPath);
304
+ return;
305
+ }
306
+ try {
307
+ await link(partialPath, finalPath);
308
+ }
309
+ catch (err) {
310
+ if (hasErrnoCode(err, 'EEXIST')) {
311
+ await rm(partialPath, { force: true });
312
+ throw alreadyExists(finalPath, err);
313
+ }
314
+ await rename(partialPath, finalPath);
315
+ return;
316
+ }
317
+ await unlink(partialPath);
318
+ }
319
+ /**
320
+ * Writes to `<finalPath>.partial`, handing `write` an open destination
321
+ * stream. On success the partial file is published as `finalPath`; on any
322
+ * failure (from `write` itself, or a stream error it lets escape) the
323
+ * partial file is removed and `finalPath` is never created or touched — so a
324
+ * killed process leaves at most a `.partial` file lying around, never
325
+ * something under the real name that looks like a complete artifact.
326
+ * Shared by `export`, `copy`, and later `backup`/`schema_snapshot`.
327
+ *
328
+ * **`wx`, not `w`.** `.partial` is a fully predictable name that
329
+ * `resolveWorkspacePath` never sees, and `w` follows a symlink: a symlink
330
+ * pre-planted there sends the export's bytes straight through to wherever it
331
+ * points, outside the allow-list that was just checked. The same flag also let
332
+ * two concurrent exports to one path share a single file — interleaving their
333
+ * bytes while both reported success and a byte count. `wx` refuses an existing
334
+ * entry of any kind, which closes both at once.
335
+ */
336
+ export async function writeFileAtomic(finalPath, write, opts = {}) {
337
+ const overwrite = opts.overwrite === true;
338
+ if (!overwrite) {
339
+ const exists = await access(finalPath).then(() => true, () => false);
340
+ // Cheap early refusal so a long export is not run for nothing; `publish`
341
+ // below is what makes the refusal race-free.
342
+ if (exists)
343
+ throw alreadyExists(finalPath);
344
+ }
345
+ const partialPath = `${finalPath}${PARTIAL_SUFFIX}`;
346
+ const dest = createWriteStream(partialPath, { flags: 'wx' });
347
+ // A write already in flight when we destroy the stream lands afterwards and
348
+ // emits ERR_STREAM_DESTROYED; with no listener Node turns that into an
349
+ // uncaughtException and kills the server. The error that matters is the one
350
+ // being thrown out of here, so this secondary one is absorbed.
351
+ dest.on('error', () => undefined);
352
+ try {
353
+ // `createWriteStream` opens asynchronously, so an `EEXIST` from `wx`
354
+ // arrives as an 'error' event, not a throw — wait for the open here rather
355
+ // than letting it surface later as an opaque stream failure.
356
+ await once(dest, 'open');
357
+ }
358
+ catch (err) {
359
+ dest.destroy();
360
+ // Deliberately does NOT remove partialPath: it belongs to another writer,
361
+ // or it is the planted symlink this flag exists to refuse.
362
+ if (hasErrnoCode(err, 'EEXIST')) {
363
+ throw new PgMcpError({
364
+ code: 'PATH_DENIED',
365
+ message: `"${partialPath}" already exists, so a write to "${finalPath}" is already in progress`,
366
+ hint: `wait for it to finish; if nothing is running, a killed process left it behind — delete ${partialPath} and re-run`,
367
+ cause: err,
368
+ });
369
+ }
370
+ throw err;
371
+ }
372
+ try {
373
+ await write(dest);
374
+ // The fd must be closed before the file is published: on Windows a rename
375
+ // or link of a file with an open handle fails EPERM. `stream.pipeline`
376
+ // (what every shipped caller uses) already ends the stream, so this is a
377
+ // no-op there — but a `write` that returns without ending it must not be
378
+ // left to corrupt the result.
379
+ if (!dest.writableEnded)
380
+ dest.end();
381
+ await finished(dest);
382
+ }
383
+ catch (err) {
384
+ dest.destroy();
385
+ await rm(partialPath, { force: true });
386
+ throw err;
387
+ }
388
+ const bytes = dest.bytesWritten;
389
+ await publish(partialPath, finalPath, overwrite);
390
+ return bytes;
391
+ }
392
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"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"}