@negusdev/dbask 0.0.0-stage → 0.1.8
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/CHANGELOG.md +40 -0
- package/LICENSE +15 -0
- package/README.md +422 -2
- package/dist/ai/factory.d.ts +4 -0
- package/dist/ai/factory.d.ts.map +1 -0
- package/dist/ai/factory.js +55 -0
- package/dist/ai/factory.js.map +1 -0
- package/dist/ai/provider.d.ts +13 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +2 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/ai/sdk-provider.d.ts +13 -0
- package/dist/ai/sdk-provider.d.ts.map +1 -0
- package/dist/ai/sdk-provider.js +36 -0
- package/dist/ai/sdk-provider.js.map +1 -0
- package/dist/cli-args.d.ts +26 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +72 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-observer.d.ts +23 -0
- package/dist/cli-observer.d.ts.map +1 -0
- package/dist/cli-observer.js +224 -0
- package/dist/cli-observer.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +127 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +247 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/validate.d.ts +31 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +205 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config/config.d.ts +42 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +94 -0
- package/dist/config/config.js.map +1 -0
- package/dist/context/db-context.d.ts +12 -0
- package/dist/context/db-context.d.ts.map +1 -0
- package/dist/context/db-context.js +29 -0
- package/dist/context/db-context.js.map +1 -0
- package/dist/core/agent/agent.d.ts +57 -0
- package/dist/core/agent/agent.d.ts.map +1 -0
- package/dist/core/agent/agent.js +518 -0
- package/dist/core/agent/agent.js.map +1 -0
- package/dist/core/agent/index.d.ts +4 -0
- package/dist/core/agent/index.d.ts.map +1 -0
- package/dist/core/agent/index.js +2 -0
- package/dist/core/agent/index.js.map +1 -0
- package/dist/core/agent/observability.d.ts +19 -0
- package/dist/core/agent/observability.d.ts.map +1 -0
- package/dist/core/agent/observability.js +4 -0
- package/dist/core/agent/observability.js.map +1 -0
- package/dist/core/agent/query-plan.d.ts +68 -0
- package/dist/core/agent/query-plan.d.ts.map +1 -0
- package/dist/core/agent/query-plan.js +282 -0
- package/dist/core/agent/query-plan.js.map +1 -0
- package/dist/core/tools/describe-table.d.ts +6 -0
- package/dist/core/tools/describe-table.d.ts.map +1 -0
- package/dist/core/tools/describe-table.js +25 -0
- package/dist/core/tools/describe-table.js.map +1 -0
- package/dist/core/tools/describe-tables.d.ts +6 -0
- package/dist/core/tools/describe-tables.d.ts.map +1 -0
- package/dist/core/tools/describe-tables.js +22 -0
- package/dist/core/tools/describe-tables.js.map +1 -0
- package/dist/core/tools/execute-query.d.ts +6 -0
- package/dist/core/tools/execute-query.d.ts.map +1 -0
- package/dist/core/tools/execute-query.js +31 -0
- package/dist/core/tools/execute-query.js.map +1 -0
- package/dist/core/tools/explain-query.d.ts +10 -0
- package/dist/core/tools/explain-query.d.ts.map +1 -0
- package/dist/core/tools/explain-query.js +34 -0
- package/dist/core/tools/explain-query.js.map +1 -0
- package/dist/core/tools/get-relationships.d.ts +6 -0
- package/dist/core/tools/get-relationships.d.ts.map +1 -0
- package/dist/core/tools/get-relationships.js +23 -0
- package/dist/core/tools/get-relationships.js.map +1 -0
- package/dist/core/tools/index.d.ts +9 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -0
- package/dist/core/tools/list-tables.d.ts +3 -0
- package/dist/core/tools/list-tables.d.ts.map +1 -0
- package/dist/core/tools/list-tables.js +18 -0
- package/dist/core/tools/list-tables.js.map +1 -0
- package/dist/core/tools/registry.d.ts +11 -0
- package/dist/core/tools/registry.d.ts.map +1 -0
- package/dist/core/tools/registry.js +48 -0
- package/dist/core/tools/registry.js.map +1 -0
- package/dist/core/types/index.d.ts +102 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +6 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/database/adapter.d.ts +3 -0
- package/dist/database/adapter.d.ts.map +1 -0
- package/dist/database/adapter.js +2 -0
- package/dist/database/adapter.js.map +1 -0
- package/dist/database/factory.d.ts +16 -0
- package/dist/database/factory.d.ts.map +1 -0
- package/dist/database/factory.js +27 -0
- package/dist/database/factory.js.map +1 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +4 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/mysql/adapter.d.ts +26 -0
- package/dist/database/mysql/adapter.d.ts.map +1 -0
- package/dist/database/mysql/adapter.js +131 -0
- package/dist/database/mysql/adapter.js.map +1 -0
- package/dist/database/postgres/adapter.d.ts +60 -0
- package/dist/database/postgres/adapter.d.ts.map +1 -0
- package/dist/database/postgres/adapter.js +307 -0
- package/dist/database/postgres/adapter.js.map +1 -0
- package/dist/database/result-limits.d.ts +17 -0
- package/dist/database/result-limits.d.ts.map +1 -0
- package/dist/database/result-limits.js +26 -0
- package/dist/database/result-limits.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/safety/limits.d.ts +7 -0
- package/dist/safety/limits.d.ts.map +1 -0
- package/dist/safety/limits.js +15 -0
- package/dist/safety/limits.js.map +1 -0
- package/dist/safety/policy.d.ts +8 -0
- package/dist/safety/policy.d.ts.map +1 -0
- package/dist/safety/policy.js +11 -0
- package/dist/safety/policy.js.map +1 -0
- package/dist/safety/sql-tokenizer.d.ts +49 -0
- package/dist/safety/sql-tokenizer.d.ts.map +1 -0
- package/dist/safety/sql-tokenizer.js +240 -0
- package/dist/safety/sql-tokenizer.js.map +1 -0
- package/dist/safety/sql-validator.d.ts +34 -0
- package/dist/safety/sql-validator.d.ts.map +1 -0
- package/dist/safety/sql-validator.js +270 -0
- package/dist/safety/sql-validator.js.map +1 -0
- package/dist/schema/cache.d.ts +24 -0
- package/dist/schema/cache.d.ts.map +1 -0
- package/dist/schema/cache.js +57 -0
- package/dist/schema/cache.js.map +1 -0
- package/dist/ui/markdown.d.ts +8 -0
- package/dist/ui/markdown.d.ts.map +1 -0
- package/dist/ui/markdown.js +205 -0
- package/dist/ui/markdown.js.map +1 -0
- package/dist/ui/spinner.d.ts +32 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +74 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/sql-highlight.d.ts +4 -0
- package/dist/ui/sql-highlight.d.ts.map +1 -0
- package/dist/ui/sql-highlight.js +28 -0
- package/dist/ui/sql-highlight.js.map +1 -0
- package/dist/ui/style.d.ts +38 -0
- package/dist/ui/style.d.ts.map +1 -0
- package/dist/ui/style.js +69 -0
- package/dist/ui/style.js.map +1 -0
- package/package.json +91 -4
- package/sql-ai.example.md +32 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SQL tokenizer used by the safety validator. It understands the
|
|
3
|
+
* lexical rules of MySQL/MariaDB and PostgreSQL.
|
|
4
|
+
*
|
|
5
|
+
* It is not a full SQL parser. Its only job is to separate the parts of a
|
|
6
|
+
* statement that carry meaning (keywords, identifiers, punctuation) from the
|
|
7
|
+
* parts that do not (string literals, quoted identifiers, comments), so safety
|
|
8
|
+
* rules are applied to real SQL tokens instead of raw text. Getting the lexical
|
|
9
|
+
* rules of the *target* database exactly right matters: if this tokenizer and
|
|
10
|
+
* the server disagree about where a string or comment ends, a second statement
|
|
11
|
+
* can hide inside what the validator thinks is a literal.
|
|
12
|
+
*
|
|
13
|
+
* MySQL/MariaDB (defaults assumed):
|
|
14
|
+
* - `-- ` and `#` line comments, non-nested block comments
|
|
15
|
+
* - backslash escapes inside '...' and "..." strings (no NO_BACKSLASH_ESCAPES)
|
|
16
|
+
* - double quotes delimit strings, not identifiers (no ANSI_QUOTES)
|
|
17
|
+
* - executable comments (`/*! ... *\/`, `/*M! ... *\/`) are rejected outright
|
|
18
|
+
* because the server executes their contents
|
|
19
|
+
*
|
|
20
|
+
* PostgreSQL:
|
|
21
|
+
* - `--` line comments (no whitespace needed); `#` is an operator, not a comment
|
|
22
|
+
* - nested block comments
|
|
23
|
+
* - '...' strings only escape quotes by doubling; backslash escapes apply only
|
|
24
|
+
* to E'...' strings (the adapter pins standard_conforming_strings = on)
|
|
25
|
+
* - "..." delimits identifiers
|
|
26
|
+
* - dollar-quoted strings ($$...$$ and $tag$...$tag$)
|
|
27
|
+
* - U&'...' and U&"..." are rejected: their escapes can spell any name
|
|
28
|
+
*/
|
|
29
|
+
const WORD_CHAR = /[A-Za-z0-9_$\u0080-]/;
|
|
30
|
+
const DOLLAR_TAG = /\$(?:[A-Za-z_\u0080-][A-Za-z0-9_\u0080-]*)?\$/y;
|
|
31
|
+
/**
|
|
32
|
+
* Tokenizes SQL using the lexical rules of `dialect`. Dialects other than
|
|
33
|
+
* PostgreSQL use the MySQL rules (no other adapter exists yet).
|
|
34
|
+
*/
|
|
35
|
+
export function tokenizeSql(sql, dialect = 'mysql') {
|
|
36
|
+
const pg = dialect === 'postgres';
|
|
37
|
+
const tokens = [];
|
|
38
|
+
const n = sql.length;
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < n) {
|
|
41
|
+
const c = sql[i];
|
|
42
|
+
const next = sql[i + 1];
|
|
43
|
+
// Whitespace.
|
|
44
|
+
if (/\s/.test(c)) {
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// `--` line comment. PostgreSQL always treats `--` as a comment. MySQL
|
|
49
|
+
// requires whitespace or a control character after the two dashes; `--x`
|
|
50
|
+
// is two minus signs there, not a comment.
|
|
51
|
+
if (c === '-' && next === '-') {
|
|
52
|
+
const after = sql[i + 2];
|
|
53
|
+
if (pg || after === undefined || /[\s\u0000-\u001f]/.test(after)) {
|
|
54
|
+
const eol = sql.indexOf('\n', i + 2);
|
|
55
|
+
i = eol === -1 ? n : eol + 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// `#` line comment (MySQL only; in PostgreSQL `#` starts operators like #>).
|
|
60
|
+
if (c === '#' && !pg) {
|
|
61
|
+
const eol = sql.indexOf('\n', i + 1);
|
|
62
|
+
i = eol === -1 ? n : eol + 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Block comment.
|
|
66
|
+
if (c === '/' && next === '*') {
|
|
67
|
+
if (!pg && (sql[i + 2] === '!' || (sql[i + 2] === 'M' && sql[i + 3] === '!'))) {
|
|
68
|
+
return {
|
|
69
|
+
tokens,
|
|
70
|
+
error: 'Executable comments (/*! ... */) are not allowed.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (pg) {
|
|
74
|
+
// PostgreSQL block comments nest.
|
|
75
|
+
let depth = 1;
|
|
76
|
+
let j = i + 2;
|
|
77
|
+
while (j < n && depth > 0) {
|
|
78
|
+
if (sql[j] === '/' && sql[j + 1] === '*') {
|
|
79
|
+
depth += 1;
|
|
80
|
+
j += 2;
|
|
81
|
+
}
|
|
82
|
+
else if (sql[j] === '*' && sql[j + 1] === '/') {
|
|
83
|
+
depth -= 1;
|
|
84
|
+
j += 2;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
j += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (depth > 0) {
|
|
91
|
+
return { tokens, error: 'The query contains an unterminated comment.' };
|
|
92
|
+
}
|
|
93
|
+
i = j;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const close = sql.indexOf('*/', i + 2);
|
|
97
|
+
if (close === -1) {
|
|
98
|
+
return { tokens, error: 'The query contains an unterminated comment.' };
|
|
99
|
+
}
|
|
100
|
+
i = close + 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// PostgreSQL dollar-quoted strings: $$...$$ or $tag$...$tag$.
|
|
104
|
+
// `$1` is a parameter placeholder and falls through to the word rule.
|
|
105
|
+
if (pg && c === '$') {
|
|
106
|
+
DOLLAR_TAG.lastIndex = i;
|
|
107
|
+
const match = DOLLAR_TAG.exec(sql);
|
|
108
|
+
if (match) {
|
|
109
|
+
const tag = match[0];
|
|
110
|
+
const close = sql.indexOf(tag, i + tag.length);
|
|
111
|
+
if (close === -1) {
|
|
112
|
+
return { tokens, error: 'The query contains an unterminated dollar-quoted string.' };
|
|
113
|
+
}
|
|
114
|
+
const end = close + tag.length;
|
|
115
|
+
tokens.push({ type: 'string', value: sql.slice(i, end), start: i, end });
|
|
116
|
+
i = end;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Quoted strings ('...', and "..." in MySQL) and, in PostgreSQL,
|
|
121
|
+
// double-quoted identifiers.
|
|
122
|
+
if (c === "'" || c === '"') {
|
|
123
|
+
const start = i;
|
|
124
|
+
if (pg) {
|
|
125
|
+
const previous = tokens[tokens.length - 1];
|
|
126
|
+
const beforePrevious = tokens[tokens.length - 2];
|
|
127
|
+
// U&'...' / U&"..." (Unicode escapes can spell any identifier).
|
|
128
|
+
if (previous?.type === 'punct' &&
|
|
129
|
+
previous.value === '&' &&
|
|
130
|
+
previous.end === i &&
|
|
131
|
+
beforePrevious?.type === 'word' &&
|
|
132
|
+
beforePrevious.value === 'U' &&
|
|
133
|
+
beforePrevious.end === previous.start) {
|
|
134
|
+
return {
|
|
135
|
+
tokens,
|
|
136
|
+
error: 'Unicode escape strings and identifiers (U&"...") are not allowed.',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// E'...' strings treat backslash as an escape character.
|
|
141
|
+
let backslashEscapes = !pg;
|
|
142
|
+
let tokenStart = start;
|
|
143
|
+
if (pg && c === "'") {
|
|
144
|
+
const previous = tokens[tokens.length - 1];
|
|
145
|
+
if (previous?.type === 'word' && previous.value === 'E' && previous.end === i) {
|
|
146
|
+
backslashEscapes = true;
|
|
147
|
+
tokenStart = previous.start;
|
|
148
|
+
tokens.pop();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let j = i + 1;
|
|
152
|
+
let closed = false;
|
|
153
|
+
while (j < n) {
|
|
154
|
+
const d = sql[j];
|
|
155
|
+
if (backslashEscapes && d === '\\') {
|
|
156
|
+
j += 2;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (d === c) {
|
|
160
|
+
if (sql[j + 1] === c) {
|
|
161
|
+
j += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
closed = true;
|
|
165
|
+
j += 1;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
j += 1;
|
|
169
|
+
}
|
|
170
|
+
if (!closed) {
|
|
171
|
+
return {
|
|
172
|
+
tokens,
|
|
173
|
+
error: pg && c === '"'
|
|
174
|
+
? 'The query contains an unterminated quoted identifier.'
|
|
175
|
+
: 'The query contains an unterminated string.',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
tokens.push({
|
|
179
|
+
type: pg && c === '"' ? 'quoted-identifier' : 'string',
|
|
180
|
+
value: sql.slice(tokenStart, j),
|
|
181
|
+
start: tokenStart,
|
|
182
|
+
end: j,
|
|
183
|
+
});
|
|
184
|
+
i = j;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Backtick-quoted identifiers (MySQL only).
|
|
188
|
+
if (c === '`' && !pg) {
|
|
189
|
+
const start = i;
|
|
190
|
+
let j = i + 1;
|
|
191
|
+
let closed = false;
|
|
192
|
+
while (j < n) {
|
|
193
|
+
if (sql[j] === '`') {
|
|
194
|
+
if (sql[j + 1] === '`') {
|
|
195
|
+
j += 2;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
closed = true;
|
|
199
|
+
j += 1;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
j += 1;
|
|
203
|
+
}
|
|
204
|
+
if (!closed) {
|
|
205
|
+
return {
|
|
206
|
+
tokens,
|
|
207
|
+
error: 'The query contains an unterminated quoted identifier.',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
tokens.push({
|
|
211
|
+
type: 'quoted-identifier',
|
|
212
|
+
value: sql.slice(start, j),
|
|
213
|
+
start,
|
|
214
|
+
end: j,
|
|
215
|
+
});
|
|
216
|
+
i = j;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// Words: keywords, identifiers, and numbers.
|
|
220
|
+
if (WORD_CHAR.test(c)) {
|
|
221
|
+
const start = i;
|
|
222
|
+
let j = i + 1;
|
|
223
|
+
while (j < n && WORD_CHAR.test(sql[j]))
|
|
224
|
+
j += 1;
|
|
225
|
+
tokens.push({
|
|
226
|
+
type: 'word',
|
|
227
|
+
value: sql.slice(start, j).toUpperCase(),
|
|
228
|
+
start,
|
|
229
|
+
end: j,
|
|
230
|
+
});
|
|
231
|
+
i = j;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Everything else is single-character punctuation.
|
|
235
|
+
tokens.push({ type: 'punct', value: c, start: i, end: i + 1 });
|
|
236
|
+
i += 1;
|
|
237
|
+
}
|
|
238
|
+
return { tokens };
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=sql-tokenizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-tokenizer.js","sourceRoot":"","sources":["../../src/safety/sql-tokenizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAqBH,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAC1C,MAAM,UAAU,GAAG,kDAAkD,CAAC;AAEtE;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,OAAO,GAAe,OAAO;IACpE,MAAM,EAAE,GAAG,OAAO,KAAK,UAAU,CAAC;IAClC,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC;IACrB,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAW,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAExB,cAAc;QACd,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACjB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,uEAAuE;QACvE,yEAAyE;QACzE,2CAA2C;QAC3C,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACzB,IAAI,EAAE,IAAI,KAAK,KAAK,SAAS,IAAI,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACjE,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;gBACrC,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;gBAC7B,SAAS;YACX,CAAC;QACH,CAAC;QAED,6EAA6E;QAC7E,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;YAC7B,SAAS;QACX,CAAC;QAED,iBAAiB;QACjB,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YAC9B,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC9E,OAAO;oBACL,MAAM;oBACN,KAAK,EAAE,mDAAmD;iBAC3D,CAAC;YACJ,CAAC;YAED,IAAI,EAAE,EAAE,CAAC;gBACP,kCAAkC;gBAClC,IAAI,KAAK,GAAG,CAAC,CAAC;gBACd,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAEd,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBAC1B,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;wBACzC,KAAK,IAAI,CAAC,CAAC;wBACX,CAAC,IAAI,CAAC,CAAC;oBACT,CAAC;yBAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;wBAChD,KAAK,IAAI,CAAC,CAAC;wBACX,CAAC,IAAI,CAAC,CAAC;oBACT,CAAC;yBAAM,CAAC;wBACN,CAAC,IAAI,CAAC,CAAC;oBACT,CAAC;gBACH,CAAC;gBAED,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBACd,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,6CAA6C,EAAE,CAAC;gBAC1E,CAAC;gBAED,CAAC,GAAG,CAAC,CAAC;gBACN,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACvC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,6CAA6C,EAAE,CAAC;YAC1E,CAAC;YAED,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;YACd,SAAS;QACX,CAAC;QAED,8DAA8D;QAC9D,sEAAsE;QACtE,IAAI,EAAE,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACpB,UAAU,CAAC,SAAS,GAAG,CAAC,CAAC;YACzB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAEnC,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBACrB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;gBAE/C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;oBACjB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,0DAA0D,EAAE,CAAC;gBACvF,CAAC;gBAED,MAAM,GAAG,GAAG,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC;gBAC/B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;gBACzE,CAAC,GAAG,GAAG,CAAC;gBACR,SAAS;YACX,CAAC;QACH,CAAC;QAED,iEAAiE;QACjE,6BAA6B;QAC7B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,CAAC,CAAC;YAEhB,IAAI,EAAE,EAAE,CAAC;gBACP,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAC3C,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAEjD,gEAAgE;gBAChE,IACE,QAAQ,EAAE,IAAI,KAAK,OAAO;oBAC1B,QAAQ,CAAC,KAAK,KAAK,GAAG;oBACtB,QAAQ,CAAC,GAAG,KAAK,CAAC;oBAClB,cAAc,EAAE,IAAI,KAAK,MAAM;oBAC/B,cAAc,CAAC,KAAK,KAAK,GAAG;oBAC5B,cAAc,CAAC,GAAG,KAAK,QAAQ,CAAC,KAAK,EACrC,CAAC;oBACD,OAAO;wBACL,MAAM;wBACN,KAAK,EAAE,mEAAmE;qBAC3E,CAAC;gBACJ,CAAC;YACH,CAAC;YAED,yDAAyD;YACzD,IAAI,gBAAgB,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,UAAU,GAAG,KAAK,CAAC;YAEvB,IAAI,EAAE,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;gBACpB,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAC3C,IAAI,QAAQ,EAAE,IAAI,KAAK,MAAM,IAAI,QAAQ,CAAC,KAAK,KAAK,GAAG,IAAI,QAAQ,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC;oBAC9E,gBAAgB,GAAG,IAAI,CAAC;oBACxB,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC;oBAC5B,MAAM,CAAC,GAAG,EAAE,CAAC;gBACf,CAAC;YACH,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,IAAI,MAAM,GAAG,KAAK,CAAC;YAEnB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACb,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAW,CAAC;gBAE3B,IAAI,gBAAgB,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;oBACnC,CAAC,IAAI,CAAC,CAAC;oBACP,SAAS;gBACX,CAAC;gBAED,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;oBACZ,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;wBACrB,CAAC,IAAI,CAAC,CAAC;wBACP,SAAS;oBACX,CAAC;oBACD,MAAM,GAAG,IAAI,CAAC;oBACd,CAAC,IAAI,CAAC,CAAC;oBACP,MAAM;gBACR,CAAC;gBAED,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YAED,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,MAAM;oBACN,KAAK,EAAE,EAAE,IAAI,CAAC,KAAK,GAAG;wBACpB,CAAC,CAAC,uDAAuD;wBACzD,CAAC,CAAC,4CAA4C;iBACjD,CAAC;YACJ,CAAC;YAED,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,QAAQ;gBACtD,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;gBAC/B,KAAK,EAAE,UAAU;gBACjB,GAAG,EAAE,CAAC;aACP,CAAC,CAAC;YACH,CAAC,GAAG,CAAC,CAAC;YACN,SAAS;QACX,CAAC;QAED,4CAA4C;QAC5C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,CAAC,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,IAAI,MAAM,GAAG,KAAK,CAAC;YAEnB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACb,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBACnB,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;wBACvB,CAAC,IAAI,CAAC,CAAC;wBACP,SAAS;oBACX,CAAC;oBACD,MAAM,GAAG,IAAI,CAAC;oBACd,CAAC,IAAI,CAAC,CAAC;oBACP,MAAM;gBACR,CAAC;gBACD,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;YAED,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,MAAM;oBACN,KAAK,EAAE,uDAAuD;iBAC/D,CAAC;YACJ,CAAC;YAED,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,mBAAmB;gBACzB,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;gBAC1B,KAAK;gBACL,GAAG,EAAE,CAAC;aACP,CAAC,CAAC;YACH,CAAC,GAAG,CAAC,CAAC;YACN,SAAS;QACX,CAAC;QAED,6CAA6C;QAC7C,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,CAAC,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAW,CAAC;gBAAE,CAAC,IAAI,CAAC,CAAC;YAEzD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,MAAM;gBACZ,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE;gBACxC,KAAK;gBACL,GAAG,EAAE,CAAC;aACP,CAAC,CAAC;YACH,CAAC,GAAG,CAAC,CAAC;YACN,SAAS;QACX,CAAC;QAED,mDAAmD;QACnD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/D,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SqlDialect, SqlSafetyPolicy } from '../core/types/index.js';
|
|
2
|
+
export interface SqlValidationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
sql?: string;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* First-stage SQL safety boundary.
|
|
9
|
+
*
|
|
10
|
+
* The SQL is tokenized with the target dialect's lexical rules so string
|
|
11
|
+
* literals, quoted identifiers, and comments are never mistaken for SQL, then
|
|
12
|
+
* these rules are applied:
|
|
13
|
+
*
|
|
14
|
+
* - exactly one statement (a single trailing semicolon is accepted)
|
|
15
|
+
* - the statement must start with a read-oriented keyword
|
|
16
|
+
* - executable comments (MySQL) and Unicode-escaped names (PostgreSQL) are
|
|
17
|
+
* rejected
|
|
18
|
+
* - when `policy.readOnly` is set: write/DDL/locking keywords, `INTO`, and
|
|
19
|
+
* locking reads (`FOR SHARE` / `FOR UPDATE`, ...) are rejected
|
|
20
|
+
* - functions that read files, block the connection, take locks, or run
|
|
21
|
+
* dynamic SQL are rejected wherever they appear, including schema-qualified
|
|
22
|
+
* (`pg_catalog.pg_sleep(...)`) and quoted (`"pg_sleep"(...)`) forms
|
|
23
|
+
*
|
|
24
|
+
* This is not a complete SQL parser. MySQL rules assume the default sql_mode
|
|
25
|
+
* (backslash escapes on, ANSI_QUOTES off); PostgreSQL rules assume
|
|
26
|
+
* standard_conforming_strings = on, which the PostgreSQL adapter enforces.
|
|
27
|
+
* User-defined functions cannot be recognized as side-effecting. The database
|
|
28
|
+
* account must still be read-only; the PostgreSQL adapter additionally runs
|
|
29
|
+
* every statement in a READ ONLY transaction.
|
|
30
|
+
*
|
|
31
|
+
* @param dialect SQL dialect of the target database. Defaults to MySQL.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateSql(sql: string, policy: SqlSafetyPolicy, dialect?: SqlDialect): SqlValidationResult;
|
|
34
|
+
//# sourceMappingURL=sql-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-validator.d.ts","sourceRoot":"","sources":["../../src/safety/sql-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA4J1E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,UAAoB,GAC5B,mBAAmB,CAgHrB"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { tokenizeSql } from './sql-tokenizer.js';
|
|
2
|
+
const MYSQL_RULES = {
|
|
3
|
+
allowedFirstKeywords: new Set(['SELECT', 'WITH', 'EXPLAIN', 'SHOW', 'DESCRIBE', 'DESC']),
|
|
4
|
+
blockedKeywords: new Set([
|
|
5
|
+
'INSERT',
|
|
6
|
+
'UPDATE',
|
|
7
|
+
'DELETE',
|
|
8
|
+
'DROP',
|
|
9
|
+
'ALTER',
|
|
10
|
+
'TRUNCATE',
|
|
11
|
+
'CREATE',
|
|
12
|
+
'REPLACE',
|
|
13
|
+
'MERGE',
|
|
14
|
+
'GRANT',
|
|
15
|
+
'REVOKE',
|
|
16
|
+
'CALL',
|
|
17
|
+
'SET',
|
|
18
|
+
'USE',
|
|
19
|
+
'LOAD',
|
|
20
|
+
'HANDLER',
|
|
21
|
+
'LOCK',
|
|
22
|
+
'UNLOCK',
|
|
23
|
+
'PREPARE',
|
|
24
|
+
'EXECUTE',
|
|
25
|
+
'DEALLOCATE',
|
|
26
|
+
'RENAME',
|
|
27
|
+
'KILL',
|
|
28
|
+
'FLUSH',
|
|
29
|
+
// Writes query output to the server filesystem or into variables.
|
|
30
|
+
'INTO',
|
|
31
|
+
]),
|
|
32
|
+
// INSERT(), REPLACE() and TRUNCATE() are also ordinary string/math functions.
|
|
33
|
+
functionLikeKeywords: new Set(['INSERT', 'REPLACE', 'TRUNCATE']),
|
|
34
|
+
// Functions that read server files, block, take locks, or are commonly used
|
|
35
|
+
// to tie up a connection.
|
|
36
|
+
blockedFunctions: new Set([
|
|
37
|
+
'LOAD_FILE',
|
|
38
|
+
'SLEEP',
|
|
39
|
+
'BENCHMARK',
|
|
40
|
+
'GET_LOCK',
|
|
41
|
+
'RELEASE_LOCK',
|
|
42
|
+
'RELEASE_ALL_LOCKS',
|
|
43
|
+
'IS_FREE_LOCK',
|
|
44
|
+
'IS_USED_LOCK',
|
|
45
|
+
'MASTER_POS_WAIT',
|
|
46
|
+
'SOURCE_POS_WAIT',
|
|
47
|
+
'WAIT_FOR_EXECUTED_GTID_SET',
|
|
48
|
+
'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS',
|
|
49
|
+
]),
|
|
50
|
+
blockedFunctionPrefixes: [],
|
|
51
|
+
lockingWords: new Set(['SHARE', 'UPDATE']),
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* PostgreSQL. Only words that can occur *inside* a single SELECT/WITH/EXPLAIN/
|
|
55
|
+
* SHOW statement matter here: the statement-starting rule already rejects
|
|
56
|
+
* everything else (COPY, DO, VACUUM, LOCK, ...). Words that are common column
|
|
57
|
+
* names (LOAD, LOCK, USE, KILL, ...) are deliberately not blocked.
|
|
58
|
+
*/
|
|
59
|
+
const POSTGRES_RULES = {
|
|
60
|
+
allowedFirstKeywords: new Set(['SELECT', 'WITH', 'EXPLAIN', 'SHOW']),
|
|
61
|
+
blockedKeywords: new Set([
|
|
62
|
+
// Data-modifying CTEs and statements nested in EXPLAIN.
|
|
63
|
+
'INSERT',
|
|
64
|
+
'UPDATE',
|
|
65
|
+
'DELETE',
|
|
66
|
+
'MERGE',
|
|
67
|
+
'CREATE',
|
|
68
|
+
'DROP',
|
|
69
|
+
'ALTER',
|
|
70
|
+
'TRUNCATE',
|
|
71
|
+
'GRANT',
|
|
72
|
+
'REVOKE',
|
|
73
|
+
'CALL',
|
|
74
|
+
'SET',
|
|
75
|
+
'COPY',
|
|
76
|
+
'DO',
|
|
77
|
+
'PREPARE',
|
|
78
|
+
'EXECUTE',
|
|
79
|
+
'DEALLOCATE',
|
|
80
|
+
// SELECT ... INTO creates a table.
|
|
81
|
+
'INTO',
|
|
82
|
+
]),
|
|
83
|
+
functionLikeKeywords: new Set(),
|
|
84
|
+
blockedFunctions: new Set([
|
|
85
|
+
// Block the connection or change the session's safety settings.
|
|
86
|
+
'PG_SLEEP',
|
|
87
|
+
'PG_SLEEP_FOR',
|
|
88
|
+
'PG_SLEEP_UNTIL',
|
|
89
|
+
'SET_CONFIG',
|
|
90
|
+
// Sequences are state.
|
|
91
|
+
'NEXTVAL',
|
|
92
|
+
'SETVAL',
|
|
93
|
+
// Notifications and backend control.
|
|
94
|
+
'PG_NOTIFY',
|
|
95
|
+
'PG_TERMINATE_BACKEND',
|
|
96
|
+
'PG_CANCEL_BACKEND',
|
|
97
|
+
'PG_RELOAD_CONF',
|
|
98
|
+
'PG_ROTATE_LOGFILE',
|
|
99
|
+
'PG_SWITCH_WAL',
|
|
100
|
+
'PG_PROMOTE',
|
|
101
|
+
'PG_IMPORT_SYSTEM_COLLATIONS',
|
|
102
|
+
// Read server files.
|
|
103
|
+
'PG_STAT_FILE',
|
|
104
|
+
// Server-side dynamic SQL: runs query text that the validator never sees.
|
|
105
|
+
'QUERY_TO_XML',
|
|
106
|
+
'QUERY_TO_XML_AND_XMLSCHEMA',
|
|
107
|
+
'QUERY_TO_XMLSCHEMA',
|
|
108
|
+
'CURSOR_TO_XML',
|
|
109
|
+
'CURSOR_TO_XMLSCHEMA',
|
|
110
|
+
]),
|
|
111
|
+
blockedFunctionPrefixes: [
|
|
112
|
+
'PG_LS_', // pg_ls_dir, pg_ls_logdir, ...
|
|
113
|
+
'PG_READ_', // pg_read_file, pg_read_binary_file
|
|
114
|
+
'PG_FILE_',
|
|
115
|
+
'PG_ADVISORY_', // pg_advisory_lock*, pg_advisory_unlock*, pg_advisory_xact_lock*
|
|
116
|
+
'PG_TRY_ADVISORY_',
|
|
117
|
+
'PG_CREATE_', // replication slots, restore points
|
|
118
|
+
'PG_DROP_',
|
|
119
|
+
'PG_REPLICATION_',
|
|
120
|
+
'PG_LOGICAL_',
|
|
121
|
+
'PG_COPY_',
|
|
122
|
+
'PG_BACKUP_',
|
|
123
|
+
'PG_WAL_REPLAY_',
|
|
124
|
+
'LO_', // large objects: lo_import / lo_export touch the server filesystem
|
|
125
|
+
'DBLINK', // other databases, arbitrary remote SQL
|
|
126
|
+
],
|
|
127
|
+
lockingWords: new Set(['SHARE', 'UPDATE', 'NO', 'KEY']),
|
|
128
|
+
};
|
|
129
|
+
function rulesFor(dialect) {
|
|
130
|
+
// Dialects without an adapter yet fall back to the MySQL rules.
|
|
131
|
+
return dialect === 'postgres' ? POSTGRES_RULES : MYSQL_RULES;
|
|
132
|
+
}
|
|
133
|
+
function isPunct(token, value) {
|
|
134
|
+
return token?.type === 'punct' && token.value === value;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Upper-cased function name when `token` is a (possibly quoted) identifier,
|
|
138
|
+
* otherwise undefined. Quoted identifiers are unquoted first so that
|
|
139
|
+
* "pg_sleep"(10) cannot hide a blocked function.
|
|
140
|
+
*/
|
|
141
|
+
function identifierName(token) {
|
|
142
|
+
if (token.type === 'word')
|
|
143
|
+
return token.value;
|
|
144
|
+
if (token.type === 'quoted-identifier') {
|
|
145
|
+
const quote = token.value[0];
|
|
146
|
+
const inner = token.value.slice(1, -1).split(quote + quote).join(quote);
|
|
147
|
+
return inner.toUpperCase();
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
function isBlockedFunction(name, rules) {
|
|
152
|
+
return (rules.blockedFunctions.has(name) ||
|
|
153
|
+
rules.blockedFunctionPrefixes.some((prefix) => name.startsWith(prefix)));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* First-stage SQL safety boundary.
|
|
157
|
+
*
|
|
158
|
+
* The SQL is tokenized with the target dialect's lexical rules so string
|
|
159
|
+
* literals, quoted identifiers, and comments are never mistaken for SQL, then
|
|
160
|
+
* these rules are applied:
|
|
161
|
+
*
|
|
162
|
+
* - exactly one statement (a single trailing semicolon is accepted)
|
|
163
|
+
* - the statement must start with a read-oriented keyword
|
|
164
|
+
* - executable comments (MySQL) and Unicode-escaped names (PostgreSQL) are
|
|
165
|
+
* rejected
|
|
166
|
+
* - when `policy.readOnly` is set: write/DDL/locking keywords, `INTO`, and
|
|
167
|
+
* locking reads (`FOR SHARE` / `FOR UPDATE`, ...) are rejected
|
|
168
|
+
* - functions that read files, block the connection, take locks, or run
|
|
169
|
+
* dynamic SQL are rejected wherever they appear, including schema-qualified
|
|
170
|
+
* (`pg_catalog.pg_sleep(...)`) and quoted (`"pg_sleep"(...)`) forms
|
|
171
|
+
*
|
|
172
|
+
* This is not a complete SQL parser. MySQL rules assume the default sql_mode
|
|
173
|
+
* (backslash escapes on, ANSI_QUOTES off); PostgreSQL rules assume
|
|
174
|
+
* standard_conforming_strings = on, which the PostgreSQL adapter enforces.
|
|
175
|
+
* User-defined functions cannot be recognized as side-effecting. The database
|
|
176
|
+
* account must still be read-only; the PostgreSQL adapter additionally runs
|
|
177
|
+
* every statement in a READ ONLY transaction.
|
|
178
|
+
*
|
|
179
|
+
* @param dialect SQL dialect of the target database. Defaults to MySQL.
|
|
180
|
+
*/
|
|
181
|
+
export function validateSql(sql, policy, dialect = 'mysql') {
|
|
182
|
+
const rules = rulesFor(dialect);
|
|
183
|
+
const normalized = sql.trim();
|
|
184
|
+
if (!normalized) {
|
|
185
|
+
return { valid: false, reason: 'SQL query is empty.' };
|
|
186
|
+
}
|
|
187
|
+
const { tokens: allTokens, error } = tokenizeSql(normalized, dialect);
|
|
188
|
+
if (error) {
|
|
189
|
+
return { valid: false, reason: error };
|
|
190
|
+
}
|
|
191
|
+
// Split on the first semicolon outside strings, quoted identifiers, and
|
|
192
|
+
// comments. Anything after it, other than comments, is a second statement.
|
|
193
|
+
let tokens = allTokens;
|
|
194
|
+
let output = normalized;
|
|
195
|
+
const semicolon = allTokens.findIndex((token) => isPunct(token, ';'));
|
|
196
|
+
if (semicolon !== -1) {
|
|
197
|
+
if (semicolon < allTokens.length - 1) {
|
|
198
|
+
return { valid: false, reason: 'Multiple SQL statements are not allowed.' };
|
|
199
|
+
}
|
|
200
|
+
tokens = allTokens.slice(0, semicolon);
|
|
201
|
+
output = normalized.slice(0, allTokens[semicolon].start).trimEnd();
|
|
202
|
+
}
|
|
203
|
+
if (tokens.length === 0) {
|
|
204
|
+
return { valid: false, reason: 'SQL query is empty.' };
|
|
205
|
+
}
|
|
206
|
+
// Allow leading parentheses, e.g. (SELECT ...) UNION (SELECT ...).
|
|
207
|
+
let first = 0;
|
|
208
|
+
while (isPunct(tokens[first], '('))
|
|
209
|
+
first += 1;
|
|
210
|
+
const head = tokens[first];
|
|
211
|
+
if (!head ||
|
|
212
|
+
head.type !== 'word' ||
|
|
213
|
+
!rules.allowedFirstKeywords.has(head.value)) {
|
|
214
|
+
return { valid: false, reason: 'Only read-oriented SQL statements are allowed.' };
|
|
215
|
+
}
|
|
216
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
217
|
+
const token = tokens[index];
|
|
218
|
+
const following = tokens[index + 1];
|
|
219
|
+
const isCall = isPunct(following, '(');
|
|
220
|
+
// Blocked functions are matched wherever they appear: a name after a dot
|
|
221
|
+
// (pg_catalog.pg_sleep) or in quotes ("pg_sleep") is still that function.
|
|
222
|
+
if (isCall) {
|
|
223
|
+
const name = identifierName(token);
|
|
224
|
+
if (name !== undefined && isBlockedFunction(name, rules)) {
|
|
225
|
+
return {
|
|
226
|
+
valid: false,
|
|
227
|
+
reason: `The function ${name.toLowerCase()}() is not allowed.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (token.type !== 'word')
|
|
232
|
+
continue;
|
|
233
|
+
// A word after a dot is part of a qualified name (t.update), not a keyword.
|
|
234
|
+
if (isPunct(tokens[index - 1], '.'))
|
|
235
|
+
continue;
|
|
236
|
+
if (!policy.readOnly)
|
|
237
|
+
continue;
|
|
238
|
+
if (rules.blockedKeywords.has(token.value)) {
|
|
239
|
+
if (rules.functionLikeKeywords.has(token.value) && isCall)
|
|
240
|
+
continue;
|
|
241
|
+
// CHARACTER SET utf8 (in CAST/CONVERT, SHOW CHARACTER SET).
|
|
242
|
+
if (dialect !== 'postgres' &&
|
|
243
|
+
token.value === 'SET' &&
|
|
244
|
+
tokens[index - 1]?.value === 'CHARACTER') {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Index hints: USE INDEX (...) / USE KEY (...).
|
|
248
|
+
if (dialect !== 'postgres' &&
|
|
249
|
+
token.value === 'USE' &&
|
|
250
|
+
following?.type === 'word' &&
|
|
251
|
+
(following.value === 'INDEX' || following.value === 'KEY')) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
valid: false,
|
|
256
|
+
reason: `The query contains a blocked SQL operation (${token.value}).`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (token.value === 'FOR' &&
|
|
260
|
+
following?.type === 'word' &&
|
|
261
|
+
rules.lockingWords.has(following.value)) {
|
|
262
|
+
return {
|
|
263
|
+
valid: false,
|
|
264
|
+
reason: 'Locking reads (FOR SHARE / FOR UPDATE) are not allowed.',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { valid: true, sql: output };
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=sql-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-validator.js","sourceRoot":"","sources":["../../src/safety/sql-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DatabaseAdapter, TableInfo, TableSchema, RelationshipInfo } from '../core/types/index.js';
|
|
2
|
+
export interface SchemaCacheOptions {
|
|
3
|
+
ttlMs?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Process-local cache for database metadata. It never caches query results.
|
|
7
|
+
* A short TTL keeps repeated agent schema discovery from repeatedly hitting
|
|
8
|
+
* information_schema while allowing schema changes to become visible.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SchemaCache {
|
|
11
|
+
private readonly ttlMs;
|
|
12
|
+
private tables?;
|
|
13
|
+
private relationships?;
|
|
14
|
+
private readonly tableSchemas;
|
|
15
|
+
constructor(options?: SchemaCacheOptions);
|
|
16
|
+
listTables(database: DatabaseAdapter): Promise<TableInfo[]>;
|
|
17
|
+
describeTable(database: DatabaseAdapter, table: string): Promise<TableSchema>;
|
|
18
|
+
describeTables(database: DatabaseAdapter, tables: string[]): Promise<TableSchema[]>;
|
|
19
|
+
getRelationships(database: DatabaseAdapter): Promise<RelationshipInfo[]>;
|
|
20
|
+
clear(): void;
|
|
21
|
+
private entry;
|
|
22
|
+
private get;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/schema/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAExG,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,MAAM,CAAC,CAA0B;IACzC,OAAO,CAAC,aAAa,CAAC,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8C;IAE3E,YAAY,OAAO,GAAE,kBAAuB,EAE3C;IAEK,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAOhE;IAEK,aAAa,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CASlF;IAEK,cAAc,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAExF;IAEK,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAO7E;IAED,KAAK,IAAI,IAAI,CAIZ;IAED,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,GAAG;CAIZ"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local cache for database metadata. It never caches query results.
|
|
3
|
+
* A short TTL keeps repeated agent schema discovery from repeatedly hitting
|
|
4
|
+
* information_schema while allowing schema changes to become visible.
|
|
5
|
+
*/
|
|
6
|
+
export class SchemaCache {
|
|
7
|
+
ttlMs;
|
|
8
|
+
tables;
|
|
9
|
+
relationships;
|
|
10
|
+
tableSchemas = new Map();
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.ttlMs = options.ttlMs ?? 300_000;
|
|
13
|
+
}
|
|
14
|
+
async listTables(database) {
|
|
15
|
+
const cached = this.get(this.tables);
|
|
16
|
+
if (cached)
|
|
17
|
+
return cached;
|
|
18
|
+
const value = await database.listTables();
|
|
19
|
+
this.tables = this.entry(value);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
async describeTable(database, table) {
|
|
23
|
+
// PostgreSQL names are case-sensitive when quoted; MySQL table names are not portable anyway.
|
|
24
|
+
const key = database.dialect === 'postgres' ? table : table.toLowerCase();
|
|
25
|
+
const cached = this.get(this.tableSchemas.get(key));
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
const value = await database.describeTable(table);
|
|
29
|
+
this.tableSchemas.set(key, this.entry(value));
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
async describeTables(database, tables) {
|
|
33
|
+
return Promise.all(tables.map((table) => this.describeTable(database, table)));
|
|
34
|
+
}
|
|
35
|
+
async getRelationships(database) {
|
|
36
|
+
const cached = this.get(this.relationships);
|
|
37
|
+
if (cached)
|
|
38
|
+
return cached;
|
|
39
|
+
const value = await database.getRelationships();
|
|
40
|
+
this.relationships = this.entry(value);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
this.tables = undefined;
|
|
45
|
+
this.relationships = undefined;
|
|
46
|
+
this.tableSchemas.clear();
|
|
47
|
+
}
|
|
48
|
+
entry(value) {
|
|
49
|
+
return { value, expiresAt: Date.now() + this.ttlMs };
|
|
50
|
+
}
|
|
51
|
+
get(entry) {
|
|
52
|
+
if (!entry || entry.expiresAt <= Date.now())
|
|
53
|
+
return undefined;
|
|
54
|
+
return entry.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/schema/cache.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACL,KAAK,CAAS;IACvB,MAAM,CAA2B;IACjC,aAAa,CAAkC;IACtC,YAAY,GAAG,IAAI,GAAG,EAAmC,CAAC;IAE3E,YAAY,OAAO,GAAuB,EAAE;QAC1C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAyB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAAyB,EAAE,KAAa;QAC1D,8FAA8F;QAC9F,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACpD,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,QAAyB,EAAE,MAAgB;QAC9D,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,QAAyB;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAC;QAChD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;IAEO,KAAK,CAAI,KAAQ;QACvB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IACvD,CAAC;IAEO,GAAG,CAAI,KAAgC;QAC7C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,SAAS,CAAC;QAC9D,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;CACF"}
|