@mondaydotcomorg/z2h-cli 0.27.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/backend/__tests__/ast.test.d.ts +2 -0
  2. package/dist/backend/__tests__/ast.test.d.ts.map +1 -0
  3. package/dist/backend/__tests__/parse.test.d.ts +2 -0
  4. package/dist/backend/__tests__/parse.test.d.ts.map +1 -0
  5. package/dist/backend/__tests__/validate.test.d.ts +2 -0
  6. package/dist/backend/__tests__/validate.test.d.ts.map +1 -0
  7. package/dist/backend/ast.d.ts +41 -0
  8. package/dist/backend/ast.d.ts.map +1 -0
  9. package/dist/backend/ast.js +152 -0
  10. package/dist/backend/detect-integrations.d.ts +8 -4
  11. package/dist/backend/detect-integrations.d.ts.map +1 -1
  12. package/dist/backend/detect-integrations.js +12 -15
  13. package/dist/backend/handler-declaration.d.ts +17 -0
  14. package/dist/backend/handler-declaration.d.ts.map +1 -0
  15. package/dist/backend/handler-declaration.js +83 -0
  16. package/dist/backend/handlers.d.ts +2 -0
  17. package/dist/backend/handlers.d.ts.map +1 -1
  18. package/dist/backend/handlers.js +7 -36
  19. package/dist/backend/messages.d.ts +10 -0
  20. package/dist/backend/messages.d.ts.map +1 -0
  21. package/dist/backend/messages.js +14 -0
  22. package/dist/backend/parse.d.ts +11 -0
  23. package/dist/backend/parse.d.ts.map +1 -0
  24. package/dist/backend/parse.js +66 -0
  25. package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts +2 -0
  26. package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +1 -0
  27. package/dist/backend/rules/index.d.ts +5 -0
  28. package/dist/backend/rules/index.d.ts.map +1 -0
  29. package/dist/backend/rules/index.js +8 -0
  30. package/dist/backend/rules/snowflake-static-sql.d.ts +15 -0
  31. package/dist/backend/rules/snowflake-static-sql.d.ts.map +1 -0
  32. package/dist/backend/rules/snowflake-static-sql.js +75 -0
  33. package/dist/backend/rules/types.d.ts +29 -0
  34. package/dist/backend/rules/types.d.ts.map +1 -0
  35. package/dist/backend/rules/types.js +1 -0
  36. package/dist/backend/validate.d.ts +23 -0
  37. package/dist/backend/validate.d.ts.map +1 -0
  38. package/dist/backend/validate.js +64 -0
  39. package/dist/commands/__tests__/backend-validate.test.d.ts +2 -0
  40. package/dist/commands/__tests__/backend-validate.test.d.ts.map +1 -0
  41. package/dist/commands/backend.d.ts +6 -0
  42. package/dist/commands/backend.d.ts.map +1 -1
  43. package/dist/commands/backend.js +22 -2
  44. package/dist/commands/create.d.ts.map +1 -1
  45. package/dist/commands/create.js +10 -3
  46. package/dist/commands/generate.d.ts.map +1 -1
  47. package/dist/commands/generate.js +2 -1
  48. package/dist/constants.d.ts +1 -2
  49. package/dist/constants.d.ts.map +1 -1
  50. package/dist/constants.js +10 -6
  51. package/dist/esm/backend/__tests__/ast.test.d.ts +2 -0
  52. package/dist/esm/backend/__tests__/ast.test.d.ts.map +1 -0
  53. package/dist/esm/backend/__tests__/parse.test.d.ts +2 -0
  54. package/dist/esm/backend/__tests__/parse.test.d.ts.map +1 -0
  55. package/dist/esm/backend/__tests__/validate.test.d.ts +2 -0
  56. package/dist/esm/backend/__tests__/validate.test.d.ts.map +1 -0
  57. package/dist/esm/backend/ast.d.ts +41 -0
  58. package/dist/esm/backend/ast.d.ts.map +1 -0
  59. package/dist/esm/backend/ast.mjs +146 -0
  60. package/dist/esm/backend/detect-integrations.d.ts +8 -4
  61. package/dist/esm/backend/detect-integrations.d.ts.map +1 -1
  62. package/dist/esm/backend/detect-integrations.mjs +12 -15
  63. package/dist/esm/backend/handler-declaration.d.ts +17 -0
  64. package/dist/esm/backend/handler-declaration.d.ts.map +1 -0
  65. package/dist/esm/backend/handler-declaration.mjs +78 -0
  66. package/dist/esm/backend/handlers.d.ts +2 -0
  67. package/dist/esm/backend/handlers.d.ts.map +1 -1
  68. package/dist/esm/backend/handlers.mjs +7 -36
  69. package/dist/esm/backend/messages.d.ts +10 -0
  70. package/dist/esm/backend/messages.d.ts.map +1 -0
  71. package/dist/esm/backend/messages.mjs +12 -0
  72. package/dist/esm/backend/parse.d.ts +11 -0
  73. package/dist/esm/backend/parse.d.ts.map +1 -0
  74. package/dist/esm/backend/parse.mjs +64 -0
  75. package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts +2 -0
  76. package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +1 -0
  77. package/dist/esm/backend/rules/index.d.ts +5 -0
  78. package/dist/esm/backend/rules/index.d.ts.map +1 -0
  79. package/dist/esm/backend/rules/index.mjs +6 -0
  80. package/dist/esm/backend/rules/snowflake-static-sql.d.ts +15 -0
  81. package/dist/esm/backend/rules/snowflake-static-sql.d.ts.map +1 -0
  82. package/dist/esm/backend/rules/snowflake-static-sql.mjs +73 -0
  83. package/dist/esm/backend/rules/types.d.ts +29 -0
  84. package/dist/esm/backend/rules/types.d.ts.map +1 -0
  85. package/dist/esm/backend/rules/types.mjs +1 -0
  86. package/dist/esm/backend/validate.d.ts +23 -0
  87. package/dist/esm/backend/validate.d.ts.map +1 -0
  88. package/dist/esm/backend/validate.mjs +62 -0
  89. package/dist/esm/commands/__tests__/backend-validate.test.d.ts +2 -0
  90. package/dist/esm/commands/__tests__/backend-validate.test.d.ts.map +1 -0
  91. package/dist/esm/commands/backend.d.ts +6 -0
  92. package/dist/esm/commands/backend.d.ts.map +1 -1
  93. package/dist/esm/commands/backend.mjs +23 -4
  94. package/dist/esm/commands/create.d.ts.map +1 -1
  95. package/dist/esm/commands/create.mjs +11 -4
  96. package/dist/esm/commands/generate.d.ts.map +1 -1
  97. package/dist/esm/commands/generate.mjs +2 -1
  98. package/dist/esm/constants.d.ts +1 -2
  99. package/dist/esm/constants.d.ts.map +1 -1
  100. package/dist/esm/constants.mjs +3 -6
  101. package/dist/esm/index.mjs +6 -2
  102. package/dist/esm/util/broker/app.d.ts +12 -2
  103. package/dist/esm/util/broker/app.d.ts.map +1 -1
  104. package/dist/esm/util/broker/app.mjs +20 -2
  105. package/dist/index.js +5 -1
  106. package/dist/util/broker/app.d.ts +12 -2
  107. package/dist/util/broker/app.d.ts.map +1 -1
  108. package/dist/util/broker/app.js +20 -1
  109. package/package.json +4 -2
  110. package/src/backend/__tests__/ast.test.ts +53 -0
  111. package/src/backend/__tests__/detect-integrations.test.ts +12 -30
  112. package/src/backend/__tests__/handlers.test.ts +21 -34
  113. package/src/backend/__tests__/parse.test.ts +25 -0
  114. package/src/backend/__tests__/validate.test.ts +246 -0
  115. package/src/backend/ast.ts +204 -0
  116. package/src/backend/detect-integrations.ts +12 -14
  117. package/src/backend/handler-declaration.ts +104 -0
  118. package/src/backend/handlers.ts +9 -44
  119. package/src/backend/messages.ts +16 -0
  120. package/src/backend/parse.ts +82 -0
  121. package/src/backend/rules/__tests__/snowflake-static-sql.test.ts +175 -0
  122. package/src/backend/rules/index.ts +7 -0
  123. package/src/backend/rules/snowflake-static-sql.ts +78 -0
  124. package/src/backend/rules/types.ts +30 -0
  125. package/src/backend/validate.ts +73 -0
  126. package/src/commands/__tests__/backend-deploy.test.ts +5 -3
  127. package/src/commands/__tests__/backend-validate.test.ts +71 -0
  128. package/src/commands/__tests__/create.test.ts +46 -0
  129. package/src/commands/__tests__/edit.test.ts +6 -0
  130. package/src/commands/backend.ts +23 -3
  131. package/src/commands/create.ts +13 -4
  132. package/src/commands/generate.ts +2 -1
  133. package/src/constants.ts +5 -6
  134. package/src/index.ts +9 -2
  135. package/src/util/broker/__tests__/app.test.ts +29 -1
  136. package/src/util/broker/app.ts +24 -2
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,75 @@
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;
@@ -0,0 +1,29 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,23 @@
1
+ export interface HandlerValidation {
2
+ /** Every `ctx.api.v1.<domain>` the handler touches (known or not), sorted and unique. */
3
+ apiDomains: string[];
4
+ }
5
+ /**
6
+ * Validates a handler's source before upload: real parsing (via acorn) catches
7
+ * syntax errors at build time instead of an opaque failure inside the isolate.
8
+ * Handlers run with no module system and no bundler — `handler` must be a plain
9
+ * top-level global, and imports/requires (which would resolve to nothing at
10
+ * runtime) are rejected.
11
+ *
12
+ * Once the handler is structurally sound, capability-specific rules from
13
+ * `./rules` run and every violation is reported at once.
14
+ *
15
+ * Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
16
+ * build-time DX feedback for agent-generated code, never a security boundary —
17
+ * the isolate has no ambient capabilities regardless of what a handler contains.
18
+ *
19
+ * Returns the `ctx.api.v1.*` domains the handler touches so callers (integration
20
+ * detection) can reuse the parse instead of re-scanning the source.
21
+ */
22
+ export declare function validateHandlerSource(file: string, source: string): HandlerValidation;
23
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,64 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const acornWalk = require('acorn-walk');
4
+ const z2hSharedUtils = require('@mondaydotcomorg/z2h-shared-utils');
5
+ const util_logger = require('../util/logger.js');
6
+ const backend_ast = require('./ast.js');
7
+ const backend_handlerDeclaration = require('./handler-declaration.js');
8
+ const backend_messages = require('./messages.js');
9
+ const backend_parse = require('./parse.js');
10
+ const backend_rules_index = require('./rules/index.js');
11
+
12
+ const KNOWN_API_DOMAINS = [...z2hSharedUtils.ALWAYS_ON_CHANNELS, ...z2hSharedUtils.DECLARED_INTEGRATIONS];
13
+ /**
14
+ * Validates a handler's source before upload: real parsing (via acorn) catches
15
+ * syntax errors at build time instead of an opaque failure inside the isolate.
16
+ * Handlers run with no module system and no bundler — `handler` must be a plain
17
+ * top-level global, and imports/requires (which would resolve to nothing at
18
+ * runtime) are rejected.
19
+ *
20
+ * Once the handler is structurally sound, capability-specific rules from
21
+ * `./rules` run and every violation is reported at once.
22
+ *
23
+ * Unknown `ctx.api.v1.<domain>` usage is a warning, not a failure: this pass is
24
+ * build-time DX feedback for agent-generated code, never a security boundary —
25
+ * the isolate has no ambient capabilities regardless of what a handler contains.
26
+ *
27
+ * Returns the `ctx.api.v1.*` domains the handler touches so callers (integration
28
+ * detection) can reuse the parse instead of re-scanning the source.
29
+ */
30
+ function validateHandlerSource(file, source) {
31
+ const program = backend_parse.parseOrThrow(file, source);
32
+ const bindings = backend_ast.collectBindings(program);
33
+ acornWalk.simple(program, {
34
+ CallExpression(node) {
35
+ if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
36
+ throw new Error(backend_messages.messages.mustNotImport(file));
37
+ }
38
+ },
39
+ ImportExpression() {
40
+ throw new Error(backend_messages.messages.mustNotImport(file));
41
+ },
42
+ });
43
+ const declaration = backend_handlerDeclaration.findHandlerDeclaration(program.body);
44
+ if (!declaration) {
45
+ throw new Error(backend_messages.messages.mustDefine(file));
46
+ }
47
+ if (declaration.kind === 'variable' && (!declaration.init || backend_handlerDeclaration.NEVER_A_FUNCTION.has(declaration.init.type))) {
48
+ throw new Error(backend_messages.messages.notAFunction(file));
49
+ }
50
+ const paths = backend_ast.createPathResolver(bindings, backend_handlerDeclaration.handlerContextParamName(declaration));
51
+ 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
+ for (const domain of apiDomains) {
57
+ if (!KNOWN_API_DOMAINS.includes(domain)) {
58
+ util_logger.warn(`${file}: ctx.api.v1.${domain} is not a known capability (available: ${KNOWN_API_DOMAINS.join(', ')})`);
59
+ }
60
+ }
61
+ return { apiDomains: [...apiDomains].sort() };
62
+ }
63
+
64
+ exports.validateHandlerSource = validateHandlerSource;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=backend-validate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-validate.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/backend-validate.test.ts"],"names":[],"mappings":""}
@@ -28,6 +28,12 @@ export declare function deployBackend(opts: {
28
28
  deployedBy: string;
29
29
  isPreview: boolean;
30
30
  }): Promise<string[]>;
