@mondaydotcomorg/z2h-cli 0.29.5 → 0.31.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 (160) hide show
  1. package/dist/backend/handlers.d.ts +3 -0
  2. package/dist/backend/handlers.d.ts.map +1 -1
  3. package/dist/backend/handlers.js +7 -4
  4. package/dist/backend/parser/__tests__/ast.test.d.ts.map +1 -0
  5. package/dist/backend/parser/__tests__/parse.test.d.ts.map +1 -0
  6. package/dist/backend/parser/api-signatures.d.ts +4 -0
  7. package/dist/backend/parser/api-signatures.d.ts.map +1 -0
  8. package/dist/backend/parser/api-signatures.js +31 -0
  9. package/dist/backend/parser/api-usage.d.ts +5 -0
  10. package/dist/backend/parser/api-usage.d.ts.map +1 -0
  11. package/dist/backend/parser/api-usage.js +61 -0
  12. package/dist/backend/parser/bindings.d.ts +5 -0
  13. package/dist/backend/parser/bindings.d.ts.map +1 -0
  14. package/dist/backend/parser/bindings.js +84 -0
  15. package/dist/backend/parser/handler-declaration.d.ts.map +1 -0
  16. package/dist/backend/parser/member-path.d.ts +3 -0
  17. package/dist/backend/parser/member-path.d.ts.map +1 -0
  18. package/dist/backend/parser/member-path.js +31 -0
  19. package/dist/backend/parser/messages.d.ts.map +1 -0
  20. package/dist/backend/parser/parse.d.ts.map +1 -0
  21. package/dist/backend/{parse.js → parser/parse.js} +9 -9
  22. package/dist/backend/parser/static-value.d.ts +6 -0
  23. package/dist/backend/parser/static-value.d.ts.map +1 -0
  24. package/dist/backend/parser/static-value.js +117 -0
  25. package/dist/backend/parser/types.d.ts +53 -0
  26. package/dist/backend/parser/types.d.ts.map +1 -0
  27. package/dist/backend/parser/types.js +1 -0
  28. package/dist/backend/parser/utils.d.ts +19 -0
  29. package/dist/backend/parser/utils.d.ts.map +1 -0
  30. package/dist/backend/parser/utils.js +97 -0
  31. package/dist/backend/validate.d.ts +3 -3
  32. package/dist/backend/validate.d.ts.map +1 -1
  33. package/dist/backend/validate.js +18 -18
  34. package/dist/commands/backend.d.ts +2 -13
  35. package/dist/commands/backend.d.ts.map +1 -1
  36. package/dist/commands/backend.js +30 -21
  37. package/dist/commands/create.d.ts.map +1 -1
  38. package/dist/commands/create.js +1 -0
  39. package/dist/commands/deploy.d.ts.map +1 -1
  40. package/dist/commands/deploy.js +21 -10
  41. package/dist/commands/generate.d.ts.map +1 -1
  42. package/dist/commands/generate.js +6 -0
  43. package/dist/esm/backend/handlers.d.ts +3 -0
  44. package/dist/esm/backend/handlers.d.ts.map +1 -1
  45. package/dist/esm/backend/handlers.mjs +7 -4
  46. package/dist/esm/backend/parser/__tests__/ast.test.d.ts.map +1 -0
  47. package/dist/esm/backend/parser/__tests__/parse.test.d.ts.map +1 -0
  48. package/dist/esm/backend/parser/api-signatures.d.ts +4 -0
  49. package/dist/esm/backend/parser/api-signatures.d.ts.map +1 -0
  50. package/dist/esm/backend/parser/api-signatures.mjs +24 -0
  51. package/dist/esm/backend/parser/api-usage.d.ts +5 -0
  52. package/dist/esm/backend/parser/api-usage.d.ts.map +1 -0
  53. package/dist/esm/backend/parser/api-usage.mjs +58 -0
  54. package/dist/esm/backend/parser/bindings.d.ts +5 -0
  55. package/dist/esm/backend/parser/bindings.d.ts.map +1 -0
  56. package/dist/esm/backend/parser/bindings.mjs +81 -0
  57. package/dist/esm/backend/parser/handler-declaration.d.ts.map +1 -0
  58. package/dist/esm/backend/parser/member-path.d.ts +3 -0
  59. package/dist/esm/backend/parser/member-path.d.ts.map +1 -0
  60. package/dist/esm/backend/parser/member-path.mjs +29 -0
  61. package/dist/esm/backend/parser/messages.d.ts.map +1 -0
  62. package/dist/esm/backend/parser/parse.d.ts.map +1 -0
  63. package/dist/esm/backend/{parse.mjs → parser/parse.mjs} +1 -1
  64. package/dist/esm/backend/parser/static-value.d.ts +6 -0
  65. package/dist/esm/backend/parser/static-value.d.ts.map +1 -0
  66. package/dist/esm/backend/parser/static-value.mjs +114 -0
  67. package/dist/esm/backend/parser/types.d.ts +53 -0
  68. package/dist/esm/backend/parser/types.d.ts.map +1 -0
  69. package/dist/esm/backend/parser/types.mjs +1 -0
  70. package/dist/esm/backend/parser/utils.d.ts +19 -0
  71. package/dist/esm/backend/parser/utils.d.ts.map +1 -0
  72. package/dist/esm/backend/parser/utils.mjs +88 -0
  73. package/dist/esm/backend/validate.d.ts +3 -3
  74. package/dist/esm/backend/validate.d.ts.map +1 -1
  75. package/dist/esm/backend/validate.mjs +8 -8
  76. package/dist/esm/commands/backend.d.ts +2 -13
  77. package/dist/esm/commands/backend.d.ts.map +1 -1
  78. package/dist/esm/commands/backend.mjs +31 -22
  79. package/dist/esm/commands/create.d.ts.map +1 -1
  80. package/dist/esm/commands/create.mjs +1 -0
  81. package/dist/esm/commands/deploy.d.ts.map +1 -1
  82. package/dist/esm/commands/deploy.mjs +23 -11
  83. package/dist/esm/commands/generate.d.ts.map +1 -1
  84. package/dist/esm/commands/generate.mjs +8 -2
  85. package/dist/esm/templates/app/usage.mjs +5 -0
  86. package/dist/esm/util/broker/app.d.ts +2 -0
  87. package/dist/esm/util/broker/app.d.ts.map +1 -1
  88. package/dist/esm/util/broker/app.mjs +4 -4
  89. package/dist/esm/util/machine-identity.d.ts +2 -3
  90. package/dist/esm/util/machine-identity.d.ts.map +1 -1
  91. package/dist/esm/util/machine-identity.mjs +7 -4
  92. package/dist/templates/app/usage.js +7 -0
  93. package/dist/util/broker/app.d.ts +2 -0
  94. package/dist/util/broker/app.d.ts.map +1 -1
  95. package/dist/util/broker/app.js +4 -4
  96. package/dist/util/machine-identity.d.ts +2 -3
  97. package/dist/util/machine-identity.d.ts.map +1 -1
  98. package/dist/util/machine-identity.js +11 -3
  99. package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +11 -14
  100. package/handler-api/signatures.json +26 -0
  101. package/package.json +1 -1
  102. package/src/backend/__tests__/handlers.test.ts +13 -2
  103. package/src/backend/__tests__/validate.test.ts +54 -0
  104. package/src/backend/handlers.ts +10 -5
  105. package/src/backend/parser/__tests__/ast.test.ts +252 -0
  106. package/src/backend/{__tests__ → parser/__tests__}/parse.test.ts +2 -2
  107. package/src/backend/parser/api-signatures.ts +23 -0
  108. package/src/backend/parser/api-usage.ts +73 -0
  109. package/src/backend/parser/bindings.ts +87 -0
  110. package/src/backend/parser/member-path.ts +31 -0
  111. package/src/backend/{parse.ts → parser/parse.ts} +1 -1
  112. package/src/backend/parser/static-value.ts +145 -0
  113. package/src/backend/parser/types.ts +66 -0
  114. package/src/backend/parser/utils.ts +106 -0
  115. package/src/backend/validate.ts +12 -8
  116. package/src/commands/__tests__/backend-deploy.test.ts +70 -5
  117. package/src/commands/__tests__/backend-validate.test.ts +11 -1
  118. package/src/commands/__tests__/create.test.ts +1 -0
  119. package/src/commands/__tests__/deploy.test.ts +28 -0
  120. package/src/commands/backend.ts +31 -20
  121. package/src/commands/create.ts +1 -0
  122. package/src/commands/deploy.ts +29 -12
  123. package/src/commands/generate.ts +8 -1
  124. package/src/templates/app/usage.json +1 -0
  125. package/src/util/broker/app.ts +17 -4
  126. package/src/util/machine-identity.ts +7 -3
  127. package/dist/backend/__tests__/ast.test.d.ts.map +0 -1
  128. package/dist/backend/__tests__/parse.test.d.ts.map +0 -1
  129. package/dist/backend/ast.d.ts +0 -39
  130. package/dist/backend/ast.d.ts.map +0 -1
  131. package/dist/backend/ast.js +0 -149
  132. package/dist/backend/handler-declaration.d.ts.map +0 -1
  133. package/dist/backend/messages.d.ts.map +0 -1
  134. package/dist/backend/parse.d.ts.map +0 -1
  135. package/dist/esm/backend/__tests__/ast.test.d.ts.map +0 -1
  136. package/dist/esm/backend/__tests__/parse.test.d.ts.map +0 -1
  137. package/dist/esm/backend/ast.d.ts +0 -39
  138. package/dist/esm/backend/ast.d.ts.map +0 -1
  139. package/dist/esm/backend/ast.mjs +0 -144
  140. package/dist/esm/backend/handler-declaration.d.ts.map +0 -1
  141. package/dist/esm/backend/messages.d.ts.map +0 -1
  142. package/dist/esm/backend/parse.d.ts.map +0 -1
  143. package/src/backend/__tests__/ast.test.ts +0 -53
  144. package/src/backend/ast.ts +0 -201
  145. /package/dist/backend/{__tests__ → parser/__tests__}/ast.test.d.ts +0 -0
  146. /package/dist/backend/{__tests__ → parser/__tests__}/parse.test.d.ts +0 -0
  147. /package/dist/backend/{handler-declaration.d.ts → parser/handler-declaration.d.ts} +0 -0
  148. /package/dist/backend/{handler-declaration.js → parser/handler-declaration.js} +0 -0
  149. /package/dist/backend/{messages.d.ts → parser/messages.d.ts} +0 -0
  150. /package/dist/backend/{messages.js → parser/messages.js} +0 -0
  151. /package/dist/backend/{parse.d.ts → parser/parse.d.ts} +0 -0
  152. /package/dist/esm/backend/{__tests__ → parser/__tests__}/ast.test.d.ts +0 -0
  153. /package/dist/esm/backend/{__tests__ → parser/__tests__}/parse.test.d.ts +0 -0
  154. /package/dist/esm/backend/{handler-declaration.d.ts → parser/handler-declaration.d.ts} +0 -0
  155. /package/dist/esm/backend/{handler-declaration.mjs → parser/handler-declaration.mjs} +0 -0
  156. /package/dist/esm/backend/{messages.d.ts → parser/messages.d.ts} +0 -0
  157. /package/dist/esm/backend/{messages.mjs → parser/messages.mjs} +0 -0
  158. /package/dist/esm/backend/{parse.d.ts → parser/parse.d.ts} +0 -0
  159. /package/src/backend/{handler-declaration.ts → parser/handler-declaration.ts} +0 -0
  160. /package/src/backend/{messages.ts → parser/messages.ts} +0 -0
