@teleporthq/teleport-plugin-next-workflows 0.43.45 → 0.43.47

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 (112) hide show
  1. package/__tests__/_helpers/run-generated-middleware.ts +130 -0
  2. package/__tests__/auth-env-secret-preservation.test.ts +35 -0
  3. package/__tests__/auth-nextauth-url.test.ts +28 -0
  4. package/__tests__/auth-session-cost.test.ts +614 -0
  5. package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
  6. package/__tests__/data-node-not-awaited.test.ts +644 -0
  7. package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
  8. package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
  9. package/__tests__/form-submitted-trigger-context.test.ts +100 -0
  10. package/__tests__/middleware-guest-order-details.test.ts +258 -0
  11. package/__tests__/middleware-row-owned-pages.test.ts +32 -9
  12. package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
  13. package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
  14. package/__tests__/raw-sql-param-binding.test.ts +82 -0
  15. package/__tests__/reserved-word-state-binding.test.ts +112 -0
  16. package/__tests__/strip-event-handler-attrs.test.ts +59 -0
  17. package/__tests__/workflow-auth-guard.test.ts +277 -0
  18. package/dist/cjs/api-route-generator.d.ts +3 -3
  19. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  20. package/dist/cjs/api-route-generator.js +136 -11
  21. package/dist/cjs/api-route-generator.js.map +1 -1
  22. package/dist/cjs/auth-generator.d.ts +67 -1
  23. package/dist/cjs/auth-generator.d.ts.map +1 -1
  24. package/dist/cjs/auth-generator.js +573 -31
  25. package/dist/cjs/auth-generator.js.map +1 -1
  26. package/dist/cjs/await-result.d.ts +31 -0
  27. package/dist/cjs/await-result.d.ts.map +1 -0
  28. package/dist/cjs/await-result.js +50 -0
  29. package/dist/cjs/await-result.js.map +1 -0
  30. package/dist/cjs/executor-generator.d.ts.map +1 -1
  31. package/dist/cjs/executor-generator.js +150 -1
  32. package/dist/cjs/executor-generator.js.map +1 -1
  33. package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
  34. package/dist/cjs/nodes/account/account-get-current.js +44 -11
  35. package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
  36. package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
  37. package/dist/cjs/nodes/account/account-login.js +10 -1
  38. package/dist/cjs/nodes/account/account-login.js.map +1 -1
  39. package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
  40. package/dist/cjs/raw-sql-param-binding.js +32 -0
  41. package/dist/cjs/raw-sql-param-binding.js.map +1 -1
  42. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  43. package/dist/cjs/segment-splitter.js +15 -7
  44. package/dist/cjs/segment-splitter.js.map +1 -1
  45. package/dist/cjs/trigger-generator.js +7 -1
  46. package/dist/cjs/trigger-generator.js.map +1 -1
  47. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  48. package/dist/cjs/workflow-auth-generator.d.ts +26 -0
  49. package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
  50. package/dist/cjs/workflow-auth-generator.js +166 -0
  51. package/dist/cjs/workflow-auth-generator.js.map +1 -0
  52. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  53. package/dist/cjs/workflow-component-plugin.js +124 -17
  54. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  55. package/dist/cjs/workflow-project-plugin.d.ts +2 -1
  56. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  57. package/dist/cjs/workflow-project-plugin.js +170 -20
  58. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  59. package/dist/esm/api-route-generator.d.ts +3 -3
  60. package/dist/esm/api-route-generator.d.ts.map +1 -1
  61. package/dist/esm/api-route-generator.js +136 -11
  62. package/dist/esm/api-route-generator.js.map +1 -1
  63. package/dist/esm/auth-generator.d.ts +67 -1
  64. package/dist/esm/auth-generator.d.ts.map +1 -1
  65. package/dist/esm/auth-generator.js +568 -30
  66. package/dist/esm/auth-generator.js.map +1 -1
  67. package/dist/esm/await-result.d.ts +31 -0
  68. package/dist/esm/await-result.d.ts.map +1 -0
  69. package/dist/esm/await-result.js +45 -0
  70. package/dist/esm/await-result.js.map +1 -0
  71. package/dist/esm/executor-generator.d.ts.map +1 -1
  72. package/dist/esm/executor-generator.js +150 -1
  73. package/dist/esm/executor-generator.js.map +1 -1
  74. package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
  75. package/dist/esm/nodes/account/account-get-current.js +44 -11
  76. package/dist/esm/nodes/account/account-get-current.js.map +1 -1
  77. package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
  78. package/dist/esm/nodes/account/account-login.js +10 -1
  79. package/dist/esm/nodes/account/account-login.js.map +1 -1
  80. package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
  81. package/dist/esm/raw-sql-param-binding.js +32 -0
  82. package/dist/esm/raw-sql-param-binding.js.map +1 -1
  83. package/dist/esm/segment-splitter.d.ts.map +1 -1
  84. package/dist/esm/segment-splitter.js +15 -7
  85. package/dist/esm/segment-splitter.js.map +1 -1
  86. package/dist/esm/trigger-generator.js +7 -1
  87. package/dist/esm/trigger-generator.js.map +1 -1
  88. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  89. package/dist/esm/workflow-auth-generator.d.ts +26 -0
  90. package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
  91. package/dist/esm/workflow-auth-generator.js +161 -0
  92. package/dist/esm/workflow-auth-generator.js.map +1 -0
  93. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  94. package/dist/esm/workflow-component-plugin.js +125 -18
  95. package/dist/esm/workflow-component-plugin.js.map +1 -1
  96. package/dist/esm/workflow-project-plugin.d.ts +2 -1
  97. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  98. package/dist/esm/workflow-project-plugin.js +169 -20
  99. package/dist/esm/workflow-project-plugin.js.map +1 -1
  100. package/package.json +4 -4
  101. package/src/api-route-generator.ts +139 -11
  102. package/src/auth-generator.ts +600 -30
  103. package/src/await-result.ts +52 -0
  104. package/src/executor-generator.ts +150 -1
  105. package/src/nodes/account/account-get-current.ts +44 -9
  106. package/src/nodes/account/account-login.ts +12 -1
  107. package/src/raw-sql-param-binding.ts +33 -0
  108. package/src/segment-splitter.ts +15 -7
  109. package/src/trigger-generator.ts +7 -1
  110. package/src/workflow-auth-generator.ts +187 -0
  111. package/src/workflow-component-plugin.ts +146 -21
  112. package/src/workflow-project-plugin.ts +186 -19
