sql-guard 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.
@@ -0,0 +1,1001 @@
1
+ // src/parser/adapter.ts
2
+ import { Parser } from "node-sql-parser";
3
+
4
+ // src/analysis/functions.ts
5
+ function extractAllFunctions(ast) {
6
+ const functions = [];
7
+ const visited = new Set;
8
+ function addFunction(name, schema) {
9
+ if (typeof name !== "string" || name.length === 0)
10
+ return;
11
+ functions.push({
12
+ name: name.toLowerCase(),
13
+ schema: typeof schema === "string" && schema.length > 0 ? schema.toLowerCase() : undefined
14
+ });
15
+ }
16
+ function traverse(node) {
17
+ if (!node || typeof node !== "object")
18
+ return;
19
+ if (visited.has(node))
20
+ return;
21
+ visited.add(node);
22
+ const typed = node;
23
+ const identity = extractFunctionIdentity(typed);
24
+ if (identity)
25
+ addFunction(identity.name, identity.schema);
26
+ for (const value of Object.values(typed)) {
27
+ if (value && typeof value === "object")
28
+ traverse(value);
29
+ }
30
+ }
31
+ traverse(ast);
32
+ const seen = new Set;
33
+ return functions.filter((fn) => {
34
+ const key = `${fn.schema ?? ""}.${fn.name}`;
35
+ if (seen.has(key))
36
+ return false;
37
+ seen.add(key);
38
+ return true;
39
+ });
40
+ }
41
+ function extractFunctionIdentity(node) {
42
+ if (node.type === "aggr_func") {
43
+ if (typeof node.name === "string" && node.name.length > 0) {
44
+ return { name: node.name.toLowerCase() };
45
+ }
46
+ return null;
47
+ }
48
+ if (node.type !== "function") {
49
+ return null;
50
+ }
51
+ const fnName = asRecord(node.name);
52
+ const schemaNode = asRecord(fnName.schema);
53
+ const schema = typeof schemaNode.value === "string" ? schemaNode.value : undefined;
54
+ if (typeof fnName.name === "string" && fnName.name.length > 0) {
55
+ return { name: fnName.name.toLowerCase(), schema: schema?.toLowerCase() };
56
+ }
57
+ if (Array.isArray(fnName.name)) {
58
+ for (const part of fnName.name) {
59
+ const partRecord = asRecord(part);
60
+ if (typeof partRecord.value === "string" && partRecord.value.length > 0) {
61
+ return { name: partRecord.value.toLowerCase(), schema: schema?.toLowerCase() };
62
+ }
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function asRecord(value) {
68
+ if (value && typeof value === "object") {
69
+ return value;
70
+ }
71
+ return {};
72
+ }
73
+
74
+ // src/analysis/relations.ts
75
+ function extractAllTables(ast) {
76
+ const tables = [];
77
+ const visited = new Set;
78
+ function addTable(schema, name, alias, cteScope) {
79
+ if (typeof name !== "string" || name.length === 0)
80
+ return;
81
+ const hasSchema = typeof schema === "string" && schema.length > 0;
82
+ if (!hasSchema && cteScope.has(canonicalName(name))) {
83
+ return;
84
+ }
85
+ tables.push({
86
+ schema: hasSchema ? schema : undefined,
87
+ name,
88
+ alias: typeof alias === "string" ? alias : undefined
89
+ });
90
+ }
91
+ function traverse(node, inheritedCtes) {
92
+ if (!node || typeof node !== "object")
93
+ return;
94
+ if (visited.has(node))
95
+ return;
96
+ visited.add(node);
97
+ const typed = node;
98
+ const localCtes = buildLocalCteScope(typed.with, inheritedCtes);
99
+ if (Array.isArray(typed.with)) {
100
+ for (const cte of typed.with) {
101
+ const cteNode = asRecord2(cte);
102
+ if (cteNode.stmt) {
103
+ traverse(cteNode.stmt, localCtes);
104
+ }
105
+ }
106
+ }
107
+ if (Array.isArray(typed.from)) {
108
+ for (const item of typed.from) {
109
+ extractFromItem(item, localCtes, addTable, traverse);
110
+ }
111
+ }
112
+ if (Array.isArray(typed.join)) {
113
+ for (const join of typed.join) {
114
+ const joinItem = asRecord2(join);
115
+ addTable(joinItem.db, joinItem.table, joinItem.as, localCtes);
116
+ if (joinItem.expr) {
117
+ traverse(joinItem.expr, localCtes);
118
+ }
119
+ }
120
+ }
121
+ if (isRelationStatementType(typed.type)) {
122
+ collectStatementTableTargets(typed.table, localCtes, addTable);
123
+ }
124
+ for (const [key, value] of Object.entries(typed)) {
125
+ if (key === "with")
126
+ continue;
127
+ if (value && typeof value === "object") {
128
+ traverse(value, localCtes);
129
+ }
130
+ }
131
+ }
132
+ traverse(ast, new Set);
133
+ const seen = new Set;
134
+ return tables.filter((table) => {
135
+ const key = `${(table.schema ?? "").toLowerCase()}.${table.name.toLowerCase()}`;
136
+ if (seen.has(key))
137
+ return false;
138
+ seen.add(key);
139
+ return true;
140
+ });
141
+ }
142
+ function extractFromItem(item, cteScope, addTable, traverse) {
143
+ if (!item || typeof item !== "object")
144
+ return;
145
+ const typed = item;
146
+ addTable(typed.db, typed.table, typed.as, cteScope);
147
+ if (typed.expr && typeof typed.expr === "object") {
148
+ traverse(typed.expr, cteScope);
149
+ }
150
+ }
151
+ function extractCteName(value) {
152
+ if (typeof value === "string")
153
+ return value;
154
+ if (!value || typeof value !== "object")
155
+ return null;
156
+ const typed = value;
157
+ if (typeof typed.value === "string" && typed.value.length > 0) {
158
+ return typed.value;
159
+ }
160
+ return null;
161
+ }
162
+ function buildLocalCteScope(withClause, inherited) {
163
+ const local = new Set(inherited);
164
+ if (!Array.isArray(withClause)) {
165
+ return local;
166
+ }
167
+ for (const cte of withClause) {
168
+ const cteNode = asRecord2(cte);
169
+ const cteName = extractCteName(cteNode.name);
170
+ if (cteName) {
171
+ local.add(canonicalName(cteName));
172
+ }
173
+ }
174
+ return local;
175
+ }
176
+ function collectStatementTableTargets(tableNode, cteScope, addTable) {
177
+ if (Array.isArray(tableNode)) {
178
+ for (const tableItem of tableNode) {
179
+ const tableRecord = asRecord2(tableItem);
180
+ addTable(tableRecord.db, tableRecord.table, tableRecord.as, cteScope);
181
+ }
182
+ return;
183
+ }
184
+ if (tableNode && typeof tableNode === "object") {
185
+ const tableRecord = asRecord2(tableNode);
186
+ addTable(tableRecord.db, tableRecord.table, tableRecord.as, cteScope);
187
+ return;
188
+ }
189
+ if (typeof tableNode === "string") {
190
+ addTable(undefined, tableNode, undefined, cteScope);
191
+ }
192
+ }
193
+ function isRelationStatementType(type) {
194
+ if (typeof type !== "string") {
195
+ return false;
196
+ }
197
+ const normalized = type.toLowerCase();
198
+ return normalized === "insert" || normalized === "update" || normalized === "delete";
199
+ }
200
+ function canonicalName(name) {
201
+ if (name.startsWith('"') && name.endsWith('"') && name.length >= 2) {
202
+ return name.slice(1, -1).toLowerCase();
203
+ }
204
+ return name.toLowerCase();
205
+ }
206
+ function asRecord2(value) {
207
+ if (value && typeof value === "object") {
208
+ return value;
209
+ }
210
+ return {};
211
+ }
212
+
213
+ // src/parser/adapter.ts
214
+ var parser = new Parser;
215
+ function parseSql(sql, dialect = "postgresql") {
216
+ try {
217
+ const ast = parser.astify(sql, { database: dialect });
218
+ const statements = Array.isArray(ast) ? ast : [ast];
219
+ return {
220
+ success: true,
221
+ statements: statements.map((statementAst) => astToParsedStatement(statementAst))
222
+ };
223
+ } catch (error) {
224
+ return {
225
+ success: false,
226
+ statements: [],
227
+ error: {
228
+ message: error instanceof Error ? error.message : "Parse error",
229
+ location: extractParserErrorLocation(error)
230
+ }
231
+ };
232
+ }
233
+ }
234
+ function astToParsedStatement(ast) {
235
+ const typed = asRecord3(ast);
236
+ return {
237
+ type: extractStatementType(typed),
238
+ tables: extractAllTables(ast),
239
+ functions: extractAllFunctions(ast),
240
+ raw: ast
241
+ };
242
+ }
243
+ function extractStatementType(ast) {
244
+ const type = String(ast.type || "").toLowerCase();
245
+ if (["select", "insert", "update", "delete"].includes(type)) {
246
+ return type;
247
+ }
248
+ return "unknown";
249
+ }
250
+ function asRecord3(value) {
251
+ if (typeof value === "object" && value !== null) {
252
+ return value;
253
+ }
254
+ return {};
255
+ }
256
+ function extractParserErrorLocation(error) {
257
+ if (!error || typeof error !== "object")
258
+ return;
259
+ const loc = error.location;
260
+ const start = loc && typeof loc === "object" ? loc.start : undefined;
261
+ if (start && typeof start.line === "number" && typeof start.column === "number") {
262
+ return { line: start.line, column: start.column };
263
+ }
264
+ if (loc && typeof loc === "object" && typeof loc.line === "number" && typeof loc.column === "number") {
265
+ return { line: loc.line, column: loc.column };
266
+ }
267
+ return;
268
+ }
269
+
270
+ // src/types/public.ts
271
+ var ErrorCode;
272
+ ((ErrorCode2) => {
273
+ ErrorCode2["PARSE_ERROR"] = "PARSE_ERROR";
274
+ ErrorCode2["UNSUPPORTED_SQL_FEATURE"] = "UNSUPPORTED_SQL_FEATURE";
275
+ ErrorCode2["TABLE_NOT_ALLOWED"] = "TABLE_NOT_ALLOWED";
276
+ ErrorCode2["STATEMENT_NOT_ALLOWED"] = "STATEMENT_NOT_ALLOWED";
277
+ ErrorCode2["FUNCTION_NOT_ALLOWED"] = "FUNCTION_NOT_ALLOWED";
278
+ ErrorCode2["MULTI_STATEMENT_DISABLED"] = "MULTI_STATEMENT_DISABLED";
279
+ ErrorCode2["INVALID_POLICY"] = "INVALID_POLICY";
280
+ })(ErrorCode ||= {});
281
+
282
+ class SqlValidationError extends Error {
283
+ code;
284
+ violations;
285
+ constructor(message, code, violations) {
286
+ super(message);
287
+ this.code = code;
288
+ this.violations = violations;
289
+ this.name = "SqlValidationError";
290
+ }
291
+ }
292
+
293
+ // src/policy/function.ts
294
+ function checkFunctionsAllowed(functions, policy) {
295
+ const allowlists = compileFunctionAllowlists(policy.allowedFunctions ?? []);
296
+ return checkFunctionsAllowedWithAllowlists(functions, allowlists);
297
+ }
298
+ function checkFunctionsAllowedCompiled(functions, policy) {
299
+ return checkFunctionsAllowedWithAllowlists(functions, {
300
+ unqualified: policy.allowedFunctionsUnqualified,
301
+ qualified: policy.allowedFunctionsQualified
302
+ });
303
+ }
304
+ function checkFunctionsAllowedWithAllowlists(functions, allowlists) {
305
+ if (functions.length === 0) {
306
+ return { allowed: true, violations: [] };
307
+ }
308
+ const violations = [];
309
+ const seenViolations = new Set;
310
+ for (const fn of functions) {
311
+ const normalizedName = fn.name.toLowerCase();
312
+ const normalizedSchema = typeof fn.schema === "string" ? fn.schema.toLowerCase() : undefined;
313
+ const allowed = normalizedSchema ? allowlists.qualified.has(`${normalizedSchema}.${normalizedName}`) : allowlists.unqualified.has(normalizedName);
314
+ if (allowed)
315
+ continue;
316
+ const key = `${normalizedSchema ?? ""}.${normalizedName}`;
317
+ if (seenViolations.has(key))
318
+ continue;
319
+ seenViolations.add(key);
320
+ violations.push({
321
+ name: normalizedName,
322
+ schema: normalizedSchema
323
+ });
324
+ }
325
+ if (violations.length === 0) {
326
+ return { allowed: true, violations: [] };
327
+ }
328
+ return {
329
+ allowed: false,
330
+ errorCode: "FUNCTION_NOT_ALLOWED" /* FUNCTION_NOT_ALLOWED */,
331
+ errorMessage: `Functions not allowed: ${violations.map(formatViolation).join(", ")}`,
332
+ violations
333
+ };
334
+ }
335
+ function normalize(value) {
336
+ return value.toLowerCase().trim();
337
+ }
338
+ function compileFunctionAllowlists(allowedFunctions) {
339
+ const unqualified = new Set;
340
+ const qualified = new Set;
341
+ for (const rawEntry of allowedFunctions) {
342
+ const entry = normalize(rawEntry);
343
+ if (!entry)
344
+ continue;
345
+ const parts = entry.split(".");
346
+ if (parts.length === 1 && parts[0].length > 0) {
347
+ unqualified.add(parts[0]);
348
+ continue;
349
+ }
350
+ if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) {
351
+ qualified.add(`${parts[0]}.${parts[1]}`);
352
+ }
353
+ }
354
+ return { unqualified, qualified };
355
+ }
356
+ function formatViolation(violation) {
357
+ if (violation.schema) {
358
+ return `${violation.schema}.${violation.name}`;
359
+ }
360
+ return violation.name;
361
+ }
362
+
363
+ // src/normalize/qualified-name.ts
364
+ function parseQualifiedName(value, mode = "strict") {
365
+ if (typeof value !== "string") {
366
+ return null;
367
+ }
368
+ const segments = splitQualifiedNameSegments(value.trim());
369
+ if (!segments || segments.length !== 2) {
370
+ return null;
371
+ }
372
+ const schemaSegment = parseIdentifierSegment(segments[0]);
373
+ const nameSegment = parseIdentifierSegment(segments[1]);
374
+ if (!schemaSegment || !nameSegment) {
375
+ return null;
376
+ }
377
+ const schema = canonicalizeIdentifier(schemaSegment.value, mode);
378
+ const name = canonicalizeIdentifier(nameSegment.value, mode);
379
+ if (!schema || !name) {
380
+ return null;
381
+ }
382
+ return {
383
+ schema,
384
+ name,
385
+ fullyQualified: `${schema}.${name}`
386
+ };
387
+ }
388
+ function canonicalizeIdentifier(value, mode) {
389
+ const trimmed = value.trim();
390
+ return mode === "caseInsensitive" ? trimmed.toLowerCase() : trimmed;
391
+ }
392
+ function parseIdentifierSegment(value) {
393
+ const trimmed = value.trim();
394
+ if (!trimmed) {
395
+ return null;
396
+ }
397
+ const hasLeadingQuote = trimmed.startsWith('"');
398
+ const hasTrailingQuote = trimmed.endsWith('"');
399
+ if (hasLeadingQuote || hasTrailingQuote) {
400
+ if (!(hasLeadingQuote && hasTrailingQuote && trimmed.length >= 2)) {
401
+ return null;
402
+ }
403
+ const inner = trimmed.slice(1, -1).replace(/""/g, '"');
404
+ if (!inner) {
405
+ return null;
406
+ }
407
+ return { value: inner };
408
+ }
409
+ if (trimmed.includes('"')) {
410
+ return null;
411
+ }
412
+ return { value: trimmed };
413
+ }
414
+ function splitQualifiedNameSegments(value) {
415
+ if (!value) {
416
+ return null;
417
+ }
418
+ const parts = [];
419
+ let current = "";
420
+ let inQuotes = false;
421
+ for (let i = 0;i < value.length; i++) {
422
+ const ch = value[i];
423
+ if (ch === '"') {
424
+ current += ch;
425
+ if (inQuotes && value[i + 1] === '"') {
426
+ current += '"';
427
+ i++;
428
+ continue;
429
+ }
430
+ inQuotes = !inQuotes;
431
+ continue;
432
+ }
433
+ if (ch === "." && !inQuotes) {
434
+ parts.push(current);
435
+ current = "";
436
+ continue;
437
+ }
438
+ current += ch;
439
+ }
440
+ if (inQuotes) {
441
+ return null;
442
+ }
443
+ parts.push(current);
444
+ return parts;
445
+ }
446
+
447
+ // src/normalize/identifier.ts
448
+ function normalizeTableReference(ref, policy, mode = policy.tableIdentifierMatching ?? "strict") {
449
+ if (ref.schema) {
450
+ const schema = normalizeIdentifier(ref.schema, mode);
451
+ const name = normalizeIdentifier(ref.name, mode);
452
+ return {
453
+ success: true,
454
+ table: {
455
+ schema,
456
+ name,
457
+ fullyQualified: `${schema}.${name}`
458
+ }
459
+ };
460
+ }
461
+ if (policy.resolver) {
462
+ let resolved;
463
+ try {
464
+ resolved = policy.resolver(ref.name);
465
+ } catch (err) {
466
+ const msg = err instanceof Error ? err.message : String(err);
467
+ return {
468
+ success: false,
469
+ error: `Resolver threw while resolving '${ref.name}': ${msg}`
470
+ };
471
+ }
472
+ if (typeof resolved === "string" && resolved.trim().length > 0) {
473
+ const parsed = parseQualifiedName(resolved, mode);
474
+ if (!parsed) {
475
+ return {
476
+ success: false,
477
+ error: `Resolver returned invalid table '${resolved}'. Expected 'schema.table'`
478
+ };
479
+ }
480
+ return {
481
+ success: true,
482
+ table: {
483
+ schema: parsed.schema,
484
+ name: parsed.name,
485
+ fullyQualified: `${parsed.schema}.${parsed.name}`
486
+ }
487
+ };
488
+ }
489
+ }
490
+ return {
491
+ success: false,
492
+ error: `Unqualified table reference '${ref.name}' not allowed without resolver`
493
+ };
494
+ }
495
+ function normalizeIdentifier(ident, mode) {
496
+ const trimmed = ident.trim();
497
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
498
+ return canonicalizeIdentifier(trimmed.slice(1, -1).replace(/""/g, '"'), mode);
499
+ }
500
+ return canonicalizeIdentifier(trimmed, mode);
501
+ }
502
+ function isTableAllowed(normalized, allowedTables, mode = "strict") {
503
+ const wanted = `${canonicalizeIdentifier(normalized.schema, mode)}.${canonicalizeIdentifier(normalized.name, mode)}`;
504
+ if (allowedTables instanceof Set) {
505
+ return allowedTables.has(wanted);
506
+ }
507
+ for (const table of allowedTables) {
508
+ const parsed = parseQualifiedName(table, mode);
509
+ if (parsed && parsed.fullyQualified === wanted) {
510
+ return true;
511
+ }
512
+ }
513
+ return false;
514
+ }
515
+
516
+ // src/policy/statement.ts
517
+ function isStatementAllowed(statement, policy) {
518
+ const type = statement.type;
519
+ if (type === "unknown") {
520
+ return {
521
+ allowed: false,
522
+ errorCode: "STATEMENT_NOT_ALLOWED" /* STATEMENT_NOT_ALLOWED */,
523
+ errorMessage: "Unknown statement type not allowed"
524
+ };
525
+ }
526
+ const allowedStatements = policy.allowedStatements ?? ["select"];
527
+ if (!allowedStatements.includes(type)) {
528
+ return {
529
+ allowed: false,
530
+ errorCode: "STATEMENT_NOT_ALLOWED" /* STATEMENT_NOT_ALLOWED */,
531
+ errorMessage: `Statement type '${type}' not allowed. Allowed: ${allowedStatements.join(", ")}`
532
+ };
533
+ }
534
+ return { allowed: true };
535
+ }
536
+ function checkMultiStatementPolicy(statements, policy) {
537
+ const allowMulti = policy.allowMultiStatement ?? false;
538
+ if (!allowMulti && statements.length > 1) {
539
+ return {
540
+ allowed: false,
541
+ errorCode: "MULTI_STATEMENT_DISABLED" /* MULTI_STATEMENT_DISABLED */,
542
+ errorMessage: `Multiple statements not allowed. Found ${statements.length} statements.`
543
+ };
544
+ }
545
+ return { allowed: true };
546
+ }
547
+
548
+ // src/policy/fail-closed.ts
549
+ var SUPPORTED_TYPES = new Set(["select", "insert", "update", "delete"]);
550
+ var WRITE_STATEMENT_TYPES = new Set(["insert", "update", "delete"]);
551
+ var UNSUPPORTED_TYPES = new Set([
552
+ "proc",
553
+ "trigger",
554
+ "drop",
555
+ "create",
556
+ "alter",
557
+ "grant",
558
+ "revoke",
559
+ "truncate",
560
+ "merge",
561
+ "lock",
562
+ "unlock",
563
+ "declare",
564
+ "set",
565
+ "show",
566
+ "analyze",
567
+ "explain",
568
+ "copy"
569
+ ]);
570
+ var UNCERTAINTY_KEYS = new Set([
571
+ "partial",
572
+ "ispartial",
573
+ "incomplete",
574
+ "uncertain",
575
+ "isuncertain",
576
+ "ambiguous",
577
+ "hasambiguity",
578
+ "parseruncertain",
579
+ "parser_uncertain"
580
+ ]);
581
+ function checkUnsupportedFeatures(ast) {
582
+ const type = extractStatementType2(ast);
583
+ if (UNSUPPORTED_TYPES.has(type)) {
584
+ return {
585
+ supported: false,
586
+ errorCode: "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */,
587
+ errorMessage: `Statement type '${type}' is not supported`
588
+ };
589
+ }
590
+ if (!SUPPORTED_TYPES.has(type)) {
591
+ return {
592
+ supported: false,
593
+ errorCode: "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */,
594
+ errorMessage: `Unknown statement type '${type}' is not supported`
595
+ };
596
+ }
597
+ if (hasRecursiveCte(ast)) {
598
+ return {
599
+ supported: false,
600
+ errorCode: "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */,
601
+ errorMessage: "Recursive CTE is not supported"
602
+ };
603
+ }
604
+ if (hasSelectInto(ast, type)) {
605
+ return {
606
+ supported: false,
607
+ errorCode: "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */,
608
+ errorMessage: "SELECT INTO is not supported"
609
+ };
610
+ }
611
+ const nestedWrite = findNestedWriteStatement(ast);
612
+ if (nestedWrite) {
613
+ return {
614
+ supported: false,
615
+ errorCode: "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */,
616
+ errorMessage: `Nested write statement '${nestedWrite.type}' is not supported at '${nestedWrite.path}'`
617
+ };
618
+ }
619
+ const uncertaintyPath = findUncertaintyMarker(ast);
620
+ if (uncertaintyPath) {
621
+ return {
622
+ supported: false,
623
+ errorCode: "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */,
624
+ errorMessage: `Parser uncertainty detected at '${uncertaintyPath}'`
625
+ };
626
+ }
627
+ return { supported: true };
628
+ }
629
+ function hasRecursiveCte(ast) {
630
+ if (typeof ast !== "object" || ast === null) {
631
+ return false;
632
+ }
633
+ const root = ast;
634
+ const withClause = root.with;
635
+ if (!Array.isArray(withClause)) {
636
+ return false;
637
+ }
638
+ for (const withItem of withClause) {
639
+ const item = asRecord4(withItem);
640
+ if (item.recursive === true) {
641
+ return true;
642
+ }
643
+ }
644
+ return false;
645
+ }
646
+ function hasSelectInto(ast, statementType) {
647
+ if (statementType !== "select") {
648
+ return false;
649
+ }
650
+ if (typeof ast !== "object" || ast === null) {
651
+ return false;
652
+ }
653
+ const root = ast;
654
+ const into = root.into;
655
+ if (into === null || into === undefined) {
656
+ return false;
657
+ }
658
+ if (typeof into === "string") {
659
+ return into.trim().length > 0;
660
+ }
661
+ if (typeof into !== "object") {
662
+ return true;
663
+ }
664
+ const intoRecord = into;
665
+ if (Object.hasOwn(intoRecord, "expr")) {
666
+ const expr = intoRecord.expr;
667
+ if (typeof expr === "string") {
668
+ return expr.trim().length > 0;
669
+ }
670
+ return expr !== null && expr !== undefined;
671
+ }
672
+ if (Object.hasOwn(intoRecord, "table")) {
673
+ const table = intoRecord.table;
674
+ if (typeof table === "string") {
675
+ return table.trim().length > 0;
676
+ }
677
+ return table !== null && table !== undefined;
678
+ }
679
+ return false;
680
+ }
681
+ function findNestedWriteStatement(value, path = "ast", depth = 0, seen = new Set) {
682
+ if (typeof value !== "object" || value === null) {
683
+ return null;
684
+ }
685
+ if (seen.has(value)) {
686
+ return null;
687
+ }
688
+ seen.add(value);
689
+ if (Array.isArray(value)) {
690
+ for (let i = 0;i < value.length; i++) {
691
+ const found = findNestedWriteStatement(value[i], `${path}[${i}]`, depth + 1, seen);
692
+ if (found) {
693
+ return found;
694
+ }
695
+ }
696
+ return null;
697
+ }
698
+ const record = value;
699
+ const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
700
+ if (depth > 0 && WRITE_STATEMENT_TYPES.has(type)) {
701
+ return { type, path };
702
+ }
703
+ for (const [key, nested] of Object.entries(record)) {
704
+ const found = findNestedWriteStatement(nested, `${path}.${key}`, depth + 1, seen);
705
+ if (found) {
706
+ return found;
707
+ }
708
+ }
709
+ return null;
710
+ }
711
+ function findUncertaintyMarker(value, path = "ast", seen = new Set) {
712
+ if (typeof value !== "object" || value === null) {
713
+ return null;
714
+ }
715
+ if (seen.has(value)) {
716
+ return null;
717
+ }
718
+ seen.add(value);
719
+ if (Array.isArray(value)) {
720
+ for (let i = 0;i < value.length; i++) {
721
+ const found = findUncertaintyMarker(value[i], `${path}[${i}]`, seen);
722
+ if (found) {
723
+ return found;
724
+ }
725
+ }
726
+ return null;
727
+ }
728
+ const record = value;
729
+ for (const [key, nestedValue] of Object.entries(record)) {
730
+ const normalizedKey = key.toLowerCase();
731
+ if (UNCERTAINTY_KEYS.has(normalizedKey) && nestedValue === true) {
732
+ return `${path}.${key}`;
733
+ }
734
+ if ((normalizedKey === "errors" || normalizedKey === "warnings") && Array.isArray(nestedValue) && nestedValue.length > 0) {
735
+ return `${path}.${key}`;
736
+ }
737
+ const found = findUncertaintyMarker(nestedValue, `${path}.${key}`, seen);
738
+ if (found) {
739
+ return found;
740
+ }
741
+ }
742
+ return null;
743
+ }
744
+ function asRecord4(value) {
745
+ if (typeof value === "object" && value !== null) {
746
+ return value;
747
+ }
748
+ return {};
749
+ }
750
+ function extractStatementType2(ast) {
751
+ if (!ast || typeof ast !== "object") {
752
+ return "unknown";
753
+ }
754
+ const typed = ast;
755
+ return String(typed.type || "unknown").toLowerCase();
756
+ }
757
+
758
+ // src/policy/compile-policy.ts
759
+ function compilePolicy(policy) {
760
+ if (!Array.isArray(policy.allowedTables)) {
761
+ return invalidPolicy("Policy 'allowedTables' must be an array of schema-qualified names");
762
+ }
763
+ const tableIdentifierMatching = policy.tableIdentifierMatching ?? "strict";
764
+ if (tableIdentifierMatching !== "strict" && tableIdentifierMatching !== "caseInsensitive") {
765
+ return invalidPolicy("Policy 'tableIdentifierMatching' must be either 'strict' or 'caseInsensitive'");
766
+ }
767
+ const allowedTables = new Set;
768
+ for (const table of policy.allowedTables) {
769
+ const canonical = canonicalizeQualifiedName(table, tableIdentifierMatching);
770
+ if (!canonical) {
771
+ return invalidPolicy(`Policy entry '${String(table)}' is invalid. allowedTables entries must be schema-qualified as 'schema.table'`);
772
+ }
773
+ allowedTables.add(canonical);
774
+ }
775
+ const allowedFunctionsUnqualified = new Set;
776
+ const allowedFunctionsQualified = new Set;
777
+ if (policy.allowedFunctions !== undefined && !Array.isArray(policy.allowedFunctions)) {
778
+ return invalidPolicy("Policy 'allowedFunctions' must be an array when provided");
779
+ }
780
+ if (policy.allowedStatements !== undefined && !Array.isArray(policy.allowedStatements)) {
781
+ return invalidPolicy("Policy 'allowedStatements' must be an array when provided");
782
+ }
783
+ for (const fn of policy.allowedFunctions ?? []) {
784
+ const canonicalFunction = canonicalizeFunctionEntry(fn);
785
+ if (!canonicalFunction) {
786
+ return invalidPolicy(`Policy entry '${String(fn)}' is invalid. allowedFunctions entries must be 'function' or 'schema.function'`);
787
+ }
788
+ if (canonicalFunction.kind === "qualified") {
789
+ allowedFunctionsQualified.add(canonicalFunction.value);
790
+ } else {
791
+ allowedFunctionsUnqualified.add(canonicalFunction.value);
792
+ }
793
+ }
794
+ return {
795
+ success: true,
796
+ compiled: {
797
+ allowedTables,
798
+ allowedFunctionsUnqualified,
799
+ allowedFunctionsQualified,
800
+ tableIdentifierMatching
801
+ }
802
+ };
803
+ }
804
+ function canonicalizeFunctionEntry(value) {
805
+ if (typeof value !== "string") {
806
+ return null;
807
+ }
808
+ const cleaned = value.trim().toLowerCase();
809
+ if (!cleaned) {
810
+ return null;
811
+ }
812
+ const parts = cleaned.split(".");
813
+ if (parts.length === 1 && parts[0].length > 0) {
814
+ return { kind: "unqualified", value: parts[0] };
815
+ }
816
+ if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) {
817
+ return { kind: "qualified", value: `${parts[0]}.${parts[1]}` };
818
+ }
819
+ return null;
820
+ }
821
+ function canonicalizeQualifiedName(value, mode = "strict") {
822
+ const parsed = parseQualifiedName(value, mode);
823
+ return parsed?.fullyQualified ?? null;
824
+ }
825
+ function invalidPolicy(message) {
826
+ return {
827
+ success: false,
828
+ errorCode: "INVALID_POLICY" /* INVALID_POLICY */,
829
+ violation: {
830
+ type: "policy",
831
+ message
832
+ }
833
+ };
834
+ }
835
+
836
+ // src/policy/engine.ts
837
+ var METADATA_SCHEMAS = new Set(["information_schema", "pg_catalog"]);
838
+ function validateAgainstPolicy(sql, policy) {
839
+ const compiledPolicyResult = compilePolicy(policy);
840
+ if (!compiledPolicyResult.success) {
841
+ return {
842
+ ok: false,
843
+ violations: [compiledPolicyResult.violation],
844
+ errorCode: compiledPolicyResult.errorCode
845
+ };
846
+ }
847
+ const compiledPolicy = compiledPolicyResult.compiled;
848
+ const parsed = parseSql(sql);
849
+ if (!parsed.success) {
850
+ const parseViolation = {
851
+ type: "parse",
852
+ message: parsed.error?.message ?? "Parse error",
853
+ location: parsed.error?.location
854
+ };
855
+ return {
856
+ ok: false,
857
+ violations: [parseViolation],
858
+ errorCode: "PARSE_ERROR" /* PARSE_ERROR */
859
+ };
860
+ }
861
+ const violations = [];
862
+ const seenViolations = new Set;
863
+ const errorCodes = [];
864
+ for (const statement of parsed.statements) {
865
+ const unsupportedCheck = checkUnsupportedFeatures(statement.raw);
866
+ if (!unsupportedCheck.supported) {
867
+ return {
868
+ ok: false,
869
+ violations: [
870
+ {
871
+ type: "unsupported",
872
+ message: unsupportedCheck.errorMessage ?? "Unsupported SQL feature"
873
+ }
874
+ ],
875
+ errorCode: unsupportedCheck.errorCode ?? "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */
876
+ };
877
+ }
878
+ }
879
+ const multiStatementCheck = checkMultiStatementPolicy(parsed.statements, policy);
880
+ if (!multiStatementCheck.allowed) {
881
+ pushViolation(violations, seenViolations, {
882
+ type: "statement",
883
+ message: multiStatementCheck.errorMessage ?? "Multiple statements not allowed"
884
+ }, multiStatementCheck.errorCode, errorCodes);
885
+ }
886
+ for (const statement of parsed.statements) {
887
+ const statementCheck = isStatementAllowed(statement, policy);
888
+ if (!statementCheck.allowed) {
889
+ pushViolation(violations, seenViolations, {
890
+ type: "statement",
891
+ message: statementCheck.errorMessage ?? `Statement type '${statement.type}' not allowed`
892
+ }, statementCheck.errorCode, errorCodes);
893
+ }
894
+ for (const tableRef of statement.tables) {
895
+ const normalized = normalizeTableReference(tableRef, policy, compiledPolicy.tableIdentifierMatching);
896
+ if (!normalized.success || !normalized.table) {
897
+ pushViolation(violations, seenViolations, {
898
+ type: "table",
899
+ message: normalized.error ?? `Table '${tableRef.name}' is not allowed`,
900
+ location: tableRef.location
901
+ }, "TABLE_NOT_ALLOWED" /* TABLE_NOT_ALLOWED */, errorCodes);
902
+ continue;
903
+ }
904
+ const metadataDenied = isMetadataTableDenied(normalized.table.schema, normalized.table.fullyQualified, compiledPolicy);
905
+ const tableAllowed = isTableAllowed(normalized.table, compiledPolicy.allowedTables, compiledPolicy.tableIdentifierMatching);
906
+ if (metadataDenied || !tableAllowed) {
907
+ const message = metadataDenied ? `Metadata table '${normalized.table.fullyQualified}' is not allowed` : `Table '${normalized.table.fullyQualified}' is not allowed`;
908
+ pushViolation(violations, seenViolations, {
909
+ type: "table",
910
+ message,
911
+ location: tableRef.location
912
+ }, "TABLE_NOT_ALLOWED" /* TABLE_NOT_ALLOWED */, errorCodes);
913
+ }
914
+ }
915
+ const functionCheck = checkFunctionsAllowedCompiled(statement.functions, compiledPolicy);
916
+ if (!functionCheck.allowed) {
917
+ for (const violation of functionCheck.violations) {
918
+ pushViolation(violations, seenViolations, {
919
+ type: "function",
920
+ message: violation.schema ? `Function '${violation.schema}.${violation.name}' is not allowed` : `Function '${violation.name}' is not allowed`
921
+ }, functionCheck.errorCode, errorCodes);
922
+ }
923
+ }
924
+ }
925
+ if (violations.length === 0) {
926
+ return { ok: true, violations: [] };
927
+ }
928
+ return {
929
+ ok: false,
930
+ violations,
931
+ errorCode: pickErrorCode(errorCodes)
932
+ };
933
+ }
934
+ function isMetadataTableDenied(schema, fullyQualified, policy) {
935
+ if (!METADATA_SCHEMAS.has(schema.toLowerCase())) {
936
+ return false;
937
+ }
938
+ return !policy.allowedTables.has(fullyQualified);
939
+ }
940
+ function pushViolation(violations, seenViolations, violation, errorCode, errorCodes) {
941
+ const key = `${violation.type}:${violation.message}:${violation.location?.line ?? ""}:${violation.location?.column ?? ""}`;
942
+ if (seenViolations.has(key)) {
943
+ return;
944
+ }
945
+ seenViolations.add(key);
946
+ violations.push(violation);
947
+ if (errorCode) {
948
+ errorCodes.push(errorCode);
949
+ }
950
+ }
951
+ function pickErrorCode(errorCodes) {
952
+ if (errorCodes.length === 0) {
953
+ return;
954
+ }
955
+ return [...errorCodes].sort((left, right) => precedenceOf(left) - precedenceOf(right))[0];
956
+ }
957
+ function precedenceOf(errorCode) {
958
+ switch (errorCode) {
959
+ case "PARSE_ERROR" /* PARSE_ERROR */:
960
+ return 0;
961
+ case "INVALID_POLICY" /* INVALID_POLICY */:
962
+ return 1;
963
+ case "STATEMENT_NOT_ALLOWED" /* STATEMENT_NOT_ALLOWED */:
964
+ return 2;
965
+ case "TABLE_NOT_ALLOWED" /* TABLE_NOT_ALLOWED */:
966
+ return 3;
967
+ case "FUNCTION_NOT_ALLOWED" /* FUNCTION_NOT_ALLOWED */:
968
+ return 4;
969
+ case "MULTI_STATEMENT_DISABLED" /* MULTI_STATEMENT_DISABLED */:
970
+ return 5;
971
+ case "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */:
972
+ default:
973
+ return 6;
974
+ }
975
+ }
976
+
977
+ // src/index.ts
978
+ function validate(sql, policy) {
979
+ return validateAgainstPolicy(sql, policy);
980
+ }
981
+ function assertSafeSql(sql, policy) {
982
+ const result = validate(sql, policy);
983
+ if (!result.ok) {
984
+ throw new SqlValidationError(`SQL validation failed: ${result.errorCode}`, result.errorCode || "UNSUPPORTED_SQL_FEATURE" /* UNSUPPORTED_SQL_FEATURE */, result.violations);
985
+ }
986
+ }
987
+ export {
988
+ validateAgainstPolicy,
989
+ validate,
990
+ parseSql,
991
+ normalizeTableReference,
992
+ isTableAllowed,
993
+ isStatementAllowed,
994
+ extractAllTables,
995
+ extractAllFunctions,
996
+ checkMultiStatementPolicy,
997
+ checkFunctionsAllowed,
998
+ assertSafeSql,
999
+ SqlValidationError,
1000
+ ErrorCode
1001
+ };