@xyne/workflow-sdk 3.2.36 → 3.2.38

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 (261) hide show
  1. package/dist/agents/agent-step.d.ts +2 -2
  2. package/dist/agents/host-agent-step.d.ts +1 -1
  3. package/dist/builder/index.d.ts +2 -2
  4. package/dist/builder/index.d.ts.map +1 -1
  5. package/dist/builder/index.js +1 -1
  6. package/dist/builder/index.js.map +1 -1
  7. package/dist/client/events-client.d.ts +57 -0
  8. package/dist/client/events-client.d.ts.map +1 -0
  9. package/dist/client/events-client.js +107 -0
  10. package/dist/client/events-client.js.map +1 -0
  11. package/dist/client/index.d.ts +2 -0
  12. package/dist/client/index.d.ts.map +1 -1
  13. package/dist/client/sse.d.ts +30 -0
  14. package/dist/client/sse.d.ts.map +1 -0
  15. package/dist/client/sse.js +76 -0
  16. package/dist/client/sse.js.map +1 -0
  17. package/dist/client/types.d.ts +31 -6
  18. package/dist/client/types.d.ts.map +1 -1
  19. package/dist/client/url.d.ts +11 -0
  20. package/dist/client/url.d.ts.map +1 -0
  21. package/dist/client/url.js +28 -0
  22. package/dist/client/url.js.map +1 -0
  23. package/dist/client/workflow-client.d.ts +1 -1
  24. package/dist/client/workflow-client.d.ts.map +1 -1
  25. package/dist/client/workflow-client.js +32 -20
  26. package/dist/client/workflow-client.js.map +1 -1
  27. package/dist/common/citation-ref.d.ts +4 -4
  28. package/dist/common/credentials.d.ts +107 -0
  29. package/dist/common/credentials.d.ts.map +1 -1
  30. package/dist/common/credentials.js +67 -0
  31. package/dist/common/credentials.js.map +1 -1
  32. package/dist/common/index.d.ts +2 -2
  33. package/dist/common/index.d.ts.map +1 -1
  34. package/dist/common/index.js +2 -2
  35. package/dist/common/index.js.map +1 -1
  36. package/dist/connectors/builtin/builtin-connector.d.ts +32 -0
  37. package/dist/connectors/builtin/builtin-connector.d.ts.map +1 -0
  38. package/dist/connectors/builtin/builtin-connector.js +62 -0
  39. package/dist/connectors/builtin/builtin-connector.js.map +1 -0
  40. package/dist/connectors/builtin/credentials.d.ts +18 -0
  41. package/dist/connectors/builtin/credentials.d.ts.map +1 -0
  42. package/dist/connectors/builtin/credentials.js +50 -0
  43. package/dist/connectors/builtin/credentials.js.map +1 -0
  44. package/dist/connectors/builtin/steps/code.step.d.ts +99 -0
  45. package/dist/connectors/builtin/steps/code.step.d.ts.map +1 -0
  46. package/dist/connectors/builtin/steps/code.step.js +152 -0
  47. package/dist/connectors/builtin/steps/code.step.js.map +1 -0
  48. package/dist/connectors/builtin/steps/conditional.step.d.ts +182 -0
  49. package/dist/connectors/builtin/steps/conditional.step.d.ts.map +1 -0
  50. package/dist/connectors/builtin/steps/conditional.step.js +82 -0
  51. package/dist/connectors/builtin/steps/conditional.step.js.map +1 -0
  52. package/dist/connectors/builtin/steps/dedup.step.d.ts +65 -0
  53. package/dist/connectors/builtin/steps/dedup.step.d.ts.map +1 -0
  54. package/dist/connectors/builtin/steps/dedup.step.js +61 -0
  55. package/dist/connectors/builtin/steps/dedup.step.js.map +1 -0
  56. package/dist/connectors/builtin/steps/http-request.step.d.ts +1027 -0
  57. package/dist/connectors/builtin/steps/http-request.step.d.ts.map +1 -0
  58. package/dist/connectors/builtin/steps/http-request.step.js +497 -0
  59. package/dist/connectors/builtin/steps/http-request.step.js.map +1 -0
  60. package/dist/connectors/builtin/steps/loop.step.d.ts +100 -0
  61. package/dist/connectors/builtin/steps/loop.step.d.ts.map +1 -0
  62. package/dist/connectors/builtin/steps/loop.step.js +73 -0
  63. package/dist/connectors/builtin/steps/loop.step.js.map +1 -0
  64. package/dist/connectors/builtin/steps/map.step.d.ts +148 -0
  65. package/dist/connectors/builtin/steps/map.step.d.ts.map +1 -0
  66. package/dist/connectors/builtin/steps/map.step.js +111 -0
  67. package/dist/connectors/builtin/steps/map.step.js.map +1 -0
  68. package/dist/connectors/builtin/steps/parallel.step.d.ts +246 -0
  69. package/dist/connectors/builtin/steps/parallel.step.d.ts.map +1 -0
  70. package/dist/connectors/builtin/steps/parallel.step.js +175 -0
  71. package/dist/connectors/builtin/steps/parallel.step.js.map +1 -0
  72. package/dist/connectors/builtin/steps/ssrf-guard.d.ts +9 -0
  73. package/dist/connectors/builtin/steps/ssrf-guard.d.ts.map +1 -0
  74. package/dist/connectors/builtin/steps/ssrf-guard.js +115 -0
  75. package/dist/connectors/builtin/steps/ssrf-guard.js.map +1 -0
  76. package/dist/connectors/builtin/steps/switch.step.d.ts +235 -0
  77. package/dist/connectors/builtin/steps/switch.step.d.ts.map +1 -0
  78. package/dist/connectors/builtin/steps/switch.step.js +101 -0
  79. package/dist/connectors/builtin/steps/switch.step.js.map +1 -0
  80. package/dist/connectors/builtin/steps/wait.step.d.ts +1072 -0
  81. package/dist/connectors/builtin/steps/wait.step.d.ts.map +1 -0
  82. package/dist/connectors/builtin/steps/wait.step.js +254 -0
  83. package/dist/connectors/builtin/steps/wait.step.js.map +1 -0
  84. package/dist/connectors/builtin/triggers/default-cron-trigger.d.ts +44 -0
  85. package/dist/connectors/builtin/triggers/default-cron-trigger.d.ts.map +1 -0
  86. package/dist/connectors/builtin/triggers/default-cron-trigger.js +31 -0
  87. package/dist/connectors/builtin/triggers/default-cron-trigger.js.map +1 -0
  88. package/dist/connectors/builtin/triggers/default-event-trigger.d.ts +32 -0
  89. package/dist/connectors/builtin/triggers/default-event-trigger.d.ts.map +1 -0
  90. package/dist/connectors/builtin/triggers/default-event-trigger.js +21 -0
  91. package/dist/connectors/builtin/triggers/default-event-trigger.js.map +1 -0
  92. package/dist/connectors/builtin/triggers/default-manual-trigger.d.ts +45 -0
  93. package/dist/connectors/builtin/triggers/default-manual-trigger.d.ts.map +1 -0
  94. package/dist/connectors/builtin/triggers/default-manual-trigger.js +51 -0
  95. package/dist/connectors/builtin/triggers/default-manual-trigger.js.map +1 -0
  96. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.d.ts +31 -0
  97. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.d.ts.map +1 -0
  98. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.js +40 -0
  99. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.js.map +1 -0
  100. package/dist/connectors/core/base-connector.d.ts +45 -0
  101. package/dist/connectors/core/base-connector.d.ts.map +1 -0
  102. package/dist/connectors/core/base-connector.js +3 -0
  103. package/dist/connectors/core/base-connector.js.map +1 -0
  104. package/dist/connectors/core/connector-registry.d.ts +130 -0
  105. package/dist/connectors/core/connector-registry.d.ts.map +1 -0
  106. package/dist/connectors/core/connector-registry.js +357 -0
  107. package/dist/connectors/core/connector-registry.js.map +1 -0
  108. package/dist/connectors/core/credentials.d.ts +104 -0
  109. package/dist/connectors/core/credentials.d.ts.map +1 -0
  110. package/dist/connectors/core/credentials.js +67 -0
  111. package/dist/connectors/core/credentials.js.map +1 -0
  112. package/dist/connectors/core/index.d.ts +7 -0
  113. package/dist/connectors/core/index.d.ts.map +1 -0
  114. package/dist/connectors/core/index.js +5 -0
  115. package/dist/connectors/core/index.js.map +1 -0
  116. package/dist/engine/available-context.d.ts +3 -4
  117. package/dist/engine/available-context.d.ts.map +1 -1
  118. package/dist/engine/available-context.js +17 -13
  119. package/dist/engine/available-context.js.map +1 -1
  120. package/dist/engine/config-validator.d.ts +13 -5
  121. package/dist/engine/config-validator.d.ts.map +1 -1
  122. package/dist/engine/config-validator.js +89 -11
  123. package/dist/engine/config-validator.js.map +1 -1
  124. package/dist/engine/credential-resolver.d.ts +42 -0
  125. package/dist/engine/credential-resolver.d.ts.map +1 -0
  126. package/dist/engine/credential-resolver.js +63 -0
  127. package/dist/engine/credential-resolver.js.map +1 -0
  128. package/dist/engine/workflow-executor.d.ts +15 -9
  129. package/dist/engine/workflow-executor.d.ts.map +1 -1
  130. package/dist/engine/workflow-executor.js +31 -40
  131. package/dist/engine/workflow-executor.js.map +1 -1
  132. package/dist/events/in-memory-adapter.d.ts +15 -0
  133. package/dist/events/in-memory-adapter.d.ts.map +1 -0
  134. package/dist/events/in-memory-adapter.js +50 -0
  135. package/dist/events/in-memory-adapter.js.map +1 -0
  136. package/dist/events/types.d.ts +154 -0
  137. package/dist/events/types.d.ts.map +1 -0
  138. package/dist/events/types.js +34 -0
  139. package/dist/events/types.js.map +1 -0
  140. package/dist/index.d.ts +50 -31
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +39 -16
  143. package/dist/index.js.map +1 -1
  144. package/dist/ingest/body.d.ts +18 -0
  145. package/dist/ingest/body.d.ts.map +1 -0
  146. package/dist/ingest/body.js +47 -0
  147. package/dist/ingest/body.js.map +1 -0
  148. package/dist/ingest/filter.d.ts +11 -0
  149. package/dist/ingest/filter.d.ts.map +1 -0
  150. package/dist/ingest/filter.js +23 -0
  151. package/dist/ingest/filter.js.map +1 -0
  152. package/dist/ingest/headers.d.ts +3 -0
  153. package/dist/ingest/headers.d.ts.map +1 -0
  154. package/dist/ingest/headers.js +11 -0
  155. package/dist/ingest/headers.js.map +1 -0
  156. package/dist/ingest/types.d.ts +50 -0
  157. package/dist/ingest/types.d.ts.map +1 -0
  158. package/dist/ingest/types.js +9 -0
  159. package/dist/ingest/types.js.map +1 -0
  160. package/dist/persistence/in-memory-adapter.d.ts +4 -8
  161. package/dist/persistence/in-memory-adapter.d.ts.map +1 -1
  162. package/dist/persistence/in-memory-adapter.js +8 -25
  163. package/dist/persistence/in-memory-adapter.js.map +1 -1
  164. package/dist/persistence/types.d.ts +5 -19
  165. package/dist/persistence/types.d.ts.map +1 -1
  166. package/dist/router/types.d.ts +31 -3
  167. package/dist/router/types.d.ts.map +1 -1
  168. package/dist/router/workflow-router.d.ts +4 -3
  169. package/dist/router/workflow-router.d.ts.map +1 -1
  170. package/dist/router/workflow-router.js +258 -63
  171. package/dist/router/workflow-router.js.map +1 -1
  172. package/dist/runtime/types.d.ts +36 -56
  173. package/dist/runtime/types.d.ts.map +1 -1
  174. package/dist/runtime/workflow-runtime.d.ts +70 -46
  175. package/dist/runtime/workflow-runtime.d.ts.map +1 -1
  176. package/dist/runtime/workflow-runtime.js +217 -141
  177. package/dist/runtime/workflow-runtime.js.map +1 -1
  178. package/dist/steps/base-step.d.ts +23 -0
  179. package/dist/steps/base-step.d.ts.map +1 -1
  180. package/dist/steps/base-step.js +22 -0
  181. package/dist/steps/base-step.js.map +1 -1
  182. package/dist/steps/step-descriptor.d.ts +52 -0
  183. package/dist/steps/step-descriptor.d.ts.map +1 -0
  184. package/dist/steps/step-descriptor.js +32 -0
  185. package/dist/steps/step-descriptor.js.map +1 -0
  186. package/dist/testing/authz-conformance.d.ts.map +1 -1
  187. package/dist/testing/authz-conformance.js +6 -7
  188. package/dist/testing/authz-conformance.js.map +1 -1
  189. package/dist/testing/index.d.ts +1 -0
  190. package/dist/testing/index.d.ts.map +1 -1
  191. package/dist/testing/index.js +1 -0
  192. package/dist/testing/index.js.map +1 -1
  193. package/dist/testing/mock-step-context.d.ts +1 -0
  194. package/dist/testing/mock-step-context.d.ts.map +1 -1
  195. package/dist/testing/mock-step-context.js +1 -0
  196. package/dist/testing/mock-step-context.js.map +1 -1
  197. package/dist/testing/test-connector.d.ts +40 -0
  198. package/dist/testing/test-connector.d.ts.map +1 -0
  199. package/dist/testing/test-connector.js +39 -0
  200. package/dist/testing/test-connector.js.map +1 -0
  201. package/dist/triggers/api-trigger.d.ts +15 -0
  202. package/dist/triggers/api-trigger.d.ts.map +1 -0
  203. package/dist/triggers/api-trigger.js +14 -0
  204. package/dist/triggers/api-trigger.js.map +1 -0
  205. package/dist/triggers/base-trigger.d.ts +29 -0
  206. package/dist/triggers/base-trigger.d.ts.map +1 -1
  207. package/dist/triggers/base-trigger.js +16 -0
  208. package/dist/triggers/base-trigger.js.map +1 -1
  209. package/dist/triggers/trigger-descriptor.d.ts +42 -0
  210. package/dist/triggers/trigger-descriptor.d.ts.map +1 -0
  211. package/dist/triggers/trigger-descriptor.js +29 -0
  212. package/dist/triggers/trigger-descriptor.js.map +1 -0
  213. package/dist/triggers/webhook-trigger.d.ts +31 -38
  214. package/dist/triggers/webhook-trigger.d.ts.map +1 -1
  215. package/dist/triggers/webhook-trigger.js +19 -6
  216. package/dist/triggers/webhook-trigger.js.map +1 -1
  217. package/dist/types/known-types.d.ts +0 -1
  218. package/dist/types/known-types.d.ts.map +1 -1
  219. package/dist/types/known-types.js +0 -1
  220. package/dist/types/known-types.js.map +1 -1
  221. package/dist/types/validation.d.ts +3 -1
  222. package/dist/types/validation.d.ts.map +1 -1
  223. package/dist/types/validation.js +2 -0
  224. package/dist/types/validation.js.map +1 -1
  225. package/dist/types/workflow-config.d.ts +23 -0
  226. package/dist/types/workflow-config.d.ts.map +1 -1
  227. package/dist/types/workflow-config.js +8 -0
  228. package/dist/types/workflow-config.js.map +1 -1
  229. package/dist/util/credential-slot-schema.d.ts +20 -0
  230. package/dist/util/credential-slot-schema.d.ts.map +1 -0
  231. package/dist/util/credential-slot-schema.js +30 -0
  232. package/dist/util/credential-slot-schema.js.map +1 -0
  233. package/dist/util/event-channel.d.ts +25 -0
  234. package/dist/util/event-channel.d.ts.map +1 -0
  235. package/dist/util/event-channel.js +61 -0
  236. package/dist/util/event-channel.js.map +1 -0
  237. package/package.json +1 -1
  238. package/dist/common/expression-eval.d.ts +0 -35
  239. package/dist/common/expression-eval.d.ts.map +0 -1
  240. package/dist/common/expression-eval.js +0 -214
  241. package/dist/common/expression-eval.js.map +0 -1
  242. package/dist/common/principal.d.ts +0 -45
  243. package/dist/common/principal.d.ts.map +0 -1
  244. package/dist/common/principal.js +0 -9
  245. package/dist/common/principal.js.map +0 -1
  246. package/dist/steps/builtin/transform.step.d.ts +0 -247
  247. package/dist/steps/builtin/transform.step.d.ts.map +0 -1
  248. package/dist/steps/builtin/transform.step.js +0 -135
  249. package/dist/steps/builtin/transform.step.js.map +0 -1
  250. package/dist/types/attachment.d.ts +0 -23
  251. package/dist/types/attachment.d.ts.map +0 -1
  252. package/dist/types/attachment.js +0 -2
  253. package/dist/types/attachment.js.map +0 -1
  254. package/dist/types/resume-payload.d.ts +0 -34
  255. package/dist/types/resume-payload.d.ts.map +0 -1
  256. package/dist/types/resume-payload.js +0 -12
  257. package/dist/types/resume-payload.js.map +0 -1
  258. package/dist/util/executable-check.d.ts +0 -42
  259. package/dist/util/executable-check.d.ts.map +0 -1
  260. package/dist/util/executable-check.js +0 -115
  261. package/dist/util/executable-check.js.map +0 -1
