@mondaydotcomorg/z2h-cli 0.29.0 → 0.29.2

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.
Files changed (42) hide show
  1. package/dist/backend/ast.d.ts +1 -3
  2. package/dist/backend/ast.d.ts.map +1 -1
  3. package/dist/backend/ast.js +0 -3
  4. package/dist/backend/validate.d.ts +0 -3
  5. package/dist/backend/validate.d.ts.map +1 -1
  6. package/dist/backend/validate.js +0 -8
  7. package/dist/esm/backend/ast.d.ts +1 -3
  8. package/dist/esm/backend/ast.d.ts.map +1 -1
  9. package/dist/esm/backend/ast.mjs +1 -3
  10. package/dist/esm/backend/validate.d.ts +0 -3
  11. package/dist/esm/backend/validate.d.ts.map +1 -1
  12. package/dist/esm/backend/validate.mjs +0 -8
  13. package/package.json +2 -2
  14. package/src/backend/__tests__/validate.test.ts +0 -41
  15. package/src/backend/ast.ts +1 -4
  16. package/src/backend/validate.ts +0 -9
  17. package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts +0 -2
  18. package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +0 -1
  19. package/dist/backend/rules/index.d.ts +0 -5
  20. package/dist/backend/rules/index.d.ts.map +0 -1
  21. package/dist/backend/rules/index.js +0 -8
  22. package/dist/backend/rules/snowflake-static-sql.d.ts +0 -15
  23. package/dist/backend/rules/snowflake-static-sql.d.ts.map +0 -1
  24. package/dist/backend/rules/snowflake-static-sql.js +0 -75
  25. package/dist/backend/rules/types.d.ts +0 -29
  26. package/dist/backend/rules/types.d.ts.map +0 -1
  27. package/dist/backend/rules/types.js +0 -1
  28. package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts +0 -2
  29. package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +0 -1
  30. package/dist/esm/backend/rules/index.d.ts +0 -5
  31. package/dist/esm/backend/rules/index.d.ts.map +0 -1
  32. package/dist/esm/backend/rules/index.mjs +0 -6
  33. package/dist/esm/backend/rules/snowflake-static-sql.d.ts +0 -15
  34. package/dist/esm/backend/rules/snowflake-static-sql.d.ts.map +0 -1
  35. package/dist/esm/backend/rules/snowflake-static-sql.mjs +0 -73
  36. package/dist/esm/backend/rules/types.d.ts +0 -29
  37. package/dist/esm/backend/rules/types.d.ts.map +0 -1
  38. package/dist/esm/backend/rules/types.mjs +0 -1
  39. package/src/backend/rules/__tests__/snowflake-static-sql.test.ts +0 -175
  40. package/src/backend/rules/index.ts +0 -7
  41. package/src/backend/rules/snowflake-static-sql.ts +0 -78
  42. package/src/backend/rules/types.ts +0 -30
@@ -1,6 +1,4 @@
1
- import type { AnyNode, Expression, Node, Pattern, Program, VariableDeclaration } from 'acorn';
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,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAG9F,+FAA+F;AAC/F,eAAO,MAAM,MAAM,GAAI,MAAM,IAAI,KAAG,MAAM,GAAG,SAAiC,CAAC;AAI/E,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"}
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"}
@@ -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":"AAWA,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAuCrF"}
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"}
@@ -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, Node, Pattern, Program, VariableDeclaration } from 'acorn';
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,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAG9F,+FAA+F;AAC/F,eAAO,MAAM,MAAM,GAAI,MAAM,IAAI,KAAG,MAAM,GAAG,SAAiC,CAAC;AAI/E,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"}
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"}
@@ -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, lineOf, patternNames };
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":"AAWA,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAuCrF"}
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.0",
3
+ "version": "0.29.2",
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.0",
55
+ "@mondaydotcomorg/z2h-shared-utils": "^0.6.1",
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(
@@ -1,9 +1,6 @@
1
- import type { AnyNode, Expression, Node, Pattern, Program, VariableDeclaration } from 'acorn';
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 {
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=snowflake-static-sql.test.d.ts.map
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=snowflake-static-sql.test.d.ts.map
@@ -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,6 +0,0 @@
1
- import { SnowflakeStaticSqlRule } from './snowflake-static-sql.mjs';
2
-
3
- /** Every rule the validator runs, in order. Add new capability rules here. */
4
- const HANDLER_RULES = [new SnowflakeStaticSqlRule()];
5
-
6
- export { 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,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
- }