31
+ /**
32
+ * Run the same handler validation deploy runs (syntax, isolate constraints,
33
+ * handler rules, integration detection) against the working tree — without
34
+ * uploading anything. Throws on the first invalid handler.
35
+ */
36
+ export declare function backendValidateCommand(appDir?: string): Promise<void>;
31
37
  export declare function backendInvokeCommand(handlerName: string, opts?: {
32
38
  input?: string;
33
39
  }): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAoBA;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CASpB;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAa1F;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G"}
@@ -42,8 +42,8 @@ async function readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel
42
42
  return [];
43
43
  }
44
44
  util_logger.info(`reading backend handlers ${logLabel}`);
45
- const { handlers } = await backend_handlers.readHandlers(appDir);
46
- const integrations = backend_detectIntegrations.detectIntegrationsFromHandlers(handlers);
45
+ const { handlers, apiDomains } = await backend_handlers.readHandlers(appDir);
46
+ const integrations = backend_detectIntegrations.detectIntegrations(apiDomains);
47
47
  if (Object.keys(handlers).length === 0) {
48
48
  return integrations;
49
49
  }
@@ -83,6 +83,25 @@ async function deployBackend(opts) {
83
83
  throw util_errors.tagDeployError('deploy_backend_error', err);
84
84
  }
