@classytic/arc 2.41.1 → 2.43.0

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 (230) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
  6. package/dist/actionPermissions-BotX1YOa.mjs +776 -0
  7. package/dist/audit/index.d.mts +43 -5
  8. package/dist/audit/index.mjs +48 -26
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +43 -13
  11. package/dist/auth/index.mjs +130 -155
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
  14. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  15. package/dist/boundedMap-BULzfXYU.mjs +57 -0
  16. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
  17. package/dist/cache/index.d.mts +22 -34
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DbCBxlgw.mjs → caching-Cc2M1DwO.mjs} +3 -3
  20. package/dist/claimed-B10RTfkM.mjs +8 -0
  21. package/dist/cleanup/index.d.mts +6 -10
  22. package/dist/cleanup/index.mjs +27 -73
  23. package/dist/cli/commands/describe.d.mts +4 -2
  24. package/dist/cli/commands/describe.mjs +7 -4
  25. package/dist/cli/commands/docs.mjs +3 -3
  26. package/dist/cli/commands/doctor.mjs +1 -1
  27. package/dist/cli/commands/generate.mjs +2 -2
  28. package/dist/cli/commands/init.mjs +2 -2
  29. package/dist/cli/commands/introspect.mjs +2 -2
  30. package/dist/cli/index.mjs +3 -3
  31. package/dist/{constants-D5Ubn1fe.mjs → constants-Cu7Mx7_U.mjs} +4 -2
  32. package/dist/context/index.d.mts +2 -5
  33. package/dist/context/index.mjs +18 -38
  34. package/dist/core/index.d.mts +3 -3
  35. package/dist/core/index.mjs +8 -8
  36. package/dist/{core-5lazhva9.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-f3boenVZ.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
  43. package/dist/discovery/index.mjs +1 -1
  44. package/dist/docs/index.d.mts +10 -4
  45. package/dist/docs/index.mjs +4 -4
  46. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  47. package/dist/{elevation-CkvG0LG2.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-BH7YU_9O.mjs → errors-CtrywMHP.mjs} +58 -23
  51. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  52. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  53. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
  55. package/dist/events/index.d.mts +33 -8
  56. package/dist/events/index.mjs +49 -53
  57. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  58. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  59. package/dist/events/transports/redis.d.mts +1 -1
  60. package/dist/events/transports/redis.mjs +2 -2
  61. package/dist/factory/index.d.mts +32 -17
  62. package/dist/factory/index.mjs +213 -32
  63. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
  64. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  65. package/dist/{fields-BHrkoF5y.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-BLRf2m3y.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-BIXWKrzQ.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-ZRHeDHXH.mjs → health-DqbsG-_w.mjs} +115 -55
  71. package/dist/hooks/index.d.mts +2 -2
  72. package/dist/hooks/index.mjs +2 -2
  73. package/dist/idempotency/index.d.mts +31 -7
  74. package/dist/idempotency/index.mjs +155 -98
  75. package/dist/idempotency/redis.d.mts +1 -1
  76. package/dist/idempotency/redis.mjs +48 -11
  77. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  78. package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
  79. package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
  80. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  81. package/dist/index-JdbZAIsm.d.mts +510 -0
  82. package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
  83. package/dist/index.d.mts +8 -8
  84. package/dist/index.mjs +11 -11
  85. package/dist/{init-CpLkjU18.mjs → init-B_oUYDXI.mjs} +1 -1
  86. package/dist/integrations/event-gateway.d.mts +8 -3
  87. package/dist/integrations/event-gateway.mjs +4 -4
  88. package/dist/integrations/index.d.mts +3 -3
  89. package/dist/integrations/index.mjs +1 -1
  90. package/dist/integrations/jobs/index.d.mts +1 -1
  91. package/dist/integrations/jobs/index.mjs +18 -5
  92. package/dist/integrations/mcp/index.d.mts +72 -59
  93. package/dist/integrations/mcp/index.mjs +19 -4
  94. package/dist/integrations/mcp/testing.d.mts +2 -2
  95. package/dist/integrations/mcp/testing.mjs +2 -2
  96. package/dist/integrations/streamline.d.mts +78 -136
  97. package/dist/integrations/streamline.mjs +116 -54
  98. package/dist/integrations/webhooks.d.mts +60 -120
  99. package/dist/integrations/webhooks.mjs +36 -80
  100. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  101. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  102. package/dist/integrations/websocket-redis.d.mts +1 -1
  103. package/dist/integrations/websocket-redis.mjs +1 -1
  104. package/dist/integrations/websocket.d.mts +3 -3
  105. package/dist/integrations/websocket.mjs +2 -2
  106. package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
  107. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-cpZCtcIE.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
  111. package/dist/logger/index.d.mts +1 -8
  112. package/dist/logger/index.mjs +3 -153
  113. package/dist/logger-DXjfIqjV.mjs +160 -0
  114. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-Iotz0cCx.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-NMsmpRmY.mjs → metrics-CWI11eRu.mjs} +39 -72
  117. package/dist/middleware/index.d.mts +2 -2
  118. package/dist/middleware/index.mjs +3 -3
  119. package/dist/migrations/index.mjs +2 -2
  120. package/dist/{module-DpLJG2_N.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-CmFf3yq_.mjs → openapi-DT_G4VhY.mjs} +48 -10
  124. package/dist/outbox-admin/index.d.mts +3 -3
  125. package/dist/outbox-admin/index.mjs +6 -18
  126. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
  127. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  128. package/dist/permissions/index.d.mts +5 -4
  129. package/dist/permissions/index.mjs +8 -6
  130. package/dist/{permissions-D8TBH44E.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-FMWNheSO.mjs → pipe-BqeGT6UO.mjs} +2 -2
  132. package/dist/pipeline/index.d.mts +2 -2
  133. package/dist/pipeline/index.mjs +2 -2
  134. package/dist/plan-FVR6LP59.mjs +565 -0
  135. package/dist/{BaseController-CYEwjyjN.mjs → plugin-BccET1tL.mjs} +1256 -345
  136. package/dist/plugins/index.d.mts +14 -11
  137. package/dist/plugins/index.mjs +44 -22
  138. package/dist/plugins/response-cache.mjs +8 -14
  139. package/dist/plugins/tracing-entry.mjs +2 -2
  140. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-CVIOcgMF.mjs} +1 -1
  141. package/dist/presets/filesUpload.d.mts +2 -2
  142. package/dist/presets/filesUpload.mjs +5 -5
  143. package/dist/presets/index.d.mts +26 -31
  144. package/dist/presets/index.mjs +4 -4
  145. package/dist/presets/multiTenant.d.mts +2 -107
  146. package/dist/presets/multiTenant.mjs +22 -162
  147. package/dist/presets/search.d.mts +2 -2
  148. package/dist/presets/search.mjs +2 -2
  149. package/dist/{presets-ZJSPgmT2.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-BzIgzoKZ.mjs → projection-Ce4aQhQk.mjs} +2 -2
  151. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  152. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  153. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-8tIQWZz4.mjs → realtime-DmPE9S54.mjs} +25 -21
  156. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  157. package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
  158. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  159. package/dist/registry/index.d.mts +9 -4
  160. package/dist/registry/index.mjs +4 -4
  161. package/dist/{registry-DNkgGQcK.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
  165. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  166. package/dist/requirement-KDdoRcOP.mjs +35 -0
  167. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  168. package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
  169. package/dist/{rolldown-runtime-eysyzFAF.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-j6uXXAeJ.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
  175. package/dist/schemas/index.d.mts +10 -0
  176. package/dist/schemas/index.mjs +8 -3
  177. package/dist/scim/index.mjs +2 -2
  178. package/dist/scope/index.d.mts +2 -2
  179. package/dist/scope/index.mjs +6 -6
  180. package/dist/{scope-BUi1xV1I.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-BeqVKQKu.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-IcRFjWQU.mjs → streaming-DjTQhZrN.mjs} +1 -1
  187. package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
  188. package/dist/tenancy--ed9eH_z.mjs +43 -0
  189. package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
  190. package/dist/testing/cleanupStoreContract.mjs +1 -1
  191. package/dist/testing/index.d.mts +193 -175
  192. package/dist/testing/index.mjs +286 -212
  193. package/dist/testing/outboxStoreContract.mjs +137 -2
  194. package/dist/testing/requiredPorts.d.mts +17 -0
  195. package/dist/testing/requiredPorts.mjs +65 -0
  196. package/dist/testing/storageContract.mjs +1 -1
  197. package/dist/timing-qczZ0YC-.mjs +46 -0
  198. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-CySF12G-.mjs} +1 -1
  200. package/dist/types/index.d.mts +4 -4
  201. package/dist/types/index.mjs +1 -1
  202. package/dist/types/storage.mjs +1 -1
  203. package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
  204. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  205. package/dist/{types-CgenB1gv.mjs → types-Bk9NOgOC.mjs} +4 -4
  206. package/dist/usage/index.d.mts +2 -3
  207. package/dist/usage/index.mjs +5 -5
  208. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-aklawRaK.mjs} +1 -1
  209. package/dist/utils/index.d.mts +84 -223
  210. package/dist/utils/index.mjs +13 -12
  211. package/dist/{utils-CxcctrAh.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-DrYWnOzI.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DKkT4hNJ.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-CxKqoIFn.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +16 -12
  218. package/skills/arc/SKILL.md +13 -4
  219. package/skills/arc/references/auth.md +8 -5
  220. package/skills/arc/references/integrations.md +1 -1
  221. package/skills/arc/references/mcp.md +8 -6
  222. package/skills/arc/references/multi-tenancy.md +19 -14
  223. package/skills/arc/references/production.md +27 -10
  224. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-YzIbF7Ii.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. package/dist/sync/changelog.mjs +0 -3
@@ -2,13 +2,9 @@
2
2
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
3
3
  //#region src/integrations/streamline.d.ts
4
4
  /**
5
- * Start options — matches @classytic/streamline v2.3+ `StartOptions`.
6
- *
7
- * v2.3 additions:
8
- * - `tenantId` — required when streamline's `multiTenant.strict: true`.
9
- * Hosts should NOT accept this from the request body in untrusted
10
- * contexts; use `tenantResolver` to extract from auth context instead.
11
- * - `bypassTenant` — admin/cross-tenant operations. Same caveat.
5
+ * Start options — matches streamline's `StartOptions`. `tenantId` /
6
+ * `bypassTenant` must come from auth context (`tenantResolver`), never the
7
+ * request body.
12
8
  */
13
9
  interface WorkflowStartOptions {
14
10
  meta?: Record<string, unknown>;
@@ -40,6 +36,13 @@ interface WorkflowLike {
40
36
  timeout?: number;
41
37
  }): Promise<WorkflowRunLike>;
42
38
  shutdown?(): void;
39
+ /**
40
+ * Read for `compensationHandlers` only — whether a failed run will roll
41
+ * back, which decides if `workflow:failed` ends its stream. Absent = no saga.
42
+ */
43
+ options?: {
44
+ compensationHandlers?: unknown;
45
+ };
43
46
  };