@@ -0,0 +1,277 @@
1
+ import {
2
+ generateWorkflowAuthHelperFile,
3
+ buildWorkflowAuthInjection,
4
+ } from '../src/workflow-auth-generator'
5
+ import {
6
+ generateServerSegmentAPIRoute,
7
+ generateStreamingServerSegmentAPIRoute,
8
+ } from '../src/api-route-generator'
9
+ import { UIDLWorkflowProtection } from '@teleporthq/teleport-types'
10
+ import { WorkflowSegment } from '../src/types'
11
+
12
+ // The generated workflow-auth.js guard is the single stateless enforcement
13
+ // point. Booting it with a mocked next-auth/jwt proves the runtime behaviour;
14
+ // the route generators prove the guard is wired in BEFORE any node runs.
15
+
16
+ type Guard = (
17
+ req: any,
18
+ context: any,
19
+ policy: any
20
+ ) => Promise<{ status: number; message: string } | null>
21
+
22
+ /** Boots the emitted guard file with getToken stubbed to read `req.__token`. */
23
+ function bootGuard(secret: string | undefined): Guard {
24
+ const code = generateWorkflowAuthHelperFile()
25
+ const moduleObj: { exports: any } = { exports: {} }
26
+ const fakeRequire = (name: string): any => {
27
+ if (name === 'next-auth/jwt') {
28
+ return { getToken: async ({ req }: any) => (req && req.__token) || null }
29
+ }
30
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
31
+ return require(name)
32
+ }
33
+ // tslint:disable-next-line:function-constructor
34
+ new Function('require', 'module', 'exports', 'process', code)(
35
+ fakeRequire,
36
+ moduleObj,
37
+ moduleObj.exports,
38
+ { env: { NEXTAUTH_SECRET: secret } }
39
+ )
40
+ return moduleObj.exports.guardWorkflowRequest as Guard
41
+ }
42
+
43
+ const reqWith = (extra: Record<string, unknown> = {}): any => ({ headers: {}, ...extra })
44
+
45
+ describe('guardWorkflowRequest (runtime enforcement)', () => {
46
+ const guard = bootGuard('server-secret')
47
+
48
+ it('is a no-op when there is no policy', async () => {
49
+ const context = { n: { userId: 'victim' } }
50
+ expect(await guard(reqWith(), context, null)).toBeNull()
51
+ expect(context.n.userId).toBe('victim')
52
+ })
53
+
54
+ it('rejects with 401 when auth is required and there is no session', async () => {
55
+ const res = await guard(reqWith(), {}, { requiresAuth: true, allowedRoles: [] })
56
+ expect(res).toEqual({ status: 401, message: 'Unauthenticated' })
57
+ })
58
+
59
+ it('allows any authenticated user when allowedRoles is empty', async () => {
60
+ const res = await guard(
61
+ reqWith({ __token: { id: 'u1', role: 'user' } }),
62
+ {},
63
+ {
64
+ requiresAuth: true,
65
+ allowedRoles: [],
66
+ }
67
+ )
68
+ expect(res).toBeNull()
69
+ })
70
+
71
+ it('rejects with 403 when the session role is not allowed', async () => {
72
+ const res = await guard(
73
+ reqWith({ __token: { id: 'u1', role: 'user' } }),
74
+ {},
75
+ {
76
+ requiresAuth: true,
77
+ allowedRoles: ['admin'],
78
+ }
79
+ )
80
+ expect(res).toEqual({ status: 403, message: 'Forbidden' })
81
+ })
82
+
83
+ it('allows a matching role (incl. roleName / roles[] spellings)', async () => {
84
+ for (const token of [
85
+ { id: 'u1', role: 'admin' },
86
+ { id: 'u1', roleName: 'admin' },
87
+ { id: 'u1', roles: ['admin'] },
88
+ ]) {
89
+ expect(
90
+ await guard(
91
+ reqWith({ __token: token }),
92
+ {},
93
+ { requiresAuth: true, allowedRoles: ['admin'] }
94
+ )
95
+ ).toBeNull()
96
+ }
97
+ })
98
+
99
+ it('OVERWRITES a forged user id with the session id (the IDOR fix)', async () => {
100
+ const context = { resolver: { userId: 'victim-id', email: 'x' } }
101
+ const res = await guard(reqWith({ __token: { id: 'attacker-real-id' } }), context, {
102
+ requiresAuth: true,
103
+ allowedRoles: [],
104
+ userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'resolver', path: ['userId'] }] },
105
+ })
106
+ expect(res).toBeNull()
107
+ expect(context.resolver.userId).toBe('attacker-real-id') // forced to the caller
108
+ expect(context.resolver.email).toBe('x') // other fields untouched
109
+ })
110
+
111
+ it('binds a nested path and creates missing context nodes', async () => {
112
+ const context: any = {}
113
+ await guard(reqWith({ __token: { id: 'sess-1' } }), context, {
114
+ requiresAuth: false,
115
+ allowedRoles: [],
116
+ userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['user', 'id'] }] },
117
+ })
118
+ expect(context.n.user.id).toBe('sess-1')
119
+ })
120
+
121
+ it('falls back to token.sub when token.id is absent', async () => {
122
+ const context = { n: { userId: 'victim' } }
123
+ await guard(reqWith({ __token: { sub: 'sub-id' } }), context, {
124
+ requiresAuth: false,
125
+ allowedRoles: [],
126
+ userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['userId'] }] },
127
+ })
128
+ expect(context.n.userId).toBe('sub-id')
129
+ })
130
+
131
+ it('leaves the client value for a GUEST (no session) on a non-required userScoped write', async () => {
132
+ // Guest checkout: no token, requiresAuth false → the anonymous client id is
133
+ // kept (there is no server session to bind), and the request proceeds.
134
+ const context = { n: { userId: 'guest-anon-uuid' } }
135
+ const res = await guard(reqWith(), context, {
136
+ requiresAuth: false,
137
+ allowedRoles: [],
138
+ userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['userId'] }] },
139
+ })
140
+ expect(res).toBeNull()
141
+ expect(context.n.userId).toBe('guest-anon-uuid')
142
+ })
143
+
144
+ it('honours the internal server-to-server secret bypass', async () => {
145
+ const res = await guard(
146
+ reqWith({ headers: { 'x-internal-data-secret': 'server-secret' } }),
147
+ {},
148
+ { requiresAuth: true, allowedRoles: ['admin'] }
149
+ )
150
+ expect(res).toBeNull()
151
+ })
152
+
153
+ it('does not treat a wrong internal secret as a bypass', async () => {
154
+ const res = await guard(
155
+ reqWith({ headers: { 'x-internal-data-secret': 'wrong' } }),
156
+ {},
157
+ { requiresAuth: true, allowedRoles: [] }
158
+ )
159
+ expect(res).toEqual({ status: 401, message: 'Unauthenticated' })
160
+ })
161
+ })
162
+
163
+ describe('guardWorkflowRequest with next-auth absent / no secret', () => {
164
+ it('treats a missing NEXTAUTH_SECRET as no session (401 when required)', async () => {
165
+ const guard = bootGuard(undefined)
166
+ const res = await guard(
167
+ reqWith({ __token: { id: 'u1' } }),
168
+ {},
169
+ { requiresAuth: true, allowedRoles: [] }
170
+ )
171
+ expect(res).toEqual({ status: 401, message: 'Unauthenticated' })
172
+ })
173
+ })
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Route wiring
177
+ // ---------------------------------------------------------------------------
178
+
179
+ const dataSegment = (): WorkflowSegment =>
180
+ ({
181
+ id: 'server-1',
182
+ env: 'server',
183
+ nodes: [
184
+ {
185
+ id: 'w1',
186
+ type: 'data-create-item',
187
+ label: 'Write',
188
+ config: { tableName: 'teleport_favourites' },
189
+ stepNumber: 1,
190
+ },
191
+ ],
192
+ edges: [],
193
+ } as unknown as WorkflowSegment)
194
+
195
+ const protection: UIDLWorkflowProtection = {
196
+ requiresAuth: true,
197
+ allowedRoles: ['admin'],
198
+ derivedFrom: 'page',
199
+ }
200
+
201
+ describe('route generators wire the guard in', () => {
202
+ it('injects the guard into a protected non-streaming route BEFORE the node loop', () => {
203
+ const code = generateServerSegmentAPIRoute(dataSegment(), 'Admin CRUD', protection)
204
+ expect(code).toContain("require('../../../utils/workflows/workflow-auth')")
205
+ expect(code).toContain('const __WF_AUTH = {')
206
+ expect(code).toContain('__wfAuth.guardWorkflowRequest(req, context, __WF_AUTH)')
207
+ // The guard (and its early 401/403 return) must precede the node loop.
208
+ expect(code.indexOf('guardWorkflowRequest')).toBeLessThan(
209
+ code.indexOf('SEGMENT_CONFIG.nodes.slice()')
210
+ )
211
+ // The baked policy carries the runtime fields, not the build-only derivedFrom.
212
+ expect(code).toContain('"requiresAuth":true')
213
+ expect(code).toContain('"allowedRoles":["admin"]')
214
+ expect(code).not.toContain('derivedFrom')
215
+ })
216
+
217
+ it('injects the guard into a protected streaming route BEFORE the stream starts', () => {
218
+ const code = generateStreamingServerSegmentAPIRoute(dataSegment(), 'Streaming', {
219
+ requiresAuth: true,
220
+ allowedRoles: [],
221
+ derivedFrom: 'graph',
222
+ })
223
+ expect(code).toContain('__wfAuth.guardWorkflowRequest(req, context, __WF_AUTH)')
224
+ // The guard runs before the node loop. `ensureStream()` (which calls
225
+ // res.writeHead for the event-stream) is only invoked DURING node execution,
226
+ // so a 401/403 is sent as an HTTP status before the stream ever starts.
227
+ expect(code.indexOf('guardWorkflowRequest')).toBeLessThan(
228
+ code.indexOf('SEGMENT_CONFIG.nodes.slice()')
229
+ )
230
+ // The only ensureStream CALL (not its definition) is inside the node loop.
231
+ expect(code.indexOf('guardWorkflowRequest')).toBeLessThan(code.lastIndexOf('ensureStream()'))
232
+ })
233
+
234
+ it('emits NOTHING guard-related for an unprotected route (byte-compatible)', () => {
235
+ const code = generateServerSegmentAPIRoute(dataSegment(), 'Public')
236
+ expect(code).not.toContain('workflow-auth')
237
+ expect(code).not.toContain('__WF_AUTH')
238
+ expect(code).not.toContain('guardWorkflowRequest')
239
+ })
240
+
241
+ it('bakes the userScoped bindings into the policy', () => {
242
+ const code = generateServerSegmentAPIRoute(dataSegment(), 'Favourites', {
243
+ requiresAuth: true,
244
+ allowedRoles: [],
245
+ userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'resolve', path: ['userId'] }] },
246
+ derivedFrom: 'graph',
247
+ })
248
+ expect(code).toContain('"userScoped"')
249
+ expect(code).toContain('"nodeId":"resolve"')
250
+ expect(code).toContain('"path":["userId"]')
251
+ })
252
+ })
253
+
254
+ describe('buildWorkflowAuthInjection', () => {
255
+ it('returns empty pieces when there is no meaningful policy', () => {
256
+ expect(buildWorkflowAuthInjection(undefined)).toEqual({
257
+ requireLine: '',
258
+ policyConst: '',
259
+ guardCall: '',
260
+ })
261
+ expect(
262
+ buildWorkflowAuthInjection({ requiresAuth: false, allowedRoles: [], derivedFrom: 'default' })
263
+ ).toEqual({ requireLine: '', policyConst: '', guardCall: '' })
264
+ })
265
+
266
+ it('emits pieces for a userScoped-only (guest-capable) policy', () => {
267
+ const injection = buildWorkflowAuthInjection({
268
+ requiresAuth: false,
269
+ allowedRoles: [],
270
+ userScoped: { ownerColumn: 'user_id', bindings: [{ nodeId: 'n', path: ['userId'] }] },
271
+ derivedFrom: 'graph',
272
+ })
273
+ expect(injection.requireLine).toContain('workflow-auth')
274
+ expect(injection.policyConst).toContain('"userScoped"')
275
+ expect(injection.guardCall).toContain('guardWorkflowRequest')
276
+ })
277
+ })
@@ -1,8 +1,8 @@
1
- import { UIDLWorkflow, UIDLCustomWorkflowNode } from '@teleporthq/teleport-types';
1
+ import { UIDLWorkflow, UIDLCustomWorkflowNode, UIDLWorkflowProtection } from '@teleporthq/teleport-types';
2
2
  import { WorkflowSegment } from './types';
