@classytic/arc 1.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
  4. package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
  5. package/dist/HookSystem-BsGV-j2l.mjs +405 -0
  6. package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
  7. package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
  8. package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
  9. package/dist/adapters/index.d.mts +5 -0
  10. package/dist/adapters/index.mjs +3 -0
  11. package/dist/audit/index.d.mts +82 -0
  12. package/dist/audit/index.d.mts.map +1 -0
  13. package/dist/audit/index.mjs +276 -0
  14. package/dist/audit/index.mjs.map +1 -0
  15. package/dist/audit/mongodb.d.mts +5 -0
  16. package/dist/audit/mongodb.mjs +3 -0
  17. package/dist/audited-C3T5DTUx.mjs +141 -0
  18. package/dist/audited-C3T5DTUx.mjs.map +1 -0
  19. package/dist/auth/index.d.mts +189 -0
  20. package/dist/auth/index.d.mts.map +1 -0
  21. package/dist/auth/index.mjs +1102 -0
  22. package/dist/auth/index.mjs.map +1 -0
  23. package/dist/auth/redis-session.d.mts +44 -0
  24. package/dist/auth/redis-session.d.mts.map +1 -0
  25. package/dist/auth/redis-session.mjs +76 -0
  26. package/dist/auth/redis-session.mjs.map +1 -0
  27. package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
  28. package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
  29. package/dist/cache/index.d.mts +146 -0
  30. package/dist/cache/index.d.mts.map +1 -0
  31. package/dist/cache/index.mjs +92 -0
  32. package/dist/cache/index.mjs.map +1 -0
  33. package/dist/caching-Bl28lYsR.mjs +94 -0
  34. package/dist/caching-Bl28lYsR.mjs.map +1 -0
  35. package/dist/chunk-C7Uep-_p.mjs +20 -0
  36. package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
  37. package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
  38. package/dist/cli/commands/describe.d.mts +19 -0
  39. package/dist/cli/commands/describe.d.mts.map +1 -0
  40. package/dist/cli/commands/describe.mjs +239 -0
  41. package/dist/cli/commands/describe.mjs.map +1 -0
  42. package/dist/cli/commands/docs.d.mts +14 -0
  43. package/dist/cli/commands/docs.d.mts.map +1 -0
  44. package/dist/cli/commands/docs.mjs +53 -0
  45. package/dist/cli/commands/docs.mjs.map +1 -0
  46. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
  47. package/dist/cli/commands/generate.d.mts.map +1 -0
  48. package/dist/cli/commands/generate.mjs +358 -0
  49. package/dist/cli/commands/generate.mjs.map +1 -0
  50. package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
  51. package/dist/cli/commands/init.d.mts.map +1 -0
  52. package/dist/cli/commands/{init.js → init.mjs} +807 -616
  53. package/dist/cli/commands/init.mjs.map +1 -0
  54. package/dist/cli/commands/introspect.d.mts +11 -0
  55. package/dist/cli/commands/introspect.d.mts.map +1 -0
  56. package/dist/cli/commands/introspect.mjs +76 -0
  57. package/dist/cli/commands/introspect.mjs.map +1 -0
  58. package/dist/cli/index.d.mts +17 -0
  59. package/dist/cli/index.d.mts.map +1 -0
  60. package/dist/cli/index.mjs +157 -0
  61. package/dist/cli/index.mjs.map +1 -0
  62. package/dist/constants-DdXFXQtN.mjs +85 -0
  63. package/dist/constants-DdXFXQtN.mjs.map +1 -0
  64. package/dist/core/index.d.mts +5 -0
  65. package/dist/core/index.mjs +4 -0
  66. package/dist/createApp-CUgNqegw.mjs +560 -0
  67. package/dist/createApp-CUgNqegw.mjs.map +1 -0
  68. package/dist/defineResource-k0_BDn8v.mjs +2197 -0
  69. package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
  70. package/dist/discovery/index.d.mts +47 -0
  71. package/dist/discovery/index.d.mts.map +1 -0
  72. package/dist/discovery/index.mjs +110 -0
  73. package/dist/discovery/index.mjs.map +1 -0
  74. package/dist/docs/index.d.mts +163 -0
  75. package/dist/docs/index.d.mts.map +1 -0
  76. package/dist/docs/index.mjs +73 -0
  77. package/dist/docs/index.mjs.map +1 -0
  78. package/dist/elevation-BRy3yFWT.mjs +113 -0
  79. package/dist/elevation-BRy3yFWT.mjs.map +1 -0
  80. package/dist/elevation-B_2dRLVP.d.mts +88 -0
  81. package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
  82. package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
  83. package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
  84. package/dist/errorHandler-C1okiriz.mjs +109 -0
  85. package/dist/errorHandler-C1okiriz.mjs.map +1 -0
  86. package/dist/errors-B9bZok84.mjs +212 -0
  87. package/dist/errors-B9bZok84.mjs.map +1 -0
  88. package/dist/errors-ChKiFz62.d.mts +125 -0
  89. package/dist/errors-ChKiFz62.d.mts.map +1 -0
  90. package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
  91. package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
  92. package/dist/eventPlugin-DGR_B2on.mjs +230 -0
  93. package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
  94. package/dist/events/index.d.mts +54 -0
  95. package/dist/events/index.d.mts.map +1 -0
  96. package/dist/events/index.mjs +52 -0
  97. package/dist/events/index.mjs.map +1 -0
  98. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  99. package/dist/events/transports/redis-stream-entry.mjs +178 -0
  100. package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
  101. package/dist/events/transports/redis.d.mts +77 -0
  102. package/dist/events/transports/redis.d.mts.map +1 -0
  103. package/dist/events/transports/redis.mjs +125 -0
  104. package/dist/events/transports/redis.mjs.map +1 -0
  105. package/dist/externalPaths-DlINfKbP.d.mts +51 -0
  106. package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
  107. package/dist/factory/index.d.mts +64 -0
  108. package/dist/factory/index.d.mts.map +1 -0
  109. package/dist/factory/index.mjs +3 -0
  110. package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
  111. package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
  112. package/dist/fields-DyaDVX4J.d.mts +110 -0
  113. package/dist/fields-DyaDVX4J.d.mts.map +1 -0
  114. package/dist/fields-iagOozy0.mjs +115 -0
  115. package/dist/fields-iagOozy0.mjs.map +1 -0
  116. package/dist/hooks/index.d.mts +4 -0
  117. package/dist/hooks/index.mjs +3 -0
  118. package/dist/idempotency/index.d.mts +97 -0
  119. package/dist/idempotency/index.d.mts.map +1 -0
  120. package/dist/idempotency/index.mjs +320 -0
  121. package/dist/idempotency/index.mjs.map +1 -0
  122. package/dist/idempotency/mongodb.d.mts +2 -0
  123. package/dist/idempotency/mongodb.mjs +115 -0
  124. package/dist/idempotency/mongodb.mjs.map +1 -0
  125. package/dist/idempotency/redis.d.mts +2 -0
  126. package/dist/idempotency/redis.mjs +104 -0
  127. package/dist/idempotency/redis.mjs.map +1 -0
  128. package/dist/index.d.mts +261 -0
  129. package/dist/index.d.mts.map +1 -0
  130. package/dist/index.mjs +105 -0
  131. package/dist/index.mjs.map +1 -0
  132. package/dist/integrations/event-gateway.d.mts +47 -0
  133. package/dist/integrations/event-gateway.d.mts.map +1 -0
  134. package/dist/integrations/event-gateway.mjs +44 -0
  135. package/dist/integrations/event-gateway.mjs.map +1 -0
  136. package/dist/integrations/index.d.mts +5 -0
  137. package/dist/integrations/index.mjs +1 -0
  138. package/dist/integrations/jobs.d.mts +104 -0
  139. package/dist/integrations/jobs.d.mts.map +1 -0
  140. package/dist/integrations/jobs.mjs +124 -0
  141. package/dist/integrations/jobs.mjs.map +1 -0
  142. package/dist/integrations/streamline.d.mts +61 -0
  143. package/dist/integrations/streamline.d.mts.map +1 -0
  144. package/dist/integrations/streamline.mjs +126 -0
  145. package/dist/integrations/streamline.mjs.map +1 -0
  146. package/dist/integrations/websocket.d.mts +83 -0
  147. package/dist/integrations/websocket.d.mts.map +1 -0
  148. package/dist/integrations/websocket.mjs +289 -0
  149. package/dist/integrations/websocket.mjs.map +1 -0
  150. package/dist/interface-B01JvPVc.d.mts +78 -0
  151. package/dist/interface-B01JvPVc.d.mts.map +1 -0
  152. package/dist/interface-CZe8IkMf.d.mts +55 -0
  153. package/dist/interface-CZe8IkMf.d.mts.map +1 -0
  154. package/dist/interface-Ch8HU9uM.d.mts +1098 -0
  155. package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
  156. package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
  157. package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
  158. package/dist/keys-BqNejWup.mjs +43 -0
  159. package/dist/keys-BqNejWup.mjs.map +1 -0
  160. package/dist/logger-Df2O2WsW.mjs +79 -0
  161. package/dist/logger-Df2O2WsW.mjs.map +1 -0
  162. package/dist/memory-cQgelFOj.mjs +144 -0
  163. package/dist/memory-cQgelFOj.mjs.map +1 -0
  164. package/dist/migrations/index.d.mts +157 -0
  165. package/dist/migrations/index.d.mts.map +1 -0
  166. package/dist/migrations/index.mjs +261 -0
  167. package/dist/migrations/index.mjs.map +1 -0
  168. package/dist/mongodb-BfJVlUJH.mjs +94 -0
  169. package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
  170. package/dist/mongodb-CGzRbfAK.d.mts +119 -0
  171. package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
  172. package/dist/mongodb-JN-9JA7K.d.mts +72 -0
  173. package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
  174. package/dist/openapi-G3Cw7XuM.mjs +524 -0
  175. package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
  176. package/dist/org/index.d.mts +69 -0
  177. package/dist/org/index.d.mts.map +1 -0
  178. package/dist/org/index.mjs +514 -0
  179. package/dist/org/index.mjs.map +1 -0
  180. package/dist/org/types.d.mts +83 -0
  181. package/dist/org/types.d.mts.map +1 -0
  182. package/dist/org/types.mjs +1 -0
  183. package/dist/permissions/index.d.mts +279 -0
  184. package/dist/permissions/index.d.mts.map +1 -0
  185. package/dist/permissions/index.mjs +579 -0
  186. package/dist/permissions/index.mjs.map +1 -0
  187. package/dist/plugins/index.d.mts +173 -0
  188. package/dist/plugins/index.d.mts.map +1 -0
  189. package/dist/plugins/index.mjs +523 -0
  190. package/dist/plugins/index.mjs.map +1 -0
  191. package/dist/plugins/response-cache.d.mts +88 -0
  192. package/dist/plugins/response-cache.d.mts.map +1 -0
  193. package/dist/plugins/response-cache.mjs +284 -0
  194. package/dist/plugins/response-cache.mjs.map +1 -0
  195. package/dist/plugins/tracing-entry.d.mts +2 -0
  196. package/dist/plugins/tracing-entry.mjs +186 -0
  197. package/dist/plugins/tracing-entry.mjs.map +1 -0
  198. package/dist/pluralize-CEweyOEm.mjs +87 -0
  199. package/dist/pluralize-CEweyOEm.mjs.map +1 -0
  200. package/dist/policies/{index.d.ts → index.d.mts} +204 -169
  201. package/dist/policies/index.d.mts.map +1 -0
  202. package/dist/policies/index.mjs +322 -0
  203. package/dist/policies/index.mjs.map +1 -0
  204. package/dist/presets/{index.d.ts → index.d.mts} +63 -131
  205. package/dist/presets/index.d.mts.map +1 -0
  206. package/dist/presets/index.mjs +144 -0
  207. package/dist/presets/index.mjs.map +1 -0
  208. package/dist/presets/multiTenant.d.mts +25 -0
  209. package/dist/presets/multiTenant.d.mts.map +1 -0
  210. package/dist/presets/multiTenant.mjs +114 -0
  211. package/dist/presets/multiTenant.mjs.map +1 -0
  212. package/dist/presets-BITljm96.mjs +120 -0
  213. package/dist/presets-BITljm96.mjs.map +1 -0
  214. package/dist/presets-DzSMwlKj.d.mts +58 -0
  215. package/dist/presets-DzSMwlKj.d.mts.map +1 -0
  216. package/dist/prisma-DJbMt3yf.mjs +628 -0
  217. package/dist/prisma-DJbMt3yf.mjs.map +1 -0
  218. package/dist/prisma-Dg9GoVdj.d.mts +275 -0
  219. package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
  220. package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
  221. package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
  222. package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
  223. package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
  224. package/dist/redis-D-JAeLtm.d.mts +50 -0
  225. package/dist/redis-D-JAeLtm.d.mts.map +1 -0
  226. package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
  227. package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
  228. package/dist/registry/index.d.mts +12 -0
  229. package/dist/registry/index.d.mts.map +1 -0
  230. package/dist/registry/index.mjs +4 -0
  231. package/dist/requestContext-QQD6ROJc.mjs +56 -0
  232. package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
  233. package/dist/schemaConverter-BwrmWroW.mjs +99 -0
  234. package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
  235. package/dist/schemas/index.d.mts +64 -0
  236. package/dist/schemas/index.d.mts.map +1 -0
  237. package/dist/schemas/index.mjs +83 -0
  238. package/dist/schemas/index.mjs.map +1 -0
  239. package/dist/scope/index.d.mts +22 -0
  240. package/dist/scope/index.d.mts.map +1 -0
  241. package/dist/scope/index.mjs +66 -0
  242. package/dist/scope/index.mjs.map +1 -0
  243. package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
  244. package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
  245. package/dist/sse-B3c3_yZp.mjs +124 -0
  246. package/dist/sse-B3c3_yZp.mjs.map +1 -0
  247. package/dist/testing/index.d.mts +908 -0
  248. package/dist/testing/index.d.mts.map +1 -0
  249. package/dist/testing/index.mjs +1977 -0
  250. package/dist/testing/index.mjs.map +1 -0
  251. package/dist/tracing-Cc7vVQPp.d.mts +71 -0
  252. package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
  253. package/dist/typeGuards-DhMNLuvU.mjs +10 -0
  254. package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
  255. package/dist/types/index.d.mts +947 -0
  256. package/dist/types/index.d.mts.map +1 -0
  257. package/dist/types/index.mjs +15 -0
  258. package/dist/types/index.mjs.map +1 -0
  259. package/dist/types-Beqn1Un7.mjs +39 -0
  260. package/dist/types-Beqn1Un7.mjs.map +1 -0
  261. package/dist/types-CIgB7UUl.d.mts +446 -0
  262. package/dist/types-CIgB7UUl.d.mts.map +1 -0
  263. package/dist/types-aYB4V7uN.d.mts +87 -0
  264. package/dist/types-aYB4V7uN.d.mts.map +1 -0
  265. package/dist/utils/index.d.mts +748 -0
  266. package/dist/utils/index.d.mts.map +1 -0
  267. package/dist/utils/index.mjs +6 -0
  268. package/package.json +194 -68
  269. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  270. package/dist/adapters/index.d.ts +0 -237
  271. package/dist/adapters/index.js +0 -668
  272. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  273. package/dist/audit/index.d.ts +0 -195
  274. package/dist/audit/index.js +0 -319
  275. package/dist/auth/index.d.ts +0 -47
  276. package/dist/auth/index.js +0 -174
  277. package/dist/cli/commands/docs.d.ts +0 -11
  278. package/dist/cli/commands/docs.js +0 -474
  279. package/dist/cli/commands/generate.js +0 -334
  280. package/dist/cli/commands/introspect.d.ts +0 -8
  281. package/dist/cli/commands/introspect.js +0 -338
  282. package/dist/cli/index.d.ts +0 -4
  283. package/dist/cli/index.js +0 -3269
  284. package/dist/core/index.d.ts +0 -220
  285. package/dist/core/index.js +0 -2786
  286. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  287. package/dist/docs/index.d.ts +0 -166
  288. package/dist/docs/index.js +0 -658
  289. package/dist/errors-8WIxGS_6.d.ts +0 -122
  290. package/dist/events/index.d.ts +0 -117
  291. package/dist/events/index.js +0 -89
  292. package/dist/factory/index.d.ts +0 -38
  293. package/dist/factory/index.js +0 -1652
  294. package/dist/hooks/index.d.ts +0 -4
  295. package/dist/hooks/index.js +0 -199
  296. package/dist/idempotency/index.d.ts +0 -323
  297. package/dist/idempotency/index.js +0 -500
  298. package/dist/index-B4t03KQ0.d.ts +0 -1366
  299. package/dist/index.d.ts +0 -135
  300. package/dist/index.js +0 -4756
  301. package/dist/migrations/index.d.ts +0 -185
  302. package/dist/migrations/index.js +0 -274
  303. package/dist/org/index.d.ts +0 -129
  304. package/dist/org/index.js +0 -220
  305. package/dist/permissions/index.d.ts +0 -144
  306. package/dist/permissions/index.js +0 -103
  307. package/dist/plugins/index.d.ts +0 -46
  308. package/dist/plugins/index.js +0 -1069
  309. package/dist/policies/index.js +0 -196
  310. package/dist/presets/index.js +0 -384
  311. package/dist/presets/multiTenant.d.ts +0 -39
  312. package/dist/presets/multiTenant.js +0 -112
  313. package/dist/registry/index.d.ts +0 -16
  314. package/dist/registry/index.js +0 -253
  315. package/dist/testing/index.d.ts +0 -618
  316. package/dist/testing/index.js +0 -48020
  317. package/dist/types/index.d.ts +0 -4
  318. package/dist/types/index.js +0 -8
  319. package/dist/types-B99TBmFV.d.ts +0 -76
  320. package/dist/types-BvckRbs2.d.ts +0 -143
  321. package/dist/utils/index.d.ts +0 -679
  322. package/dist/utils/index.js +0 -931