@@ -1,8 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
+ import { readJson, writeJson } from 'fs-extra';
3
4
  import { PutObjectCommand } from '@aws-sdk/client-s3';
4
5
  import { output, info } from '../util/logger.mjs';
5
- import { getCwdAppName, mfAppName } from '../util/app-name.mjs';
6
+ import { mfAppName, getCwdAppName } from '../util/app-name.mjs';
7
+ import { whoAmI } from '../util/machine-identity.mjs';
6
8
  import { readHandlers } from '../backend/handlers.mjs';
7
9
  import { detectIntegrations } from '../backend/detect-integrations.mjs';
8
10
  import { getS3Client } from '../util/s3/client.mjs';
@@ -17,30 +19,41 @@ function currentAppName() {
17
19
  }
18
20
  return mfAppName(raw);
19
21
  }
20
- /**
21
- * True if the app ships an app-level backend — a `backend/handlers/` directory.
22
- * Callers gate on this so the deploy path is explicit about whether a backend is in
23
- * play.
24
- */
25
22
  function hasHandlers(appDir) {
26
23
  return existsSync(path.join(appDir, 'backend', 'handlers'));
27
24
  }
25
+ /** Other features may add top-level fields, so writes below merge into this copy instead of replacing it. */
26
+ async function readExistingUsage(appDir) {
27
+ try {
28
+ return (await readJson(path.join(appDir, 'usage.json')));
29
+ }
30
+ catch (err) {
31
+ if (err.code === 'ENOENT') {
32
+ return {};
33
+ }
34
+ throw err;
35
+ }
36
+ }
37
+ /** Keeps the app's git-tracked `usage.json` current with every handler parse (validate or deploy). */
38
+ async function writeUsageJson(appDir, patch) {
39
+ const existing = await readExistingUsage(appDir);
40
+ await writeJson(path.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
41
+ }
28
42
  /**
29
- * Read handlers, detect declared integrations, and upload to `prefix` when there
30
- * is anything to upload. Returns `[]` when there is no `backend/handlers/` dir
31
- * (or it is empty of sources) so callers can always pass the result to
32
- * `registerOrUpdateApp` without a separate gate.
43
+ * Returns `[]` when there is no `backend/handlers/` dir (or it is empty of sources)
44
+ * so callers can always pass the result to `registerOrUpdateApp` without a separate gate.
33
45
  */
34
- async function readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel) {
46
+ async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel) {
35
47
  if (!hasHandlers(appDir)) {
36
48
  return [];
37
49
  }
38
50
  info(`reading backend handlers ${logLabel}`);
39
- const { handlers, apiDomains } = await readHandlers(appDir);
51
+ const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
40
52
  const integrations = detectIntegrations(apiDomains);
41
53
  if (Object.keys(handlers).length === 0) {
42
54
  return integrations;
43
55
  }
56
+ await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
44
57
  const s3 = getS3Client();
45
58
  const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
46
59
  const put = (key, body, contentType) => s3.send(new PutObjectCommand({ Bucket: BUCKET_WRITE_NAME, Key: key, Body: body, ContentType: contentType }));
@@ -55,11 +68,6 @@ async function readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel
55
68
  * from their sources. Each `.js` handler is uploaded as its own flat file — no
56
69
  * compilation, no bundling, no npm deps.
57
70
  *
58
- * Live and preview differ only in where the handlers land, so that is the only
59
- * thing `isPreview` switches:
60
- * - live: `<appName>/backend-<version>` — the app's immutable per-version slot.
61
- * - preview: `<mfKeyPrefix without /MF>/MS` — beside the preview's `/MF` frontend.
62
- *
63
71
  * Integration sync note: the returned set is re-derived from the CURRENT backend
64
72
  * sources on every deploy, and the platform set is reconciled to exactly match
65
73
  * it (there is no stored declaration to fall back on). A deploy therefore always
@@ -71,25 +79,26 @@ async function deployBackend(opts) {
71
79
  const prefix = isPreview ? `${mfKeyPrefix.replace(/\/MF$/, '')}/MS` : `${appName}/backend-${version}`;
72
80
  const logLabel = isPreview ? 'for preview' : `for "${appName}"`;
73
81
  try {
74
- return await readDetectAndUpload(appDir, prefix, version, deployedBy, logLabel);
82
+ return await readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel);
75
83
  }
76
84
  catch (err) {
77
85
  throw tagDeployError('deploy_backend_error', err);
78
86
  }
79
87
  }
