pg-admin-mcp 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +409 -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 +48 -6
|
@@ -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":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,kBAAkB,CAAC;AAC3G,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC,SAAS,YAAY,CAAC,GAAY,EAAE,IAAY;IAC9C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,MAAM,IAAI,GAAG,IAAK,GAA0B,CAAC,IAAI,KAAK,IAAI,CAAC;AAC/G,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,OAAO,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AACrC,CAAC;AAED,yIAAyI;AACzI,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAW;IAClD,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,uBAAuB;YAC7D,IAAI,EAAE,wFAAwF;SAC/F,CAAC,CAAC;IACL,CAAC;IACD,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,IAAI,GAAG,gCAAgC;YAChD,IAAI,EAAE,gHAAgH;SACvH,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,sBAAsB,CAAC,CAAS;IAC7C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,GAAG,CAAC;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC/B,IAAI,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC,CAAC,uDAAuD;QACnF,MAAM,UAAU,GAAG,MAAM,sBAAsB,CAAC,MAAM,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAe,EAAE,QAAyB;IACrE,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,SAAiB,EAAE,GAAW,EAAE,QAAyB;IAChF,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC3F,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/E,IAAI,aAAa,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAOD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAa,EACb,WAA8B,EAC9B,OAAoC,EAAE;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAEnD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,IAAI,KAAK,mDAAmD;YACrE,IAAI,EAAE,qGAAqG;SAC5G,CAAC,CAAC;IACL,CAAC;IAED,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAEhC,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAW,CAAC,CAAC,0BAA0B;IACnE,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC/F,wBAAwB,CAAC,SAAS,CAAC,CAAC;IAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAE1C,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,UAAU,GAAG,MAAM,sBAAsB,CAAC,SAAS,CAAC,CAAC;IACvD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,IAAI,KAAK,4BAA4B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;YAChG,IAAI,EAAE,qFAAqF;YAC3F,KAAK,EAAE,GAAG;SACX,CAAC,CAAC;IACL,CAAC;IAED,MAAM,mBAAmB,GAAG,MAAM,OAAO,CAAC,GAAG,CAC3C,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CACnG,CAAC;IAEF,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,eAAe,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;IACtG,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,yEAAyE;QACzE,uEAAuE;QACvE,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,MAAM,kBAAkB,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;QACvG,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,kBAAkB;gBACzB,CAAC,CAAC,IAAI,KAAK,yDAAyD;gBACpE,CAAC,CAAC,IAAI,KAAK,iBAAiB,QAAQ,4CAA4C;YAClF,IAAI,EAAE,kBAAkB;gBACtB,CAAC,CAAC,gFAAgF;gBAClF,CAAC,CAAC,4BAA4B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YACxD,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,GAAG,WAAW,CAAC,EAAE;SAClD,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,YAAoB,EACpB,WAA8B,EAC9B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE,4BAA4B,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAErE,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC;IAC9D,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACnE,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IAEhC,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACxD,OAAO,GAAG,eAAe,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/D,CAAC;AAmBD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,KAAa,EAAE,IAA8B;IAC1F,OAAO,IAAI,UAAU,CAAC;QACpB,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,IAAI,QAAQ,QAAQ,KAAK,oBAAoB,IAAI,CAAC,QAAQ,SAAS,IAAI,CAAC,UAAU,QAAQ;QACnG,IAAI,EAAE,IAAI,CAAC,SAAS;QACpB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE;KAC5D,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,KAAa,EACb,WAA8B,EAC9B,IAA8B;IAE9B,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IAEjE,IAAI,MAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,aAAa;YACnB,OAAO,EAAE,IAAI,KAAK,oCAAoC;YACtD,IAAI,EAAE,IAAI,CAAC,WAAW;YACtB,OAAO,EAAE,EAAE,KAAK,EAAE;YAClB,KAAK,EAAE,GAAG;SACX,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACnB,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,IAAI,KAAK,yBAAyB;gBAC3C,IAAI,EAAE,IAAI,CAAC,WAAW;gBACtB,OAAO,EAAE,EAAE,KAAK,EAAE;aACnB,CAAC,CAAC;QACL,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,oBAAoB,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;QACxF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,CAAC;QACxC,IAAI,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,IAAI,KAAK,uFAAuF;gBACzG,IAAI,EAAE,kDAAkD;gBACxD,OAAO,EAAE,EAAE,KAAK,EAAE;aACnB,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ;YAAE,MAAM,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAEjF,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAOD,SAAS,aAAa,CAAC,SAAiB,EAAE,KAAe;IACvD,OAAO,IAAI,UAAU,CAAC;QACpB,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,IAAI,SAAS,kBAAkB;QACxC,IAAI,EAAE,4EAA4E;QAClF,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;KAC1C,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,KAAK,UAAU,OAAO,CAAC,WAAmB,EAAE,SAAiB,EAAE,SAAkB;IAC/E,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,MAAM,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IACD,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,SAAiB,EACjB,KAAqD,EACrD,OAA2B,EAAE;IAE7B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC;IAC1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CACzC,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;QACF,yEAAyE;QACzE,6CAA6C;QAC7C,IAAI,MAAM;YAAE,MAAM,aAAa,CAAC,SAAS,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,WAAW,GAAG,GAAG,SAAS,GAAG,cAAc,EAAE,CAAC;IACpD,MAAM,IAAI,GAAG,iBAAiB,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,4EAA4E;IAC5E,uEAAuE;IACvE,4EAA4E;IAC5E,+DAA+D;IAC/D,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC;QACH,qEAAqE;QACrE,2EAA2E;QAC3E,6DAA6D;QAC7D,MAAM,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,0EAA0E;QAC1E,2DAA2D;QAC3D,IAAI,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,IAAI,WAAW,oCAAoC,SAAS,0BAA0B;gBAC/F,IAAI,EAAE,0FAA0F,WAAW,aAAa;gBACxH,KAAK,EAAE,GAAG;aACX,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;QAClB,0EAA0E;QAC1E,uEAAuE;QACvE,yEAAyE;QACzE,yEAAyE;QACzE,8BAA8B;QAC9B,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,IAAI,CAAC,GAAG,EAAE,CAAC;QACpC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC;IAChC,MAAM,OAAO,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACjD,OAAO,KAAK,CAAC;AACf,CAAC"}
|