@blogic-cz/agent-tools 0.15.13 → 0.16.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.
- package/README.md +5 -0
- package/package.json +29 -78
- package/src/audit-tool/index.ts +0 -0
- package/src/az-tool/errors.ts +4 -4
- package/src/az-tool/service.ts +2 -2
- package/src/db-tool/errors.ts +5 -18
- package/src/db-tool/index.ts +2 -0
- package/src/db-tool/security.ts +96 -15
- package/src/db-tool/service.ts +71 -146
- package/src/db-tool/sql-client.ts +80 -0
- package/src/gh-tool/errors.ts +11 -14
- package/src/gh-tool/pr/helpers.ts +11 -9
- package/src/index.ts +2 -0
- package/src/k8s-tool/errors.ts +4 -4
- package/src/k8s-tool/service.ts +33 -28
- package/src/logs-tool/errors.ts +11 -14
- package/src/observability-tool/errors.ts +1 -1
- package/src/observability-tool/index.ts +0 -0
- package/src/session-tool/errors.ts +7 -10
- package/src/session-tool/index.ts +0 -0
- package/src/shared/binary-preflight.ts +42 -0
- package/src/shared/env-template.ts +1 -1
- package/src/shared/exec.ts +1 -1
- package/src/shared/index.ts +8 -1
- package/src/shared/prerequisites/errors.ts +1 -1
- package/src/shared/prerequisites/index.ts +12 -0
- package/src/shared/throttle.ts +1 -1
- package/src/shared/types.ts +6 -0
- package/src/db-tool/psql.ts +0 -27
package/README.md
CHANGED
|
@@ -27,6 +27,11 @@ These tools wrap each CLI with:
|
|
|
27
27
|
bun add -d @blogic-cz/agent-tools
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
`effect` is a peer dependency pinned to an exact version, and package managers install
|
|
31
|
+
it for you. If your project depends on `effect` too, pin it to the same exact version.
|
|
32
|
+
Two copies of `effect` are structurally incompatible types, so the values this package
|
|
33
|
+
exports stop typechecking in your project when the versions drift.
|
|
34
|
+
|
|
30
35
|
### Skill installation
|
|
31
36
|
|
|
32
37
|
Install per-tool skills for only the tools your project uses:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -39,90 +39,38 @@
|
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
41
|
"src/",
|
|
42
|
-
"dist/",
|
|
43
42
|
"schemas/",
|
|
44
43
|
"README.md",
|
|
45
44
|
"LICENSE"
|
|
46
45
|
],
|
|
47
46
|
"type": "module",
|
|
48
|
-
"types": "./
|
|
47
|
+
"types": "./src/index.ts",
|
|
49
48
|
"imports": {
|
|
50
|
-
"#shared":
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"#config/*": {
|
|
63
|
-
"types": "./dist/config/*.d.ts",
|
|
64
|
-
"default": "./src/config/*.ts"
|
|
65
|
-
},
|
|
66
|
-
"#gh/*": {
|
|
67
|
-
"types": "./dist/gh-tool/*.d.ts",
|
|
68
|
-
"default": "./src/gh-tool/*.ts"
|
|
69
|
-
},
|
|
70
|
-
"#k8s/*": {
|
|
71
|
-
"types": "./dist/k8s-tool/*.d.ts",
|
|
72
|
-
"default": "./src/k8s-tool/*.ts"
|
|
73
|
-
},
|
|
74
|
-
"#db/*": {
|
|
75
|
-
"types": "./dist/db-tool/*.d.ts",
|
|
76
|
-
"default": "./src/db-tool/*.ts"
|
|
77
|
-
},
|
|
78
|
-
"#observability/*": {
|
|
79
|
-
"types": "./dist/observability-tool/*.d.ts",
|
|
80
|
-
"default": "./src/observability-tool/*.ts"
|
|
81
|
-
},
|
|
82
|
-
"#logs/*": {
|
|
83
|
-
"types": "./dist/logs-tool/*.d.ts",
|
|
84
|
-
"default": "./src/logs-tool/*.ts"
|
|
85
|
-
},
|
|
86
|
-
"#az/*": {
|
|
87
|
-
"types": "./dist/az-tool/*.d.ts",
|
|
88
|
-
"default": "./src/az-tool/*.ts"
|
|
89
|
-
},
|
|
90
|
-
"#guard": {
|
|
91
|
-
"types": "./dist/credential-guard/index.d.ts",
|
|
92
|
-
"default": "./src/credential-guard/index.ts"
|
|
93
|
-
},
|
|
94
|
-
"#session/*": {
|
|
95
|
-
"types": "./dist/session-tool/*.d.ts",
|
|
96
|
-
"default": "./src/session-tool/*.ts"
|
|
97
|
-
}
|
|
49
|
+
"#shared": "./src/shared/index.ts",
|
|
50
|
+
"#shared/*": "./src/shared/*.ts",
|
|
51
|
+
"#config": "./src/config/index.ts",
|
|
52
|
+
"#config/*": "./src/config/*.ts",
|
|
53
|
+
"#gh/*": "./src/gh-tool/*.ts",
|
|
54
|
+
"#k8s/*": "./src/k8s-tool/*.ts",
|
|
55
|
+
"#db/*": "./src/db-tool/*.ts",
|
|
56
|
+
"#observability/*": "./src/observability-tool/*.ts",
|
|
57
|
+
"#logs/*": "./src/logs-tool/*.ts",
|
|
58
|
+
"#az/*": "./src/az-tool/*.ts",
|
|
59
|
+
"#guard": "./src/credential-guard/index.ts",
|
|
60
|
+
"#session/*": "./src/session-tool/*.ts"
|
|
98
61
|
},
|
|
99
62
|
"exports": {
|
|
100
|
-
".":
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"./
|
|
105
|
-
|
|
106
|
-
"default": "./src/credential-guard/index.ts"
|
|
107
|
-
},
|
|
108
|
-
"./config": {
|
|
109
|
-
"types": "./dist/config/index.d.ts",
|
|
110
|
-
"default": "./src/config/index.ts"
|
|
111
|
-
},
|
|
112
|
-
"./audit": {
|
|
113
|
-
"types": "./dist/shared/audit.d.ts",
|
|
114
|
-
"default": "./src/shared/audit.ts"
|
|
115
|
-
},
|
|
116
|
-
"./k8s-probe": {
|
|
117
|
-
"types": "./dist/shared/k8s-probe.d.ts",
|
|
118
|
-
"default": "./src/shared/k8s-probe.ts"
|
|
119
|
-
}
|
|
63
|
+
".": "./src/index.ts",
|
|
64
|
+
"./credential-guard": "./src/credential-guard/index.ts",
|
|
65
|
+
"./config": "./src/config/index.ts",
|
|
66
|
+
"./audit": "./src/shared/audit.ts",
|
|
67
|
+
"./k8s-probe": "./src/shared/k8s-probe.ts",
|
|
68
|
+
"./prerequisites": "./src/shared/prerequisites/index.ts"
|
|
120
69
|
},
|
|
121
70
|
"publishConfig": {
|
|
122
71
|
"access": "public"
|
|
123
72
|
},
|
|
124
73
|
"scripts": {
|
|
125
|
-
"build:types": "tsc -p tsconfig.declarations.json",
|
|
126
74
|
"check": "bun check.ts",
|
|
127
75
|
"check:ci": "bun check.ts ci",
|
|
128
76
|
"format": "oxfmt",
|
|
@@ -137,23 +85,26 @@
|
|
|
137
85
|
"test": "vitest run"
|
|
138
86
|
},
|
|
139
87
|
"dependencies": {
|
|
140
|
-
"@effect/platform-bun": "4.0.0-beta.
|
|
141
|
-
"@toon-format/toon": "2.1.0"
|
|
142
|
-
"effect": "4.0.0-beta.98"
|
|
88
|
+
"@effect/platform-bun": "4.0.0-beta.105",
|
|
89
|
+
"@toon-format/toon": "2.1.0"
|
|
143
90
|
},
|
|
144
91
|
"devDependencies": {
|
|
145
92
|
"@effect/language-service": "0.86.2",
|
|
146
|
-
"@effect/vitest": "4.0.0-beta.
|
|
93
|
+
"@effect/vitest": "4.0.0-beta.105",
|
|
147
94
|
"@types/bun": "1.3.12",
|
|
95
|
+
"effect": "4.0.0-beta.105",
|
|
148
96
|
"oxfmt": "0.44.0",
|
|
149
97
|
"oxlint": "1.59.0",
|
|
150
98
|
"typescript": "6.0.2",
|
|
151
99
|
"vitest": "^4.1.4"
|
|
152
100
|
},
|
|
101
|
+
"peerDependencies": {
|
|
102
|
+
"effect": "4.0.0-beta.105"
|
|
103
|
+
},
|
|
153
104
|
"overrides": {
|
|
154
|
-
"@effect/platform-node-shared": "4.0.0-beta.
|
|
105
|
+
"@effect/platform-node-shared": "4.0.0-beta.105"
|
|
155
106
|
},
|
|
156
107
|
"engines": {
|
|
157
|
-
"bun": ">=1.
|
|
108
|
+
"bun": ">=1.3.0"
|
|
158
109
|
}
|
|
159
110
|
}
|
package/src/audit-tool/index.ts
CHANGED
|
File without changes
|
package/src/az-tool/errors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
export class AzSecurityError extends Schema.
|
|
3
|
+
export class AzSecurityError extends Schema.TaggedError<AzSecurityError>()("AzSecurityError", {
|
|
4
4
|
message: Schema.String,
|
|
5
5
|
command: Schema.String,
|
|
6
6
|
hint: Schema.optionalKey(Schema.String),
|
|
@@ -8,7 +8,7 @@ export class AzSecurityError extends Schema.TaggedErrorClass<AzSecurityError>()(
|
|
|
8
8
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
9
9
|
}) {}
|
|
10
10
|
|
|
11
|
-
export class AzCommandError extends Schema.
|
|
11
|
+
export class AzCommandError extends Schema.TaggedError<AzCommandError>()("AzCommandError", {
|
|
12
12
|
message: Schema.String,
|
|
13
13
|
command: Schema.String,
|
|
14
14
|
exitCode: Schema.optionalKey(Schema.Number),
|
|
@@ -18,7 +18,7 @@ export class AzCommandError extends Schema.TaggedErrorClass<AzCommandError>()("A
|
|
|
18
18
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
19
19
|
}) {}
|
|
20
20
|
|
|
21
|
-
export class AzTimeoutError extends Schema.
|
|
21
|
+
export class AzTimeoutError extends Schema.TaggedError<AzTimeoutError>()("AzTimeoutError", {
|
|
22
22
|
message: Schema.String,
|
|
23
23
|
command: Schema.String,
|
|
24
24
|
timeoutMs: Schema.Number,
|
|
@@ -27,7 +27,7 @@ export class AzTimeoutError extends Schema.TaggedErrorClass<AzTimeoutError>()("A
|
|
|
27
27
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
28
28
|
}) {}
|
|
29
29
|
|
|
30
|
-
export class AzParseError extends Schema.
|
|
30
|
+
export class AzParseError extends Schema.TaggedError<AzParseError>()("AzParseError", {
|
|
31
31
|
message: Schema.String,
|
|
32
32
|
rawOutput: Schema.String,
|
|
33
33
|
hint: Schema.optionalKey(Schema.String),
|
package/src/az-tool/service.ts
CHANGED
|
@@ -141,7 +141,7 @@ export class AzService extends Context.Service<
|
|
|
141
141
|
message: result.stderr || `Command failed with exit code ${result.exitCode}`,
|
|
142
142
|
command: fullCommand,
|
|
143
143
|
exitCode: result.exitCode,
|
|
144
|
-
stderr: result.stderr
|
|
144
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -209,7 +209,7 @@ export class AzService extends Context.Service<
|
|
|
209
209
|
message: result.stderr || `Invoke failed with exit code ${result.exitCode}`,
|
|
210
210
|
command: fullCommand,
|
|
211
211
|
exitCode: result.exitCode,
|
|
212
|
-
stderr: result.stderr
|
|
212
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
|
package/src/db-tool/errors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
export class DbConnectionError extends Schema.
|
|
3
|
+
export class DbConnectionError extends Schema.TaggedError<DbConnectionError>()(
|
|
4
4
|
"DbConnectionError",
|
|
5
5
|
{
|
|
6
6
|
message: Schema.String,
|
|
@@ -11,7 +11,7 @@ export class DbConnectionError extends Schema.TaggedErrorClass<DbConnectionError
|
|
|
11
11
|
},
|
|
12
12
|
) {}
|
|
13
13
|
|
|
14
|
-
export class DbQueryError extends Schema.
|
|
14
|
+
export class DbQueryError extends Schema.TaggedError<DbQueryError>()("DbQueryError", {
|
|
15
15
|
message: Schema.String,
|
|
16
16
|
sql: Schema.String,
|
|
17
17
|
stderr: Schema.optionalKey(Schema.String),
|
|
@@ -20,7 +20,7 @@ export class DbQueryError extends Schema.TaggedErrorClass<DbQueryError>()("DbQue
|
|
|
20
20
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
21
21
|
}) {}
|
|
22
22
|
|
|
23
|
-
export class DbTunnelError extends Schema.
|
|
23
|
+
export class DbTunnelError extends Schema.TaggedError<DbTunnelError>()("DbTunnelError", {
|
|
24
24
|
message: Schema.String,
|
|
25
25
|
port: Schema.Number,
|
|
26
26
|
hint: Schema.optionalKey(Schema.String),
|
|
@@ -28,15 +28,7 @@ export class DbTunnelError extends Schema.TaggedErrorClass<DbTunnelError>()("DbT
|
|
|
28
28
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
29
29
|
}) {}
|
|
30
30
|
|
|
31
|
-
export class
|
|
32
|
-
message: Schema.String,
|
|
33
|
-
rawOutput: Schema.String,
|
|
34
|
-
hint: Schema.optionalKey(Schema.String),
|
|
35
|
-
nextCommand: Schema.optionalKey(Schema.String),
|
|
36
|
-
retryable: Schema.optionalKey(Schema.Boolean),
|
|
37
|
-
}) {}
|
|
38
|
-
|
|
39
|
-
export class DbMutationBlockedError extends Schema.TaggedErrorClass<DbMutationBlockedError>()(
|
|
31
|
+
export class DbMutationBlockedError extends Schema.TaggedError<DbMutationBlockedError>()(
|
|
40
32
|
"DbMutationBlockedError",
|
|
41
33
|
{
|
|
42
34
|
message: Schema.String,
|
|
@@ -47,9 +39,4 @@ export class DbMutationBlockedError extends Schema.TaggedErrorClass<DbMutationBl
|
|
|
47
39
|
},
|
|
48
40
|
) {}
|
|
49
41
|
|
|
50
|
-
export type DbError =
|
|
51
|
-
| DbConnectionError
|
|
52
|
-
| DbMutationBlockedError
|
|
53
|
-
| DbParseError
|
|
54
|
-
| DbQueryError
|
|
55
|
-
| DbTunnelError;
|
|
42
|
+
export type DbError = DbConnectionError | DbMutationBlockedError | DbQueryError | DbTunnelError;
|
package/src/db-tool/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#confi
|
|
|
17
17
|
import { DbConfigService, makeDbConfigLayer } from "./config-service";
|
|
18
18
|
import { DbConnectionError } from "./errors";
|
|
19
19
|
import { DbService } from "./service";
|
|
20
|
+
import { DbSqlClient } from "./sql-client";
|
|
20
21
|
|
|
21
22
|
// Extract --profile from argv before @effect/cli parsing
|
|
22
23
|
// so we can build the correct config layer.
|
|
@@ -243,6 +244,7 @@ const dbConfigLayer = makeDbConfigLayer(profileArg);
|
|
|
243
244
|
const MainLayer = DbService.layer.pipe(
|
|
244
245
|
// provideMerge (not provide) so DbConfigService stays in the program context for the `envs` command,
|
|
245
246
|
// which reads it directly rather than going through DbService.
|
|
247
|
+
Layer.provideMerge(DbSqlClient.layer),
|
|
246
248
|
Layer.provideMerge(dbConfigLayer),
|
|
247
249
|
Layer.provideMerge(ConfigServiceLayer),
|
|
248
250
|
Layer.provideMerge(BunServices.layer),
|
package/src/db-tool/security.ts
CHANGED
|
@@ -25,9 +25,27 @@ const MUTATION_TARGET_PATTERNS: Array<[DbMutationOperation, RegExp]> = [
|
|
|
25
25
|
["delete", new RegExp(String.raw`^\s*DELETE\s+FROM\s+(${QUALIFIED_IDENTIFIER_PATTERN})`, "i")],
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
+
const DOLLAR_QUOTE_TAG = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Postgres treats a backslash as an escape only inside an E'...' literal; in a standard string
|
|
32
|
+
* it is an ordinary character, so applying escapes everywhere would swallow a real closing quote.
|
|
33
|
+
*/
|
|
34
|
+
function isEscapeStringPrefix(sql: string, quoteIndex: number): boolean {
|
|
35
|
+
const prefix = sql[quoteIndex - 1];
|
|
36
|
+
if (prefix !== "E" && prefix !== "e") return false;
|
|
37
|
+
const before = sql[quoteIndex - 2];
|
|
38
|
+
return before === undefined || !/[A-Za-z0-9_$]/.test(before);
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
/**
|
|
29
|
-
* Strip SQL comments (block and line)
|
|
30
|
-
*
|
|
42
|
+
* Strip SQL comments (block and line) and empty out every string literal, keeping only
|
|
43
|
+
* double-quoted identifiers verbatim because the mutation-target parser needs them.
|
|
44
|
+
* Single quotes, double-quoted identifiers and dollar-quoted bodies must all be tracked:
|
|
45
|
+
* a quote or comment token surviving inside a region another check reads differently is a
|
|
46
|
+
* guard bypass. `SELECT * FROM "weird--table"; DROP TABLE users` would lose its statement
|
|
47
|
+
* separator to a phantom comment, and `SELECT $$O\'Brien$$ AS n; DROP TABLE users` would
|
|
48
|
+
* leave an unpaired apostrophe that swallows the separator instead.
|
|
31
49
|
*/
|
|
32
50
|
export function stripSqlComments(sql: string): string {
|
|
33
51
|
let result = "";
|
|
@@ -38,38 +56,63 @@ export function stripSqlComments(sql: string): string {
|
|
|
38
56
|
const ch = sql[i];
|
|
39
57
|
const next = i + 1 < len ? sql[i + 1] : "";
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
if (ch === "'" || ch === '"') {
|
|
60
|
+
const keepBody = ch === '"';
|
|
61
|
+
const backslashEscapes = ch === "'" && isEscapeStringPrefix(sql, i);
|
|
43
62
|
result += ch;
|
|
44
63
|
i++;
|
|
45
64
|
while (i < len) {
|
|
46
|
-
if (sql[i] === "
|
|
47
|
-
result +=
|
|
65
|
+
if (backslashEscapes && sql[i] === "\\" && i + 1 < len) {
|
|
66
|
+
if (keepBody) result += sql[i] + sql[i + 1];
|
|
48
67
|
i += 2;
|
|
49
|
-
} else if (sql[i] ===
|
|
50
|
-
result +=
|
|
68
|
+
} else if (sql[i] === ch && sql[i + 1] === ch) {
|
|
69
|
+
if (keepBody) result += ch + ch;
|
|
70
|
+
i += 2;
|
|
71
|
+
} else if (sql[i] === ch) {
|
|
72
|
+
result += ch;
|
|
51
73
|
i++;
|
|
52
74
|
break;
|
|
53
75
|
} else {
|
|
54
|
-
result += sql[i];
|
|
76
|
+
if (keepBody) result += sql[i];
|
|
55
77
|
i++;
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
continue;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
if (ch === "$") {
|
|
84
|
+
const tag = DOLLAR_QUOTE_TAG.exec(sql.slice(i))?.[0];
|
|
85
|
+
if (tag) {
|
|
86
|
+
const closing = sql.indexOf(tag, i + tag.length);
|
|
87
|
+
const stop = closing === -1 ? len : closing + tag.length;
|
|
88
|
+
result += closing === -1 ? tag : tag + tag;
|
|
89
|
+
i = stop;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Postgres block comments nest, so track depth rather than stopping at the first */.
|
|
62
95
|
if (ch === "/" && next === "*") {
|
|
96
|
+
let depth = 1;
|
|
63
97
|
i += 2;
|
|
64
|
-
while (i + 1 < len &&
|
|
65
|
-
i
|
|
98
|
+
while (i + 1 < len && depth > 0) {
|
|
99
|
+
if (sql[i] === "/" && sql[i + 1] === "*") {
|
|
100
|
+
depth++;
|
|
101
|
+
i += 2;
|
|
102
|
+
} else if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
103
|
+
depth--;
|
|
104
|
+
i += 2;
|
|
105
|
+
} else {
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (depth > 0) {
|
|
110
|
+
i = len;
|
|
66
111
|
}
|
|
67
|
-
|
|
68
|
-
result += " "; // replace comment with space
|
|
112
|
+
result += " ";
|
|
69
113
|
continue;
|
|
70
114
|
}
|
|
71
115
|
|
|
72
|
-
// Line comment -- ...
|
|
73
116
|
if (ch === "-" && next === "-") {
|
|
74
117
|
i += 2;
|
|
75
118
|
while (i < len && sql[i] !== "\n") {
|
|
@@ -86,6 +129,44 @@ export function stripSqlComments(sql: string): string {
|
|
|
86
129
|
return result;
|
|
87
130
|
}
|
|
88
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Postgres runs every statement in a simple-query batch, but MUTATION_PATTERNS only match the
|
|
134
|
+
* first one, so `select 1; delete from x` would otherwise pass the guard as a plain read.
|
|
135
|
+
* Dollar-quoted bodies containing `;` are rejected as well: this fails closed by design.
|
|
136
|
+
*/
|
|
137
|
+
export function hasMultipleStatements(sql: string): boolean {
|
|
138
|
+
const stripped = stripSqlComments(sql);
|
|
139
|
+
let index = 0;
|
|
140
|
+
|
|
141
|
+
while (index < stripped.length) {
|
|
142
|
+
const char = stripped[index];
|
|
143
|
+
|
|
144
|
+
if (char === "'" || char === '"') {
|
|
145
|
+
index++;
|
|
146
|
+
while (index < stripped.length) {
|
|
147
|
+
if (stripped[index] === char && stripped[index + 1] === char) {
|
|
148
|
+
index += 2;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (stripped[index] === char) {
|
|
152
|
+
index++;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
index++;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (char === ";" && stripped.slice(index + 1).trim().length > 0) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
index++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
89
170
|
export function isMutationQuery(sql: string): boolean {
|
|
90
171
|
const stripped = stripSqlComments(sql);
|
|
91
172
|
return MUTATION_PATTERNS.some((pattern) => pattern.test(stripped));
|