85
85
  }
86
+ /**
87
+ * Run the same handler validation deploy runs (syntax, isolate constraints,
88
+ * handler rules, integration detection) against the working tree — without
89
+ * uploading anything. Throws on the first invalid handler.
90
+ */
91
+ async function backendValidateCommand(appDir = process.cwd()) {
92
+ if (!hasHandlers(appDir)) {
93
+ util_logger.output('[z2h-cli] no backend/handlers/ directory — nothing to validate', { handlers: [], integrations: [] });
94
+ return;
95
+ }
96
+ const { handlers, apiDomains } = await backend_handlers.readHandlers(appDir);
97
+ const names = Object.keys(handlers).sort();
98
+ const integrations = backend_detectIntegrations.detectIntegrations(apiDomains);
99
+ const list = (xs) => (xs.length ? xs.join(', ') : 'none');
100
+ util_logger.output(`[z2h-cli] ${names.length} handler(s) valid: ${list(names)}\n[z2h-cli] integrations: ${list(integrations)}`, {
101
+ handlers: names,
102
+ integrations,
103
+ });
104
+ }
86
105
  async function backendInvokeCommand(handlerName, opts = {}) {
87
106
  const appName = currentAppName();
88
107
  let input = null;
@@ -99,5 +118,6 @@ async function backendInvokeCommand(handlerName, opts = {}) {
99
118
  }
100
119
 
101
120
  exports.backendInvokeCommand = backendInvokeCommand;
121
+ exports.backendValidateCommand = backendValidateCommand;
102
122
  exports.deployBackend = deployBackend;
103
123
  exports.hasHandlers = hasHandlers;
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF5F"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAKpH,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAwBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6F5F"}
@@ -58,11 +58,19 @@ async function createCommand(appName, opts = {}) {
58
58
  if (!(await isDirEmpty(appDir))) {
59
59
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
60
60
  }
61
- // Check the broker — fail fast if the app is already registered.
61
+ // Check the broker — fail fast if the app already has a real release.
62
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past:
63
+ // either it's this same name being re-created after a local wipe (we'd only
64
+ // see it at all if we still have access to it), or it genuinely doesn't
65
+ // exist yet and we claim it below.
62
66
  const existing = await util_broker_app.fetchApp(util_appName.mfAppName(appName));
63
- if (existing) {
67
+ if (existing?.deployed) {
64
68
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
65
69
  }
70
+ const gitRemote = util_git_remoteOptions.resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
71
+ if (!existing) {
72
+ await util_broker_app.claimApp(util_appName.mfAppName(appName), gitRemote);
73
+ }
66
74
  // Compute substitutions.
67
75
  const vars = {
68
76
  __APP_NAME__: appName,
@@ -87,7 +95,6 @@ async function createCommand(appName, opts = {}) {
87
95
  await fs.copy(opts.htmlFile, path__default.default.join(appDir, 'src', 'page.html'));
88
96
  }
89
97
  util_logger.info(`initializing git repo for ${appName}...`);
90
- const gitRemote = util_git_remoteOptions.resolveRemoteUrl(opts.remote, appName, opts.repoUrl);
91
98
  await util_git_repo.initRepo(appDir, gitRemote);
92
99
  util_logger.info(`installing dependencies for ${appName}...`);
93
100
  await util_npm_install.runYarnInstall({ cwd: workspace });
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA6E3F"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA8E3F"}
@@ -136,8 +136,9 @@ async function generateCommand(appName, opts) {
136
136
  if (!(await isDirEmpty(appDir))) {
137
137
  throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
138
138
  }
139
+ // A claimed-but-unpublished stub (`entry: null`) is fine to proceed past — only a real release blocks generate.
139
140
  const existing = await util_broker_app.fetchApp(util_appName.mfAppName(appName));
140
- if (existing) {
141
+ if (existing?.deployed) {
141
142
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
142
143
  }
143
144
  util_logger.info(`copying template "${template.id}" → ${appDir}`);
@@ -20,8 +20,7 @@ export declare const S3_SKILLS_BUCKET_NAME = "prod-use1-bigbrain-zth-skills";
20
20
  export declare const DATAVIZ_SKILLS_PREFIX = "dataviz/";
21
21
  export declare const APP_NAME_PATTERN: RegExp;
22
22
  export declare const PREVIEW_APP_NAME_PREFIX = "zth-preview-";
23
- export declare const DECLARED_INTEGRATIONS: readonly ["monday"];
24
- export type DeclaredIntegration = (typeof DECLARED_INTEGRATIONS)[number];
23
+ export { ALWAYS_ON_CHANNELS, DECLARED_INTEGRATIONS, type DeclaredIntegration } from '@mondaydotcomorg/z2h-shared-utils';
25
24
  export declare const DEFAULT_BROKER_BASE_URL = "https://bigbrain-zth.bigbrain.me";
26
25
  /**
27
26
  * Resolves the broker origin (no trailing slash). Honors
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,SAAS,CAAC;AAGtC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEjD,CAAC;AAKF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAElD,CAAC;AAMF,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAC9B,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAG9B,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAQ3C,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAK7C,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAM1E,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAIrE,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAMhD,eAAO,MAAM,gBAAgB,QAA2B,CAAC;AAEzD,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAMtD,eAAO,MAAM,qBAAqB,qBAAsB,CAAC;AACzD,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAKzE,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAID,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGvD;AAKD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAG9C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAGnD,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAMrE,eAAO,MAAM,iBAAiB,EAAG,eAAwB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,SAAS,CAAC;AAGtC,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEjD,CAAC;AAKF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAElD,CAAC;AAMF,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAC9B,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAG9B,eAAO,MAAM,iBAAiB,UAAY,CAAC;AAQ3C,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAK7C,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAM1E,eAAO,MAAM,qBAAqB,kCAAkC,CAAC;AAIrE,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAMhD,eAAO,MAAM,gBAAgB,QAA2B,CAAC;AAEzD,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAMtD,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxH,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAID,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGvD;AAKD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAG9C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAGnD,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAMrE,eAAO,MAAM,iBAAiB,EAAG,eAAwB,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
package/dist/constants.js CHANGED
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
+ const z2hSharedUtils = require('@mondaydotcomorg/z2h-shared-utils');
4
+
3
5
  /**
4
6
  * Cross-file constants. Narrowly-used literals (templates, prefixes used in
5
7
  * one place) stay local to their file to keep this index small.
@@ -57,11 +59,6 @@ const DATAVIZ_SKILLS_PREFIX = 'dataviz/';
57
59
  // upfront for friendlier errors.
58
60
  const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
59
61
  const PREVIEW_APP_NAME_PREFIX = 'zth-preview-';
60
- // ─────────── Declared (OAuth-gated) backend integrations ─────
61
- // Closed set the CLI detects from handler sources and the broker validates on
62
- // register/update. Always-on channels (`db`, `llm`, `snowflake`) are not listed —
63
- // the runner builds those regardless. Shared via `@mondaydotcomorg/z2h-cli/constants`.
64
- const DECLARED_INTEGRATIONS = ['monday'];
65
62
  // ─────────── Broker origin + npm registry (shared §3 contract) ─────
66
63
  // The bigbrain-zth broker fronts both the STS credential route and the npm
67
64
  // proxy. `Z2H_BROKER_BASE_URL` overrides the origin for tests/staging.
@@ -103,6 +100,14 @@ function brokerNpmRegistryUrl() {
103
100
  return `${brokerBaseUrl()}${NPM_BROKER_PATH}`;
104
101
  }
105
102
 
103
+ Object.defineProperty(exports, 'ALWAYS_ON_CHANNELS', {
104
+ enumerable: true,
105
+ get: () => z2hSharedUtils.ALWAYS_ON_CHANNELS
106
+ });
107
+ Object.defineProperty(exports, 'DECLARED_INTEGRATIONS', {
108
+ enumerable: true,
109
+ get: () => z2hSharedUtils.DECLARED_INTEGRATIONS
110
+ });
106
111
  exports.APP_NAME_PATTERN = APP_NAME_PATTERN;
107
112
  exports.BROKER_NPM_SCOPE = BROKER_NPM_SCOPE;
108
113
  exports.BUCKET_READ_DNS_NAME = BUCKET_READ_DNS_NAME;
@@ -110,7 +115,6 @@ exports.BUCKET_READ_DNS_URL = BUCKET_READ_DNS_URL;
110
115
  exports.BUCKET_WRITE_NAME = BUCKET_WRITE_NAME;
111
116
  exports.BUCKET_WRITE_URL = BUCKET_WRITE_URL;
112
117
  exports.DATAVIZ_SKILLS_PREFIX = DATAVIZ_SKILLS_PREFIX;
113
- exports.DECLARED_INTEGRATIONS = DECLARED_INTEGRATIONS;
114
118
  exports.DEFAULT_BROKER_BASE_URL = DEFAULT_BROKER_BASE_URL;
115
119
  exports.GITIGNORE_ENTRIES = GITIGNORE_ENTRIES;
116
120
  exports.MANIFEST_KEY = MANIFEST_KEY;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ast.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.test.d.ts","sourceRoot":"","sources":["../../../../src/backend/__tests__/ast.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parse.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../../../src/backend/__tests__/parse.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=validate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.test.d.ts","sourceRoot":"","sources":["../../../../src/backend/__tests__/validate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
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;
4
+ export interface Declaration {
5
+ kind: VariableDeclaration['kind'];
6
+ init: Expression | null | undefined;
7
+ /**
8
+ * Property path from `init` to this binding: `[]` for `const x = init`,
9
+ * `['a', 'b']` for `const { a: { b: x } } = init`, `undefined` when the value
10
+ * is not a plain property read (array/rest/default patterns).
11
+ */
12
+ path: string[] | undefined;
13
+ }
14
+ export interface Bindings {
15
+ /** Every `var|let|const <name> = …` in the file, by name. */
16
+ declarations: Map<string, Declaration[]>;
17
+ /** Names whose value cannot be pinned: reassigned, updated, or bound as parameters. */
18
+ unpinnable: Set<string>;
19
+ }
20
+ /** Every identifier a binding pattern introduces (`{ a, b: [c] } = …` → a, c). */
21
+ export declare const patternNames: (pattern: Pattern) => string[];
22
+ export declare function collectBindings(program: Program): Bindings;
23
+ export interface PathResolver {
24
+ /**
25
+ * Dotted path of a member chain, following `const` aliases back to their
26
+ * source: with `const sf = ctx.api.v1.snowflake`, `sf.query` resolves to
27
+ * `ctx.api.v1.snowflake.query`. Destructuring counts too
28
+ * (`const { snowflake: sf } = ctx.api.v1`). `undefined` if any link is dynamic.
29
+ *
30
+ * Only single, never-reassigned, never-shadowed `const` bindings are followed —
31
+ * anything else is left as written, so a `let` alias or a parameter of the same
32
+ * name is never mistaken for the channel. The handler's first parameter is
33
+ * treated as `ctx` even when renamed (`handler(context)` → `context.api.v1.db`
34
+ * resolves as `ctx.api.v1.db`).
35
+ */
36
+ pathOf(node: AnyNode): string | undefined;
37
+ }
38
+ export declare function createPathResolver(bindings: Bindings, contextParam?: string): PathResolver;
39
+ /** Every `ctx.api.v1.<domain>` the program touches (known or not), through `paths`. */
40
+ export declare function collectApiDomains(program: Program, paths: PathResolver): Set<string>;
41
+ //# sourceMappingURL=ast.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,146 @@
1
+ import { full, simple } from 'acorn-walk';
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
+ /**
6
+ * Every identifier a binding pattern introduces, with its property path from the
7
+ * pattern's value. `path` is deliberately not defaulted: callers pass `[]` for
8
+ * the root and `undefined` once the pattern stops being a plain property read.
9
+ */
10
+ function patternBindings(pattern, path) {
11
+ switch (pattern.type) {
12
+ case 'Identifier':
13
+ return [{ name: pattern.name, path }];
14
+ case 'ObjectPattern':
15
+ return pattern.properties.flatMap(p => {
16
+ if (p.type === 'RestElement') {
17
+ return patternBindings(p.argument, undefined);
18
+ }
19
+ const key = propertyKey(p.key, p.computed);
20
+ return patternBindings(p.value, path && key !== undefined ? [...path, key] : undefined);
21
+ });
22
+ case 'ArrayPattern':
23
+ return pattern.elements.flatMap(e => (e ? patternBindings(e, undefined) : []));
24
+ case 'AssignmentPattern':
25
+ return patternBindings(pattern.left, undefined);
26
+ case 'RestElement':
27
+ return patternBindings(pattern.argument, undefined);
28
+ default:
29
+ // MemberExpression targets (`obj.x = …`) bind no names.
30
+ return [];
31
+ }
32
+ }
33
+ function propertyKey(key, computed) {
34
+ if (key.type === 'Literal' && typeof key.value === 'string') {
35
+ return key.value;
36
+ }
37
+ if (!computed && key.type === 'Identifier') {
38
+ return key.name;
39
+ }
40
+ return undefined;
41
+ }
42
+ /** Every identifier a binding pattern introduces (`{ a, b: [c] } = …` → a, c). */
43
+ const patternNames = (pattern) => patternBindings(pattern, []).map(b => b.name);
44
+ function collectBindings(program) {
45
+ const declarations = new Map();
46
+ const unpinnable = new Set();
47
+ const markUnpinnable = (pattern) => patternNames(pattern).forEach(n => unpinnable.add(n));
48
+ full(program, node => {
49
+ switch (node.type) {
50
+ case 'VariableDeclaration':
51
+ for (const declarator of node.declarations) {
52
+ for (const { name, path } of patternBindings(declarator.id, [])) {
53
+ const list = declarations.get(name) ?? [];
54
+ list.push({ kind: node.kind, init: declarator.init, path });
55
+ declarations.set(name, list);
56
+ }
57
+ }
58
+ break;
59
+ case 'AssignmentExpression':
60
+ markUnpinnable(node.left);
61
+ break;
62
+ case 'UpdateExpression':
63
+ if (node.argument.type === 'Identifier') {
64
+ unpinnable.add(node.argument.name);
65
+ }
66
+ break;
67
+ case 'ForInStatement':
68
+ case 'ForOfStatement':
69
+ if (node.left.type !== 'VariableDeclaration') {
70
+ markUnpinnable(node.left);
71
+ }
72
+ break;
73
+ case 'FunctionDeclaration':
74
+ case 'FunctionExpression':
75
+ case 'ArrowFunctionExpression':
76
+ node.params.forEach(markUnpinnable);
77
+ break;
78
+ case 'CatchClause':
79
+ if (node.param) {
80
+ markUnpinnable(node.param);
81
+ }
82
+ break;
83
+ }
84
+ });
85
+ return { declarations, unpinnable };
86
+ }
87
+ /** `a.b.c` / `a['b'].c` → root `a`, parts `[b, c]`; `undefined` if any link is dynamic or the root is not an identifier. */
88
+ function memberChain(node) {
89
+ if (node.type === 'ChainExpression') {
90
+ return memberChain(node.expression);
91
+ }
92
+ const parts = [];
93
+ let cur = node;
94
+ while (cur.type === 'MemberExpression') {
95
+ const key = propertyKey(cur.property, cur.computed);
96
+ if (key === undefined) {
97
+ return undefined;
98
+ }
99
+ parts.unshift(key);
100
+ cur = cur.object;
101
+ }
102
+ return cur.type === 'Identifier' ? { root: cur.name, parts } : undefined;
103
+ }
104
+ function createPathResolver(bindings, contextParam) {
105
+ function resolveChain(node, resolving) {
106
+ const chain = memberChain(node);
107
+ if (!chain) {
108
+ return undefined;
109
+ }
110
+ return [resolveRoot(chain.root, resolving), ...chain.parts].join('.');
111
+ }
112
+ function resolveRoot(name, resolving) {
113
+ if (contextParam !== undefined && name === contextParam) {
114
+ return 'ctx';
115
+ }
116
+ if (bindings.unpinnable.has(name) || resolving.has(name)) {
117
+ return name;
118
+ }
119
+ const decls = bindings.declarations.get(name);
120
+ if (!decls || decls.length !== 1) {
121
+ return name;
122
+ }
123
+ const [{ kind, init, path }] = decls;
124
+ if (kind !== 'const' || !init || !path) {
125
+ return name;
126
+ }
127
+ const base = resolveChain(init, new Set(resolving).add(name));
128
+ return base ? [base, ...path].join('.') : name;
129
+ }
130
+ return { pathOf: node => resolveChain(node, new Set()) };
131
+ }
132
+ /** Every `ctx.api.v1.<domain>` the program touches (known or not), through `paths`. */
133
+ function collectApiDomains(program, paths) {
134
+ const apiDomains = new Set();
135
+ simple(program, {
136
+ MemberExpression(node) {
137
+ const [obj, api, version, domain] = paths.pathOf(node)?.split('.') ?? [];
138
+ if (obj === 'ctx' && api === 'api' && version === 'v1' && domain) {
139
+ apiDomains.add(domain);
140
+ }
141
+ },
142
+ });
143
+ return apiDomains;
144
+ }
145
+
146
+ export { collectApiDomains, collectBindings, createPathResolver, lineOf, patternNames };
@@ -1,11 +1,15 @@
1
+ import { type DeclaredIntegration } from '@mondaydotcomorg/z2h-shared-utils';
1
2
  /**
2
- * Integrations inferred from handler source at deploy time — whatever
3
+ * Integrations inferred from handler sources at deploy time — whatever
3
4
  * `ctx.api.v1.<name>` the handlers touch (from `DECLARED_INTEGRATIONS`) is
4
5
  * registered with the platform. There is no `integrations.json` and no declare
5
6
  * CLI step.
6
7
  *
7
- * Only OAuth-gated integrations belong in that set. `db`, `llm`, and `snowflake`
8
- * are always-on channels and are never declared.
8
+ * `apiDomains` comes from the handler parse in `validateHandlerSource`, so a
9
+ * domain mentioned only in a comment or string does not count as usage.
10
+ *
11
+ * Only OAuth-gated integrations belong in the result. `db`, `llm`, and
12
+ * `snowflake` are always-on channels and are never declared.
9
13
  */
10
- export declare function detectIntegrationsFromHandlers(handlers: Record<string, string>): string[];
14
+ export declare function detectIntegrations(apiDomains: Iterable<string>): DeclaredIntegration[];
11
15
  //# sourceMappingURL=detect-integrations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detect-integrations.d.ts","sourceRoot":"","sources":["../../../src/backend/detect-integrations.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAUzF"}
1
+ {"version":3,"file":"detect-integrations.d.ts","sourceRoot":"","sources":["../../../src/backend/detect-integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKpG;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,mBAAmB,EAAE,CAEtF"}