@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.
- package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
- package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
- package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
- package/dist/acquire-BozJ-9yt.mjs +35 -0
- package/dist/actionPermissions-5N04zBL4.mjs +772 -0
- package/dist/audit/index.d.mts +46 -8
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.d.mts +8 -8
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +46 -16
- package/dist/auth/index.mjs +134 -156
- package/dist/auth/redis-session.d.mts +4 -5
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-DWiKW7lP.mjs +57 -0
- package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
- package/dist/cache/index.d.mts +31 -43
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
- package/dist/claimed-DYvQoZLv.mjs +8 -0
- package/dist/cleanup/index.d.mts +18 -22
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +7 -5
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.d.mts +2 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.d.mts +2 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -3
- package/dist/cli/commands/init.mjs +2 -3
- package/dist/cli/commands/introspect.d.mts +2 -3
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
- package/dist/context/index.d.mts +11 -14
- package/dist/context/index.mjs +18 -38
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +8 -8
- package/dist/{core-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
- package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
- package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
- package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
- package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
- package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
- package/dist/discovery/index.d.mts +6 -7
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +11 -5
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/encryption/index.d.mts +5 -5
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
- package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.mjs} +58 -23
- package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
- package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
- package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
- package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
- package/dist/events/index.d.mts +43 -18
- package/dist/events/index.mjs +49 -53
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +139 -157
- package/dist/events/transports/redis.d.mts +5 -6
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +42 -27
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
- package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
- package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
- package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
- package/dist/health-BYAwA6YS.mjs +22 -0
- package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +36 -10
- package/dist/idempotency/index.mjs +230 -111
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +77 -12
- package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/index-CUJvoyw3.d.mts +510 -0
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +11 -11
- package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +11 -7
- package/dist/integrations/event-gateway.mjs +4 -4
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +18 -5
- package/dist/integrations/mcp/index.d.mts +102 -89
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +8 -9
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +85 -144
- package/dist/integrations/streamline.mjs +131 -55
- package/dist/integrations/webhooks.d.mts +83 -143
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +5 -6
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.d.mts +3 -3
- package/dist/integrations/websocket.mjs +2 -2
- package/dist/{interface-DEWNOMza.d.mts → interface-JJzwH4Ro.d.mts} +34 -2
- package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
- package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
- package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
- package/dist/logger/index.d.mts +9 -17
- package/dist/logger/index.mjs +2 -153
- package/dist/logger-D4V1zGLI.mjs +160 -0
- package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
- package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
- package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
- package/dist/middleware/index.d.mts +5 -5
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.d.mts +13 -13
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
- package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
- package/dist/mongoose-CYJCLyi-.mjs +67451 -0
- package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
- package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
- package/dist/node-D8UZv_8c.mjs +41236 -0
- package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +5 -5
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.mjs} +203 -33
- package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +8 -6
- package/dist/{permissions-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
- package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
- package/dist/pipeline/index.d.mts +7 -7
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-CJGMKoui.mjs +565 -0
- package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
- package/dist/plugins/index.d.mts +17 -14
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.d.mts +5 -6
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +8 -8
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +35 -40
- package/dist/presets/index.mjs +4 -4
- package/dist/presets/multiTenant.d.mts +2 -107
- package/dist/presets/multiTenant.mjs +22 -162
- package/dist/presets/search.d.mts +9 -10
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
- package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
- package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
- package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +15 -10
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
- package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
- package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
- package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
- package/dist/requirement-Bu50QALH.mjs +35 -0
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
- package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
- package/dist/routeValidator-DHtpzrzk.mjs +70 -0
- package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
- package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
- package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
- package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
- package/dist/schemas/index.d.mts +20 -10
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.d.mts +13 -13
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +4 -4
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
- package/dist/shutdown-LpLMH8T1.mjs +59 -0
- package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
- package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
- package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
- package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
- package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
- package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
- package/dist/tenancy-DxV8OuX2.mjs +43 -0
- package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
- package/dist/testing/cleanupStoreContract.d.mts +4 -5
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +215 -197
- package/dist/testing/index.mjs +289 -214
- package/dist/testing/outboxStoreContract.d.mts +4 -5
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +16 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.d.mts +4 -5
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-CntavTlR.mjs +46 -0
- package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
- package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
- package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
- package/dist/usage/index.d.mts +5 -6
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
- package/dist/utils/index.d.mts +125 -264
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
- package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
- package/package.json +57 -59
- package/skills/arc/SKILL.md +13 -4
- package/skills/arc/references/auth.md +8 -5
- package/skills/arc/references/integrations.md +1 -1
- package/skills/arc/references/mcp.md +8 -6
- package/skills/arc/references/multi-tenancy.md +19 -14
- package/skills/arc/references/production.md +27 -10
- package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-YzIbF7Ii.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
- package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
- package/dist/sync/changelog.d.mts +0 -3
- package/dist/sync/changelog.mjs +0 -3
|
@@ -1,17 +1,38 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { f as createError, i as NotFoundError, r as ForbiddenError } from "../errors-
|
|
3
|
-
import { a as promoteStreamTokenToHeader, n as forwardedStreamHeaders } from "../streaming-
|
|
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
|
|
8
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
56
|
-
*
|
|
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
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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 (
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
|
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`,
|
|
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`,
|
|
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`,
|
|
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`,
|
|
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
|
|
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 &&
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
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 };
|