@classytic/arc 2.41.1 → 2.44.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 (248) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
  6. package/dist/acquire-BozJ-9yt.mjs +35 -0
  7. package/dist/actionPermissions-5N04zBL4.mjs +772 -0
  8. package/dist/audit/index.d.mts +46 -8
  9. package/dist/audit/index.mjs +48 -26
  10. package/dist/auth/audit.d.mts +8 -8
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +46 -16
  13. package/dist/auth/index.mjs +134 -156
  14. package/dist/auth/redis-session.d.mts +4 -5
  15. package/dist/auth/redis-session.mjs +1 -1
  16. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
  17. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  18. package/dist/boundedMap-DWiKW7lP.mjs +57 -0
  19. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
  20. package/dist/cache/index.d.mts +31 -43
  21. package/dist/cache/index.mjs +4 -4
  22. package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
  23. package/dist/claimed-DYvQoZLv.mjs +8 -0
  24. package/dist/cleanup/index.d.mts +18 -22
  25. package/dist/cleanup/index.mjs +27 -73
  26. package/dist/cli/commands/describe.d.mts +7 -5
  27. package/dist/cli/commands/describe.mjs +7 -4
  28. package/dist/cli/commands/docs.d.mts +2 -3
  29. package/dist/cli/commands/docs.mjs +3 -3
  30. package/dist/cli/commands/doctor.d.mts +2 -3
  31. package/dist/cli/commands/doctor.mjs +1 -1
  32. package/dist/cli/commands/generate.mjs +2 -3
  33. package/dist/cli/commands/init.mjs +2 -3
  34. package/dist/cli/commands/introspect.d.mts +2 -3
  35. package/dist/cli/commands/introspect.mjs +2 -2
  36. package/dist/cli/index.mjs +3 -3
  37. package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
  38. package/dist/context/index.d.mts +11 -14
  39. package/dist/context/index.mjs +18 -38
  40. package/dist/core/index.d.mts +3 -3
  41. package/dist/core/index.mjs +8 -8
  42. package/dist/{core-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
  43. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
  44. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
  45. package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
  46. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
  47. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
  48. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
  49. package/dist/discovery/index.d.mts +6 -7
  50. package/dist/discovery/index.mjs +1 -1
  51. package/dist/docs/index.d.mts +11 -5
  52. package/dist/docs/index.mjs +4 -4
  53. package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
  54. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  55. package/dist/encryption/index.d.mts +5 -5
  56. package/dist/encryption/index.mjs +3 -3
  57. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
  58. package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.mjs} +58 -23
  59. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  60. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  61. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
  62. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
  63. package/dist/events/index.d.mts +43 -18
  64. package/dist/events/index.mjs +49 -53
  65. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  66. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  67. package/dist/events/transports/redis.d.mts +5 -6
  68. package/dist/events/transports/redis.mjs +2 -2
  69. package/dist/factory/index.d.mts +42 -27
  70. package/dist/factory/index.mjs +213 -32
  71. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
  72. package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
  73. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  74. package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
  75. package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
  76. package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
  77. package/dist/health-BYAwA6YS.mjs +22 -0
  78. package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
  79. package/dist/hooks/index.d.mts +2 -2
  80. package/dist/hooks/index.mjs +2 -3
  81. package/dist/idempotency/index.d.mts +36 -10
  82. package/dist/idempotency/index.mjs +230 -111
  83. package/dist/idempotency/redis.d.mts +1 -1
  84. package/dist/idempotency/redis.mjs +77 -12
  85. package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
  86. package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
  87. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  88. package/dist/index-CUJvoyw3.d.mts +510 -0
  89. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  90. package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
  91. package/dist/index.d.mts +9 -9
  92. package/dist/index.mjs +11 -11
  93. package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
  94. package/dist/integrations/event-gateway.d.mts +11 -7
  95. package/dist/integrations/event-gateway.mjs +4 -4
  96. package/dist/integrations/index.d.mts +3 -3
  97. package/dist/integrations/index.mjs +1 -1
  98. package/dist/integrations/jobs/index.d.mts +1 -1
  99. package/dist/integrations/jobs/index.mjs +18 -5
  100. package/dist/integrations/mcp/index.d.mts +102 -89
  101. package/dist/integrations/mcp/index.mjs +19 -4
  102. package/dist/integrations/mcp/testing.d.mts +8 -9
  103. package/dist/integrations/mcp/testing.mjs +2 -2
  104. package/dist/integrations/streamline.d.mts +85 -144
  105. package/dist/integrations/streamline.mjs +131 -55
  106. package/dist/integrations/webhooks.d.mts +83 -143
  107. package/dist/integrations/webhooks.mjs +36 -80
  108. package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
  109. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  110. package/dist/integrations/websocket-redis.d.mts +5 -6
  111. package/dist/integrations/websocket-redis.mjs +1 -1
  112. package/dist/integrations/websocket.d.mts +3 -3
  113. package/dist/integrations/websocket.mjs +2 -2
  114. package/dist/{interface-DEWNOMza.d.mts → interface-JJzwH4Ro.d.mts} +34 -2
  115. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
  116. package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
  117. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
  118. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
  119. package/dist/logger/index.d.mts +9 -17
  120. package/dist/logger/index.mjs +2 -153
  121. package/dist/logger-D4V1zGLI.mjs +160 -0
  122. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
  123. package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
  124. package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
  125. package/dist/middleware/index.d.mts +5 -5
  126. package/dist/middleware/index.mjs +3 -3
  127. package/dist/migrations/index.d.mts +13 -13
  128. package/dist/migrations/index.mjs +2 -2
  129. package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
  130. package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
  131. package/dist/mongoose-CYJCLyi-.mjs +67451 -0
  132. package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
  133. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
  134. package/dist/node-D8UZv_8c.mjs +41236 -0
  135. package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
  136. package/dist/outbox-admin/index.d.mts +5 -5
  137. package/dist/outbox-admin/index.mjs +6 -18
  138. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.mjs} +203 -33
  139. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  140. package/dist/permissions/index.d.mts +5 -4
  141. package/dist/permissions/index.mjs +8 -6
  142. package/dist/{permissions-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
  143. package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
  144. package/dist/pipeline/index.d.mts +7 -7
  145. package/dist/pipeline/index.mjs +2 -2
  146. package/dist/plan-CJGMKoui.mjs +565 -0
  147. package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
  148. package/dist/plugins/index.d.mts +17 -14
  149. package/dist/plugins/index.mjs +44 -22
  150. package/dist/plugins/response-cache.d.mts +5 -6
  151. package/dist/plugins/response-cache.mjs +8 -14
  152. package/dist/plugins/tracing-entry.mjs +2 -2
  153. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
  154. package/dist/presets/filesUpload.d.mts +8 -8
  155. package/dist/presets/filesUpload.mjs +5 -5
  156. package/dist/presets/index.d.mts +35 -40
  157. package/dist/presets/index.mjs +4 -4
  158. package/dist/presets/multiTenant.d.mts +2 -107
  159. package/dist/presets/multiTenant.mjs +22 -162
  160. package/dist/presets/search.d.mts +9 -10
  161. package/dist/presets/search.mjs +2 -2
  162. package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
  163. package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
  164. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  165. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
  166. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  167. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
  168. package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
  169. package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
  170. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  171. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  172. package/dist/registry/index.d.mts +15 -10
  173. package/dist/registry/index.mjs +4 -4
  174. package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
  175. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
  176. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
  177. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
  178. package/dist/requirement-Bu50QALH.mjs +35 -0
  179. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  180. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  181. package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
  182. package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
  183. package/dist/routeValidator-DHtpzrzk.mjs +70 -0
  184. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
  185. package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
  186. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
  187. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
  188. package/dist/schemas/index.d.mts +20 -10
  189. package/dist/schemas/index.mjs +8 -3
  190. package/dist/scim/index.d.mts +13 -13
  191. package/dist/scim/index.mjs +2 -2
  192. package/dist/scope/index.d.mts +4 -4
  193. package/dist/scope/index.mjs +6 -6
  194. package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
  195. package/dist/shutdown-LpLMH8T1.mjs +59 -0
  196. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
  197. package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
  198. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
  199. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
  200. package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
  201. package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
  202. package/dist/tenancy-DxV8OuX2.mjs +43 -0
  203. package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
  204. package/dist/testing/cleanupStoreContract.d.mts +4 -5
  205. package/dist/testing/cleanupStoreContract.mjs +1 -1
  206. package/dist/testing/index.d.mts +215 -197
  207. package/dist/testing/index.mjs +289 -214
  208. package/dist/testing/outboxStoreContract.d.mts +4 -5
  209. package/dist/testing/outboxStoreContract.mjs +137 -2
  210. package/dist/testing/requiredPorts.d.mts +16 -0
  211. package/dist/testing/requiredPorts.mjs +65 -0
  212. package/dist/testing/storageContract.d.mts +4 -5
  213. package/dist/testing/storageContract.mjs +1 -1
  214. package/dist/timing-CntavTlR.mjs +46 -0
  215. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
  216. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.mjs} +1 -1
  217. package/dist/types/index.d.mts +4 -4
  218. package/dist/types/index.mjs +1 -1
  219. package/dist/types/storage.mjs +1 -1
  220. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  221. package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
  222. package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
  223. package/dist/usage/index.d.mts +5 -6
  224. package/dist/usage/index.mjs +5 -5
  225. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
  226. package/dist/utils/index.d.mts +125 -264
  227. package/dist/utils/index.mjs +13 -12
  228. package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
  229. package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
  230. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  231. package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
  232. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  233. package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
  234. package/package.json +57 -59
  235. package/skills/arc/SKILL.md +13 -4
  236. package/skills/arc/references/auth.md +8 -5
  237. package/skills/arc/references/integrations.md +1 -1
  238. package/skills/arc/references/mcp.md +8 -6
  239. package/skills/arc/references/multi-tenancy.md +19 -14
  240. package/skills/arc/references/production.md +27 -10
  241. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  242. package/dist/index-CFfLKMH4.d.mts +0 -290
  243. package/dist/interface-YzIbF7Ii.mjs +0 -17
  244. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  245. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  246. package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
  247. package/dist/sync/changelog.d.mts +0 -3
  248. package/dist/sync/changelog.mjs +0 -3