3
- export declare const generateServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string) => string;
3
+ export declare const generateServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string, protection?: UIDLWorkflowProtection) => string;
4
4
  export declare const hasStreamingAINode: (segment: WorkflowSegment) => boolean;
5
- export declare const generateStreamingServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string) => string;
5
+ export declare const generateStreamingServerSegmentAPIRoute: (segment: WorkflowSegment, workflowName?: string, protection?: UIDLWorkflowProtection) => string;
6
6
  export declare const generateCronAPIRoute: (workflow: UIDLWorkflow, customNodes?: Record<string, UIDLCustomWorkflowNode>) => string;
7
7
  export declare const getAPIRouteFileName: (workflowId: string, segmentId: string, workflowName?: string) => string;
8
8
  export declare const getCronRouteFileName: (workflow: UIDLWorkflow) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAIZ,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAoBzC,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,KACpB,MAwUF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,KACpB,MAwYF,CAAA;AA0HD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MAoFF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MAuBF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AAkQD,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MA6LF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
1
+ {"version":3,"file":"api-route-generator.d.ts","sourceRoot":"","sources":["../../src/api-route-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAIZ,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAqBzC,eAAO,MAAM,6BAA6B,YAC/B,eAAe,iBACT,MAAM,eACR,sBAAsB,KAClC,MAgXF,CAAA;AAED,eAAO,MAAM,kBAAkB,YAAa,eAAe,KAAG,OAE7D,CAAA;AAED,eAAO,MAAM,sCAAsC,YACxC,eAAe,iBACT,MAAM,eACR,sBAAsB,KAClC,MAsbF,CAAA;AA0HD,eAAO,MAAM,oBAAoB,aACrB,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MAuFF,CAAA;AAED,eAAO,MAAM,mBAAmB,eAClB,MAAM,aACP,MAAM,iBACF,MAAM,KACpB,MAuBF,CAAA;AAED,eAAO,MAAM,oBAAoB,aAAc,YAAY,KAAG,MAa7D,CAAA;AA4RD,eAAO,MAAM,+BAA+B,aAChC,YAAY,gBACR,OAAO,MAAM,EAAE,sBAAsB,CAAC,KACnD,MAsMF,CAAA;AAED,eAAO,MAAM,uBAAuB,aAAc,YAAY,KAAG,MAiBhE,CAAA;AAED,eAAO,MAAM,mBAAmB,aAAc,YAAY,KAAG,MAAM,EAqBlE,CAAA"}
@@ -5,6 +5,7 @@ const nodes_1 = require("./nodes");
5
5
  const types_1 = require("./nodes/types");
6
6
  const graph_utils_1 = require("./graph-utils");
7
7
  const webhook_signature_verification_1 = require("./webhook-signature-verification");
8
+ const workflow_auth_generator_1 = require("./workflow-auth-generator");
8
9
  // Workflow/segment names, cron schedules, and webhook paths are free-form
9
10
  // UIDL data — never guaranteed not to contain `*/`. Every generated route
10
11
  // file opens with a `/** ... */` JSDoc header interpolating these values
@@ -15,10 +16,11 @@ const webhook_signature_verification_1 = require("./webhook-signature-verificati
15
16
  // breaks parsing outright). Breaking up any `*/` sequence keeps the comment
16
17
  // intact; this is purely cosmetic (header text only, never evaluated).
17
18
  const sanitizeForBlockComment = (value) => value.replace(/\*\//g, '* /');
18
- const generateServerSegmentAPIRoute = (segment, workflowName) => {
19
+ const generateServerSegmentAPIRoute = (segment, workflowName, protection) => {
19
20
  const usedNodeTypes = new Set(segment.nodes.map((n) => n.type));
20
21
  const nodeHandlersEntries = generateNodeHandlersForSegment(usedNodeTypes, true);
21
22
  const hasRateLimiter = usedNodeTypes.has('general-rate-limiter');
23
+ const auth = (0, workflow_auth_generator_1.buildWorkflowAuthInjection)(protection);
22
24
  const segmentConfig = JSON.stringify({
23
25
  nodes: segment.nodes.map((n) => ({
24
26
  id: n.id,
@@ -53,10 +55,10 @@ const generateServerSegmentAPIRoute = (segment, workflowName) => {
53
55
  : '';
54
56
  return `${header}
55
57
  const utils = require('../../../utils/workflows/server-runtime');
56
- const resolveConfig = utils.resolveConfig;
58
+ ${auth.requireLine}const resolveConfig = utils.resolveConfig;
57
59
 
58
60
  const SEGMENT_CONFIG = ${segmentConfig};
59
-
61
+ ${auth.policyConst}
60
62
  const nodeHandlers = {
61
63
  ${nodeHandlersEntries}
62
64
  };
@@ -66,18 +68,32 @@ module.exports = async function handler(req, res) {
66
68
  res.status(405).json({ error: 'Method not allowed' });
67
69
  return;
68
70
  }
71
+ // Kept outside the try so the catch below can still drain any fire-and-forget
72
+ // query this segment started before it failed.
73
+ var __wfContext = null;
69
74
  try {
70
75
  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
71
76
  const incomingContext = body.context || {};
72
77
 
73
- const context = Object.assign({}, incomingContext);${requestInjection}
78
+ const context = Object.assign({}, incomingContext);
79
+ __wfContext = context;
80
+ // Created eagerly so a custom node invoked from here shares the SAME
81
+ // fire-and-forget queue (it shallow-copies this context) and its queries
82
+ // are drained by the settle below rather than being lost when we reply.
83
+ context.__pendingNodePromises = [];${requestInjection}
74
84
  var __proto = req.headers['x-forwarded-proto'] || (req.headers.host && (req.headers.host.startsWith('localhost') || req.headers.host.startsWith('127.0.0.1')) ? 'http' : 'https');
75
- context.__baseUrl = __proto + '://' + req.headers.host;
85
+ context.__baseUrl = __proto + '://' + req.headers.host;${auth.guardCall}
76
86
  const sortedNodes = SEGMENT_CONFIG.nodes.slice().sort(function(a, b) { return a.stepNumber - b.stepNumber; });
77
87
 
78
88
  for (let i = 0; i < sortedNodes.length; i++) {
79
89
  const node = sortedNodes[i];
80
90
  const resolved = resolveConfig(node.config, context);
91
+ // Every data node lives in a SERVER segment, so this guard — which turns an
92
+ // unresolved {{…}} into a validation error instead of letting the literal
93
+ // token reach SQL — was doing nothing at all: it ran only in the CLIENT
94
+ // executor. Run 02783f65 shipped 55 route files, none of which called it.
95
+ const __configError = utils.finalizeResolvedConfig(node.type, resolved);
96
+ if (__configError) { throw new Error(__configError); }
81
97
  resolved.__nodeId = node.id;
82
98
 
83
99
  // If-statement nodes must be evaluated using the runtime's evaluateCondition
@@ -178,6 +194,11 @@ module.exports = async function handler(req, res) {
178
194
  }
179
195
  var bHandler = nodeHandlers[bNode.type];
180
196
  if (!bHandler) continue;
197
+ if (utils.isFireAndForgetNode(bNode)) {
198
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
199
+ context[bNode.id] = null;
200
+ continue;
201
+ }
181
202
  var bResult = await bHandler(bResolved, context);
182
203
  if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
183
204
  throw new Error(bResult.error || 'Loop body node execution failed');
@@ -263,6 +284,13 @@ module.exports = async function handler(req, res) {
263
284
  pRes.__nodeId = pNode.id;
264
285
  var pHandler = nodeHandlers[pNode.type];
265
286
  if (!pHandler) continue;
287
+ if (utils.isFireAndForgetNode(pNode)) {
288
+ // Registered on the ROUTE context (not the branch copy) so the
289
+ // response still waits for the query to land.
290
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(pNode, pHandler, pRes, branchCtx));
291
+ branchCtx[pNode.id] = null;
292
+ continue;
293
+ }
266
294
  var pResult = await pHandler(pRes, branchCtx);
267
295
  branchCtx[pNode.id] = pResult;
268
296
  }
@@ -310,6 +338,11 @@ module.exports = async function handler(req, res) {
310
338
  console.warn('No handler for node type: ' + node.type);
311
339
  continue;
312
340
  }
341
+ if (utils.isFireAndForgetNode(node)) {
342
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(node, handler, resolved, context));
343
+ context[node.id] = null;
344
+ continue;
345
+ }
313
346
  const result = await handler(resolved, context);
314
347
  if (result && result.__earlyResponse) {
315
348
  var earlyRes = result.__earlyResponse;
@@ -317,6 +350,7 @@ module.exports = async function handler(req, res) {
317
350
  for (var h = 0; h < hKeys.length; h++) {
318
351
  res.setHeader(hKeys[h], earlyRes.headers[hKeys[h]]);
319
352
  }
353
+ await utils.settlePendingNodePromises(context);
320
354
  res.status(earlyRes.status || 500).json(earlyRes.body || {});
321
355
  return;
322
356
  }
@@ -327,10 +361,17 @@ module.exports = async function handler(req, res) {
327
361
  if (result && result.__terminal) break;
328
362
  }
329
363
 
364
+ // Land every fire-and-forget query BEFORE replying: the platform may freeze
365
+ // this function the moment the response is sent, which would drop the write.
366
+ // The visitor does not pay for this — the client dispatched this segment
367
+ // without awaiting it (see seg.fireAndForget in the client runtime).
368
+ await utils.settlePendingNodePromises(context);
330
369
  delete context.__request;
370
+ delete context.__pendingNodePromises;
331
371
  res.status(200).json({ success: true, results: context });
332
372
  } catch (error) {
333
373
  console.error('Workflow segment error:', error);
374
+ if (__wfContext) { await utils.settlePendingNodePromises(__wfContext); }
334
375
  res.status(500).json({ success: false, error: error.message || 'Internal server error' });
335
376
  }
336
377
  };
@@ -341,10 +382,11 @@ const hasStreamingAINode = (segment) => {
341
382
  return segment.nodes.some((n) => { var _a; return graph_utils_1.AI_NODE_TYPES.has(n.type) && ((_a = n.config) === null || _a === void 0 ? void 0 : _a.streaming) === true; });
342
383
  };
343
384
  exports.hasStreamingAINode = hasStreamingAINode;
344
- const generateStreamingServerSegmentAPIRoute = (segment, workflowName) => {
385
+ const generateStreamingServerSegmentAPIRoute = (segment, workflowName, protection) => {
345
386
  const usedNodeTypes = new Set(segment.nodes.map((n) => n.type));
346
387
  const nodeHandlersEntries = generateNodeHandlersForSegment(usedNodeTypes, true);
347
388
  const hasRateLimiter = usedNodeTypes.has('general-rate-limiter');
389
+ const auth = (0, workflow_auth_generator_1.buildWorkflowAuthInjection)(protection);
348
390
  const segmentConfig = JSON.stringify({
349
391
  nodes: segment.nodes.map((n) => ({
350
392
  id: n.id,
@@ -371,10 +413,10 @@ const generateStreamingServerSegmentAPIRoute = (segment, workflowName) => {
371
413
  : '';
372
414
  return `${header}
373
415
  const utils = require('../../../utils/workflows/server-runtime');
374
- const resolveConfig = utils.resolveConfig;
416
+ ${auth.requireLine}const resolveConfig = utils.resolveConfig;
375
417
 
376
418
  const SEGMENT_CONFIG = ${segmentConfig};
377
-
419
+ ${auth.policyConst}
378
420
  const nodeHandlers = {
379
421
  ${nodeHandlersEntries}
380
422
  };
@@ -420,11 +462,17 @@ module.exports = async function handler(req, res) {
420
462
  }
421
463
  }
422
464
 
465
+ // Kept outside the try so the catch below can still drain any fire-and-forget
466
+ // query this segment started before it failed.
467
+ var __wfContext = null;
423
468
  try {
424
469
  const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
425
- const context = Object.assign({}, body.context || {});${requestInjection}
470
+ const context = Object.assign({}, body.context || {});
471
+ __wfContext = context;
472
+ // See the non-streaming segment route — shared queue for nested custom nodes.
473
+ context.__pendingNodePromises = [];${requestInjection}
426
474
  var __proto = req.headers['x-forwarded-proto'] || (req.headers.host && (req.headers.host.startsWith('localhost') || req.headers.host.startsWith('127.0.0.1')) ? 'http' : 'https');
427
- context.__baseUrl = __proto + '://' + req.headers.host;
475
+ context.__baseUrl = __proto + '://' + req.headers.host;${auth.guardCall}
428
476
  const sortedNodes = SEGMENT_CONFIG.nodes.slice().sort(function(a, b) { return a.stepNumber - b.stepNumber; });
429
477
  const executed = {};
430
478
 
@@ -432,6 +480,10 @@ module.exports = async function handler(req, res) {
432
480
  const node = sortedNodes[i];
433
481
  if (executed[node.id]) continue;
434
482
  const resolved = resolveConfig(node.config, context);
483
+ // See the non-streaming segment above — without this the unresolved-token
484
+ // guard never runs on the server, which is where every data node executes.
485
+ const __configError = utils.finalizeResolvedConfig(node.type, resolved);
486
+ if (__configError) { throw new Error(__configError); }
435
487
  resolved.__nodeId = node.id;
436
488
 
437
489
  // If-statement nodes must be evaluated using the runtime's evaluateCondition
@@ -490,6 +542,11 @@ module.exports = async function handler(req, res) {
490
542
  }
491
543
  var ssbHandler = nodeHandlers[ssbNode.type];
492
544
  if (!ssbHandler) continue;
545
+ if (utils.isFireAndForgetNode(ssbNode)) {
546
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(ssbNode, ssbHandler, ssbRes, context));
547
+ context[ssbNode.id] = null;
548
+ continue;
549
+ }
493
550
  var ssbResult = await ssbHandler(ssbRes, context);
494
551
  context[ssbNode.id] = ssbResult;
495
552
  }
@@ -573,6 +630,13 @@ module.exports = async function handler(req, res) {
573
630
  sspRes.__nodeId = sspNode.id;
574
631
  var sspHandler = nodeHandlers[sspNode.type];
575
632
  if (!sspHandler) continue;
633
+ if (utils.isFireAndForgetNode(sspNode)) {
634
+ // Registered on the ROUTE context (not the branch copy) so the
635
+ // response still waits for the query to land.
636
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(sspNode, sspHandler, sspRes, ssPBranchCtx));
637
+ ssPBranchCtx[sspNode.id] = null;
638
+ continue;
639
+ }
576
640
  var sspResult = await sspHandler(sspRes, ssPBranchCtx);
577
641
  ssPBranchCtx[sspNode.id] = sspResult;
578
642
  }
@@ -646,6 +710,11 @@ module.exports = async function handler(req, res) {
646
710
  const snHandler = nodeHandlers[sn.type];
647
711
  if (!snHandler) continue;
648
712
  const snResolved = resolveConfig(sn.config, context);
713
+ if (utils.isFireAndForgetNode(sn)) {
714
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(sn, snHandler, snResolved, context));
715
+ context[sn.id] = null;
716
+ continue;
717
+ }
649
718
  const snResult = await snHandler(snResolved, context);
650
719
  context[sn.id] = snResult;
651
720
  res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: sn.id, result: snResult }) + '\\n\\n');
@@ -665,6 +734,12 @@ module.exports = async function handler(req, res) {
665
734
  const enHandler = nodeHandlers[en.type];
666
735
  if (!enHandler) continue;
667
736
  const enResolved = resolveConfig(en.config, context);
737
+ if (utils.isFireAndForgetNode(en)) {
738
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(en, enHandler, enResolved, context));
739
+ context[en.id] = null;
740
+ executed[en.id] = true;
741
+ continue;
742
+ }
668
743
  const enResult = await enHandler(enResolved, context);
669
744
  context[en.id] = enResult;
670
745
  res.write('data: ' + JSON.stringify({ type: 'node-result', nodeId: en.id, result: enResult }) + '\\n\\n');
@@ -674,8 +749,14 @@ module.exports = async function handler(req, res) {
674
749
  executed[onStreamNodes[si2].id] = true;
675
750
  }
676
751
  } else {
752
+ if (utils.isFireAndForgetNode(node)) {
753
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(node, nodeHandler, resolved, context));
754
+ context[node.id] = null;
755
+ continue;
756
+ }
677
757
  const result = await nodeHandler(resolved, context);
678
758
  if (result && result.__earlyResponse) {
759
+ await utils.settlePendingNodePromises(context);
679
760
  if (streamStarted) {
680
761
  res.write('data: ' + JSON.stringify({ type: 'error', error: (result.__earlyResponse.body && result.__earlyResponse.body.message) || 'Request rejected' }) + '\\n\\n');
681
762
  res.end();
@@ -700,7 +781,12 @@ module.exports = async function handler(req, res) {
700
781
  }
701
782
  }
702
783
 
784
+ // See the non-streaming segment route: land every fire-and-forget query
785
+ // before the response completes, or the platform may freeze this function
786
+ // with the write still in flight.
787
+ await utils.settlePendingNodePromises(context);
703
788
  delete context.__request;
789
+ delete context.__pendingNodePromises;
704
790
  if (streamStarted) {
705
791
  res.write('data: ' + JSON.stringify({ type: 'done', success: true, results: context }) + '\\n\\n');
706
792
  res.end();
@@ -709,6 +795,7 @@ module.exports = async function handler(req, res) {
709
795
  }
710
796
  } catch (error) {
711
797
  console.error('Streaming workflow segment error:', error);
798
+ if (__wfContext) { await utils.settlePendingNodePromises(__wfContext); }
712
799
  if (streamStarted) {
713
800
  try {
714
801
  res.write('data: ' + JSON.stringify({ type: 'error', error: error.message || 'Internal server error' }) + '\\n\\n');
@@ -900,6 +987,9 @@ module.exports = async function handler(req, res) {
900
987
  try {
901
988
  const triggerContext = { timestamp: Date.now(), schedule: '${workflow.trigger.config.schedule || ''}' };
902
989
  const context = {};
990
+ // Shared fire-and-forget queue (see the segment routes) — drained by the
991
+ // execution loop before this route responds.
992
+ context.__pendingNodePromises = [];
903
993
  context[WORKFLOW_CONFIG.triggerNodeId] = triggerContext;${requestInjection}
904
994
 
905
995
  ${executionLoop}
@@ -974,6 +1064,10 @@ const generateNodeExecutionLoop = (configVar, options) => {
974
1064
  for (var i = 0; i < sortedNodes.length; i++) {
975
1065
  var node = sortedNodes[i];
976
1066
  var resolved = resolveConfig(node.config, context);
1067
+ // See the other segment executors — the unresolved-token guard must run
1068
+ // wherever data nodes execute, and they all execute on the server.
1069
+ var __configError = utils.finalizeResolvedConfig(node.type, resolved);
1070
+ if (__configError) { throw new Error(__configError); }
977
1071
  resolved.__nodeId = node.id;
978
1072
  if (resolved && Array.isArray(resolved.templateParams)) {
979
1073
  if (typeof resolved.body === 'string') { resolved.body = utils.applyTemplateParams(resolved.body, resolved.templateParams); }
@@ -1059,6 +1153,11 @@ const generateNodeExecutionLoop = (configVar, options) => {
1059
1153
  }
1060
1154
  var bHandler = nodeHandlers[bNode.type];
1061
1155
  if (!bHandler) continue;
1156
+ if (utils.isFireAndForgetNode(bNode)) {
1157
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
1158
+ context[bNode.id] = null;
1159
+ continue;
1160
+ }
1062
1161
  var bResult = await bHandler(bResolved, context);
1063
1162
  if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
1064
1163
  throw new Error(bResult.error || 'Loop body node execution failed');
@@ -1140,6 +1239,13 @@ const generateNodeExecutionLoop = (configVar, options) => {
1140
1239
  wlpRes.__nodeId = wlpNode.id;
1141
1240
  var wlpHandler = nodeHandlers[wlpNode.type];
1142
1241
  if (!wlpHandler) continue;
1242
+ if (utils.isFireAndForgetNode(wlpNode)) {
1243
+ // Registered on the ROUTE context (not the branch copy) so the
1244
+ // response still waits for the query to land.
1245
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(wlpNode, wlpHandler, wlpRes, wlBranchCtx));
1246
+ wlBranchCtx[wlpNode.id] = null;
1247
+ continue;
1248
+ }
1143
1249
  var wlpResult = await wlpHandler(wlpRes, wlBranchCtx);
1144
1250
  wlBranchCtx[wlpNode.id] = wlpResult;
1145
1251
  }
@@ -1187,6 +1293,11 @@ const generateNodeExecutionLoop = (configVar, options) => {
1187
1293
  console.warn('No handler for node type: ' + node.type);
1188
1294
  continue;
1189
1295
  }
1296
+ if (utils.isFireAndForgetNode(node)) {
1297
+ utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(node, handler, resolved, context));
1298
+ context[node.id] = null;
1299
+ continue;
1300
+ }
1190
1301
  var result = await handler(resolved, context);
1191
1302
  if (result && result.__earlyResponse) {
1192
1303
  var earlyRes = result.__earlyResponse;
@@ -1194,6 +1305,7 @@ const generateNodeExecutionLoop = (configVar, options) => {
1194
1305
  for (var h = 0; h < hKeys.length; h++) {
1195
1306
  res.setHeader(hKeys[h], earlyRes.headers[hKeys[h]]);
1196
1307
  }
1308
+ await utils.settlePendingNodePromises(context);
1197
1309
  res.status(earlyRes.status || 500).json(earlyRes.body || {});
1198
1310
  return;
1199
1311
  }${customNodeBlock}
@@ -1202,7 +1314,11 @@ const generateNodeExecutionLoop = (configVar, options) => {
1202
1314
  }
1203
1315
  context[node.id] = result;
1204
1316
  if (result && result.__terminal) break;
1205
- }`;
1317
+ }
1318
+
1319
+ // Land every fire-and-forget query before this route responds — a
1320
+ // serverless function can be frozen the instant it replies.
1321
+ await utils.settlePendingNodePromises(context);`;
1206
1322
  };
1207
1323
  const generateWebhookWorkflowAPIRoute = (workflow, customNodes) => {
1208
1324
  var _a;
@@ -1327,6 +1443,9 @@ ${generateSignatureVerificationBlock(webhookConfig)}
1327
1443
  };
1328
1444
 
1329
1445
  var context = {};
1446
+ // Shared fire-and-forget queue (see the segment routes) — drained by the
1447
+ // execution loop before this route responds.
1448
+ context.__pendingNodePromises = [];
1330
1449
  context[WORKFLOW_CONFIG.triggerNodeId] = triggerContext;
1331
1450
  var __proto = req.headers['x-forwarded-proto'] || (req.headers.host && (req.headers.host.startsWith('localhost') || req.headers.host.startsWith('127.0.0.1')) ? 'http' : 'https');
1332
1451
  context.__baseUrl = __proto + '://' + req.headers.host;${requestInjection}
@@ -1351,10 +1470,16 @@ ${executionLoop}
1351
1470
  eResolved.__nodeId = eNode.id;
1352
1471
  var eHandler = nodeHandlers[eNode.type];
1353
1472
  if (eHandler) {
1473
+ if (utils.isFireAndForgetNode(eNode)) {
1474
+ utils.registerPendingNodePromise(errCtx, utils.startFireAndForgetNode(eNode, eHandler, eResolved, errCtx));
1475
+ errCtx[eNode.id] = null;
1476
+ continue;
1477
+ }
1354
1478
  var eResult = await eHandler(eResolved, errCtx);
1355
1479
  errCtx[eNode.id] = eResult;
1356
1480
  }
1357
1481
  }
1482
+ await utils.settlePendingNodePromises(errCtx);
1358
1483
  } catch (innerErr) {
1359
1484
  console.error('Webhook error handler failed:', innerErr);
1360
1485
  }