@xyne/workflow-sdk 3.2.15 → 3.2.16
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/dist/agents/agent-step.d.ts.map +1 -1
- package/dist/agents/agent-step.js +3 -1
- package/dist/agents/agent-step.js.map +1 -1
- package/dist/authz/attributes.d.ts +41 -0
- package/dist/authz/attributes.d.ts.map +1 -0
- package/dist/authz/attributes.js +2 -0
- package/dist/authz/attributes.js.map +1 -0
- package/dist/authz/default.d.ts +31 -0
- package/dist/authz/default.d.ts.map +1 -0
- package/dist/authz/default.js +47 -0
- package/dist/authz/default.js.map +1 -0
- package/dist/authz/id-scope.d.ts +27 -0
- package/dist/authz/id-scope.d.ts.map +1 -0
- package/dist/authz/id-scope.js +24 -0
- package/dist/authz/id-scope.js.map +1 -0
- package/dist/authz/principal-field.d.ts +23 -0
- package/dist/authz/principal-field.d.ts.map +1 -0
- package/dist/authz/principal-field.js +34 -0
- package/dist/authz/principal-field.js.map +1 -0
- package/dist/authz/types.d.ts +129 -0
- package/dist/authz/types.d.ts.map +1 -0
- package/dist/authz/types.js +74 -0
- package/dist/authz/types.js.map +1 -0
- package/dist/client/types.d.ts +26 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/workflow-client.d.ts.map +1 -1
- package/dist/client/workflow-client.js +21 -2
- package/dist/client/workflow-client.js.map +1 -1
- package/dist/engine/workflow-executor.d.ts +1 -1
- package/dist/engine/workflow-executor.d.ts.map +1 -1
- package/dist/engine/workflow-executor.js +13 -2
- package/dist/engine/workflow-executor.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/persistence/in-memory-adapter.d.ts +40 -20
- package/dist/persistence/in-memory-adapter.d.ts.map +1 -1
- package/dist/persistence/in-memory-adapter.js +108 -38
- package/dist/persistence/in-memory-adapter.js.map +1 -1
- package/dist/persistence/types.d.ts +117 -20
- package/dist/persistence/types.d.ts.map +1 -1
- package/dist/router/types.d.ts +11 -8
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/workflow-router.d.ts +2 -2
- package/dist/router/workflow-router.d.ts.map +1 -1
- package/dist/router/workflow-router.js +179 -62
- package/dist/router/workflow-router.js.map +1 -1
- package/dist/runtime/types.d.ts +22 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/workflow-runtime.d.ts +137 -43
- package/dist/runtime/workflow-runtime.d.ts.map +1 -1
- package/dist/runtime/workflow-runtime.js +288 -98
- package/dist/runtime/workflow-runtime.js.map +1 -1
- package/dist/steps/base-step.d.ts +19 -2
- package/dist/steps/base-step.d.ts.map +1 -1
- package/dist/steps/base-step.js.map +1 -1
- package/dist/storage/types.d.ts +14 -4
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/testing/authz-conformance.d.ts +90 -0
- package/dist/testing/authz-conformance.d.ts.map +1 -0
- package/dist/testing/authz-conformance.js +250 -0
- package/dist/testing/authz-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock-context.d.ts +4 -0
- package/dist/testing/mock-context.d.ts.map +1 -1
- package/dist/testing/mock-context.js +4 -1
- package/dist/testing/mock-context.js.map +1 -1
- package/dist/testing/mock-step-context.d.ts.map +1 -1
- package/dist/testing/mock-step-context.js +1 -0
- package/dist/testing/mock-step-context.js.map +1 -1
- package/dist/types/context.d.ts +22 -3
- package/dist/types/context.d.ts.map +1 -1
- package/package.json +9 -3
- package/dist/common/principal.d.ts +0 -45
- package/dist/common/principal.d.ts.map +0 -1
- package/dist/common/principal.js +0 -9
- package/dist/common/principal.js.map +0 -1
- package/dist/steps/builtin/transform.step.d.ts +0 -247
- package/dist/steps/builtin/transform.step.d.ts.map +0 -1
- package/dist/steps/builtin/transform.step.js +0 -135
- package/dist/steps/builtin/transform.step.js.map +0 -1
- package/dist/types/attachment.d.ts +0 -23
- package/dist/types/attachment.d.ts.map +0 -1
- package/dist/types/attachment.js +0 -2
- package/dist/types/attachment.js.map +0 -1
- package/dist/types/resume-payload.d.ts +0 -34
- package/dist/types/resume-payload.d.ts.map +0 -1
- package/dist/types/resume-payload.js +0 -12
- package/dist/types/resume-payload.js.map +0 -1
- package/dist/util/executable-check.d.ts +0 -42
- package/dist/util/executable-check.d.ts.map +0 -1
- package/dist/util/executable-check.js +0 -115
- package/dist/util/executable-check.js.map +0 -1
|
@@ -27,6 +27,7 @@ import { OPERATOR_DESCRIPTORS } from '../types/operators.js';
|
|
|
27
27
|
import { getAvailableContext } from '../engine/available-context.js';
|
|
28
28
|
import { isExecutable } from '../common/executable-check.js';
|
|
29
29
|
import { createEventChannel } from '../runtime/execution-event-bus.js';
|
|
30
|
+
import { AccessDeniedError, ResourceNotFoundError } from '../authz/default.js';
|
|
30
31
|
// ─── Helpers ───
|
|
31
32
|
function ok(body) {
|
|
32
33
|
return { status: 200, body };
|
|
@@ -43,16 +44,22 @@ function badRequest(msg) {
|
|
|
43
44
|
function error(status, msg) {
|
|
44
45
|
return { status, body: { error: msg } };
|
|
45
46
|
}
|
|
46
|
-
function authMetadata(auth) {
|
|
47
|
-
return auth?.metadata ?? {};
|
|
48
|
-
}
|
|
49
47
|
/** Format a single SSE message. */
|
|
50
48
|
function formatSSE(event, data) {
|
|
51
49
|
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
52
50
|
}
|
|
53
51
|
// ─── Router Factory ───
|
|
54
|
-
export function createWorkflowRouter(
|
|
52
|
+
export function createWorkflowRouter(
|
|
53
|
+
// `any` for services: the router never touches TServices, only forwards ctx.
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
runtime,
|
|
56
|
+
// `authenticate` is invoked by the host glue, not the factory (see file header).
|
|
57
|
+
_opts) {
|
|
55
58
|
const routes = [];
|
|
59
|
+
// Authenticated route handlers always receive a resolved ctx — the host runs
|
|
60
|
+
// `authenticate` before dispatch. Unauthenticated routes never forward `auth`
|
|
61
|
+
// to a guarded runtime call, so this narrowing is sound.
|
|
62
|
+
const asCtx = (auth) => auth;
|
|
56
63
|
// ─── Workflow CRUD ───
|
|
57
64
|
routes.push({
|
|
58
65
|
method: 'GET',
|
|
@@ -66,7 +73,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
66
73
|
filter.limit = Number(q['limit']);
|
|
67
74
|
if (typeof q['offset'] === 'string')
|
|
68
75
|
filter.offset = Number(q['offset']);
|
|
69
|
-
const workflows = await runtime.listWorkflows(
|
|
76
|
+
const workflows = await runtime.listWorkflows(asCtx(auth), filter);
|
|
70
77
|
return ok({ workflows });
|
|
71
78
|
},
|
|
72
79
|
});
|
|
@@ -76,27 +83,25 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
76
83
|
method: 'GET',
|
|
77
84
|
path: '/workflows/counts',
|
|
78
85
|
handler: async (_req, auth) => {
|
|
79
|
-
const counts = await runtime.workflowCounts(
|
|
86
|
+
const counts = await runtime.workflowCounts(asCtx(auth));
|
|
80
87
|
return ok(counts);
|
|
81
88
|
},
|
|
82
89
|
});
|
|
83
90
|
routes.push({
|
|
84
91
|
method: 'GET',
|
|
85
92
|
path: '/workflows/:id',
|
|
86
|
-
handler: async (req) => {
|
|
93
|
+
handler: async (req, auth) => {
|
|
87
94
|
const id = req.params['id'];
|
|
88
95
|
if (!id)
|
|
89
96
|
return badRequest('Missing workflow ID');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return notFound(`Workflow ${id} not found`);
|
|
93
|
-
return ok(workflow);
|
|
97
|
+
// Missing → ResourceNotFoundError → 404 at the wrapper below.
|
|
98
|
+
return ok(await runtime.getWorkflow(asCtx(auth), id));
|
|
94
99
|
},
|
|
95
100
|
});
|
|
96
101
|
routes.push({
|
|
97
102
|
method: 'POST',
|
|
98
103
|
path: '/workflows',
|
|
99
|
-
handler: async (req,
|
|
104
|
+
handler: async (req, auth) => {
|
|
100
105
|
const body = req.body;
|
|
101
106
|
if (!body || !body['name'] || !body['config']) {
|
|
102
107
|
return badRequest('Body must contain "name" and "config"');
|
|
@@ -105,16 +110,19 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
105
110
|
return badRequest('Body must contain a "folderId" (every workflow belongs to a folder)');
|
|
106
111
|
}
|
|
107
112
|
try {
|
|
108
|
-
const workflow = await runtime.createWorkflow({
|
|
113
|
+
const workflow = await runtime.createWorkflow(asCtx(auth), {
|
|
109
114
|
name: body['name'],
|
|
110
115
|
config: body['config'],
|
|
111
116
|
metadata: body['metadata'] ?? undefined,
|
|
112
117
|
summary: typeof body['summary'] === 'string' ? body['summary'] : null,
|
|
113
118
|
folderId: body['folderId'],
|
|
119
|
+
attributes: body['attributes'],
|
|
114
120
|
});
|
|
115
121
|
return created(workflow);
|
|
116
122
|
}
|
|
117
123
|
catch (err) {
|
|
124
|
+
if (err instanceof AccessDeniedError)
|
|
125
|
+
throw err;
|
|
118
126
|
return error(400, err instanceof Error ? err.message : 'Create failed');
|
|
119
127
|
}
|
|
120
128
|
},
|
|
@@ -122,7 +130,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
122
130
|
routes.push({
|
|
123
131
|
method: 'PUT',
|
|
124
132
|
path: '/workflows/:id',
|
|
125
|
-
handler: async (req,
|
|
133
|
+
handler: async (req, auth) => {
|
|
126
134
|
const id = req.params['id'];
|
|
127
135
|
if (!id)
|
|
128
136
|
return badRequest('Missing workflow ID');
|
|
@@ -142,10 +150,12 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
142
150
|
}
|
|
143
151
|
if (typeof body['folderId'] === 'string')
|
|
144
152
|
input.folderId = body['folderId'];
|
|
145
|
-
const workflow = await runtime.updateWorkflow(id, input);
|
|
153
|
+
const workflow = await runtime.updateWorkflow(asCtx(auth), id, input);
|
|
146
154
|
return ok(workflow);
|
|
147
155
|
}
|
|
148
156
|
catch (err) {
|
|
157
|
+
if (err instanceof AccessDeniedError)
|
|
158
|
+
throw err;
|
|
149
159
|
if (err instanceof Error && err.message.includes('not found')) {
|
|
150
160
|
return notFound(err.message);
|
|
151
161
|
}
|
|
@@ -158,10 +168,31 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
158
168
|
method: 'GET',
|
|
159
169
|
path: '/folders',
|
|
160
170
|
handler: async (_req, auth) => {
|
|
161
|
-
const folders = await runtime.listFolders(
|
|
171
|
+
const folders = await runtime.listFolders(asCtx(auth));
|
|
162
172
|
return ok({ folders });
|
|
163
173
|
},
|
|
164
174
|
});
|
|
175
|
+
routes.push({
|
|
176
|
+
method: 'GET',
|
|
177
|
+
path: '/folders/:id',
|
|
178
|
+
handler: async (req, auth) => {
|
|
179
|
+
const id = req.params['id'];
|
|
180
|
+
if (!id)
|
|
181
|
+
return badRequest('Missing folder ID');
|
|
182
|
+
// Missing → ResourceNotFoundError → 404 at the wrapper below.
|
|
183
|
+
return ok(await runtime.getFolder(asCtx(auth), id));
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
// What may this caller do at the top level? Folder and workflow rows carry
|
|
187
|
+
// their own `permittedActions`, but a root-level "New folder" button has no
|
|
188
|
+
// row to read — this is that decision, and it is the same one create enforces.
|
|
189
|
+
routes.push({
|
|
190
|
+
method: 'GET',
|
|
191
|
+
path: '/capabilities',
|
|
192
|
+
handler: async (_req, auth) => {
|
|
193
|
+
return ok({ permittedActions: await runtime.capabilities(asCtx(auth)) });
|
|
194
|
+
},
|
|
195
|
+
});
|
|
165
196
|
routes.push({
|
|
166
197
|
method: 'POST',
|
|
167
198
|
path: '/folders',
|
|
@@ -172,14 +203,17 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
172
203
|
}
|
|
173
204
|
const parentId = typeof body['parentId'] === 'string' ? body['parentId'] : null;
|
|
174
205
|
try {
|
|
175
|
-
const folder = await runtime.createFolder({
|
|
206
|
+
const folder = await runtime.createFolder(asCtx(auth), {
|
|
176
207
|
name: body['name'],
|
|
177
|
-
metadata:
|
|
208
|
+
...(body['metadata'] ? { metadata: body['metadata'] } : {}),
|
|
178
209
|
parentId,
|
|
210
|
+
attributes: body['attributes'],
|
|
179
211
|
});
|
|
180
212
|
return created(folder);
|
|
181
213
|
}
|
|
182
214
|
catch (err) {
|
|
215
|
+
if (err instanceof AccessDeniedError)
|
|
216
|
+
throw err;
|
|
183
217
|
return error(400, err instanceof Error ? err.message : 'Create failed');
|
|
184
218
|
}
|
|
185
219
|
},
|
|
@@ -187,7 +221,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
187
221
|
routes.push({
|
|
188
222
|
method: 'PUT',
|
|
189
223
|
path: '/folders/:id',
|
|
190
|
-
handler: async (req) => {
|
|
224
|
+
handler: async (req, auth) => {
|
|
191
225
|
const id = req.params['id'];
|
|
192
226
|
if (!id)
|
|
193
227
|
return badRequest('Missing folder ID');
|
|
@@ -201,10 +235,12 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
201
235
|
update.parentId = typeof body['parentId'] === 'string' ? body['parentId'] : null;
|
|
202
236
|
}
|
|
203
237
|
try {
|
|
204
|
-
await runtime.updateFolder(id, update);
|
|
238
|
+
await runtime.updateFolder(asCtx(auth), id, update);
|
|
205
239
|
return ok({ updated: true });
|
|
206
240
|
}
|
|
207
241
|
catch (err) {
|
|
242
|
+
if (err instanceof AccessDeniedError)
|
|
243
|
+
throw err;
|
|
208
244
|
return error(400, err instanceof Error ? err.message : 'Update failed');
|
|
209
245
|
}
|
|
210
246
|
},
|
|
@@ -217,10 +253,12 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
217
253
|
if (!id)
|
|
218
254
|
return badRequest('Missing folder ID');
|
|
219
255
|
try {
|
|
220
|
-
await runtime.deleteFolder(
|
|
256
|
+
await runtime.deleteFolder(asCtx(auth), id);
|
|
221
257
|
return ok({ deleted: true });
|
|
222
258
|
}
|
|
223
259
|
catch (err) {
|
|
260
|
+
if (err instanceof AccessDeniedError)
|
|
261
|
+
throw err;
|
|
224
262
|
const msg = err instanceof Error ? err.message : 'Delete failed';
|
|
225
263
|
// Folder still holds workflows — conflict, not a generic failure.
|
|
226
264
|
if (msg.includes('not empty'))
|
|
@@ -242,10 +280,12 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
242
280
|
if (folders.length === 0 && workflows.length === 0)
|
|
243
281
|
return badRequest('Nothing to move');
|
|
244
282
|
try {
|
|
245
|
-
await runtime.moveBatch(
|
|
283
|
+
await runtime.moveBatch(asCtx(auth), { folders, workflows });
|
|
246
284
|
return ok({ moved: folders.length + workflows.length });
|
|
247
285
|
}
|
|
248
286
|
catch (err) {
|
|
287
|
+
if (err instanceof AccessDeniedError)
|
|
288
|
+
throw err;
|
|
249
289
|
return error(400, err instanceof Error ? err.message : 'Move failed');
|
|
250
290
|
}
|
|
251
291
|
},
|
|
@@ -267,15 +307,17 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
267
307
|
routes.push({
|
|
268
308
|
method: 'POST',
|
|
269
309
|
path: '/workflows/:id/activate',
|
|
270
|
-
handler: async (req,
|
|
310
|
+
handler: async (req, auth) => {
|
|
271
311
|
const id = req.params['id'];
|
|
272
312
|
if (!id)
|
|
273
313
|
return badRequest('Missing workflow ID');
|
|
274
314
|
try {
|
|
275
|
-
await runtime.activateWorkflow(id);
|
|
315
|
+
await runtime.activateWorkflow(asCtx(auth), id);
|
|
276
316
|
return ok({ activated: true });
|
|
277
317
|
}
|
|
278
318
|
catch (err) {
|
|
319
|
+
if (err instanceof AccessDeniedError)
|
|
320
|
+
throw err;
|
|
279
321
|
return error(400, err instanceof Error ? err.message : 'Activation failed');
|
|
280
322
|
}
|
|
281
323
|
},
|
|
@@ -283,15 +325,17 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
283
325
|
routes.push({
|
|
284
326
|
method: 'POST',
|
|
285
327
|
path: '/workflows/:id/deactivate',
|
|
286
|
-
handler: async (req,
|
|
328
|
+
handler: async (req, auth) => {
|
|
287
329
|
const id = req.params['id'];
|
|
288
330
|
if (!id)
|
|
289
331
|
return badRequest('Missing workflow ID');
|
|
290
332
|
try {
|
|
291
|
-
await runtime.deactivateWorkflow(id);
|
|
333
|
+
await runtime.deactivateWorkflow(asCtx(auth), id);
|
|
292
334
|
return ok({ deactivated: true });
|
|
293
335
|
}
|
|
294
336
|
catch (err) {
|
|
337
|
+
if (err instanceof AccessDeniedError)
|
|
338
|
+
throw err;
|
|
295
339
|
return error(400, err instanceof Error ? err.message : 'Deactivation failed');
|
|
296
340
|
}
|
|
297
341
|
},
|
|
@@ -300,16 +344,18 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
300
344
|
routes.push({
|
|
301
345
|
method: 'POST',
|
|
302
346
|
path: '/workflows/:id/trigger',
|
|
303
|
-
handler: async (req,
|
|
347
|
+
handler: async (req, auth) => {
|
|
304
348
|
const id = req.params['id'];
|
|
305
349
|
if (!id)
|
|
306
350
|
return badRequest('Missing workflow ID');
|
|
307
351
|
try {
|
|
308
352
|
const payload = req.body ?? {};
|
|
309
|
-
const executionId = await runtime.triggerManual(id, payload);
|
|
353
|
+
const executionId = await runtime.triggerManual(asCtx(auth), id, payload);
|
|
310
354
|
return created({ executionId });
|
|
311
355
|
}
|
|
312
356
|
catch (err) {
|
|
357
|
+
if (err instanceof AccessDeniedError)
|
|
358
|
+
throw err;
|
|
313
359
|
if (err instanceof Error && err.message.includes('not found')) {
|
|
314
360
|
return notFound(err.message);
|
|
315
361
|
}
|
|
@@ -321,7 +367,8 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
321
367
|
routes.push({
|
|
322
368
|
method: 'POST',
|
|
323
369
|
path: '/uploads',
|
|
324
|
-
|
|
370
|
+
// TODO(uploads): guard/scope via the authorizer once uploads are ACL-modeled.
|
|
371
|
+
handler: async (req, _auth) => {
|
|
325
372
|
if (!runtime.storage) {
|
|
326
373
|
return error(501, 'No StorageAdapter configured — file uploads are disabled');
|
|
327
374
|
}
|
|
@@ -340,7 +387,10 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
340
387
|
}
|
|
341
388
|
}
|
|
342
389
|
}
|
|
343
|
-
|
|
390
|
+
// TODO(uploads): uploads are not yet ACL-scoped. The storage scope must be
|
|
391
|
+
// produceable from BOTH a caller (here) and a workflow (execution-time
|
|
392
|
+
// writes have no caller) — designed in the uploads pass. Empty for now.
|
|
393
|
+
const scope = {};
|
|
344
394
|
try {
|
|
345
395
|
const attachments = [];
|
|
346
396
|
for (const file of files) {
|
|
@@ -353,6 +403,8 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
353
403
|
return created({ attachments });
|
|
354
404
|
}
|
|
355
405
|
catch (err) {
|
|
406
|
+
if (err instanceof AccessDeniedError)
|
|
407
|
+
throw err;
|
|
356
408
|
return error(500, err instanceof Error ? err.message : 'Upload failed');
|
|
357
409
|
}
|
|
358
410
|
},
|
|
@@ -394,6 +446,8 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
394
446
|
};
|
|
395
447
|
}
|
|
396
448
|
catch (err) {
|
|
449
|
+
if (err instanceof AccessDeniedError)
|
|
450
|
+
throw err;
|
|
397
451
|
return error(500, err instanceof Error ? err.message : 'Download failed');
|
|
398
452
|
}
|
|
399
453
|
},
|
|
@@ -403,20 +457,20 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
403
457
|
routes.push({
|
|
404
458
|
method: 'GET',
|
|
405
459
|
path: '/executions/pending-approvals',
|
|
406
|
-
handler: async (req) => {
|
|
460
|
+
handler: async (req, auth) => {
|
|
407
461
|
const limit = typeof req.query['limit'] === 'string' ? Number(req.query['limit']) : undefined;
|
|
408
|
-
const items = await runtime.listPendingApprovals(limit);
|
|
462
|
+
const items = await runtime.listPendingApprovals(asCtx(auth), limit);
|
|
409
463
|
return ok({ items });
|
|
410
464
|
},
|
|
411
465
|
});
|
|
412
466
|
routes.push({
|
|
413
467
|
method: 'GET',
|
|
414
468
|
path: '/executions/:execId',
|
|
415
|
-
handler: async (req) => {
|
|
469
|
+
handler: async (req, auth) => {
|
|
416
470
|
const execId = req.params['execId'];
|
|
417
471
|
if (!execId)
|
|
418
472
|
return badRequest('Missing execution ID');
|
|
419
|
-
const detail = await runtime.getExecutionDetail(execId);
|
|
473
|
+
const detail = await runtime.getExecutionDetail(asCtx(auth), execId);
|
|
420
474
|
if (!detail)
|
|
421
475
|
return notFound(`Execution ${execId} not found`);
|
|
422
476
|
return ok({
|
|
@@ -426,13 +480,14 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
426
480
|
triggerData: detail.triggerData,
|
|
427
481
|
summary: detail.summary,
|
|
428
482
|
name: detail.name,
|
|
483
|
+
permittedActions: detail.permittedActions,
|
|
429
484
|
});
|
|
430
485
|
},
|
|
431
486
|
});
|
|
432
487
|
routes.push({
|
|
433
488
|
method: 'POST',
|
|
434
489
|
path: '/executions/:execId/resume',
|
|
435
|
-
handler: async (req,
|
|
490
|
+
handler: async (req, auth) => {
|
|
436
491
|
const execId = req.params['execId'];
|
|
437
492
|
if (!execId)
|
|
438
493
|
return badRequest('Missing execution ID');
|
|
@@ -453,10 +508,12 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
453
508
|
resumeInput.nodePath = body['nodePath'];
|
|
454
509
|
}
|
|
455
510
|
try {
|
|
456
|
-
await runtime.resume(execId, resumeInput);
|
|
511
|
+
await runtime.resume(asCtx(auth), execId, resumeInput);
|
|
457
512
|
return ok({ resumed: true });
|
|
458
513
|
}
|
|
459
514
|
catch (err) {
|
|
515
|
+
if (err instanceof AccessDeniedError)
|
|
516
|
+
throw err;
|
|
460
517
|
if (err instanceof Error && err.message.includes('not found')) {
|
|
461
518
|
return notFound(err.message);
|
|
462
519
|
}
|
|
@@ -471,7 +528,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
471
528
|
routes.push({
|
|
472
529
|
method: 'GET',
|
|
473
530
|
path: '/executions/:execId/steps/:stepName/events',
|
|
474
|
-
handler: async (req) => {
|
|
531
|
+
handler: async (req, auth) => {
|
|
475
532
|
const execId = req.params['execId'];
|
|
476
533
|
const stepName = req.params['stepName'];
|
|
477
534
|
if (!execId || !stepName)
|
|
@@ -481,7 +538,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
481
538
|
if (after && isNaN(after.getTime())) {
|
|
482
539
|
return badRequest('Invalid query param: after must be an ISO 8601 date string');
|
|
483
540
|
}
|
|
484
|
-
const events = await runtime.getStepEvents(execId, stepName, after);
|
|
541
|
+
const events = await runtime.getStepEvents(asCtx(auth), execId, stepName, after);
|
|
485
542
|
return ok({ events });
|
|
486
543
|
},
|
|
487
544
|
});
|
|
@@ -498,16 +555,15 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
498
555
|
if (isNaN(since.getTime()))
|
|
499
556
|
return badRequest('Invalid query param: since must be an ISO 8601 date string');
|
|
500
557
|
const rangeDays = typeof rangeDaysStr === 'string' && rangeDaysStr ? parseInt(rangeDaysStr, 10) : 7;
|
|
501
|
-
const metadata = auth?.metadata ?? {};
|
|
502
558
|
const folderId = typeof req.query['folderId'] === 'string' ? req.query['folderId'] : undefined;
|
|
503
|
-
const summary = await runtime.getAnalyticsSummary({ since, rangeDays,
|
|
559
|
+
const summary = await runtime.getAnalyticsSummary(asCtx(auth), { since, rangeDays, ...(folderId ? { folderId } : {}) });
|
|
504
560
|
return ok(summary);
|
|
505
561
|
},
|
|
506
562
|
});
|
|
507
563
|
routes.push({
|
|
508
564
|
method: 'GET',
|
|
509
565
|
path: '/analytics/top-errors',
|
|
510
|
-
handler: async (req) => {
|
|
566
|
+
handler: async (req, auth) => {
|
|
511
567
|
const sinceStr = req.query['since'];
|
|
512
568
|
const limitStr = req.query['limit'];
|
|
513
569
|
const folderIdStr = req.query['folderId'];
|
|
@@ -517,7 +573,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
517
573
|
const since = sinceDate;
|
|
518
574
|
const limit = typeof limitStr === 'string' && limitStr ? parseInt(limitStr, 10) : 5;
|
|
519
575
|
const folderId = typeof folderIdStr === 'string' ? folderIdStr : undefined;
|
|
520
|
-
const topErrors = await runtime.getTopErrors({ ...(since ? { since } : {}), limit, ...(folderId !== undefined ? { folderId } : {}) });
|
|
576
|
+
const topErrors = await runtime.getTopErrors(asCtx(auth), { ...(since ? { since } : {}), limit, ...(folderId !== undefined ? { folderId } : {}) });
|
|
521
577
|
return ok({ topErrors });
|
|
522
578
|
},
|
|
523
579
|
});
|
|
@@ -628,7 +684,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
628
684
|
routes.push({
|
|
629
685
|
method: 'GET',
|
|
630
686
|
path: '/executions',
|
|
631
|
-
handler: async (req) => {
|
|
687
|
+
handler: async (req, auth) => {
|
|
632
688
|
const query = req.query;
|
|
633
689
|
const params = {};
|
|
634
690
|
if (typeof query['workflowId'] === 'string')
|
|
@@ -641,7 +697,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
641
697
|
params.cursor = query['cursor'];
|
|
642
698
|
if (typeof query['folderId'] === 'string')
|
|
643
699
|
params.folderId = query['folderId'];
|
|
644
|
-
const result = await runtime.listExecutions(params);
|
|
700
|
+
const result = await runtime.listExecutions(asCtx(auth), params);
|
|
645
701
|
return ok(result);
|
|
646
702
|
},
|
|
647
703
|
});
|
|
@@ -649,7 +705,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
649
705
|
routes.push({
|
|
650
706
|
method: 'POST',
|
|
651
707
|
path: '/executions/:execId/rerun',
|
|
652
|
-
handler: async (req) => {
|
|
708
|
+
handler: async (req, auth) => {
|
|
653
709
|
const execId = req.params['execId'];
|
|
654
710
|
if (!execId)
|
|
655
711
|
return badRequest('Missing execution ID');
|
|
@@ -666,10 +722,12 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
666
722
|
opts.inheritStepState = body['inheritStepState'];
|
|
667
723
|
}
|
|
668
724
|
try {
|
|
669
|
-
const newExecutionId = await runtime.rerunFromStep(execId, fromStepId, opts);
|
|
725
|
+
const newExecutionId = await runtime.rerunFromStep(asCtx(auth), execId, fromStepId, opts);
|
|
670
726
|
return ok({ executionId: newExecutionId });
|
|
671
727
|
}
|
|
672
728
|
catch (err) {
|
|
729
|
+
if (err instanceof AccessDeniedError)
|
|
730
|
+
throw err;
|
|
673
731
|
if (err instanceof Error && err.message.includes('not found')) {
|
|
674
732
|
return notFound(err.message);
|
|
675
733
|
}
|
|
@@ -681,15 +739,17 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
681
739
|
routes.push({
|
|
682
740
|
method: 'POST',
|
|
683
741
|
path: '/executions/:execId/cancel',
|
|
684
|
-
handler: async (req) => {
|
|
742
|
+
handler: async (req, auth) => {
|
|
685
743
|
const execId = req.params['execId'];
|
|
686
744
|
if (!execId)
|
|
687
745
|
return badRequest('Missing execution ID');
|
|
688
746
|
try {
|
|
689
|
-
await runtime.cancelExecution(execId);
|
|
747
|
+
await runtime.cancelExecution(asCtx(auth), execId);
|
|
690
748
|
return ok({ cancelled: true });
|
|
691
749
|
}
|
|
692
750
|
catch (err) {
|
|
751
|
+
if (err instanceof AccessDeniedError)
|
|
752
|
+
throw err;
|
|
693
753
|
if (err instanceof Error && err.message.includes('not found')) {
|
|
694
754
|
return notFound(err.message);
|
|
695
755
|
}
|
|
@@ -702,10 +762,23 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
702
762
|
method: 'GET',
|
|
703
763
|
path: '/credentials',
|
|
704
764
|
handler: async (_req, auth) => {
|
|
705
|
-
const credentials = await runtime.listCredentials(
|
|
765
|
+
const credentials = await runtime.listCredentials(asCtx(auth));
|
|
706
766
|
return ok({ credentials });
|
|
707
767
|
},
|
|
708
768
|
});
|
|
769
|
+
routes.push({
|
|
770
|
+
method: 'GET',
|
|
771
|
+
path: '/credentials/:id',
|
|
772
|
+
handler: async (req, auth) => {
|
|
773
|
+
const id = req.params['id'];
|
|
774
|
+
if (!id)
|
|
775
|
+
return badRequest('Missing credential id');
|
|
776
|
+
const cred = await runtime.getCredential(asCtx(auth), id);
|
|
777
|
+
if (!cred)
|
|
778
|
+
return notFound(`Credential ${id} not found`);
|
|
779
|
+
return ok(cred);
|
|
780
|
+
},
|
|
781
|
+
});
|
|
709
782
|
routes.push({
|
|
710
783
|
method: 'POST',
|
|
711
784
|
path: '/credentials',
|
|
@@ -714,26 +787,45 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
714
787
|
if (!body || !body['name'] || !body['type'] || !body['data']) {
|
|
715
788
|
return badRequest('Body must contain "name", "type", and "data"');
|
|
716
789
|
}
|
|
717
|
-
|
|
718
|
-
|
|
790
|
+
try {
|
|
791
|
+
const cred = await runtime.createCredential(asCtx(auth), {
|
|
792
|
+
name: body['name'],
|
|
793
|
+
type: body['type'],
|
|
794
|
+
data: body['data'],
|
|
795
|
+
attributes: body['attributes'],
|
|
796
|
+
});
|
|
797
|
+
return created(cred);
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
if (err instanceof AccessDeniedError)
|
|
801
|
+
throw err;
|
|
802
|
+
return error(400, err instanceof Error ? err.message : 'Create failed');
|
|
803
|
+
}
|
|
719
804
|
},
|
|
720
805
|
});
|
|
721
806
|
routes.push({
|
|
722
807
|
method: 'DELETE',
|
|
723
|
-
path: '/credentials/:
|
|
808
|
+
path: '/credentials/:id',
|
|
724
809
|
handler: async (req, auth) => {
|
|
725
|
-
const
|
|
726
|
-
if (!
|
|
727
|
-
return badRequest('Missing credential
|
|
728
|
-
|
|
729
|
-
|
|
810
|
+
const id = req.params['id'];
|
|
811
|
+
if (!id)
|
|
812
|
+
return badRequest('Missing credential id');
|
|
813
|
+
try {
|
|
814
|
+
await runtime.deleteCredential(asCtx(auth), id);
|
|
815
|
+
return { status: 204, body: undefined };
|
|
816
|
+
}
|
|
817
|
+
catch (err) {
|
|
818
|
+
if (err instanceof AccessDeniedError)
|
|
819
|
+
throw err;
|
|
820
|
+
return notFound(err instanceof Error ? err.message : 'Delete failed');
|
|
821
|
+
}
|
|
730
822
|
},
|
|
731
823
|
});
|
|
732
824
|
// ─── SSE: Execution Event Stream ───
|
|
733
825
|
routes.push({
|
|
734
826
|
method: 'GET',
|
|
735
827
|
path: '/executions/:execId/stream',
|
|
736
|
-
handler: async (req) => {
|
|
828
|
+
handler: async (req, auth) => {
|
|
737
829
|
const execId = req.params['execId'];
|
|
738
830
|
if (!execId)
|
|
739
831
|
return badRequest('Missing execution ID');
|
|
@@ -741,7 +833,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
741
833
|
if (!eventBus) {
|
|
742
834
|
return error(501, 'Event streaming not configured');
|
|
743
835
|
}
|
|
744
|
-
const detail = await runtime.getExecutionDetail(execId);
|
|
836
|
+
const detail = await runtime.getExecutionDetail(asCtx(auth), execId);
|
|
745
837
|
if (!detail)
|
|
746
838
|
return notFound(`Execution ${execId} not found`);
|
|
747
839
|
const isTerminal = ['COMPLETED', 'FAILED', 'CANCELLED'].includes(detail.execution.status);
|
|
@@ -844,6 +936,8 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
844
936
|
}
|
|
845
937
|
}
|
|
846
938
|
catch (err) {
|
|
939
|
+
if (err instanceof AccessDeniedError)
|
|
940
|
+
throw err;
|
|
847
941
|
const m = err instanceof Error ? err.message : String(err);
|
|
848
942
|
console.error(`[${tag}] agentLoop failed:`, m);
|
|
849
943
|
yield formatSSE('error', { error: `LLM call failed: ${m}` });
|
|
@@ -870,7 +964,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
870
964
|
method: 'POST',
|
|
871
965
|
path: '/ai-builder/chat',
|
|
872
966
|
authenticated: false,
|
|
873
|
-
handler: async (req) => {
|
|
967
|
+
handler: async (req, auth) => {
|
|
874
968
|
const body = req.body;
|
|
875
969
|
if (!body.messages || !Array.isArray(body.messages)) {
|
|
876
970
|
return badRequest('messages array is required');
|
|
@@ -885,7 +979,7 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
885
979
|
}
|
|
886
980
|
// The chat always runs inside a specific run, so we load it server-side
|
|
887
981
|
// rather than trusting/transferring a large client blob.
|
|
888
|
-
const detail = await runtime.getExecutionDetail(approvalBody.executionId);
|
|
982
|
+
const detail = await runtime.getExecutionDetail(asCtx(auth), approvalBody.executionId);
|
|
889
983
|
if (!detail)
|
|
890
984
|
return notFound(`Execution ${approvalBody.executionId} not found`);
|
|
891
985
|
const execCtx = {
|
|
@@ -976,6 +1070,8 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
976
1070
|
}
|
|
977
1071
|
}
|
|
978
1072
|
catch (err) {
|
|
1073
|
+
if (err instanceof AccessDeniedError)
|
|
1074
|
+
throw err;
|
|
979
1075
|
const msg = err instanceof Error ? err.message : String(err);
|
|
980
1076
|
console.error('[approval-ai] agentLoop failed:', msg);
|
|
981
1077
|
return { status: 502, body: { error: `LLM call failed: ${msg}` } };
|
|
@@ -1046,6 +1142,8 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
1046
1142
|
}
|
|
1047
1143
|
}
|
|
1048
1144
|
catch (err) {
|
|
1145
|
+
if (err instanceof AccessDeniedError)
|
|
1146
|
+
throw err;
|
|
1049
1147
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1050
1148
|
console.error('[ai-builder] agentLoop failed:', msg);
|
|
1051
1149
|
return { status: 502, body: { error: `LLM call failed: ${msg}` } };
|
|
@@ -1058,7 +1156,26 @@ export function createWorkflowRouter(runtime, _opts) {
|
|
|
1058
1156
|
},
|
|
1059
1157
|
});
|
|
1060
1158
|
}
|
|
1061
|
-
|
|
1159
|
+
// Uniformly map the runtime's two authorization outcomes to HTTP across every
|
|
1160
|
+
// handler, so enforcement in the runtime surfaces as a proper status rather
|
|
1161
|
+
// than a 500 — and so a new route cannot forget to do it.
|
|
1162
|
+
return routes.map((route) => ({
|
|
1163
|
+
...route,
|
|
1164
|
+
handler: async (req, auth) => {
|
|
1165
|
+
try {
|
|
1166
|
+
return await route.handler(req, auth);
|
|
1167
|
+
}
|
|
1168
|
+
catch (err) {
|
|
1169
|
+
if (err instanceof AccessDeniedError) {
|
|
1170
|
+
return { status: 403, body: { error: err.message } };
|
|
1171
|
+
}
|
|
1172
|
+
if (err instanceof ResourceNotFoundError) {
|
|
1173
|
+
return { status: 404, body: { error: err.message } };
|
|
1174
|
+
}
|
|
1175
|
+
throw err;
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
}));
|
|
1062
1179
|
}
|
|
1063
1180
|
// ─── AI Builder helpers ───
|
|
1064
1181
|
const AI_BUILDER_SYSTEM_PROMPT = `You are a workflow automation assistant. Output valid WorkflowConfig JSON.
|