@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,110 @@
1
+ //#region src/permissions/fields.d.ts
2
+ /**
3
+ * Field-Level Permissions
4
+ *
5
+ * Control field visibility and writability per role.
6
+ * Integrated into the response path (read) and sanitization path (write).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { fields, defineResource } from '@classytic/arc';
11
+ *
12
+ * const userResource = defineResource({
13
+ * name: 'user',
14
+ * adapter: userAdapter,
15
+ * fields: {
16
+ * salary: fields.visibleTo(['admin', 'hr']),
17
+ * internalNotes: fields.writableBy(['admin']),
18
+ * email: fields.redactFor(['viewer']),
19
+ * password: fields.hidden(),
20
+ * },
21
+ * });
22
+ * ```
23
+ */
24
+ type FieldPermissionType = 'hidden' | 'visibleTo' | 'writableBy' | 'redactFor';
25
+ interface FieldPermission {
26
+ readonly _type: FieldPermissionType;
27
+ readonly roles?: readonly string[];
28
+ readonly redactValue?: unknown;
29
+ }
30
+ type FieldPermissionMap = Record<string, FieldPermission>;
31
+ declare const fields: {
32
+ /**
33
+ * Field is never included in responses. Not writable via API.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * fields: { password: fields.hidden() }
38
+ * ```
39
+ */
40
+ hidden(): FieldPermission;
41
+ /**
42
+ * Field is only visible to users with specified roles.
43
+ * Other users don't see the field at all.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * fields: { salary: fields.visibleTo(['admin', 'hr']) }
48
+ * ```
49
+ */
50
+ visibleTo(roles: readonly string[]): FieldPermission;
51
+ /**
52
+ * Field is only writable by users with specified roles.
53
+ * All users can still read the field. Users without the role
54
+ * have the field silently stripped from write operations.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * fields: { role: fields.writableBy(['admin']) }
59
+ * ```
60
+ */
61
+ writableBy(roles: readonly string[]): FieldPermission;
62
+ /**
63
+ * Field is redacted (replaced with a placeholder) for specified roles.
64
+ * Other users see the real value.
65
+ *
66
+ * @param roles - Roles that see the redacted value
67
+ * @param redactValue - Replacement value (default: '***')
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * fields: {
72
+ * email: fields.redactFor(['viewer']),
73
+ * ssn: fields.redactFor(['basic'], '***-**-****'),
74
+ * }
75
+ * ```
76
+ */
77
+ redactFor(roles: readonly string[], redactValue?: unknown): FieldPermission;
78
+ };
79
+ /**
80
+ * Apply field-level READ permissions to a response object.
81
+ * Strips hidden fields, enforces visibility, and applies redaction.
82
+ *
83
+ * @param data - The response object (mutated in place for performance)
84
+ * @param fieldPermissions - Field permission map from resource config
85
+ * @param userRoles - Current user's roles (empty array for unauthenticated)
86
+ * @returns The filtered object
87
+ */
88
+ declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
89
+ /**
90
+ * Apply field-level WRITE permissions to request body.
91
+ * Strips fields that the user doesn't have permission to write.
92
+ *
93
+ * @param body - The request body (returns a new filtered copy)
94
+ * @param fieldPermissions - Field permission map from resource config
95
+ * @param userRoles - Current user's roles
96
+ * @returns Filtered body
97
+ */
98
+ declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
99
+ /**
100
+ * Resolve effective roles by merging global user roles with org-level roles.
101
+ *
102
+ * Global roles come from `req.user.roles` (Better Auth user object).
103
+ * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
104
+ *
105
+ * When no org context exists, returns global roles only — backward compatible.
106
+ */
107
+ declare function resolveEffectiveRoles(userRoles: readonly string[], orgRoles: readonly string[]): string[];
108
+ //#endregion
109
+ export { applyFieldWritePermissions as a, applyFieldReadPermissions as i, FieldPermissionMap as n, fields as o, FieldPermissionType as r, resolveEffectiveRoles as s, FieldPermission as t };
110
+ //# sourceMappingURL=fields-DyaDVX4J.d.mts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,115 @@
1
+ //#region src/permissions/fields.ts
2
+ /**
3
+ * Field-Level Permissions
4
+ *
5
+ * Control field visibility and writability per role.
6
+ * Integrated into the response path (read) and sanitization path (write).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { fields, defineResource } from '@classytic/arc';
11
+ *
12
+ * const userResource = defineResource({
13
+ * name: 'user',
14
+ * adapter: userAdapter,
15
+ * fields: {
16
+ * salary: fields.visibleTo(['admin', 'hr']),
17
+ * internalNotes: fields.writableBy(['admin']),
18
+ * email: fields.redactFor(['viewer']),
19
+ * password: fields.hidden(),
20
+ * },
21
+ * });
22
+ * ```
23
+ */
24
+ /** Type guard for Mongoose-like documents with toObject() */
25
+ function isMongooseDoc(obj) {
26
+ return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
27
+ }
28
+ const fields = {
29
+ hidden() {
30
+ return { _type: "hidden" };
31
+ },
32
+ visibleTo(roles) {
33
+ return {
34
+ _type: "visibleTo",
35
+ roles
36
+ };
37
+ },
38
+ writableBy(roles) {
39
+ return {
40
+ _type: "writableBy",
41
+ roles
42
+ };
43
+ },
44
+ redactFor(roles, redactValue = "***") {
45
+ return {
46
+ _type: "redactFor",
47
+ roles,
48
+ redactValue
49
+ };
50
+ }
51
+ };
52
+ /**
53
+ * Apply field-level READ permissions to a response object.
54
+ * Strips hidden fields, enforces visibility, and applies redaction.
55
+ *
56
+ * @param data - The response object (mutated in place for performance)
57
+ * @param fieldPermissions - Field permission map from resource config
58
+ * @param userRoles - Current user's roles (empty array for unauthenticated)
59
+ * @returns The filtered object
60
+ */
61
+ function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
62
+ if (!data || typeof data !== "object") return data;
63
+ const result = { ...isMongooseDoc(data) ? data.toObject() : data };
64
+ for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
65
+ case "hidden":
66
+ delete result[field];
67
+ break;
68
+ case "visibleTo":
69
+ if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
70
+ break;
71
+ case "redactFor":
72
+ if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
73
+ break;
74
+ case "writableBy": break;
75
+ }
76
+ return result;
77
+ }
78
+ /**
79
+ * Apply field-level WRITE permissions to request body.
80
+ * Strips fields that the user doesn't have permission to write.
81
+ *
82
+ * @param body - The request body (returns a new filtered copy)
83
+ * @param fieldPermissions - Field permission map from resource config
84
+ * @param userRoles - Current user's roles
85
+ * @returns Filtered body
86
+ */
87
+ function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
88
+ const result = { ...body };
89
+ for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
90
+ case "hidden":
91
+ delete result[field];
92
+ break;
93
+ case "writableBy":
94
+ if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
95
+ break;
96
+ }
97
+ return result;
98
+ }
99
+ /**
100
+ * Resolve effective roles by merging global user roles with org-level roles.
101
+ *
102
+ * Global roles come from `req.user.roles` (Better Auth user object).
103
+ * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
104
+ *
105
+ * When no org context exists, returns global roles only — backward compatible.
106
+ */
107
+ function resolveEffectiveRoles(userRoles, orgRoles) {
108
+ if (orgRoles.length === 0) return [...userRoles];
109
+ if (userRoles.length === 0) return [...orgRoles];
110
+ return [...new Set([...userRoles, ...orgRoles])];
111
+ }
112
+
113
+ //#endregion
114
+ export { resolveEffectiveRoles as i, applyFieldWritePermissions as n, fields as r, applyFieldReadPermissions as t };
115
+ //# sourceMappingURL=fields-iagOozy0.mjs.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,4 @@
1
+ import "../elevation-B_2dRLVP.mjs";
2
+ import { $ as beforeUpdate, B as DefineHookOptions, G as HookRegistration, H as HookHandler, J as afterCreate, K as HookSystem, Q as beforeDelete, U as HookOperation, V as HookContext, W as HookPhase, X as afterUpdate, Y as afterDelete, Z as beforeCreate, et as createHookSystem, q as HookSystemOptions, tt as defineHook } from "../interface-Ch8HU9uM.mjs";
3
+ import "../types-aYB4V7uN.mjs";
4
+ export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -0,0 +1,3 @@
1
+ import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-BsGV-j2l.mjs";
2
+
3
+ export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -0,0 +1,97 @@
1
+ import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-B01JvPVc.mjs";
2
+ import { r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-D-JAeLtm.mjs";
3
+ import { n as MongoIdempotencyStoreOptions } from "../mongodb-JN-9JA7K.mjs";
4
+ import { FastifyPluginAsync } from "fastify";
5
+
6
+ //#region src/idempotency/idempotencyPlugin.d.ts
7
+ interface IdempotencyPluginOptions {
8
+ /** Enable idempotency (default: false) */
9
+ enabled?: boolean;
10
+ /** Header name for idempotency key (default: 'idempotency-key') */
11
+ headerName?: string;
12
+ /** TTL for cached responses in ms (default: 86400000 = 24h) */
13
+ ttlMs?: number;
14
+ /** Lock timeout in ms (default: 30000 = 30s) */
15
+ lockTimeoutMs?: number;
16
+ /** HTTP methods to apply idempotency to (default: ['POST', 'PUT', 'PATCH']) */
17
+ methods?: string[];
18
+ /** URL patterns to include (regex). If set, only matching URLs use idempotency */
19
+ include?: RegExp[];
20
+ /** URL patterns to exclude (regex). Excluded patterns take precedence */
21
+ exclude?: RegExp[];
22
+ /** Custom store (default: MemoryIdempotencyStore) */
23
+ store?: IdempotencyStore;
24
+ /** Retry-After header value in seconds when request is in-flight (default: 1) */
25
+ retryAfterSeconds?: number;
26
+ }
27
+ declare module 'fastify' {
28
+ interface FastifyRequest {
29
+ /** The idempotency key for this request (if present) */
30
+ idempotencyKey?: string;
31
+ /** Whether this response was replayed from cache */
32
+ idempotencyReplayed?: boolean;
33
+ /** @internal Full key with fingerprint for store lookups */
34
+ _idempotencyFullKey?: string;
35
+ }
36
+ interface FastifyInstance {
37
+ /** Idempotency utilities */
38
+ idempotency: {
39
+ /** Manually invalidate an idempotency key */invalidate: (key: string) => Promise<void>; /** Check if a key has a cached response */
40
+ has: (key: string) => Promise<boolean>;
41
+ /**
42
+ * Route-level preHandler for idempotency check + lock.
43
+ * Wire AFTER authenticate in the preHandler chain so that
44
+ * `request.user` is populated before the fingerprint is computed.
45
+ *
46
+ * `createCrudRouter` injects this automatically for mutation routes.
47
+ * For custom routes, add it manually:
48
+ * ```typescript
49
+ * fastify.post('/orders', {
50
+ * preHandler: [fastify.authenticate, fastify.idempotency.middleware],
51
+ * }, handler);
52
+ * ```
53
+ */
54
+ middleware: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
55
+ };
56
+ }
57
+ }
58
+ declare const idempotencyPlugin: FastifyPluginAsync<IdempotencyPluginOptions>;
59
+ declare const _default: FastifyPluginAsync<IdempotencyPluginOptions>;
60
+ //#endregion
61
+ //#region src/idempotency/stores/memory.d.ts
62
+ interface MemoryIdempotencyStoreOptions {
63
+ /** Default TTL in milliseconds (default: 86400000 = 24h) */
64
+ ttlMs?: number;
65
+ /** Cleanup interval in milliseconds (default: 60000 = 1 min) */
66
+ cleanupIntervalMs?: number;
67
+ /** Maximum entries before oldest are evicted (default: 10000) */
68
+ maxEntries?: number;
69
+ }
70
+ declare class MemoryIdempotencyStore implements IdempotencyStore {
71
+ readonly name = "memory";
72
+ private results;
73
+ private locks;
74
+ private ttlMs;
75
+ private maxEntries;
76
+ private cleanupInterval;
77
+ constructor(options?: MemoryIdempotencyStoreOptions);
78
+ get(key: string): Promise<IdempotencyResult | undefined>;
79
+ set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void>;
80
+ tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
81
+ unlock(key: string, requestId: string): Promise<void>;
82
+ isLocked(key: string): Promise<boolean>;
83
+ delete(key: string): Promise<void>;
84
+ deleteByPrefix(prefix: string): Promise<number>;
85
+ findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;
86
+ close(): Promise<void>;
87
+ /** Get current stats (for debugging/monitoring) */
88
+ getStats(): {
89
+ results: number;
90
+ locks: number;
91
+ };
92
+ private cleanup;
93
+ private evictOldest;
94
+ }
95
+ //#endregion
96
+ export { type IdempotencyLock, type IdempotencyPluginOptions, type IdempotencyResult, type IdempotencyStore, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, type MongoIdempotencyStoreOptions, type RedisClient, type RedisIdempotencyStoreOptions, createIdempotencyResult, _default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
97
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,320 @@
1
+ import fp from "fastify-plugin";
2
+ import { createHash } from "crypto";
3
+
4
+ //#region src/idempotency/stores/interface.ts
5
+ /**
6
+ * Helper to create a result object
7
+ */
8
+ function createIdempotencyResult(statusCode, body, headers, ttlMs) {
9
+ const now = /* @__PURE__ */ new Date();
10
+ return {
11
+ statusCode,
12
+ headers,
13
+ body,
14
+ createdAt: now,
15
+ expiresAt: new Date(now.getTime() + ttlMs)
16
+ };
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/idempotency/stores/memory.ts
21
+ var MemoryIdempotencyStore = class {
22
+ name = "memory";
23
+ results = /* @__PURE__ */ new Map();
24
+ locks = /* @__PURE__ */ new Map();
25
+ ttlMs;
26
+ maxEntries;
27
+ cleanupInterval = null;
28
+ constructor(options = {}) {
29
+ this.ttlMs = options.ttlMs ?? 864e5;
30
+ this.maxEntries = options.maxEntries ?? 1e4;
31
+ const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
32
+ this.cleanupInterval = setInterval(() => {
33
+ this.cleanup();
34
+ }, cleanupIntervalMs);
35
+ if (this.cleanupInterval.unref) this.cleanupInterval.unref();
36
+ }
37
+ async get(key) {
38
+ const result = this.results.get(key);
39
+ if (!result) return void 0;
40
+ if (/* @__PURE__ */ new Date() > result.expiresAt) {
41
+ this.results.delete(key);
42
+ return;
43
+ }
44
+ return result;
45
+ }
46
+ async set(key, result) {
47
+ if (this.results.size >= this.maxEntries) this.evictOldest();
48
+ this.results.set(key, {
49
+ ...result,
50
+ key
51
+ });
52
+ }
53
+ async tryLock(key, requestId, ttlMs) {
54
+ const existing = this.locks.get(key);
55
+ if (existing) if (/* @__PURE__ */ new Date() > existing.expiresAt) this.locks.delete(key);
56
+ else return false;
57
+ this.locks.set(key, {
58
+ key,
59
+ requestId,
60
+ lockedAt: /* @__PURE__ */ new Date(),
61
+ expiresAt: new Date(Date.now() + ttlMs)
62
+ });
63
+ return true;
64
+ }
65
+ async unlock(key, requestId) {
66
+ const lock = this.locks.get(key);
67
+ if (lock && lock.requestId === requestId) this.locks.delete(key);
68
+ }
69
+ async isLocked(key) {
70
+ const lock = this.locks.get(key);
71
+ if (!lock) return false;
72
+ if (/* @__PURE__ */ new Date() > lock.expiresAt) {
73
+ this.locks.delete(key);
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ async delete(key) {
79
+ this.results.delete(key);
80
+ this.locks.delete(key);
81
+ }
82
+ async deleteByPrefix(prefix) {
83
+ let count = 0;
84
+ for (const key of this.results.keys()) if (key.startsWith(prefix)) {
85
+ this.results.delete(key);
86
+ count++;
87
+ }
88
+ for (const key of this.locks.keys()) if (key.startsWith(prefix)) this.locks.delete(key);
89
+ return count;
90
+ }
91
+ async findByPrefix(prefix) {
92
+ const now = /* @__PURE__ */ new Date();
93
+ for (const [key, result] of this.results) if (key.startsWith(prefix)) {
94
+ if (now > result.expiresAt) {
95
+ this.results.delete(key);
96
+ continue;
97
+ }
98
+ return result;
99
+ }
100
+ }
101
+ async close() {
102
+ if (this.cleanupInterval) {
103
+ clearInterval(this.cleanupInterval);
104
+ this.cleanupInterval = null;
105
+ }
106
+ this.results.clear();
107
+ this.locks.clear();
108
+ }
109
+ /** Get current stats (for debugging/monitoring) */
110
+ getStats() {
111
+ return {
112
+ results: this.results.size,
113
+ locks: this.locks.size
114
+ };
115
+ }
116
+ cleanup() {
117
+ const now = /* @__PURE__ */ new Date();
118
+ for (const [key, result] of this.results) if (now > result.expiresAt) this.results.delete(key);
119
+ for (const [key, lock] of this.locks) if (now > lock.expiresAt) this.locks.delete(key);
120
+ }
121
+ evictOldest() {
122
+ const entries = Array.from(this.results.entries()).sort((a, b) => a[1].createdAt.getTime() - b[1].createdAt.getTime());
123
+ const toRemove = Math.max(1, Math.floor(entries.length * .1));
124
+ for (let i = 0; i < toRemove; i++) {
125
+ const entry = entries[i];
126
+ if (entry) this.results.delete(entry[0]);
127
+ }
128
+ }
129
+ };
130
+
131
+ //#endregion
132
+ //#region src/idempotency/idempotencyPlugin.ts
133
+ /**
134
+ * Idempotency Plugin
135
+ *
136
+ * Duplicate request protection for mutating operations.
137
+ * Uses idempotency keys to ensure safe retries.
138
+ *
139
+ * ## Auth Safety
140
+ *
141
+ * The idempotency check runs as a **route-level middleware**
142
+ * (`idempotency.middleware`) that must be wired AFTER authentication in the
143
+ * preHandler chain. This ensures the fingerprint includes the real caller
144
+ * identity, preventing cross-user replay attacks.
145
+ *
146
+ * Arc's `createCrudRouter` does this automatically for mutation routes.
147
+ * For custom routes, wire it manually:
148
+ *
149
+ * ```typescript
150
+ * fastify.post('/orders', {
151
+ * preHandler: [fastify.authenticate, fastify.idempotency.middleware],
152
+ * }, handler);
153
+ * ```
154
+ *
155
+ * @example
156
+ * import { idempotencyPlugin } from '@classytic/arc/idempotency';
157
+ *
158
+ * await fastify.register(idempotencyPlugin, {
159
+ * enabled: true,
160
+ * headerName: 'idempotency-key',
161
+ * ttlMs: 86400000, // 24 hours
162
+ * });
163
+ *
164
+ * // Client sends:
165
+ * // POST /api/orders
166
+ * // Idempotency-Key: order-123-abc
167
+ *
168
+ * // If same key sent again within TTL, returns cached response
169
+ */
170
+ const HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
171
+ const HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
172
+ const idempotencyPlugin = async (fastify, opts = {}) => {
173
+ const { enabled = false, headerName = "idempotency-key", ttlMs = 864e5, lockTimeoutMs = 3e4, methods = [
174
+ "POST",
175
+ "PUT",
176
+ "PATCH"
177
+ ], include, exclude, store = new MemoryIdempotencyStore({ ttlMs }), retryAfterSeconds = 1 } = opts;
178
+ if (!enabled) {
179
+ fastify.decorate("idempotency", {
180
+ invalidate: async () => {},
181
+ has: async () => false,
182
+ middleware: async () => {}
183
+ });
184
+ fastify.decorateRequest("idempotencyKey", void 0);
185
+ fastify.decorateRequest("idempotencyReplayed", false);
186
+ fastify.log?.debug?.("Idempotency plugin disabled");
187
+ return;
188
+ }
189
+ const methodSet = new Set(methods.map((m) => m.toUpperCase()));
190
+ fastify.decorateRequest("idempotencyKey", void 0);
191
+ fastify.decorateRequest("idempotencyReplayed", false);
192
+ /**
193
+ * Check if this request should use idempotency
194
+ */
195
+ function shouldApplyIdempotency(request) {
196
+ if (!methodSet.has(request.method)) return false;
197
+ const url = request.url;
198
+ if (exclude?.some((pattern) => pattern.test(url))) return false;
199
+ if (include && !include.some((pattern) => pattern.test(url))) return false;
200
+ return true;
201
+ }
202
+ /**
203
+ * Normalize body for consistent hashing (sort keys recursively)
204
+ */
205
+ function normalizeBody(obj) {
206
+ if (obj === null || typeof obj !== "object") return obj;
207
+ if (Array.isArray(obj)) return obj.map(normalizeBody);
208
+ const sorted = {};
209
+ const keys = Object.keys(obj).sort();
210
+ for (const key of keys) sorted[key] = normalizeBody(obj[key]);
211
+ return sorted;
212
+ }
213
+ /**
214
+ * Generate a fingerprint for the request (for key generation).
215
+ * Includes caller identity so the same idempotency key from different
216
+ * users doesn't replay one user's response to another.
217
+ *
218
+ * IMPORTANT: This must be called AFTER auth has populated request.user,
219
+ * otherwise userId falls back to 'anon' and cross-user replay is possible.
220
+ */
221
+ function getRequestFingerprint(request) {
222
+ let bodyHash = "nobody";
223
+ if (request.body && typeof request.body === "object") {
224
+ const normalized = normalizeBody(request.body);
225
+ const bodyString = JSON.stringify(normalized);
226
+ bodyHash = createHash("sha256").update(bodyString).digest("hex").substring(0, 16);
227
+ if (request.log && request.log.debug) request.log.debug({ bodyHash }, "Generated body hash");
228
+ }
229
+ const user = request.user;
230
+ const userId = user?.id ?? user?._id ?? "anon";
231
+ return `${request.method}:${request.url}:${bodyHash}:u=${userId}`;
232
+ }
233
+ const idempotencyMiddleware = async (request, reply) => {
234
+ if (!shouldApplyIdempotency(request)) return;
235
+ const keyHeader = request.headers[headerName.toLowerCase()];
236
+ const idempotencyKey = typeof keyHeader === "string" ? keyHeader.trim() : void 0;
237
+ if (!idempotencyKey) return;
238
+ request.idempotencyKey = idempotencyKey;
239
+ const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;
240
+ const cached = await store.get(fullKey);
241
+ if (cached) {
242
+ request.idempotencyReplayed = true;
243
+ reply.header(HEADER_IDEMPOTENCY_REPLAYED, "true");
244
+ reply.header(HEADER_IDEMPOTENCY_KEY, idempotencyKey);
245
+ for (const [key, value] of Object.entries(cached.headers)) if (!key.startsWith("x-idempotency")) reply.header(key, value);
246
+ reply.code(cached.statusCode).send(cached.body);
247
+ return;
248
+ }
249
+ if (!await store.tryLock(fullKey, request.id, lockTimeoutMs)) {
250
+ reply.code(409).header("Retry-After", retryAfterSeconds.toString()).send({
251
+ error: "Request with this idempotency key is already in progress",
252
+ code: "IDEMPOTENCY_CONFLICT",
253
+ retryAfter: retryAfterSeconds
254
+ });
255
+ return;
256
+ }
257
+ request._idempotencyFullKey = fullKey;
258
+ };
259
+ fastify.decorate("idempotency", {
260
+ invalidate: async (key) => {
261
+ await store.deleteByPrefix(`${key}:`);
262
+ },
263
+ has: async (key) => {
264
+ return !!await store.findByPrefix(`${key}:`);
265
+ },
266
+ middleware: idempotencyMiddleware
267
+ });
268
+ fastify.addHook("onSend", async (request, reply, payload) => {
269
+ if (request.idempotencyReplayed) return payload;
270
+ const fullKey = request._idempotencyFullKey;
271
+ if (!fullKey) return payload;
272
+ const statusCode = reply.statusCode;
273
+ if (statusCode < 200 || statusCode >= 300) {
274
+ await store.unlock(fullKey, request.id);
275
+ return payload;
276
+ }
277
+ const headersToCache = {};
278
+ const excludeHeaders = new Set([
279
+ "content-length",
280
+ "transfer-encoding",
281
+ "connection",
282
+ "keep-alive",
283
+ "date",
284
+ "set-cookie"
285
+ ]);
286
+ const rawHeaders = reply.getHeaders();
287
+ for (const [key, value] of Object.entries(rawHeaders)) if (!excludeHeaders.has(key.toLowerCase()) && typeof value === "string") headersToCache[key] = value;
288
+ let body;
289
+ try {
290
+ body = typeof payload === "string" ? JSON.parse(payload) : payload;
291
+ } catch {
292
+ body = payload;
293
+ }
294
+ const result = createIdempotencyResult(statusCode, body, headersToCache, ttlMs);
295
+ await store.set(fullKey, result);
296
+ await store.unlock(fullKey, request.id);
297
+ reply.header(HEADER_IDEMPOTENCY_KEY, request.idempotencyKey);
298
+ return payload;
299
+ });
300
+ fastify.addHook("onError", async (request) => {
301
+ const fullKey = request._idempotencyFullKey;
302
+ if (fullKey) await store.unlock(fullKey, request.id);
303
+ });
304
+ fastify.addHook("onClose", async () => {
305
+ await store.close?.();
306
+ });
307
+ fastify.log?.debug?.({
308
+ headerName,
309
+ ttlMs,
310
+ methods
311
+ }, "Idempotency plugin enabled");
312
+ };
313
+ var idempotencyPlugin_default = fp(idempotencyPlugin, {
314
+ name: "arc-idempotency",
315
+ fastify: "5.x"
316
+ });
317
+
318
+ //#endregion
319
+ export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
320
+ //# sourceMappingURL=index.mjs.map