80
88
  /**
81
- * Run the same handler validation deploy runs (syntax, isolate constraints,
82
- * handler rules, integration detection) against the working tree — without
83
- * uploading anything. Throws on the first invalid handler.
89
+ * Runs the same checks deploy does against the working tree, refreshing the
90
+ * local `usage.json` — no upload to S3. Throws on the first invalid handler.
84
91
  */
85
92
  async function backendValidateCommand(appDir = process.cwd()) {
86
93
  if (!hasHandlers(appDir)) {
87
94
  output('[z2h-cli] no backend/handlers/ directory — nothing to validate', { handlers: [], integrations: [] });
88
95
  return;
89
96
  }
90
- const { handlers, apiDomains } = await readHandlers(appDir);
97
+ const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
91
98
  const names = Object.keys(handlers).sort();
92
99
  const integrations = detectIntegrations(apiDomains);
100
+ const { name } = (await readJson(path.join(appDir, 'package.json')));
101
+ await writeUsageJson(appDir, { appName: name ? mfAppName(name) : undefined, deployedBy: whoAmI(), apiUsages });
93
102
  const list = (xs) => (xs.length ? xs.join(', ') : 'none');
94
103
  output(`[z2h-cli] ${names.length} handler(s) valid: ${list(names)}\n[z2h-cli] integrations: ${list(integrations)}`, {
95
104
  handlers: names,
@@ -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,CA6F5F"}
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,CA8F5F"}
@@ -85,6 +85,7 @@ async function createCommand(appName, opts = {}) {
85
85
  await copy(path.join(appTemplatesDir, 'gitignore'), path.join(appDir, '.gitignore'));
86
86
  await copy(path.join(appTemplatesDir, 'claude-settings.json'), path.join(appDir, '.claude', 'settings.json'));
87
87
  await copy(path.join(appTemplatesDir, 'CLAUDE.md'), path.join(appDir, 'CLAUDE.md'));
88
+ await copy(path.join(appTemplatesDir, 'usage.json'), path.join(appDir, 'usage.json'));
88
89
  // html-embed: also copy the source HTML.
89
90
  if (template === 'html-embed' && opts.htmlFile) {
90
91
  await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAkBtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IAIrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAatG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IAIrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E"}
@@ -1,4 +1,3 @@
1
- import os from 'node:os';
2
1
  import path from 'node:path';
3
2
  import { readJson, ensureDir, copy } from 'fs-extra';
4
3
  import { info, output, getOutputMode, warn } from '../util/logger.mjs';
@@ -13,8 +12,8 @@ import { getRemoteUrl, initRepo, guardCleanTree, pullAndResolve, pushToRemote }
13
12
  import { resolveGitEnv } from '../util/git/git-env.mjs';
14
13
  import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
15
14
  import { setupS3Client } from '../util/s3/client.mjs';
16
- import { getMachineAppName } from '../util/machine-identity.mjs';
17
- import { registerOrUpdateApp } from '../util/broker/app.mjs';
15
+ import { whoAmI } from '../util/machine-identity.mjs';
16
+ import { fetchApp, registerOrUpdateApp } from '../util/broker/app.mjs';
18
17
  import { openInBrowser } from '../util/open-browser.mjs';
19
18
  import { resolvePortAndMetadata } from '../shadow/prepare.mjs';
20
19
  import { tagDeployError, DeployError, grantReasonForStatus } from '../util/errors.mjs';
@@ -22,10 +21,6 @@ import { deployBackend } from './backend.mjs';
22
21
  import { buildCommand } from './build.mjs';
23
22
  import { promptForTags } from './tag.mjs';
24
23
 
25
- /** Who is publishing this version — the machine identity, else the OS user. */
26
- function whoDeployed() {
27
- return getMachineAppName() ?? os.userInfo().username;
28
- }
29
24
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
30
25
  async function deployCommand(opts = {}) {
31
26
  const paths = resolveConsumerPaths(process.cwd());
@@ -68,7 +63,7 @@ async function buildAndStamp(opts) {
68
63
  else {
69
64
  ({ assetManifest } = await buildCommand({ publicUrl: opts.publicUrl }));
70
65
  }
71
- const deployedBy = whoDeployed();
66
+ const deployedBy = whoAmI();
72
67
  return {
73
68
  ...assetManifest,
74
69
  version: opts.nextVersion,
@@ -83,7 +78,7 @@ async function buildAndStamp(opts) {
83
78
  * register the release with the platform in one server-side transaction.
84
79
  */
85
80
  async function uploadAndRegister(opts) {
86
- const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } = opts;
81
+ const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
87
82
  info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
88
83
  await uploadDir(paths.shadowBuild, keyPrefix);
89
84
  // `integrations` is inferred from the backend sources in the working tree and
@@ -94,12 +89,28 @@ async function uploadAndRegister(opts) {
94
89
  appName,
95
90
  mfKeyPrefix: keyPrefix,
96
91
  version: nextVersion,
97
- deployedBy: whoDeployed(),
92
+ deployedBy: whoAmI(),
98
93
  isPreview,
99
94
  });
100
- await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, integrations, description });
95
+ await registerOrUpdateApp({
96
+ appName,
97
+ token,
98
+ nextVersion,
99
+ entry,
100
+ gitRemote,
101
+ integrations,
102
+ description,
103
+ masterAppName,
104
+ });
101
105
  }
102
106
  async function deployPreview({ paths, consumerName, appName, token, opts }) {
107
+ // A preview inherits from the live app it previews, resolved by name server-side —
108
+ // fail fast with an actionable message if that app was never claimed/deployed (e.g. a
109
+ // local checkout scaffolded before `create` started claiming names), rather than
110
+ // letting a deep backend 404 surface as a generic "App registration failed".
111
+ if (!(await fetchApp(appName))) {
112
+ throw new DeployError('register_error', `Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`);
113
+ }
103
114
  await ensureDir(paths.shadowDir);
104
115
  const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
105
116
  const previewSlug = buildPreviewSlug(consumerName, previewId);
@@ -131,6 +142,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
131
142
  entry,
132
143
  gitRemote: '',
133
144
  isPreview: true,
145
+ masterAppName: appName,
134
146
  });
135
147
  const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
136
148
  output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
@@ -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,CA8E3F"}
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,CAqF3F"}
@@ -5,10 +5,10 @@ import { execa } from 'execa';
5
5
  import { pathExists, readdir } from 'fs-extra';
6
6
  import { assertWorkspace } from '../util/workspace.mjs';
7
7
  import { validateAppName, mfAppName, cleanAppName } from '../util/app-name.mjs';
8
- import { fetchApp } from '../util/broker/app.mjs';
8
+ import { fetchApp, claimApp } from '../util/broker/app.mjs';
9
9
  import { patchBuiltBundle, patchOneFile } from '../util/dashboard-templates/patch-bundle.mjs';
10
10
  import { findTemplate } from '../util/dashboard-templates/load-catalog.mjs';
11
- import { initRepo, defaultS3Remote } from '../util/git/repo.mjs';
11
+ import { defaultS3Remote, initRepo } from '../util/git/repo.mjs';
12
12
  import { info, output } from '../util/logger.mjs';
13
13
 
14
14
  // Must match zth-backend-runner's route pattern — fail at generate time, not at first invocation.
@@ -133,6 +133,12 @@ async function generateCommand(appName, opts) {
133
133
  if (existing?.deployed) {
134
134
  throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
135
135
  }
136
+ // generate skips `create` entirely, but the preview it deploys below still resolves this
137
+ // app's own row by name server-side — claim it now so that lookup finds something, same as
138
+ // `create` does for the normal flow.
139
+ if (!existing) {
140
+ await claimApp(mfAppName(appName), defaultS3Remote(appName));
141
+ }
136
142
  info(`copying template "${template.id}" → ${appDir}`);
137
143
  // Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
138
144
  // needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
@@ -0,0 +1,5 @@
1
+ const usage = {
2
+
3
+ };
4
+
5
+ export { usage as default };
@@ -30,6 +30,8 @@ export interface RegisterOrUpdateOptions {
30
30
  gitRemote: string;
31
31
  integrations?: string[];
32
32
  description?: string;
33
+ /** For a preview app's first registration, the real app it previews. Ignored past v1. */
34
+ masterAppName?: string;
33
35
  }
34
36
  /**
35
37
  * v1 creates the app row (+ owner grant, prompting for visibility); later
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA6CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtF"}
@@ -47,11 +47,11 @@ async function claimApp(appName, gitRemote) {
47
47
  throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
48
48
  }
49
49
  }
50
- async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
50
+ async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description, masterAppName) {
51
51
  const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
52
52
  method: 'POST',
53
53
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
54
- body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
54
+ body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
55
55
  });
56
56
  if (!res.ok) {
57
57
  const text = await res.text();
@@ -77,12 +77,12 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
77
77
  * deploy and preview so this branching logic lives in exactly one place.
78
78
  */
79
79
  async function registerOrUpdateApp(opts) {
80
- const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
80
+ const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
81
81
  try {
82
82
  if (nextVersion === 1) {
83
83
  const visibility = await resolveAppVisibility(appName);
84
84
  info(`registering "${appName}" → v${nextVersion} (${visibility})`);
85
- await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
85
+ await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description, masterAppName);
86
86
  }
87
87
  else {
88
88
  info(`updating "${appName}" → v${nextVersion}`);
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Returns the machine app name when running as a service (e.g. agent-runner).
3
3
  * Available when the process is a monday.com microservice with `APP_NAME` set
4
- * by the platform runtime.
5
- *
6
- * Returns `undefined` for interactive human users (fallback to os.userInfo).
4
+ * by the platform runtime. Returns `undefined` for interactive human users.
7
5
  */
8
6
  export declare function getMachineAppName(): string | undefined;
7
+ export declare function whoAmI(): string;
9
8
  //# sourceMappingURL=machine-identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
1
+ {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../../src/util/machine-identity.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD;AAED,wBAAgB,MAAM,IAAI,MAAM,CAE/B"}
@@ -1,12 +1,15 @@
1
+ import os from 'node:os';
2
+
1
3
  /**
2
4
  * Returns the machine app name when running as a service (e.g. agent-runner).
3
5
  * Available when the process is a monday.com microservice with `APP_NAME` set
4
- * by the platform runtime.
5
- *
6
- * Returns `undefined` for interactive human users (fallback to os.userInfo).
6
+ * by the platform runtime. Returns `undefined` for interactive human users.
7
7
  */
8
8
  function getMachineAppName() {
9
9
  return process.env.APP_NAME || undefined;
10
10
  }
11
+ function whoAmI() {
12
+ return getMachineAppName() ?? os.userInfo().username;
13
+ }
11
14
 
12
- export { getMachineAppName };
15
+ export { getMachineAppName, whoAmI };
@@ -0,0 +1,7 @@
1
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
2
+
3
+ const usage = {
4
+
5
+ };
6
+
7
+ exports.default = usage;
@@ -30,6 +30,8 @@ export interface RegisterOrUpdateOptions {
30
30
  gitRemote: string;
31
31
  integrations?: string[];
32
32
  description?: string;
33
+ /** For a preview app's first registration, the real app it previews. Ignored past v1. */
34
+ masterAppName?: string;
33
35
  }
34
36
  /**
35
37
  * v1 creates the app row (+ owner grant, prompting for visibility); later
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA4CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AA6CD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtF"}
@@ -49,11 +49,11 @@ async function claimApp(appName, gitRemote) {
49
49
  throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
50
50
  }
51
51
  }
52
- async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description) {
52
+ async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description, masterAppName) {
53
53
  const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
54
54
  method: 'POST',
55
55
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
56
- body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
56
+ body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
57
57
  });
58
58
  if (!res.ok) {
59
59
  const text = await res.text();
@@ -79,12 +79,12 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
79
79
  * deploy and preview so this branching logic lives in exactly one place.
80
80
  */
81
81
  async function registerOrUpdateApp(opts) {
82
- const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
82
+ const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
83
83
  try {
84
84
  if (nextVersion === 1) {
85
85
  const visibility = await util_promptVisibility.resolveAppVisibility(appName);
86
86
  util_logger.info(`registering "${appName}" → v${nextVersion} (${visibility})`);
87
- await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
87
+ await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description, masterAppName);
88
88
  }
89
89
  else {
90
90
  util_logger.info(`updating "${appName}" → v${nextVersion}`);
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Returns the machine app name when running as a service (e.g. agent-runner).
3
3
  * Available when the process is a monday.com microservice with `APP_NAME` set
4
- * by the platform runtime.
5
- *
6
- * Returns `undefined` for interactive human users (fallback to os.userInfo).
4
+ * by the platform runtime. Returns `undefined` for interactive human users.
7
5
  */
8
6
  export declare function getMachineAppName(): string | undefined;
7
+ export declare function whoAmI(): string;
9
8
  //# sourceMappingURL=machine-identity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
1
+ {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../src/util/machine-identity.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD;AAED,wBAAgB,MAAM,IAAI,MAAM,CAE/B"}
@@ -1,14 +1,22 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
+ const os = require('node:os');
4
+
5
+ const _interopDefault = e => e && e.__esModule ? e : { default: e };
6
+
7
+ const os__default = /*#__PURE__*/_interopDefault(os);
8
+
3
9
  /**
4
10
  * Returns the machine app name when running as a service (e.g. agent-runner).
5
11
  * Available when the process is a monday.com microservice with `APP_NAME` set
6
- * by the platform runtime.
7
- *
8
- * Returns `undefined` for interactive human users (fallback to os.userInfo).
12
+ * by the platform runtime. Returns `undefined` for interactive human users.
9
13
  */
10
14
  function getMachineAppName() {
11
15
  return process.env.APP_NAME || undefined;
12
16
  }
17
+ function whoAmI() {
18
+ return getMachineAppName() ?? os__default.default.userInfo().username;
19
+ }
13
20
 
14
21
  exports.getMachineAppName = getMachineAppName;
22
+ exports.whoAmI = whoAmI;
@@ -58,35 +58,32 @@ function hashQueryShape(rawSql: string): string {
58
58
  }
59
59
 
60
60
  /**
61
- * The permissions-aware pool returns different rows to different people, so the
62
- * cache key has to carry whatever decides that — the caller's roles and user
63
- * group, exactly as bigbrain-zth keys it. Get this wrong and one user is served
64
- * another's rows. The stateless pool reads as a fixed service account, so every
65
- * caller shares one entry there.
61
+ * The permissions-aware pool returns different rows to different people —
62
+ * including different rows to two people with the same roles/user_group, since
63
+ * Snowflake now enforces column/row-level grants per user. The cache key must
64
+ * therefore scope to the individual caller, exactly as bigbrain-zth keys it.
65
+ * The stateless pool reads as a fixed service account, so every caller shares
66
+ * one entry there.
66
67
  */
67
68
  /**
68
69
  * Returns the cache scope key for the current caller, or null when we must not
69
70
  * cache at all.
70
71
  *
71
- * On the permissions-aware path we hash the caller's roles + user_group, mirroring
72
+ * On the permissions-aware path we scope to the caller's user id, mirroring
72
73
  * bigbrain-zth exactly. If there is no execution context (e.g. the call arrived
73
74
  * outside a normal invoke flow) we fail **closed**: skipping both the cache read
74
75
  * and the write prevents an unauthenticated scope-collapse where every caller
75
- * would share one entry keyed by `hash('|')`.
76
+ * would share one entry.
76
77
  */
77
78
  function cacheScope(useStateless: boolean): string | null {
78
79
  if (useStateless) {
79
80
  return 'stateless';
80
81
  }
81
- const user = context.execution.get()?.user as { roles?: string[]; user_group?: string } | undefined;
82
- if (!user) {
82
+ const user = context.execution.get()?.user as { id?: number } | undefined;
83
+ if (!user?.id) {
83
84
  return null;
84
85
  }
85
- const roles = [...(user.roles ?? [])].sort((a, b) => a.localeCompare(b)).join(',');
86
- return createHash('sha256')
87
- .update(`${roles}|${user.user_group ?? ''}`)
88
- .digest('hex')
89
- .slice(0, 16);
86
+ return `user:${user.id}`;
90
87
  }
91
88
 
92
89
  /**
@@ -0,0 +1,26 @@
1
+ {
2
+ "db.delete": [
3
+ "key"
4
+ ],
5
+ "db.get": [
6
+ "key"
7
+ ],
8
+ "db.list": [],
9
+ "db.put": [
10
+ "key",
11
+ "value"
12
+ ],
13
+ "llm.complete": [
14
+ "modelId",
15
+ "messages"
16
+ ],
17
+ "monday.query": [
18
+ "query",
19
+ "variables"
20
+ ],
21
+ "snowflake.query": [
22
+ "sql",
23
+ "params",
24
+ "options"
25
+ ]
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.29.5",
3
+ "version": "0.31.0",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -5,6 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import { readHandlers } from '../handlers';
7
7
 
8
+ vi.mock('../parser/api-signatures', () => ({
9
+ loadApiSignatures: () => ({ 'monday.query': ['query', 'variables'] }),
10
+ }));
8
11
  vi.mock('../../util/tracker', () => ({
9
12
  trackEvent: vi.fn().mockResolvedValue(undefined),
10
13
  trackDuration: vi.fn(),
@@ -28,14 +31,14 @@ describe('readHandlers — discovery', () => {
28
31
  it('returns empty handlers when backend/handlers does not exist', async () => {
29
32
  const dir = await mkdtemp(path.join(os.tmpdir(), 'z2h-empty-'));
30
33
  try {
31
- await expect(readHandlers(dir)).resolves.toEqual({ handlers: {}, apiDomains: [] });
34
+ await expect(readHandlers(dir)).resolves.toEqual({ handlers: {}, apiDomains: [], apiUsages: {} });
32
35
  } finally {
33
36
  await rm(dir, { recursive: true, force: true });
34
37
  }
35
38
  });
36
39
 
37
40
  it('returns empty handlers when the directory is empty', async () => {
38
- await expect(readHandlers(appDir)).resolves.toEqual({ handlers: {}, apiDomains: [] });
41
+ await expect(readHandlers(appDir)).resolves.toEqual({ handlers: {}, apiDomains: [], apiUsages: {} });
39
42
  });
40
43
 
41
44
  it('uses the filename (without .js) as the handler name', async () => {
@@ -69,6 +72,14 @@ describe('readHandlers — output contract', () => {
69
72
  const { apiDomains } = await readHandlers(appDir);
70
73
  expect(apiDomains).toEqual(['db', 'monday']);
71
74
  });
75
+
76
+ it('attaches per-handler apiUsages keyed by handler name', async () => {
77
+ await writeHandler('a.js', 'async function handler(ctx) { return ctx.api.v1.monday.query("x"); }');
78
+ await writeHandler('c.js', 'async function handler(ctx) { return ctx.input; }');
79
+ const { apiUsages } = await readHandlers(appDir);
80
+ expect(apiUsages.a).toEqual({ monday: { query: [{ query: 'x' }] } });
81
+ expect(apiUsages.c).toEqual({});
82
+ });
72
83
  });
73
84
 
74
85
  // Per-shape validation rules are covered in validate.test.ts; here we only prove
@@ -3,6 +3,12 @@ import { warn } from '../../util/logger';
3
3
  import { validateHandlerSource } from '../validate';
4
4
 
5
5
  vi.mock('../../util/logger');
6
+ vi.mock('../parser/api-signatures', () => ({
7
+ loadApiSignatures: () => ({
8
+ 'snowflake.query': ['sql', 'params', 'options'],
9
+ 'monday.query': ['query', 'variables'],
10
+ }),
11
+ }));
6
12
  vi.mock('../../util/tracker', () => ({
7
13
  trackEvent: vi.fn().mockResolvedValue(undefined),
8
14
  trackDuration: vi.fn(),
@@ -203,3 +209,51 @@ describe('validateHandlerSource — ctx.api.v1 domains', () => {
203
209
  expect(warnMock).toHaveBeenCalledTimes(1);
204
210
  });
205
211
  });
212
+
213
+ describe('validateHandlerSource — apiUsages (per-call domain/method/args)', () => {
214
+ it('records each call keyed by parameter name — static strings resolved, dynamic object values kept as source text', () => {
215
+ const { apiUsages } = validate(
216
+ 'async function handler(ctx) { return ctx.api.v1.snowflake.query("SELECT 1", { userId: ctx.caller.id }); }'
217
+ );
218
+ expect(apiUsages).toEqual({ snowflake: { query: [{ sql: 'SELECT 1', params: { userId: 'ctx.caller.id' } }] } });
219
+ });
220
+
221
+ it('keeps multiple calls to the same method as separate entries', () => {
222
+ const { apiUsages } = validate(
223
+ 'async function handler(ctx) { await ctx.api.v1.snowflake.query("SELECT 1"); return ctx.api.v1.snowflake.query("SELECT 2"); }'
224
+ );
225
+ expect(apiUsages.snowflake.query).toEqual([{ sql: 'SELECT 1' }, { sql: 'SELECT 2' }]);
226
+ });
227
+
228
+ it('falls back to source text for a dynamic argument and a dynamic array element', () => {
229
+ const { apiUsages } = validate(
230
+ 'async function handler(ctx) { const q = ctx.input.q; return ctx.api.v1.monday.query(q, [ctx.input.id]); }'
231
+ );
232
+ expect(apiUsages.monday.query).toEqual([{ query: 'ctx.input.q', variables: ['ctx.input.id'] }]);
233
+ });
234
+
235
+ it('resolves a same-scope const alias back to its ctx.api.v1 chain', () => {
236
+ const { apiUsages } = validate(
237
+ 'async function handler(ctx) { const d = ctx.api.v1.snowflake; return d.query("SELECT 1"); }'
238
+ );
239
+ expect(apiUsages).toEqual({ snowflake: { query: [{ sql: 'SELECT 1' }] } });
240
+ });
241
+
242
+ it('returns {} for a handler with no ctx.api.v1 calls', () => {
243
+ expect(validate('async function handler(ctx) { return ctx.input; }').apiUsages).toEqual({});
244
+ });
245
+
246
+ it('tracks a call reached through a computed string-literal member access', () => {
247
+ const { apiUsages } = validate(
248
+ 'async function handler(ctx) { return ctx.api.v1.snowflake[\'query\']("SELECT 1"); }'
249
+ );
250
+ expect(apiUsages).toEqual({ snowflake: { query: [{ sql: 'SELECT 1' }] } });
251
+ });
252
+
253
+ it('does not track a call reached through a dynamic member access', () => {
254
+ const { apiUsages } = validate(
255
+ 'async function handler(ctx) { const m = "query"; return ctx.api.v1.snowflake[m]("SELECT 1"); }'
256
+ );
257
+ expect(apiUsages).toEqual({});
258
+ });
259
+ });