@teleporthq/teleport-plugin-next-workflows 0.43.35 → 0.43.37

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 (50) hide show
  1. package/__tests__/ai-error-contract.test.ts +149 -0
  2. package/__tests__/data-api-safe-query.test.ts +16 -16
  3. package/__tests__/node-handler-file-inline-map.test.ts +114 -0
  4. package/__tests__/raw-query-param-binding.test.ts +18 -17
  5. package/__tests__/streaming-on-end-env-filter.test.ts +405 -0
  6. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  7. package/dist/cjs/api-route-generator.js +14 -8
  8. package/dist/cjs/api-route-generator.js.map +1 -1
  9. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  10. package/dist/cjs/data-api-route-generator.js +13 -2
  11. package/dist/cjs/data-api-route-generator.js.map +1 -1
  12. package/dist/cjs/executor-generator.d.ts.map +1 -1
  13. package/dist/cjs/executor-generator.js +47 -5
  14. package/dist/cjs/executor-generator.js.map +1 -1
  15. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  16. package/dist/cjs/nodes/general/general-custom-js.js +7 -0
  17. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  18. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  19. package/dist/cjs/workflow-component-plugin.js +9 -3
  20. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  21. package/dist/cjs/workflow-project-plugin.d.ts +1 -0
  22. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  23. package/dist/cjs/workflow-project-plugin.js +83 -5
  24. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  25. package/dist/esm/api-route-generator.d.ts.map +1 -1
  26. package/dist/esm/api-route-generator.js +14 -8
  27. package/dist/esm/api-route-generator.js.map +1 -1
  28. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  29. package/dist/esm/data-api-route-generator.js +13 -2
  30. package/dist/esm/data-api-route-generator.js.map +1 -1
  31. package/dist/esm/executor-generator.d.ts.map +1 -1
  32. package/dist/esm/executor-generator.js +47 -5
  33. package/dist/esm/executor-generator.js.map +1 -1
  34. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  35. package/dist/esm/nodes/general/general-custom-js.js +7 -0
  36. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  37. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  38. package/dist/esm/workflow-component-plugin.js +9 -3
  39. package/dist/esm/workflow-component-plugin.js.map +1 -1
  40. package/dist/esm/workflow-project-plugin.d.ts +1 -0
  41. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  42. package/dist/esm/workflow-project-plugin.js +83 -5
  43. package/dist/esm/workflow-project-plugin.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/api-route-generator.ts +14 -8
  46. package/src/data-api-route-generator.ts +13 -2
  47. package/src/executor-generator.ts +47 -5
  48. package/src/nodes/general/general-custom-js.ts +8 -0
  49. package/src/workflow-component-plugin.ts +15 -10
  50. package/src/workflow-project-plugin.ts +87 -4
@@ -331,8 +331,8 @@ module.exports = async function handler(req, res) {
331
331
  res.status(earlyRes.status || 500).json(earlyRes.body || {});
332
332
  return;
333
333
  }
334
- if (result && (result.success === false || (typeof result.error === 'string' && result.error))) {
335
- throw new Error(result.error || 'Node execution failed');
334
+ if (utils.isFatalNodeResult(result)) {
335
+ throw new Error(utils.fatalNodeResultMessage(result));
336
336
  }
337
337
  context[node.id] = result;
338
338
  if (result && result.__terminal) break;
@@ -680,6 +680,12 @@ module.exports = async function handler(req, res) {
680
680
  res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: sn.id, result: snResult }) + '\\n\\n');
681
681
  }
682
682
  });
683
+ // A provider/auth failure surfaces as { error: true, message, code }.
684
+ // Without this gate the on-end branch would run against a failed AI
685
+ // result (e.g. persist a NULL chat answer → NOT NULL 500 downstream).
686
+ if (utils.isFatalNodeResult(result)) {
687
+ throw new Error(utils.fatalNodeResultMessage(result));
688
+ }
683
689
  context[node.id] = result;
684
690
  res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: node.id, result: result }) + '\\n\\n');
685
691
 
@@ -712,8 +718,8 @@ module.exports = async function handler(req, res) {
712
718
  }
713
719
  return;
714
720
  }
