@xyne/workflow-sdk 3.2.14 → 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.
Files changed (97) hide show
  1. package/dist/agents/agent-step.d.ts.map +1 -1
  2. package/dist/agents/agent-step.js +3 -1
  3. package/dist/agents/agent-step.js.map +1 -1
  4. package/dist/authz/attributes.d.ts +41 -0
  5. package/dist/authz/attributes.d.ts.map +1 -0
  6. package/dist/authz/attributes.js +2 -0
  7. package/dist/authz/attributes.js.map +1 -0
  8. package/dist/authz/default.d.ts +31 -0
  9. package/dist/authz/default.d.ts.map +1 -0
  10. package/dist/authz/default.js +47 -0
  11. package/dist/authz/default.js.map +1 -0
  12. package/dist/authz/id-scope.d.ts +27 -0
  13. package/dist/authz/id-scope.d.ts.map +1 -0
  14. package/dist/authz/id-scope.js +24 -0
  15. package/dist/authz/id-scope.js.map +1 -0
  16. package/dist/authz/principal-field.d.ts +23 -0
  17. package/dist/authz/principal-field.d.ts.map +1 -0
  18. package/dist/authz/principal-field.js +34 -0
  19. package/dist/authz/principal-field.js.map +1 -0
  20. package/dist/authz/types.d.ts +129 -0
  21. package/dist/authz/types.d.ts.map +1 -0
  22. package/dist/authz/types.js +74 -0
  23. package/dist/authz/types.js.map +1 -0
  24. package/dist/client/types.d.ts +26 -1
  25. package/dist/client/types.d.ts.map +1 -1
  26. package/dist/client/workflow-client.d.ts.map +1 -1
  27. package/dist/client/workflow-client.js +21 -2
  28. package/dist/client/workflow-client.js.map +1 -1
  29. package/dist/engine/workflow-executor.d.ts +1 -1
  30. package/dist/engine/workflow-executor.d.ts.map +1 -1
  31. package/dist/engine/workflow-executor.js +13 -2
  32. package/dist/engine/workflow-executor.js.map +1 -1
  33. package/dist/index.d.ts +7 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/persistence/in-memory-adapter.d.ts +40 -20
  38. package/dist/persistence/in-memory-adapter.d.ts.map +1 -1
  39. package/dist/persistence/in-memory-adapter.js +108 -38
  40. package/dist/persistence/in-memory-adapter.js.map +1 -1
  41. package/dist/persistence/types.d.ts +117 -20
  42. package/dist/persistence/types.d.ts.map +1 -1
  43. package/dist/router/types.d.ts +11 -8
  44. package/dist/router/types.d.ts.map +1 -1
  45. package/dist/router/workflow-router.d.ts +2 -2
  46. package/dist/router/workflow-router.d.ts.map +1 -1
  47. package/dist/router/workflow-router.js +179 -62
  48. package/dist/router/workflow-router.js.map +1 -1
  49. package/dist/runtime/types.d.ts +22 -2
  50. package/dist/runtime/types.d.ts.map +1 -1
  51. package/dist/runtime/workflow-runtime.d.ts +137 -43
  52. package/dist/runtime/workflow-runtime.d.ts.map +1 -1
  53. package/dist/runtime/workflow-runtime.js +288 -98
  54. package/dist/runtime/workflow-runtime.js.map +1 -1
  55. package/dist/steps/base-step.d.ts +19 -2
  56. package/dist/steps/base-step.d.ts.map +1 -1
  57. package/dist/steps/base-step.js.map +1 -1
  58. package/dist/storage/types.d.ts +14 -4
  59. package/dist/storage/types.d.ts.map +1 -1
  60. package/dist/testing/authz-conformance.d.ts +90 -0
  61. package/dist/testing/authz-conformance.d.ts.map +1 -0
  62. package/dist/testing/authz-conformance.js +250 -0
  63. package/dist/testing/authz-conformance.js.map +1 -0
  64. package/dist/testing/index.d.ts +7 -0
  65. package/dist/testing/index.d.ts.map +1 -1
  66. package/dist/testing/index.js +6 -0
  67. package/dist/testing/index.js.map +1 -1
  68. package/dist/testing/mock-context.d.ts +4 -0
  69. package/dist/testing/mock-context.d.ts.map +1 -1
  70. package/dist/testing/mock-context.js +4 -1
  71. package/dist/testing/mock-context.js.map +1 -1
  72. package/dist/testing/mock-step-context.d.ts.map +1 -1
  73. package/dist/testing/mock-step-context.js +1 -0
  74. package/dist/testing/mock-step-context.js.map +1 -1
  75. package/dist/types/context.d.ts +22 -3
  76. package/dist/types/context.d.ts.map +1 -1
  77. package/package.json +9 -3
  78. package/dist/common/principal.d.ts +0 -45
  79. package/dist/common/principal.d.ts.map +0 -1
  80. package/dist/common/principal.js +0 -9
  81. package/dist/common/principal.js.map +0 -1
  82. package/dist/steps/builtin/transform.step.d.ts +0 -247
  83. package/dist/steps/builtin/transform.step.d.ts.map +0 -1
  84. package/dist/steps/builtin/transform.step.js +0 -135
  85. package/dist/steps/builtin/transform.step.js.map +0 -1
  86. package/dist/types/attachment.d.ts +0 -23
  87. package/dist/types/attachment.d.ts.map +0 -1
  88. package/dist/types/attachment.js +0 -2
  89. package/dist/types/attachment.js.map +0 -1
  90. package/dist/types/resume-payload.d.ts +0 -34
  91. package/dist/types/resume-payload.d.ts.map +0 -1
  92. package/dist/types/resume-payload.js +0 -12
  93. package/dist/types/resume-payload.js.map +0 -1
  94. package/dist/util/executable-check.d.ts +0 -42
  95. package/dist/util/executable-check.d.ts.map +0 -1
  96. package/dist/util/executable-check.js +0 -115
  97. 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(runtime, _opts) {
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(authMetadata(auth), filter);
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(authMetadata(auth));
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
- const workflow = await runtime.getWorkflow(id);
91
- if (!workflow)
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, _auth) => {
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, _auth) => {
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(authMetadata(auth));
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: authMetadata(auth),
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(id, authMetadata(auth));
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(authMetadata(auth), { folders, workflows });
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, _auth) => {
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, _auth) => {
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, _auth) => {
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
- handler: async (req, auth) => {
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
- const scope = auth?.metadata ?? {};
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, _auth) => {
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, metadata, ...(folderId ? { folderId } : {}) });
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(authMetadata(auth));
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
- const cred = await runtime.createCredential(authMetadata(auth), body['name'], { type: body['type'], data: body['data'] });
718
- return created(cred);
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/:name',
808
+ path: '/credentials/:id',
724
809
  handler: async (req, auth) => {
725
- const name = req.params['name'];
726
- if (!name)
727
- return badRequest('Missing credential name');
728
- await runtime.deleteCredential(authMetadata(auth), name);
729
- return { status: 204, body: undefined };
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
- return routes;
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.