agent-sql 0.3.0 → 0.3.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 +13 -15
- package/dist/index.mjs +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,20 +4,20 @@ Sanitise agent-written SQL for multi-tenant DBs.
|
|
|
4
4
|
|
|
5
5
|
You provide a tenant ID, and the agent supplies the query.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
CTEs and other complex things that we aren't confident of securing: error.
|
|
10
|
-
|
|
11
|
-
It ensures that that the needed tenant table is somewhere in the query,
|
|
12
|
-
and adds a `WHERE` clause ensuring that only values from the supplied ID are returned.
|
|
13
|
-
Then it checks that the tables and `JOIN`s follow the schema, preventing sneaky joins.
|
|
7
|
+
Apparently this is how [Trigger.dev does it](https://x.com/mattaitken/status/2033928542975639785).
|
|
8
|
+
And [Cloudflare](https://x.com/thomas_ankcorn/status/2033931057133748330).
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
## How it works
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
agent-sql works by fully parsing the supplied SQL query into an AST and transforming it:
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
- **Only `SELECT`:** it's impossible to insert, drop or anything else.
|
|
15
|
+
- **Reduced subset:** CTEs, subqueries and other tricky things are rejected.
|
|
16
|
+
- **Limited functions:** passed through a (configurable) whitelist.
|
|
17
|
+
- **No DoS:** a default `LIMIT` is applied, but can be adjusted.
|
|
18
|
+
- **`WHERE` guards:** insert multiple tenant/ownership conditions to be inserted.
|
|
19
|
+
- **`JOIN`s added:** if needed to reach the guard tenant tables (save on tokens).
|
|
20
|
+
- **No sneaky joins:** no `join secrets on true`. We have your back.
|
|
21
21
|
|
|
22
22
|
## Quickstart
|
|
23
23
|
|
|
@@ -28,17 +28,15 @@ npm install agent-sql
|
|
|
28
28
|
```ts
|
|
29
29
|
import { agentSql } from "agent-sql";
|
|
30
30
|
|
|
31
|
-
const sql = agentSql(`SELECT * FROM msg`, "msg.
|
|
31
|
+
const sql = agentSql(`SELECT * FROM msg`, "msg.tenant_id", 123);
|
|
32
32
|
|
|
33
33
|
console.log(sql);
|
|
34
34
|
// SELECT *
|
|
35
35
|
// FROM msg
|
|
36
|
-
// WHERE msg.
|
|
36
|
+
// WHERE msg.tenant_id = 123
|
|
37
37
|
// LIMIT 10000
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
`agent-sql` parses the SQL, enforces a mandatory equality filter on the given column as the outermost `AND` condition (so it cannot be short-circuited by agent-supplied `OR` clauses), and returns the sanitised SQL string.
|
|
41
|
-
|
|
42
40
|
## Usage
|
|
43
41
|
|
|
44
42
|
### Define once, use many times
|
package/dist/index.mjs
CHANGED
|
@@ -822,6 +822,7 @@ function resolveGraphForJoins(ast, schema, guards) {
|
|
|
822
822
|
const haveTables = /* @__PURE__ */ new Set();
|
|
823
823
|
haveTables.add(ast.from.table.name);
|
|
824
824
|
for (const join of ast.joins) haveTables.add(join.table.name);
|
|
825
|
+
const originalTables = new Set(haveTables);
|
|
825
826
|
const adj = buildAdjacency(schema);
|
|
826
827
|
const newJoins = [];
|
|
827
828
|
for (const guard of guards) {
|
|
@@ -839,8 +840,10 @@ function resolveGraphForJoins(ast, schema, guards) {
|
|
|
839
840
|
}
|
|
840
841
|
}
|
|
841
842
|
if (newJoins.length === 0) return Ok(ast);
|
|
843
|
+
const columns = qualifyWildcards(ast.columns, originalTables);
|
|
842
844
|
return Ok({
|
|
843
845
|
...ast,
|
|
846
|
+
columns,
|
|
844
847
|
joins: [...ast.joins, ...newJoins]
|
|
845
848
|
});
|
|
846
849
|
}
|
|
@@ -927,6 +930,20 @@ function edgeToJoin(edge, fromTable) {
|
|
|
927
930
|
}
|
|
928
931
|
};
|
|
929
932
|
}
|
|
933
|
+
function qualifyWildcards(columns, tables) {
|
|
934
|
+
if (!columns.some((c) => c.expr.kind === "wildcard")) return columns;
|
|
935
|
+
const qualified = [];
|
|
936
|
+
for (const col of columns) if (col.expr.kind === "wildcard") for (const table of tables) qualified.push({
|
|
937
|
+
type: "column",
|
|
938
|
+
expr: {
|
|
939
|
+
type: "column_expr",
|
|
940
|
+
kind: "qualified_wildcard",
|
|
941
|
+
table
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
else qualified.push(col);
|
|
945
|
+
return qualified;
|
|
946
|
+
}
|
|
930
947
|
//#endregion
|
|
931
948
|
//#region src/utils.ts
|
|
932
949
|
function unreachable(x) {
|