@classytic/arc 2.41.0 → 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.
- package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
- package/dist/{EventTransport-n7qO5Cz3.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
- package/dist/{HookSystem-CxvZBo77.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-Cu7uTJp9.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
- package/dist/actionPermissions-BotX1YOa.mjs +776 -0
- package/dist/audit/index.d.mts +43 -5
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +43 -13
- package/dist/auth/index.mjs +130 -155
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-2YY_Kaj1.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-BULzfXYU.mjs +57 -0
- package/dist/{buildHandler-CUhjGHGZ.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
- package/dist/cache/index.d.mts +22 -34
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DsWDjIxx.mjs → caching-Cc2M1DwO.mjs} +3 -3
- package/dist/claimed-B10RTfkM.mjs +8 -0
- package/dist/cleanup/index.d.mts +6 -10
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +4 -2
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-BxLgNFF7.mjs → constants-Cu7Mx7_U.mjs} +4 -2
- package/dist/context/index.d.mts +2 -5
- 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-iNGqYerO.mjs → core-Cb0LjccA.mjs} +5 -4
- package/dist/{createActionRouter-BBlbPs3k.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
- package/dist/{createAggregationRouter-Dv5SuK2x.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
- package/dist/{preloadResources-Bbp4QJ8V.mjs → createApp-D-953ZAq.mjs} +299 -390
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
- package/dist/{defineAggregation-UyeurGkp.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
- package/dist/{defineResource-DzGT-2LP.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +10 -4
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/{elevation-BF-vhGxY.mjs → elevation-DG8SrLN-.mjs} +41 -12
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-CIMkCOWm.mjs → errorHandler-BnfDviL1.mjs} +26 -5
- package/dist/{errors-C_YhELhe.mjs → errors-CtrywMHP.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-CmXaPWLv.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
- package/dist/{subscribe-helpers-AVYPgqTl.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
- package/dist/events/index.d.mts +33 -8
- 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 +1 -1
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +32 -17
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/{fields-BTyPLHK0.mjs → fields-ToPYpMqS.mjs} +1 -1
- package/dist/filter-merge-IpGywvym.mjs +99 -0
- package/dist/{generate-D_YmiCNf.mjs → generate-DRTkLlIl.mjs} +2 -2
- package/dist/{headers-CNECDE6q.mjs → headers-BbkIF6vA.mjs} +2 -2
- package/dist/health-D-Ft6JM6.mjs +22 -0
- package/dist/{health-CxQzbJmr.mjs → health-DqbsG-_w.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +31 -7
- package/dist/idempotency/index.mjs +155 -98
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +48 -11
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
- package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/index-JdbZAIsm.d.mts +510 -0
- package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +11 -11
- package/dist/{init-Y2MlSN9n.mjs → init-B_oUYDXI.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +8 -3
- 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 +72 -59
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +78 -136
- package/dist/integrations/streamline.mjs +116 -54
- package/dist/integrations/webhooks.d.mts +60 -120
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- 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-C0395yUN.d.mts} +13 -1
- package/dist/{jsonBody-Cq9KYl2Z.mjs → jsonBody-B4YVGcql.mjs} +2 -2
- package/dist/{keys-Cmv4cBRa.mjs → keys-B2KtgMLJ.mjs} +7 -18
- package/dist/{loadResources-CblF7xt2.mjs → loadResources-Dn4lalw2.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-99fIMC3p.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
- package/dist/logger/index.d.mts +1 -8
- package/dist/logger/index.mjs +3 -153
- package/dist/logger-DXjfIqjV.mjs +160 -0
- package/dist/{mcpPlugin-xF3Gw-HW.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
- package/dist/{memory-CpDZ9NPw.mjs → memory-9Cp7611C.mjs} +36 -34
- package/dist/{metrics-BaKnq8ag.mjs → metrics-CWI11eRu.mjs} +39 -72
- package/dist/middleware/index.d.mts +2 -2
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-BkQEesNQ.mjs → module-BSvOQC7E.mjs} +8 -8
- package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
- package/dist/{multipartBody-C92aJ8am.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
- package/dist/{openapi-C3LFRzPq.mjs → openapi-DT_G4VhY.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +3 -3
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-dsCyLxXH.mjs → outbox-module-Bjgzl6tg.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-DiHXy8Ck.mjs → permissions-Cm0uYHvd.mjs} +149 -33
- package/dist/{pipe-CBVmUJ-j.mjs → pipe-BqeGT6UO.mjs} +2 -2
- package/dist/pipeline/index.d.mts +2 -2
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-FVR6LP59.mjs +565 -0
- package/dist/{BaseController-DYJRZ0EJ.mjs → plugin-BccET1tL.mjs} +1256 -344
- package/dist/plugins/index.d.mts +14 -11
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-2_U6QZQg.mjs → pluralize-CVIOcgMF.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +26 -31
- 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 +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-BdlugYTc.mjs → presets-Cpvj6fGO.mjs} +99 -107
- package/dist/{projection-Uvgp-EEj.mjs → projection-Ce4aQhQk.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{queryCachePlugin-B5F09bYr.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
- package/dist/{rateLimitKey-CK-EU2L8.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
- package/dist/{realtime-FnnD0n3Z.mjs → realtime-DmPE9S54.mjs} +25 -21
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +9 -4
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-D3y8VEJ1.mjs → registry-Bcz9G1bh.mjs} +10 -8
- package/dist/{renewingLease-B3VYh5aU.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
- package/dist/{replyHelpers-DPHsdw-5.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
- package/dist/{requestContext-cSVKnYPk.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/requirement-KDdoRcOP.mjs +35 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
- package/dist/{rolldown-runtime-DjDuyglE.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
- package/dist/routeValidator-D36SW8qt.mjs +70 -0
- package/dist/{runtimeCapabilities-DSA_dH1_.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
- package/dist/{schedules-BktJb3Y3.mjs → schedules-C-OpmHBq.mjs} +46 -37
- package/dist/{schemaConverter-BLCSmNbr.mjs → schemaConverter-BORek3O-.mjs} +2 -2
- package/dist/{schemaIR-K16Bb5BX.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
- package/dist/schemas/index.d.mts +10 -0
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-jSxuBTFM.mjs → scope-DQthe88b.mjs} +4 -4
- package/dist/shutdown-MqA81z_P.mjs +59 -0
- package/dist/{simpleEqualityMatcher-BiOnyflP.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
- package/dist/{sse-CH7Slxc1.mjs → sse-CYzQBVFe.mjs} +11 -15
- package/dist/{sseStream-CN-Lh1Tu.mjs → sseStream-B5_Afiqn.mjs} +2 -2
- package/dist/{store-helpers-9DX9-1j_.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
- package/dist/{streaming-dvppnxsr.mjs → streaming-DjTQhZrN.mjs} +1 -1
- package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
- package/dist/tenancy--ed9eH_z.mjs +43 -0
- package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +193 -175
- package/dist/testing/index.mjs +286 -212
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +17 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-qczZ0YC-.mjs +46 -0
- package/dist/{transactionContext-DCWMI6hV.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
- package/dist/{typeGuards-D7tHFdH6.mjs → typeGuards-CySF12G-.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-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
- package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CDy3-zw1.mjs → types-Bk9NOgOC.mjs} +4 -4
- package/dist/usage/index.d.mts +2 -3
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-DDMOZ_Pm.mjs → userHelpers-aklawRaK.mjs} +1 -1
- package/dist/utils/index.d.mts +84 -223
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-B-zj_b__.mjs → utils-ec1oTo1n.mjs} +315 -404
- package/dist/{validate-R0G_JrnK.mjs → validate-9yzHC1e2.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DuVWcu-F.mjs → versioning-fhhWHV74.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-BhsWde4k.mjs → workScope-zpPyge4t.mjs} +2 -2
- package/package.json +578 -574
- 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-DPwPWPOV.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-BIpgzbZl.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-DRsSHPSM.mjs +0 -364
- package/dist/sync/changelog.d.mts +0 -3
- 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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
-
*
|
|
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
|
|
157
|
-
* engine telemetry) to Arc's event bus
|
|
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
|
|
181
|
-
*
|
|
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
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
|
207
|
-
*
|
|
208
|
-
* `undefined` skips tenant
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
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
|
|
250
|
-
*
|
|
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
|
|
265
|
-
*
|
|
266
|
-
*
|
|
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 (
|
|
271
|
-
*
|
|
272
|
-
*
|
|
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
|
-
*
|
|
278
|
-
*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
*
|
|
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.
|
|
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.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
|
|
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({
|
|
@@ -395,16 +461,12 @@ const streamlinePluginImpl = async (fastify, options) => {
|
|
|
395
461
|
});
|
|
396
462
|
};
|
|
397
463
|
/**
|
|
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.
|
|
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 };
|