@@ -2,8 +2,8 @@
2
2
  * createWorkflowRouter — framework-agnostic route definitions.
3
3
  *
4
4
  * Returns an array of `RouteDefinition` objects the host maps to their
5
- * HTTP framework. Each definition has: method, path, handler, and whether
6
- * it needs authentication.
5
+ * HTTP framework. Each definition has: method, path, handler, and its
6
+ * `access` class, which says who authenticates the caller.
7
7
  *
8
8
  * Usage with Express:
9
9
  * ```ts
@@ -14,7 +14,8 @@
14
14
  * const handler = async (req, res) => {
15
15
  * const result = await route.handler(
16
16
  * { params: req.params, query: req.query, body: req.body, headers: req.headers },
17
- * route.authenticated !== false ? await authenticate(req.headers) : null,
17
+ * // The signed-in user; null for `provider` routes.
18
+ * route.access === 'provider' ? null : await authenticate(req.headers),
18
19
  * );
19
20
  * res.status(result.status).json(result.body);
20
21
  * };
@@ -27,8 +28,9 @@ import { createChatAgentTools } from '../agents/builtin/chat-tools.js';
27
28
  import { buildWorkflowContext, buildExecutionDataFile, collectExecutionAttachments, EXECUTION_DATA_PATH } from './approval-context.js';
28
29
  import { OPERATOR_DESCRIPTORS } from '../types/operators.js';
29
30
  import { getAvailableContext } from '../engine/available-context.js';
31
+ import { executionTopic, workflowTopic } from '../events/types.js';
30
32
  import { isExecutable } from '../common/executable-check.js';
31
- import { createEventChannel } from '../runtime/execution-event-bus.js';
33
+ import { createEventChannel } from '../util/event-channel.js';
32
34
  import { AccessDeniedError, ResourceNotFoundError } from '../authz/default.js';
33
35
  import { applyResolvedCredentialAuth, CredentialNameSchema, CredentialAuthSchema, validateCredentialValues, } from '../common/credentials.js';
34
36
  // ─── Helpers ───
@@ -131,6 +133,54 @@ async function buildMultipartTriggerPayload(req, storage, scope, inputSchema) {
131
133
  function formatSSE(event, data) {
132
134
  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
133
135
  }
136
+ /**
137
+ * An SSE comment. Carries no event, so it never reaches a client handler — it
138
+ * exists purely so that something crosses the wire on an idle connection, before
139
+ * a proxy or load balancer decides the stream is dead and closes it.
140
+ */
141
+ const SSE_KEEPALIVE = ': keepalive\n\n';
142
+ /** How long an event stream may sit silent before it sends a keepalive. */
143
+ const KEEPALIVE_INTERVAL_MS = 25_000;
144
+ /**
145
+ * Marker pushed through an event channel to trigger a keepalive. A symbol so it
146
+ * can never be mistaken for — or collide with — a real event.
147
+ */
148
+ const KEEPALIVE = Symbol('keepalive');
149
+ /** Ceiling on topics per multiplexed connection. */
150
+ const MAX_STREAM_TOPICS = 100;
151
+ /**
152
+ * The writer's self-declared identity, read from `X-Workflow-Origin` and echoed
153
+ * back on the change event the mutation emits.
154
+ *
155
+ * A header rather than a body field: it describes the call, not the resource, so
156
+ * it works the same on JSON, multipart and empty-bodied routes without any of
157
+ * them having to make room for it in their schema.
158
+ *
159
+ * Untrusted by construction — a client may send whatever it likes. Nothing is
160
+ * authorized on it; it only ever lets a client recognise the echo of its own
161
+ * write. Truncated so a subscriber cannot be made to carry an unbounded string.
162
+ */
163
+ function readOrigin(req) {
164
+ const raw = req.headers['x-workflow-origin'];
165
+ const value = Array.isArray(raw) ? raw[0] : raw;
166
+ return value ? { origin: value.slice(0, 128) } : {};
167
+ }
168
+ /**
169
+ * Parse an id list from a query parameter, accepting both `?a=1,2` and `?a=1&a=2`
170
+ * so the route does not depend on how the host's framework folds repeats.
171
+ */
172
+ function parseIdList(raw) {
173
+ const parts = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];
174
+ const ids = new Set();
175
+ for (const part of parts) {
176
+ for (const id of part.split(',')) {
177
+ const trimmed = id.trim();
178
+ if (trimmed)
179
+ ids.add(trimmed);
180
+ }
181
+ }
182
+ return [...ids];
183
+ }
134
184
  // ─── Router Factory ───
135
185
  export function createWorkflowRouter(
136
186
  // `any` for services: the router never touches TServices, only forwards ctx.
@@ -139,9 +189,9 @@ runtime,
139
189
  // `authenticate` is invoked by the host glue, not the factory (see file header).
140
190
  _opts) {
141
191
  const routes = [];
142
- // Authenticated route handlers always receive a resolved ctx — the host runs
143
- // `authenticate` before dispatch. Unauthenticated routes never forward `auth`
144
- // to a guarded runtime call, so this narrowing is sound.
192
+ // `session` handlers always receive a resolved ctx — the host authenticates
193
+ // before dispatch. `provider` routes never forward `auth` to a guarded runtime
194
+ // call, so this narrowing is sound.
145
195
  const asCtx = (auth) => auth;
146
196
  // ─── Workflow CRUD ───
147
197
  routes.push({
@@ -200,7 +250,7 @@ _opts) {
200
250
  summary: typeof body['summary'] === 'string' ? body['summary'] : null,
201
251
  folderId: body['folderId'],
202
252
  attributes: body['attributes'],
203
- });
253
+ }, readOrigin(req));
204
254
  return created(workflow);
205
255
  }
206
256
  catch (err) {
@@ -233,7 +283,7 @@ _opts) {
233
283
  }
234
284
  if (typeof body['folderId'] === 'string')
235
285
  input.folderId = body['folderId'];
236
- const workflow = await runtime.updateWorkflow(asCtx(auth), id, input);
286
+ const workflow = await runtime.updateWorkflow(asCtx(auth), id, input, readOrigin(req));
237
287
  return ok(workflow);
238
288
  }
239
289
  catch (err) {
@@ -363,7 +413,7 @@ _opts) {
363
413
  if (folders.length === 0 && workflows.length === 0)
364
414
  return badRequest('Nothing to move');
365
415
  try {
366
- await runtime.moveBatch(asCtx(auth), { folders, workflows });
416
+ await runtime.moveBatch(asCtx(auth), { folders, workflows }, readOrigin(req));
367
417
  return ok({ moved: folders.length + workflows.length });
368
418
  }
369
419
  catch (err) {
@@ -395,7 +445,7 @@ _opts) {
395
445
  if (!id)
396
446
  return badRequest('Missing workflow ID');
397
447
  try {
398
- await runtime.activateWorkflow(asCtx(auth), id);
448
+ await runtime.activateWorkflow(asCtx(auth), id, readOrigin(req));
399
449
  return ok({ activated: true });
400
450
  }
401
451
  catch (err) {
@@ -413,7 +463,7 @@ _opts) {
413
463
  if (!id)
414
464
  return badRequest('Missing workflow ID');
415
465
  try {
416
- await runtime.deactivateWorkflow(asCtx(auth), id);
466
+ await runtime.deactivateWorkflow(asCtx(auth), id, readOrigin(req));
417
467
  return ok({ deactivated: true });
418
468
  }
419
469
  catch (err) {
@@ -449,9 +499,11 @@ _opts) {
449
499
  routes.push({
450
500
  method: 'POST',
451
501
  path: '/v2/workflows/:workflowId/trigger/v2',
452
- authenticated: false,
502
+ // No session. The SDK does not authenticate this caller; a host that exposes
503
+ // the route guards it, for example with an API key.
504
+ access: 'provider',
453
505
  multipart: 'any',
454
- handler: async (req, _auth) => {
506
+ handler: async (req) => {
455
507
  const workflowId = req.params['workflowId'];
456
508
  if (!workflowId)
457
509
  return badRequest('Missing workflow ID');
@@ -468,13 +520,15 @@ _opts) {
468
520
  }
469
521
  }
470
522
  try {
471
- const inputSchema = await runtime.getWorkflowTriggerInputSchemaPublic(workflowId, 'WEBHOOK_V2');
472
- const storageScope = await runtime.getWorkflowStorageScopePublic(workflowId, 'WEBHOOK_V2');
473
- const payload = await buildMultipartTriggerPayload(req, runtime.storage, storageScope, inputSchema);
474
- const executionId = await runtime.triggerWebhookV2Public(workflowId, payload);
523
+ const target = await runtime.getApiTriggerTarget(workflowId);
524
+ const payload = await buildMultipartTriggerPayload(req, runtime.storage, target.storageScope, target.inputSchema);
525
+ const executionId = await runtime.triggerViaApi(workflowId, payload);
475
526
  return created({ executionId });
476
527
  }
477
528
  catch (err) {
529
+ // An unknown workflow goes to the uniform 404 mapping at the end of this file.
530
+ if (err instanceof ResourceNotFoundError)
531
+ throw err;
478
532
  if (err instanceof Error && err.message.includes('not found')) {
479
533
  return notFound(err.message);
480
534
  }
@@ -707,34 +761,36 @@ _opts) {
707
761
  return ok({ topErrors });
708
762
  },
709
763
  });
710
- // ─── Webhooks (no authvalidated by secret in path) ───
764
+ // ─── Webhooks (no sessionthe trigger's own verify authenticates the sender) ───
711
765
  routes.push({
712
766
  method: 'POST',
713
- path: '/webhooks/:workflowId/:secret',
714
- authenticated: false,
715
- handler: async (req, _auth) => {
716
- const path = `/webhooks/${req.params['workflowId'] ?? ''}/${req.params['secret'] ?? ''}`;
717
- const result = await runtime.handleWebhook(path, {
718
- method: req.params['method'] ?? 'POST',
767
+ path: '/webhooks/:workflowId',
768
+ access: 'provider',
769
+ rawBody: true,
770
+ handler: async (req) => {
771
+ if (!req.rawBody) {
772
+ // A signature covers the bytes as sent, so a parsed body cannot be
773
+ // verified. Refuse rather than silently skip authentication.
774
+ return error(500, 'Webhook route requires the raw request body; the host must honour RouteDefinition.rawBody');
775
+ }
776
+ const { reply } = await runtime.handleWebhook(req.params['workflowId'] ?? '', {
777
+ method: 'POST',
719
778
  headers: req.headers,
720
- body: req.body,
721
779
  query: req.query,
780
+ rawBody: req.rawBody,
722
781
  });
723
- const response = {
724
- status: result.httpResponse.status,
725
- body: result.httpResponse.body,
782
+ return {
783
+ status: reply.status,
784
+ body: reply.body,
785
+ ...(reply.headers ? { headers: { ...reply.headers } } : {}),
726
786
  };
727
- if (result.httpResponse.headers) {
728
- response.headers = result.httpResponse.headers;
729
- }
730
- return response;
731
787
  },
732
788
  });
733
789
  // ─── Wait callbacks (no auth — validated by workflow secret in path) ───
734
790
  routes.push({
735
791
  method: 'POST',
736
792
  path: '/wait/callback/:secret/:executionId',
737
- authenticated: false,
793
+ access: 'provider',
738
794
  handler: async (req, _auth) => {
739
795
  const secret = req.params['secret'] ?? '';
740
796
  const executionId = req.params['executionId'] ?? '';
@@ -743,12 +799,19 @@ _opts) {
743
799
  },
744
800
  });
745
801
  // ─── Schema metadata (for UI) ───
802
+ routes.push({
803
+ method: 'GET',
804
+ path: '/schema/credentials',
805
+ handler: async () => {
806
+ return ok({ connectors: runtime.connectors.listCredentialTypes() });
807
+ },
808
+ });
746
809
  routes.push({
747
810
  method: 'GET',
748
811
  path: '/schema/triggers',
749
812
  handler: async () => {
750
813
  return ok({
751
- triggers: runtime.triggers.listSummaries(),
814
+ triggers: runtime.connectors.listTriggerSummaries(),
752
815
  });
753
816
  },
754
817
  });
@@ -757,7 +820,7 @@ _opts) {
757
820
  path: '/schema/steps',
758
821
  handler: async () => {
759
822
  return ok({
760
- steps: runtime.steps.listSummaries(),
823
+ steps: runtime.connectors.listStepSummaries(),
761
824
  });
762
825
  },
763
826
  });
@@ -769,10 +832,10 @@ _opts) {
769
832
  const type = req.params['type'];
770
833
  if (!type)
771
834
  return badRequest('Missing trigger type');
772
- if (!runtime.triggers.has(type)) {
835
+ if (!runtime.connectors.hasTrigger(type)) {
773
836
  return notFound(`Unknown trigger type "${type}"`);
774
837
  }
775
- return ok(runtime.triggers.getDescriptor(type));
838
+ return ok(runtime.connectors.getTriggerDescriptor(type));
776
839
  },
777
840
  });
778
841
  routes.push({
@@ -782,10 +845,10 @@ _opts) {
782
845
  const type = req.params['type'];
783
846
  if (!type)
784
847
  return badRequest('Missing step type');
785
- if (!runtime.steps.has(type)) {
848
+ if (!runtime.connectors.hasStep(type)) {
786
849
  return notFound(`Unknown step type "${type}"`);
787
850
  }
788
- return ok(runtime.steps.getDescriptor(type));
851
+ return ok(runtime.connectors.getStepDescriptor(type));
789
852
  },
790
853
  });
791
854
  routes.push({
@@ -806,7 +869,7 @@ _opts) {
806
869
  }
807
870
  const config = body['config'];
808
871
  const atStepId = body['atStepId'];
809
- const context = getAvailableContext(config, atStepId, runtime.steps, runtime.triggers);
872
+ const context = getAvailableContext(config, atStepId, runtime.connectors);
810
873
  return ok({ context });
811
874
  },
812
875
  });
@@ -909,8 +972,9 @@ _opts) {
909
972
  },
910
973
  });
911
974
  // ── POST /credentials ──
912
- // Create a new credential. Strictly validates name and the discriminated
913
- // authType/values pair.
975
+ // Create a new credential of a connector-declared type. The shape comes from
976
+ // the type's definition, never from the client, and values are validated
977
+ // strictly against it.
914
978
  routes.push({
915
979
  method: 'POST',
916
980
  path: '/credentials',
@@ -929,15 +993,23 @@ _opts) {
929
993
  if (!nameResult.success) {
930
994
  return badRequest('Invalid or missing "name"');
931
995
  }
996
+ const credType = body['credType'];
997
+ const definition = typeof credType === 'string'
998
+ ? runtime.connectors.findCredentialDefinition(credType)
999
+ : undefined;
1000
+ if (!definition) {
1001
+ return badRequest('Invalid or missing "credType"');
1002
+ }
932
1003
  const credentialAuthResult = CredentialAuthSchema.safeParse({
933
- authType: body['authType'],
1004
+ authType: definition.authType,
934
1005
  values: body['values'],
935
1006
  });
936
1007
  if (!credentialAuthResult.success) {
937
- return badRequest('Invalid or mismatched "authType" and "values"');
1008
+ return badRequest(`Invalid "values" for credential type "${definition.credType}"`);
938
1009
  }
939
1010
  const input = {
940
1011
  name: nameResult.data,
1012
+ credType: definition.credType,
941
1013
  ...credentialAuthResult.data,
942
1014
  };
943
1015
  try {
@@ -1113,6 +1185,99 @@ _opts) {
1113
1185
  }
1114
1186
  },
1115
1187
  });
1188
+ // ─── SSE: Multiplexed Change Stream ───
1189
+ //
1190
+ // One connection, many topics: `GET /events?workflows=a,b`.
1191
+ //
1192
+ // The topic set lives in the URL and nowhere else, which is what keeps this
1193
+ // working behind a load balancer with no session affinity. There is no
1194
+ // server-side subscription to mutate, so a client that wants to watch something
1195
+ // new simply reconnects with a different query string — and any pod can serve
1196
+ // it, because every pod subscribes to whatever its own clients asked for.
1197
+ //
1198
+ // Frames are notifications, not payloads: `workflow_changed` says *that* a
1199
+ // workflow changed, and the client re-reads through the ordinary authorized
1200
+ // GET. Only `workflows` is accepted today — a live execution has its own richer
1201
+ // stream at `/executions/:execId/stream`, which patches step state in place
1202
+ // rather than re-reading, and duplicating it here would give it two meanings.
1203
+ // The query shape is keyed by entity so further families slot in unchanged.
1204
+ routes.push({
1205
+ method: 'GET',
1206
+ path: '/events',
1207
+ handler: async (req, auth) => {
1208
+ const eventBus = runtime.eventBus;
1209
+ if (!eventBus) {
1210
+ return error(501, 'Event streaming not configured');
1211
+ }
1212
+ const requested = parseIdList(req.query['workflows']);
1213
+ if (requested.length === 0) {
1214
+ return badRequest('Specify at least one id, e.g. ?workflows=a,b');
1215
+ }
1216
+ if (requested.length > MAX_STREAM_TOPICS) {
1217
+ return badRequest(`Too many topics (${String(requested.length)}); the limit is ${String(MAX_STREAM_TOPICS)}`);
1218
+ }
1219
+ // Authorize every id up front, through the same read the client would do.
1220
+ // A bus is addressed by resource id and knows nothing about principals, so
1221
+ // this is the only place the caller's access is actually decided.
1222
+ //
1223
+ // One unreadable id does not fail the connection: on a multiplexed stream
1224
+ // that would take down every other topic the client is watching, and access
1225
+ // can be lost mid-session. The `ready` frame reports what was accepted, so
1226
+ // the client is never left believing it is watching something it is not.
1227
+ const subscribed = [];
1228
+ const rejected = [];
1229
+ await Promise.all(requested.map(async (id) => {
1230
+ try {
1231
+ await runtime.getWorkflow(asCtx(auth), id);
1232
+ subscribed.push(id);
1233
+ }
1234
+ catch {
1235
+ rejected.push(id);
1236
+ }
1237
+ }));
1238
+ const channel = createEventChannel();
1239
+ const results = await Promise.allSettled(subscribed.map((id) => eventBus.subscribe(workflowTopic(id), channel.push)));
1240
+ const unsubs = results.flatMap((r) => r.status === 'fulfilled' ? [r.value] : []);
1241
+ if (unsubs.length !== results.length) {
1242
+ // A partially-subscribed stream is worse than none: it looks live and is
1243
+ // silently blind to some of its topics. Tear it down and let the client
1244
+ // reconnect.
1245
+ for (const unsub of unsubs)
1246
+ unsub();
1247
+ channel.end();
1248
+ return error(503, 'Could not open the event stream');
1249
+ }
1250
+ const keepalive = setInterval(() => {
1251
+ channel.push(KEEPALIVE);
1252
+ }, KEEPALIVE_INTERVAL_MS);
1253
+ async function* generateSSE() {
1254
+ try {
1255
+ // Sent before anything else so the client knows the subscription is
1256
+ // live and which topics it actually got.
1257
+ yield formatSSE('ready', { subscribed, rejected });
1258
+ for await (const frame of channel) {
1259
+ yield frame === KEEPALIVE ? SSE_KEEPALIVE : formatSSE(frame.event, frame.data);
1260
+ }
1261
+ }
1262
+ finally {
1263
+ clearInterval(keepalive);
1264
+ for (const unsub of unsubs)
1265
+ unsub();
1266
+ channel.end();
1267
+ }
1268
+ }
1269
+ return {
1270
+ status: 200,
1271
+ body: null,
1272
+ headers: {
1273
+ 'Content-Type': 'text/event-stream',
1274
+ 'Cache-Control': 'no-cache',
1275
+ 'Connection': 'keep-alive',
1276
+ },
1277
+ stream: generateSSE(),
1278
+ };
1279
+ },
1280
+ });
1116
1281
  // ─── SSE: Execution Event Stream ───
1117
1282
  routes.push({
1118
1283
  method: 'GET',
@@ -1125,27 +1290,51 @@ _opts) {
1125
1290
  if (!eventBus) {
1126
1291
  return error(501, 'Event streaming not configured');
1127
1292
  }
1128
- const detail = await runtime.getExecutionDetail(asCtx(auth), execId);
1129
- if (!detail)
1293
+ // Subscribe BEFORE reading the snapshot, not after.
1294
+ //
1295
+ // Anything published in between is then delivered twice — once folded into
1296
+ // the snapshot, once live — which costs a redundant repaint, because every
1297
+ // execution event is idempotent. A miss is not recoverable: drop
1298
+ // `execution_completed` and the client sits on an open connection waiting
1299
+ // for a frame that has already been and gone. Subscribing second is a race
1300
+ // even in-process; against a distributed bus, where subscribe is a network
1301
+ // round trip, it is a race you lose regularly.
1302
+ //
1303
+ // Ordering it this way subscribes fractionally before the caller's access is
1304
+ // checked. Nothing is delivered in that window: every failure path below
1305
+ // tears the subscription down and drops the channel unread, and the first
1306
+ // frame is only ever yielded after `getExecutionDetail` has authorized it.
1307
+ const channel = createEventChannel();
1308
+ const unsub = await eventBus.subscribe(executionTopic(execId), channel.push);
1309
+ const found = await runtime
1310
+ .getExecutionDetail(asCtx(auth), execId)
1311
+ .catch((err) => {
1312
+ unsub();
1313
+ throw err;
1314
+ });
1315
+ if (!found) {
1316
+ unsub();
1130
1317
  return notFound(`Execution ${execId} not found`);
1318
+ }
1319
+ // Re-bound after the guard: `generateSSE` is a hoisted declaration, so TS
1320
+ // will not carry the null-narrowing of `found` into it.
1321
+ const detail = found;
1131
1322
  const isTerminal = ['COMPLETED', 'FAILED', 'CANCELLED'].includes(detail.execution.status);
1132
1323
  async function* generateSSE() {
1133
- // 1. Send initial snapshot
1134
- yield formatSSE('snapshot', {
1135
- ...detail.execution,
1136
- steps: detail.steps,
1137
- config: detail.config,
1138
- triggerData: detail.triggerData,
1139
- summary: detail.summary,
1140
- name: detail.name,
1141
- });
1142
- // 2. If already terminal, close immediately
1143
- if (isTerminal)
1144
- return;
1145
- // 3. Subscribe to live events
1146
- const channel = createEventChannel();
1147
- const unsub = eventBus.subscribe(execId, channel.push);
1148
1324
  try {
1325
+ // 1. Send the initial snapshot (buffered events wait their turn).
1326
+ yield formatSSE('snapshot', {
1327
+ ...detail.execution,
1328
+ steps: detail.steps,
1329
+ config: detail.config,
1330
+ triggerData: detail.triggerData,
1331
+ summary: detail.summary,
1332
+ name: detail.name,
1333
+ });
1334
+ // 2. If already terminal, close immediately.
1335
+ if (isTerminal)
1336
+ return;
1337
+ // 3. Drain live events.
1149
1338
  for await (const event of channel) {
1150
1339
  yield formatSSE(event.event, event.data);
1151
1340
  // Close stream on terminal events
@@ -1157,7 +1346,10 @@ _opts) {
1157
1346
  }
1158
1347
  }
1159
1348
  finally {
1349
+ // Runs however the generator ends — terminal event, client disconnect,
1350
+ // or a throw mid-yield.
1160
1351
  unsub();
1352
+ channel.end();
1161
1353
  }
1162
1354
  }
1163
1355
  return {
@@ -1255,7 +1447,7 @@ _opts) {
1255
1447
  routes.push({
1256
1448
  method: 'POST',
1257
1449
  path: '/ai-builder/chat',
1258
- authenticated: false,
1450
+ access: 'session',
1259
1451
  handler: async (req, auth) => {
1260
1452
  const body = req.body;
1261
1453
  if (!body.messages || !Array.isArray(body.messages)) {
@@ -1453,6 +1645,9 @@ _opts) {
1453
1645
  // than a 500 — and so a new route cannot forget to do it.
1454
1646
  return routes.map((route) => ({
1455
1647
  ...route,
1648
+ // A route that does not declare its access needs a signed-in user, so
1649
+ // forgetting to classify one never mounts it open.
1650
+ access: route.access ?? 'session',
1456
1651
  handler: async (req, auth) => {
1457
1652
  try {
1458
1653
  return await route.handler(req, auth);