44
47
  start(input: unknown, options?: WorkflowStartOptions): Promise<WorkflowRunLike>;
45
48
  resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;
@@ -48,7 +51,7 @@ interface WorkflowLike {
48
51
  }): Promise<WorkflowRunLike>;
49
52
  get(runId: string): Promise<WorkflowRunLike | null>;
50
53
  shutdown?(): void;
51
- /** Streamline container for event bridging + repository access (streamline >=2.1) */
54
+ /** Streamline container for event bridging + repository access. */
52
55
  container?: {
53
56
  eventBus: {
54
57
  on(event: string, listener: (...args: unknown[]) => void): void;
@@ -58,38 +61,20 @@ interface WorkflowLike {
58
61
  repository?: {
59
62
  getAll(params: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
60
63
  /**
61
- * Tenant-scoped lookup by id. Used by the DELETE handler for a
62
- * defense-in-depth pre-flight: streamline 2.3.3's `wf.get(runId)` /
63
- * `engine.get` does NOT accept tenant options, so a cross-tenant
64
- * runId can leak data through the engine path. Going through the
65
- * repository here means mongokit's tenant-filter plugin scopes the
66
- * read — cross-tenant requests get a clean 404 and DELETEs only
67
- * touch rows the caller actually owns.
64
+ * Tenant-scoped lookup — the per-run ownership pre-flight. The engine's
65
+ * `get` takes no tenant options, so the tenant-filtered repository read
66
+ * is what stops cross-tenant access (clean 404).
68
67
  */
69
68
  getById?(id: string, options?: Record<string, unknown>): Promise<WorkflowRunLike | null>;
70
- /**
71
- * Hard-delete a run by id. Routed through mongokit's inherited
72
- * `Repository.delete()` so multi-tenant scope + audit/cache plugins
73
- * fire. Wired into `DELETE /:workflowId/runs/:runId` — operator
74
- * escape hatch for dead-lettered or stuck rows.
75
- */
69
+ /** Tenant-scoped hard delete, backing `DELETE /:workflowId/runs/:runId`. */
76
70
  delete?(id: string, options?: Record<string, unknown>): Promise<unknown>;
77
71
  };
78
72
  /**
79
- * Streamline >= 2.3.2 — explicit deploy-time index sync (TTL on
80
- * terminal runs + tenant compounds). When the host configured
81
- * `createContainer({ retention })`, arc's app-level deploy hook
82
- * should call `await container.syncRetentionIndexes()` after
83
- * `mongoose.connect`. Optional so older streamline versions
84
- * (and partial mocks) still satisfy the structural shape.
73
+ * Deploy-time index sync (retention TTL + tenant compounds). Hosts using
74
+ * `createContainer({ retention })` call it after connecting.
85
75
  */
86
76
  syncRetentionIndexes?: () => Promise<void>;
87
- /**
88
- * Streamline >= 2.3.2 — stop background sweepers and release timers.
89
- * Arc's `onClose` hook below calls this on every workflow's container
90
- * during graceful shutdown so SIGTERM doesn't leave the stale-run
91
- * sweeper running. Optional + idempotent.
92
- */
77
+ /** Stop background sweepers; called on `onClose`. Idempotent. */
93
78
  dispose?: () => void;
94
79
  };
95
80
  }
@@ -107,36 +92,19 @@ interface WorkflowRunLike {
107
92
  stepLogs?: unknown[];
108
93
  createdAt?: Date;
109
94
  updatedAt?: Date;
110
- /**
111
- * Streamline >= 2.3.3 — pinned definition version (semver) the run
112
- * started under. Hosts surfacing a "stuck on old version" UI read this
113
- * to decide whether to nudge a migration. Optional for back-compat
114
- * with runs created before 2.3.3.
115
- */
95
+ /** Definition version (semver) the run started under. */
116
96
  definitionVersion?: string;
117
- /**
118
- * Streamline >= 2.3.3 — count of stale-recovery / sweeper transitions
119
- * applied to this run. Sweeper dead-letters once this hits
120
- * `RetentionOptions.maxStaleRecoveries`; UIs can highlight runs trending
121
- * toward dead-letter.
122
- */
97
+ /** Stale-recovery transitions so far; dead-lettered at `maxStaleRecoveries`. */
123
98
  recoveryAttempts?: number;
124
- /**
125
- * Streamline >= 2.7 — operator-supplied cancellation reason, set when
126
- * `cancel(runId, { reason })` was called. Absent on runs cancelled without
127
- * a reason or under older streamline. Surfaced so dashboards can show WHY.
128
- */
99
+ /** Reason passed to `cancel(runId, { reason })`, if any. */
129
100
  cancellationReason?: string;
130
101
  }
131
102
  /**
132
- * Streamline >= 2.3.3 dead-letter discriminator. The run.status stays
133
- * `'failed'`; the discrimination is `error.code`:
103
+ * Dead-letter discriminator: `run.status` stays `'failed'`; `error.code` says why:
134
104
  * - `'stale_heartbeat'` — sweeper terminated; transient crash signal.
135
105
  * - `'dead_lettered'` — exceeded `maxStaleRecoveries`; permanent.
136
106
  * - `'VERSION_MISMATCH'` — engine deployed a step graph the run can't
137
107
  * resume against; admin must rewind / migrate / cancel.
138
- *
139
- * Hosts switch on `error.code` for dashboards / alerting.
140
108
  */
141
109
  declare const STREAMLINE_FAILURE_CODES: {
142
110
  readonly STALE_HEARTBEAT: "stale_heartbeat";
@@ -153,65 +121,35 @@ interface StreamlinePluginOptions {
153
121
  /** Connect workflow lifecycle events to Arc's event bus (default: true) */
154
122
  bridgeEvents?: boolean;
155
123
  /**
156
- * Bridge the workflow's internal event bus (step + workflow lifecycle +
157
- * engine telemetry) to Arc's event bus, topic-scoped as
158
- * `workflow.${workflowId}.${eventName}`.
159
- *
160
- * Covers the full streamline 2.3 event surface:
161
- * - Step events: started, completed, failed, waiting, skipped,
162
- * retry-scheduled, compensated
163
- * - Workflow lifecycle: started, completed, failed, waiting, resumed,
164
- * cancelled, recovered, retry, compensating, paused (>= 2.7)
165
- * - Engine telemetry: engine:error, scheduler:error,
166
- * scheduler:circuit-open
167
- *
168
- * Subscriptions use structural `container.eventBus.on(...)` — future
169
- * streamline releases can add events without breaking arc; missing
170
- * events are simply never handled (no crash). Requires the workflow
171
- * to expose `container.eventBus`.
172
- *
173
- * Disabled by default — enable for dashboards or monitoring.
124
+ * Bridge every {@link STREAMLINE_BUS_EVENTS} event (step, workflow
125
+ * lifecycle, engine telemetry) from `container.eventBus` to Arc's event bus
126
+ * as `workflow.${workflowId}.${eventName}`.
174
127
  * @default false
175
128
  */
176
129
  bridgeBusEvents?: boolean;
177
130
  /**
178
131
  * Enable SSE streaming endpoint: GET /:workflowId/runs/:runId/stream
179
132
  * Streams step-level + lifecycle events as Server-Sent Events for live
180
- * UI updates. Auto-closes the stream on terminal workflow events
181
- * (completed / failed / cancelled).
133
+ * UI updates. Auto-closes the stream when the run ends: completed /
134
+ * cancelled / compensated / compensation_failed, and failed only for a
135
+ * workflow with no compensation (a saga keeps streaming its rollback).
182
136
  * @default false
183
137
  */
184
138
  enableStreaming?: boolean;
185
139
  /**
186
- * Enable webhook resume endpoint: POST /hooks/:token
187
- *
188
- * Routes incoming webhook calls through streamline's `resumeHook(token,
189
- * body)` — which validates the token against the stored `hookToken` on
190
- * the waiting step (fail-closed since streamline 2.3). Hosts use this
191
- * for "wait for external approval / SaaS callback" patterns.
192
- *
193
- * Workflows MUST pass `{ hookToken: hook.token }` to `ctx.wait(...)` —
194
- * streamline 2.3 rejects resume otherwise (security). The endpoint is
195
- * registered at the plugin's `prefix` root, NOT scoped per workflow,
196
- * because the token encodes the runId.
197
- *
198
- * Auth is OPTIONAL on this route by design — the token IS the
199
- * authentication. If you also want to gate by user (e.g. only the
200
- * inviting user can approve), set `auth: true` and a permission check.
140
+ * Enable `POST /hooks/:token` (at the prefix root — the token encodes the
141
+ * runId), routed through streamline's `resumeHook(token, body)`, which
142
+ * validates against the waiting step's stored `hookToken` (fail-closed).
143
+ * Workflows MUST pass `{ hookToken: hook.token }` to `ctx.wait(...)`.
201
144
  *
145
+ * The token IS the authentication; `auth` adds a user gate on top.
202
146
  * @default false
203
147
  */
204
148
  enableHookEndpoint?: boolean;
205
149
  /**
206
- * Resolve the tenant id for a request — extract from auth context
207
- * (JWT claim, session, header), NOT from the request body. Returning
208
- * `undefined` skips tenant injection (use for non-multi-tenant routes
209
- * or admin paths that pass `bypassTenant` explicitly).
210
- *
211
- * When set, the resolved tenantId is forwarded to every streamline
212
- * call (`start`, `resume`, `cancel`, `get`, `list`, etc.) so
213
- * streamline's `multiTenant.strict` mode never throws "missing
214
- * tenantId" inside arc's request lifecycle.
150
+ * Resolve the request's tenant id from auth context — NEVER the request
151
+ * body. Forwarded to streamline calls, and enables the per-run tenant
152
+ * ownership check. `undefined` skips tenant scoping.
215
153
  *
216
154
  * @example
217
155
  * tenantResolver: (req) => req.user?.organizationId
@@ -224,63 +162,67 @@ interface StreamlinePluginOptions {
224
162
  * (`allowBypass: true`, the default).
225
163
  */
226
164
  bypassTenantResolver?: (request: FastifyRequest) => boolean;
227
- /** Custom permission check for workflow operations */
165
+ /**
166
+ * Custom permission check for workflow operations. `execute` falls back to
167
+ * `start`, and `pause` / `rewind` to `cancel`, when not set — so restricting
168
+ * the broader operation also covers these.
169
+ */
228
170
  permissions?: {
229
171
  start?: (request: unknown) => boolean | Promise<boolean>;
230
172
  resume?: (request: unknown) => boolean | Promise<boolean>;
231
173
  cancel?: (request: unknown) => boolean | Promise<boolean>;
232
174
  list?: (request: unknown) => boolean | Promise<boolean>;
233
175
  get?: (request: unknown) => boolean | Promise<boolean>;
176
+ execute?: (request: unknown) => boolean | Promise<boolean>;
177
+ pause?: (request: unknown) => boolean | Promise<boolean>;
178
+ rewind?: (request: unknown) => boolean | Promise<boolean>;
179
+ };
180
+ /**
181
+ * Operations whose call requires an idempotency key (400 `arc.idempotency_key_required`
182
+ * when absent; a keyed retry replays). `workflows` narrows it to those ids (default: all).
183
+ * On `start` the header key IS the run's `idempotencyKey`; a different body key is refused.
184
+ * Needs `idempotencyPlugin` enabled.
185
+ */
186
+ idempotency?: {
187
+ readonly required: readonly ("start" | "execute" | "resume" | "rewind")[];
188
+ readonly workflows?: readonly string[];
234
189
  };
235
190
  }
236
191
  /**
237
- * Full event list published on a streamline workflow's internal `eventBus`
238
- * (tracks streamline 2.3's `EventPayloadMap` in
239
- * `@classytic/streamline/src/core/events.ts`).
240
- *
241
- * Hardcoded here by design — arc subscribes via structural
242
- * `eventBus.on(name, handler)`, which is a no-op for events the running
243
- * streamline version doesn't emit. New events a future streamline release
244
- * adds can be bridged by updating this list; arc never breaks just
245
- * because streamline extended its bus.
192
+ * Events published on a streamline workflow's internal `eventBus`. Subscribing
193
+ * to one the running version doesn't emit is a no-op. Pinned against
194
+ * streamline's `ALL_WORKFLOW_EVENT_NAMES` by `tests/integrations/streamline-drift.test.ts`.
246
195
  */
247
- declare const STREAMLINE_BUS_EVENTS: readonly ["step:started", "step:completed", "step:failed", "step:waiting", "step:skipped", "step:retry-scheduled", "step:compensated", "workflow:started", "workflow:completed", "workflow:failed", "workflow:waiting", "workflow:resumed", "workflow:cancelled", "workflow:recovered", "workflow:retry", "workflow:compensating", "workflow:paused", "engine:error", "scheduler:error", "scheduler:circuit-open"];
196
+ declare const STREAMLINE_BUS_EVENTS: readonly ["step:started", "step:completed", "step:failed", "step:waiting", "step:skipped", "step:retry-scheduled", "step:compensated", "workflow:started", "workflow:completed", "workflow:failed", "workflow:waiting", "workflow:resumed", "workflow:cancelled", "workflow:recovered", "workflow:retry", "workflow:compensating", "workflow:paused", "workflow:compensated", "workflow:compensation_failed", "engine:error", "scheduler:error", "scheduler:circuit-open"];
248
197
  /**
249
- * Non-durable streaming frames from streamline >= 2.6's `ctx.stream(frame)`
250
- * (LLM tokens, percent-complete, live previews).
251
- *
252
- * Delivered on the SSE endpoint ONLY — deliberately excluded from
253
- * `STREAMLINE_BUS_EVENTS` / `bridgeBusEvents` because frames are
254
- * high-frequency, at-most-once UI traffic, not domain events: republishing
255
- * every token onto arc's Redis/Kafka transport would flood it. Subscribers
256
- * that genuinely want frames on the transport can bridge
257
- * `streamline:step.stream` from streamline's own arc-shape transport.
258
- *
259
- * On streamline < 2.6 the subscription is a structural no-op (the bus never
260
- * emits the event) — no version gate needed.
198
+ * Non-durable `ctx.stream(frame)` frames (LLM tokens, progress). SSE ONLY —
199
+ * excluded from `bridgeBusEvents` because high-frequency at-most-once UI
200
+ * traffic would flood arc's event transport.
261
201
  */
262
202
  declare const STREAMLINE_STREAM_EVENTS: readonly ["step:stream"];
263
203
  /**
264
- * Workflow events that should auto-close an SSE stream when observed.
265
- * Recovered / waiting / resumed / retry / compensating are NOT terminal —
266
- * the run is still active after them.
204
+ * Workflow events that end a run and auto-close an SSE stream.
205
+ *
206
+ * `workflow:failed` is terminal ONLY without compensation handlers: a saga
207
+ * emits `failed` THEN rolls back, so closing there would hide the rollback.
208
+ * See {@link isStreamTerminal}.
267
209
  */
268
- declare const STREAMLINE_TERMINAL_EVENTS: readonly ["workflow:completed", "workflow:failed", "workflow:cancelled"];
210
+ declare const STREAMLINE_TERMINAL_EVENTS: readonly ["workflow:completed", "workflow:failed", "workflow:cancelled", "workflow:compensated", "workflow:compensation_failed"];
269
211
  /**
270
- * Terminal run STATUSES (distinct from the terminal EVENT names above).
271
- * Mirrors streamline's `isTerminalState()` — a run in one of these will
272
- * emit no further bus events, so the SSE handler sends its snapshot and
273
- * closes immediately rather than holding a connection open on a dead run.
212
+ * Terminal run STATUSES (vs. the event names above). A run in one emits no
213
+ * further events, so SSE sends its snapshot and closes. Pinned against
214
+ * streamline's own list by `tests/integrations/streamline-drift.test.ts`.
274
215
  */
275
216
  declare const TERMINAL_RUN_STATUSES: Set<string>;
276
217
  /**
277
- * Pluggable streamline integration for Arc.
278
- *
279
- * Wrapped in `fastify-plugin` so Fastify treats `options.prefix` as a
280
- * plain plugin option (NOT an encapsulation prefix). Without the wrapper,
281
- * Fastify would prepend `options.prefix` to every route, then the plugin
282
- * code would prepend it again — the duplicate-prefix bug.
218
+ * Whether a run has ended, for the purpose of closing its stream — by event
219
+ * name or by status. `failed` ends a run only when nothing will compensate it.
220
+ */
221
+ declare function isStreamTerminal(eventOrStatus: string, compensates: boolean, kind: "event" | "status"): boolean;
222
+ /**
223
+ * Streamline integration for Arc. Wrapped in `fastify-plugin` so `prefix` is
224
+ * a plain option, not an encapsulation prefix (which would apply it twice).
283
225
  */
284
226
  declare const streamlinePlugin: FastifyPluginAsync<StreamlinePluginOptions>;
285
227
  //#endregion
286
- export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_STREAM_EVENTS, STREAMLINE_TERMINAL_EVENTS, StreamlinePluginOptions, TERMINAL_RUN_STATUSES, WorkflowLike, WorkflowRunLike, WorkflowStartOptions, streamlinePlugin };
228
+ export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_STREAM_EVENTS, STREAMLINE_TERMINAL_EVENTS, StreamlinePluginOptions, TERMINAL_RUN_STATUSES, WorkflowLike, WorkflowRunLike, WorkflowStartOptions, isStreamTerminal, streamlinePlugin };
@@ -1,17 +1,38 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { f as createError, i as NotFoundError, r as ForbiddenError } from "../errors-BH7YU_9O.mjs";
3
- import { a as promoteStreamTokenToHeader, n as forwardedStreamHeaders } from "../streaming-IcRFjWQU.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { f as createError, i as NotFoundError, r as ForbiddenError } from "../errors-CtrywMHP.mjs";
3
+ import { a as promoteStreamTokenToHeader, n as forwardedStreamHeaders } from "../streaming-DjTQhZrN.mjs";
4
+ import { ARC_ERROR_CODES } from "@classytic/repo-core/errors";
4
5
  import fp from "fastify-plugin";
5
6
  //#region src/integrations/streamline.ts
6
7
  /**
7
- * Streamline >= 2.3.3 dead-letter discriminator. The run.status stays
8
- * `'failed'`; the discrimination is `error.code`:
8
+ * Streamline integration — wires `@classytic/streamline` workflows into arc.
9
+ *
10
+ * REST endpoints for workflow management, auto-connected to arc's event bus,
11
+ * honouring arc auth/permissions and surfacing streamline's repo-core-aligned
12
+ * `HttpError`s with their real status codes rather than a blanket 500.
13
+ *
14
+ * A separate subpath, loaded only when used:
15
+ * `@classytic/arc/integrations/streamline`. Peer: `@classytic/streamline`.
16
+ *
17
+ * Routes mounted under `prefix`: `POST /:workflow/start`,
18
+ * `GET /:workflow/runs[/:runId]`, `POST /:workflow/runs/:runId/{resume,cancel}`,
19
+ * and `POST /hooks/:token` when `enableHookEndpoint` is set.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * await fastify.register(streamlinePlugin, {
24
+ * workflows: [orderWorkflow],
25
+ * prefix: '/api/workflows',
26
+ * tenantResolver: (req) => req.user?.organizationId,
27
+ * });
28
+ * ```
29
+ */
30
+ /**
31
+ * Dead-letter discriminator: `run.status` stays `'failed'`; `error.code` says why:
9
32
  * - `'stale_heartbeat'` — sweeper terminated; transient crash signal.
10
33
  * - `'dead_lettered'` — exceeded `maxStaleRecoveries`; permanent.
11
34
  * - `'VERSION_MISMATCH'` — engine deployed a step graph the run can't
12
35
  * resume against; admin must rewind / migrate / cancel.
13
- *
14
- * Hosts switch on `error.code` for dashboards / alerting.
15
36
  */
16
37
  const STREAMLINE_FAILURE_CODES = {
17
38
  STALE_HEARTBEAT: "stale_heartbeat",
@@ -19,15 +40,9 @@ const STREAMLINE_FAILURE_CODES = {
19
40
  VERSION_MISMATCH: "VERSION_MISMATCH"
20
41
  };
21
42
  /**
22
- * Full event list published on a streamline workflow's internal `eventBus`
23
- * (tracks streamline 2.3's `EventPayloadMap` in
24
- * `@classytic/streamline/src/core/events.ts`).
25
- *
26
- * Hardcoded here by design — arc subscribes via structural
27
- * `eventBus.on(name, handler)`, which is a no-op for events the running
28
- * streamline version doesn't emit. New events a future streamline release
29
- * adds can be bridged by updating this list; arc never breaks just
30
- * because streamline extended its bus.
43
+ * Events published on a streamline workflow's internal `eventBus`. Subscribing
44
+ * to one the running version doesn't emit is a no-op. Pinned against
45
+ * streamline's `ALL_WORKFLOW_EVENT_NAMES` by `tests/integrations/streamline-drift.test.ts`.
31
46
  */
32
47
  const STREAMLINE_BUS_EVENTS = [
33
48
  "step:started",
@@ -47,48 +62,63 @@ const STREAMLINE_BUS_EVENTS = [
47
62
  "workflow:retry",
48
63
  "workflow:compensating",
49
64
  "workflow:paused",
65
+ "workflow:compensated",
66
+ "workflow:compensation_failed",
50
67
  "engine:error",
51
68
  "scheduler:error",
52
69
  "scheduler:circuit-open"
53
70
  ];
54
71
  /**
55
- * Non-durable streaming frames from streamline >= 2.6's `ctx.stream(frame)`
56
- * (LLM tokens, percent-complete, live previews).
57
- *
58
- * Delivered on the SSE endpoint ONLY — deliberately excluded from
59
- * `STREAMLINE_BUS_EVENTS` / `bridgeBusEvents` because frames are
60
- * high-frequency, at-most-once UI traffic, not domain events: republishing
61
- * every token onto arc's Redis/Kafka transport would flood it. Subscribers
62
- * that genuinely want frames on the transport can bridge
63
- * `streamline:step.stream` from streamline's own arc-shape transport.
64
- *
65
- * On streamline < 2.6 the subscription is a structural no-op (the bus never
66
- * emits the event) — no version gate needed.
72
+ * Non-durable `ctx.stream(frame)` frames (LLM tokens, progress). SSE ONLY —
73
+ * excluded from `bridgeBusEvents` because high-frequency at-most-once UI
74
+ * traffic would flood arc's event transport.
67
75
  */
68
76
  const STREAMLINE_STREAM_EVENTS = ["step:stream"];
69
77
  /**
70
- * Workflow events that should auto-close an SSE stream when observed.
71
- * Recovered / waiting / resumed / retry / compensating are NOT terminal —
72
- * the run is still active after them.
78
+ * Workflow events that end a run and auto-close an SSE stream.
79
+ *
80
+ * `workflow:failed` is terminal ONLY without compensation handlers: a saga
81
+ * emits `failed` THEN rolls back, so closing there would hide the rollback.
82
+ * See {@link isStreamTerminal}.
73
83
  */
74
84
  const STREAMLINE_TERMINAL_EVENTS = [
75
85
  "workflow:completed",
76
86
  "workflow:failed",
77
- "workflow:cancelled"
87
+ "workflow:cancelled",
88
+ "workflow:compensated",
89
+ "workflow:compensation_failed"
78
90
  ];
79
91
  /**
80
- * Terminal run STATUSES (distinct from the terminal EVENT names above).
81
- * Mirrors streamline's `isTerminalState()` — a run in one of these will
82
- * emit no further bus events, so the SSE handler sends its snapshot and
83
- * closes immediately rather than holding a connection open on a dead run.
92
+ * Terminal run STATUSES (vs. the event names above). A run in one emits no
93
+ * further events, so SSE sends its snapshot and closes. Pinned against
94
+ * streamline's own list by `tests/integrations/streamline-drift.test.ts`.
84
95
  */
85
96
  const TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
86
97
  "done",
87
98
  "failed",
88
- "cancelled"
99
+ "cancelled",
100
+ "compensated",
101
+ "compensation_failed"
89
102
  ]);
103
+ /** Whether a workflow rolls back on failure (has any `onCompensate` step). */
104
+ function hasCompensation(wf) {
105
+ const handlers = wf.engine.options?.compensationHandlers;
106
+ return !!handlers && typeof handlers === "object" && Object.keys(handlers).length > 0;
107
+ }
108
+ /**
109
+ * Whether a run has ended, for the purpose of closing its stream — by event
110
+ * name or by status. `failed` ends a run only when nothing will compensate it.
111
+ */
112
+ function isStreamTerminal(eventOrStatus, compensates, kind) {
113
+ if (kind === "event") {
114
+ if (eventOrStatus === "workflow:failed") return !compensates;
115
+ return STREAMLINE_TERMINAL_EVENTS.includes(eventOrStatus);
116
+ }
117
+ if (eventOrStatus === "failed") return !compensates;
118
+ return TERMINAL_RUN_STATUSES.has(eventOrStatus);
119
+ }
90
120
  const streamlinePluginImpl = async (fastify, options) => {
91
- const { workflows, prefix = "/workflows", auth = true, bridgeEvents = true, enableStreaming = false, enableHookEndpoint = false, tenantResolver, bypassTenantResolver, permissions: perms } = options;
121
+ const { workflows, prefix = "/workflows", auth = true, bridgeEvents = true, enableStreaming = false, enableHookEndpoint = false, tenantResolver, bypassTenantResolver, permissions: perms, idempotency } = options;
92
122
  const routeScope = prefix;
93
123
  const bridgeBus = options.bridgeBusEvents ?? false;
94
124
  const registry = /* @__PURE__ */ new Map();
@@ -110,11 +140,38 @@ const streamlinePluginImpl = async (fastify, options) => {
110
140
  if (tenantId !== void 0) opts.tenantId = tenantId;
111
141
  return opts;
112
142
  };
143
+ const PERM_FALLBACK = {
144
+ execute: "start",
145
+ pause: "cancel",
146
+ rewind: "cancel"
147
+ };
113
148
  const checkPerm = async (op, request) => {
114
- const check = perms?.[op];
149
+ const fallback = PERM_FALLBACK[op];
150
+ const check = perms?.[op] ?? (fallback ? perms?.[fallback] : void 0);
115
151
  if (!check) return true;
116
152
  return check(request);
117
153
  };
154
+ const fencedOps = new Set(idempotency?.required ?? []);
155
+ if (fencedOps.size > 0 && !fastify.hasDecorator("idempotency")) throw new Error("[Arc/Streamline] `idempotency.required` is declared, but idempotencyPlugin is not registered — register it (enabled) before streamlinePlugin, or drop the declaration.");
156
+ for (const wfId of idempotency?.workflows ?? []) if (!workflows.some((wf) => wf.definition.id === wfId)) throw new Error(`[Arc/Streamline] idempotency.workflows names unknown workflow '${wfId}'.`);
157
+ /**
158
+ * Route options for a mutating op: a fenced op gets `config.idempotency.required` and
159
+ * runs auth → permission → idempotency, so a refused caller never reaches the key check.
160
+ */
161
+ const routeOpts = (wfId, op, permOp = op) => {
162
+ if (!(fencedOps.has(op) && (!idempotency?.workflows || idempotency.workflows.includes(wfId)))) return { preHandler: authPreHandler };
163
+ const permGate = async (request) => {
164
+ if (!await checkPerm(permOp, request)) throw new ForbiddenError();
165
+ };
166
+ return {
167
+ config: { idempotency: { required: true } },
168
+ preHandler: [
169
+ ...authPreHandler,
170
+ permGate,
171
+ fastify.idempotency.middleware
172
+ ]
173
+ };
174
+ };
118
175
  for (const [id, wf] of registry) {
119
176
  const routePrefix = `${routeScope}/${id}`;
120
177
  const visibilityRepo = wf.container?.repository;
@@ -125,7 +182,7 @@ const streamlinePluginImpl = async (fastify, options) => {
125
182
  if (tenantOpts.bypassTenant || tenantOpts.tenantId === void 0) return;
126
183
  if (!await visibilityGetById(runId, { tenantId: tenantOpts.tenantId })) throw new NotFoundError("Workflow run", runId);
127
184
  };
128
- fastify.post(`${routePrefix}/start`, { preHandler: authPreHandler }, async (request, reply) => {
185
+ fastify.post(`${routePrefix}/start`, routeOpts(id, "start"), async (request, reply) => {
129
186
  if (!await checkPerm("start", request)) throw new ForbiddenError();
130
187
  const body = request.body;
131
188
  if (body !== null && body !== void 0 && typeof body !== "object") throw createError(422, `[Arc/Streamline] '/${id}/start' body must be a JSON object.`, {
@@ -152,7 +209,13 @@ const streamlinePluginImpl = async (fastify, options) => {
152
209
  workflowId: id,
153
210
  unknown: unknownKeys
154
211
  });
155
- const { input, meta, idempotencyKey, priority } = bodyRecord;
212
+ const { input, meta, idempotencyKey: bodyKey, priority } = bodyRecord;
213
+ const headerKey = request.idempotencyKey;
214
+ if (headerKey !== void 0 && bodyKey !== void 0 && bodyKey !== headerKey) throw createError(422, `[Arc/Streamline] '/${id}/start' got an idempotency-key header and a different body \`idempotencyKey\` — send one key, or the same key in both.`, {
215
+ code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REUSED,
216
+ workflowId: id
217
+ });
218
+ const idempotencyKey = bodyKey ?? headerKey;
156
219
  const tenantOpts = resolveTenantOpts(request);
157
220
  const run = await wf.start(input, {
158
221
  meta,
@@ -201,7 +264,7 @@ const streamlinePluginImpl = async (fastify, options) => {
201
264
  if (!run) throw new NotFoundError("Workflow run", runId);
202
265
  return run;
203
266
  });
204
- fastify.post(`${routePrefix}/runs/:runId/resume`, { preHandler: authPreHandler }, async (request, _reply) => {
267
+ fastify.post(`${routePrefix}/runs/:runId/resume`, routeOpts(id, "resume"), async (request, _reply) => {
205
268
  if (!await checkPerm("resume", request)) throw new ForbiddenError();
206
269
  const { runId } = request.params;
207
270
  await assertRunVisible(request, runId);
@@ -241,7 +304,8 @@ const streamlinePluginImpl = async (fastify, options) => {
241
304
  }
242
305
  return run;
243
306
  });
244
- fastify.post(`${routePrefix}/runs/:runId/execute`, { preHandler: authPreHandler }, async (request, _reply) => {
307
+ fastify.post(`${routePrefix}/runs/:runId/execute`, routeOpts(id, "execute"), async (request, _reply) => {
308
+ if (!await checkPerm("execute", request)) throw new ForbiddenError();
245
309
  const { runId } = request.params;
246
310
  await assertRunVisible(request, runId);
247
311
  return await wf.engine.execute(runId);
@@ -273,12 +337,14 @@ const streamlinePluginImpl = async (fastify, options) => {
273
337
  return await wf.engine.waitFor?.(runId, { timeout: Math.min(timeoutMs, 12e4) });
274
338
  });
275
339
  if (wf.engine.pause) fastify.post(`${routePrefix}/runs/:runId/pause`, { preHandler: authPreHandler }, async (request, _reply) => {
340
+ if (!await checkPerm("pause", request)) throw new ForbiddenError();
276
341
  const { runId } = request.params;
277
342
  await assertRunVisible(request, runId);
278
343
  const { reason: pauseReason } = request.body ?? {};
279
344
  return await wf.engine.pause?.(runId, pauseReason !== void 0 ? { reason: pauseReason } : void 0);
280
345
  });
281
- if (wf.engine.rewindTo) fastify.post(`${routePrefix}/runs/:runId/rewind`, { preHandler: authPreHandler }, async (request, _reply) => {
346
+ if (wf.engine.rewindTo) fastify.post(`${routePrefix}/runs/:runId/rewind`, routeOpts(id, "rewind"), async (request, _reply) => {
347
+ if (!await checkPerm("rewind", request)) throw new ForbiddenError();
282
348
  const { runId } = request.params;
283
349
  await assertRunVisible(request, runId);
284
350
  const { stepId } = request.body ?? {};
@@ -315,7 +381,7 @@ const streamlinePluginImpl = async (fastify, options) => {
315
381
  ...forwardedStreamHeaders(reply)
316
382
  });
317
383
  reply.raw.flushHeaders();
318
- const terminalEvents = new Set(STREAMLINE_TERMINAL_EVENTS);
384
+ const compensates = hasCompensation(wf);
319
385
  const listeners = [];
320
386
  let closed = false;
321
387
  let heartbeat;
@@ -339,7 +405,7 @@ const streamlinePluginImpl = async (fastify, options) => {
339
405
  };
340
406
  send("workflow:snapshot", run);
341
407
  const runStatus = run.status;
342
- if (runStatus && TERMINAL_RUN_STATUSES.has(runStatus)) {
408
+ if (runStatus && isStreamTerminal(runStatus, compensates, "status")) {
343
409
  cleanup();
344
410
  return;
345
411
  }
@@ -357,7 +423,7 @@ const streamlinePluginImpl = async (fastify, options) => {
357
423
  const p = payload;
358
424
  if (typeof p?.runId === "string" && p.runId !== runId) return;
359
425
  send(eventName, p);
360
- if (terminalEvents.has(eventName) && p?.runId === runId) cleanup();
426
+ if (p?.runId === runId && isStreamTerminal(eventName, compensates, "event")) cleanup();
361
427
  };
362
428
  wf.container?.eventBus.on(eventName, fn);
363
429
  listeners.push({
@@ -395,16 +461,12 @@ const streamlinePluginImpl = async (fastify, options) => {
395
461
  });
396
462
  };
397
463
  /**
398
- * Pluggable streamline integration for Arc.
399
- *
400
- * Wrapped in `fastify-plugin` so Fastify treats `options.prefix` as a
401
- * plain plugin option (NOT an encapsulation prefix). Without the wrapper,
402
- * Fastify would prepend `options.prefix` to every route, then the plugin
403
- * code would prepend it again — the duplicate-prefix bug.
464
+ * Streamline integration for Arc. Wrapped in `fastify-plugin` so `prefix` is
465
+ * a plain option, not an encapsulation prefix (which would apply it twice).
404
466
  */
405
467
  const streamlinePlugin = fp(streamlinePluginImpl, {
406
468
  name: "streamline-routes",
407
469
  fastify: "5.x"
408
470
  });
409
471
  //#endregion
410
- export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_STREAM_EVENTS, STREAMLINE_TERMINAL_EVENTS, TERMINAL_RUN_STATUSES, streamlinePlugin };
472
+ export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_STREAM_EVENTS, STREAMLINE_TERMINAL_EVENTS, TERMINAL_RUN_STATUSES, isStreamTerminal, streamlinePlugin };