@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.
- package/__tests__/_helpers/run-generated-middleware.ts +130 -0
- package/__tests__/auth-env-secret-preservation.test.ts +35 -0
- package/__tests__/auth-nextauth-url.test.ts +28 -0
- package/__tests__/auth-session-cost.test.ts +614 -0
- package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
- package/__tests__/data-node-not-awaited.test.ts +644 -0
- package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
- package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
- package/__tests__/form-submitted-trigger-context.test.ts +100 -0
- package/__tests__/middleware-guest-order-details.test.ts +258 -0
- package/__tests__/middleware-row-owned-pages.test.ts +32 -9
- package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
- package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
- package/__tests__/raw-sql-param-binding.test.ts +82 -0
- package/__tests__/reserved-word-state-binding.test.ts +112 -0
- package/__tests__/strip-event-handler-attrs.test.ts +59 -0
- package/__tests__/workflow-auth-guard.test.ts +277 -0
- package/dist/cjs/api-route-generator.d.ts +3 -3
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +136 -11
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +67 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +573 -31
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/await-result.d.ts +31 -0
- package/dist/cjs/await-result.d.ts.map +1 -0
- package/dist/cjs/await-result.js +50 -0
- package/dist/cjs/await-result.js.map +1 -0
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +150 -1
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.js +44 -11
- package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
- package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-login.js +10 -1
- package/dist/cjs/nodes/account/account-login.js.map +1 -1
- package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/cjs/raw-sql-param-binding.js +32 -0
- package/dist/cjs/raw-sql-param-binding.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +15 -7
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/trigger-generator.js +7 -1
- package/dist/cjs/trigger-generator.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-auth-generator.d.ts +26 -0
- package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
- package/dist/cjs/workflow-auth-generator.js +166 -0
- package/dist/cjs/workflow-auth-generator.js.map +1 -0
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +124 -17
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts +2 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +170 -20
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts +3 -3
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +136 -11
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +67 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +568 -30
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/await-result.d.ts +31 -0
- package/dist/esm/await-result.d.ts.map +1 -0
- package/dist/esm/await-result.js +45 -0
- package/dist/esm/await-result.js.map +1 -0
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +150 -1
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-get-current.js +44 -11
- package/dist/esm/nodes/account/account-get-current.js.map +1 -1
- package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-login.js +10 -1
- package/dist/esm/nodes/account/account-login.js.map +1 -1
- package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/esm/raw-sql-param-binding.js +32 -0
- package/dist/esm/raw-sql-param-binding.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +15 -7
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/trigger-generator.js +7 -1
- package/dist/esm/trigger-generator.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-auth-generator.d.ts +26 -0
- package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
- package/dist/esm/workflow-auth-generator.js +161 -0
- package/dist/esm/workflow-auth-generator.js.map +1 -0
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +125 -18
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts +2 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +169 -20
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/api-route-generator.ts +139 -11
- package/src/auth-generator.ts +600 -30
- package/src/await-result.ts +52 -0
- package/src/executor-generator.ts +150 -1
- package/src/nodes/account/account-get-current.ts +44 -9
- package/src/nodes/account/account-login.ts +12 -1
- package/src/raw-sql-param-binding.ts +33 -0
- package/src/segment-splitter.ts +15 -7
- package/src/trigger-generator.ts +7 -1
- package/src/workflow-auth-generator.ts +187 -0
- package/src/workflow-component-plugin.ts +146 -21
- 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;
|
|
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)
|
|
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 || {})
|
|
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
|
}
|