@@ -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.44.0 | MIT | https://classytic.com */
2
+ import { f as createError, i as NotFoundError, r as ForbiddenError } from "../errors-CW4anJBy.mjs";
3
+ import { a as promoteStreamTokenToHeader, n as forwardedStreamHeaders } from "../streaming-JzB5CAga.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({
@@ -370,10 +436,24 @@ const streamlinePluginImpl = async (fastify, options) => {
370
436
  }
371
437
  if (enableHookEndpoint) {
372
438
  let resumeHookFn;
439
+ /**
440
+ * The container the token's run lives in. Streamline's DB-fallback resume writes through the DEFAULT
441
+ * connection's repository and strict-concurrency counter unless it is handed the run's container —
442
+ * wrong for a streamline bound to another connection. One distinct container is the common case;
443
+ * several are told apart by which one holds the run.
444
+ */
445
+ const containerForRun = async (runId) => {
446
+ const containers = [...new Set([...registry.values()].map((wf) => wf.container).filter((c) => c !== void 0))];
447
+ if (containers.length <= 1) return containers[0];
448
+ for (const container of containers) try {
449
+ if (await container.repository?.getById?.(runId)) return container;
450
+ } catch {}
451
+ };
373
452
  fastify.post(`${routeScope}/hooks/:token`, { preHandler: authPreHandler }, async (request, _reply) => {
374
453
  if (!resumeHookFn) resumeHookFn = (await import("@classytic/streamline")).resumeHook;
375
454
  const { token } = request.params;
376
- const result = await resumeHookFn(token, request.body);
455
+ const container = await containerForRun(token.split(":")[0] ?? token);
456
+ const result = await resumeHookFn(token, request.body, container ? { container } : void 0);
377
457
  return {
378
458
  runId: result.runId,
379
459
  run: result.run
@@ -395,16 +475,12 @@ const streamlinePluginImpl = async (fastify, options) => {
395
475
  });
396
476
  };
397
477
  /**
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.
478
+ * Streamline integration for Arc. Wrapped in `fastify-plugin` so `prefix` is
479
+ * a plain option, not an encapsulation prefix (which would apply it twice).
404
480
  */
405
481
  const streamlinePlugin = fp(streamlinePluginImpl, {
406
482
  name: "streamline-routes",
407
483
  fastify: "5.x"
408
484
  });
409
485
  //#endregion
410
- export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_STREAM_EVENTS, STREAMLINE_TERMINAL_EVENTS, TERMINAL_RUN_STATUSES, streamlinePlugin };
486
+ export { STREAMLINE_BUS_EVENTS, STREAMLINE_FAILURE_CODES, STREAMLINE_STREAM_EVENTS, STREAMLINE_TERMINAL_EVENTS, TERMINAL_RUN_STATUSES, isStreamTerminal, streamlinePlugin };