715
- if (result && (result.success === false || (typeof result.error === 'string' && result.error))) {
716
- throw new Error(result.error || 'Node execution failed');
721
+ if (utils.isFatalNodeResult(result)) {
722
+ throw new Error(utils.fatalNodeResultMessage(result));
717
723
  }
718
724
  context[node.id] = result;
719
725
  if (streamStarted) {
@@ -925,8 +931,8 @@ module.exports = async function handler(req, res) {
925
931
  res.status(earlyRes.status || 500).json(earlyRes.body || {});
926
932
  return;
927
933
  }
928
- if (result && (result.success === false || (typeof result.error === 'string' && result.error))) {
929
- throw new Error(result.error || 'Node execution failed');
934
+ if (utils.isFatalNodeResult(result)) {
935
+ throw new Error(utils.fatalNodeResultMessage(result));
930
936
  }
931
937
  context[node.id] = result;
932
938
  if (result && result.__terminal) break;
@@ -1225,8 +1231,8 @@ const generateNodeExecutionLoop = (
1225
1231
  res.status(earlyRes.status || 500).json(earlyRes.body || {});
1226
1232
  return;
1227
1233
  }${customNodeBlock}
1228
- if (result && (result.success === false || (typeof result.error === 'string' && result.error))) {
1229
- throw new Error(result.error || 'Node execution failed');
1234
+ if (utils.isFatalNodeResult(result)) {
1235
+ throw new Error(utils.fatalNodeResultMessage(result));
1230
1236
  }
1231
1237
  context[node.id] = result;
1232
1238
  if (result && result.__terminal) break;
@@ -159,12 +159,23 @@ function validateFilters(filters) {
159
159
  // 22003 (numeric value out of range), 22023 (invalid parameter value).
160
160
  // We pass the original query / params back to the caller as a debug
161
161
  // label so log output stays diagnosable.
162
- var SAFE_COERCION_ERROR_CODES = { '22P02': 1, '22008': 1, '22003': 1, '22023': 1 };
162
+ // Also covers 22007 (invalid_datetime_format an empty "Filter by date" value
163
+ // hitting a date column: invalid input syntax for type date). Beyond the code
164
+ // list we ALSO suppress ANY "invalid input syntax for type" message: every one
165
+ // means the bound value could never coerce to the column type, so no row can
166
+ // match and an empty result is the correct semantics — never a 500 the user sees
167
+ // as "Failed to update. Please try again".
168
+ var SAFE_COERCION_ERROR_CODES = { '22P02': 1, '22007': 1, '22008': 1, '22003': 1, '22023': 1 };
169
+ function isSafeCoercionError(err) {
170
+ if (!err) return false;
171
+ if (SAFE_COERCION_ERROR_CODES[err.code]) return true;
172
+ return typeof err.message === 'string' && /invalid input syntax for type/i.test(err.message);
173
+ }
163
174
  async function safeQuery(client, sql, params, mode) {
164
175
  try {
165
176
  return await client.query(sql, params);
166
177
  } catch (err) {
167
- if (err && SAFE_COERCION_ERROR_CODES[err.code]) {
178
+ if (isSafeCoercionError(err)) {
168
179
  console.warn('[data-api] suppressed Postgres coercion error (' + err.code + '): ' + (err.message || err) +
169
180
  ' — returning empty result for ' + (mode || 'query') + '. SQL=' + sql + ' params=' + JSON.stringify(params));
170
181
  if (mode === 'count') {
@@ -586,6 +586,24 @@ function topoSortNodes(nodes, edges) {
586
586
  return sorted;
587
587
  }
588
588
 
589
+ // A node result signals failure either through the legacy string contract
590
+ // ({ error: '...' } / { success: false }) or through the AI-node contract
591
+ // ({ error: true, message, code } — provider/auth failures). Both must halt
592
+ // the workflow; treating error:true as success used to let the pipeline limp
593
+ // into downstream NOT NULL violations.
594
+ function isFatalNodeResult(result) {
595
+ if (!result) return false;
596
+ return result.success === false ||
597
+ (typeof result.error === 'string' && result.error) ||
598
+ result.error === true;
599
+ }
600
+
601
+ function fatalNodeResultMessage(result) {
602
+ if (typeof result.error === 'string' && result.error) return result.error;
603
+ if (typeof result.message === 'string' && result.message) return result.message;
604
+ return 'Node execution failed';
605
+ }
606
+
589
607
  async function executeNodes(nodes, edges, context, nodeHandlers, workflowConfig, callServerSegment, executionId) {
590
608
  const executed = {};
591
609
  const sortedNodes = topoSortNodes(nodes, edges);
@@ -684,6 +702,9 @@ async function executeNodes(nodes, edges, context, nodeHandlers, workflowConfig,
684
702
  await executeNodes(onStreamNodes, edges, context, nodeHandlers, workflowConfig, callServerSegment, executionId);
685
703
  }
686
704
  });
705
+ if (isFatalNodeResult(streamResult)) {
706
+ throw new Error(fatalNodeResultMessage(streamResult));
707
+ }
687
708
  context[node.id] = streamResult;
688
709
 
689
710
  if (onEndNodes.length > 0) {
@@ -726,8 +747,8 @@ async function executeNodes(nodes, edges, context, nodeHandlers, workflowConfig,
726
747
  throw earlyErr;
727
748
  }
728
749
 
729
- if (result && (result.success === false || (typeof result.error === 'string' && result.error))) {
730
- throw new Error(result.error || 'Node execution failed');
750
+ if (isFatalNodeResult(result)) {
751
+ throw new Error(fatalNodeResultMessage(result));
731
752
  }
732
753
 
733
754
  context[node.id] = result;
@@ -1006,7 +1027,9 @@ module.exports = {
1006
1027
  executeParallel,
1007
1028
  collectBranchNodes,
1008
1029
  markAllBranchNodes,
1009
- isStreamingAINode
1030
+ isStreamingAINode,
1031
+ isFatalNodeResult,
1032
+ fatalNodeResultMessage
1010
1033
  };
1011
1034
  `
1012
1035
  }
@@ -1200,6 +1223,19 @@ function findStreamingAINodes(allNodes, allEdges) {
1200
1223
  return map;
1201
1224
  }
1202
1225
 
1226
+ // A streaming AI node's on-stream / on-end branches are collected from the
1227
+ // FULL workflow node list, so they may contain server nodes (e.g. the SQL
1228
+ // insert persisting a chat answer). Those already execute inside their own
1229
+ // server segments with full configs; the browser bundle only ships their
1230
+ // config redacted down to the client-safe whitelist (segment-splitter's
1231
+ // redactServerNodeConfig), so executing them here with CLIENT handlers both
1232
+ // double-executes them and crashes on the missing config. Emitted nodes carry
1233
+ // executionEnv for exactly this filter; nodes without the marker (older
1234
+ // bundles) keep executing client-side as before.
1235
+ function clientExecutableBranchNodes(branchNodes) {
1236
+ return branchNodes.filter(function(n) { return !n || n.executionEnv !== 'server'; });
1237
+ }
1238
+
1203
1239
  async function callStreamingServerSegment(segmentUrl, context, streamingInfo, allNodes, allEdges, clientHandlers, workflowConfig, executionId) {
1204
1240
  const prunedContext = pruneContext(context);
1205
1241
  const handledNodeIds = {};
@@ -1254,7 +1290,10 @@ async function callStreamingServerSegment(segmentUrl, context, streamingInfo, al
1254
1290
  context[data.nodeId] = { chunk: data.chunk, fullResponse: data.fullResponse, model: data.model };
1255
1291
  const info = streamingInfo[data.nodeId];
1256
1292
  if (info && info.onStreamNodes.length > 0) {
1257
- await utils.executeNodes(info.onStreamNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1293
+ const runnableStreamNodes = clientExecutableBranchNodes(info.onStreamNodes);
1294
+ if (runnableStreamNodes.length > 0) {
1295
+ await utils.executeNodes(runnableStreamNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1296
+ }
1258
1297
  for (let sni = 0; sni < info.onStreamNodes.length; sni++) {
1259
1298
  handledNodeIds[info.onStreamNodes[sni].id] = true;
1260
1299
  }
@@ -1269,7 +1308,10 @@ async function callStreamingServerSegment(segmentUrl, context, streamingInfo, al
1269
1308
  for (let sk = 0; sk < streamedKeys.length; sk++) {
1270
1309
  const endInfo = streamingInfo[streamedKeys[sk]];
1271
1310
  if (endInfo && endInfo.onEndNodes.length > 0) {
1272
- await utils.executeNodes(endInfo.onEndNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1311
+ const runnableEndNodes = clientExecutableBranchNodes(endInfo.onEndNodes);
1312
+ if (runnableEndNodes.length > 0) {
1313
+ await utils.executeNodes(runnableEndNodes, allEdges, context, clientHandlers, workflowConfig, null, executionId);
1314
+ }
1273
1315
  for (let eni = 0; eni < endInfo.onEndNodes.length; eni++) {
1274
1316
  handledNodeIds[endInfo.onEndNodes[eni].id] = true;
1275
1317
  }
@@ -23,6 +23,14 @@ import { NodeHandlerGenerator, handlerToString } from '../types'
23
23
  async function general_custom_js(config: any, context: Record<string, unknown>) {
24
24
  const code = config.code
25
25
 
26
+ // A server-classified custom-js node's config is redacted to the client-safe
27
+ // whitelist ({} here) before it ships to the browser. If such a node ever
28
+ // reaches a client handler (defense-in-depth — the runtime filters them),
29
+ // no-op cleanly instead of crashing the whole workflow on `code.length`.
30
+ if (typeof code !== 'string' || code.length === 0) {
31
+ return {}
32
+ }
33
+
26
34
  // Reserved context keys that must never leak into params / innerParams.
27
35
  // Anything starting with "__" is internal scaffolding; these named entries
28
36
  // are existing well-known runtime keys.
@@ -1308,16 +1308,21 @@ function __normalizeAdminFormRow(row, defaults) {
1308
1308
  }))
1309
1309
  )
1310
1310
  const nodesJson = JSON.stringify(
1311
- wf.nodes.map((n: UIDLWorkflowNode) => ({
1312
- id: n.id,
1313
- type: n.type,
1314
- config: redactServerNodeConfig(
1315
- n.config,
1316
- nodeEnvById.get(n.id) ?? resolveNodeExecutionEnv(n)
1317
- ),
1318
- stepNumber: n.stepNumber,
1319
- label: n.label,
1320
- }))
1311
+ wf.nodes.map((n: UIDLWorkflowNode) => {
1312
+ // Runtime marker consumed by clientExecutableBranchNodes (client
1313
+ // runtime): a streaming AI node's on-stream/on-end branches are
1314
+ // collected from this full node list, and server nodes in them must
1315
+ // never execute with client handlers (their config is redacted below).
1316
+ const env = nodeEnvById.get(n.id) ?? resolveNodeExecutionEnv(n)
1317
+ return {
1318
+ id: n.id,
1319
+ type: n.type,
1320
+ config: redactServerNodeConfig(n.config, env),
1321
+ stepNumber: n.stepNumber,
1322
+ label: n.label,
1323
+ executionEnv: env,
1324
+ }
1325
+ })
1321
1326
  )
1322
1327
  const edgesJson = JSON.stringify(
1323
1328
  wf.edges.map((e: UIDLWorkflowEdge) => ({
@@ -887,18 +887,97 @@ export class NextWorkflowProjectPlugin implements ProjectPlugin {
887
887
  return ''
888
888
  }
889
889
 
890
- const exports = exportNames.map((t) => ` '${t}': ${t.replace(/-/g, '_')}`).join(',\n')
890
+ // Inline each handler as its `module.exports` map value inside a
891
+ // self-invoking function that returns the handler's entry-point, rather
892
+ // than emitting separate top-level `async function <name>` declarations
893
+ // plus a map that references them by name. `handlers` and `exportNames`
894
+ // are populated together in the loop above, so `handlers[index]` is
895
+ // exactly the handler for `exportNames[index]`.
896
+ //
897
+ // Why the IIFE (not a bare inline expression): a handler is NOT always a
898
+ // single function expression. AI nodes emit shared provider utils
899
+ // (`var __ai_… = …`) BEFORE their entry function, and payment/rate-limiter
900
+ // nodes append helper functions AFTER it. Splicing such a multi-statement
901
+ // string directly as an object-literal value (`'ai-custom-prompt': var … `)
902
+ // is a SyntaxError. Wrapping in `(function () { <handler>; return <fn>; })()`
903
+ // runs every statement in an isolated scope and yields the entry function as
904
+ // the value — so single- AND multi-statement handlers are both valid.
905
+ //
906
+ // Why not top-level declarations + a name reference: that forward-reference
907
+ // shape is fragile in production. If a bundler/minifier drops a single-use
908
+ // declaration (or a handler's emitted name ever drifts from its
909
+ // `nodeType`-derived key), the map is left referencing an undefined
910
+ // identifier — `ReferenceError: <name> is not defined` at MODULE-EVAL time,
911
+ // which fails the Next.js/Vercel build during "Collecting page data". The
912
+ // returned identifier is derived from the same `t` that keys the map and is
913
+ // defined by the handler string right above the `return`, so there is no
914
+ // cross-statement forward reference to dangle. The entry-function naming
915
+ // contract (`handlerToString(fn)` preserves `fn.name`, and every handler
916
+ // names its entry `nodeType.replace(/-/g, '_')`) is enforced by
917
+ // node-handler-file-inline-map.test.ts across the whole node registry.
918
+ //
919
+ // That contract holds for the handler's OWN source (checked below via
920
+ // resolveHandlerEntryName), but not necessarily for the RUNTIME name a
921
+ // caller's bundler assigns it. Handlers built from `handlerToString(fn)`
922
+ // embed `fn.toString()` — a snapshot of whatever `fn` was actually named
923
+ // at the moment it's read. When this package is bundled and minified by a
924
+ // consumer (e.g. teleport-gui's browser packer worker, built with
925
+ // Next.js/Terser), the minifier freely renames `fn`'s declaration: nothing
926
+ // in the bundle calls it by name, only `.toString()` reads it at runtime,
927
+ // which is invisible to the minifier. The naive
928
+ // `nodeType.replace(/-/g, '_')` name would then reference an identifier
929
+ // that no longer exists in the embedded source — a Vercel-only
930
+ // "Collecting page data" ReferenceError, since local dev never runs a
931
+ // minifier. resolveHandlerEntryName reads the name the source text
932
+ // ACTUALLY declares instead of assuming it matches the convention.
933
+ const entries = exportNames
934
+ .map((t, index) => {
935
+ const source = handlers[index].trim()
936
+ const entryFn = this.resolveHandlerEntryName(source, t)
937
+ return ` '${t}': (function () {\n${source}\nreturn ${entryFn};\n})()`
938
+ })
939
+ .join(',\n')
891
940
 
892
941
  return `// Auto-generated workflow node handlers (${env})
893
942
 
894
- ${handlers.join('\n\n')}
895
-
896
943
  module.exports = {
897
- ${exports}
944
+ ${entries}
898
945
  };
899
946
  `
900
947
  }
901
948
 
949
+ // Resolves the name a handler's entry function is ACTUALLY declared with in
950
+ // `source`, rather than assuming it always equals the
951
+ // `nodeType.replace(/-/g, '_')` convention name (see the comment above
952
+ // `generateNodeHandlerFile`'s `entries` for why that assumption can break
953
+ // under minification).
954
+ //
955
+ // Handlers composed from string-literal templates (not `fn.toString()`) are
956
+ // immune to that class of breakage — a minifier never rewrites the contents
957
+ // of a string literal — so the convention name is still correct for those;
958
+ // this only needs to look further when it's genuinely absent from `source`.
959
+ private resolveHandlerEntryName(source: string, nodeType: string): string {
960
+ const conventionName = nodeType.replace(/-/g, '_')
961
+ const conventionNameUsed = new RegExp(`(?:^|[^\\w$])${conventionName}\\s*\\(`).test(source)
962
+ if (conventionNameUsed) {
963
+ return conventionName
964
+ }
965
+
966
+ // `source` came from `handlerToString(fn)` on a real, possibly-minified
967
+ // function — its ENTIRE text is that one function's declaration, so
968
+ // whatever name follows the leading `function`/`async function` keyword
969
+ // is the name it was actually assigned at runtime.
970
+ const leadingDeclaration = source.match(/^(?:async\s+)?function\s*([A-Za-z0-9_$]+)\s*\(/)
971
+ if (leadingDeclaration) {
972
+ return leadingDeclaration[1]
973
+ }
974
+
975
+ throw new Error(
976
+ `generateNodeHandlerFile: could not resolve the entry function for workflow node type "${nodeType}" — ` +
977
+ `expected a "${conventionName}" declaration or a single toString()'d function, found neither.`
978
+ )
979
+ }
980
+
902
981
  private getDefaultRouteUrl(uidl: ProjectUIDL, strategy: ProjectStrategy): string | null {
903
982
  const routeDef = uidl.root?.stateDefinitions?.route
904
983
  if (!routeDef?.values || !routeDef.defaultValue) {
@@ -972,6 +1051,10 @@ ${exports}
972
1051
  (cn.nodes || []).map((n: any) => ({
973
1052
  ...n,
974
1053
  config: redactServerNodeConfig(n.config, resolveNodeExecutionEnv(n)),
1054
+ // Runtime marker consumed by clientExecutableBranchNodes (client
1055
+ // runtime) so streaming on-stream/on-end branches never execute
1056
+ // server nodes (whose config was just redacted) client-side.
1057
+ executionEnv: resolveNodeExecutionEnv(n),
975
1058
  }))
976
1059
  )
977
1060
  const edgesJson = JSON.stringify(cn.edges)