@mondaydotcomorg/z2h-cli 0.29.1 → 0.29.3
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/dist/backend/ast.d.ts +1 -3
- package/dist/backend/ast.d.ts.map +1 -1
- package/dist/backend/ast.js +0 -3
- package/dist/backend/validate.d.ts +0 -3
- package/dist/backend/validate.d.ts.map +1 -1
- package/dist/backend/validate.js +0 -8
- package/dist/esm/backend/ast.d.ts +1 -3
- package/dist/esm/backend/ast.d.ts.map +1 -1
- package/dist/esm/backend/ast.mjs +1 -3
- package/dist/esm/backend/validate.d.ts +0 -3
- package/dist/esm/backend/validate.d.ts.map +1 -1
- package/dist/esm/backend/validate.mjs +0 -8
- package/package.json +2 -2
- package/src/backend/__tests__/validate.test.ts +0 -41
- package/src/backend/ast.ts +1 -4
- package/src/backend/validate.ts +0 -9
- package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts +0 -2
- package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +0 -1
- package/dist/backend/rules/index.d.ts +0 -5
- package/dist/backend/rules/index.d.ts.map +0 -1
- package/dist/backend/rules/index.js +0 -8
- package/dist/backend/rules/snowflake-static-sql.d.ts +0 -15
- package/dist/backend/rules/snowflake-static-sql.d.ts.map +0 -1
- package/dist/backend/rules/snowflake-static-sql.js +0 -75
- package/dist/backend/rules/types.d.ts +0 -29
- package/dist/backend/rules/types.d.ts.map +0 -1
- package/dist/backend/rules/types.js +0 -1
- package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts +0 -2
- package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +0 -1
- package/dist/esm/backend/rules/index.d.ts +0 -5
- package/dist/esm/backend/rules/index.d.ts.map +0 -1
- package/dist/esm/backend/rules/index.mjs +0 -6
- package/dist/esm/backend/rules/snowflake-static-sql.d.ts +0 -15
- package/dist/esm/backend/rules/snowflake-static-sql.d.ts.map +0 -1
- package/dist/esm/backend/rules/snowflake-static-sql.mjs +0 -73
- package/dist/esm/backend/rules/types.d.ts +0 -29
- package/dist/esm/backend/rules/types.d.ts.map +0 -1
- package/dist/esm/backend/rules/types.mjs +0 -1
- package/src/backend/rules/__tests__/snowflake-static-sql.test.ts +0 -175
- package/src/backend/rules/index.ts +0 -7
- package/src/backend/rules/snowflake-static-sql.ts +0 -78
- package/src/backend/rules/types.ts +0 -30
package/dist/backend/ast.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { AnyNode, Expression,
|
|
2
|
-
/** 1-based line a node starts on; requires the program to be parsed with `locations: true`. */
|
|
3
|
-
export declare const lineOf: (node: Node) => number | undefined;
|
|
1
|
+
import type { AnyNode, Expression, Pattern, Program, VariableDeclaration } from 'acorn';
|
|
4
2
|
export interface Declaration {
|
|
5
3
|
kind: VariableDeclaration['kind'];
|
|
6
4
|
init: Expression | null | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/backend/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/backend/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAKxF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,QAAQ;IACvB,6DAA6D;IAC7D,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACzC,uFAAuF;IACvF,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AA8CD,kFAAkF;AAClF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,MAAM,EAAmD,CAAC;AAE1G,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA4C1D;AA2BD,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CA6B1F;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,CAWpF"}
|
package/dist/backend/ast.js
CHANGED
|
@@ -2,8 +2,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
|
|
3
3
|
const acornWalk = require('acorn-walk');
|
|
4
4
|
|
|
5
|
-
/** 1-based line a node starts on; requires the program to be parsed with `locations: true`. */
|
|
6
|
-
const lineOf = (node) => node.loc?.start.line;
|
|
7
5
|
/**
|
|
8
6
|
* Every identifier a binding pattern introduces, with its property path from the
|
|
9
7
|
* pattern's value. `path` is deliberately not defaulted: callers pass `[]` for
|
|
@@ -148,5 +146,4 @@ function collectApiDomains(program, paths) {
|
|
|
148
146
|
exports.collectApiDomains = collectApiDomains;
|
|
149
147
|
exports.collectBindings = collectBindings;
|
|
150
148
|
exports.createPathResolver = createPathResolver;
|
|
151
|
-
exports.lineOf = lineOf;
|
|
152
149
|
exports.patternNames = patternNames;
|
|
@@ -9,9 +9,6 @@ export interface HandlerValidation {
|
|
|
9
9
|
* top-level global, and imports/requires (which would resolve to nothing at
|
|
10
10
|
* runtime) are rejected.
|
|
11
11
|
*
|
|
12
|
-
* Once the handler is structurally sound, capability-specific rules from
|
|
13
|
-
* `./rules` run and every violation is reported at once.
|
|
14
|
-
*
|
|
15
12
|
* Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
|
|
16
13
|
* build-time DX feedback for agent-generated code, never a security boundary —
|
|
17
14
|
* the isolate has no ambient capabilities regardless of what a handler contains.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/backend/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/backend/validate.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAkCrF"}
|
package/dist/backend/validate.js
CHANGED
|
@@ -7,7 +7,6 @@ const backend_ast = require('./ast.js');
|
|
|
7
7
|
const backend_handlerDeclaration = require('./handler-declaration.js');
|
|
8
8
|
const backend_messages = require('./messages.js');
|
|
9
9
|
const backend_parse = require('./parse.js');
|
|
10
|
-
const backend_rules_index = require('./rules/index.js');
|
|
11
10
|
|
|
12
11
|
const KNOWN_API_DOMAINS = [...z2hSharedUtils.ALWAYS_ON_CHANNELS, ...z2hSharedUtils.DECLARED_INTEGRATIONS];
|
|
13
12
|
/**
|
|
@@ -17,9 +16,6 @@ const KNOWN_API_DOMAINS = [...z2hSharedUtils.ALWAYS_ON_CHANNELS, ...z2hSharedUti
|
|
|
17
16
|
* top-level global, and imports/requires (which would resolve to nothing at
|
|
18
17
|
* runtime) are rejected.
|
|
19
18
|
*
|
|
20
|
-
* Once the handler is structurally sound, capability-specific rules from
|
|
21
|
-
* `./rules` run and every violation is reported at once.
|
|
22
|
-
*
|
|
23
19
|
* Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
|
|
24
20
|
* build-time DX feedback for agent-generated code, never a security boundary —
|
|
25
21
|
* the isolate has no ambient capabilities regardless of what a handler contains.
|
|
@@ -49,10 +45,6 @@ function validateHandlerSource(file, source) {
|
|
|
49
45
|
}
|
|
50
46
|
const paths = backend_ast.createPathResolver(bindings, backend_handlerDeclaration.handlerContextParamName(declaration));
|
|
51
47
|
const apiDomains = backend_ast.collectApiDomains(program, paths);
|
|
52
|
-
const violations = backend_rules_index.HANDLER_RULES.flatMap(rule => rule.check({ file, program, bindings, paths }));
|
|
53
|
-
if (violations.length > 0) {
|
|
54
|
-
throw new Error(violations.join('\n'));
|
|
55
|
-
}
|
|
56
48
|
for (const domain of apiDomains) {
|
|
57
49
|
if (!KNOWN_API_DOMAINS.includes(domain)) {
|
|
58
50
|
util_logger.warn(`${file}: ctx.api.v1.${domain} is not a known capability (available: ${KNOWN_API_DOMAINS.join(', ')})`);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { AnyNode, Expression,
|
|
2
|
-
/** 1-based line a node starts on; requires the program to be parsed with `locations: true`. */
|
|
3
|
-
export declare const lineOf: (node: Node) => number | undefined;
|
|
1
|
+
import type { AnyNode, Expression, Pattern, Program, VariableDeclaration } from 'acorn';
|
|
4
2
|
export interface Declaration {
|
|
5
3
|
kind: VariableDeclaration['kind'];
|
|
6
4
|
init: Expression | null | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../../src/backend/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../../src/backend/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAKxF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC;;;;OAIG;IACH,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,QAAQ;IACvB,6DAA6D;IAC7D,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACzC,uFAAuF;IACvF,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AA8CD,kFAAkF;AAClF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,MAAM,EAAmD,CAAC;AAE1G,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA4C1D;AA2BD,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CA6B1F;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,CAWpF"}
|
package/dist/esm/backend/ast.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { full, simple } from 'acorn-walk';
|
|
2
2
|
|
|
3
|
-
/** 1-based line a node starts on; requires the program to be parsed with `locations: true`. */
|
|
4
|
-
const lineOf = (node) => node.loc?.start.line;
|
|
5
3
|
/**
|
|
6
4
|
* Every identifier a binding pattern introduces, with its property path from the
|
|
7
5
|
* pattern's value. `path` is deliberately not defaulted: callers pass `[]` for
|
|
@@ -143,4 +141,4 @@ function collectApiDomains(program, paths) {
|
|
|
143
141
|
return apiDomains;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
export { collectApiDomains, collectBindings, createPathResolver,
|
|
144
|
+
export { collectApiDomains, collectBindings, createPathResolver, patternNames };
|
|
@@ -9,9 +9,6 @@ export interface HandlerValidation {
|
|
|
9
9
|
* top-level global, and imports/requires (which would resolve to nothing at
|
|
10
10
|
* runtime) are rejected.
|
|
11
11
|
*
|
|
12
|
-
* Once the handler is structurally sound, capability-specific rules from
|
|
13
|
-
* `./rules` run and every violation is reported at once.
|
|
14
|
-
*
|
|
15
12
|
* Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
|
|
16
13
|
* build-time DX feedback for agent-generated code, never a security boundary —
|
|
17
14
|
* the isolate has no ambient capabilities regardless of what a handler contains.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/backend/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/backend/validate.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAkCrF"}
|
|
@@ -5,7 +5,6 @@ import { collectBindings, createPathResolver, collectApiDomains } from './ast.mj
|
|
|
5
5
|
import { findHandlerDeclaration, NEVER_A_FUNCTION, handlerContextParamName } from './handler-declaration.mjs';
|
|
6
6
|
import { messages } from './messages.mjs';
|
|
7
7
|
import { parseOrThrow } from './parse.mjs';
|
|
8
|
-
import { HANDLER_RULES } from './rules/index.mjs';
|
|
9
8
|
|
|
10
9
|
const KNOWN_API_DOMAINS = [...ALWAYS_ON_CHANNELS, ...DECLARED_INTEGRATIONS];
|
|
11
10
|
/**
|
|
@@ -15,9 +14,6 @@ const KNOWN_API_DOMAINS = [...ALWAYS_ON_CHANNELS, ...DECLARED_INTEGRATIONS];
|
|
|
15
14
|
* top-level global, and imports/requires (which would resolve to nothing at
|
|
16
15
|
* runtime) are rejected.
|
|
17
16
|
*
|
|
18
|
-
* Once the handler is structurally sound, capability-specific rules from
|
|
19
|
-
* `./rules` run and every violation is reported at once.
|
|
20
|
-
*
|
|
21
17
|
* Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
|
|
22
18
|
* build-time DX feedback for agent-generated code, never a security boundary —
|
|
23
19
|
* the isolate has no ambient capabilities regardless of what a handler contains.
|
|
@@ -47,10 +43,6 @@ function validateHandlerSource(file, source) {
|
|
|
47
43
|
}
|
|
48
44
|
const paths = createPathResolver(bindings, handlerContextParamName(declaration));
|
|
49
45
|
const apiDomains = collectApiDomains(program, paths);
|
|
50
|
-
const violations = HANDLER_RULES.flatMap(rule => rule.check({ file, program, bindings, paths }));
|
|
51
|
-
if (violations.length > 0) {
|
|
52
|
-
throw new Error(violations.join('\n'));
|
|
53
|
-
}
|
|
54
46
|
for (const domain of apiDomains) {
|
|
55
47
|
if (!KNOWN_API_DOMAINS.includes(domain)) {
|
|
56
48
|
warn(`${file}: ctx.api.v1.${domain} is not a known capability (available: ${KNOWN_API_DOMAINS.join(', ')})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mondaydotcomorg/z2h-cli",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.3",
|
|
4
4
|
"bin": "./bin/z2h-cli.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@aws-sdk/client-s3": "^3.706.0",
|
|
54
54
|
"@mondaydotcomorg/z2h-dashboard-templates": "^0.3.4",
|
|
55
|
-
"@mondaydotcomorg/z2h-shared-utils": "^0.6.
|
|
55
|
+
"@mondaydotcomorg/z2h-shared-utils": "^0.6.2",
|
|
56
56
|
"acorn": "^8.14.0",
|
|
57
57
|
"acorn-walk": "^8.3.4",
|
|
58
58
|
"chokidar": "^3.6.0",
|
|
@@ -112,47 +112,6 @@ describe('validateHandlerSource — syntax errors', () => {
|
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
describe('validateHandlerSource — capability rules', () => {
|
|
116
|
-
it('runs the rule set once the handler is structurally valid', () => {
|
|
117
|
-
expect(() => validate('async function handler(ctx) { return ctx.api.v1.snowflake.query(ctx.input.sql); }')).toThrow(
|
|
118
|
-
/snowflake\.query must receive SQL written in the handler/
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('runs snowflake SQL rules through a renamed handler parameter', () => {
|
|
123
|
-
expect(() =>
|
|
124
|
-
validate('async function handler(context) { return context.api.v1.snowflake.query(context.input.sql); }')
|
|
125
|
-
).toThrow(/snowflake\.query must receive SQL written in the handler/);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('runs snowflake SQL rules through computed channel access', () => {
|
|
129
|
-
expect(() =>
|
|
130
|
-
validate('async function handler(ctx) { return ctx.api.v1["snowflake"].query(ctx.input.sql); }')
|
|
131
|
-
).toThrow(/snowflake\.query must receive SQL written in the handler/);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('runs snowflake SQL rules through a wrapped handler with a renamed parameter', () => {
|
|
135
|
-
expect(() =>
|
|
136
|
-
validate(
|
|
137
|
-
'const handler = wrap(async (context) => context.api.v1.snowflake.query(context.input.sql)); function wrap(f) { return f; }'
|
|
138
|
-
)
|
|
139
|
-
).toThrow(/snowflake\.query must receive SQL written in the handler/);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('reports structural problems before rule violations', () => {
|
|
143
|
-
expect(() => validate('async function main(ctx) { return ctx.api.v1.snowflake.query(ctx.input.sql); }')).toThrow(
|
|
144
|
-
/must define a handler/
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('still returns api domains when every rule passes', () => {
|
|
149
|
-
expect(
|
|
150
|
-
validate('async function handler(ctx) { return ctx.api.v1.snowflake.query("select 1", { id: ctx.input.id }); }')
|
|
151
|
-
.apiDomains
|
|
152
|
-
).toEqual(['snowflake']);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
115
|
describe('validateHandlerSource — ctx.api.v1 domains', () => {
|
|
157
116
|
it('returns every domain touched, sorted and unique', () => {
|
|
158
117
|
const { apiDomains } = validate(
|
package/src/backend/ast.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { AnyNode, Expression,
|
|
1
|
+
import type { AnyNode, Expression, Pattern, Program, VariableDeclaration } from 'acorn';
|
|
2
2
|
import { full as walkFull, simple as walkSimple } from 'acorn-walk';
|
|
3
3
|
|
|
4
|
-
/** 1-based line a node starts on; requires the program to be parsed with `locations: true`. */
|
|
5
|
-
export const lineOf = (node: Node): number | undefined => node.loc?.start.line;
|
|
6
|
-
|
|
7
4
|
// ─────────── Bindings ───────────
|
|
8
5
|
|
|
9
6
|
export interface Declaration {
|
package/src/backend/validate.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { collectApiDomains, collectBindings, createPathResolver } from './ast';
|
|
|
5
5
|
import { findHandlerDeclaration, handlerContextParamName, NEVER_A_FUNCTION } from './handler-declaration';
|
|
6
6
|
import { messages } from './messages';
|
|
7
7
|
import { parseOrThrow } from './parse';
|
|
8
|
-
import { HANDLER_RULES } from './rules';
|
|
9
8
|
|
|
10
9
|
const KNOWN_API_DOMAINS: readonly string[] = [...ALWAYS_ON_CHANNELS, ...DECLARED_INTEGRATIONS];
|
|
11
10
|
|
|
@@ -21,9 +20,6 @@ export interface HandlerValidation {
|
|
|
21
20
|
* top-level global, and imports/requires (which would resolve to nothing at
|
|
22
21
|
* runtime) are rejected.
|
|
23
22
|
*
|
|
24
|
-
* Once the handler is structurally sound, capability-specific rules from
|
|
25
|
-
* `./rules` run and every violation is reported at once.
|
|
26
|
-
*
|
|
27
23
|
* Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
|
|
28
24
|
* build-time DX feedback for agent-generated code, never a security boundary —
|
|
29
25
|
* the isolate has no ambient capabilities regardless of what a handler contains.
|
|
@@ -58,11 +54,6 @@ export function validateHandlerSource(file: string, source: string): HandlerVali
|
|
|
58
54
|
const paths = createPathResolver(bindings, handlerContextParamName(declaration));
|
|
59
55
|
const apiDomains = collectApiDomains(program, paths);
|
|
60
56
|
|
|
61
|
-
const violations = HANDLER_RULES.flatMap(rule => rule.check({ file, program, bindings, paths }));
|
|
62
|
-
if (violations.length > 0) {
|
|
63
|
-
throw new Error(violations.join('\n'));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
for (const domain of apiDomains) {
|
|
67
58
|
if (!KNOWN_API_DOMAINS.includes(domain)) {
|
|
68
59
|
warn(`${file}: ctx.api.v1.${domain} is not a known capability (available: ${KNOWN_API_DOMAINS.join(', ')})`);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snowflake-static-sql.test.d.ts","sourceRoot":"","sources":["../../../../src/backend/rules/__tests__/snowflake-static-sql.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { HandlerRule } from './types';
|
|
2
|
-
export type { HandlerRule, RuleContext } from './types';
|
|
3
|
-
/** Every rule the validator runs, in order. Add new capability rules here. */
|
|
4
|
-
export declare const HANDLER_RULES: readonly HandlerRule[];
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/backend/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG3C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAExD,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAAmC,CAAC"}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
|
|
3
|
-
const backend_rules_snowflakeStaticSql = require('./snowflake-static-sql.js');
|
|
4
|
-
|
|
5
|
-
/** Every rule the validator runs, in order. Add new capability rules here. */
|
|
6
|
-
const HANDLER_RULES = [new backend_rules_snowflakeStaticSql.SnowflakeStaticSqlRule()];
|
|
7
|
-
|
|
8
|
-
exports.HANDLER_RULES = HANDLER_RULES;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { HandlerRule, RuleContext } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* `ctx.api.v1.snowflake.query(sql, params?, options?)` — the SQL text must live
|
|
4
|
-
* in the handler. The frontend chooses *which* handler to call and supplies
|
|
5
|
-
* `params` / `options`; it never ships SQL. Binding placeholders (`:name`) are
|
|
6
|
-
* how client values reach the query.
|
|
7
|
-
*
|
|
8
|
-
* The call is matched through `const` aliases of the channel
|
|
9
|
-
* (`const sf = ctx.api.v1.snowflake; sf.query(…)`), see `PathResolver`.
|
|
10
|
-
*/
|
|
11
|
-
export declare class SnowflakeStaticSqlRule implements HandlerRule {
|
|
12
|
-
readonly name = "snowflake/static-sql";
|
|
13
|
-
check({ file, program, bindings, paths }: RuleContext): string[];
|
|
14
|
-
}
|
|
15
|
-
//# sourceMappingURL=snowflake-static-sql.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snowflake-static-sql.d.ts","sourceRoot":"","sources":["../../../src/backend/rules/snowflake-static-sql.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgDxD;;;;;;;;GAQG;AACH,qBAAa,sBAAuB,YAAW,WAAW;IACxD,QAAQ,CAAC,IAAI,0BAA0B;IAEvC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,WAAW,GAAG,MAAM,EAAE;CAcjE"}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
|
|
3
|
-
const acornWalk = require('acorn-walk');
|
|
4
|
-
const backend_ast = require('../ast.js');
|
|
5
|
-
|
|
6
|
-
const SNOWFLAKE_QUERY = 'ctx.api.v1.snowflake.query';
|
|
7
|
-
const message = (file, line) => `${file}${line ? `:${line}` : ''}: ${SNOWFLAKE_QUERY} must receive SQL written in the handler — a string literal, ` +
|
|
8
|
-
`or a variable bound to one. Client-supplied values go through params (\`:name\` bindings) and options, never into the SQL text.`;
|
|
9
|
-
/**
|
|
10
|
-
* True when every possible value of `node` is text authored in this file:
|
|
11
|
-
* string literals, templates whose holes are themselves static, `+`
|
|
12
|
-
* concatenation, either branch of a conditional, and identifiers that resolve
|
|
13
|
-
* (within the file) to such values. Anything reaching outside the file —
|
|
14
|
-
* `ctx.input`, calls, member reads, destructuring — is not static.
|
|
15
|
-
*/
|
|
16
|
-
function isStaticSql(node, bindings, resolving) {
|
|
17
|
-
if (!node) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const staticChild = (child) => isStaticSql(child, bindings, resolving);
|
|
21
|
-
switch (node.type) {
|
|
22
|
-
case 'Literal':
|
|
23
|
-
return typeof node.value === 'string';
|
|
24
|
-
case 'TemplateLiteral':
|
|
25
|
-
return node.expressions.every(staticChild);
|
|
26
|
-
case 'BinaryExpression':
|
|
27
|
-
return node.operator === '+' && staticChild(node.left) && staticChild(node.right);
|
|
28
|
-
case 'ConditionalExpression':
|
|
29
|
-
return staticChild(node.consequent) && staticChild(node.alternate);
|
|
30
|
-
case 'LogicalExpression':
|
|
31
|
-
return staticChild(node.left) && staticChild(node.right);
|
|
32
|
-
case 'Identifier': {
|
|
33
|
-
const { name } = node;
|
|
34
|
-
if (bindings.unpinnable.has(name) || resolving.has(name)) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
const decls = bindings.declarations.get(name);
|
|
38
|
-
if (!decls || decls.length === 0) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
const next = new Set(resolving).add(name);
|
|
42
|
-
return decls.every(d => d.path?.length === 0 && isStaticSql(d.init, bindings, next));
|
|
43
|
-
}
|
|
44
|
-
default:
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* `ctx.api.v1.snowflake.query(sql, params?, options?)` — the SQL text must live
|
|
50
|
-
* in the handler. The frontend chooses *which* handler to call and supplies
|
|
51
|
-
* `params` / `options`; it never ships SQL. Binding placeholders (`:name`) are
|
|
52
|
-
* how client values reach the query.
|
|
53
|
-
*
|
|
54
|
-
* The call is matched through `const` aliases of the channel
|
|
55
|
-
* (`const sf = ctx.api.v1.snowflake; sf.query(…)`), see `PathResolver`.
|
|
56
|
-
*/
|
|
57
|
-
class SnowflakeStaticSqlRule {
|
|
58
|
-
name = 'snowflake/static-sql';
|
|
59
|
-
check({ file, program, bindings, paths }) {
|
|
60
|
-
const violations = [];
|
|
61
|
-
acornWalk.simple(program, {
|
|
62
|
-
CallExpression(node) {
|
|
63
|
-
if (paths.pathOf(node.callee) !== SNOWFLAKE_QUERY) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (!isStaticSql(node.arguments[0], bindings, new Set())) {
|
|
67
|
-
violations.push(message(file, backend_ast.lineOf(node)));
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
return violations;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
exports.SnowflakeStaticSqlRule = SnowflakeStaticSqlRule;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Program } from 'acorn';
|
|
2
|
-
import type { Bindings, PathResolver } from '../ast';
|
|
3
|
-
export interface RuleContext {
|
|
4
|
-
/** Handler file name, used to prefix messages. */
|
|
5
|
-
file: string;
|
|
6
|
-
/** Parsed with `locations: true`, so rules can point at a line. */
|
|
7
|
-
program: Program;
|
|
8
|
-
/** Every variable binding in the file — see `collectBindings`. */
|
|
9
|
-
bindings: Bindings;
|
|
10
|
-
/** Resolves member chains through `const` aliases — see `createPathResolver`. */
|
|
11
|
-
paths: PathResolver;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* A capability-specific check that runs once the handler is structurally valid
|
|
15
|
-
* (parses as a script, defines `handler`, imports nothing).
|
|
16
|
-
*
|
|
17
|
-
* Rules return human-readable violations rather than throwing so the validator
|
|
18
|
-
* can report every problem in one pass — agent-generated handlers are fixed in
|
|
19
|
-
* a loop, and one round-trip per violation is slow.
|
|
20
|
-
*
|
|
21
|
-
* Implement as a class (see `SnowflakeStaticSqlRule`) and add an instance to
|
|
22
|
-
* `HANDLER_RULES` in `./index.ts` — keeps every rule the same shape to read.
|
|
23
|
-
*/
|
|
24
|
-
export interface HandlerRule {
|
|
25
|
-
/** Stable identifier, e.g. `snowflake/static-sql`. */
|
|
26
|
-
name: string;
|
|
27
|
-
check(ctx: RuleContext): string[];
|
|
28
|
-
}
|
|
29
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/backend/rules/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,EAAE,OAAO,CAAC;IACjB,kEAAkE;IAClE,QAAQ,EAAE,QAAQ,CAAC;IACnB,iFAAiF;IACjF,KAAK,EAAE,YAAY,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,EAAE,CAAC;CACnC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snowflake-static-sql.test.d.ts","sourceRoot":"","sources":["../../../../../src/backend/rules/__tests__/snowflake-static-sql.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { HandlerRule } from './types';
|
|
2
|
-
export type { HandlerRule, RuleContext } from './types';
|
|
3
|
-
/** Every rule the validator runs, in order. Add new capability rules here. */
|
|
4
|
-
export declare const HANDLER_RULES: readonly HandlerRule[];
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/backend/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG3C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAExD,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAAmC,CAAC"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { HandlerRule, RuleContext } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* `ctx.api.v1.snowflake.query(sql, params?, options?)` — the SQL text must live
|
|
4
|
-
* in the handler. The frontend chooses *which* handler to call and supplies
|
|
5
|
-
* `params` / `options`; it never ships SQL. Binding placeholders (`:name`) are
|
|
6
|
-
* how client values reach the query.
|
|
7
|
-
*
|
|
8
|
-
* The call is matched through `const` aliases of the channel
|
|
9
|
-
* (`const sf = ctx.api.v1.snowflake; sf.query(…)`), see `PathResolver`.
|
|
10
|
-
*/
|
|
11
|
-
export declare class SnowflakeStaticSqlRule implements HandlerRule {
|
|
12
|
-
readonly name = "snowflake/static-sql";
|
|
13
|
-
check({ file, program, bindings, paths }: RuleContext): string[];
|
|
14
|
-
}
|
|
15
|
-
//# sourceMappingURL=snowflake-static-sql.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snowflake-static-sql.d.ts","sourceRoot":"","sources":["../../../../src/backend/rules/snowflake-static-sql.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgDxD;;;;;;;;GAQG;AACH,qBAAa,sBAAuB,YAAW,WAAW;IACxD,QAAQ,CAAC,IAAI,0BAA0B;IAEvC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,WAAW,GAAG,MAAM,EAAE;CAcjE"}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { simple } from 'acorn-walk';
|
|
2
|
-
import { lineOf } from '../ast.mjs';
|
|
3
|
-
|
|
4
|
-
const SNOWFLAKE_QUERY = 'ctx.api.v1.snowflake.query';
|
|
5
|
-
const message = (file, line) => `${file}${line ? `:${line}` : ''}: ${SNOWFLAKE_QUERY} must receive SQL written in the handler — a string literal, ` +
|
|
6
|
-
`or a variable bound to one. Client-supplied values go through params (\`:name\` bindings) and options, never into the SQL text.`;
|
|
7
|
-
/**
|
|
8
|
-
* True when every possible value of `node` is text authored in this file:
|
|
9
|
-
* string literals, templates whose holes are themselves static, `+`
|
|
10
|
-
* concatenation, either branch of a conditional, and identifiers that resolve
|
|
11
|
-
* (within the file) to such values. Anything reaching outside the file —
|
|
12
|
-
* `ctx.input`, calls, member reads, destructuring — is not static.
|
|
13
|
-
*/
|
|
14
|
-
function isStaticSql(node, bindings, resolving) {
|
|
15
|
-
if (!node) {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
const staticChild = (child) => isStaticSql(child, bindings, resolving);
|
|
19
|
-
switch (node.type) {
|
|
20
|
-
case 'Literal':
|
|
21
|
-
return typeof node.value === 'string';
|
|
22
|
-
case 'TemplateLiteral':
|
|
23
|
-
return node.expressions.every(staticChild);
|
|
24
|
-
case 'BinaryExpression':
|
|
25
|
-
return node.operator === '+' && staticChild(node.left) && staticChild(node.right);
|
|
26
|
-
case 'ConditionalExpression':
|
|
27
|
-
return staticChild(node.consequent) && staticChild(node.alternate);
|
|
28
|
-
case 'LogicalExpression':
|
|
29
|
-
return staticChild(node.left) && staticChild(node.right);
|
|
30
|
-
case 'Identifier': {
|
|
31
|
-
const { name } = node;
|
|
32
|
-
if (bindings.unpinnable.has(name) || resolving.has(name)) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
const decls = bindings.declarations.get(name);
|
|
36
|
-
if (!decls || decls.length === 0) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
const next = new Set(resolving).add(name);
|
|
40
|
-
return decls.every(d => d.path?.length === 0 && isStaticSql(d.init, bindings, next));
|
|
41
|
-
}
|
|
42
|
-
default:
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* `ctx.api.v1.snowflake.query(sql, params?, options?)` — the SQL text must live
|
|
48
|
-
* in the handler. The frontend chooses *which* handler to call and supplies
|
|
49
|
-
* `params` / `options`; it never ships SQL. Binding placeholders (`:name`) are
|
|
50
|
-
* how client values reach the query.
|
|
51
|
-
*
|
|
52
|
-
* The call is matched through `const` aliases of the channel
|
|
53
|
-
* (`const sf = ctx.api.v1.snowflake; sf.query(…)`), see `PathResolver`.
|
|
54
|
-
*/
|
|
55
|
-
class SnowflakeStaticSqlRule {
|
|
56
|
-
name = 'snowflake/static-sql';
|
|
57
|
-
check({ file, program, bindings, paths }) {
|
|
58
|
-
const violations = [];
|
|
59
|
-
simple(program, {
|
|
60
|
-
CallExpression(node) {
|
|
61
|
-
if (paths.pathOf(node.callee) !== SNOWFLAKE_QUERY) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (!isStaticSql(node.arguments[0], bindings, new Set())) {
|
|
65
|
-
violations.push(message(file, lineOf(node)));
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
return violations;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export { SnowflakeStaticSqlRule };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Program } from 'acorn';
|
|
2
|
-
import type { Bindings, PathResolver } from '../ast';
|
|
3
|
-
export interface RuleContext {
|
|
4
|
-
/** Handler file name, used to prefix messages. */
|
|
5
|
-
file: string;
|
|
6
|
-
/** Parsed with `locations: true`, so rules can point at a line. */
|
|
7
|
-
program: Program;
|
|
8
|
-
/** Every variable binding in the file — see `collectBindings`. */
|
|
9
|
-
bindings: Bindings;
|
|
10
|
-
/** Resolves member chains through `const` aliases — see `createPathResolver`. */
|
|
11
|
-
paths: PathResolver;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* A capability-specific check that runs once the handler is structurally valid
|
|
15
|
-
* (parses as a script, defines `handler`, imports nothing).
|
|
16
|
-
*
|
|
17
|
-
* Rules return human-readable violations rather than throwing so the validator
|
|
18
|
-
* can report every problem in one pass — agent-generated handlers are fixed in
|
|
19
|
-
* a loop, and one round-trip per violation is slow.
|
|
20
|
-
*
|
|
21
|
-
* Implement as a class (see `SnowflakeStaticSqlRule`) and add an instance to
|
|
22
|
-
* `HANDLER_RULES` in `./index.ts` — keeps every rule the same shape to read.
|
|
23
|
-
*/
|
|
24
|
-
export interface HandlerRule {
|
|
25
|
-
/** Stable identifier, e.g. `snowflake/static-sql`. */
|
|
26
|
-
name: string;
|
|
27
|
-
check(ctx: RuleContext): string[];
|
|
28
|
-
}
|
|
29
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/backend/rules/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,EAAE,OAAO,CAAC;IACjB,kEAAkE;IAClE,QAAQ,EAAE,QAAQ,CAAC;IACnB,iFAAiF;IACjF,KAAK,EAAE,YAAY,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,EAAE,CAAC;CACnC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { parse } from 'acorn';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { collectBindings, createPathResolver } from '../../ast';
|
|
4
|
-
import { findHandlerDeclaration, handlerContextParamName } from '../../handler-declaration';
|
|
5
|
-
import { SnowflakeStaticSqlRule } from '../snowflake-static-sql';
|
|
6
|
-
|
|
7
|
-
const rule = new SnowflakeStaticSqlRule();
|
|
8
|
-
|
|
9
|
-
const check = (source: string) => {
|
|
10
|
-
const program = parse(source, { ecmaVersion: 'latest', sourceType: 'script', locations: true });
|
|
11
|
-
const bindings = collectBindings(program);
|
|
12
|
-
const declaration = findHandlerDeclaration(program.body);
|
|
13
|
-
return rule.check({
|
|
14
|
-
file: 'h.js',
|
|
15
|
-
program,
|
|
16
|
-
bindings,
|
|
17
|
-
paths: createPathResolver(bindings, declaration && handlerContextParamName(declaration)),
|
|
18
|
-
});
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const inHandler = (body: string) => `async function handler(ctx) {\n${body}\n}`;
|
|
22
|
-
|
|
23
|
-
describe('snowflake/static-sql — accepted', () => {
|
|
24
|
-
it.each([
|
|
25
|
-
['a string literal', 'return ctx.api.v1.snowflake.query("select 1");'],
|
|
26
|
-
['a template literal without holes', 'return ctx.api.v1.snowflake.query(`select 1`);'],
|
|
27
|
-
[
|
|
28
|
-
'a template literal whose holes are static',
|
|
29
|
-
'const t = "items"; return ctx.api.v1.snowflake.query(`select * from ${t}`);',
|
|
30
|
-
],
|
|
31
|
-
['a local const', 'const sql = "select 1"; return ctx.api.v1.snowflake.query(sql);'],
|
|
32
|
-
[
|
|
33
|
-
'a var declared before use',
|
|
34
|
-
'var sql = "select 1"; return ctx.api.v1.snowflake.query(sql, undefined, { identifier: "x" });',
|
|
35
|
-
],
|
|
36
|
-
['concatenated literals', 'return ctx.api.v1.snowflake.query("select 1" + " from dual");'],
|
|
37
|
-
[
|
|
38
|
-
'a variable chain',
|
|
39
|
-
'const base = "select 1"; const sql = base + " limit 1"; return ctx.api.v1.snowflake.query(sql);',
|
|
40
|
-
],
|
|
41
|
-
[
|
|
42
|
-
'a conditional between two static queries',
|
|
43
|
-
'const sql = ctx.input.all ? "select 1" : "select 2"; return ctx.api.v1.snowflake.query(sql);',
|
|
44
|
-
],
|
|
45
|
-
[
|
|
46
|
-
'a nullish fallback between static queries',
|
|
47
|
-
'return ctx.api.v1.snowflake.query(A ?? "select 1"); const A = "select 2";',
|
|
48
|
-
],
|
|
49
|
-
[
|
|
50
|
-
'client values in params and options',
|
|
51
|
-
'return ctx.api.v1.snowflake.query("select :id", { id: ctx.input.id }, { timeoutMs: ctx.input.t });',
|
|
52
|
-
],
|
|
53
|
-
['optional chaining on the channel', 'return ctx?.api?.v1?.snowflake?.query("select 1");'],
|
|
54
|
-
])('accepts %s', (_label, body) => {
|
|
55
|
-
expect(check(inHandler(body))).toEqual([]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('accepts a top-level const shared by several calls', () => {
|
|
59
|
-
expect(
|
|
60
|
-
check(
|
|
61
|
-
`const SQL = "select 1";\n${inHandler(
|
|
62
|
-
'await ctx.api.v1.snowflake.query(SQL); return ctx.api.v1.snowflake.query(SQL);'
|
|
63
|
-
)}`
|
|
64
|
-
)
|
|
65
|
-
).toEqual([]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('ignores other snowflake members and other channels', () => {
|
|
69
|
-
expect(
|
|
70
|
-
check(inHandler('ctx.api.v1.snowflake.other(ctx.input.sql); return ctx.api.v1.monday.query(ctx.input.q);'))
|
|
71
|
-
).toEqual([]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('accepts static SQL through a const alias of the channel', () => {
|
|
75
|
-
expect(
|
|
76
|
-
check(inHandler('const sf = ctx.api.v1.snowflake; return sf.query("select 1", { id: ctx.input.id });'))
|
|
77
|
-
).toEqual([]);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('snowflake/static-sql — channel aliases', () => {
|
|
82
|
-
it.each([
|
|
83
|
-
['a const alias of the channel', 'const sf = ctx.api.v1.snowflake; return sf.query(ctx.input.sql);'],
|
|
84
|
-
['a const alias of the api root', 'const api = ctx.api.v1; return api.snowflake.query(ctx.input.sql);'],
|
|
85
|
-
['a chain of const aliases', 'const api = ctx.api; const sf = api.v1.snowflake; return sf.query(ctx.input.sql);'],
|
|
86
|
-
['a destructured channel', 'const { snowflake } = ctx.api.v1; return snowflake.query(ctx.input.sql);'],
|
|
87
|
-
['a renamed destructured channel', 'const { snowflake: sf } = ctx.api.v1; return sf.query(ctx.input.sql);'],
|
|
88
|
-
['a nested destructuring', 'const { api: { v1: { snowflake } } } = ctx; return snowflake.query(ctx.input.sql);'],
|
|
89
|
-
['a destructured method', 'const { query } = ctx.api.v1.snowflake; return query(ctx.input.sql);'],
|
|
90
|
-
['an optional-chained alias', 'const sf = ctx?.api?.v1?.snowflake; return sf.query(ctx.input.sql);'],
|
|
91
|
-
['a computed string-literal channel', 'return ctx.api.v1["snowflake"].query(ctx.input.sql);'],
|
|
92
|
-
['a computed alias', 'const sf = ctx.api.v1["snowflake"]; return sf.query(ctx.input.sql);'],
|
|
93
|
-
])('sees through %s', (_label, body) => {
|
|
94
|
-
expect(check(inHandler(body))).toHaveLength(1);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('follows a top-level const alias into the handler', () => {
|
|
98
|
-
expect(check(`const sf = ctx.api.v1.snowflake;\n${inHandler('return sf.query(ctx.input.sql);')}`)).toHaveLength(1);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('sees through a renamed handler parameter', () => {
|
|
102
|
-
expect(
|
|
103
|
-
check('async function handler(context) { return context.api.v1.snowflake.query(context.input.sql); }')
|
|
104
|
-
).toHaveLength(1);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it.each([
|
|
108
|
-
['a let alias', 'let sf = ctx.api.v1.snowflake; return sf.query(ctx.input.sql);'],
|
|
109
|
-
['a reassigned alias', 'var sf = ctx.api.v1.snowflake; sf = other; return sf.query(ctx.input.sql);'],
|
|
110
|
-
[
|
|
111
|
-
'an alias shadowed by a parameter',
|
|
112
|
-
'const sf = ctx.api.v1.snowflake; const run = (sf) => sf.query(ctx.input.sql); return run(fake);',
|
|
113
|
-
],
|
|
114
|
-
[
|
|
115
|
-
'an alias declared twice',
|
|
116
|
-
'const sf = ctx.api.v1.snowflake; { const sf = fake; return sf.query(ctx.input.sql); }',
|
|
117
|
-
],
|
|
118
|
-
['an alias with a default value', 'const { snowflake: sf = fake } = ctx.api.v1; return sf.query(ctx.input.sql);'],
|
|
119
|
-
])('does not follow %s — the binding cannot be pinned', (_label, body) => {
|
|
120
|
-
expect(check(inHandler(body))).toEqual([]);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('snowflake/static-sql — rejected', () => {
|
|
125
|
-
it.each([
|
|
126
|
-
['ctx.input directly', 'return ctx.api.v1.snowflake.query(ctx.input.sql);'],
|
|
127
|
-
['a variable bound to ctx.input', 'const sql = ctx.input.sql; return ctx.api.v1.snowflake.query(sql);'],
|
|
128
|
-
[
|
|
129
|
-
'a template literal with a ctx.input hole',
|
|
130
|
-
'return ctx.api.v1.snowflake.query(`select * from ${ctx.input.table}`);',
|
|
131
|
-
],
|
|
132
|
-
['concatenation with ctx.input', 'return ctx.api.v1.snowflake.query("select * from " + ctx.input.table);'],
|
|
133
|
-
[
|
|
134
|
-
'a conditional with one dynamic branch',
|
|
135
|
-
'return ctx.api.v1.snowflake.query(ctx.input.x ? "select 1" : ctx.input.sql);',
|
|
136
|
-
],
|
|
137
|
-
['a call result', 'return ctx.api.v1.snowflake.query(buildSql(ctx.input));'],
|
|
138
|
-
['a destructured binding', 'const { sql } = ctx.input; return ctx.api.v1.snowflake.query(sql);'],
|
|
139
|
-
['a reassigned variable', 'let sql = "select 1"; sql = ctx.input.sql; return ctx.api.v1.snowflake.query(sql);'],
|
|
140
|
-
[
|
|
141
|
-
'a compound-assigned variable',
|
|
142
|
-
'let sql = "select 1"; sql += ctx.input.where; return ctx.api.v1.snowflake.query(sql);',
|
|
143
|
-
],
|
|
144
|
-
['a function parameter', 'const run = (sql) => ctx.api.v1.snowflake.query(sql); return run(ctx.input.sql);'],
|
|
145
|
-
['an undeclared identifier', 'return ctx.api.v1.snowflake.query(sql);'],
|
|
146
|
-
['a spread argument', 'return ctx.api.v1.snowflake.query(...ctx.input.args);'],
|
|
147
|
-
['a non-string literal', 'return ctx.api.v1.snowflake.query(1);'],
|
|
148
|
-
['no arguments', 'return ctx.api.v1.snowflake.query();'],
|
|
149
|
-
])('rejects %s', (_label, body) => {
|
|
150
|
-
const violations = check(inHandler(body));
|
|
151
|
-
expect(violations).toHaveLength(1);
|
|
152
|
-
expect(violations[0]).toMatch(/^h\.js:2: ctx\.api\.v1\.snowflake\.query must receive SQL written in the handler/);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('rejects a variable shadowed by a parameter anywhere in the file', () => {
|
|
156
|
-
expect(
|
|
157
|
-
check(
|
|
158
|
-
`const sql = "select 1";\nfunction run(sql) { return ctx.api.v1.snowflake.query(sql); }\n${inHandler(
|
|
159
|
-
'return run(ctx.input.sql);'
|
|
160
|
-
)}`
|
|
161
|
-
)
|
|
162
|
-
).toHaveLength(1);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('does not loop on self-referential bindings', () => {
|
|
166
|
-
expect(check(inHandler('const a = b; const b = a; return ctx.api.v1.snowflake.query(a);'))).toHaveLength(1);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('reports every offending call with its own line', () => {
|
|
170
|
-
const violations = check(
|
|
171
|
-
inHandler('await ctx.api.v1.snowflake.query(ctx.input.a);\nreturn ctx.api.v1.snowflake.query(ctx.input.b);')
|
|
172
|
-
);
|
|
173
|
-
expect(violations.map(v => v.slice(0, 6))).toEqual(['h.js:2', 'h.js:3']);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { HandlerRule } from './types';
|
|
2
|
-
import { SnowflakeStaticSqlRule } from './snowflake-static-sql';
|
|
3
|
-
|
|
4
|
-
export type { HandlerRule, RuleContext } from './types';
|
|
5
|
-
|
|
6
|
-
/** Every rule the validator runs, in order. Add new capability rules here. */
|
|
7
|
-
export const HANDLER_RULES: readonly HandlerRule[] = [new SnowflakeStaticSqlRule()];
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { AnyNode } from 'acorn';
|
|
2
|
-
import { simple as walkSimple } from 'acorn-walk';
|
|
3
|
-
import { type Bindings, lineOf } from '../ast';
|
|
4
|
-
import type { HandlerRule, RuleContext } from './types';
|
|
5
|
-
|
|
6
|
-
const SNOWFLAKE_QUERY = 'ctx.api.v1.snowflake.query';
|
|
7
|
-
|
|
8
|
-
const message = (file: string, line: number | undefined) =>
|
|
9
|
-
`${file}${line ? `:${line}` : ''}: ${SNOWFLAKE_QUERY} must receive SQL written in the handler — a string literal, ` +
|
|
10
|
-
`or a variable bound to one. Client-supplied values go through params (\`:name\` bindings) and options, never into the SQL text.`;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* True when every possible value of `node` is text authored in this file:
|
|
14
|
-
* string literals, templates whose holes are themselves static, `+`
|
|
15
|
-
* concatenation, either branch of a conditional, and identifiers that resolve
|
|
16
|
-
* (within the file) to such values. Anything reaching outside the file —
|
|
17
|
-
* `ctx.input`, calls, member reads, destructuring — is not static.
|
|
18
|
-
*/
|
|
19
|
-
function isStaticSql(node: AnyNode | null | undefined, bindings: Bindings, resolving: ReadonlySet<string>): boolean {
|
|
20
|
-
if (!node) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
const staticChild = (child: AnyNode | null | undefined) => isStaticSql(child, bindings, resolving);
|
|
24
|
-
switch (node.type) {
|
|
25
|
-
case 'Literal':
|
|
26
|
-
return typeof node.value === 'string';
|
|
27
|
-
case 'TemplateLiteral':
|
|
28
|
-
return node.expressions.every(staticChild);
|
|
29
|
-
case 'BinaryExpression':
|
|
30
|
-
return node.operator === '+' && staticChild(node.left) && staticChild(node.right);
|
|
31
|
-
case 'ConditionalExpression':
|
|
32
|
-
return staticChild(node.consequent) && staticChild(node.alternate);
|
|
33
|
-
case 'LogicalExpression':
|
|
34
|
-
return staticChild(node.left) && staticChild(node.right);
|
|
35
|
-
case 'Identifier': {
|
|
36
|
-
const { name } = node;
|
|
37
|
-
if (bindings.unpinnable.has(name) || resolving.has(name)) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
const decls = bindings.declarations.get(name);
|
|
41
|
-
if (!decls || decls.length === 0) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
const next = new Set(resolving).add(name);
|
|
45
|
-
return decls.every(d => d.path?.length === 0 && isStaticSql(d.init, bindings, next));
|
|
46
|
-
}
|
|
47
|
-
default:
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* `ctx.api.v1.snowflake.query(sql, params?, options?)` — the SQL text must live
|
|
54
|
-
* in the handler. The frontend chooses *which* handler to call and supplies
|
|
55
|
-
* `params` / `options`; it never ships SQL. Binding placeholders (`:name`) are
|
|
56
|
-
* how client values reach the query.
|
|
57
|
-
*
|
|
58
|
-
* The call is matched through `const` aliases of the channel
|
|
59
|
-
* (`const sf = ctx.api.v1.snowflake; sf.query(…)`), see `PathResolver`.
|
|
60
|
-
*/
|
|
61
|
-
export class SnowflakeStaticSqlRule implements HandlerRule {
|
|
62
|
-
readonly name = 'snowflake/static-sql';
|
|
63
|
-
|
|
64
|
-
check({ file, program, bindings, paths }: RuleContext): string[] {
|
|
65
|
-
const violations: string[] = [];
|
|
66
|
-
walkSimple(program, {
|
|
67
|
-
CallExpression(node) {
|
|
68
|
-
if (paths.pathOf(node.callee) !== SNOWFLAKE_QUERY) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (!isStaticSql(node.arguments[0], bindings, new Set())) {
|
|
72
|
-
violations.push(message(file, lineOf(node)));
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
return violations;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Program } from 'acorn';
|
|
2
|
-
import type { Bindings, PathResolver } from '../ast';
|
|
3
|
-
|
|
4
|
-
export interface RuleContext {
|
|
5
|
-
/** Handler file name, used to prefix messages. */
|
|
6
|
-
file: string;
|
|
7
|
-
/** Parsed with `locations: true`, so rules can point at a line. */
|
|
8
|
-
program: Program;
|
|
9
|
-
/** Every variable binding in the file — see `collectBindings`. */
|
|
10
|
-
bindings: Bindings;
|
|
11
|
-
/** Resolves member chains through `const` aliases — see `createPathResolver`. */
|
|
12
|
-
paths: PathResolver;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A capability-specific check that runs once the handler is structurally valid
|
|
17
|
-
* (parses as a script, defines `handler`, imports nothing).
|
|
18
|
-
*
|
|
19
|
-
* Rules return human-readable violations rather than throwing so the validator
|
|
20
|
-
* can report every problem in one pass — agent-generated handlers are fixed in
|
|
21
|
-
* a loop, and one round-trip per violation is slow.
|
|
22
|
-
*
|
|
23
|
-
* Implement as a class (see `SnowflakeStaticSqlRule`) and add an instance to
|
|
24
|
-
* `HANDLER_RULES` in `./index.ts` — keeps every rule the same shape to read.
|
|
25
|
-
*/
|
|
26
|
-
export interface HandlerRule {
|
|
27
|
-
/** Stable identifier, e.g. `snowflake/static-sql`. */
|
|
28
|
-
name: string;
|
|
29
|
-
check(ctx: RuleContext): string[];
|
|
30
|
-
}
|