@@ -0,0 +1,126 @@
1
+ //#region src/integrations/streamline.ts
2
+ const streamlinePluginImpl = async (fastify, options) => {
3
+ const { workflows, prefix = "/workflows", auth = true, bridgeEvents = true, permissions: perms } = options;
4
+ const registry = /* @__PURE__ */ new Map();
5
+ for (const wf of workflows) {
6
+ const id = wf.definition.id;
7
+ if (registry.has(id)) throw new Error(`Duplicate workflow ID: '${id}'`);
8
+ registry.set(id, wf);
9
+ }
10
+ if (!fastify.hasDecorator("workflows")) fastify.decorate("workflows", registry);
11
+ if (!fastify.hasDecorator("getWorkflow")) fastify.decorate("getWorkflow", (id) => registry.get(id) ?? null);
12
+ const authPreHandler = auth && typeof fastify.authenticate === "function" ? [fastify.authenticate] : [];
13
+ const checkPerm = async (op, request) => {
14
+ const check = perms?.[op];
15
+ if (!check) return true;
16
+ return check(request);
17
+ };
18
+ for (const [id, wf] of registry) {
19
+ const routePrefix = `${prefix}/${id}`;
20
+ fastify.post(`${routePrefix}/start`, { preHandler: authPreHandler }, async (request, reply) => {
21
+ if (!await checkPerm("start", request)) return reply.status(403).send({
22
+ success: false,
23
+ error: "Forbidden"
24
+ });
25
+ const { input, meta } = request.body ?? {};
26
+ const run = await wf.start(input, meta);
27
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.started`, {
28
+ runId: run._id,
29
+ workflowId: id,
30
+ status: run.status
31
+ });
32
+ return reply.status(201).send({
33
+ success: true,
34
+ data: run
35
+ });
36
+ });
37
+ fastify.get(`${routePrefix}/runs/:runId`, { preHandler: authPreHandler }, async (request, reply) => {
38
+ if (!await checkPerm("get", request)) return reply.status(403).send({
39
+ success: false,
40
+ error: "Forbidden"
41
+ });
42
+ const { runId } = request.params;
43
+ const run = await wf.get(runId);
44
+ if (!run) return reply.status(404).send({
45
+ success: false,
46
+ error: "Workflow run not found"
47
+ });
48
+ return {
49
+ success: true,
50
+ data: run
51
+ };
52
+ });
53
+ fastify.post(`${routePrefix}/runs/:runId/resume`, { preHandler: authPreHandler }, async (request, reply) => {
54
+ if (!await checkPerm("resume", request)) return reply.status(403).send({
55
+ success: false,
56
+ error: "Forbidden"
57
+ });
58
+ const { runId } = request.params;
59
+ const { payload } = request.body ?? {};
60
+ const run = await wf.resume(runId, payload);
61
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.resumed`, {
62
+ runId: run._id,
63
+ workflowId: id,
64
+ status: run.status
65
+ });
66
+ return {
67
+ success: true,
68
+ data: run
69
+ };
70
+ });
71
+ fastify.post(`${routePrefix}/runs/:runId/cancel`, { preHandler: authPreHandler }, async (request, reply) => {
72
+ if (!await checkPerm("cancel", request)) return reply.status(403).send({
73
+ success: false,
74
+ error: "Forbidden"
75
+ });
76
+ const { runId } = request.params;
77
+ const run = await wf.cancel(runId);
78
+ if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.cancelled`, {
79
+ runId: run._id,
80
+ workflowId: id
81
+ });
82
+ return {
83
+ success: true,
84
+ data: run
85
+ };
86
+ });
87
+ if (wf.engine.pause) fastify.post(`${routePrefix}/runs/:runId/pause`, { preHandler: authPreHandler }, async (request, reply) => {
88
+ const { runId } = request.params;
89
+ return {
90
+ success: true,
91
+ data: await wf.engine.pause(runId)
92
+ };
93
+ });
94
+ if (wf.engine.rewindTo) fastify.post(`${routePrefix}/runs/:runId/rewind`, { preHandler: authPreHandler }, async (request, reply) => {
95
+ const { runId } = request.params;
96
+ const { stepId } = request.body ?? {};
97
+ if (!stepId) return reply.status(400).send({
98
+ success: false,
99
+ error: "stepId is required"
100
+ });
101
+ return {
102
+ success: true,
103
+ data: await wf.engine.rewindTo(runId, stepId)
104
+ };
105
+ });
106
+ }
107
+ fastify.get(prefix, { preHandler: authPreHandler }, async () => {
108
+ return {
109
+ success: true,
110
+ data: Array.from(registry.entries()).map(([id, wf]) => ({
111
+ id,
112
+ name: wf.definition.name ?? id,
113
+ steps: Object.keys(wf.definition.steps)
114
+ }))
115
+ };
116
+ });
117
+ fastify.addHook("onClose", async () => {
118
+ for (const wf of registry.values()) wf.shutdown?.();
119
+ });
120
+ };
121
+ /** Pluggable streamline integration for Arc */
122
+ const streamlinePlugin = streamlinePluginImpl;
123
+
124
+ //#endregion
125
+ export { streamlinePlugin as default, streamlinePlugin };
126
+ //# sourceMappingURL=streamline.mjs.map
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,83 @@
1
+ import { FastifyPluginAsync } from "fastify";
2
+
3
+ //#region src/integrations/websocket.d.ts
4
+ interface WebSocketClient {
5
+ id: string;
6
+ socket: {
7
+ send(data: string): void;
8
+ close(): void;
9
+ readyState: number;
10
+ };
11
+ subscriptions: Set<string>;
12
+ userId?: string;
13
+ organizationId?: string;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+ interface WebSocketMessage {
17
+ type: string;
18
+ resource?: string;
19
+ channel?: string;
20
+ data?: unknown;
21
+ }
22
+ interface WebSocketPluginOptions {
23
+ /** WebSocket endpoint path (default: '/ws') */
24
+ path?: string;
25
+ /** Require authentication for WebSocket connections (default: true) */
26
+ auth?: boolean;
27
+ /** Resources to auto-broadcast CRUD events for */
28
+ resources?: string[];
29
+ /** Heartbeat interval in ms (default: 30000). Set 0 to disable. */
30
+ heartbeatInterval?: number;
31
+ /** Custom authentication function for WebSocket upgrade */
32
+ authenticate?: (request: unknown) => Promise<{
33
+ userId?: string;
34
+ organizationId?: string;
35
+ } | null>;
36
+ /** Max clients per resource subscription (default: 10000) */
37
+ maxClientsPerRoom?: number;
38
+ /**
39
+ * Expose a stats endpoint at `{path}/stats`.
40
+ * - `false` (default): stats endpoint is not registered
41
+ * - `true`: registered without auth
42
+ * - `'authenticated'`: guarded by `fastify.authenticate` if available
43
+ */
44
+ exposeStats?: boolean | "authenticated";
45
+ /**
46
+ * Authorize room subscriptions. Return true to allow, false to deny.
47
+ * Called before every subscribe. If not provided, all rooms are allowed.
48
+ */
49
+ roomPolicy?: (client: WebSocketClient, room: string) => boolean | Promise<boolean>;
50
+ /** Maximum message size in bytes from client (default: 16384 = 16KB). Messages exceeding this are dropped. */
51
+ maxMessageBytes?: number;
52
+ /** Maximum subscriptions per client (default: 100). Prevents resource exhaustion. */
53
+ maxSubscriptionsPerClient?: number;
54
+ /** Custom message handler */
55
+ onMessage?: (client: WebSocketClient, message: WebSocketMessage) => void | Promise<void>;
56
+ /** Called when a client connects */
57
+ onConnect?: (client: WebSocketClient) => void | Promise<void>;
58
+ /** Called when a client disconnects */
59
+ onDisconnect?: (client: WebSocketClient) => void | Promise<void>;
60
+ }
61
+ declare class RoomManager {
62
+ private rooms;
63
+ private clients;
64
+ private maxPerRoom;
65
+ constructor(maxPerRoom?: number);
66
+ addClient(client: WebSocketClient): void;
67
+ removeClient(clientId: string): void;
68
+ subscribe(clientId: string, room: string): boolean;
69
+ unsubscribe(clientId: string, room: string): void;
70
+ broadcast(room: string, message: string, excludeClientId?: string): void;
71
+ broadcastToOrg(organizationId: string, room: string, message: string): void;
72
+ getClient(clientId: string): WebSocketClient | undefined;
73
+ getStats(): {
74
+ clients: number;
75
+ rooms: number;
76
+ subscriptions: Record<string, number>;
77
+ };
78
+ }
79
+ /** Pluggable WebSocket integration for Arc */
80
+ declare const websocketPlugin: FastifyPluginAsync<WebSocketPluginOptions>;
81
+ //#endregion
82
+ export { RoomManager, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin as default, websocketPlugin };
83
+ //# sourceMappingURL=websocket.d.mts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,289 @@
1
+ import fp from "fastify-plugin";
2
+
3
+ //#region src/integrations/websocket.ts
4
+ var RoomManager = class {
5
+ rooms = /* @__PURE__ */ new Map();
6
+ clients = /* @__PURE__ */ new Map();
7
+ maxPerRoom;
8
+ constructor(maxPerRoom = 1e4) {
9
+ this.maxPerRoom = maxPerRoom;
10
+ }
11
+ addClient(client) {
12
+ this.clients.set(client.id, client);
13
+ }
14
+ removeClient(clientId) {
15
+ const client = this.clients.get(clientId);
16
+ if (!client) return;
17
+ for (const room of client.subscriptions) {
18
+ const members = this.rooms.get(room);
19
+ if (members) {
20
+ members.delete(clientId);
21
+ if (members.size === 0) this.rooms.delete(room);
22
+ }
23
+ }
24
+ client.subscriptions.clear();
25
+ this.clients.delete(clientId);
26
+ }
27
+ subscribe(clientId, room) {
28
+ const client = this.clients.get(clientId);
29
+ if (!client) return false;
30
+ const members = this.rooms.get(room);
31
+ if (members && members.size >= this.maxPerRoom) return false;
32
+ if (!this.rooms.has(room)) this.rooms.set(room, /* @__PURE__ */ new Set());
33
+ this.rooms.get(room).add(clientId);
34
+ client.subscriptions.add(room);
35
+ return true;
36
+ }
37
+ unsubscribe(clientId, room) {
38
+ const client = this.clients.get(clientId);
39
+ if (!client) return;
40
+ const members = this.rooms.get(room);
41
+ if (members) {
42
+ members.delete(clientId);
43
+ if (members.size === 0) this.rooms.delete(room);
44
+ }
45
+ client.subscriptions.delete(room);
46
+ }
47
+ broadcast(room, message, excludeClientId) {
48
+ const members = this.rooms.get(room);
49
+ if (!members) return;
50
+ for (const clientId of members) {
51
+ if (clientId === excludeClientId) continue;
52
+ const client = this.clients.get(clientId);
53
+ if (client && client.socket.readyState === 1) try {
54
+ client.socket.send(message);
55
+ } catch {}
56
+ }
57
+ }
58
+ broadcastToOrg(organizationId, room, message) {
59
+ const members = this.rooms.get(room);
60
+ if (!members) return;
61
+ for (const clientId of members) {
62
+ const client = this.clients.get(clientId);
63
+ if (client && client.organizationId === organizationId && client.socket.readyState === 1) try {
64
+ client.socket.send(message);
65
+ } catch {}
66
+ }
67
+ }
68
+ getClient(clientId) {
69
+ return this.clients.get(clientId);
70
+ }
71
+ getStats() {
72
+ const subscriptions = {};
73
+ for (const [room, members] of this.rooms) subscriptions[room] = members.size;
74
+ return {
75
+ clients: this.clients.size,
76
+ rooms: this.rooms.size,
77
+ subscriptions
78
+ };
79
+ }
80
+ };
81
+ const websocketPluginImpl = async (fastify, options) => {
82
+ let clientCounter = 0;
83
+ const { path = "/ws", auth = true, resources = [], heartbeatInterval = 3e4, authenticate: customAuth, maxClientsPerRoom = 1e4, roomPolicy, maxMessageBytes = 16384, maxSubscriptionsPerClient = 100, exposeStats = false, onMessage, onConnect, onDisconnect } = options;
84
+ if (auth && !customAuth && !fastify.hasDecorator("authenticate")) throw new Error("[arc-websocket] auth is true but fastify.authenticate is not registered. Register an auth plugin before WebSocket, provide a custom authenticate function, or set auth: false.");
85
+ const rooms = new RoomManager(maxClientsPerRoom);
86
+ if (!fastify.hasDecorator("ws")) fastify.decorate("ws", {
87
+ rooms,
88
+ broadcast: (room, data) => {
89
+ rooms.broadcast(room, JSON.stringify({
90
+ type: "broadcast",
91
+ channel: room,
92
+ data
93
+ }));
94
+ },
95
+ broadcastToOrg: (orgId, room, data) => {
96
+ rooms.broadcastToOrg(orgId, room, JSON.stringify({
97
+ type: "broadcast",
98
+ channel: room,
99
+ data
100
+ }));
101
+ },
102
+ getStats: () => rooms.getStats()
103
+ });
104
+ const eventUnsubscribers = [];
105
+ if (resources.length > 0 && fastify.events?.subscribe) for (const resourceName of resources) for (const op of [
106
+ "created",
107
+ "updated",
108
+ "deleted"
109
+ ]) {
110
+ const unsub = await fastify.events.subscribe(`${resourceName}.${op}`, async (event) => {
111
+ const room = resourceName;
112
+ const payload = JSON.stringify({
113
+ type: `${resourceName}.${op}`,
114
+ data: event.payload,
115
+ meta: {
116
+ timestamp: event.meta?.timestamp,
117
+ userId: event.meta?.userId,
118
+ organizationId: event.meta?.organizationId
119
+ }
120
+ });
121
+ if (event.meta?.organizationId) rooms.broadcastToOrg(event.meta.organizationId, room, payload);
122
+ else rooms.broadcast(room, payload);
123
+ });
124
+ eventUnsubscribers.push(unsub);
125
+ }
126
+ fastify.get(path, { websocket: true }, async (socket, request) => {
127
+ const clientId = `ws_${++clientCounter}_${Date.now()}`;
128
+ let userId;
129
+ let organizationId;
130
+ if (auth) if (customAuth) {
131
+ const result = await customAuth(request);
132
+ if (!result) {
133
+ socket.close(4001, "Unauthorized");
134
+ return;
135
+ }
136
+ userId = result.userId;
137
+ organizationId = result.organizationId;
138
+ } else {
139
+ if (fastify.authenticate) try {
140
+ let rejected = false;
141
+ const fakeReply = {
142
+ code(_statusCode) {
143
+ rejected = true;
144
+ return fakeReply;
145
+ },
146
+ send() {
147
+ return fakeReply;
148
+ },
149
+ sent: false
150
+ };
151
+ await fastify.authenticate(request, fakeReply);
152
+ if (rejected) {
153
+ socket.close(4001, "Unauthorized");
154
+ return;
155
+ }
156
+ } catch {
157
+ socket.close(4001, "Unauthorized");
158
+ return;
159
+ }
160
+ if (request.user) {
161
+ userId = request.user.id ?? request.user.sub;
162
+ organizationId = request.scope?.organizationId;
163
+ } else {
164
+ socket.close(4001, "Unauthorized");
165
+ return;
166
+ }
167
+ }
168
+ const client = {
169
+ id: clientId,
170
+ socket,
171
+ subscriptions: /* @__PURE__ */ new Set(),
172
+ userId,
173
+ organizationId
174
+ };
175
+ rooms.addClient(client);
176
+ await onConnect?.(client);
177
+ socket.send(JSON.stringify({
178
+ type: "connected",
179
+ clientId,
180
+ resources
181
+ }));
182
+ let heartbeatTimer;
183
+ if (heartbeatInterval > 0) heartbeatTimer = setInterval(() => {
184
+ if (socket.readyState === 1) socket.send(JSON.stringify({
185
+ type: "ping",
186
+ timestamp: Date.now()
187
+ }));
188
+ }, heartbeatInterval);
189
+ socket.on("message", async (raw) => {
190
+ if ((typeof raw === "string" ? Buffer.byteLength(raw) : raw.length) > maxMessageBytes) {
191
+ socket.send(JSON.stringify({
192
+ type: "error",
193
+ error: "Message too large"
194
+ }));
195
+ return;
196
+ }
197
+ try {
198
+ const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString());
199
+ switch (msg.type) {
200
+ case "subscribe": {
201
+ const room = msg.resource ?? msg.channel;
202
+ if (room) {
203
+ if (client.subscriptions.size >= maxSubscriptionsPerClient) {
204
+ socket.send(JSON.stringify({
205
+ type: "error",
206
+ channel: room,
207
+ error: "Subscription limit reached"
208
+ }));
209
+ break;
210
+ }
211
+ if (roomPolicy) {
212
+ if (!await roomPolicy(client, room)) {
213
+ socket.send(JSON.stringify({
214
+ type: "error",
215
+ channel: room,
216
+ error: "Subscription denied"
217
+ }));
218
+ break;
219
+ }
220
+ }
221
+ const ok = rooms.subscribe(clientId, room);
222
+ socket.send(JSON.stringify({
223
+ type: ok ? "subscribed" : "error",
224
+ channel: room,
225
+ ...ok ? {} : { error: "Room at capacity" }
226
+ }));
227
+ }
228
+ break;
229
+ }
230
+ case "unsubscribe": {
231
+ const room = msg.resource ?? msg.channel;
232
+ if (room) {
233
+ rooms.unsubscribe(clientId, room);
234
+ socket.send(JSON.stringify({
235
+ type: "unsubscribed",
236
+ channel: room
237
+ }));
238
+ }
239
+ break;
240
+ }
241
+ case "pong": break;
242
+ default:
243
+ await onMessage?.(client, msg);
244
+ break;
245
+ }
246
+ } catch {
247
+ socket.send(JSON.stringify({
248
+ type: "error",
249
+ error: "Invalid message format"
250
+ }));
251
+ }
252
+ });
253
+ socket.on("close", async () => {
254
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
255
+ await onDisconnect?.(client);
256
+ rooms.removeClient(clientId);
257
+ });
258
+ socket.on("error", () => {
259
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
260
+ rooms.removeClient(clientId);
261
+ });
262
+ });
263
+ if (exposeStats === true) fastify.get(`${path}/stats`, async () => {
264
+ return {
265
+ success: true,
266
+ data: rooms.getStats()
267
+ };
268
+ });
269
+ else if (exposeStats === "authenticated") if (fastify.hasDecorator("authenticate")) fastify.get(`${path}/stats`, { preHandler: fastify.authenticate }, async () => {
270
+ return {
271
+ success: true,
272
+ data: rooms.getStats()
273
+ };
274
+ });
275
+ else fastify.log.warn("arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped");
276
+ fastify.addHook("onClose", async () => {
277
+ for (const unsub of eventUnsubscribers) unsub();
278
+ eventUnsubscribers.length = 0;
279
+ });
280
+ };
281
+ /** Pluggable WebSocket integration for Arc */
282
+ const websocketPlugin = fp(websocketPluginImpl, {
283
+ name: "arc-websocket",
284
+ fastify: "5.x"
285
+ });
286
+
287
+ //#endregion
288
+ export { RoomManager, websocketPlugin as default, websocketPlugin };
289
+ //# sourceMappingURL=websocket.mjs.map
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,78 @@
1
+ //#region src/idempotency/stores/interface.d.ts
2
+ /**
3
+ * Idempotency Store Interface
4
+ *
5
+ * Defines the contract for idempotency key storage backends.
6
+ * Implement this interface for custom stores (Redis, DynamoDB, etc.)
7
+ */
8
+ interface IdempotencyResult {
9
+ /** The idempotency key */
10
+ key: string;
11
+ /** HTTP status code of the cached response */
12
+ statusCode: number;
13
+ /** Response headers to replay */
14
+ headers: Record<string, string>;
15
+ /** Response body */
16
+ body: unknown;
17
+ /** When this entry was created */
18
+ createdAt: Date;
19
+ /** When this entry expires */
20
+ expiresAt: Date;
21
+ }
22
+ interface IdempotencyLock {
23
+ /** The idempotency key being locked */
24
+ key: string;
25
+ /** Request ID that holds the lock */
26
+ requestId: string;
27
+ /** When the lock was acquired */
28
+ lockedAt: Date;
29
+ /** When the lock expires (auto-release) */
30
+ expiresAt: Date;
31
+ }
32
+ interface IdempotencyStore {
33
+ /** Store name for logging */
34
+ readonly name: string;
35
+ /**
36
+ * Get a cached result for an idempotency key.
37
+ * Returns undefined if not found or expired.
38
+ */
39
+ get(key: string): Promise<IdempotencyResult | undefined>;
40
+ /**
41
+ * Store a result for an idempotency key.
42
+ * TTL is handled by the store implementation.
43
+ */
44
+ set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void>;
45
+ /**
46
+ * Try to acquire a lock for processing a key.
47
+ * Returns true if lock acquired, false if already locked.
48
+ */
49
+ tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
50
+ /** Release a lock after processing complete */
51
+ unlock(key: string, requestId: string): Promise<void>;
52
+ /** Check if a key is currently locked */
53
+ isLocked(key: string): Promise<boolean>;
54
+ /** Delete a cached result by exact key */
55
+ delete(key: string): Promise<void>;
56
+ /**
57
+ * Delete all cached results whose key starts with the given prefix.
58
+ * Used by invalidate() to clear entries by raw idempotency key
59
+ * regardless of fingerprint.
60
+ * Returns the number of entries deleted.
61
+ */
62
+ deleteByPrefix(prefix: string): Promise<number>;
63
+ /**
64
+ * Find the first cached result whose key starts with the given prefix.
65
+ * Used by has() to check if any entry exists for a raw idempotency key.
66
+ * Returns undefined if no matching entry found.
67
+ */
68
+ findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;
69
+ /** Close the store (cleanup connections) */
70
+ close?(): Promise<void>;
71
+ }
72
+ /**
73
+ * Helper to create a result object
74
+ */
75
+ declare function createIdempotencyResult(statusCode: number, body: unknown, headers: Record<string, string>, ttlMs: number): Omit<IdempotencyResult, 'key'>;
76
+ //#endregion
77
+ export { createIdempotencyResult as i, IdempotencyResult as n, IdempotencyStore as r, IdempotencyLock as t };
78
+ //# sourceMappingURL=interface-B01JvPVc.d.mts.map
@@ -0,0 +1 @@
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"}