@classytic/arc 2.1.2 → 2.1.7
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/README.md +6 -5
- package/bin/arc.js +1 -0
- package/dist/{EventTransport-BD2U0BTc.d.mts → EventTransport-BkUDYZEb.d.mts} +1 -2
- package/dist/HookSystem-BsGV-j2l.mjs +1 -2
- package/dist/{ResourceRegistry-DsN4KJjV.mjs → ResourceRegistry-7Ic20ZMw.mjs} +1 -2
- package/dist/adapters/index.d.mts +4 -4
- package/dist/audit/index.d.mts +5 -6
- package/dist/audit/index.mjs +2 -3
- package/dist/audit/mongodb.d.mts +4 -4
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/{audited-C3T5DTUx.mjs → audited-CGdLiSlE.mjs} +1 -2
- package/dist/auth/index.d.mts +6 -7
- package/dist/auth/index.mjs +10 -16
- package/dist/auth/redis-session.d.mts +2 -3
- package/dist/auth/redis-session.mjs +1 -2
- package/dist/{betterAuthOpenApi-BrHKeSAx.mjs → betterAuthOpenApi-DjWDddNc.mjs} +2 -3
- package/dist/cache/index.d.mts +3 -4
- package/dist/cache/index.mjs +4 -5
- package/dist/{caching-Bl28lYsR.mjs → caching-GSDJcA6-.mjs} +1 -2
- package/dist/{circuitBreaker-DeY4FCjs.mjs → circuitBreaker-DYhWBW_D.mjs} +1 -2
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/describe.mjs +1 -2
- package/dist/cli/commands/docs.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +3 -4
- package/dist/cli/commands/generate.d.mts +6 -2
- package/dist/cli/commands/generate.mjs +89 -58
- package/dist/cli/commands/init.d.mts +1 -2
- package/dist/cli/commands/init.mjs +15 -19
- package/dist/cli/commands/introspect.d.mts +1 -2
- package/dist/cli/commands/introspect.mjs +2 -3
- package/dist/cli/index.d.mts +1 -2
- package/dist/cli/index.mjs +1 -2
- package/dist/constants-DdXFXQtN.mjs +1 -2
- package/dist/core/index.d.mts +4 -4
- package/dist/core/index.mjs +1 -1
- package/dist/{createApp-CUgNqegw.mjs → createApp-D2D5XXaV.mjs} +9 -10
- package/dist/{defineResource-k0_BDn8v.mjs → defineResource-DZVbwsFb.mjs} +17 -39
- package/dist/discovery/index.d.mts +1 -2
- package/dist/discovery/index.mjs +1 -2
- package/dist/docs/index.d.mts +5 -6
- package/dist/docs/index.mjs +5 -4
- package/dist/{elevation-B_2dRLVP.d.mts → elevation-DGo5shaX.d.mts} +1 -2
- package/dist/{elevation-BRy3yFWT.mjs → elevation-DSTbVvYj.mjs} +4 -4
- package/dist/{errorHandler-C1okiriz.mjs → errorHandler-C3GY3_ow.mjs} +2 -3
- package/dist/{errorHandler-BbcgBmIH.d.mts → errorHandler-CW3OOeYq.d.mts} +2 -3
- package/dist/{errors-ChKiFz62.d.mts → errors-DAWRdiYP.d.mts} +1 -2
- package/dist/{errors-B9bZok84.mjs → errors-DBANPbGr.mjs} +1 -2
- package/dist/{eventPlugin-DGR_B2on.mjs → eventPlugin-BEOvaDqo.mjs} +2 -3
- package/dist/{eventPlugin-CTrLH3mt.d.mts → eventPlugin-H6wDDjGO.d.mts} +2 -3
- package/dist/events/index.d.mts +4 -5
- package/dist/events/index.mjs +2 -3
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +1 -2
- package/dist/events/transports/redis.d.mts +2 -3
- package/dist/events/transports/redis.mjs +1 -2
- package/dist/{externalPaths-DlINfKbP.d.mts → externalPaths-SyPF2tgK.d.mts} +1 -2
- package/dist/factory/index.d.mts +8 -9
- package/dist/factory/index.mjs +1 -1
- package/dist/{fastifyAdapter-BkrGrlFi.d.mts → fastifyAdapter-sGkvUvf5.d.mts} +4 -5
- package/dist/{fields-DyaDVX4J.d.mts → fields-Bi_AVKSo.d.mts} +2 -3
- package/dist/{fields-iagOozy0.mjs → fields-CTd_CrKr.mjs} +2 -3
- package/dist/hooks/index.d.mts +3 -3
- package/dist/idempotency/index.d.mts +4 -5
- package/dist/idempotency/index.mjs +1 -2
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +1 -2
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -2
- package/dist/index.d.mts +9 -10
- package/dist/index.mjs +7 -8
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/event-gateway.mjs +2 -3
- package/dist/integrations/jobs.d.mts +1 -2
- package/dist/integrations/jobs.mjs +1 -2
- package/dist/integrations/streamline.d.mts +1 -2
- package/dist/integrations/streamline.mjs +1 -2
- package/dist/integrations/websocket.d.mts +1 -2
- package/dist/integrations/websocket.mjs +1 -2
- package/dist/{interface-B01JvPVc.d.mts → interface-CSNjltAc.d.mts} +1 -2
- package/dist/{interface-Ch8HU9uM.d.mts → interface-Cb2klgid.d.mts} +10 -10
- package/dist/{interface-CZe8IkMf.d.mts → interface-DTbsvIWe.d.mts} +1 -2
- package/dist/{introspectionPlugin-rFdO8ZUa.mjs → introspectionPlugin-B3JkrjwU.mjs} +1 -2
- package/dist/{keys-BqNejWup.mjs → keys-DhqDRxv3.mjs} +1 -2
- package/dist/{logger-Df2O2WsW.mjs → logger-ByrvQWZO.mjs} +1 -2
- package/dist/{memory-cQgelFOj.mjs → memory-B2v7KrCB.mjs} +1 -2
- package/dist/migrations/index.d.mts +1 -2
- package/dist/migrations/index.mjs +1 -2
- package/dist/{mongodb-CGzRbfAK.d.mts → mongodb-ClykrfGo.d.mts} +2 -3
- package/dist/{mongodb-BfJVlUJH.mjs → mongodb-DNKEExbf.mjs} +1 -2
- package/dist/{mongodb-JN-9JA7K.d.mts → mongodb-Dg8O_gvd.d.mts} +2 -3
- package/dist/{openapi-G3Cw7XuM.mjs → openapi-9nB_kiuR.mjs} +5 -4
- package/dist/org/index.d.mts +4 -5
- package/dist/org/index.mjs +1 -2
- package/dist/org/types.d.mts +1 -2
- package/dist/permissions/index.d.mts +5 -6
- package/dist/permissions/index.mjs +7 -7
- package/dist/plugins/index.d.mts +7 -8
- package/dist/plugins/index.mjs +7 -8
- package/dist/plugins/response-cache.d.mts +1 -2
- package/dist/plugins/response-cache.mjs +2 -3
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -2
- package/dist/{pluralize-CEweyOEm.mjs → pluralize-CM-jZg7p.mjs} +1 -2
- package/dist/policies/index.d.mts +4 -5
- package/dist/policies/index.mjs +1 -2
- package/dist/presets/index.d.mts +4 -5
- package/dist/presets/index.mjs +2 -3
- package/dist/presets/multiTenant.d.mts +4 -5
- package/dist/presets/multiTenant.mjs +1 -2
- package/dist/{presets-DzSMwlKj.d.mts → presets-BTeYbw7h.d.mts} +2 -3
- package/dist/{presets-BITljm96.mjs → presets-CeFtfDR8.mjs} +1 -2
- package/dist/prisma-DJbMt3yf.mjs +1 -2
- package/dist/{prisma-Dg9GoVdj.d.mts → prisma-DQBSSHAB.d.mts} +2 -3
- package/dist/{queryCachePlugin-DMBnp2Q0.mjs → queryCachePlugin-B6R0d4av.mjs} +4 -5
- package/dist/{queryCachePlugin-7THaI5mt.d.mts → queryCachePlugin-Q6SYuHZ6.d.mts} +2 -3
- package/dist/{redis-D-JAeLtm.d.mts → redis-UwjEp8Ea.d.mts} +2 -3
- package/dist/{redis-stream-Bdh_vUU8.d.mts → redis-stream-CBg0upHI.d.mts} +2 -3
- package/dist/registry/index.d.mts +4 -5
- package/dist/registry/index.mjs +2 -2
- package/dist/{requestContext-QQD6ROJc.mjs → requestContext-xi6OKBL-.mjs} +1 -2
- package/dist/{schemaConverter-BwrmWroW.mjs → schemaConverter-Dtg0Kt9T.mjs} +1 -2
- package/dist/schemas/index.d.mts +1 -2
- package/dist/schemas/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -3
- package/dist/scope/index.mjs +2 -3
- package/dist/{sessionManager-jPKLbHE0.d.mts → sessionManager-D_iEHjQl.d.mts} +1 -2
- package/dist/{sse-B3c3_yZp.mjs → sse-DkqQ1uxb.mjs} +2 -3
- package/dist/testing/index.d.mts +8 -9
- package/dist/testing/index.mjs +3 -4
- package/dist/{tracing-Cc7vVQPp.d.mts → tracing-8CEbhF0w.d.mts} +1 -2
- package/dist/{typeGuards-DhMNLuvU.mjs → typeGuards-DwxA1t_L.mjs} +1 -2
- package/dist/types/index.d.mts +7 -8
- package/dist/types/index.mjs +1 -2
- package/dist/{types-CIgB7UUl.d.mts → types-B0dhNrnd.d.mts} +9 -10
- package/dist/types-Beqn1Un7.mjs +1 -2
- package/dist/types-DelU6kln.mjs +25 -0
- package/dist/{types-aYB4V7uN.d.mts → types-RLkFVgaw.d.mts} +18 -4
- package/dist/utils/index.d.mts +5 -6
- package/dist/utils/index.mjs +4 -4
- package/package.json +1 -1
- package/dist/EventTransport-BD2U0BTc.d.mts.map +0 -1
- package/dist/HookSystem-BsGV-j2l.mjs.map +0 -1
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +0 -1
- package/dist/audit/index.d.mts.map +0 -1
- package/dist/audit/index.mjs.map +0 -1
- package/dist/audited-C3T5DTUx.mjs.map +0 -1
- package/dist/auth/index.d.mts.map +0 -1
- package/dist/auth/index.mjs.map +0 -1
- package/dist/auth/redis-session.d.mts.map +0 -1
- package/dist/auth/redis-session.mjs.map +0 -1
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +0 -1
- package/dist/cache/index.d.mts.map +0 -1
- package/dist/cache/index.mjs.map +0 -1
- package/dist/caching-Bl28lYsR.mjs.map +0 -1
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +0 -1
- package/dist/cli/commands/describe.d.mts.map +0 -1
- package/dist/cli/commands/describe.mjs.map +0 -1
- package/dist/cli/commands/docs.d.mts.map +0 -1
- package/dist/cli/commands/docs.mjs.map +0 -1
- package/dist/cli/commands/generate.d.mts.map +0 -1
- package/dist/cli/commands/generate.mjs.map +0 -1
- package/dist/cli/commands/init.d.mts.map +0 -1
- package/dist/cli/commands/init.mjs.map +0 -1
- package/dist/cli/commands/introspect.d.mts.map +0 -1
- package/dist/cli/commands/introspect.mjs.map +0 -1
- package/dist/cli/index.d.mts.map +0 -1
- package/dist/cli/index.mjs.map +0 -1
- package/dist/constants-DdXFXQtN.mjs.map +0 -1
- package/dist/createApp-CUgNqegw.mjs.map +0 -1
- package/dist/defineResource-k0_BDn8v.mjs.map +0 -1
- package/dist/discovery/index.d.mts.map +0 -1
- package/dist/discovery/index.mjs.map +0 -1
- package/dist/docs/index.d.mts.map +0 -1
- package/dist/docs/index.mjs.map +0 -1
- package/dist/elevation-BRy3yFWT.mjs.map +0 -1
- package/dist/elevation-B_2dRLVP.d.mts.map +0 -1
- package/dist/errorHandler-BbcgBmIH.d.mts.map +0 -1
- package/dist/errorHandler-C1okiriz.mjs.map +0 -1
- package/dist/errors-B9bZok84.mjs.map +0 -1
- package/dist/errors-ChKiFz62.d.mts.map +0 -1
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +0 -1
- package/dist/eventPlugin-DGR_B2on.mjs.map +0 -1
- package/dist/events/index.d.mts.map +0 -1
- package/dist/events/index.mjs.map +0 -1
- package/dist/events/transports/redis-stream-entry.mjs.map +0 -1
- package/dist/events/transports/redis.d.mts.map +0 -1
- package/dist/events/transports/redis.mjs.map +0 -1
- package/dist/externalPaths-DlINfKbP.d.mts.map +0 -1
- package/dist/factory/index.d.mts.map +0 -1
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +0 -1
- package/dist/fields-DyaDVX4J.d.mts.map +0 -1
- package/dist/fields-iagOozy0.mjs.map +0 -1
- package/dist/idempotency/index.d.mts.map +0 -1
- package/dist/idempotency/index.mjs.map +0 -1
- package/dist/idempotency/mongodb.mjs.map +0 -1
- package/dist/idempotency/redis.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/integrations/event-gateway.d.mts.map +0 -1
- package/dist/integrations/event-gateway.mjs.map +0 -1
- package/dist/integrations/jobs.d.mts.map +0 -1
- package/dist/integrations/jobs.mjs.map +0 -1
- package/dist/integrations/streamline.d.mts.map +0 -1
- package/dist/integrations/streamline.mjs.map +0 -1
- package/dist/integrations/websocket.d.mts.map +0 -1
- package/dist/integrations/websocket.mjs.map +0 -1
- package/dist/interface-B01JvPVc.d.mts.map +0 -1
- package/dist/interface-CZe8IkMf.d.mts.map +0 -1
- package/dist/interface-Ch8HU9uM.d.mts.map +0 -1
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +0 -1
- package/dist/keys-BqNejWup.mjs.map +0 -1
- package/dist/logger-Df2O2WsW.mjs.map +0 -1
- package/dist/memory-cQgelFOj.mjs.map +0 -1
- package/dist/migrations/index.d.mts.map +0 -1
- package/dist/migrations/index.mjs.map +0 -1
- package/dist/mongodb-BfJVlUJH.mjs.map +0 -1
- package/dist/mongodb-CGzRbfAK.d.mts.map +0 -1
- package/dist/mongodb-JN-9JA7K.d.mts.map +0 -1
- package/dist/openapi-G3Cw7XuM.mjs.map +0 -1
- package/dist/org/index.d.mts.map +0 -1
- package/dist/org/index.mjs.map +0 -1
- package/dist/org/types.d.mts.map +0 -1
- package/dist/permissions/index.d.mts.map +0 -1
- package/dist/permissions/index.mjs.map +0 -1
- package/dist/plugins/index.d.mts.map +0 -1
- package/dist/plugins/index.mjs.map +0 -1
- package/dist/plugins/response-cache.d.mts.map +0 -1
- package/dist/plugins/response-cache.mjs.map +0 -1
- package/dist/plugins/tracing-entry.mjs.map +0 -1
- package/dist/pluralize-CEweyOEm.mjs.map +0 -1
- package/dist/policies/index.d.mts.map +0 -1
- package/dist/policies/index.mjs.map +0 -1
- package/dist/presets/index.d.mts.map +0 -1
- package/dist/presets/index.mjs.map +0 -1
- package/dist/presets/multiTenant.d.mts.map +0 -1
- package/dist/presets/multiTenant.mjs.map +0 -1
- package/dist/presets-BITljm96.mjs.map +0 -1
- package/dist/presets-DzSMwlKj.d.mts.map +0 -1
- package/dist/prisma-DJbMt3yf.mjs.map +0 -1
- package/dist/prisma-Dg9GoVdj.d.mts.map +0 -1
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +0 -1
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +0 -1
- package/dist/redis-D-JAeLtm.d.mts.map +0 -1
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +0 -1
- package/dist/registry/index.d.mts.map +0 -1
- package/dist/requestContext-QQD6ROJc.mjs.map +0 -1
- package/dist/schemaConverter-BwrmWroW.mjs.map +0 -1
- package/dist/schemas/index.d.mts.map +0 -1
- package/dist/schemas/index.mjs.map +0 -1
- package/dist/scope/index.d.mts.map +0 -1
- package/dist/scope/index.mjs.map +0 -1
- package/dist/sessionManager-jPKLbHE0.d.mts.map +0 -1
- package/dist/sse-B3c3_yZp.mjs.map +0 -1
- package/dist/testing/index.d.mts.map +0 -1
- package/dist/testing/index.mjs.map +0 -1
- package/dist/tracing-Cc7vVQPp.d.mts.map +0 -1
- package/dist/typeGuards-DhMNLuvU.mjs.map +0 -1
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs.map +0 -1
- package/dist/types-Beqn1Un7.mjs.map +0 -1
- package/dist/types-CIgB7UUl.d.mts.map +0 -1
- package/dist/types-aYB4V7uN.d.mts.map +0 -1
- package/dist/utils/index.d.mts.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors-ChKiFz62.d.mts","names":[],"sources":["../src/utils/errors.ts"],"mappings":";;AAMA;;;;UAAiB,YAAA;EACf,IAAA;EACA,UAAA;EACA,OAAA,GAAU,MAAA;EACV,KAAA,GAAQ,KAAA;EACR,SAAA;AAAA;;;AAgBF;;;;;;;;;;;cAAa,QAAA,SAAiB,KAAA;EACnB,IAAA;EAAA,SACA,IAAA;EAAA,SACA,UAAA;EAAA,SACA,OAAA,GAAU,MAAA;EAAA,SACD,KAAA,GAAQ,KAAA;EAAA,SACjB,SAAA;EACT,SAAA;cAEY,OAAA,UAAiB,OAAA,GAAS,YAAA;;;;EAqBtC,aAAA,CAAc,SAAA;EAAd;;;;EASA,MAAA,CAAA,GAAU,MAAA;AAAA;AAoBZ;;;AAAA,cAAa,aAAA,SAAsB,QAAA;cACrB,QAAA,UAAkB,UAAA;AAAA;;;;cAiBnB,eAAA,SAAwB,QAAA;EAAA,SAC1B,MAAA,EAAQ,KAAA;IAAQ,KAAA;IAAe,OAAA;EAAA;cAGtC,OAAA,UACA,MAAA,GAAQ,KAAA;IAAQ,KAAA;IAAe,OAAA;EAAA;AAAA;;;;cAetB,iBAAA,SAA0B,QAAA;cACzB,OAAA;AAAA;;;;cAYD,cAAA,SAAuB,QAAA;cACtB,OAAA;AAAA;AAdd;;;AAAA,cA0Ba,aAAA,SAAsB,QAAA;cACrB,OAAA,UAAiB,KAAA;AAAA;;;;cAalB,gBAAA,SAAyB,QAAA;EAAA,SAC3B,aAAA,GAAgB,KAAA;IAAQ,EAAA;IAAY,KAAA;EAAA;cAG3C,OAAA,UACA,aAAA,GAAgB,KAAA;IAAQ,EAAA;IAAY,KAAA;EAAA;AAAA;;;;cAe3B,oBAAA,SAA6B,QAAA;cAC5B,KAAA;AAAA;;;;cAaD,cAAA,SAAuB,QAAA;EAAA,SACzB,UAAA;cAEG,OAAA,WAA+B,UAAA;AAAA;;;;cAchC,uBAAA,SAAgC,QAAA;cAC/B,OAAA;AAAA;;;;iBAYE,WAAA,CACd,UAAA,UACA,OAAA,UACA,OAAA,GAAU,MAAA,oBACT,QAAA;;;;iBAsBa,UAAA,CAAW,KAAA,YAAiB,KAAA,IAAS,QAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"eventPlugin-CTrLH3mt.d.mts","names":[],"sources":["../src/events/retry.ts","../src/events/eventPlugin.ts"],"mappings":";;;;UAiCiB,YAAA;EAoDN;;;;EA/CT,UAAA;EA8CA;;;;EAxCA,SAAA;EA0Ca;AA6Ef;;;EAjHE,YAAA;EAkHwD;;;;;EA3GxD,MAAA;EA4GiD;;;;EAtGjD,MAAA,IAAU,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,KAAA,cAAmB,OAAA;EAqGpB;;;EAhGrC,IAAA;EAgGA;;;;EA1FA,MAAA,GAAS,WAAA;AAAA;;;;;;AC1CX;iBDmDgB,SAAA,CACd,OAAA,EAAS,YAAA,EACT,OAAA,GAAS,YAAA,GACR,YAAA;;;;;;;;;;;;;;;;;;;;;;iBA6Ea,yBAAA,CACd,MAAA;EAAU,OAAA,MAAa,IAAA,UAAc,OAAA,EAAS,CAAA,EAAG,IAAA,GAAO,MAAA,sBAA4B,OAAA;AAAA,KAClF,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,KAAA,OAAY,OAAA;;;UCrI3B,kBAAA;ED+BL;EC7BV,SAAA,GAAY,cAAA;ED6BkB;EC3B9B,SAAA;EDgCA;;;;;EC1BA,QAAA;EDyCuB;;;;;ECnCvB,GAAA;IACE,IAAA,GAAO,KAAA,EAAO,WAAA,KAAgB,OAAA;IAC9B,WAAA,IAAe,OAAA,aAAoB,OAAA;EAAA;EDmC5B;;;;EC7BT,KAAA,GAAQ,IAAA,CACN,YAAA;ED0GY;;;;;EClGd,eAAA;IDoGS,gFClGP,KAAA,IAAS,KAAA,EAAO,WAAA,EAAa,MAAA,EAAQ,KAAA,cAAmB,OAAA;EAAA;EDkGT;EC/FjD,SAAA,IAAa,KAAA,EAAO,WAAA;ED8FV;EC5FV,cAAA,IAAkB,KAAA,EAAO,WAAA,EAAa,KAAA,EAAO,KAAA;AAAA;AAAA;EAAA,UAInC,eAAA;IACR,MAAA;MDuF+C,uBCrF7C,OAAA,MACE,IAAA,UACA,OAAA,EAAS,CAAA,EACT,IAAA,GAAO,OAAA,CAAQ,WAAA,cACZ,OAAA,QDiFT;MC/EI,SAAA,GACE,OAAA,UACA,OAAA,EAAS,YAAA,KACN,OAAA,cD6EP;MC3EE,aAAA;IAAA;EAAA;AAAA;AAAA,cAKA,WAAA,EAAa,kBAAA,CAAmB,kBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"eventPlugin-DGR_B2on.mjs","names":[],"sources":["../src/events/EventTransport.ts","../src/events/retry.ts","../src/events/eventPlugin.ts"],"sourcesContent":["/**\n * Event Transport Interface\n *\n * Defines contract for event delivery backends.\n * Implement for durable transports (Redis, RabbitMQ, Kafka, etc.)\n *\n * @example\n * // Redis Pub/Sub implementation\n * class RedisEventTransport implements EventTransport {\n * async publish(event) {\n * await redis.publish(event.type, JSON.stringify(event));\n * }\n * async subscribe(pattern, handler) {\n * redis.psubscribe(pattern);\n * redis.on('pmessage', (p, channel, msg) => handler(JSON.parse(msg)));\n * }\n * }\n */\n\nexport interface DomainEvent<T = unknown> {\n /** Event type (e.g., 'product.created', 'order.shipped') */\n type: string;\n /** Event payload */\n payload: T;\n /** Event metadata */\n meta: {\n /** Unique event ID */\n id: string;\n /** Event timestamp */\n timestamp: Date;\n /** Source resource */\n resource?: string;\n /** Resource ID */\n resourceId?: string;\n /** User who triggered the event */\n userId?: string;\n /** Organization context */\n organizationId?: string;\n /** Correlation ID for tracing */\n correlationId?: string;\n };\n}\n\nexport type EventHandler<T = unknown> = (event: DomainEvent<T>) => void | Promise<void>;\n\n/**\n * Minimal logger interface for event transports.\n * Compatible with `console`, `pino`, `fastify.log`, and any custom logger.\n *\n * @example\n * ```typescript\n * // Use Fastify's logger\n * new MemoryEventTransport({ logger: fastify.log });\n *\n * // Use a custom logger\n * new MemoryEventTransport({ logger: { warn: myWarn, error: myError } });\n *\n * // Default: console (no logger option needed)\n * new MemoryEventTransport();\n * ```\n */\nexport interface EventLogger {\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\nexport interface EventTransport {\n /** Transport name for logging */\n readonly name: string;\n\n /**\n * Publish an event to the transport\n */\n publish(event: DomainEvent): Promise<void>;\n\n /**\n * Subscribe to events matching a pattern\n * @param pattern - Event type pattern (e.g., 'product.*', '*')\n * @param handler - Handler function\n * @returns Unsubscribe function\n */\n subscribe(pattern: string, handler: EventHandler): Promise<() => void>;\n\n /**\n * Close transport connections\n */\n close?(): Promise<void>;\n}\n\nexport interface MemoryEventTransportOptions {\n /** Logger for error/warning messages (default: console) */\n logger?: EventLogger;\n}\n\n/**\n * In-memory event transport (default)\n * Events are delivered synchronously within the process.\n * Not suitable for multi-instance deployments.\n */\nexport class MemoryEventTransport implements EventTransport {\n readonly name = 'memory';\n private handlers = new Map<string, Set<EventHandler>>();\n private logger: EventLogger;\n\n constructor(options?: MemoryEventTransportOptions) {\n this.logger = options?.logger ?? console;\n }\n\n async publish(event: DomainEvent): Promise<void> {\n // Exact match handlers\n const exactHandlers = this.handlers.get(event.type) ?? new Set();\n\n // Wildcard handlers\n const wildcardHandlers = this.handlers.get('*') ?? new Set();\n\n // Pattern match handlers (e.g., 'product.*' matches 'product.created')\n const patternHandlers = new Set<EventHandler>();\n for (const [pattern, handlers] of this.handlers.entries()) {\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2);\n if (event.type.startsWith(prefix + '.')) {\n handlers.forEach((h) => patternHandlers.add(h));\n }\n }\n }\n\n const allHandlers = new Set([...exactHandlers, ...wildcardHandlers, ...patternHandlers]);\n\n // Execute handlers (catch errors to prevent one handler from blocking others)\n for (const handler of allHandlers) {\n try {\n await handler(event);\n } catch (err) {\n this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);\n }\n }\n }\n\n async subscribe(pattern: string, handler: EventHandler): Promise<() => void> {\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, new Set());\n }\n this.handlers.get(pattern)!.add(handler);\n\n return () => {\n this.handlers.get(pattern)?.delete(handler);\n };\n }\n\n async close(): Promise<void> {\n this.handlers.clear();\n }\n}\n\n/**\n * Create a domain event with auto-generated metadata\n */\nexport function createEvent<T>(\n type: string,\n payload: T,\n meta?: Partial<DomainEvent['meta']>\n): DomainEvent<T> {\n return {\n type,\n payload,\n meta: {\n id: crypto.randomUUID(),\n timestamp: new Date(),\n ...meta,\n },\n };\n}\n\nexport default MemoryEventTransport;\n","/**\n * Event Handler Retry with Dead Letter Queue\n *\n * Transport-agnostic retry wrapper for event handlers.\n * Works with any EventTransport (Memory, Redis Pub/Sub, Redis Streams).\n *\n * @example\n * ```typescript\n * import { withRetry } from '@classytic/arc/events';\n *\n * // Retry up to 3 times with exponential backoff\n * await fastify.events.subscribe('order.created', withRetry(\n * async (event) => {\n * await sendConfirmationEmail(event.payload);\n * },\n * { maxRetries: 3, backoffMs: 1000 }\n * ));\n *\n * // With dead letter callback\n * await fastify.events.subscribe('order.created', withRetry(\n * async (event) => { ... },\n * {\n * maxRetries: 3,\n * onDead: async (event, errors) => {\n * await fastify.events.publish('$deadLetter', { event, errors });\n * },\n * }\n * ));\n * ```\n */\n\nimport type { DomainEvent, EventHandler, EventLogger } from './EventTransport.js';\n\nexport interface RetryOptions {\n /**\n * Max retry attempts (not counting the initial attempt).\n * @default 3\n */\n maxRetries?: number;\n\n /**\n * Initial backoff delay in ms. Doubles on each retry (exponential backoff).\n * @default 1000\n */\n backoffMs?: number;\n\n /**\n * Maximum backoff delay in ms (caps exponential growth).\n * @default 30000\n */\n maxBackoffMs?: number;\n\n /**\n * Jitter factor (0-1). Adds randomness to prevent thundering herd.\n * 0 = no jitter, 1 = full jitter (delay ∈ [0, calculated]).\n * @default 0.1\n */\n jitter?: number;\n\n /**\n * Callback when all retries are exhausted. The event is \"dead\".\n * Use this to publish to a `$deadLetter` channel, log, alert, etc.\n */\n onDead?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;\n\n /**\n * Optional name for logging/debugging.\n */\n name?: string;\n\n /**\n * Logger for retry warnings and error messages (default: console).\n * Pass `fastify.log` to integrate with your application logger.\n */\n logger?: EventLogger;\n}\n\n/**\n * Wrap an event handler with retry logic and dead letter support.\n *\n * On failure, retries with exponential backoff (with jitter).\n * After all retries exhausted, calls `onDead` callback if provided.\n */\nexport function withRetry(\n handler: EventHandler,\n options: RetryOptions = {},\n): EventHandler {\n const {\n maxRetries = 3,\n backoffMs = 1000,\n maxBackoffMs = 30_000,\n jitter = 0.1,\n onDead,\n name,\n logger = console,\n } = options;\n\n const label = name ?? handler.name ?? 'anonymous';\n\n return async (event: DomainEvent): Promise<void> => {\n const errors: Error[] = [];\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n await handler(event);\n return; // Success\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n errors.push(error);\n\n if (attempt < maxRetries) {\n // Calculate delay with exponential backoff + jitter\n const baseDelay = Math.min(backoffMs * 2 ** attempt, maxBackoffMs);\n const jitterAmount = jitter * baseDelay * Math.random();\n const delay = baseDelay + jitterAmount;\n\n logger.warn(\n `[Arc Events] Handler '${label}' failed for ${event.type} ` +\n `(attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${Math.round(delay)}ms: ${error.message}`,\n );\n\n await sleep(delay);\n }\n }\n }\n\n // All retries exhausted — event is dead\n logger.error(\n `[Arc Events] Handler '${label}' permanently failed for ${event.type} ` +\n `after ${maxRetries + 1} attempts. ${errors.length} errors.`,\n );\n\n if (onDead) {\n try {\n await onDead(event, errors);\n } catch (dlqErr) {\n logger.error('[Arc Events] Dead letter callback failed:', dlqErr);\n }\n }\n };\n}\n\n/**\n * Create a dead letter publisher that sends failed events to a `$deadLetter` channel.\n *\n * @example\n * ```typescript\n * import { withRetry, createDeadLetterPublisher } from '@classytic/arc/events';\n *\n * const toDlq = createDeadLetterPublisher(fastify.events);\n *\n * await fastify.events.subscribe('order.created', withRetry(handler, {\n * maxRetries: 3,\n * onDead: toDlq,\n * }));\n *\n * // Monitor dead letters\n * await fastify.events.subscribe('$deadLetter', async (event) => {\n * console.error('Dead letter:', event.payload);\n * await alertOps(event.payload);\n * });\n * ```\n */\nexport function createDeadLetterPublisher(\n events: { publish: <T>(type: string, payload: T, meta?: Record<string, unknown>) => Promise<void> },\n): (event: DomainEvent, errors: Error[]) => Promise<void> {\n return async (event: DomainEvent, errors: Error[]) => {\n await events.publish('$deadLetter', {\n originalEvent: event,\n errors: errors.map((e) => ({\n message: e.message,\n stack: e.stack,\n })),\n failedAt: new Date().toISOString(),\n });\n };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Event Plugin\n *\n * Integrates event transport with Fastify.\n * Defaults to in-memory transport; configure durable transport for production.\n *\n * @example\n * // Development (in-memory)\n * await fastify.register(eventPlugin);\n *\n * // Production (Redis)\n * await fastify.register(eventPlugin, {\n * transport: new RedisEventTransport({ url: process.env.REDIS_URL }),\n * });\n */\n\nimport fp from \"fastify-plugin\";\nimport type { FastifyInstance, FastifyPluginAsync } from \"fastify\";\nimport {\n MemoryEventTransport,\n createEvent,\n type EventTransport,\n type DomainEvent,\n type EventHandler,\n} from \"./EventTransport.js\";\nimport {\n withRetry,\n createDeadLetterPublisher,\n type RetryOptions,\n} from \"./retry.js\";\nimport { requestContext } from \"../context/requestContext.js\";\n\nexport interface EventPluginOptions {\n /** Event transport (default: MemoryEventTransport) */\n transport?: EventTransport;\n /** Enable event logging (default: false) */\n logEvents?: boolean;\n /**\n * Fail-open mode for runtime resilience (default: true).\n * - true: publish/subscribe/close errors are logged and suppressed\n * - false: errors are thrown to caller\n */\n failOpen?: boolean;\n /**\n * Write-Ahead Log (WAL) configuration for at-least-once delivery guarantees.\n * If provided, events will be saved to the WAL *before* passing to the transport.\n * After a successful publish, they are acknowledged.\n */\n wal?: {\n save: (event: DomainEvent) => Promise<void>;\n acknowledge?: (eventId: string) => Promise<void>;\n };\n /**\n * Auto-wrap all subscribed handlers with retry logic.\n * When enabled, failed handler invocations are retried with exponential backoff.\n */\n retry?: Pick<\n RetryOptions,\n \"maxRetries\" | \"backoffMs\" | \"maxBackoffMs\" | \"jitter\"\n >;\n /**\n * Dead letter queue for events that exhaust all retries.\n * Requires `retry` to be enabled. If `retry` is set but no custom `store`,\n * failed events are published to the `$deadLetter` event type by default.\n */\n deadLetterQueue?: {\n /** Custom store function. If omitted, publishes to '$deadLetter' event type. */\n store?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;\n };\n /** Callback after successful publish (for metrics/tracking) */\n onPublish?: (event: DomainEvent) => void;\n /** Callback on publish failure (for metrics/alerting) */\n onPublishError?: (event: DomainEvent, error: Error) => void;\n}\n\ndeclare module \"fastify\" {\n interface FastifyInstance {\n events: {\n /** Publish an event */\n publish: <T>(\n type: string,\n payload: T,\n meta?: Partial<DomainEvent[\"meta\"]>,\n ) => Promise<void>;\n /** Subscribe to events */\n subscribe: (\n pattern: string,\n handler: EventHandler,\n ) => Promise<() => void>;\n /** Get transport name */\n transportName: string;\n };\n }\n}\n\nconst eventPlugin: FastifyPluginAsync<EventPluginOptions> = async (\n fastify: FastifyInstance,\n opts: EventPluginOptions = {},\n) => {\n const {\n transport = new MemoryEventTransport(),\n logEvents = false,\n failOpen = true,\n retry: retryOpts,\n deadLetterQueue: dlqOpts,\n wal,\n onPublish,\n onPublishError,\n } = opts;\n\n // Decorate fastify with event utilities\n fastify.decorate(\"events\", {\n publish: async <T>(\n type: string,\n payload: T,\n meta?: Partial<DomainEvent[\"meta\"]>,\n ): Promise<void> => {\n // Auto-inject correlationId from request context if not already set\n const store = requestContext.get();\n const enrichedMeta: Partial<DomainEvent[\"meta\"]> = {\n ...(store?.requestId && !meta?.correlationId\n ? { correlationId: store.requestId }\n : {}),\n ...meta,\n };\n const event = createEvent(type, payload, enrichedMeta);\n\n if (logEvents) {\n fastify.log?.info?.(\n {\n eventType: type,\n eventId: event.meta.id,\n correlationId: event.meta.correlationId,\n },\n \"Publishing event\",\n );\n }\n\n try {\n if (wal) {\n await wal.save(event);\n }\n await transport.publish(event);\n if (wal?.acknowledge) {\n await wal.acknowledge(event.meta.id);\n }\n onPublish?.(event);\n } catch (error) {\n fastify.log?.error?.(\n { transport: transport.name, eventType: type, error },\n \"[Arc Events] Failed to publish event\",\n );\n onPublishError?.(event, error as Error);\n if (!failOpen) throw error;\n }\n },\n\n subscribe: async (\n pattern: string,\n handler: EventHandler,\n ): Promise<() => void> => {\n // Auto-wrap handler with retry if configured (skip for DLQ subscriptions)\n let wrappedHandler = handler;\n if (retryOpts && pattern !== \"$deadLetter\") {\n wrappedHandler = withRetry(handler, {\n ...retryOpts,\n onDead: dlqOpts?.store ?? createDeadLetterPublisher(fastify.events),\n logger: fastify.log as import(\"./EventTransport.js\").EventLogger,\n });\n }\n\n if (logEvents) {\n fastify.log?.info?.(\n { pattern, retry: !!retryOpts },\n \"Subscribing to events\",\n );\n }\n try {\n return await transport.subscribe(pattern, wrappedHandler);\n } catch (error) {\n fastify.log?.error?.(\n { transport: transport.name, pattern, error },\n \"[Arc Events] Failed to subscribe to events\",\n );\n if (!failOpen) throw error;\n return () => {};\n }\n },\n\n transportName: transport.name,\n });\n\n // Cleanup on close\n fastify.addHook(\"onClose\", async () => {\n try {\n await transport.close?.();\n } catch (error) {\n fastify.log?.warn?.(\n { transport: transport.name, error },\n \"[Arc Events] Transport close failed\",\n );\n if (!failOpen) throw error;\n }\n });\n\n // Log transport type\n if (transport.name === \"memory\") {\n fastify.log?.warn?.(\n \"[Arc Events] Using in-memory transport. Events will not persist or scale across instances. \" +\n \"For production, configure a durable transport (Redis, RabbitMQ, etc.)\",\n );\n } else {\n fastify.log?.debug?.(`[Arc Events] Using ${transport.name} transport`);\n }\n};\n\nexport default fp(eventPlugin, {\n name: \"arc-events\",\n fastify: \"5.x\",\n});\n\nexport { eventPlugin };\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/events/eventTypes.ts"],"mappings":";;;;;;;;;;;;AAmBA;;;;;AAGA;;;;;AAWA;;cAda,mBAAA;;KAGD,eAAA,UAAyB,mBAAA;;;;;AAgBrC;;;;;iBALgB,aAAA,CAAc,QAAA,UAAkB,MAAA,EAAQ,eAAA;;cAK3C,oBAAA,EAAoB,QAAA;8GAQJ;EAAA;;;KAAjB,iBAAA,UAA2B,oBAAA,cAAkC,oBAAA;;cAG5D,YAAA,EAAY,QAAA;EAAA;;;;KAQb,UAAA,UAAoB,YAAA,cAA0B,YAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/events/eventTypes.ts"],"sourcesContent":["/**\r\n * Event Type Constants and Helpers\r\n *\r\n * Provides well-typed event names for CRUD operations and lifecycle events.\r\n * Use these instead of hand-typing event strings to prevent typos.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { crudEventType, ARC_LIFECYCLE_EVENTS } from '@classytic/arc/events';\r\n *\r\n * // Subscribe to product creation events\r\n * events.subscribe(crudEventType('product', 'created'), handler);\r\n *\r\n * // Subscribe to Arc lifecycle events\r\n * events.subscribe(ARC_LIFECYCLE_EVENTS.READY, handler);\r\n * ```\r\n */\r\n\r\n/** Suffixes for auto-emitted CRUD events */\r\nexport const CRUD_EVENT_SUFFIXES = Object.freeze(['created', 'updated', 'deleted'] as const);\r\n\r\n/** Type for CRUD event suffixes */\r\nexport type CrudEventSuffix = typeof CRUD_EVENT_SUFFIXES[number];\r\n\r\n/**\r\n * Build a CRUD event type string.\r\n *\r\n * @example\r\n * ```typescript\r\n * crudEventType('product', 'created') // 'product.created'\r\n * crudEventType('order', 'deleted') // 'order.deleted'\r\n * ```\r\n */\r\nexport function crudEventType(resource: string, suffix: CrudEventSuffix): string {\r\n return `${resource}.${suffix}`;\r\n}\r\n\r\n/** Arc framework lifecycle events — emitted automatically by the framework */\r\nexport const ARC_LIFECYCLE_EVENTS = Object.freeze({\r\n /** Emitted when a resource plugin is registered */\r\n RESOURCE_REGISTERED: 'arc.resource.registered',\r\n /** Emitted when Arc is fully ready (all resources registered, onReady fired) */\r\n READY: 'arc.ready',\r\n} as const);\r\n\r\n/** Type for Arc lifecycle event names */\r\nexport type ArcLifecycleEvent = typeof ARC_LIFECYCLE_EVENTS[keyof typeof ARC_LIFECYCLE_EVENTS];\r\n\r\n/** Cache-specific event types for observability and external triggers */\r\nexport const CACHE_EVENTS = Object.freeze({\r\n /** Emitted when a resource's cache version is bumped */\r\n VERSION_BUMPED: 'arc.cache.version.bumped',\r\n /** Emitted when a tag version is bumped */\r\n TAG_VERSION_BUMPED: 'arc.cache.tag.bumped',\r\n} as const);\r\n\r\n/** Type for cache event names */\r\nexport type CacheEvent = typeof CACHE_EVENTS[keyof typeof CACHE_EVENTS];\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmBA,MAAa,sBAAsB,OAAO,OAAO;CAAC;CAAW;CAAW;CAAU,CAAU;;;;;;;;;;AAc5F,SAAgB,cAAc,UAAkB,QAAiC;AAC/E,QAAO,GAAG,SAAS,GAAG;;;AAIxB,MAAa,uBAAuB,OAAO,OAAO;CAEhD,qBAAqB;CAErB,OAAO;CACR,CAAU;;AAMX,MAAa,eAAe,OAAO,OAAO;CAExC,gBAAgB;CAEhB,oBAAoB;CACrB,CAAU"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-stream-entry.mjs","names":[],"sources":["../../../src/events/transports/redis-stream.ts"],"sourcesContent":["/**\n * Redis Stream Event Transport — Durable Event Delivery\n *\n * Uses Redis Streams (`XADD`/`XREADGROUP`) for persistent, exactly-once event\n * delivery across multiple service instances. Unlike Pub/Sub, events are stored\n * in Redis and survive crashes/restarts.\n *\n * Key features:\n * - Consumer groups: each event processed by exactly one consumer per group\n * - Crash recovery: pending entries are auto-claimed after `claimTimeoutMs`\n * - Dead letter stream: events exceeding `maxRetries` are moved to a DLQ\n * - Backpressure: configurable block time and batch size\n *\n * @example\n * ```typescript\n * import { RedisStreamTransport } from '@classytic/arc/events';\n * import Redis from 'ioredis';\n *\n * const transport = new RedisStreamTransport(new Redis(), {\n * stream: 'arc-events',\n * group: 'api-service',\n * consumer: 'worker-1',\n * });\n *\n * await app.register(eventPlugin, { transport });\n * ```\n */\n\nimport type { EventTransport, DomainEvent, EventHandler, EventLogger } from '../EventTransport.js';\n\n// ---------------------------------------------------------------------------\n// Minimal Redis-like interface for Streams support\n// ---------------------------------------------------------------------------\n\nexport interface RedisStreamLike {\n xadd(key: string, id: string, ...fieldValues: string[]): Promise<string | null>;\n xreadgroup(\n command: 'GROUP',\n group: string,\n consumer: string,\n ...args: (string | number)[]\n ): Promise<Array<[string, Array<[string, string[]]>]> | null>;\n xack(key: string, group: string, ...ids: string[]): Promise<number>;\n xgroup(command: string, key: string, group: string, ...args: string[]): Promise<unknown>;\n xpending(\n key: string,\n group: string,\n ...args: (string | number)[]\n ): Promise<Array<[string, string, number, number]>>;\n xclaim(\n key: string,\n group: string,\n consumer: string,\n minIdleTime: number,\n ...ids: string[]\n ): Promise<Array<[string, string[]]>>;\n xlen(key: string): Promise<number>;\n quit(): Promise<unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface RedisStreamTransportOptions {\n /**\n * Redis stream key name.\n * @default 'arc:events'\n */\n stream?: string;\n\n /**\n * Consumer group name. Each group receives every event independently.\n * Multiple instances of the same service should share a group name.\n * @default 'default'\n */\n group?: string;\n\n /**\n * Consumer name within the group. Must be unique per instance.\n * @default 'consumer-<random>'\n */\n consumer?: string;\n\n /**\n * Block time in ms when waiting for new events.\n * @default 5000\n */\n blockTimeMs?: number;\n\n /**\n * Max events to read per batch.\n * @default 10\n */\n batchSize?: number;\n\n /**\n * Max delivery attempts before moving to dead letter stream.\n * @default 5\n */\n maxRetries?: number;\n\n /**\n * Idle time in ms before pending entries are claimed by this consumer.\n * Handles crash recovery — if a consumer dies mid-processing, another\n * consumer will claim its pending entries after this timeout.\n * @default 30000\n */\n claimTimeoutMs?: number;\n\n /**\n * Dead letter stream name. Failed events are moved here after maxRetries.\n * Set to `false` to disable DLQ (failed events are acked and dropped).\n * @default 'arc:events:dlq'\n */\n deadLetterStream?: string | false;\n\n /**\n * Max stream length (approximate). Uses XADD MAXLEN ~ to trim old entries.\n * Set to 0 to disable trimming.\n * @default 10000\n */\n maxLen?: number;\n\n /**\n * Logger for error messages (default: console).\n * Pass `fastify.log` to integrate with your application logger.\n */\n logger?: EventLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Transport\n// ---------------------------------------------------------------------------\n\nexport class RedisStreamTransport implements EventTransport {\n readonly name = 'redis-stream';\n\n private redis: RedisStreamLike;\n private stream: string;\n private group: string;\n private consumer: string;\n private blockTimeMs: number;\n private batchSize: number;\n private maxRetries: number;\n private claimTimeoutMs: number;\n private deadLetterStream: string | false;\n private maxLen: number;\n\n private logger: EventLogger;\n\n private handlers = new Map<string, Set<EventHandler>>();\n private running = false;\n private pollPromise: Promise<void> | null = null;\n private groupCreated = false;\n\n constructor(redis: RedisStreamLike, options: RedisStreamTransportOptions = {}) {\n this.redis = redis;\n this.stream = options.stream ?? 'arc:events';\n this.group = options.group ?? 'default';\n this.consumer = options.consumer ?? `consumer-${crypto.randomUUID().slice(0, 8)}`;\n this.blockTimeMs = options.blockTimeMs ?? 5000;\n this.batchSize = options.batchSize ?? 10;\n this.maxRetries = options.maxRetries ?? 5;\n this.claimTimeoutMs = options.claimTimeoutMs ?? 30_000;\n this.deadLetterStream = options.deadLetterStream ?? 'arc:events:dlq';\n this.maxLen = options.maxLen ?? 10_000;\n this.logger = options.logger ?? console;\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.publish\n // -----------------------------------------------------------------------\n\n async publish(event: DomainEvent): Promise<void> {\n const args: string[] = [\n this.stream,\n ...(this.maxLen > 0 ? ['MAXLEN', '~', String(this.maxLen)] : []),\n '*',\n 'type', event.type,\n 'data', JSON.stringify(event),\n ];\n\n // Use spread to call xadd with dynamic args\n await (this.redis as any).xadd(...args);\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.subscribe\n // -----------------------------------------------------------------------\n\n async subscribe(pattern: string, handler: EventHandler): Promise<() => void> {\n if (!this.handlers.has(pattern)) {\n this.handlers.set(pattern, new Set());\n }\n this.handlers.get(pattern)!.add(handler);\n\n // Start the consumer loop if not already running\n if (!this.running) {\n await this.ensureGroup();\n this.running = true;\n this.pollPromise = this.pollLoop();\n }\n\n return () => {\n const set = this.handlers.get(pattern);\n if (set) {\n set.delete(handler);\n if (set.size === 0) this.handlers.delete(pattern);\n }\n // Stop polling when no handlers remain — prevents CPU/network waste\n if (this.handlers.size === 0 && this.running) {\n this.running = false;\n }\n };\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.close\n // -----------------------------------------------------------------------\n\n async close(): Promise<void> {\n this.running = false;\n this.handlers.clear();\n\n // Wait for the poll loop to finish its current iteration\n if (this.pollPromise) {\n await this.pollPromise;\n this.pollPromise = null;\n }\n }\n\n // -----------------------------------------------------------------------\n // Consumer group management\n // -----------------------------------------------------------------------\n\n private async ensureGroup(): Promise<void> {\n if (this.groupCreated) return;\n\n try {\n // Create the consumer group, starting from new messages ('$')\n // Use MKSTREAM to auto-create the stream if it doesn't exist\n await this.redis.xgroup('CREATE', this.stream, this.group, '$', 'MKSTREAM');\n } catch (err: unknown) {\n // BUSYGROUP = group already exists, which is fine\n if (err instanceof Error && !err.message.includes('BUSYGROUP')) {\n throw err;\n }\n }\n\n this.groupCreated = true;\n }\n\n // -----------------------------------------------------------------------\n // Poll loop — reads new messages + claims pending (crash recovery)\n // -----------------------------------------------------------------------\n\n private async pollLoop(): Promise<void> {\n while (this.running) {\n try {\n // Phase 1: Claim pending entries from dead consumers (crash recovery)\n await this.claimPending();\n\n // Phase 2: Read new messages\n await this.readNewMessages();\n } catch (err) {\n if (this.running) {\n this.logger.error('[RedisStreamTransport] Poll error:', err);\n // Brief pause before retrying on error\n await this.sleep(1000);\n }\n }\n }\n }\n\n private async readNewMessages(): Promise<void> {\n // XREADGROUP GROUP <group> <consumer> COUNT <n> BLOCK <ms> STREAMS <key> >\n const result = await this.redis.xreadgroup(\n 'GROUP', this.group, this.consumer,\n 'COUNT', this.batchSize,\n 'BLOCK', this.blockTimeMs,\n 'STREAMS', this.stream, '>',\n );\n\n if (!result) return; // Timeout, no new messages\n\n for (const [, entries] of result) {\n for (const [messageId, fields] of entries) {\n await this.processEntry(messageId, fields);\n }\n }\n }\n\n private async claimPending(): Promise<void> {\n try {\n // Check for pending entries across all consumers that have been idle\n const pending = await this.redis.xpending(\n this.stream, this.group,\n '-', '+', 10, // Check up to 10 pending entries\n );\n\n if (!pending || pending.length === 0) return;\n\n const staleIds: string[] = [];\n const overRetryIds: string[] = [];\n\n for (const entry of pending) {\n const [id, , idleTime, deliveryCount] = entry;\n if (idleTime > this.claimTimeoutMs) {\n if (deliveryCount >= this.maxRetries) {\n overRetryIds.push(id);\n } else {\n staleIds.push(id);\n }\n }\n }\n\n // Move over-retry entries to DLQ\n if (overRetryIds.length > 0) {\n await this.moveToDlq(overRetryIds);\n }\n\n // Claim stale entries\n if (staleIds.length > 0) {\n const claimed = await this.redis.xclaim(\n this.stream, this.group, this.consumer,\n this.claimTimeoutMs, ...staleIds,\n );\n\n for (const [messageId, fields] of claimed) {\n await this.processEntry(messageId, fields);\n }\n }\n } catch {\n // Pending check failures are non-fatal — will retry next iteration\n }\n }\n\n // -----------------------------------------------------------------------\n // Message processing\n // -----------------------------------------------------------------------\n\n private async processEntry(messageId: string, fields: string[]): Promise<void> {\n // Parse fields array into key-value pairs\n const fieldMap = new Map<string, string>();\n for (let i = 0; i < fields.length; i += 2) {\n fieldMap.set(fields[i]!, fields[i + 1]!);\n }\n\n const eventType = fieldMap.get('type');\n const rawData = fieldMap.get('data');\n if (!eventType || !rawData) {\n // Malformed entry — ack and skip\n await this.redis.xack(this.stream, this.group, messageId);\n return;\n }\n\n let event: DomainEvent;\n try {\n event = JSON.parse(rawData, (key, value) => {\n if (key === 'timestamp' && typeof value === 'string') return new Date(value);\n return value;\n }) as DomainEvent;\n } catch {\n // Unparseable — ack and skip\n await this.redis.xack(this.stream, this.group, messageId);\n return;\n }\n\n // Dispatch to matching handlers\n const matchingHandlers = this.getMatchingHandlers(event.type);\n let allSucceeded = true;\n\n for (const handler of matchingHandlers) {\n try {\n await handler(event);\n } catch (err) {\n allSucceeded = false;\n this.logger.error(`[RedisStreamTransport] Handler error for ${event.type}:`, err);\n }\n }\n\n if (allSucceeded) {\n await this.redis.xack(this.stream, this.group, messageId);\n }\n // If not all succeeded, leave unacked — will be retried via pending claim\n }\n\n private getMatchingHandlers(eventType: string): EventHandler[] {\n const matched: EventHandler[] = [];\n\n for (const [pattern, handlers] of this.handlers) {\n if (this.matchesPattern(pattern, eventType)) {\n for (const h of handlers) {\n matched.push(h);\n }\n }\n }\n\n return matched;\n }\n\n private matchesPattern(pattern: string, eventType: string): boolean {\n if (pattern === '*') return true;\n if (pattern === eventType) return true;\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2);\n return eventType.startsWith(prefix + '.');\n }\n return false;\n }\n\n // -----------------------------------------------------------------------\n // Dead letter queue\n // -----------------------------------------------------------------------\n\n private async moveToDlq(ids: string[]): Promise<void> {\n if (this.deadLetterStream === false) {\n // DLQ disabled — just ack and drop\n for (const id of ids) {\n await this.redis.xack(this.stream, this.group, id);\n }\n return;\n }\n\n // Read the entries, write to DLQ stream, then ack\n for (const id of ids) {\n try {\n await (this.redis as any).xadd(\n this.deadLetterStream,\n '*',\n 'originalStream', this.stream,\n 'originalId', id,\n 'group', this.group,\n 'failedAt', new Date().toISOString(),\n );\n await this.redis.xack(this.stream, this.group, id);\n } catch (err) {\n this.logger.error(`[RedisStreamTransport] DLQ write failed for ${id}:`, err);\n }\n }\n }\n\n // -----------------------------------------------------------------------\n // Utility\n // -----------------------------------------------------------------------\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\nexport default RedisStreamTransport;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis.d.mts","names":[],"sources":["../../../src/events/transports/redis.ts"],"mappings":";;;UAwBiB,SAAA;EACf,OAAA,CAAQ,OAAA,UAAiB,OAAA,WAAkB,OAAA;EAC3C,SAAA,IAAa,QAAA,aAAqB,OAAA;EAClC,UAAA,IAAc,QAAA,aAAqB,OAAA;EACnC,EAAA,CAAG,KAAA,UAAe,OAAA,MAAa,IAAA;EAC/B,SAAA,IAAa,SAAA;EACb,IAAA,IAAQ,OAAA;AAAA;AAAA,UAOO,0BAAA;EATgB;;;;;EAe/B,OAAA;EAbe;;AAOjB;;;EAaE,iBAAA;EAPA;;;;EAaA,MAAA,GAAS,WAAA;AAAA;AAAA,cA8BE,mBAAA,YAA+B,cAAA;EAAA,SACjC,IAAA;;UAGD,GAAA;EAoB+B;EAAA,QAjB/B,GAAA;EAmC2B;EAAA,QAhC3B,OAAA;EAyCiD;EAAA,QAtCjD,iBAAA;EAbkC;EAAA,QAgBlC,MAAA;EAhBgD;EAAA,QAmBhD,QAAA;EAlBC;EAAA,QAqBD,gBAAA;cAEI,KAAA,EAAO,SAAA,EAAW,OAAA,GAAS,0BAAA;EAkBjC,OAAA,CAAQ,KAAA,EAAO,WAAA,GAAc,OAAA;EAS7B,SAAA,CAAU,OAAA,UAAiB,OAAA,EAAS,YAAA,GAAe,OAAA;EAmCnD,KAAA,CAAA,GAAS,OAAA;EAnEP;;;EAAA,QAyFA,cAAA;EApFI;;;EAAA,QAwGJ,QAAA;EAtFa;;;;;;;;;;EAAA,QA6Hb,cAAA;EAvCA;;;EAAA,QA8CA,MAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis.mjs","names":[],"sources":["../../../src/events/transports/redis.ts"],"sourcesContent":["/**\n * Redis Event Transport\n *\n * Uses Redis Pub/Sub for cross-process event delivery.\n * Optional dependency — only imported when explicitly used.\n *\n * @example\n * import { RedisEventTransport } from '@classytic/arc/events/redis';\n * import Redis from 'ioredis';\n *\n * const redis = new Redis();\n * const transport = new RedisEventTransport(redis, { channel: 'arc-events' });\n *\n * await app.register(eventPlugin, { transport });\n */\n\nimport type { EventTransport, DomainEvent, EventHandler, EventLogger } from '../EventTransport.js';\n\n// ---------------------------------------------------------------------------\n// Minimal Redis-like interface so consumers don't need ioredis at type level.\n// Any Redis client implementing these methods (ioredis, node-redis wrapper,\n// etc.) will work.\n// ---------------------------------------------------------------------------\n\nexport interface RedisLike {\n publish(channel: string, message: string): Promise<number>;\n subscribe(...channels: string[]): Promise<unknown>;\n psubscribe(...patterns: string[]): Promise<unknown>;\n on(event: string, handler: (...args: unknown[]) => void): unknown;\n duplicate(): RedisLike;\n quit(): Promise<unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface RedisEventTransportOptions {\n /**\n * Redis channel prefix for all events.\n * Events are published to `<channel>:<event.type>`.\n * @default 'arc-events'\n */\n channel?: string;\n\n /**\n * If `true`, the transport will NOT call `quit()` on the Redis clients when\n * `close()` is called. Useful when you manage the Redis lifecycle externally.\n * @default false\n */\n externalLifecycle?: boolean;\n\n /**\n * Logger for error messages (default: console).\n * Pass `fastify.log` to integrate with your application logger.\n */\n logger?: EventLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Serialization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * DomainEvent contains a `Date` in `meta.timestamp`. We serialize it as an\n * ISO string and revive it on the other end so subscribers always receive a\n * proper Date object.\n */\nfunction serialize(event: DomainEvent): string {\n return JSON.stringify(event, (_key, value) => {\n if (value instanceof Date) return value.toISOString();\n return value;\n });\n}\n\nfunction deserialize(raw: string): DomainEvent {\n return JSON.parse(raw, (key, value) => {\n if (key === 'timestamp' && typeof value === 'string') return new Date(value);\n return value;\n }) as DomainEvent;\n}\n\n// ---------------------------------------------------------------------------\n// Transport\n// ---------------------------------------------------------------------------\n\nexport class RedisEventTransport implements EventTransport {\n readonly name = 'redis';\n\n /** Publish-side client (the original client or a duplicate). */\n private pub: RedisLike;\n\n /** Subscribe-side client (always a duplicate — ioredis requires a dedicated connection for subscriptions). */\n private sub: RedisLike;\n\n /** Channel prefix. */\n private channel: string;\n\n /** Whether we own the Redis client lifecycle. */\n private externalLifecycle: boolean;\n\n /** Logger for error messages. */\n private logger: EventLogger;\n\n /** Registered handlers keyed by their *Redis* pattern (with channel prefix). */\n private handlers = new Map<string, Set<EventHandler>>();\n\n /** Tracks whether the pmessage listener has been attached. */\n private listenerAttached = false;\n\n constructor(redis: RedisLike, options: RedisEventTransportOptions = {}) {\n const { channel = 'arc-events', externalLifecycle = false, logger = console } = options;\n\n this.channel = channel;\n this.externalLifecycle = externalLifecycle;\n this.logger = logger;\n\n // Use the provided client for publishing, create a dedicated duplicate for subscribing.\n // ioredis requires separate connections for pub and sub because a client in\n // subscriber mode cannot issue regular commands.\n this.pub = redis;\n this.sub = redis.duplicate();\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.publish\n // -----------------------------------------------------------------------\n\n async publish(event: DomainEvent): Promise<void> {\n const redisChannel = `${this.channel}:${event.type}`;\n await this.pub.publish(redisChannel, serialize(event));\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.subscribe\n // -----------------------------------------------------------------------\n\n async subscribe(pattern: string, handler: EventHandler): Promise<() => void> {\n this.ensureListener();\n\n const redisPattern = this.toRedisPattern(pattern);\n\n if (!this.handlers.has(redisPattern)) {\n this.handlers.set(redisPattern, new Set());\n\n // Decide between exact SUBSCRIBE or pattern PSUBSCRIBE.\n if (this.isGlob(redisPattern)) {\n await this.sub.psubscribe(redisPattern);\n } else {\n await this.sub.subscribe(redisPattern);\n }\n }\n\n this.handlers.get(redisPattern)!.add(handler);\n\n // Return unsubscribe function.\n return () => {\n const set = this.handlers.get(redisPattern);\n if (set) {\n set.delete(handler);\n // Note: we intentionally do NOT punsubscribe/unsubscribe from Redis\n // when the last handler for a pattern is removed. This keeps the\n // implementation simple and avoids race conditions. The pattern stays\n // active until close() is called.\n }\n };\n }\n\n // -----------------------------------------------------------------------\n // EventTransport.close\n // -----------------------------------------------------------------------\n\n async close(): Promise<void> {\n this.handlers.clear();\n\n if (!this.externalLifecycle) {\n // The subscriber connection is always a duplicate we created, so we\n // always quit it. The publisher is the client the user passed in, so\n // we only quit it when externalLifecycle is false.\n await Promise.all([this.sub.quit(), this.pub.quit()]);\n } else {\n // Even with external lifecycle we quit the subscriber duplicate since we\n // created it ourselves.\n await this.sub.quit();\n }\n }\n\n // -----------------------------------------------------------------------\n // Internal helpers\n // -----------------------------------------------------------------------\n\n /**\n * Attach the Redis message listeners exactly once.\n */\n private ensureListener(): void {\n if (this.listenerAttached) return;\n this.listenerAttached = true;\n\n // Pattern-matched messages (from psubscribe)\n this.sub.on('pmessage', (...args: unknown[]) => {\n const [redisPattern, , message] = args as [string, string, string];\n this.dispatch(redisPattern, message);\n });\n\n // Exact-matched messages (from subscribe)\n this.sub.on('message', (...args: unknown[]) => {\n const [channel, message] = args as [string, string];\n this.dispatch(channel, message);\n });\n }\n\n /**\n * Dispatch an incoming Redis message to all registered handlers.\n */\n private dispatch(redisPatternOrChannel: string, raw: string): void {\n const handlers = this.handlers.get(redisPatternOrChannel);\n if (!handlers || handlers.size === 0) return;\n\n let event: DomainEvent;\n try {\n event = deserialize(raw);\n } catch {\n // Ignore malformed messages — they may come from non-Arc publishers\n // sharing the same Redis instance.\n return;\n }\n\n for (const handler of handlers) {\n try {\n const result = handler(event);\n // If the handler returns a promise, catch rejections so one failing\n // handler doesn't prevent the rest from executing.\n if (result && typeof (result as Promise<void>).catch === 'function') {\n (result as Promise<void>).catch((err) => {\n this.logger.error(`[RedisEventTransport] Handler error for ${event.type}:`, err);\n });\n }\n } catch (err) {\n this.logger.error(`[RedisEventTransport] Handler error for ${event.type}:`, err);\n }\n }\n }\n\n /**\n * Convert an Arc event pattern to a Redis channel/pattern string.\n *\n * Arc patterns use `*` as a single-segment wildcard (e.g., `product.*`).\n * Redis PSUBSCRIBE uses the same glob syntax, so we just prepend the\n * channel prefix.\n *\n * Special case: bare `*` means \"all events\", which maps to\n * `<channel>:*` in Redis.\n */\n private toRedisPattern(pattern: string): string {\n return `${this.channel}:${pattern}`;\n }\n\n /**\n * Returns true if the pattern contains glob characters.\n */\n private isGlob(pattern: string): boolean {\n return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');\n }\n}\n\nexport default RedisEventTransport;\n"],"mappings":";;;;;;AAoEA,SAAS,UAAU,OAA4B;AAC7C,QAAO,KAAK,UAAU,QAAQ,MAAM,UAAU;AAC5C,MAAI,iBAAiB,KAAM,QAAO,MAAM,aAAa;AACrD,SAAO;GACP;;AAGJ,SAAS,YAAY,KAA0B;AAC7C,QAAO,KAAK,MAAM,MAAM,KAAK,UAAU;AACrC,MAAI,QAAQ,eAAe,OAAO,UAAU,SAAU,QAAO,IAAI,KAAK,MAAM;AAC5E,SAAO;GACP;;AAOJ,IAAa,sBAAb,MAA2D;CACzD,AAAS,OAAO;;CAGhB,AAAQ;;CAGR,AAAQ;;CAGR,AAAQ;;CAGR,AAAQ;;CAGR,AAAQ;;CAGR,AAAQ,2BAAW,IAAI,KAAgC;;CAGvD,AAAQ,mBAAmB;CAE3B,YAAY,OAAkB,UAAsC,EAAE,EAAE;EACtE,MAAM,EAAE,UAAU,cAAc,oBAAoB,OAAO,SAAS,YAAY;AAEhF,OAAK,UAAU;AACf,OAAK,oBAAoB;AACzB,OAAK,SAAS;AAKd,OAAK,MAAM;AACX,OAAK,MAAM,MAAM,WAAW;;CAO9B,MAAM,QAAQ,OAAmC;EAC/C,MAAM,eAAe,GAAG,KAAK,QAAQ,GAAG,MAAM;AAC9C,QAAM,KAAK,IAAI,QAAQ,cAAc,UAAU,MAAM,CAAC;;CAOxD,MAAM,UAAU,SAAiB,SAA4C;AAC3E,OAAK,gBAAgB;EAErB,MAAM,eAAe,KAAK,eAAe,QAAQ;AAEjD,MAAI,CAAC,KAAK,SAAS,IAAI,aAAa,EAAE;AACpC,QAAK,SAAS,IAAI,8BAAc,IAAI,KAAK,CAAC;AAG1C,OAAI,KAAK,OAAO,aAAa,CAC3B,OAAM,KAAK,IAAI,WAAW,aAAa;OAEvC,OAAM,KAAK,IAAI,UAAU,aAAa;;AAI1C,OAAK,SAAS,IAAI,aAAa,CAAE,IAAI,QAAQ;AAG7C,eAAa;GACX,MAAM,MAAM,KAAK,SAAS,IAAI,aAAa;AAC3C,OAAI,IACF,KAAI,OAAO,QAAQ;;;CAazB,MAAM,QAAuB;AAC3B,OAAK,SAAS,OAAO;AAErB,MAAI,CAAC,KAAK,kBAIR,OAAM,QAAQ,IAAI,CAAC,KAAK,IAAI,MAAM,EAAE,KAAK,IAAI,MAAM,CAAC,CAAC;MAIrD,OAAM,KAAK,IAAI,MAAM;;;;;CAWzB,AAAQ,iBAAuB;AAC7B,MAAI,KAAK,iBAAkB;AAC3B,OAAK,mBAAmB;AAGxB,OAAK,IAAI,GAAG,aAAa,GAAG,SAAoB;GAC9C,MAAM,CAAC,gBAAgB,WAAW;AAClC,QAAK,SAAS,cAAc,QAAQ;IACpC;AAGF,OAAK,IAAI,GAAG,YAAY,GAAG,SAAoB;GAC7C,MAAM,CAAC,SAAS,WAAW;AAC3B,QAAK,SAAS,SAAS,QAAQ;IAC/B;;;;;CAMJ,AAAQ,SAAS,uBAA+B,KAAmB;EACjE,MAAM,WAAW,KAAK,SAAS,IAAI,sBAAsB;AACzD,MAAI,CAAC,YAAY,SAAS,SAAS,EAAG;EAEtC,IAAI;AACJ,MAAI;AACF,WAAQ,YAAY,IAAI;UAClB;AAGN;;AAGF,OAAK,MAAM,WAAW,SACpB,KAAI;GACF,MAAM,SAAS,QAAQ,MAAM;AAG7B,OAAI,UAAU,OAAQ,OAAyB,UAAU,WACvD,CAAC,OAAyB,OAAO,QAAQ;AACvC,SAAK,OAAO,MAAM,2CAA2C,MAAM,KAAK,IAAI,IAAI;KAChF;WAEG,KAAK;AACZ,QAAK,OAAO,MAAM,2CAA2C,MAAM,KAAK,IAAI,IAAI;;;;;;;;;;;;;CAetF,AAAQ,eAAe,SAAyB;AAC9C,SAAO,GAAG,KAAK,QAAQ,GAAG;;;;;CAM5B,AAAQ,OAAO,SAA0B;AACvC,SAAO,QAAQ,SAAS,IAAI,IAAI,QAAQ,SAAS,IAAI,IAAI,QAAQ,SAAS,IAAI"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"externalPaths-DlINfKbP.d.mts","names":[],"sources":["../src/docs/externalPaths.ts"],"mappings":";;AA0BA;;;;;;;;;;;;;;;;;;;;;;;;UAAiB,oBAAA;EAQA;EANf,KAAA,EAAO,MAAA,SAAe,MAAA;EAetB;EAbA,OAAA,GAAU,MAAA,SAAe,MAAA;EAaA;EAXzB,eAAA,GAAkB,MAAA,SAAe,MAAA;EAWF;EAT/B,IAAA,GAAO,KAAA;IAAQ,IAAA;IAAc,WAAA;EAAA;;;;;;;;;EAS7B,gBAAA,GAAmB,KAAA,CAAM,MAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/factory/createApp.ts","../../src/factory/presets.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;iBAkHsB,SAAA,CAAU,OAAA,EAAS,gBAAA,GAAmB,OAAA,CAAQ,eAAA;;;;cAievD,UAAA;EAkBsE;;;sBAdvD,IAAA,CAAK,gBAAA,cAA8B,OAAA,CAAQ,eAAA;ECzkB1D;;;uBDglBgB,IAAA,CAAK,gBAAA,cAA8B,OAAA,CAAQ,eAAA;EChlBjB;AA8DvD;;mBDyhByB,IAAA,CAAK,gBAAA,cAA8B,OAAA,CAAQ,eAAA;AAAA;;;;AAnfpE;;cCpGa,gBAAA,EAAkB,OAAA,CAAQ,gBAAA;;;;cA8D1B,iBAAA,EAAmB,OAAA,CAAQ,gBAAA;;;;cA6C3B,aAAA,EAAe,OAAA,CAAQ,gBAAA;;;;iBAiEpB,SAAA,CAAU,IAAA,sDAA0D,OAAA,CAAQ,gBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fastifyAdapter-BkrGrlFi.d.mts","names":[],"sources":["../src/core/createCrudRouter.ts","../src/core/createActionRouter.ts","../src/constants.ts","../src/core/fastifyAdapter.ts"],"mappings":";;;;;;;;;;;;;;iBA4WgB,gBAAA,gBAAA,CACd,OAAA,EAAS,qBAAA,EACT,UAAA,EAAY,cAAA,CAAe,IAAA,eAC3B,OAAA,GAAS,iBAAA;;;;AAmOX;iBAAgB,0BAAA,CACd,UAAA,EAAY,eAAA,EACZ,YAAA,UACA,MAAA,WACC,kBAAA;;;;;;;;;;KCjiBS,aAAA,gCACV,EAAA,UACA,IAAA,EAAM,KAAA,EACN,GAAA,EAAK,iBAAA,KACF,OAAA,CAAQ,OAAA;;;;UAKI,kBAAA;EALZ;;;EASH,GAAA;EAJe;;;;EAUf,OAAA,EAAS,MAAA,SAAe,aAAA;EAMW;;;;EAAnC,iBAAA,GAAoB,MAAA,SAAe,eAAA;EAiBd;;;;EAXrB,aAAA,GAAgB,MAAA,SAAe,MAAA;EAZ/B;;;EAiBA,UAAA,GAAa,eAAA;EAXO;;;;EAiBpB,kBAAA,GAAqB,kBAAA;EANrB;;;;;;;EAeA,OAAA,IACE,KAAA,EAAO,KAAA,EACP,MAAA,UACA,EAAA;IACK,UAAA;IAAoB,KAAA;IAAe,IAAA;EAAA;AAAA;;AAO5C;;;UAAiB,kBAAA;EACf,KAAA,CAAM,GAAA,UAAa,OAAA,QAAe,OAAA;IAAU,KAAA;IAAgB,cAAA;EAAA;EAC5D,QAAA,CAAS,GAAA,sBAAyB,MAAA,QAAc,OAAA;EAChD,IAAA,CAAK,GAAA,sBAAyB,KAAA,EAAO,KAAA,GAAQ,OAAA;AAAA;;;;;;;;;;iBAY/B,kBAAA,CAAmB,OAAA,EAAS,eAAA,EAAiB,MAAA,EAAQ,kBAAA;;;;;;;;;;AD0OrE;;;;cE5Va,eAAA;AAAA,KAGD,aAAA,WAAwB,eAAA;;cAGvB,mBAAA;AAAA,KAGD,iBAAA,WAA4B,mBAAA;;cAO3B,WAAA;AAAA,KAGD,SAAA,WAAoB,WAAA;;cAGnB,eAAA;AAAA,KAGD,aAAA,WAAwB,eAAA;;cAOvB,aAAA;;cAGA,iBAAA;AF+hBb;AAAA,cE5hBa,YAAA;;cAOA,gBAAA;;cAGA,oBAAA;;cAGA,qBAAA;;cAGA,aAAA;;cASA,gBAAA;;cAGA,iBAAA;AD7Bb;AAAA,cCgCa,gBAAA;;;;;cAUA,qBAAA,EAAqB,QAAA,CAAA,GAAA;;;;;;;;iBCAlB,oBAAA,CAAqB,GAAA,EAAK,cAAA,GAAiB,eAAA;;;;;;;iBA8D3C,oBAAA,CAAqB,GAAA,EAAK,eAAA,GAAkB,cAAA;;;;;iBAQ5C,kBAAA,CAAmB,GAAA,EAAK,eAAA,GAAkB,YAAA;;;;;;;iBAyC1C,sBAAA,GAAA,CACd,KAAA,EAAO,YAAA,EACP,QAAA,EAAU,mBAAA,CAAoB,CAAA,GAC9B,OAAA,GAAU,cAAA;;;;;;;AF5JZ;;;;;;;;;;iBEoQgB,oBAAA,GAAA,CACd,gBAAA,GAAmB,GAAA,EAAK,eAAA,KAAoB,OAAA,CAAQ,mBAAA,CAAoB,CAAA,MAE1D,GAAA,EAAK,cAAA,EAAgB,KAAA,EAAO,YAAA,KAAe,OAAA;;;;;;;;;;AF9P3D;;;;;;;;iBEsRgB,kBAAA,MAAA,CAAyB,UAAA,EAAY,WAAA,CAAY,IAAA;cAxB5C,cAAA,EAAc,KAAA,EAAS,YAAA,KAAe,OAAA;aAAtC,cAAA,EAAc,KAAA,EAAS,YAAA,KAAe,OAAA;gBAAtC,cAAA,EAAc,KAAA,EAAS,YAAA,KAAe,OAAA;gBAAtC,cAAA,EAAc,KAAA,EAAS,YAAA,KAAe,OAAA;gBAAtC,cAAA,EAAc,KAAA,EAAS,YAAA,KAAe,OAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fields-DyaDVX4J.d.mts","names":[],"sources":["../src/permissions/fields.ts"],"mappings":";;AAgCA;;;;;AAEA;;;;;;;;;;AAMA;;;;;AAMA;KAdY,mBAAA;AAAA,UAEK,eAAA;EAAA,SACN,KAAA,EAAO,mBAAA;EAAA,SACP,KAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGC,kBAAA,GAAqB,MAAA,SAAe,eAAA;AAAA,cAMnC,MAAA;;;;;;;;;YASD,eAAA;;;;;;AAgEZ;;;;uCAnDuC,eAAA;EAqDnB;;;;;;;;;;wCAvCoB,eAAA;EAyCrC;;;AAgDH;;;;;;;;;;;;sCAtEoC,WAAA,aAAiC,eAAA;AAAA;;;;;;AA8GrE;;;;iBA5FgB,yBAAA,WAAoC,MAAA,kBAAA,CAClD,IAAA,EAAM,CAAA,EACN,gBAAA,EAAkB,kBAAA,EAClB,SAAA,sBACC,CAAA;;;;;;;;;;iBAgDa,0BAAA,WAAqC,MAAA,kBAAA,CACnD,IAAA,EAAM,CAAA,EACN,gBAAA,EAAkB,kBAAA,EAClB,SAAA,sBACC,CAAA;;;;;;;;;iBAoCa,qBAAA,CACd,SAAA,qBACA,QAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fields-iagOozy0.mjs","names":[],"sources":["../src/permissions/fields.ts"],"sourcesContent":["/**\n * Field-Level Permissions\n *\n * Control field visibility and writability per role.\n * Integrated into the response path (read) and sanitization path (write).\n *\n * @example\n * ```typescript\n * import { fields, defineResource } from '@classytic/arc';\n *\n * const userResource = defineResource({\n * name: 'user',\n * adapter: userAdapter,\n * fields: {\n * salary: fields.visibleTo(['admin', 'hr']),\n * internalNotes: fields.writableBy(['admin']),\n * email: fields.redactFor(['viewer']),\n * password: fields.hidden(),\n * },\n * });\n * ```\n */\n\n/** Type guard for Mongoose-like documents with toObject() */\nfunction isMongooseDoc(obj: unknown): obj is { toObject(): Record<string, unknown> } {\n return !!obj && typeof obj === 'object' && 'toObject' in obj && typeof (obj as Record<string, unknown>).toObject === 'function';\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type FieldPermissionType = 'hidden' | 'visibleTo' | 'writableBy' | 'redactFor';\n\nexport interface FieldPermission {\n readonly _type: FieldPermissionType;\n readonly roles?: readonly string[];\n readonly redactValue?: unknown;\n}\n\nexport type FieldPermissionMap = Record<string, FieldPermission>;\n\n// ---------------------------------------------------------------------------\n// Field Permission Helpers\n// ---------------------------------------------------------------------------\n\nexport const fields = {\n /**\n * Field is never included in responses. Not writable via API.\n *\n * @example\n * ```typescript\n * fields: { password: fields.hidden() }\n * ```\n */\n hidden(): FieldPermission {\n return { _type: 'hidden' };\n },\n\n /**\n * Field is only visible to users with specified roles.\n * Other users don't see the field at all.\n *\n * @example\n * ```typescript\n * fields: { salary: fields.visibleTo(['admin', 'hr']) }\n * ```\n */\n visibleTo(roles: readonly string[]): FieldPermission {\n return { _type: 'visibleTo', roles };\n },\n\n /**\n * Field is only writable by users with specified roles.\n * All users can still read the field. Users without the role\n * have the field silently stripped from write operations.\n *\n * @example\n * ```typescript\n * fields: { role: fields.writableBy(['admin']) }\n * ```\n */\n writableBy(roles: readonly string[]): FieldPermission {\n return { _type: 'writableBy', roles };\n },\n\n /**\n * Field is redacted (replaced with a placeholder) for specified roles.\n * Other users see the real value.\n *\n * @param roles - Roles that see the redacted value\n * @param redactValue - Replacement value (default: '***')\n *\n * @example\n * ```typescript\n * fields: {\n * email: fields.redactFor(['viewer']),\n * ssn: fields.redactFor(['basic'], '***-**-****'),\n * }\n * ```\n */\n redactFor(roles: readonly string[], redactValue: unknown = '***'): FieldPermission {\n return { _type: 'redactFor', roles, redactValue };\n },\n};\n\n// ---------------------------------------------------------------------------\n// Application Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Apply field-level READ permissions to a response object.\n * Strips hidden fields, enforces visibility, and applies redaction.\n *\n * @param data - The response object (mutated in place for performance)\n * @param fieldPermissions - Field permission map from resource config\n * @param userRoles - Current user's roles (empty array for unauthenticated)\n * @returns The filtered object\n */\nexport function applyFieldReadPermissions<T extends Record<string, unknown>>(\n data: T,\n fieldPermissions: FieldPermissionMap,\n userRoles: readonly string[],\n): T {\n if (!data || typeof data !== 'object') return data;\n\n // Normalize Mongoose documents to plain objects before spreading.\n // HydratedDocument's spread gives internal properties ($__, $isNew, etc.),\n // not the actual document fields — toObject() returns a proper plain object.\n const plain = isMongooseDoc(data) ? data.toObject() as T : data;\n const result = { ...plain };\n\n for (const [field, perm] of Object.entries(fieldPermissions)) {\n switch (perm._type) {\n case 'hidden':\n // Always strip\n delete result[field];\n break;\n\n case 'visibleTo':\n // Strip if user doesn't have any of the required roles\n if (!perm.roles?.some((r) => userRoles.includes(r))) {\n delete result[field];\n }\n break;\n\n case 'redactFor':\n // Redact if user HAS any of the specified roles\n if (perm.roles?.some((r) => userRoles.includes(r))) {\n (result as Record<string, unknown>)[field] = perm.redactValue ?? '***';\n }\n break;\n\n case 'writableBy':\n // Write-only permission — no effect on reads\n break;\n }\n }\n\n return result;\n}\n\n/**\n * Apply field-level WRITE permissions to request body.\n * Strips fields that the user doesn't have permission to write.\n *\n * @param body - The request body (returns a new filtered copy)\n * @param fieldPermissions - Field permission map from resource config\n * @param userRoles - Current user's roles\n * @returns Filtered body\n */\nexport function applyFieldWritePermissions<T extends Record<string, unknown>>(\n body: T,\n fieldPermissions: FieldPermissionMap,\n userRoles: readonly string[],\n): T {\n const result = { ...body };\n\n for (const [field, perm] of Object.entries(fieldPermissions)) {\n switch (perm._type) {\n case 'hidden':\n // Hidden fields can never be written\n delete result[field];\n break;\n\n case 'writableBy':\n // Only writable by specific roles — strip if user lacks them\n if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {\n delete result[field];\n }\n break;\n\n // visibleTo and redactFor don't affect writes\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Role Resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve effective roles by merging global user roles with org-level roles.\n *\n * Global roles come from `req.user.roles` (Better Auth user object).\n * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).\n *\n * When no org context exists, returns global roles only — backward compatible.\n */\nexport function resolveEffectiveRoles(\n userRoles: readonly string[],\n orgRoles: readonly string[],\n): string[] {\n if (orgRoles.length === 0) return [...userRoles];\n if (userRoles.length === 0) return [...orgRoles];\n return [...new Set([...userRoles, ...orgRoles])];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAS,cAAc,KAA8D;AACnF,QAAO,CAAC,CAAC,OAAO,OAAO,QAAQ,YAAY,cAAc,OAAO,OAAQ,IAAgC,aAAa;;AAqBvH,MAAa,SAAS;CASpB,SAA0B;AACxB,SAAO,EAAE,OAAO,UAAU;;CAY5B,UAAU,OAA2C;AACnD,SAAO;GAAE,OAAO;GAAa;GAAO;;CAatC,WAAW,OAA2C;AACpD,SAAO;GAAE,OAAO;GAAc;GAAO;;CAkBvC,UAAU,OAA0B,cAAuB,OAAwB;AACjF,SAAO;GAAE,OAAO;GAAa;GAAO;GAAa;;CAEpD;;;;;;;;;;AAeD,SAAgB,0BACd,MACA,kBACA,WACG;AACH,KAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;CAM9C,MAAM,SAAS,EAAE,GADH,cAAc,KAAK,GAAG,KAAK,UAAU,GAAQ,MAChC;AAE3B,MAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,iBAAiB,CAC1D,SAAQ,KAAK,OAAb;EACE,KAAK;AAEH,UAAO,OAAO;AACd;EAEF,KAAK;AAEH,OAAI,CAAC,KAAK,OAAO,MAAM,MAAM,UAAU,SAAS,EAAE,CAAC,CACjD,QAAO,OAAO;AAEhB;EAEF,KAAK;AAEH,OAAI,KAAK,OAAO,MAAM,MAAM,UAAU,SAAS,EAAE,CAAC,CAChD,CAAC,OAAmC,SAAS,KAAK,eAAe;AAEnE;EAEF,KAAK,aAEH;;AAIN,QAAO;;;;;;;;;;;AAYT,SAAgB,2BACd,MACA,kBACA,WACG;CACH,MAAM,SAAS,EAAE,GAAG,MAAM;AAE1B,MAAK,MAAM,CAAC,OAAO,SAAS,OAAO,QAAQ,iBAAiB,CAC1D,SAAQ,KAAK,OAAb;EACE,KAAK;AAEH,UAAO,OAAO;AACd;EAEF,KAAK;AAEH,OAAI,SAAS,UAAU,CAAC,KAAK,OAAO,MAAM,MAAM,UAAU,SAAS,EAAE,CAAC,CACpE,QAAO,OAAO;AAEhB;;AAMN,QAAO;;;;;;;;;;AAeT,SAAgB,sBACd,WACA,UACU;AACV,KAAI,SAAS,WAAW,EAAG,QAAO,CAAC,GAAG,UAAU;AAChD,KAAI,UAAU,WAAW,EAAG,QAAO,CAAC,GAAG,SAAS;AAChD,QAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,WAAW,GAAG,SAAS,CAAC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/idempotency/idempotencyPlugin.ts","../../src/idempotency/stores/memory.ts"],"mappings":";;;;;;UA6CiB,wBAAA;EA0Bb;EAxBF,OAAA;EA6BU;EA3BV,UAAA;EA+BI;EA7BJ,KAAA;EA6BiC;EA3BjC,aAAA;EA6BU;EA3BV,OAAA;EAyCI;EAvCJ,OAAA,GAAU,MAAA;EAuCO;EArCjB,OAAA,GAAU,MAAA;EAqCgC;EAnC1C,KAAA,GAAQ,gBAAA;EAmCiE;EAjCzE,iBAAA;AAAA;AAAA;EAAA,UAIU,cAAA;IAqCgC;IAnCxC,cAAA;IAmCgE;IAjChE,mBAAA;;IAEA,mBAAA;EAAA;EAAA,UAGQ,eAAA;;IAER,WAAA;mDAEE,UAAA,GAAa,GAAA,aAAgB,OAAA,QCjEW;MDmExC,GAAA,GAAM,GAAA,aAAgB,OAAA;MCnEkB;;;;;;AAS9C;;;;;;;MDwEM,UAAA,GAAa,OAAA,EAAS,cAAA,EAAgB,KAAA,EAAO,YAAA,KAAiB,OAAA;IAAA;EAAA;AAAA;AAAA,cAQ9D,iBAAA,EAAmB,kBAAA,CAAmB,wBAAA;AAAA,cAAwB,QAAA;;;UCzFnD,6BAAA;EDgCf;EC9BA,KAAA;EDkCA;EChCA,iBAAA;EDoCA;EClCA,UAAA;AAAA;AAAA,cAGW,sBAAA,YAAkC,gBAAA;EAAA,SACpC,IAAA;EAAA,QACD,OAAA;EAAA,QACA,KAAA;EAAA,QACA,KAAA;EAAA,QACA,UAAA;EAAA,QACA,eAAA;cAEI,OAAA,GAAS,6BAAA;EAgBf,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,iBAAA;EAa1B,GAAA,CAAI,GAAA,UAAa,MAAA,EAAQ,IAAA,CAAK,iBAAA,WAA4B,OAAA;EAS1D,OAAA,CAAQ,GAAA,UAAa,SAAA,UAAmB,KAAA,WAAgB,OAAA;EAwBxD,MAAA,CAAO,GAAA,UAAa,SAAA,WAAoB,OAAA;EAOxC,QAAA,CAAS,GAAA,WAAc,OAAA;EAavB,MAAA,CAAO,GAAA,WAAc,OAAA;EAKrB,cAAA,CAAe,MAAA,WAAiB,OAAA;EAgBhC,YAAA,CAAa,MAAA,WAAiB,OAAA,CAAQ,iBAAA;EActC,KAAA,CAAA,GAAS,OAAA;EDlFL;EC4FV,QAAA,CAAA;IAAc,OAAA;IAAiB,KAAA;EAAA;EAAA,QAOvB,OAAA;EAAA,QAgBA,WAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/idempotency/stores/interface.ts","../../src/idempotency/stores/memory.ts","../../src/idempotency/idempotencyPlugin.ts"],"sourcesContent":["/**\n * Idempotency Store Interface\n *\n * Defines the contract for idempotency key storage backends.\n * Implement this interface for custom stores (Redis, DynamoDB, etc.)\n */\n\nexport interface IdempotencyResult {\n /** The idempotency key */\n key: string;\n /** HTTP status code of the cached response */\n statusCode: number;\n /** Response headers to replay */\n headers: Record<string, string>;\n /** Response body */\n body: unknown;\n /** When this entry was created */\n createdAt: Date;\n /** When this entry expires */\n expiresAt: Date;\n}\n\nexport interface IdempotencyLock {\n /** The idempotency key being locked */\n key: string;\n /** Request ID that holds the lock */\n requestId: string;\n /** When the lock was acquired */\n lockedAt: Date;\n /** When the lock expires (auto-release) */\n expiresAt: Date;\n}\n\nexport interface IdempotencyStore {\n /** Store name for logging */\n readonly name: string;\n\n /**\n * Get a cached result for an idempotency key.\n * Returns undefined if not found or expired.\n */\n get(key: string): Promise<IdempotencyResult | undefined>;\n\n /**\n * Store a result for an idempotency key.\n * TTL is handled by the store implementation.\n */\n set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void>;\n\n /**\n * Try to acquire a lock for processing a key.\n * Returns true if lock acquired, false if already locked.\n */\n tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;\n\n /** Release a lock after processing complete */\n unlock(key: string, requestId: string): Promise<void>;\n\n /** Check if a key is currently locked */\n isLocked(key: string): Promise<boolean>;\n\n /** Delete a cached result by exact key */\n delete(key: string): Promise<void>;\n\n /**\n * Delete all cached results whose key starts with the given prefix.\n * Used by invalidate() to clear entries by raw idempotency key\n * regardless of fingerprint.\n * Returns the number of entries deleted.\n */\n deleteByPrefix(prefix: string): Promise<number>;\n\n /**\n * Find the first cached result whose key starts with the given prefix.\n * Used by has() to check if any entry exists for a raw idempotency key.\n * Returns undefined if no matching entry found.\n */\n findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;\n\n /** Close the store (cleanup connections) */\n close?(): Promise<void>;\n}\n\n/**\n * Helper to create a result object\n */\nexport function createIdempotencyResult(\n statusCode: number,\n body: unknown,\n headers: Record<string, string>,\n ttlMs: number\n): Omit<IdempotencyResult, 'key'> {\n const now = new Date();\n return {\n statusCode,\n headers,\n body,\n createdAt: now,\n expiresAt: new Date(now.getTime() + ttlMs),\n };\n}\n","/**\n * In-Memory Idempotency Store\n *\n * Default store for development and small deployments.\n * NOT suitable for multi-instance deployments - use Redis or similar.\n *\n * Features:\n * - Automatic TTL expiration\n * - Lock support for concurrent request handling\n * - Periodic cleanup of expired entries\n * - Prefix-based operations for raw key invalidation\n */\n\nimport type { IdempotencyResult, IdempotencyStore, IdempotencyLock } from './interface.js';\n\nexport interface MemoryIdempotencyStoreOptions {\n /** Default TTL in milliseconds (default: 86400000 = 24h) */\n ttlMs?: number;\n /** Cleanup interval in milliseconds (default: 60000 = 1 min) */\n cleanupIntervalMs?: number;\n /** Maximum entries before oldest are evicted (default: 10000) */\n maxEntries?: number;\n}\n\nexport class MemoryIdempotencyStore implements IdempotencyStore {\n readonly name = 'memory';\n private results: Map<string, IdempotencyResult> = new Map();\n private locks: Map<string, IdempotencyLock> = new Map();\n private ttlMs: number;\n private maxEntries: number;\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n\n constructor(options: MemoryIdempotencyStoreOptions = {}) {\n this.ttlMs = options.ttlMs ?? 86400000; // 24 hours\n this.maxEntries = options.maxEntries ?? 10000;\n\n // Start cleanup timer\n const cleanupIntervalMs = options.cleanupIntervalMs ?? 60000;\n this.cleanupInterval = setInterval(() => {\n this.cleanup();\n }, cleanupIntervalMs);\n\n // Don't keep Node process alive just for cleanup\n if (this.cleanupInterval.unref) {\n this.cleanupInterval.unref();\n }\n }\n\n async get(key: string): Promise<IdempotencyResult | undefined> {\n const result = this.results.get(key);\n if (!result) return undefined;\n\n // Check expiration\n if (new Date() > result.expiresAt) {\n this.results.delete(key);\n return undefined;\n }\n\n return result;\n }\n\n async set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void> {\n // Evict oldest if at capacity\n if (this.results.size >= this.maxEntries) {\n this.evictOldest();\n }\n\n this.results.set(key, { ...result, key });\n }\n\n async tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean> {\n const existing = this.locks.get(key);\n\n if (existing) {\n // Check if lock expired\n if (new Date() > existing.expiresAt) {\n this.locks.delete(key);\n } else {\n // Lock held by someone else\n return false;\n }\n }\n\n // Acquire lock\n this.locks.set(key, {\n key,\n requestId,\n lockedAt: new Date(),\n expiresAt: new Date(Date.now() + ttlMs),\n });\n\n return true;\n }\n\n async unlock(key: string, requestId: string): Promise<void> {\n const lock = this.locks.get(key);\n if (lock && lock.requestId === requestId) {\n this.locks.delete(key);\n }\n }\n\n async isLocked(key: string): Promise<boolean> {\n const lock = this.locks.get(key);\n if (!lock) return false;\n\n // Check if expired\n if (new Date() > lock.expiresAt) {\n this.locks.delete(key);\n return false;\n }\n\n return true;\n }\n\n async delete(key: string): Promise<void> {\n this.results.delete(key);\n this.locks.delete(key);\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n let count = 0;\n for (const key of this.results.keys()) {\n if (key.startsWith(prefix)) {\n this.results.delete(key);\n count++;\n }\n }\n for (const key of this.locks.keys()) {\n if (key.startsWith(prefix)) {\n this.locks.delete(key);\n }\n }\n return count;\n }\n\n async findByPrefix(prefix: string): Promise<IdempotencyResult | undefined> {\n const now = new Date();\n for (const [key, result] of this.results) {\n if (key.startsWith(prefix)) {\n if (now > result.expiresAt) {\n this.results.delete(key);\n continue;\n }\n return result;\n }\n }\n return undefined;\n }\n\n async close(): Promise<void> {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.results.clear();\n this.locks.clear();\n }\n\n /** Get current stats (for debugging/monitoring) */\n getStats(): { results: number; locks: number } {\n return {\n results: this.results.size,\n locks: this.locks.size,\n };\n }\n\n private cleanup(): void {\n const now = new Date();\n\n for (const [key, result] of this.results) {\n if (now > result.expiresAt) {\n this.results.delete(key);\n }\n }\n\n for (const [key, lock] of this.locks) {\n if (now > lock.expiresAt) {\n this.locks.delete(key);\n }\n }\n }\n\n private evictOldest(): void {\n const entries = Array.from(this.results.entries())\n .sort((a, b) => a[1].createdAt.getTime() - b[1].createdAt.getTime());\n\n const toRemove = Math.max(1, Math.floor(entries.length * 0.1));\n for (let i = 0; i < toRemove; i++) {\n const entry = entries[i];\n if (entry) {\n this.results.delete(entry[0]);\n }\n }\n }\n}\n\nexport default MemoryIdempotencyStore;\n","/**\n * Idempotency Plugin\n *\n * Duplicate request protection for mutating operations.\n * Uses idempotency keys to ensure safe retries.\n *\n * ## Auth Safety\n *\n * The idempotency check runs as a **route-level middleware**\n * (`idempotency.middleware`) that must be wired AFTER authentication in the\n * preHandler chain. This ensures the fingerprint includes the real caller\n * identity, preventing cross-user replay attacks.\n *\n * Arc's `createCrudRouter` does this automatically for mutation routes.\n * For custom routes, wire it manually:\n *\n * ```typescript\n * fastify.post('/orders', {\n * preHandler: [fastify.authenticate, fastify.idempotency.middleware],\n * }, handler);\n * ```\n *\n * @example\n * import { idempotencyPlugin } from '@classytic/arc/idempotency';\n *\n * await fastify.register(idempotencyPlugin, {\n * enabled: true,\n * headerName: 'idempotency-key',\n * ttlMs: 86400000, // 24 hours\n * });\n *\n * // Client sends:\n * // POST /api/orders\n * // Idempotency-Key: order-123-abc\n *\n * // If same key sent again within TTL, returns cached response\n */\n\nimport fp from 'fastify-plugin';\nimport { createHash } from 'crypto';\nimport type { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';\nimport type { IdempotencyStore } from './stores/interface.js';\nimport { createIdempotencyResult } from './stores/interface.js';\nimport { MemoryIdempotencyStore } from './stores/memory.js';\n\nexport interface IdempotencyPluginOptions {\n /** Enable idempotency (default: false) */\n enabled?: boolean;\n /** Header name for idempotency key (default: 'idempotency-key') */\n headerName?: string;\n /** TTL for cached responses in ms (default: 86400000 = 24h) */\n ttlMs?: number;\n /** Lock timeout in ms (default: 30000 = 30s) */\n lockTimeoutMs?: number;\n /** HTTP methods to apply idempotency to (default: ['POST', 'PUT', 'PATCH']) */\n methods?: string[];\n /** URL patterns to include (regex). If set, only matching URLs use idempotency */\n include?: RegExp[];\n /** URL patterns to exclude (regex). Excluded patterns take precedence */\n exclude?: RegExp[];\n /** Custom store (default: MemoryIdempotencyStore) */\n store?: IdempotencyStore;\n /** Retry-After header value in seconds when request is in-flight (default: 1) */\n retryAfterSeconds?: number;\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** The idempotency key for this request (if present) */\n idempotencyKey?: string;\n /** Whether this response was replayed from cache */\n idempotencyReplayed?: boolean;\n /** @internal Full key with fingerprint for store lookups */\n _idempotencyFullKey?: string;\n }\n\n interface FastifyInstance {\n /** Idempotency utilities */\n idempotency: {\n /** Manually invalidate an idempotency key */\n invalidate: (key: string) => Promise<void>;\n /** Check if a key has a cached response */\n has: (key: string) => Promise<boolean>;\n /**\n * Route-level preHandler for idempotency check + lock.\n * Wire AFTER authenticate in the preHandler chain so that\n * `request.user` is populated before the fingerprint is computed.\n *\n * `createCrudRouter` injects this automatically for mutation routes.\n * For custom routes, add it manually:\n * ```typescript\n * fastify.post('/orders', {\n * preHandler: [fastify.authenticate, fastify.idempotency.middleware],\n * }, handler);\n * ```\n */\n middleware: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n };\n }\n}\n\nconst HEADER_IDEMPOTENCY_REPLAYED = 'x-idempotency-replayed';\nconst HEADER_IDEMPOTENCY_KEY = 'x-idempotency-key';\n\nconst idempotencyPlugin: FastifyPluginAsync<IdempotencyPluginOptions> = async (\n fastify: FastifyInstance,\n opts: IdempotencyPluginOptions = {}\n) => {\n const {\n enabled = false,\n headerName = 'idempotency-key',\n ttlMs = 86400000, // 24 hours\n lockTimeoutMs = 30000, // 30 seconds\n methods = ['POST', 'PUT', 'PATCH'],\n include,\n exclude,\n store = new MemoryIdempotencyStore({ ttlMs }),\n retryAfterSeconds = 1,\n } = opts;\n\n // Skip if not enabled\n if (!enabled) {\n // Provide no-op utilities\n fastify.decorate('idempotency', {\n invalidate: async () => {},\n has: async () => false,\n middleware: async () => {},\n });\n fastify.decorateRequest('idempotencyKey', undefined);\n fastify.decorateRequest('idempotencyReplayed', false);\n fastify.log?.debug?.('Idempotency plugin disabled');\n return;\n }\n\n const methodSet = new Set(methods.map((m) => m.toUpperCase()));\n\n fastify.decorateRequest('idempotencyKey', undefined);\n fastify.decorateRequest('idempotencyReplayed', false);\n\n /**\n * Check if this request should use idempotency\n */\n function shouldApplyIdempotency(request: FastifyRequest): boolean {\n // Check method\n if (!methodSet.has(request.method)) {\n return false;\n }\n\n const url = request.url;\n\n // Check exclusions first (take precedence)\n if (exclude?.some((pattern) => pattern.test(url))) {\n return false;\n }\n\n // Check inclusions (if specified, only matching URLs apply)\n if (include && !include.some((pattern) => pattern.test(url))) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Normalize body for consistent hashing (sort keys recursively)\n */\n function normalizeBody(obj: unknown): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map(normalizeBody);\n }\n\n // Sort object keys\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(obj).sort();\n for (const key of keys) {\n sorted[key] = normalizeBody((obj as Record<string, unknown>)[key]);\n }\n return sorted;\n }\n\n /**\n * Generate a fingerprint for the request (for key generation).\n * Includes caller identity so the same idempotency key from different\n * users doesn't replay one user's response to another.\n *\n * IMPORTANT: This must be called AFTER auth has populated request.user,\n * otherwise userId falls back to 'anon' and cross-user replay is possible.\n */\n function getRequestFingerprint(request: FastifyRequest): string {\n // Combine method + URL + body hash + user identity for uniqueness\n let bodyHash = 'nobody';\n\n if (request.body && typeof request.body === 'object') {\n // Normalize body (sort keys) for consistent hashing\n const normalized = normalizeBody(request.body);\n const bodyString = JSON.stringify(normalized);\n bodyHash = createHash('sha256').update(bodyString).digest('hex').substring(0, 16);\n // SECURITY: Only log hash, never log full body (can contain secrets)\n if (request.log && request.log.debug) {\n request.log.debug({ bodyHash }, 'Generated body hash');\n }\n }\n\n // Scope to caller identity to prevent cross-user replay\n const user = request.user as { id?: string; _id?: string } | undefined;\n const userId = user?.id ?? user?._id ?? 'anon';\n\n const fingerprint = `${request.method}:${request.url}:${bodyHash}:u=${userId}`;\n return fingerprint;\n }\n\n // ---- Route-level middleware: check + lock (AFTER auth) ----\n const idempotencyMiddleware = async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n if (!shouldApplyIdempotency(request)) {\n return;\n }\n\n // Get idempotency key from header\n const keyHeader = request.headers[headerName.toLowerCase()];\n const idempotencyKey = typeof keyHeader === 'string' ? keyHeader.trim() : undefined;\n\n if (!idempotencyKey) {\n // No key provided - proceed normally\n return;\n }\n\n // Store key on request for later use\n request.idempotencyKey = idempotencyKey;\n\n // Create full key with request fingerprint (user is now populated by auth)\n const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;\n\n // Check for cached result\n const cached = await store.get(fullKey);\n if (cached) {\n // Replay cached response\n request.idempotencyReplayed = true;\n\n // Set response headers\n reply.header(HEADER_IDEMPOTENCY_REPLAYED, 'true');\n reply.header(HEADER_IDEMPOTENCY_KEY, idempotencyKey);\n\n // Replay original headers\n for (const [key, value] of Object.entries(cached.headers)) {\n if (!key.startsWith('x-idempotency')) {\n reply.header(key, value);\n }\n }\n\n reply.code(cached.statusCode).send(cached.body);\n return;\n }\n\n // Try to acquire lock\n const lockAcquired = await store.tryLock(fullKey, request.id, lockTimeoutMs);\n if (!lockAcquired) {\n // Another request is processing this key\n reply\n .code(409)\n .header('Retry-After', retryAfterSeconds.toString())\n .send({\n error: 'Request with this idempotency key is already in progress',\n code: 'IDEMPOTENCY_CONFLICT',\n retryAfter: retryAfterSeconds,\n });\n return;\n }\n\n // Store full key for onSend hook\n request._idempotencyFullKey = fullKey;\n };\n\n // Decorate with utilities + middleware\n fastify.decorate('idempotency', {\n invalidate: async (key: string) => {\n // Delete all entries for this raw idempotency key regardless of fingerprint\n await store.deleteByPrefix(`${key}:`);\n },\n has: async (key: string) => {\n // Check if any entry exists for this raw idempotency key\n const result = await store.findByPrefix(`${key}:`);\n return !!result;\n },\n middleware: idempotencyMiddleware,\n });\n\n // Store response after successful request\n fastify.addHook('onSend', async (request, reply, payload) => {\n // Skip if this was a replayed response\n if (request.idempotencyReplayed) {\n return payload;\n }\n\n const fullKey = request._idempotencyFullKey;\n if (!fullKey) {\n return payload;\n }\n\n // Only cache successful responses (2xx)\n const statusCode = reply.statusCode;\n if (statusCode < 200 || statusCode >= 300) {\n // Unlock without caching\n await store.unlock(fullKey, request.id);\n return payload;\n }\n\n // Extract headers to cache (exclude certain headers)\n const headersToCache: Record<string, string> = {};\n const excludeHeaders = new Set([\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'date',\n 'set-cookie',\n ]);\n\n const rawHeaders = reply.getHeaders();\n for (const [key, value] of Object.entries(rawHeaders)) {\n if (!excludeHeaders.has(key.toLowerCase()) && typeof value === 'string') {\n headersToCache[key] = value;\n }\n }\n\n // Parse body if it's a string\n let body: unknown;\n try {\n body = typeof payload === 'string' ? JSON.parse(payload) : payload;\n } catch {\n body = payload;\n }\n\n // Store the result\n const result = createIdempotencyResult(statusCode, body, headersToCache, ttlMs);\n await store.set(fullKey, result);\n\n // Unlock (result is now cached)\n await store.unlock(fullKey, request.id);\n\n // Add idempotency key header to response\n reply.header(HEADER_IDEMPOTENCY_KEY, request.idempotencyKey);\n\n return payload;\n });\n\n // Handle errors - ensure lock is released\n fastify.addHook('onError', async (request) => {\n const fullKey = request._idempotencyFullKey;\n if (fullKey) {\n await store.unlock(fullKey, request.id);\n }\n });\n\n // Cleanup on close\n fastify.addHook('onClose', async () => {\n await store.close?.();\n });\n\n fastify.log?.debug?.({ headerName, ttlMs, methods }, 'Idempotency plugin enabled');\n};\n\nexport default fp(idempotencyPlugin, {\n name: 'arc-idempotency',\n fastify: '5.x',\n});\n\nexport { idempotencyPlugin };\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mongodb.mjs","names":[],"sources":["../../src/idempotency/stores/mongodb.ts"],"sourcesContent":["/**\n * MongoDB Idempotency Store\n *\n * Durable idempotency store using MongoDB.\n * Suitable for multi-instance deployments.\n *\n * @example\n * import mongoose from 'mongoose';\n * import { MongoIdempotencyStore } from '@classytic/arc/idempotency';\n *\n * await fastify.register(idempotencyPlugin, {\n * store: new MongoIdempotencyStore({\n * connection: mongoose.connection,\n * collection: 'idempotency_keys',\n * }),\n * });\n */\n\nimport type { IdempotencyStore, IdempotencyResult } from './interface.js';\n\nexport interface MongoConnection {\n db: {\n collection(name: string): MongoCollection;\n };\n}\n\ninterface MongoCollection {\n findOne(filter: object): Promise<IdempotencyDocument | null>;\n insertOne(doc: object): Promise<{ acknowledged: boolean }>;\n updateOne(filter: object, update: object, options?: object): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;\n deleteOne(filter: object): Promise<{ deletedCount: number }>;\n deleteMany(filter: object): Promise<{ deletedCount: number }>;\n createIndex(spec: object, options?: object): Promise<string>;\n}\n\ninterface IdempotencyDocument {\n _id: string;\n result?: {\n statusCode: number;\n headers: Record<string, string>;\n body: unknown;\n };\n lock?: {\n requestId: string;\n expiresAt: Date;\n };\n createdAt: Date;\n expiresAt: Date;\n}\n\nexport interface MongoIdempotencyStoreOptions {\n /** Mongoose connection or MongoDB connection object */\n connection: MongoConnection;\n /** Collection name (default: 'arc_idempotency') */\n collection?: string;\n /** Create TTL index on startup (default: true) */\n createIndex?: boolean;\n /** Default TTL in ms (default: 86400000 = 24 hours) */\n ttlMs?: number;\n}\n\nexport class MongoIdempotencyStore implements IdempotencyStore {\n readonly name = 'mongodb';\n private connection: MongoConnection;\n private collectionName: string;\n private ttlMs: number;\n private indexCreated = false;\n\n constructor(options: MongoIdempotencyStoreOptions) {\n this.connection = options.connection;\n this.collectionName = options.collection ?? 'arc_idempotency';\n this.ttlMs = options.ttlMs ?? 86400000;\n\n if (options.createIndex !== false) {\n this.ensureIndex().catch((err) => {\n console.warn('[MongoIdempotencyStore] Failed to create index:', err);\n });\n }\n }\n\n private get collection(): MongoCollection {\n return this.connection.db.collection(this.collectionName);\n }\n\n private async ensureIndex(): Promise<void> {\n if (this.indexCreated) return;\n try {\n // TTL index for automatic cleanup\n await this.collection.createIndex(\n { expiresAt: 1 },\n { expireAfterSeconds: 0 }\n );\n this.indexCreated = true;\n } catch {\n // Index might already exist\n this.indexCreated = true;\n }\n }\n\n async get(key: string): Promise<IdempotencyResult | undefined> {\n const doc = await this.collection.findOne({ _id: key });\n if (!doc || !doc.result) return undefined;\n\n // Check expiration\n if (new Date(doc.expiresAt) < new Date()) {\n return undefined;\n }\n\n return {\n key,\n statusCode: doc.result.statusCode,\n headers: doc.result.headers,\n body: doc.result.body,\n createdAt: new Date(doc.createdAt),\n expiresAt: new Date(doc.expiresAt),\n };\n }\n\n async set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void> {\n await this.collection.updateOne(\n { _id: key },\n {\n $set: {\n result: {\n statusCode: result.statusCode,\n headers: result.headers,\n body: result.body,\n },\n createdAt: result.createdAt,\n expiresAt: result.expiresAt,\n },\n $unset: { lock: '' },\n },\n { upsert: true }\n );\n }\n\n async tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean> {\n const now = new Date();\n const expiresAt = new Date(now.getTime() + ttlMs);\n\n try {\n // Atomic upsert: acquire lock only when no active lock exists.\n // Uses a single updateOne with upsert to avoid TOCTOU races —\n // matchedCount/upsertedCount tells us if WE acquired the lock\n // (not just that MongoDB acknowledged the command).\n const result = await this.collection.updateOne(\n {\n _id: key,\n $or: [\n { lock: { $exists: false } },\n { 'lock.expiresAt': { $lt: now } },\n ],\n },\n {\n $set: {\n lock: { requestId, expiresAt },\n },\n $setOnInsert: {\n createdAt: now,\n expiresAt: new Date(now.getTime() + this.ttlMs),\n },\n },\n { upsert: true },\n );\n\n // matchedCount === 1: existing doc matched (lock was free/expired)\n // modifiedCount can be 0 if the $set value is identical — use matchedCount\n // upsertedCount === 1 (implied by acknowledged + matchedCount === 0):\n // new doc created with our lock\n return result.matchedCount === 1 || result.modifiedCount === 1;\n } catch {\n // Duplicate key on upsert race or other error → someone else got the lock\n return false;\n }\n }\n\n async unlock(key: string, requestId: string): Promise<void> {\n // Only unlock if we hold the lock\n await this.collection.updateOne(\n { _id: key, 'lock.requestId': requestId },\n { $unset: { lock: '' } }\n );\n }\n\n async isLocked(key: string): Promise<boolean> {\n const doc = await this.collection.findOne({ _id: key });\n if (!doc || !doc.lock) return false;\n return new Date(doc.lock.expiresAt) > new Date();\n }\n\n async delete(key: string): Promise<void> {\n await this.collection.deleteOne({ _id: key });\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n const result = await this.collection.deleteMany({\n _id: { $regex: `^${prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}` },\n });\n return result.deletedCount;\n }\n\n async findByPrefix(prefix: string): Promise<IdempotencyResult | undefined> {\n // Filter expired docs at the query level so MongoDB doesn't return a stale\n // entry when a valid one exists further down the index.\n const doc = await this.collection.findOne({\n _id: { $regex: `^${prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}` },\n result: { $exists: true },\n expiresAt: { $gt: new Date() },\n });\n if (!doc || !doc.result) return undefined;\n\n return {\n key: doc._id,\n statusCode: doc.result.statusCode,\n headers: doc.result.headers,\n body: doc.result.body,\n createdAt: new Date(doc.createdAt),\n expiresAt: new Date(doc.expiresAt),\n };\n }\n\n async close(): Promise<void> {\n // Don't close the connection - it's passed in and may be shared\n }\n}\n\nexport default MongoIdempotencyStore;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis.mjs","names":[],"sources":["../../src/idempotency/stores/redis.ts"],"sourcesContent":["/**\n * Redis Idempotency Store\n *\n * Durable idempotency store using Redis.\n * Suitable for multi-instance deployments.\n *\n * @example\n * import { createClient } from 'redis';\n * import { RedisIdempotencyStore } from '@classytic/arc/idempotency';\n *\n * const redis = createClient({ url: process.env.REDIS_URL });\n * await redis.connect();\n *\n * await fastify.register(idempotencyPlugin, {\n * store: new RedisIdempotencyStore({ client: redis }),\n * });\n */\n\nimport type { IdempotencyStore, IdempotencyResult } from './interface.js';\n\nexport interface RedisClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, options?: { EX?: number; NX?: boolean }): Promise<string | null>;\n del(key: string | string[]): Promise<number>;\n exists(key: string | string[]): Promise<number>;\n /** SCAN command — compatible with node-redis and ioredis varargs signatures. */\n scan?(cursor: string | number, ...args: (string | number)[]): Promise<[string | number, string[]]>;\n quit?(): Promise<string>;\n disconnect?(): Promise<void>;\n}\n\nexport interface RedisIdempotencyStoreOptions {\n /** Redis client instance */\n client: RedisClient;\n /** Key prefix (default: 'idem:') */\n prefix?: string;\n /** Lock key prefix (default: 'idem:lock:') */\n lockPrefix?: string;\n /** Default TTL in ms (default: 86400000 = 24 hours) */\n ttlMs?: number;\n}\n\nexport class RedisIdempotencyStore implements IdempotencyStore {\n readonly name = 'redis';\n private client: RedisClient;\n private prefix: string;\n private lockPrefix: string;\n private ttlMs: number;\n\n constructor(options: RedisIdempotencyStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? 'idem:';\n this.lockPrefix = options.lockPrefix ?? 'idem:lock:';\n this.ttlMs = options.ttlMs ?? 86400000;\n }\n\n private resultKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n private lockKey(key: string): string {\n return `${this.lockPrefix}${key}`;\n }\n\n async get(key: string): Promise<IdempotencyResult | undefined> {\n const data = await this.client.get(this.resultKey(key));\n if (!data) return undefined;\n\n try {\n const result = JSON.parse(data) as IdempotencyResult;\n // Check if expired (Redis TTL should handle this, but double-check)\n if (new Date(result.expiresAt) < new Date()) {\n await this.delete(key);\n return undefined;\n }\n return {\n ...result,\n createdAt: new Date(result.createdAt),\n expiresAt: new Date(result.expiresAt),\n };\n } catch {\n return undefined;\n }\n }\n\n async set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void> {\n const data: IdempotencyResult = { key, ...result };\n const ttlSeconds = Math.ceil(\n (new Date(result.expiresAt).getTime() - Date.now()) / 1000\n );\n\n if (ttlSeconds > 0) {\n await this.client.set(this.resultKey(key), JSON.stringify(data), {\n EX: ttlSeconds,\n });\n }\n }\n\n async tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean> {\n const ttlSeconds = Math.ceil(ttlMs / 1000);\n const result = await this.client.set(\n this.lockKey(key),\n requestId,\n { EX: ttlSeconds, NX: true }\n );\n return result === 'OK';\n }\n\n async unlock(key: string, requestId: string): Promise<void> {\n // Only unlock if we hold the lock (check requestId)\n const currentHolder = await this.client.get(this.lockKey(key));\n if (currentHolder === requestId) {\n await this.client.del(this.lockKey(key));\n }\n }\n\n async isLocked(key: string): Promise<boolean> {\n const exists = await this.client.exists(this.lockKey(key));\n return exists > 0;\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del([this.resultKey(key), this.lockKey(key)]);\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n const resultKeys = await this.scanByPrefix(this.resultKey(prefix));\n const lockKeys = await this.scanByPrefix(this.lockKey(prefix));\n const allKeys = [...resultKeys, ...lockKeys];\n if (allKeys.length === 0) return 0;\n return this.client.del(allKeys);\n }\n\n async findByPrefix(prefix: string): Promise<IdempotencyResult | undefined> {\n const keys = await this.scanByPrefix(this.resultKey(prefix));\n for (const key of keys) {\n const data = await this.client.get(key);\n if (!data) continue;\n try {\n const result = JSON.parse(data) as IdempotencyResult;\n if (new Date(result.expiresAt) < new Date()) continue;\n return {\n ...result,\n createdAt: new Date(result.createdAt),\n expiresAt: new Date(result.expiresAt),\n };\n } catch {\n continue;\n }\n }\n return undefined;\n }\n\n /** Scan Redis keys matching a prefix pattern. Falls back to empty if SCAN unavailable. */\n private async scanByPrefix(prefix: string): Promise<string[]> {\n if (!this.client.scan) return [];\n const keys: string[] = [];\n let cursor: string | number = '0';\n do {\n const [nextCursor, batch] = await this.client.scan(\n cursor, 'MATCH', `${prefix}*`, 'COUNT', 100,\n );\n cursor = nextCursor;\n keys.push(...batch);\n } while (String(cursor) !== '0');\n return keys;\n }\n\n async close(): Promise<void> {\n // Don't close the client - it's passed in and may be shared\n // The caller is responsible for closing it\n }\n}\n\nexport default RedisIdempotencyStore;\n"],"mappings":"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"}
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/core/validateResourceConfig.ts","../src/pipeline/guard.ts","../src/pipeline/transform.ts","../src/pipeline/intercept.ts","../src/pipeline/pipe.ts","../src/middleware/middleware.ts","../src/context/requestContext.ts","../src/logger/index.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/pipeline/guard.ts","../src/pipeline/transform.ts","../src/pipeline/intercept.ts","../src/middleware/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * guard() — Boolean check that short-circuits on failure.\n *\n * Guards run BEFORE transforms and the handler. If a guard fails (returns false\n * or throws), the request is rejected immediately.\n *\n * @example\n * ```typescript\n * import { guard } from '@classytic/arc';\n *\n * const isActive = guard('isActive', (ctx) => {\n * if (!ctx.user?.isActive) throw new ForbiddenError('Account suspended');\n * return true;\n * });\n *\n * // With operation filter\n * const requireBody = guard('requireBody', {\n * operations: ['create', 'update'],\n * handler: (ctx) => {\n * if (!ctx.body || Object.keys(ctx.body).length === 0) {\n * throw new ValidationError('Request body is required');\n * }\n * return true;\n * },\n * });\n * ```\n */\n\nimport type { PipelineContext, Guard, OperationFilter } from './types.js';\n\ninterface GuardOptions {\n operations?: OperationFilter;\n handler: (ctx: PipelineContext) => boolean | Promise<boolean>;\n}\n\n/**\n * Create a named guard.\n *\n * @param name - Guard name (for debugging/introspection)\n * @param handlerOrOptions - Handler function or options object\n */\nexport function guard(\n name: string,\n handlerOrOptions: ((ctx: PipelineContext) => boolean | Promise<boolean>) | GuardOptions,\n): Guard {\n const opts = typeof handlerOrOptions === 'function'\n ? { handler: handlerOrOptions }\n : handlerOrOptions;\n\n return {\n _type: 'guard' as const,\n name,\n operations: opts.operations,\n handler: opts.handler,\n };\n}\n","/**\n * transform() — Modifies request data before the handler.\n *\n * Transforms run AFTER guards but BEFORE the handler. They can modify\n * the request body, params, or context.\n *\n * @example\n * ```typescript\n * import { transform } from '@classytic/arc';\n *\n * const slugify = transform('slugify', {\n * operations: ['create'],\n * handler: (ctx) => {\n * if (ctx.body?.name && !ctx.body?.slug) {\n * ctx.body.slug = ctx.body.name.toLowerCase().replace(/\\s+/g, '-');\n * }\n * },\n * });\n *\n * const trimStrings = transform('trimStrings', (ctx) => {\n * if (ctx.body && typeof ctx.body === 'object') {\n * for (const [key, value] of Object.entries(ctx.body)) {\n * if (typeof value === 'string') {\n * (ctx.body as Record<string, unknown>)[key] = value.trim();\n * }\n * }\n * }\n * });\n * ```\n */\n\nimport type { PipelineContext, Transform, OperationFilter } from './types.js';\n\ninterface TransformOptions {\n operations?: OperationFilter;\n handler: (ctx: PipelineContext) => PipelineContext | void | Promise<PipelineContext | void>;\n}\n\n/**\n * Create a named transform.\n *\n * @param name - Transform name (for debugging/introspection)\n * @param handlerOrOptions - Handler function or options object\n */\nexport function transform(\n name: string,\n handlerOrOptions: ((ctx: PipelineContext) => PipelineContext | void | Promise<PipelineContext | void>) | TransformOptions,\n): Transform {\n const opts = typeof handlerOrOptions === 'function'\n ? { handler: handlerOrOptions }\n : handlerOrOptions;\n\n return {\n _type: 'transform' as const,\n name,\n operations: opts.operations,\n handler: opts.handler,\n };\n}\n","/**\n * intercept() — Wraps handler execution (before + after pattern).\n *\n * Interceptors wrap the handler like an onion — they can run code before\n * the handler, after the handler, modify the response, measure timing, etc.\n *\n * @example\n * ```typescript\n * import { intercept } from '@classytic/arc';\n *\n * const timing = intercept('timing', async (ctx, next) => {\n * const start = performance.now();\n * const result = await next();\n * result.meta = { ...result.meta, durationMs: Math.round(performance.now() - start) };\n * return result;\n * });\n *\n * const cache = intercept('cache', {\n * operations: ['list', 'get'],\n * handler: async (ctx, next) => {\n * const cached = await redis.get(cacheKey(ctx));\n * if (cached) return JSON.parse(cached);\n * const result = await next();\n * await redis.setex(cacheKey(ctx), 60, JSON.stringify(result));\n * return result;\n * },\n * });\n * ```\n */\n\nimport type { PipelineContext, NextFunction, Interceptor, OperationFilter } from './types.js';\nimport type { IControllerResponse } from '../types/index.js';\n\ninterface InterceptOptions {\n operations?: OperationFilter;\n handler: (ctx: PipelineContext, next: NextFunction) => Promise<IControllerResponse<unknown>>;\n}\n\n/**\n * Create a named interceptor.\n *\n * @param name - Interceptor name (for debugging/introspection)\n * @param handlerOrOptions - Handler function or options object\n */\nexport function intercept(\n name: string,\n handlerOrOptions: ((ctx: PipelineContext, next: NextFunction) => Promise<IControllerResponse<unknown>>) | InterceptOptions,\n): Interceptor {\n const opts = typeof handlerOrOptions === 'function'\n ? { handler: handlerOrOptions }\n : handlerOrOptions;\n\n return {\n _type: 'interceptor' as const,\n name,\n operations: opts.operations,\n handler: opts.handler,\n };\n}\n","/**\n * Named Middleware — Priority-based, conditional middleware execution.\n *\n * Named middleware replaces flat arrays with structured, inspectable middleware\n * that runs in priority order and supports conditional execution.\n *\n * @example\n * ```typescript\n * import { middleware } from '@classytic/arc';\n *\n * const verifyEmail = middleware('verifyEmail', {\n * operations: ['create', 'update'],\n * priority: 5,\n * when: (req) => !req.user?.emailVerified,\n * handler: async (req, reply) => {\n * reply.code(403).send({ error: 'Email verification required' });\n * },\n * });\n *\n * const rateLimit = middleware('rateLimit', {\n * priority: 1,\n * handler: async (req, reply) => {\n * // rate limit logic\n * },\n * });\n *\n * const productResource = defineResource({\n * name: 'product',\n * adapter,\n * middlewares: sortMiddlewares([verifyEmail, rateLimit]),\n * });\n * ```\n */\n\nimport type { MiddlewareConfig, MiddlewareHandler, RequestWithExtras } from '../types/index.js';\nimport { CRUD_OPERATIONS } from '../constants.js';\n\nexport interface NamedMiddleware {\n /** Unique name for debugging/introspection */\n readonly name: string;\n /** Operations this middleware applies to (default: all) */\n readonly operations?: Array<'list' | 'get' | 'create' | 'update' | 'delete' | string>;\n /** Priority — lower numbers run first (default: 10) */\n readonly priority: number;\n /** Conditional execution — return true to run, false to skip */\n readonly when?: (request: RequestWithExtras) => boolean | Promise<boolean>;\n /** The middleware handler */\n readonly handler: MiddlewareHandler;\n}\n\ninterface MiddlewareOptions {\n operations?: NamedMiddleware['operations'];\n priority?: number;\n when?: NamedMiddleware['when'];\n handler: MiddlewareHandler;\n}\n\n/**\n * Create a named middleware with priority and conditions.\n */\nexport function middleware(\n name: string,\n options: MiddlewareOptions,\n): NamedMiddleware {\n return {\n name,\n operations: options.operations,\n priority: options.priority ?? 10,\n when: options.when,\n handler: options.handler,\n };\n}\n\n/**\n * Sort named middlewares by priority (ascending — lower runs first).\n * Returns a MiddlewareConfig map keyed by operation, ready to pass to `defineResource()`.\n */\nexport function sortMiddlewares(middlewares: NamedMiddleware[]): MiddlewareConfig {\n const sorted = [...middlewares].sort((a, b) => a.priority - b.priority);\n\n const operations = CRUD_OPERATIONS;\n const result: MiddlewareConfig = {};\n\n for (const op of operations) {\n const applicable = sorted.filter(\n (m) => !m.operations || m.operations.length === 0 || m.operations.includes(op),\n );\n if (applicable.length > 0) {\n result[op] = applicable.map((m) => {\n if (!m.when) return m.handler;\n // Wrap with conditional check\n const wrapped: MiddlewareHandler = async (request, reply) => {\n const shouldRun = await m.when!(request);\n if (shouldRun) {\n return m.handler(request, reply);\n }\n };\n return wrapped;\n });\n }\n }\n\n return result;\n}\n","/**\n * @classytic/arc\n *\n * Resource-oriented backend framework for Fastify.\n * Production-ready MongoDB support is provided via MongoKit.\n * Prisma/PostgreSQL/MySQL/SQLite adapter support is available as experimental\n * bring-your-own-repository integration via Arc's DataAdapter interface.\n *\n * ## Import Strategy (Tree-Shaking)\n *\n * This main entry exports ONLY the essentials for defining resources.\n * All other features live in dedicated subpaths — Node.js does NOT\n * tree-shake, so barrel re-exports load eagerly at runtime.\n *\n * ```typescript\n * // Main entry — resource definition + permissions + errors\n * import { defineResource, createMongooseAdapter, allowPublic } from '@classytic/arc';\n *\n * // Everything else from dedicated subpaths:\n * import { createApp } from '@classytic/arc/factory';\n * import { createTestApp } from '@classytic/arc/testing';\n * import { eventPlugin } from '@classytic/arc/events';\n * import { beforeCreate } from '@classytic/arc/hooks';\n * import { healthPlugin } from '@classytic/arc/plugins';\n * import { RedisEventTransport } from '@classytic/arc/events/redis';\n * import { tracingPlugin } from '@classytic/arc/plugins/tracing';\n * import { MongoAuditStore } from '@classytic/arc/audit/mongodb';\n * ```\n *\n * ## Subpath Exports\n *\n * | Subpath | Purpose |\n * |---------|---------|\n * | `@classytic/arc` | Core: defineResource, adapters, permissions, errors |\n * | `@classytic/arc/factory` | App creation (createApp, ArcFactory) |\n * | `@classytic/arc/permissions` | Permission functions (also in main) |\n * | `@classytic/arc/adapters` | Database adapters + PrismaQueryParser |\n * | `@classytic/arc/presets` | Preset functions (softDelete, tree, etc.) |\n * | `@classytic/arc/hooks` | Hook helpers (beforeCreate, afterUpdate) |\n * | `@classytic/arc/events` | Event bus (MemoryTransport only) |\n * | `@classytic/arc/events/redis` | Redis Pub/Sub transport (requires ioredis) |\n * | `@classytic/arc/events/redis-stream` | Redis Streams transport (requires ioredis) |\n * | `@classytic/arc/plugins` | Fastify plugins (health, requestId, etc.) |\n * | `@classytic/arc/plugins/tracing` | OpenTelemetry tracing (requires @opentelemetry/*) |\n * | `@classytic/arc/audit` | Audit trail (MemoryStore only) |\n * | `@classytic/arc/audit/mongodb` | MongoDB audit store (requires mongoose) |\n * | `@classytic/arc/idempotency` | Idempotency (MemoryStore only) |\n * | `@classytic/arc/idempotency/redis` | Redis idempotency store (requires ioredis) |\n * | `@classytic/arc/idempotency/mongodb` | MongoDB idempotency store (requires mongoose) |\n * | `@classytic/arc/utils` | Utilities (errors, state machine, circuit breaker) |\n * | `@classytic/arc/org` | Organization/multi-tenant |\n * | `@classytic/arc/auth` | Authentication (JWT, Better Auth) |\n * | `@classytic/arc/testing` | Test utilities, mocks, TestHarness |\n * | `@classytic/arc/schemas` | TypeBox schema helpers |\n * | `@classytic/arc/types` | TypeScript types only |\n * | `@classytic/arc/discovery` | Auto-discovery plugin |\n * | `@classytic/arc/integrations/streamline` | @classytic/streamline adapter |\n * | `@classytic/arc/integrations/websocket` | @fastify/websocket adapter |\n * | `@classytic/arc/integrations/jobs` | BullMQ job queue adapter |\n *\n * @example Basic Resource\n * ```typescript\n * import { defineResource, createMongooseAdapter, allowPublic, requireRoles } from '@classytic/arc';\n *\n * const productResource = defineResource({\n * name: 'product',\n * adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),\n * permissions: {\n * list: allowPublic(),\n * create: requireRoles(['admin']),\n * },\n * });\n * ```\n *\n * @example Full Application\n * ```typescript\n * import { createApp } from '@classytic/arc/factory';\n * import { productResource } from './modules/product.resource.js';\n *\n * const app = await createApp({\n * preset: 'production',\n * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },\n * plugins: async (fastify) => {\n * await fastify.register(productResource.toPlugin());\n * },\n * });\n * ```\n */\n\n// ============================================================================\n// Adapters (database abstraction — zero external deps at this level)\n// ============================================================================\nexport {\n MongooseAdapter,\n createMongooseAdapter,\n PrismaAdapter,\n createPrismaAdapter,\n} from \"./adapters/index.js\";\nexport type {\n DataAdapter,\n SchemaMetadata,\n FieldMetadata,\n RelationMetadata,\n ValidationResult as AdapterValidationResult,\n MongooseAdapterOptions,\n PrismaAdapterOptions,\n RepositoryLike,\n} from \"./adapters/index.js\";\n\n// ============================================================================\n// Core — defineResource, BaseController\n// ============================================================================\nexport {\n BaseController,\n defineResource,\n ResourceDefinition,\n getControllerScope,\n} from \"./core/index.js\";\nexport type { BaseControllerOptions } from \"./core/index.js\";\n\n/**\n * Note: Arc is database-agnostic\n *\n * Import Repository directly from your database kit:\n * - MongoDB: `import { Repository } from '@classytic/mongokit'`\n *\n * Arc provides adapters (createMongooseAdapter, createPrismaAdapter) that work\n * with any repository implementing the CrudRepository interface.\n */\n\n// ============================================================================\n// Types — re-export all types (zero runtime cost, eliminated at compile time)\n// ============================================================================\nexport type {\n // Base types\n AnyRecord,\n PaginatedResult,\n ApiResponse,\n // Framework-agnostic controller types (MongoKit-compatible)\n IRequestContext,\n IControllerResponse,\n IController,\n ControllerLike,\n // User & Auth\n UserBase,\n UserOrganization,\n JWTPayload,\n // Request context\n RequestContext,\n ArcInternalMetadata,\n OwnershipCheck,\n FastifyRequestExtras,\n RequestWithExtras,\n FastifyWithAuth,\n FastifyWithDecorators,\n RouteHandlerMethod,\n // Service & Repository\n ServiceContext,\n QueryOptions,\n CrudRepository,\n // Controller\n RouteHandler,\n CrudController,\n CrudRouteKey,\n // Schema\n RouteSchemaOptions,\n FieldRule,\n CrudSchemas,\n // Routes\n AdditionalRoute,\n MiddlewareConfig,\n // Presets\n PresetResult,\n PresetFunction,\n // Resource\n ResourceConfig,\n EventDefinition,\n ResourceMetadata,\n // Registry\n RegistryEntry,\n RegistryStats,\n IntrospectionData,\n // Plugin options\n AuthPluginOptions,\n IntrospectionPluginOptions,\n CrudRouterOptions,\n RateLimitConfig,\n ConfigError,\n ValidationResult,\n ValidateOptions,\n HealthCheck,\n HealthOptions,\n GracefulShutdownOptions,\n RequestIdOptions,\n // Utility types for better type inference\n InferDocType,\n InferAdapterDoc,\n InferResourceDoc,\n TypedResourceConfig,\n TypedController,\n TypedRepository,\n} from \"./types/index.js\";\n\n// ============================================================================\n// Constants — single source of truth for defaults and magic values (zero deps)\n// ============================================================================\nexport * from \"./constants.js\";\n\n// ============================================================================\n// Errors — commonly needed alongside defineResource (zero deps, pure classes)\n// ============================================================================\nexport {\n ArcError,\n NotFoundError,\n ValidationError,\n UnauthorizedError,\n ForbiddenError,\n} from \"./utils/errors.js\";\n\n// ============================================================================\n// Validation — resource config validation (zero deps, pure functions)\n// ============================================================================\nexport {\n validateResourceConfig,\n formatValidationErrors,\n assertValidConfig,\n} from \"./core/validateResourceConfig.js\";\n\n// ============================================================================\n// Permission System — commonly used with defineResource (pure functions)\n// ============================================================================\nexport {\n // Permission presets (common patterns in one call)\n permissions,\n publicRead,\n publicReadAdminWrite,\n authenticated,\n adminOnly,\n ownerWithAdminBypass,\n fullPublic,\n readOnly,\n // Low-level permission helpers\n allowPublic,\n requireAuth,\n requireRoles,\n requireOwnership,\n allOf,\n anyOf,\n denyAll,\n when,\n // Organization permissions\n requireOrgMembership,\n requireOrgRole,\n createOrgPermissions,\n createDynamicPermissionMatrix,\n requireTeamMembership,\n // Field-level permissions\n fields,\n applyFieldReadPermissions,\n applyFieldWritePermissions,\n} from \"./permissions/index.js\";\n\nexport type {\n PermissionCheck,\n PermissionContext,\n PermissionResult,\n DynamicPermissionMatrixConfig,\n DynamicPermissionMatrix,\n FieldPermission,\n FieldPermissionMap,\n} from \"./permissions/index.js\";\n\n// ============================================================================\n// Pipeline — functional guard/transform/intercept (zero deps, pure functions)\n// ============================================================================\nexport { guard, transform, intercept, pipe } from \"./pipeline/index.js\";\nexport type {\n PipelineContext,\n PipelineStep,\n PipelineConfig,\n Guard,\n Transform,\n Interceptor,\n} from \"./pipeline/index.js\";\n\n// ============================================================================\n// Middleware — named, priority-based (zero deps, pure functions)\n// ============================================================================\nexport { middleware, sortMiddlewares } from \"./middleware/index.js\";\nexport type { NamedMiddleware } from \"./middleware/index.js\";\n\n// ============================================================================\n// Request Context — AsyncLocalStorage (zero deps)\n// ============================================================================\nexport { requestContext } from \"./context/index.js\";\nexport type { RequestStore } from \"./context/index.js\";\n\n// ============================================================================\n// MOVED TO DEDICATED SUBPATHS (no longer re-exported from main barrel)\n// ============================================================================\n//\n// These were previously re-exported here but pulled in heavy dependencies.\n// Import from their dedicated subpaths instead:\n//\n// Factory (pulls in security plugins):\n// import { createApp, ArcFactory } from '@classytic/arc/factory';\n//\n// Plugins (each plugin is self-contained):\n// import { healthPlugin } from '@classytic/arc/plugins';\n// import { tracingPlugin } from '@classytic/arc/plugins/tracing';\n//\n// Hooks (HookSystem):\n// import { createHookSystem, HookSystem } from '@classytic/arc/hooks';\n//\n// Events (eventPlugin + transports):\n// import { eventPlugin } from '@classytic/arc/events';\n// import { RedisEventTransport } from '@classytic/arc/events/redis';\n//\n// Registry:\n// import { ResourceRegistry } from '@classytic/arc/registry';\n//\n\n// ============================================================================\n// Logger — centralized debug/warning system (zero deps)\n// ============================================================================\nexport { configureArcLogger, arcLog } from \"./logger/index.js\";\nexport type { ArcLoggerOptions, ArcLogWriter, ArcLogger } from \"./logger/index.js\";\n\n// Version from package.json (injected at build time via tsdown define)\nexport const version: string = \"__ARC_VERSION__\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAyCA,SAAgB,MACd,MACA,kBACO;CACP,MAAM,OAAO,OAAO,qBAAqB,aACrC,EAAE,SAAS,kBAAkB,GAC7B;AAEJ,QAAO;EACL,OAAO;EACP;EACA,YAAY,KAAK;EACjB,SAAS,KAAK;EACf;;;;;;;;;;;ACVH,SAAgB,UACd,MACA,kBACW;CACX,MAAM,OAAO,OAAO,qBAAqB,aACrC,EAAE,SAAS,kBAAkB,GAC7B;AAEJ,QAAO;EACL,OAAO;EACP;EACA,YAAY,KAAK;EACjB,SAAS,KAAK;EACf;;;;;;;;;;;ACbH,SAAgB,UACd,MACA,kBACa;CACb,MAAM,OAAO,OAAO,qBAAqB,aACrC,EAAE,SAAS,kBAAkB,GAC7B;AAEJ,QAAO;EACL,OAAO;EACP;EACA,YAAY,KAAK;EACjB,SAAS,KAAK;EACf;;;;;;;;ACGH,SAAgB,WACd,MACA,SACiB;AACjB,QAAO;EACL;EACA,YAAY,QAAQ;EACpB,UAAU,QAAQ,YAAY;EAC9B,MAAM,QAAQ;EACd,SAAS,QAAQ;EAClB;;;;;;AAOH,SAAgB,gBAAgB,aAAkD;CAChF,MAAM,SAAS,CAAC,GAAG,YAAY,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS;CAEvE,MAAM,aAAa;CACnB,MAAM,SAA2B,EAAE;AAEnC,MAAK,MAAM,MAAM,YAAY;EAC3B,MAAM,aAAa,OAAO,QACvB,MAAM,CAAC,EAAE,cAAc,EAAE,WAAW,WAAW,KAAK,EAAE,WAAW,SAAS,GAAG,CAC/E;AACD,MAAI,WAAW,SAAS,EACtB,QAAO,MAAM,WAAW,KAAK,MAAM;AACjC,OAAI,CAAC,EAAE,KAAM,QAAO,EAAE;GAEtB,MAAM,UAA6B,OAAO,SAAS,UAAU;AAE3D,QADkB,MAAM,EAAE,KAAM,QAAQ,CAEtC,QAAO,EAAE,QAAQ,SAAS,MAAM;;AAGpC,UAAO;IACP;;AAIN,QAAO;;;;;ACmOT,MAAa,UAAkB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"event-gateway.d.mts","names":[],"sources":["../../src/integrations/event-gateway.ts"],"mappings":";;;;;UA+BiB,mBAAA;EAEf;EAAA,IAAA;EAGE;EADF,YAAA,IACE,OAAA,cACG,OAAA;IAAU,MAAA;IAAiB,cAAA;EAAA;EAIhC;EAFA,SAAA;EAG6B;EAD7B,UAAA,IACE,MAAA;IAAU,MAAA;IAAiB,cAAA;EAAA,GAC3B,IAAA,uBACa,OAAA;EAIf;EAFA,eAAA;EAQM;EANN,yBAAA;EAQM;EALN,GAAA;IAGM,IAAA;IACA,QAAA;IACA,SAAA;IACA,MAAA,IAAU,KAAA,EAAO,WAAA,WAAsB,OAAA,EAAS,cAAA;EAAA;EAOhD;EAHN,EAAA;IAGM,IAAA;IACA,SAAA;IACA,iBAAA;IACA,iBAAA;IACA,WAAA;IACA,SAAA,IACE,MAAA,EAAQ,eAAA,EACR,OAAA,EAAS,gBAAA,YACC,OAAA;IACZ,SAAA,IAAa,MAAA,EAAQ,eAAA,YAA2B,OAAA;IAChD,YAAA,IAAgB,MAAA,EAAQ,eAAA,YAA2B,OAAA;EAAA;AAAA;AAAA,cAgE9C,kBAAA,EAGP,kBAAA,CAAmB,mBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"event-gateway.mjs","names":[],"sources":["../../src/integrations/event-gateway.ts"],"sourcesContent":["/**\r\n * @classytic/arc — Event Gateway\r\n *\r\n * Unified real-time configuration point that wires SSE and WebSocket with\r\n * shared auth, org-scoping, and policy enforcement. Replaces the need to\r\n * configure SSE and WebSocket plugins independently.\r\n *\r\n * import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';\r\n *\r\n * await fastify.register(eventGatewayPlugin, {\r\n * auth: true,\r\n * orgScoped: true,\r\n * roomPolicy: (client, room) => ['product', 'order'].includes(room),\r\n * sse: { path: '/api/events', patterns: ['order.*', 'product.*'] },\r\n * ws: { path: '/ws', resources: ['product', 'order'] },\r\n * });\r\n */\r\n\r\nimport type {\r\n FastifyInstance,\r\n FastifyPluginAsync,\r\n FastifyRequest,\r\n} from \"fastify\";\r\nimport fp from \"fastify-plugin\";\r\nimport type { DomainEvent } from \"../events/EventTransport.js\";\r\nimport type { WebSocketClient, WebSocketMessage } from \"./websocket.js\";\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport interface EventGatewayOptions {\r\n /** Require auth for all real-time connections (default: true) */\r\n auth?: boolean;\r\n /** Custom auth function for WebSocket upgrade */\r\n authenticate?: (\r\n request: unknown,\r\n ) => Promise<{ userId?: string; organizationId?: string } | null>;\r\n /** Filter events by org from request.scope (default: false) */\r\n orgScoped?: boolean;\r\n /** Room/subscription authorization policy */\r\n roomPolicy?: (\r\n client: { userId?: string; organizationId?: string },\r\n room: string,\r\n ) => boolean | Promise<boolean>;\r\n /** Max message size from WS clients in bytes (default: 16384) */\r\n maxMessageBytes?: number;\r\n /** Max subscriptions per client (default: 100) */\r\n maxSubscriptionsPerClient?: number;\r\n\r\n /** SSE config. Set false to disable SSE. */\r\n sse?:\r\n | false\r\n | {\r\n path?: string;\r\n patterns?: string[];\r\n heartbeat?: number;\r\n filter?: (event: DomainEvent<unknown>, request: FastifyRequest) => boolean;\r\n };\r\n\r\n /** WebSocket config. Set false to disable WebSocket. */\r\n ws?:\r\n | false\r\n | {\r\n path?: string;\r\n resources?: string[];\r\n heartbeatInterval?: number;\r\n maxClientsPerRoom?: number;\r\n exposeStats?: boolean | \"authenticated\";\r\n onMessage?: (\r\n client: WebSocketClient,\r\n message: WebSocketMessage,\r\n ) => void | Promise<void>;\r\n onConnect?: (client: WebSocketClient) => void | Promise<void>;\r\n onDisconnect?: (client: WebSocketClient) => void | Promise<void>;\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Plugin\r\n// ============================================================================\r\n\r\nconst eventGatewayPluginImpl: FastifyPluginAsync<EventGatewayOptions> = async (\r\n fastify: FastifyInstance,\r\n opts: EventGatewayOptions = {},\r\n) => {\r\n const {\r\n auth = true,\r\n orgScoped = false,\r\n roomPolicy,\r\n maxMessageBytes,\r\n maxSubscriptionsPerClient,\r\n authenticate,\r\n } = opts;\r\n\r\n // Fail-closed: validate auth decorator once for both SSE and WebSocket\r\n if (auth && !authenticate && !fastify.hasDecorator(\"authenticate\")) {\r\n throw new Error(\r\n \"[arc-event-gateway] auth is true but fastify.authenticate is not registered. \" +\r\n \"Register an auth plugin first, provide a custom authenticate function, or set auth: false.\",\r\n );\r\n }\r\n\r\n // Register SSE if not disabled\r\n if (opts.sse !== false) {\r\n // Lazy import to avoid pulling SSE into bundles when disabled\r\n const { default: ssePlugin } = await import(\"../plugins/sse.js\");\r\n await fastify.register(ssePlugin, {\r\n path: opts.sse?.path ?? \"/events/stream\",\r\n requireAuth: auth,\r\n patterns: opts.sse?.patterns ?? [\"*\"],\r\n heartbeat: opts.sse?.heartbeat ?? 30000,\r\n orgScoped,\r\n filter: opts.sse?.filter,\r\n });\r\n }\r\n\r\n // Register WebSocket if not disabled\r\n if (opts.ws !== false) {\r\n const { websocketPlugin } = await import(\"./websocket.js\");\r\n await fastify.register(websocketPlugin, {\r\n path: opts.ws?.path ?? \"/ws\",\r\n auth,\r\n authenticate,\r\n resources: opts.ws?.resources ?? [],\r\n heartbeatInterval: opts.ws?.heartbeatInterval ?? 30000,\r\n maxClientsPerRoom: opts.ws?.maxClientsPerRoom,\r\n roomPolicy,\r\n maxMessageBytes,\r\n maxSubscriptionsPerClient,\r\n exposeStats: opts.ws?.exposeStats,\r\n onMessage: opts.ws?.onMessage,\r\n onConnect: opts.ws?.onConnect,\r\n onDisconnect: opts.ws?.onDisconnect,\r\n });\r\n }\r\n};\r\n\r\nexport const eventGatewayPlugin = fp(eventGatewayPluginImpl, {\r\n name: \"arc-event-gateway\",\r\n fastify: \"5.x\",\r\n}) as FastifyPluginAsync<EventGatewayOptions>;\r\n\r\nexport default eventGatewayPlugin;\r\n"],"mappings":";;;AAkFA,MAAM,yBAAkE,OACtE,SACA,OAA4B,EAAE,KAC3B;CACH,MAAM,EACJ,OAAO,MACP,YAAY,OACZ,YACA,iBACA,2BACA,iBACE;AAGJ,KAAI,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,aAAa,eAAe,CAChE,OAAM,IAAI,MACR,0KAED;AAIH,KAAI,KAAK,QAAQ,OAAO;EAEtB,MAAM,EAAE,SAAS,cAAc,MAAM,OAAO;AAC5C,QAAM,QAAQ,SAAS,WAAW;GAChC,MAAM,KAAK,KAAK,QAAQ;GACxB,aAAa;GACb,UAAU,KAAK,KAAK,YAAY,CAAC,IAAI;GACrC,WAAW,KAAK,KAAK,aAAa;GAClC;GACA,QAAQ,KAAK,KAAK;GACnB,CAAC;;AAIJ,KAAI,KAAK,OAAO,OAAO;EACrB,MAAM,EAAE,oBAAoB,MAAM,OAAO;AACzC,QAAM,QAAQ,SAAS,iBAAiB;GACtC,MAAM,KAAK,IAAI,QAAQ;GACvB;GACA;GACA,WAAW,KAAK,IAAI,aAAa,EAAE;GACnC,mBAAmB,KAAK,IAAI,qBAAqB;GACjD,mBAAmB,KAAK,IAAI;GAC5B;GACA;GACA;GACA,aAAa,KAAK,IAAI;GACtB,WAAW,KAAK,IAAI;GACpB,WAAW,KAAK,IAAI;GACpB,cAAc,KAAK,IAAI;GACxB,CAAC;;;AAIN,MAAa,qBAAqB,GAAG,wBAAwB;CAC3D,MAAM;CACN,SAAS;CACV,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.d.mts","names":[],"sources":["../../src/integrations/jobs.ts"],"mappings":";;;UA+CiB,aAAA;EAyBA;EAvBf,IAAA;;EAEA,OAAA,GAAU,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,OAAA,KAAY,OAAA,CAAQ,OAAA;EAuBjD;EArBA,OAAA;EAyBA;EAvBA,OAAA;IAAY,IAAA;IAA+B,KAAA;EAAA;EA8B5B;EA5Bf,OAAA;;EAEA,WAAA;EA4BA;EA1BA,SAAA;IAAc,GAAA;IAAa,QAAA;EAAA;EA4B3B;EA1BA,eAAA;AAAA;AAAA,UAGe,OAAA;EACf,KAAA;EACA,YAAA;EACA,SAAA;AAAA;AAAA,UAGe,kBAAA;EA0Bb;EAxBF,KAAA;EA0BE;EAxBF,QAAA;EAwBc;EAtBd,KAAA;EA0B4B;EAxB5B,gBAAA;EA2BQ;EAzBR,YAAA;AAAA;AAAA,UAGe,iBAAA;EA0BK;EAxBpB,UAAA;IAAc,IAAA;IAAc,IAAA;IAAc,QAAA;IAAmB,EAAA;EAAA;EAmB3D;EAjBF,IAAA,EAAM,aAAA;EAkBJ;EAhBF,MAAA;EAiBE;EAfF,YAAA;EAgBa;EAdb,QAAA;IACE,OAAA;IACA,OAAA;MAAY,IAAA;MAA+B,KAAA;IAAA;IAC3C,OAAA;IACA,gBAAA;IACA,YAAA;EAAA;AAAA;AAAA,UAIa,aAAA;EACf,QAAA,kBACE,IAAA,UACA,IAAA,EAAM,KAAA,EACN,OAAA,GAAU,kBAAA,GACT,OAAA;IAAU,KAAA;EAAA;EACb,QAAA,CAAS,IAAA;EACT,QAAA,IAAY,OAAA,CAAQ,MAAA,SAAe,UAAA;EACnC,KAAA,IAAS,OAAA;AAAA;AAAA,UAGM,UAAA;EACf,OAAA;EACA,MAAA;EACA,SAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;;;;;;;;iBAoBc,SAAA,oCAAA,CACd,UAAA,EAAY,aAAA,CAAc,KAAA,EAAO,OAAA,IAChC,aAAA,CAAc,KAAA,EAAO,OAAA;;cAoKX,UAAA,EAAY,kBAAA,CAAmB,iBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.mjs","names":[],"sources":["../../src/integrations/jobs.ts"],"sourcesContent":["/**\n * @classytic/arc — Job Queue Integration\n *\n * Pluggable adapter for background job processing with BullMQ.\n * Provides a clean defineJob() API, auto-connects to Arc's event bus,\n * and supports retries, delays, priorities, and dead-letter queues.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { jobsPlugin, defineJob } from '@classytic/arc/integrations/jobs';\n *\n * Requires: bullmq (peer dependency)\n *\n * NOTE: Job processing requires a persistent process and Redis.\n * This does NOT work on serverless platforms.\n *\n * @example\n * ```typescript\n * import { jobsPlugin, defineJob } from '@classytic/arc/integrations/jobs';\n *\n * const sendEmail = defineJob({\n * name: 'send-email',\n * handler: async (data) => {\n * await emailService.send(data.to, data.subject, data.body);\n * },\n * retries: 3,\n * backoff: { type: 'exponential', delay: 1000 },\n * });\n *\n * await fastify.register(jobsPlugin, {\n * connection: { host: 'localhost', port: 6379 },\n * jobs: [sendEmail],\n * });\n *\n * // Dispatch a job from anywhere\n * await fastify.jobs.dispatch('send-email', {\n * to: 'user@example.com',\n * subject: 'Hello',\n * body: 'Welcome!',\n * });\n * ```\n */\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\n// ============================================================================\n// Types (no BullMQ import at module level)\n// ============================================================================\n\nexport interface JobDefinition<TData = unknown, TResult = unknown> {\n /** Unique job name */\n name: string;\n /** Job handler function */\n handler: (data: TData, meta: JobMeta) => Promise<TResult>;\n /** Number of retries on failure (default: 3) */\n retries?: number;\n /** Backoff strategy */\n backoff?: { type: 'exponential' | 'fixed'; delay: number };\n /** Job timeout in ms (default: 30000) */\n timeout?: number;\n /** Concurrency per worker (default: 1) */\n concurrency?: number;\n /** Rate limit: max jobs per duration */\n rateLimit?: { max: number; duration: number };\n /** Dead letter queue name (default: '{name}:dead') */\n deadLetterQueue?: string;\n}\n\nexport interface JobMeta {\n jobId: string;\n attemptsMade: number;\n timestamp: number;\n}\n\nexport interface JobDispatchOptions {\n /** Delay job execution by ms */\n delay?: number;\n /** Job priority (lower = higher priority) */\n priority?: number;\n /** Unique job ID (for deduplication) */\n jobId?: string;\n /** Remove job after completion */\n removeOnComplete?: boolean | number;\n /** Remove job after failure */\n removeOnFail?: boolean | number;\n}\n\nexport interface JobsPluginOptions {\n /** Redis connection options (passed to BullMQ) */\n connection: { host: string; port: number; password?: string; db?: number } | unknown;\n /** Job definitions to register */\n jobs: JobDefinition[];\n /** URL prefix for job management endpoints (default: '/jobs') */\n prefix?: string;\n /** Bridge job events to Arc's event bus (default: true) */\n bridgeEvents?: boolean;\n /** Default job options applied to all jobs */\n defaults?: {\n retries?: number;\n backoff?: { type: 'exponential' | 'fixed'; delay: number };\n timeout?: number;\n removeOnComplete?: boolean | number;\n removeOnFail?: boolean | number;\n };\n}\n\nexport interface JobDispatcher {\n dispatch<TData = unknown>(\n name: string,\n data: TData,\n options?: JobDispatchOptions\n ): Promise<{ jobId: string }>;\n getQueue(name: string): unknown | null;\n getStats(): Promise<Record<string, QueueStats>>;\n close(): Promise<void>;\n}\n\nexport interface QueueStats {\n waiting: number;\n active: number;\n completed: number;\n failed: number;\n delayed: number;\n}\n\n// ============================================================================\n// defineJob — declarative job definition\n// ============================================================================\n\n/**\n * Define a background job with typed data and configuration.\n *\n * @example\n * const processImage = defineJob({\n * name: 'process-image',\n * handler: async (data: { url: string; width: number }) => {\n * return await sharp(data.url).resize(data.width).toBuffer();\n * },\n * retries: 3,\n * timeout: 60000,\n * });\n */\nexport function defineJob<TData = unknown, TResult = unknown>(\n definition: JobDefinition<TData, TResult>\n): JobDefinition<TData, TResult> {\n return definition;\n}\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\nconst jobsPluginImpl: FastifyPluginAsync<JobsPluginOptions> = async (\n fastify: FastifyInstance,\n options: JobsPluginOptions\n) => {\n const {\n connection,\n jobs,\n prefix = '/jobs',\n bridgeEvents = true,\n defaults = {},\n } = options;\n\n // Dynamic import of BullMQ (only when plugin is actually registered)\n let Queue: any;\n let Worker: any;\n\n try {\n const bullmq = await import('bullmq');\n Queue = bullmq.Queue;\n Worker = bullmq.Worker;\n } catch {\n throw new Error(\n '@classytic/arc/integrations/jobs requires \"bullmq\" package.\\n' +\n 'Install it: npm install bullmq'\n );\n }\n\n const queues = new Map<string, InstanceType<typeof Queue>>();\n const workers = new Map<string, InstanceType<typeof Worker>>();\n\n // Register each job as a queue + worker pair\n for (const job of jobs) {\n const queueName = job.name;\n\n // Create queue\n const queue = new Queue(queueName, { connection });\n queues.set(queueName, queue);\n\n // Create worker\n const worker = new Worker(\n queueName,\n async (bullJob: any) => {\n const meta: JobMeta = {\n jobId: bullJob.id,\n attemptsMade: bullJob.attemptsMade,\n timestamp: Date.now(),\n };\n\n const result = await job.handler(bullJob.data, meta);\n\n // Bridge completion event\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`job.${queueName}.completed`, {\n jobId: bullJob.id,\n data: bullJob.data,\n result,\n });\n }\n\n return result;\n },\n {\n connection,\n concurrency: job.concurrency ?? 1,\n limiter: job.rateLimit\n ? { max: job.rateLimit.max, duration: job.rateLimit.duration }\n : undefined,\n }\n );\n\n // Bridge failure event\n worker.on('failed', async (bullJob: any, error: Error) => {\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`job.${queueName}.failed`, {\n jobId: bullJob?.id,\n data: bullJob?.data,\n error: error.message,\n attemptsMade: bullJob?.attemptsMade,\n });\n }\n });\n\n workers.set(queueName, worker);\n }\n\n // Dispatcher interface\n const dispatcher: JobDispatcher = {\n async dispatch(name, data, opts = {}) {\n const queue = queues.get(name);\n if (!queue) {\n throw new Error(`Job queue '${name}' not registered. Available: ${Array.from(queues.keys()).join(', ')}`);\n }\n\n const jobDef = jobs.find((j) => j.name === name);\n const bullJob = await queue.add(name, data, {\n delay: opts.delay,\n priority: opts.priority,\n jobId: opts.jobId,\n removeOnComplete: opts.removeOnComplete ?? defaults.removeOnComplete ?? 100,\n removeOnFail: opts.removeOnFail ?? defaults.removeOnFail ?? 500,\n attempts: jobDef?.retries ?? defaults.retries ?? 3,\n backoff: jobDef?.backoff ?? defaults.backoff ?? { type: 'exponential', delay: 1000 },\n });\n\n return { jobId: bullJob.id };\n },\n\n getQueue(name) {\n return queues.get(name) ?? null;\n },\n\n async getStats() {\n const stats: Record<string, QueueStats> = {};\n for (const [name, queue] of queues) {\n const counts = await queue.getJobCounts();\n stats[name] = {\n waiting: counts.waiting ?? 0,\n active: counts.active ?? 0,\n completed: counts.completed ?? 0,\n failed: counts.failed ?? 0,\n delayed: counts.delayed ?? 0,\n };\n }\n return stats;\n },\n\n async close() {\n const closePromises: Promise<void>[] = [];\n for (const worker of workers.values()) {\n closePromises.push(worker.close());\n }\n for (const queue of queues.values()) {\n closePromises.push(queue.close());\n }\n await Promise.all(closePromises);\n },\n };\n\n // Decorate fastify\n if (!fastify.hasDecorator('jobs')) {\n fastify.decorate('jobs', dispatcher);\n }\n\n // Management endpoints\n fastify.get(`${prefix}/stats`, async () => {\n const stats = await dispatcher.getStats();\n return { success: true, data: stats };\n });\n\n // Graceful shutdown\n fastify.addHook('onClose', async () => {\n await dispatcher.close();\n });\n};\n\n/** Pluggable BullMQ job queue integration for Arc */\nexport const jobsPlugin: FastifyPluginAsync<JobsPluginOptions> = jobsPluginImpl;\nexport default jobsPlugin;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"streamline.d.mts","names":[],"sources":["../../src/integrations/streamline.ts"],"mappings":";;;;UAsCiB,YAAA;EACf,UAAA;IAAc,EAAA;IAAY,IAAA;IAAe,KAAA,EAAO,MAAA;EAAA;EAChD,MAAA;IACE,KAAA,CAAM,KAAA,WAAgB,IAAA,aAAiB,OAAA,CAAQ,eAAA;IAC/C,OAAA,CAAQ,KAAA,WAAgB,OAAA,CAAQ,eAAA;IAChC,MAAA,CAAO,KAAA,UAAe,OAAA,aAAoB,OAAA,CAAQ,eAAA;IAClD,MAAA,CAAO,KAAA,WAAgB,OAAA,CAAQ,eAAA;IAC/B,KAAA,EAAO,KAAA,WAAgB,OAAA,CAAQ,eAAA;IAC/B,QAAA,EAAU,KAAA,UAAe,MAAA,WAAiB,OAAA,CAAQ,eAAA;IAClD,GAAA,CAAI,KAAA,WAAgB,OAAA,CAAQ,eAAA;IAC5B,QAAA;EAAA;EAEF,KAAA,CAAM,KAAA,WAAgB,IAAA,aAAiB,OAAA,CAAQ,eAAA;EAC/C,MAAA,CAAO,KAAA,UAAe,OAAA,aAAoB,OAAA,CAAQ,eAAA;EAClD,MAAA,CAAO,KAAA,WAAgB,OAAA,CAAQ,eAAA;EAC/B,GAAA,CAAI,KAAA,WAAgB,OAAA,CAAQ,eAAA;EAC5B,QAAA;AAAA;AAAA,UAGe,eAAA;EACf,GAAA;EACA,UAAA;EACA,MAAA;EACA,OAAA;EACA,KAAA;EACA,KAAA,GAAQ,MAAA;EACR,KAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;EAAA,CACX,GAAA;AAAA;AAAA,UAGc,uBAAA;EAtBb;EAwBF,SAAA,EAAW,YAAA;EAtBL;EAwBN,MAAA;EAxBuC;EA0BvC,IAAA;EAzBA;EA2BA,YAAA;EA3BsB;EA6BtB,WAAA;IACE,KAAA,IAAS,OAAA,wBAA+B,OAAA;IACxC,MAAA,IAAU,OAAA,wBAA+B,OAAA;IACzC,MAAA,IAAU,OAAA,wBAA+B,OAAA;IACzC,IAAA,IAAQ,OAAA,wBAA+B,OAAA;IACvC,GAAA,IAAO,OAAA,wBAA+B,OAAA;EAAA;AAAA;;cAyL7B,gBAAA,EAAkB,kBAAA,CAAmB,uBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"streamline.mjs","names":[],"sources":["../../src/integrations/streamline.ts"],"sourcesContent":["/**\n * @classytic/arc — Streamline Integration\n *\n * Pluggable adapter that wires @classytic/streamline workflows into Arc's\n * Fastify application. Provides REST endpoints for workflow management,\n * auto-connects to Arc's event bus, and respects Arc's auth/permissions.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { streamlinePlugin } from '@classytic/arc/integrations/streamline';\n *\n * Requires: @classytic/streamline (peer dependency)\n *\n * @example\n * ```typescript\n * import { streamlinePlugin } from '@classytic/arc/integrations/streamline';\n * import { orderWorkflow } from './workflows/order.js';\n *\n * await fastify.register(streamlinePlugin, {\n * workflows: [orderWorkflow],\n * prefix: '/api/workflows',\n * auth: true, // require authentication for workflow endpoints\n * });\n *\n * // Starts the workflow\n * // POST /api/workflows/order/start { input }\n * // GET /api/workflows/order/runs/:runId\n * // POST /api/workflows/order/runs/:runId/resume { payload }\n * // POST /api/workflows/order/runs/:runId/cancel\n * // GET /api/workflows/order/runs (list runs)\n * ```\n */\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\n// ============================================================================\n// Types (defined here so we don't import streamline at module level)\n// ============================================================================\n\n/** Minimal workflow interface — matches @classytic/streamline's createWorkflow() return */\nexport interface WorkflowLike {\n definition: { id: string; name?: string; steps: Record<string, unknown> };\n engine: {\n start(input: unknown, meta?: unknown): Promise<WorkflowRunLike>;\n execute(runId: string): Promise<WorkflowRunLike>;\n resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;\n cancel(runId: string): Promise<WorkflowRunLike>;\n pause?(runId: string): Promise<WorkflowRunLike>;\n rewindTo?(runId: string, stepId: string): Promise<WorkflowRunLike>;\n get(runId: string): Promise<WorkflowRunLike | null>;\n shutdown?(): void;\n };\n start(input: unknown, meta?: unknown): Promise<WorkflowRunLike>;\n resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;\n cancel(runId: string): Promise<WorkflowRunLike>;\n get(runId: string): Promise<WorkflowRunLike | null>;\n shutdown?(): void;\n}\n\nexport interface WorkflowRunLike {\n _id: string;\n workflowId: string;\n status: string;\n context?: unknown;\n input?: unknown;\n steps?: Record<string, unknown>;\n error?: unknown;\n createdAt?: Date;\n updatedAt?: Date;\n [key: string]: unknown;\n}\n\nexport interface StreamlinePluginOptions {\n /** Array of workflows created with createWorkflow() */\n workflows: WorkflowLike[];\n /** URL prefix for workflow endpoints (default: '/workflows') */\n prefix?: string;\n /** Require authentication for all workflow endpoints (default: true) */\n auth?: boolean;\n /** Connect workflow events to Arc's event bus (default: true) */\n bridgeEvents?: boolean;\n /** Custom permission check for workflow operations */\n permissions?: {\n start?: (request: unknown) => boolean | Promise<boolean>;\n resume?: (request: unknown) => boolean | Promise<boolean>;\n cancel?: (request: unknown) => boolean | Promise<boolean>;\n list?: (request: unknown) => boolean | Promise<boolean>;\n get?: (request: unknown) => boolean | Promise<boolean>;\n };\n}\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\nconst streamlinePluginImpl: FastifyPluginAsync<StreamlinePluginOptions> = async (\n fastify: FastifyInstance,\n options: StreamlinePluginOptions\n) => {\n const {\n workflows,\n prefix = '/workflows',\n auth = true,\n bridgeEvents = true,\n permissions: perms,\n } = options;\n\n // Registry: workflowId → workflow instance\n const registry = new Map<string, WorkflowLike>();\n\n for (const wf of workflows) {\n const id = wf.definition.id;\n if (registry.has(id)) {\n throw new Error(`Duplicate workflow ID: '${id}'`);\n }\n registry.set(id, wf);\n }\n\n // Decorate fastify with workflow accessor\n if (!fastify.hasDecorator('workflows')) {\n fastify.decorate('workflows', registry);\n }\n if (!fastify.hasDecorator('getWorkflow')) {\n fastify.decorate('getWorkflow', (id: string) => registry.get(id) ?? null);\n }\n\n // Build auth preHandler if needed\n const authPreHandler = auth && typeof fastify.authenticate === 'function'\n ? [fastify.authenticate]\n : [];\n\n // Permission check helper\n const checkPerm = async (\n op: keyof NonNullable<StreamlinePluginOptions['permissions']>,\n request: unknown\n ): Promise<boolean> => {\n const check = perms?.[op];\n if (!check) return true;\n return check(request);\n };\n\n // Register routes per workflow\n for (const [id, wf] of registry) {\n const routePrefix = `${prefix}/${id}`;\n\n // POST /:workflowId/start — Start a new workflow run\n fastify.post(`${routePrefix}/start`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('start', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { input, meta } = (request.body ?? {}) as { input?: unknown; meta?: unknown };\n const run = await wf.start(input, meta);\n\n // Bridge event to Arc's event bus\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`workflow.${id}.started`, {\n runId: run._id,\n workflowId: id,\n status: run.status,\n });\n }\n\n return reply.status(201).send({ success: true, data: run });\n });\n\n // GET /:workflowId/runs/:runId — Get a workflow run\n fastify.get(`${routePrefix}/runs/:runId`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('get', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { runId } = request.params as { runId: string };\n const run = await wf.get(runId);\n if (!run) {\n return reply.status(404).send({ success: false, error: 'Workflow run not found' });\n }\n return { success: true, data: run };\n });\n\n // POST /:workflowId/runs/:runId/resume — Resume a waiting workflow\n fastify.post(`${routePrefix}/runs/:runId/resume`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('resume', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { runId } = request.params as { runId: string };\n const { payload } = (request.body ?? {}) as { payload?: unknown };\n const run = await wf.resume(runId, payload);\n\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`workflow.${id}.resumed`, {\n runId: run._id,\n workflowId: id,\n status: run.status,\n });\n }\n\n return { success: true, data: run };\n });\n\n // POST /:workflowId/runs/:runId/cancel — Cancel a workflow run\n fastify.post(`${routePrefix}/runs/:runId/cancel`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('cancel', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { runId } = request.params as { runId: string };\n const run = await wf.cancel(runId);\n\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`workflow.${id}.cancelled`, {\n runId: run._id,\n workflowId: id,\n });\n }\n\n return { success: true, data: run };\n });\n\n // POST /:workflowId/runs/:runId/pause — Pause a running workflow (if supported)\n if (wf.engine.pause) {\n fastify.post(`${routePrefix}/runs/:runId/pause`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n const { runId } = request.params as { runId: string };\n const run = await wf.engine.pause!(runId);\n return { success: true, data: run };\n });\n }\n\n // POST /:workflowId/runs/:runId/rewind — Rewind to a step (if supported)\n if (wf.engine.rewindTo) {\n fastify.post(`${routePrefix}/runs/:runId/rewind`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n const { runId } = request.params as { runId: string };\n const { stepId } = (request.body ?? {}) as { stepId: string };\n if (!stepId) {\n return reply.status(400).send({ success: false, error: 'stepId is required' });\n }\n const run = await wf.engine.rewindTo!(runId, stepId);\n return { success: true, data: run };\n });\n }\n }\n\n // List all registered workflows\n fastify.get(prefix, {\n preHandler: authPreHandler,\n }, async () => {\n const list = Array.from(registry.entries()).map(([id, wf]) => ({\n id,\n name: wf.definition.name ?? id,\n steps: Object.keys(wf.definition.steps),\n }));\n return { success: true, data: list };\n });\n\n // Graceful shutdown\n fastify.addHook('onClose', async () => {\n for (const wf of registry.values()) {\n wf.shutdown?.();\n }\n });\n};\n\n/** Pluggable streamline integration for Arc */\nexport const streamlinePlugin: FastifyPluginAsync<StreamlinePluginOptions> = streamlinePluginImpl;\nexport default streamlinePlugin;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.mts","names":[],"sources":["../../src/integrations/websocket.ts"],"mappings":";;;UAwCiB,eAAA;EACf,EAAA;EACA,MAAA;IAAU,IAAA,CAAK,IAAA;IAAqB,KAAA;IAAe,UAAA;EAAA;EACnD,aAAA,EAAe,GAAA;EACf,MAAA;EACA,cAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,gBAAA;EACf,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,UAGe,sBAAA;EAYA;EAVf,IAAA;EAYA;EAVA,IAAA;EAsBA;EApBA,SAAA;EAqBE;EAnBF,iBAAA;EAqBe;EAnBf,YAAA,IACE,OAAA,cACG,OAAA;IAAU,MAAA;IAAiB,cAAA;EAAA;EAwB9B;EAtBF,iBAAA;EAuBE;;;;;;EAhBF,WAAA;EAqBwB;;;;EAhBxB,UAAA,IACE,MAAA,EAAQ,eAAA,EACR,IAAA,uBACa,OAAA;EAoBJ;EAlBX,eAAA;;EAEA,yBAAA;EA6G6B;EA3G7B,SAAA,IACE,MAAA,EAAQ,eAAA,EACR,OAAA,EAAS,gBAAA,YACC,OAAA;EA+GW;EA7GvB,SAAA,IAAa,MAAA,EAAQ,eAAA,YAA2B,OAAA;EAUxC;EARR,YAAA,IAAgB,MAAA,EAAQ,eAAA,YAA2B,OAAA;AAAA;AAAA,cAOxC,WAAA;EAAA,QACH,KAAA;EAAA,QACA,OAAA;EAAA,QACA,UAAA;cAEI,UAAA;EAIZ,SAAA,CAAU,MAAA,EAAQ,eAAA;EAIlB,YAAA,CAAa,QAAA;EAiBb,SAAA,CAAU,QAAA,UAAkB,IAAA;EAc5B,WAAA,CAAY,QAAA,UAAkB,IAAA;EAY9B,SAAA,CAAU,IAAA,UAAc,OAAA,UAAiB,eAAA;EAiBzC,cAAA,CAAe,cAAA,UAAwB,IAAA,UAAc,OAAA;EAoBrD,SAAA,CAAU,QAAA,WAAmB,eAAA;EAI7B,QAAA,CAAA;IACE,OAAA;IACA,KAAA;IACA,aAAA,EAAe,MAAA;EAAA;AAAA;;cAoUN,eAAA,EAGP,kBAAA,CAAmB,sBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.mjs","names":[],"sources":["../../src/integrations/websocket.ts"],"sourcesContent":["/**\n * @classytic/arc — WebSocket Integration\n *\n * Pluggable adapter that wires @fastify/websocket into Arc's resource system.\n * Provides room-based subscriptions, auto-broadcasts resource CRUD events,\n * and respects Arc's auth/org scoping.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { websocketPlugin } from '@classytic/arc/integrations/websocket';\n *\n * Requires: @fastify/websocket (peer dependency)\n *\n * NOTE: WebSocket requires persistent connections. This does NOT work on\n * serverless platforms (Lambda, Vercel). Only use on persistent runtimes\n * (Docker, VPS, K8s, Cloud Run with min-instances > 0).\n *\n * @example\n * ```typescript\n * import { websocketPlugin } from '@classytic/arc/integrations/websocket';\n *\n * await fastify.register(websocketPlugin, {\n * path: '/ws',\n * auth: true,\n * resources: ['product', 'order'], // Auto-broadcast CRUD events\n * heartbeatInterval: 30000,\n * });\n *\n * // Client connects to ws://localhost:3000/ws\n * // Server pushes: { type: 'product.created', data: { ... } }\n * // Client sends: { type: 'subscribe', resource: 'product' }\n * // Client sends: { type: 'unsubscribe', resource: 'product' }\n * ```\n */\nimport type { FastifyInstance, FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WebSocketClient {\n id: string;\n socket: { send(data: string): void; close(): void; readyState: number };\n subscriptions: Set<string>;\n userId?: string;\n organizationId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface WebSocketMessage {\n type: string;\n resource?: string;\n channel?: string;\n data?: unknown;\n}\n\nexport interface WebSocketPluginOptions {\n /** WebSocket endpoint path (default: '/ws') */\n path?: string;\n /** Require authentication for WebSocket connections (default: true) */\n auth?: boolean;\n /** Resources to auto-broadcast CRUD events for */\n resources?: string[];\n /** Heartbeat interval in ms (default: 30000). Set 0 to disable. */\n heartbeatInterval?: number;\n /** Custom authentication function for WebSocket upgrade */\n authenticate?: (\n request: unknown,\n ) => Promise<{ userId?: string; organizationId?: string } | null>;\n /** Max clients per resource subscription (default: 10000) */\n maxClientsPerRoom?: number;\n /**\n * Expose a stats endpoint at `{path}/stats`.\n * - `false` (default): stats endpoint is not registered\n * - `true`: registered without auth\n * - `'authenticated'`: guarded by `fastify.authenticate` if available\n */\n exposeStats?: boolean | \"authenticated\";\n /**\n * Authorize room subscriptions. Return true to allow, false to deny.\n * Called before every subscribe. If not provided, all rooms are allowed.\n */\n roomPolicy?: (\n client: WebSocketClient,\n room: string,\n ) => boolean | Promise<boolean>;\n /** Maximum message size in bytes from client (default: 16384 = 16KB). Messages exceeding this are dropped. */\n maxMessageBytes?: number;\n /** Maximum subscriptions per client (default: 100). Prevents resource exhaustion. */\n maxSubscriptionsPerClient?: number;\n /** Custom message handler */\n onMessage?: (\n client: WebSocketClient,\n message: WebSocketMessage,\n ) => void | Promise<void>;\n /** Called when a client connects */\n onConnect?: (client: WebSocketClient) => void | Promise<void>;\n /** Called when a client disconnects */\n onDisconnect?: (client: WebSocketClient) => void | Promise<void>;\n}\n\n// ============================================================================\n// Room Manager — manages subscriptions efficiently\n// ============================================================================\n\nexport class RoomManager {\n private rooms = new Map<string, Set<string>>(); // room → clientIds\n private clients = new Map<string, WebSocketClient>(); // clientId → client\n private maxPerRoom: number;\n\n constructor(maxPerRoom = 10000) {\n this.maxPerRoom = maxPerRoom;\n }\n\n addClient(client: WebSocketClient): void {\n this.clients.set(client.id, client);\n }\n\n removeClient(clientId: string): void {\n const client = this.clients.get(clientId);\n if (!client) return;\n\n // Remove from all rooms\n for (const room of client.subscriptions) {\n const members = this.rooms.get(room);\n if (members) {\n members.delete(clientId);\n if (members.size === 0) this.rooms.delete(room);\n }\n }\n\n client.subscriptions.clear();\n this.clients.delete(clientId);\n }\n\n subscribe(clientId: string, room: string): boolean {\n const client = this.clients.get(clientId);\n if (!client) return false;\n\n // Check room capacity\n const members = this.rooms.get(room);\n if (members && members.size >= this.maxPerRoom) return false;\n\n if (!this.rooms.has(room)) this.rooms.set(room, new Set());\n this.rooms.get(room)!.add(clientId);\n client.subscriptions.add(room);\n return true;\n }\n\n unsubscribe(clientId: string, room: string): void {\n const client = this.clients.get(clientId);\n if (!client) return;\n\n const members = this.rooms.get(room);\n if (members) {\n members.delete(clientId);\n if (members.size === 0) this.rooms.delete(room);\n }\n client.subscriptions.delete(room);\n }\n\n broadcast(room: string, message: string, excludeClientId?: string): void {\n const members = this.rooms.get(room);\n if (!members) return;\n\n for (const clientId of members) {\n if (clientId === excludeClientId) continue;\n const client = this.clients.get(clientId);\n if (client && client.socket.readyState === 1) {\n try {\n client.socket.send(message);\n } catch {\n // Client disconnected, will be cleaned up\n }\n }\n }\n }\n\n broadcastToOrg(organizationId: string, room: string, message: string): void {\n const members = this.rooms.get(room);\n if (!members) return;\n\n for (const clientId of members) {\n const client = this.clients.get(clientId);\n if (\n client &&\n client.organizationId === organizationId &&\n client.socket.readyState === 1\n ) {\n try {\n client.socket.send(message);\n } catch {\n // Client disconnected\n }\n }\n }\n }\n\n getClient(clientId: string): WebSocketClient | undefined {\n return this.clients.get(clientId);\n }\n\n getStats(): {\n clients: number;\n rooms: number;\n subscriptions: Record<string, number>;\n } {\n const subscriptions: Record<string, number> = {};\n for (const [room, members] of this.rooms) {\n subscriptions[room] = members.size;\n }\n return {\n clients: this.clients.size,\n rooms: this.rooms.size,\n subscriptions,\n };\n }\n}\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\nconst websocketPluginImpl: FastifyPluginAsync<WebSocketPluginOptions> = async (\n fastify: FastifyInstance,\n options: WebSocketPluginOptions,\n) => {\n // Instance-scoped counter — no global leak across test runs or multiple app instances\n let clientCounter = 0;\n const {\n path = \"/ws\",\n auth = true,\n resources = [],\n heartbeatInterval = 30000,\n authenticate: customAuth,\n maxClientsPerRoom = 10000,\n roomPolicy,\n maxMessageBytes = 16384,\n maxSubscriptionsPerClient = 100,\n exposeStats = false,\n onMessage,\n onConnect,\n onDisconnect,\n } = options;\n\n // Fail-closed: throw early if auth required but no authenticator available\n if (auth && !customAuth && !fastify.hasDecorator(\"authenticate\")) {\n throw new Error(\n \"[arc-websocket] auth is true but fastify.authenticate is not registered. \" +\n \"Register an auth plugin before WebSocket, provide a custom authenticate function, or set auth: false.\",\n );\n }\n\n const rooms = new RoomManager(maxClientsPerRoom);\n\n // Decorate fastify with room manager for external access\n if (!fastify.hasDecorator(\"ws\")) {\n fastify.decorate(\"ws\", {\n rooms,\n broadcast: (room: string, data: unknown) => {\n rooms.broadcast(\n room,\n JSON.stringify({ type: \"broadcast\", channel: room, data }),\n );\n },\n broadcastToOrg: (orgId: string, room: string, data: unknown) => {\n rooms.broadcastToOrg(\n orgId,\n room,\n JSON.stringify({ type: \"broadcast\", channel: room, data }),\n );\n },\n getStats: () => rooms.getStats(),\n });\n }\n\n // Wire into Arc's event bus for auto-broadcasting resource events\n // Track unsubscribe handles so we can clean up on server close\n const eventUnsubscribers: Array<() => void> = [];\n\n if (resources.length > 0 && fastify.events?.subscribe) {\n for (const resourceName of resources) {\n for (const op of [\"created\", \"updated\", \"deleted\"] as const) {\n const unsub = await fastify.events.subscribe(\n `${resourceName}.${op}`,\n async (event: any) => {\n const room = resourceName;\n const payload = JSON.stringify({\n type: `${resourceName}.${op}`,\n data: event.payload,\n meta: {\n timestamp: event.meta?.timestamp,\n userId: event.meta?.userId,\n organizationId: event.meta?.organizationId,\n },\n });\n\n // If org-scoped, only broadcast to clients in same org\n if (event.meta?.organizationId) {\n rooms.broadcastToOrg(event.meta.organizationId, room, payload);\n } else {\n rooms.broadcast(room, payload);\n }\n },\n );\n eventUnsubscribers.push(unsub);\n }\n }\n }\n\n // Register WebSocket route\n // Requires @fastify/websocket to be registered beforehand\n fastify.get(\n path,\n { websocket: true } as any,\n async (socket: any, request: any) => {\n const clientId = `ws_${++clientCounter}_${Date.now()}`;\n\n // Authentication\n let userId: string | undefined;\n let organizationId: string | undefined;\n\n if (auth) {\n if (customAuth) {\n const result = await customAuth(request);\n if (!result) {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n userId = result.userId;\n organizationId = result.organizationId;\n } else {\n // Run fastify.authenticate to parse token and populate request.user\n // during the WebSocket handshake. Without this, request.user is never\n // set and all authenticated WS connections are rejected.\n if (fastify.authenticate) {\n try {\n // Create a minimal reply-like object for authenticate()\n // that captures the status code without sending a real HTTP response\n let rejected = false;\n const fakeReply = {\n code(_statusCode: number) { rejected = true; return fakeReply; },\n send() { return fakeReply; },\n sent: false,\n };\n await (fastify.authenticate as any)(request, fakeReply);\n if (rejected) {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n } catch {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n }\n\n if (request.user) {\n userId = (request.user as any).id ?? (request.user as any).sub;\n organizationId = (request.scope as any)?.organizationId;\n } else {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n }\n }\n\n const client: WebSocketClient = {\n id: clientId,\n socket,\n subscriptions: new Set(),\n userId,\n organizationId,\n };\n\n rooms.addClient(client);\n await onConnect?.(client);\n\n // Send connection confirmation\n socket.send(\n JSON.stringify({\n type: \"connected\",\n clientId,\n resources: resources,\n }),\n );\n\n // Heartbeat\n let heartbeatTimer: ReturnType<typeof setInterval> | undefined;\n if (heartbeatInterval > 0) {\n heartbeatTimer = setInterval(() => {\n if (socket.readyState === 1) {\n socket.send(\n JSON.stringify({ type: \"ping\", timestamp: Date.now() }),\n );\n }\n }, heartbeatInterval);\n }\n\n // Handle incoming messages\n socket.on(\"message\", async (raw: Buffer | string) => {\n // Message size cap — drop oversized messages\n const rawSize = typeof raw === \"string\" ? Buffer.byteLength(raw) : raw.length;\n if (rawSize > maxMessageBytes) {\n socket.send(\n JSON.stringify({ type: \"error\", error: \"Message too large\" }),\n );\n return;\n }\n\n try {\n const msg: WebSocketMessage = JSON.parse(\n typeof raw === \"string\" ? raw : raw.toString(),\n );\n\n switch (msg.type) {\n case \"subscribe\": {\n const room = msg.resource ?? msg.channel;\n if (room) {\n // Subscription limit per client\n if (client.subscriptions.size >= maxSubscriptionsPerClient) {\n socket.send(\n JSON.stringify({\n type: \"error\",\n channel: room,\n error: \"Subscription limit reached\",\n }),\n );\n break;\n }\n\n // Room authorization policy\n if (roomPolicy) {\n const allowed = await roomPolicy(client, room);\n if (!allowed) {\n socket.send(\n JSON.stringify({\n type: \"error\",\n channel: room,\n error: \"Subscription denied\",\n }),\n );\n break;\n }\n }\n\n const ok = rooms.subscribe(clientId, room);\n socket.send(\n JSON.stringify({\n type: ok ? \"subscribed\" : \"error\",\n channel: room,\n ...(ok ? {} : { error: \"Room at capacity\" }),\n }),\n );\n }\n break;\n }\n\n case \"unsubscribe\": {\n const room = msg.resource ?? msg.channel;\n if (room) {\n rooms.unsubscribe(clientId, room);\n socket.send(\n JSON.stringify({ type: \"unsubscribed\", channel: room }),\n );\n }\n break;\n }\n\n case \"pong\":\n // Heartbeat response, ignore\n break;\n\n default:\n // Forward to custom handler\n await onMessage?.(client, msg);\n break;\n }\n } catch {\n socket.send(\n JSON.stringify({ type: \"error\", error: \"Invalid message format\" }),\n );\n }\n });\n\n // Cleanup on disconnect\n socket.on(\"close\", async () => {\n if (heartbeatTimer) clearInterval(heartbeatTimer);\n await onDisconnect?.(client);\n rooms.removeClient(clientId);\n });\n\n socket.on(\"error\", () => {\n if (heartbeatTimer) clearInterval(heartbeatTimer);\n rooms.removeClient(clientId);\n });\n },\n );\n\n // Stats endpoint (opt-in)\n if (exposeStats === true) {\n fastify.get(`${path}/stats`, async () => {\n return { success: true, data: rooms.getStats() };\n });\n } else if (exposeStats === \"authenticated\") {\n if (fastify.hasDecorator(\"authenticate\")) {\n fastify.get(\n `${path}/stats`,\n { preHandler: fastify.authenticate } as any,\n async () => {\n return { success: true, data: rooms.getStats() };\n },\n );\n } else {\n fastify.log.warn(\n 'arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped',\n );\n }\n }\n\n // Cleanup on server close — unsubscribe event handlers to prevent leaks\n fastify.addHook(\"onClose\", async () => {\n for (const unsub of eventUnsubscribers) {\n unsub();\n }\n eventUnsubscribers.length = 0;\n });\n};\n\n/** Pluggable WebSocket integration for Arc */\nexport const websocketPlugin = fp(websocketPluginImpl, {\n name: \"arc-websocket\",\n fastify: \"5.x\",\n}) as FastifyPluginAsync<WebSocketPluginOptions>;\nexport default websocketPlugin;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"interface-B01JvPVc.d.mts","names":[],"sources":["../src/idempotency/stores/interface.ts"],"mappings":";;AAOA;;;;;UAAiB,iBAAA;EAYA;EAVf,GAAA;EAAA;EAEA,UAAA;EAEA;EAAA,OAAA,EAAS,MAAA;EAET;EAAA,IAAA;EAEW;EAAX,SAAA,EAAW,IAAA;EAEA;EAAX,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,eAAA;EAAe;EAE9B,GAAA;EAMe;EAJf,SAAA;EAAA;EAEA,QAAA,EAAU,IAAA;EAAA;EAEV,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,gBAAA;EAHA;EAAA,SAKN,IAAA;EAFsB;;;;EAQ/B,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,iBAAA;EAMD;;;;EAAzB,GAAA,CAAI,GAAA,UAAa,MAAA,EAAQ,IAAA,CAAK,iBAAA,WAA4B,OAAA;EAerC;;;;EATrB,OAAA,CAAQ,GAAA,UAAa,SAAA,UAAmB,KAAA,WAAgB,OAAA;EA2BvC;EAxBjB,MAAA,CAAO,GAAA,UAAa,SAAA,WAAoB,OAAA;EArB/B;EAwBT,QAAA,CAAS,GAAA,WAAc,OAAA;EAlBnB;EAqBJ,MAAA,CAAO,GAAA,WAAc,OAAA;EArBK;;;;;;EA6B1B,cAAA,CAAe,MAAA,WAAiB,OAAA;EAjBhC;;;;;EAwBA,YAAA,CAAa,MAAA,WAAiB,OAAA,CAAQ,iBAAA;EArB/B;EAwBP,KAAA,KAAU,OAAA;AAAA;;;;iBAMI,uBAAA,CACd,UAAA,UACA,IAAA,WACA,OAAA,EAAS,MAAA,kBACT,KAAA,WACC,IAAA,CAAK,iBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"interface-CZe8IkMf.d.mts","names":[],"sources":["../src/cache/interface.ts"],"mappings":";;AAOA;;;;;UAAiB,WAAA;EACf,IAAA,CAAK,OAAA,aAAoB,IAAA;EACzB,KAAA,CAAM,OAAA,aAAoB,IAAA;AAAA;AAAA,UAGX,eAAA;EAHe;EAK9B,KAAA;AAAA;AAAA,UAGe,UAAA;;EAEf,OAAA;EALK;EAOL,WAAA;EAJyB;EAMzB,IAAA;EANyB;EAQzB,MAAA;EAJA;EAMA,SAAA;AAAA;AAAA,UAGe,UAAA;EAHN;EAAA,SAKA,IAAA;EAFM;;;;EAQf,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,MAAA;EAMF;;;;EAAxB,GAAA,CAAI,GAAA,UAAa,KAAA,EAAO,MAAA,EAAQ,OAAA,GAAU,eAAA,GAAkB,OAAA;EAclD;;;EATV,MAAA,CAAO,GAAA,WAAc,OAAA;EAjBZ;;;;EAuBT,KAAA,KAAU,OAAA;EAXV;EAcA,KAAA,KAAU,UAAA;AAAA"}
|