convex-cms 0.0.1

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 (379) hide show
  1. package/dist/cli/commands/admin.d.ts +16 -0
  2. package/dist/cli/commands/admin.d.ts.map +1 -0
  3. package/dist/cli/commands/admin.js +88 -0
  4. package/dist/cli/commands/admin.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +18 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
  10. package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
  11. package/dist/cli/utils/detectConvexUrl.js +48 -0
  12. package/dist/cli/utils/detectConvexUrl.js.map +1 -0
  13. package/dist/cli/utils/openBrowser.d.ts +7 -0
  14. package/dist/cli/utils/openBrowser.d.ts.map +1 -0
  15. package/dist/cli/utils/openBrowser.js +17 -0
  16. package/dist/cli/utils/openBrowser.js.map +1 -0
  17. package/dist/client/admin-config.d.ts +126 -0
  18. package/dist/client/admin-config.d.ts.map +1 -0
  19. package/dist/client/admin-config.js +117 -0
  20. package/dist/client/admin-config.js.map +1 -0
  21. package/dist/client/adminApi.d.ts +2273 -0
  22. package/dist/client/adminApi.d.ts.map +1 -0
  23. package/dist/client/adminApi.js +716 -0
  24. package/dist/client/adminApi.js.map +1 -0
  25. package/dist/client/agentTools.d.ts +933 -0
  26. package/dist/client/agentTools.d.ts.map +1 -0
  27. package/dist/client/agentTools.js +1004 -0
  28. package/dist/client/agentTools.js.map +1 -0
  29. package/dist/client/argTypes.d.ts +212 -0
  30. package/dist/client/argTypes.d.ts.map +1 -0
  31. package/dist/client/argTypes.js +5 -0
  32. package/dist/client/argTypes.js.map +1 -0
  33. package/dist/client/field-types.d.ts +55 -0
  34. package/dist/client/field-types.d.ts.map +1 -0
  35. package/dist/client/field-types.js +152 -0
  36. package/dist/client/field-types.js.map +1 -0
  37. package/dist/client/index.d.ts +189 -0
  38. package/dist/client/index.d.ts.map +1 -0
  39. package/dist/client/index.js +668 -0
  40. package/dist/client/index.js.map +1 -0
  41. package/dist/client/queryBuilder.d.ts +765 -0
  42. package/dist/client/queryBuilder.d.ts.map +1 -0
  43. package/dist/client/queryBuilder.js +970 -0
  44. package/dist/client/queryBuilder.js.map +1 -0
  45. package/dist/client/schema/codegen.d.ts +128 -0
  46. package/dist/client/schema/codegen.d.ts.map +1 -0
  47. package/dist/client/schema/codegen.js +318 -0
  48. package/dist/client/schema/codegen.js.map +1 -0
  49. package/dist/client/schema/defineContentType.d.ts +221 -0
  50. package/dist/client/schema/defineContentType.d.ts.map +1 -0
  51. package/dist/client/schema/defineContentType.js +380 -0
  52. package/dist/client/schema/defineContentType.js.map +1 -0
  53. package/dist/client/schema/index.d.ts +85 -0
  54. package/dist/client/schema/index.d.ts.map +1 -0
  55. package/dist/client/schema/index.js +92 -0
  56. package/dist/client/schema/index.js.map +1 -0
  57. package/dist/client/schema/schemaDrift.d.ts +199 -0
  58. package/dist/client/schema/schemaDrift.d.ts.map +1 -0
  59. package/dist/client/schema/schemaDrift.js +340 -0
  60. package/dist/client/schema/schemaDrift.js.map +1 -0
  61. package/dist/client/schema/typedClient.d.ts +401 -0
  62. package/dist/client/schema/typedClient.d.ts.map +1 -0
  63. package/dist/client/schema/typedClient.js +269 -0
  64. package/dist/client/schema/typedClient.js.map +1 -0
  65. package/dist/client/schema/types.d.ts +477 -0
  66. package/dist/client/schema/types.d.ts.map +1 -0
  67. package/dist/client/schema/types.js +39 -0
  68. package/dist/client/schema/types.js.map +1 -0
  69. package/dist/client/types.d.ts +449 -0
  70. package/dist/client/types.d.ts.map +1 -0
  71. package/dist/client/types.js +149 -0
  72. package/dist/client/types.js.map +1 -0
  73. package/dist/client/workflows.d.ts +51 -0
  74. package/dist/client/workflows.d.ts.map +1 -0
  75. package/dist/client/workflows.js +103 -0
  76. package/dist/client/workflows.js.map +1 -0
  77. package/dist/client/wrapper.d.ts +2198 -0
  78. package/dist/client/wrapper.d.ts.map +1 -0
  79. package/dist/client/wrapper.js +2651 -0
  80. package/dist/client/wrapper.js.map +1 -0
  81. package/dist/component/_generated/api.d.ts +124 -0
  82. package/dist/component/_generated/api.d.ts.map +1 -0
  83. package/dist/component/_generated/api.js +31 -0
  84. package/dist/component/_generated/api.js.map +1 -0
  85. package/dist/component/_generated/component.d.ts +4321 -0
  86. package/dist/component/_generated/component.d.ts.map +1 -0
  87. package/dist/component/_generated/component.js +11 -0
  88. package/dist/component/_generated/component.js.map +1 -0
  89. package/dist/component/_generated/dataModel.d.ts +46 -0
  90. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  91. package/dist/component/_generated/dataModel.js +11 -0
  92. package/dist/component/_generated/dataModel.js.map +1 -0
  93. package/dist/component/_generated/server.d.ts +121 -0
  94. package/dist/component/_generated/server.d.ts.map +1 -0
  95. package/dist/component/_generated/server.js +78 -0
  96. package/dist/component/_generated/server.js.map +1 -0
  97. package/dist/component/auditLog.d.ts +410 -0
  98. package/dist/component/auditLog.d.ts.map +1 -0
  99. package/dist/component/auditLog.js +607 -0
  100. package/dist/component/auditLog.js.map +1 -0
  101. package/dist/component/authorization.d.ts +323 -0
  102. package/dist/component/authorization.d.ts.map +1 -0
  103. package/dist/component/authorization.js +464 -0
  104. package/dist/component/authorization.js.map +1 -0
  105. package/dist/component/authorizationHooks.d.ts +184 -0
  106. package/dist/component/authorizationHooks.d.ts.map +1 -0
  107. package/dist/component/authorizationHooks.js +521 -0
  108. package/dist/component/authorizationHooks.js.map +1 -0
  109. package/dist/component/bulkOperations.d.ts +200 -0
  110. package/dist/component/bulkOperations.d.ts.map +1 -0
  111. package/dist/component/bulkOperations.js +568 -0
  112. package/dist/component/bulkOperations.js.map +1 -0
  113. package/dist/component/contentEntries.d.ts +719 -0
  114. package/dist/component/contentEntries.d.ts.map +1 -0
  115. package/dist/component/contentEntries.js +1617 -0
  116. package/dist/component/contentEntries.js.map +1 -0
  117. package/dist/component/contentEntryMutations.d.ts +505 -0
  118. package/dist/component/contentEntryMutations.d.ts.map +1 -0
  119. package/dist/component/contentEntryMutations.js +1009 -0
  120. package/dist/component/contentEntryMutations.js.map +1 -0
  121. package/dist/component/contentEntryValidation.d.ts +115 -0
  122. package/dist/component/contentEntryValidation.d.ts.map +1 -0
  123. package/dist/component/contentEntryValidation.js +546 -0
  124. package/dist/component/contentEntryValidation.js.map +1 -0
  125. package/dist/component/contentLock.d.ts +328 -0
  126. package/dist/component/contentLock.d.ts.map +1 -0
  127. package/dist/component/contentLock.js +471 -0
  128. package/dist/component/contentLock.js.map +1 -0
  129. package/dist/component/contentTypeMigration.d.ts +411 -0
  130. package/dist/component/contentTypeMigration.d.ts.map +1 -0
  131. package/dist/component/contentTypeMigration.js +805 -0
  132. package/dist/component/contentTypeMigration.js.map +1 -0
  133. package/dist/component/contentTypeMutations.d.ts +975 -0
  134. package/dist/component/contentTypeMutations.d.ts.map +1 -0
  135. package/dist/component/contentTypeMutations.js +768 -0
  136. package/dist/component/contentTypeMutations.js.map +1 -0
  137. package/dist/component/contentTypes.d.ts +538 -0
  138. package/dist/component/contentTypes.d.ts.map +1 -0
  139. package/dist/component/contentTypes.js +304 -0
  140. package/dist/component/contentTypes.js.map +1 -0
  141. package/dist/component/convex.config.d.ts +42 -0
  142. package/dist/component/convex.config.d.ts.map +1 -0
  143. package/dist/component/convex.config.js +43 -0
  144. package/dist/component/convex.config.js.map +1 -0
  145. package/dist/component/documentTypes.d.ts +186 -0
  146. package/dist/component/documentTypes.d.ts.map +1 -0
  147. package/dist/component/documentTypes.js +23 -0
  148. package/dist/component/documentTypes.js.map +1 -0
  149. package/dist/component/eventEmitter.d.ts +281 -0
  150. package/dist/component/eventEmitter.d.ts.map +1 -0
  151. package/dist/component/eventEmitter.js +300 -0
  152. package/dist/component/eventEmitter.js.map +1 -0
  153. package/dist/component/exportImport.d.ts +1120 -0
  154. package/dist/component/exportImport.d.ts.map +1 -0
  155. package/dist/component/exportImport.js +931 -0
  156. package/dist/component/exportImport.js.map +1 -0
  157. package/dist/component/index.d.ts +28 -0
  158. package/dist/component/index.d.ts.map +1 -0
  159. package/dist/component/index.js +142 -0
  160. package/dist/component/index.js.map +1 -0
  161. package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
  162. package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
  163. package/dist/component/lib/deepReferenceResolver.js +601 -0
  164. package/dist/component/lib/deepReferenceResolver.js.map +1 -0
  165. package/dist/component/lib/errors.d.ts +306 -0
  166. package/dist/component/lib/errors.d.ts.map +1 -0
  167. package/dist/component/lib/errors.js +407 -0
  168. package/dist/component/lib/errors.js.map +1 -0
  169. package/dist/component/lib/index.d.ts +10 -0
  170. package/dist/component/lib/index.d.ts.map +1 -0
  171. package/dist/component/lib/index.js +33 -0
  172. package/dist/component/lib/index.js.map +1 -0
  173. package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
  174. package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
  175. package/dist/component/lib/mediaReferenceResolver.js +326 -0
  176. package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
  177. package/dist/component/lib/metadataExtractor.d.ts +245 -0
  178. package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
  179. package/dist/component/lib/metadataExtractor.js +548 -0
  180. package/dist/component/lib/metadataExtractor.js.map +1 -0
  181. package/dist/component/lib/mutationAuth.d.ts +95 -0
  182. package/dist/component/lib/mutationAuth.d.ts.map +1 -0
  183. package/dist/component/lib/mutationAuth.js +146 -0
  184. package/dist/component/lib/mutationAuth.js.map +1 -0
  185. package/dist/component/lib/queries.d.ts +17 -0
  186. package/dist/component/lib/queries.d.ts.map +1 -0
  187. package/dist/component/lib/queries.js +49 -0
  188. package/dist/component/lib/queries.js.map +1 -0
  189. package/dist/component/lib/ragContentChunker.d.ts +423 -0
  190. package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
  191. package/dist/component/lib/ragContentChunker.js +897 -0
  192. package/dist/component/lib/ragContentChunker.js.map +1 -0
  193. package/dist/component/lib/referenceResolver.d.ts +175 -0
  194. package/dist/component/lib/referenceResolver.d.ts.map +1 -0
  195. package/dist/component/lib/referenceResolver.js +293 -0
  196. package/dist/component/lib/referenceResolver.js.map +1 -0
  197. package/dist/component/lib/slugGenerator.d.ts +71 -0
  198. package/dist/component/lib/slugGenerator.d.ts.map +1 -0
  199. package/dist/component/lib/slugGenerator.js +207 -0
  200. package/dist/component/lib/slugGenerator.js.map +1 -0
  201. package/dist/component/lib/slugUniqueness.d.ts +131 -0
  202. package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
  203. package/dist/component/lib/slugUniqueness.js +229 -0
  204. package/dist/component/lib/slugUniqueness.js.map +1 -0
  205. package/dist/component/lib/softDelete.d.ts +18 -0
  206. package/dist/component/lib/softDelete.d.ts.map +1 -0
  207. package/dist/component/lib/softDelete.js +29 -0
  208. package/dist/component/lib/softDelete.js.map +1 -0
  209. package/dist/component/localeFallbackChain.d.ts +410 -0
  210. package/dist/component/localeFallbackChain.d.ts.map +1 -0
  211. package/dist/component/localeFallbackChain.js +467 -0
  212. package/dist/component/localeFallbackChain.js.map +1 -0
  213. package/dist/component/localeFields.d.ts +508 -0
  214. package/dist/component/localeFields.d.ts.map +1 -0
  215. package/dist/component/localeFields.js +592 -0
  216. package/dist/component/localeFields.js.map +1 -0
  217. package/dist/component/mediaAssetMutations.d.ts +235 -0
  218. package/dist/component/mediaAssetMutations.d.ts.map +1 -0
  219. package/dist/component/mediaAssetMutations.js +558 -0
  220. package/dist/component/mediaAssetMutations.js.map +1 -0
  221. package/dist/component/mediaAssets.d.ts +168 -0
  222. package/dist/component/mediaAssets.d.ts.map +1 -0
  223. package/dist/component/mediaAssets.js +618 -0
  224. package/dist/component/mediaAssets.js.map +1 -0
  225. package/dist/component/mediaFolderMutations.d.ts +642 -0
  226. package/dist/component/mediaFolderMutations.d.ts.map +1 -0
  227. package/dist/component/mediaFolderMutations.js +849 -0
  228. package/dist/component/mediaFolderMutations.js.map +1 -0
  229. package/dist/component/mediaUploadMutations.d.ts +136 -0
  230. package/dist/component/mediaUploadMutations.d.ts.map +1 -0
  231. package/dist/component/mediaUploadMutations.js +205 -0
  232. package/dist/component/mediaUploadMutations.js.map +1 -0
  233. package/dist/component/mediaVariantMutations.d.ts +468 -0
  234. package/dist/component/mediaVariantMutations.d.ts.map +1 -0
  235. package/dist/component/mediaVariantMutations.js +737 -0
  236. package/dist/component/mediaVariantMutations.js.map +1 -0
  237. package/dist/component/mediaVariants.d.ts +525 -0
  238. package/dist/component/mediaVariants.d.ts.map +1 -0
  239. package/dist/component/mediaVariants.js +661 -0
  240. package/dist/component/mediaVariants.js.map +1 -0
  241. package/dist/component/ragContentIndexer.d.ts +595 -0
  242. package/dist/component/ragContentIndexer.d.ts.map +1 -0
  243. package/dist/component/ragContentIndexer.js +794 -0
  244. package/dist/component/ragContentIndexer.js.map +1 -0
  245. package/dist/component/rateLimitHooks.d.ts +266 -0
  246. package/dist/component/rateLimitHooks.d.ts.map +1 -0
  247. package/dist/component/rateLimitHooks.js +412 -0
  248. package/dist/component/rateLimitHooks.js.map +1 -0
  249. package/dist/component/roles.d.ts +649 -0
  250. package/dist/component/roles.d.ts.map +1 -0
  251. package/dist/component/roles.js +884 -0
  252. package/dist/component/roles.js.map +1 -0
  253. package/dist/component/scheduledPublish.d.ts +182 -0
  254. package/dist/component/scheduledPublish.d.ts.map +1 -0
  255. package/dist/component/scheduledPublish.js +304 -0
  256. package/dist/component/scheduledPublish.js.map +1 -0
  257. package/dist/component/schema.d.ts +4114 -0
  258. package/dist/component/schema.d.ts.map +1 -0
  259. package/dist/component/schema.js +469 -0
  260. package/dist/component/schema.js.map +1 -0
  261. package/dist/component/taxonomies.d.ts +476 -0
  262. package/dist/component/taxonomies.d.ts.map +1 -0
  263. package/dist/component/taxonomies.js +785 -0
  264. package/dist/component/taxonomies.js.map +1 -0
  265. package/dist/component/taxonomyMutations.d.ts +206 -0
  266. package/dist/component/taxonomyMutations.d.ts.map +1 -0
  267. package/dist/component/taxonomyMutations.js +1001 -0
  268. package/dist/component/taxonomyMutations.js.map +1 -0
  269. package/dist/component/trash.d.ts +265 -0
  270. package/dist/component/trash.d.ts.map +1 -0
  271. package/dist/component/trash.js +621 -0
  272. package/dist/component/trash.js.map +1 -0
  273. package/dist/component/types.d.ts +4 -0
  274. package/dist/component/types.d.ts.map +1 -0
  275. package/dist/component/types.js +2 -0
  276. package/dist/component/types.js.map +1 -0
  277. package/dist/component/userContext.d.ts +508 -0
  278. package/dist/component/userContext.d.ts.map +1 -0
  279. package/dist/component/userContext.js +615 -0
  280. package/dist/component/userContext.js.map +1 -0
  281. package/dist/component/validation.d.ts +387 -0
  282. package/dist/component/validation.d.ts.map +1 -0
  283. package/dist/component/validation.js +1052 -0
  284. package/dist/component/validation.js.map +1 -0
  285. package/dist/component/validators.d.ts +4645 -0
  286. package/dist/component/validators.d.ts.map +1 -0
  287. package/dist/component/validators.js +641 -0
  288. package/dist/component/validators.js.map +1 -0
  289. package/dist/component/versionMutations.d.ts +216 -0
  290. package/dist/component/versionMutations.d.ts.map +1 -0
  291. package/dist/component/versionMutations.js +321 -0
  292. package/dist/component/versionMutations.js.map +1 -0
  293. package/dist/component/webhookTrigger.d.ts +770 -0
  294. package/dist/component/webhookTrigger.d.ts.map +1 -0
  295. package/dist/component/webhookTrigger.js +1413 -0
  296. package/dist/component/webhookTrigger.js.map +1 -0
  297. package/dist/react/index.d.ts +316 -0
  298. package/dist/react/index.d.ts.map +1 -0
  299. package/dist/react/index.js +558 -0
  300. package/dist/react/index.js.map +1 -0
  301. package/dist/test.d.ts +2230 -0
  302. package/dist/test.d.ts.map +1 -0
  303. package/dist/test.js +1107 -0
  304. package/dist/test.js.map +1 -0
  305. package/package.json +95 -0
  306. package/src/cli/commands/admin.ts +104 -0
  307. package/src/cli/index.ts +21 -0
  308. package/src/cli/utils/detectConvexUrl.ts +54 -0
  309. package/src/cli/utils/openBrowser.ts +16 -0
  310. package/src/client/admin-config.ts +138 -0
  311. package/src/client/adminApi.ts +942 -0
  312. package/src/client/agentTools.ts +1311 -0
  313. package/src/client/argTypes.ts +316 -0
  314. package/src/client/field-types.ts +187 -0
  315. package/src/client/index.ts +1301 -0
  316. package/src/client/queryBuilder.ts +1100 -0
  317. package/src/client/schema/codegen.ts +500 -0
  318. package/src/client/schema/defineContentType.ts +501 -0
  319. package/src/client/schema/index.ts +169 -0
  320. package/src/client/schema/schemaDrift.ts +574 -0
  321. package/src/client/schema/typedClient.ts +688 -0
  322. package/src/client/schema/types.ts +666 -0
  323. package/src/client/types.ts +723 -0
  324. package/src/client/workflows.ts +141 -0
  325. package/src/client/wrapper.ts +4304 -0
  326. package/src/component/_generated/api.ts +140 -0
  327. package/src/component/_generated/component.ts +5029 -0
  328. package/src/component/_generated/dataModel.ts +60 -0
  329. package/src/component/_generated/server.ts +156 -0
  330. package/src/component/authorization.ts +647 -0
  331. package/src/component/authorizationHooks.ts +668 -0
  332. package/src/component/bulkOperations.ts +687 -0
  333. package/src/component/contentEntries.ts +1976 -0
  334. package/src/component/contentEntryMutations.ts +1223 -0
  335. package/src/component/contentEntryValidation.ts +707 -0
  336. package/src/component/contentLock.ts +550 -0
  337. package/src/component/contentTypeMigration.ts +1064 -0
  338. package/src/component/contentTypeMutations.ts +969 -0
  339. package/src/component/contentTypes.ts +346 -0
  340. package/src/component/convex.config.ts +44 -0
  341. package/src/component/documentTypes.ts +240 -0
  342. package/src/component/eventEmitter.ts +485 -0
  343. package/src/component/exportImport.ts +1169 -0
  344. package/src/component/index.ts +491 -0
  345. package/src/component/lib/deepReferenceResolver.ts +999 -0
  346. package/src/component/lib/errors.ts +816 -0
  347. package/src/component/lib/index.ts +145 -0
  348. package/src/component/lib/mediaReferenceResolver.ts +495 -0
  349. package/src/component/lib/metadataExtractor.ts +792 -0
  350. package/src/component/lib/mutationAuth.ts +199 -0
  351. package/src/component/lib/queries.ts +79 -0
  352. package/src/component/lib/ragContentChunker.ts +1371 -0
  353. package/src/component/lib/referenceResolver.ts +430 -0
  354. package/src/component/lib/slugGenerator.ts +262 -0
  355. package/src/component/lib/slugUniqueness.ts +333 -0
  356. package/src/component/lib/softDelete.ts +44 -0
  357. package/src/component/localeFallbackChain.ts +673 -0
  358. package/src/component/localeFields.ts +896 -0
  359. package/src/component/mediaAssetMutations.ts +725 -0
  360. package/src/component/mediaAssets.ts +932 -0
  361. package/src/component/mediaFolderMutations.ts +1046 -0
  362. package/src/component/mediaUploadMutations.ts +224 -0
  363. package/src/component/mediaVariantMutations.ts +900 -0
  364. package/src/component/mediaVariants.ts +793 -0
  365. package/src/component/ragContentIndexer.ts +1067 -0
  366. package/src/component/rateLimitHooks.ts +572 -0
  367. package/src/component/roles.ts +1360 -0
  368. package/src/component/scheduledPublish.ts +358 -0
  369. package/src/component/schema.ts +617 -0
  370. package/src/component/taxonomies.ts +949 -0
  371. package/src/component/taxonomyMutations.ts +1210 -0
  372. package/src/component/trash.ts +724 -0
  373. package/src/component/userContext.ts +898 -0
  374. package/src/component/validation.ts +1388 -0
  375. package/src/component/validators.ts +949 -0
  376. package/src/component/versionMutations.ts +392 -0
  377. package/src/component/webhookTrigger.ts +1922 -0
  378. package/src/react/index.ts +898 -0
  379. package/src/test.ts +1580 -0
@@ -0,0 +1,1413 @@
1
+ /**
2
+ * Webhook Trigger Module
3
+ *
4
+ * Scheduled function to process content events and trigger configured webhooks.
5
+ * Supports retry logic with exponential backoff and delivery confirmation.
6
+ *
7
+ * Architecture:
8
+ * 1. Events are captured via the event emitter system (cmsEvents table)
9
+ * 2. A background processor polls for unprocessed events
10
+ * 3. For each event, matching webhook configurations are found
11
+ * 4. Delivery records are created and HTTP requests are dispatched
12
+ * 5. Success/failure is tracked with automatic retry for failures
13
+ *
14
+ * Security Features:
15
+ * - HMAC-SHA256 signature generation for payload verification
16
+ * - Configurable timeout to prevent hanging requests
17
+ * - Secret keys never exposed in API responses
18
+ *
19
+ * Retry Behavior:
20
+ * - Exponential backoff: 1min, 5min, 15min, 1hr, 4hr (configurable)
21
+ * - Automatic retry scheduling via Convex scheduler
22
+ * - Maximum retry limit per webhook configuration
23
+ *
24
+ * @module
25
+ */
26
+ import { v } from "convex/values";
27
+ import { isDeleted } from "./lib/softDelete.js";
28
+ import { mutation, query, internalMutation, internalQuery,
29
+ // action,
30
+ internalAction, } from "./_generated/server.js";
31
+ import { internal,
32
+ // api
33
+ } from "./_generated/api.js";
34
+ // =============================================================================
35
+ // Constants
36
+ // =============================================================================
37
+ /**
38
+ * Default configuration values.
39
+ */
40
+ export const DEFAULT_WEBHOOK_CONFIG = {
41
+ batchSize: 50,
42
+ pollingIntervalMs: 60000, // 1 minute
43
+ defaultTimeoutMs: 30000, // 30 seconds
44
+ defaultMaxRetries: 5,
45
+ };
46
+ /**
47
+ * Exponential backoff delays in milliseconds.
48
+ * Attempt 1: 1 minute
49
+ * Attempt 2: 5 minutes
50
+ * Attempt 3: 15 minutes
51
+ * Attempt 4: 1 hour
52
+ * Attempt 5: 4 hours
53
+ */
54
+ export const RETRY_DELAYS_MS = [
55
+ 1 * 60 * 1000, // 1 minute
56
+ 5 * 60 * 1000, // 5 minutes
57
+ 15 * 60 * 1000, // 15 minutes
58
+ 60 * 60 * 1000, // 1 hour
59
+ 4 * 60 * 60 * 1000, // 4 hours
60
+ ];
61
+ /**
62
+ * Maximum response body length to store (truncated for storage).
63
+ */
64
+ const MAX_RESPONSE_BODY_LENGTH = 1000;
65
+ // =============================================================================
66
+ // Validators
67
+ // =============================================================================
68
+ /**
69
+ * Validator for webhook delivery status.
70
+ */
71
+ export const webhookDeliveryStatusValidator = v.union(v.literal("pending"), v.literal("processing"), v.literal("delivered"), v.literal("failed"), v.literal("retrying"));
72
+ /**
73
+ * Validator for creating a webhook configuration.
74
+ */
75
+ export const createWebhookArgs = v.object({
76
+ /** Human-readable name for the webhook */
77
+ name: v.string(),
78
+ /** Optional description */
79
+ description: v.optional(v.string()),
80
+ /** Target URL (must be HTTPS in production) */
81
+ url: v.string(),
82
+ /** Secret for HMAC signature (optional but recommended) */
83
+ secret: v.optional(v.string()),
84
+ /** Event types to subscribe to (e.g., ["contentEntry.published"]) */
85
+ eventTypes: v.array(v.string()),
86
+ /** Filter by resource types (optional) */
87
+ resourceTypes: v.optional(v.array(v.union(v.literal("contentEntry"), v.literal("contentType"), v.literal("mediaAsset"), v.literal("mediaFolder")))),
88
+ /** Filter by content types (optional, for contentEntry events) */
89
+ contentTypes: v.optional(v.array(v.string())),
90
+ /** Additional HTTP headers */
91
+ headers: v.optional(v.any()),
92
+ /** Whether the webhook is enabled (default: true) */
93
+ enabled: v.optional(v.boolean()),
94
+ /** Maximum retry attempts (default: 5) */
95
+ maxRetries: v.optional(v.number()),
96
+ /** Request timeout in ms (default: 30000) */
97
+ timeoutMs: v.optional(v.number()),
98
+ /** User creating the webhook */
99
+ createdBy: v.optional(v.string()),
100
+ });
101
+ /**
102
+ * Validator for updating a webhook configuration.
103
+ */
104
+ export const updateWebhookArgs = v.object({
105
+ /** Webhook ID to update */
106
+ id: v.id("webhookConfigs"),
107
+ /** New name */
108
+ name: v.optional(v.string()),
109
+ /** New description */
110
+ description: v.optional(v.string()),
111
+ /** New URL */
112
+ url: v.optional(v.string()),
113
+ /** New secret (set to empty string to remove) */
114
+ secret: v.optional(v.string()),
115
+ /** New event types filter */
116
+ eventTypes: v.optional(v.array(v.string())),
117
+ /** New resource types filter */
118
+ resourceTypes: v.optional(v.array(v.union(v.literal("contentEntry"), v.literal("contentType"), v.literal("mediaAsset"), v.literal("mediaFolder")))),
119
+ /** New content types filter */
120
+ contentTypes: v.optional(v.array(v.string())),
121
+ /** New headers */
122
+ headers: v.optional(v.any()),
123
+ /** Enable/disable the webhook */
124
+ enabled: v.optional(v.boolean()),
125
+ /** New max retries */
126
+ maxRetries: v.optional(v.number()),
127
+ /** New timeout */
128
+ timeoutMs: v.optional(v.number()),
129
+ /** User performing the update */
130
+ updatedBy: v.optional(v.string()),
131
+ });
132
+ /**
133
+ * Validator for deleting a webhook configuration.
134
+ */
135
+ export const deleteWebhookArgs = v.object({
136
+ /** Webhook ID to delete */
137
+ id: v.id("webhookConfigs"),
138
+ /** Hard delete (true) or soft delete (false, default) */
139
+ hardDelete: v.optional(v.boolean()),
140
+ /** User performing the deletion */
141
+ deletedBy: v.optional(v.string()),
142
+ });
143
+ /**
144
+ * Validator for webhook configuration document (return type).
145
+ */
146
+ export const webhookConfigDoc = v.object({
147
+ _id: v.id("webhookConfigs"),
148
+ _creationTime: v.number(),
149
+ name: v.string(),
150
+ description: v.optional(v.string()),
151
+ url: v.string(),
152
+ // Note: secret is NOT included in return type for security
153
+ eventTypes: v.array(v.string()),
154
+ resourceTypes: v.optional(v.array(v.union(v.literal("contentEntry"), v.literal("contentType"), v.literal("mediaAsset"), v.literal("mediaFolder")))),
155
+ contentTypes: v.optional(v.array(v.string())),
156
+ headers: v.optional(v.any()),
157
+ enabled: v.boolean(),
158
+ maxRetries: v.optional(v.number()),
159
+ timeoutMs: v.optional(v.number()),
160
+ deletedAt: v.optional(v.number()),
161
+ createdBy: v.optional(v.string()),
162
+ updatedBy: v.optional(v.string()),
163
+ });
164
+ /**
165
+ * Validator for webhook delivery document.
166
+ */
167
+ export const webhookDeliveryDoc = v.object({
168
+ _id: v.id("webhookDeliveries"),
169
+ _creationTime: v.number(),
170
+ webhookId: v.id("webhookConfigs"),
171
+ eventId: v.id("cmsEvents"),
172
+ eventType: v.string(),
173
+ status: webhookDeliveryStatusValidator,
174
+ attemptCount: v.number(),
175
+ maxAttempts: v.number(),
176
+ lastAttemptAt: v.optional(v.number()),
177
+ nextRetryAt: v.optional(v.number()),
178
+ lastStatusCode: v.optional(v.number()),
179
+ lastError: v.optional(v.string()),
180
+ lastResponseBody: v.optional(v.string()),
181
+ lastDurationMs: v.optional(v.number()),
182
+ payload: v.any(),
183
+ deliveredAt: v.optional(v.number()),
184
+ });
185
+ // =============================================================================
186
+ // Webhook Configuration CRUD
187
+ // =============================================================================
188
+ /**
189
+ * Create a new webhook configuration.
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const webhookId = await ctx.runMutation(api.webhookTrigger.createWebhook, {
194
+ * name: "CDN Invalidation",
195
+ * url: "https://api.example.com/webhooks/cms",
196
+ * secret: "my-secret-key",
197
+ * eventTypes: ["contentEntry.published", "contentEntry.deleted"],
198
+ * });
199
+ * ```
200
+ */
201
+ export const createWebhook = mutation({
202
+ args: createWebhookArgs.fields,
203
+ returns: v.id("webhookConfigs"),
204
+ handler: async (ctx, args) => {
205
+ const { name, description, url, secret, eventTypes, resourceTypes, contentTypes, headers, enabled = true, maxRetries = DEFAULT_WEBHOOK_CONFIG.defaultMaxRetries, timeoutMs = DEFAULT_WEBHOOK_CONFIG.defaultTimeoutMs, createdBy, } = args;
206
+ // Validate URL format
207
+ try {
208
+ const parsedUrl = new URL(url);
209
+ // Warn about non-HTTPS URLs (but allow for development)
210
+ if (parsedUrl.protocol !== "https:") {
211
+ console.warn(`Webhook URL is not HTTPS: ${url}. ` +
212
+ "HTTPS is required for production security.");
213
+ }
214
+ }
215
+ catch {
216
+ throw new Error(`Invalid webhook URL: ${url}`);
217
+ }
218
+ // Validate event types format
219
+ for (const eventType of eventTypes) {
220
+ if (!eventType.includes(".")) {
221
+ throw new Error(`Invalid event type format: "${eventType}". ` +
222
+ 'Expected format: "resourceType.action" (e.g., "contentEntry.published")');
223
+ }
224
+ }
225
+ const webhookId = await ctx.db.insert("webhookConfigs", {
226
+ name,
227
+ description,
228
+ url,
229
+ secret,
230
+ eventTypes,
231
+ resourceTypes,
232
+ contentTypes,
233
+ headers,
234
+ enabled,
235
+ maxRetries,
236
+ timeoutMs,
237
+ createdBy,
238
+ });
239
+ return webhookId;
240
+ },
241
+ });
242
+ /**
243
+ * Update an existing webhook configuration.
244
+ */
245
+ export const updateWebhook = mutation({
246
+ args: updateWebhookArgs.fields,
247
+ returns: webhookConfigDoc,
248
+ handler: async (ctx, args) => {
249
+ const { id, ...updates } = args;
250
+ const existing = await ctx.db.get(id);
251
+ if (!existing) {
252
+ throw new Error(`Webhook not found: ${id}`);
253
+ }
254
+ if (isDeleted(existing)) {
255
+ throw new Error(`Webhook has been deleted: ${id}`);
256
+ }
257
+ // Validate URL if being updated
258
+ if (updates.url) {
259
+ try {
260
+ new URL(updates.url);
261
+ }
262
+ catch {
263
+ throw new Error(`Invalid webhook URL: ${updates.url}`);
264
+ }
265
+ }
266
+ // Validate event types if being updated
267
+ if (updates.eventTypes) {
268
+ for (const eventType of updates.eventTypes) {
269
+ if (!eventType.includes(".")) {
270
+ throw new Error(`Invalid event type format: "${eventType}". ` +
271
+ 'Expected format: "resourceType.action"');
272
+ }
273
+ }
274
+ }
275
+ // Build update object, excluding undefined values
276
+ const updateData = {};
277
+ for (const [key, value] of Object.entries(updates)) {
278
+ if (value !== undefined) {
279
+ updateData[key] = value;
280
+ }
281
+ }
282
+ await ctx.db.patch(id, updateData);
283
+ const updated = await ctx.db.get(id);
284
+ if (!updated) {
285
+ throw new Error("Failed to retrieve updated webhook");
286
+ }
287
+ // Return without secret
288
+ const { secret: _secret, ...safeWebhook } = updated;
289
+ return safeWebhook;
290
+ },
291
+ });
292
+ /**
293
+ * Delete a webhook configuration.
294
+ */
295
+ export const deleteWebhook = mutation({
296
+ args: deleteWebhookArgs.fields,
297
+ returns: v.object({
298
+ success: v.boolean(),
299
+ message: v.string(),
300
+ }),
301
+ handler: async (ctx, args) => {
302
+ const { id, hardDelete = false, deletedBy } = args;
303
+ const webhook = await ctx.db.get(id);
304
+ if (!webhook) {
305
+ throw new Error(`Webhook not found: ${id}`);
306
+ }
307
+ if (hardDelete) {
308
+ // Delete all delivery records for this webhook
309
+ const deliveries = await ctx.db
310
+ .query("webhookDeliveries")
311
+ .withIndex("by_webhook", (q) => q.eq("webhookId", id))
312
+ .collect();
313
+ for (const delivery of deliveries) {
314
+ await ctx.db.delete(delivery._id);
315
+ }
316
+ // Delete the webhook
317
+ await ctx.db.delete(id);
318
+ return {
319
+ success: true,
320
+ message: `Webhook "${webhook.name}" permanently deleted with ${deliveries.length} delivery records`,
321
+ };
322
+ }
323
+ else {
324
+ // Soft delete
325
+ await ctx.db.patch(id, {
326
+ deletedAt: Date.now(),
327
+ enabled: false,
328
+ updatedBy: deletedBy,
329
+ });
330
+ return {
331
+ success: true,
332
+ message: `Webhook "${webhook.name}" soft-deleted`,
333
+ };
334
+ }
335
+ },
336
+ });
337
+ /**
338
+ * Restore a soft-deleted webhook.
339
+ */
340
+ export const restoreWebhook = mutation({
341
+ args: {
342
+ id: v.id("webhookConfigs"),
343
+ restoredBy: v.optional(v.string()),
344
+ },
345
+ returns: webhookConfigDoc,
346
+ handler: async (ctx, args) => {
347
+ const { id, restoredBy } = args;
348
+ const webhook = await ctx.db.get(id);
349
+ if (!webhook) {
350
+ throw new Error(`Webhook not found: ${id}`);
351
+ }
352
+ if (!isDeleted(webhook)) {
353
+ throw new Error(`Webhook is not deleted: ${id}`);
354
+ }
355
+ await ctx.db.patch(id, {
356
+ deletedAt: undefined,
357
+ updatedBy: restoredBy,
358
+ });
359
+ const restored = await ctx.db.get(id);
360
+ if (!restored) {
361
+ throw new Error("Failed to retrieve restored webhook");
362
+ }
363
+ const { secret: _secret, ...safeWebhook } = restored;
364
+ return safeWebhook;
365
+ },
366
+ });
367
+ /**
368
+ * Get a single webhook configuration by ID.
369
+ * Note: Secret is not returned for security.
370
+ */
371
+ export const getWebhook = query({
372
+ args: {
373
+ id: v.id("webhookConfigs"),
374
+ includeDeleted: v.optional(v.boolean()),
375
+ },
376
+ returns: v.union(webhookConfigDoc, v.null()),
377
+ handler: async (ctx, args) => {
378
+ const { id, includeDeleted = false } = args;
379
+ const webhook = await ctx.db.get(id);
380
+ if (!webhook)
381
+ return null;
382
+ if (!includeDeleted && isDeleted(webhook)) {
383
+ return null;
384
+ }
385
+ const { secret: _secret, ...safeWebhook } = webhook;
386
+ return safeWebhook;
387
+ },
388
+ });
389
+ /**
390
+ * List all webhook configurations with optional filtering.
391
+ */
392
+ export const listWebhooks = query({
393
+ args: {
394
+ enabled: v.optional(v.boolean()),
395
+ includeDeleted: v.optional(v.boolean()),
396
+ limit: v.optional(v.number()),
397
+ },
398
+ returns: v.array(webhookConfigDoc),
399
+ handler: async (ctx, args) => {
400
+ const { enabled, includeDeleted = false, limit = 50 } = args;
401
+ let webhooks;
402
+ if (enabled !== undefined) {
403
+ webhooks = await ctx.db
404
+ .query("webhookConfigs")
405
+ .withIndex("by_enabled", (q) => q.eq("enabled", enabled))
406
+ .take(limit * 2);
407
+ }
408
+ else {
409
+ webhooks = await ctx.db
410
+ .query("webhookConfigs")
411
+ .order("desc")
412
+ .take(limit * 2);
413
+ }
414
+ // Filter deleted
415
+ if (!includeDeleted) {
416
+ webhooks = webhooks.filter((w) => !isDeleted(w));
417
+ }
418
+ // Remove secrets before returning
419
+ return webhooks.slice(0, limit).map((webhook) => {
420
+ const { secret: _secret, ...safeWebhook } = webhook;
421
+ return safeWebhook;
422
+ });
423
+ },
424
+ });
425
+ // =============================================================================
426
+ // Event Processing & Delivery
427
+ // =============================================================================
428
+ /**
429
+ * Internal query to get webhooks matching an event.
430
+ */
431
+ export const getMatchingWebhooks = internalQuery({
432
+ args: {
433
+ eventType: v.string(),
434
+ resourceType: v.union(v.literal("contentEntry"), v.literal("contentType"), v.literal("mediaAsset"), v.literal("mediaFolder")),
435
+ contentTypeName: v.optional(v.string()),
436
+ },
437
+ handler: async (ctx, args) => {
438
+ const { eventType, resourceType, contentTypeName } = args;
439
+ // Get all enabled webhooks
440
+ const webhooks = await ctx.db
441
+ .query("webhookConfigs")
442
+ .withIndex("by_enabled", (q) => q.eq("enabled", true))
443
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
444
+ .collect();
445
+ // Filter to matching webhooks
446
+ return webhooks.filter((webhook) => {
447
+ // Check event type filter
448
+ if (webhook.eventTypes.length > 0 &&
449
+ !webhook.eventTypes.includes(eventType)) {
450
+ return false;
451
+ }
452
+ // Check resource type filter
453
+ if (webhook.resourceTypes &&
454
+ webhook.resourceTypes.length > 0 &&
455
+ !webhook.resourceTypes.includes(resourceType)) {
456
+ return false;
457
+ }
458
+ // Check content type filter (only for contentEntry events)
459
+ if (resourceType === "contentEntry" &&
460
+ webhook.contentTypes &&
461
+ webhook.contentTypes.length > 0 &&
462
+ contentTypeName &&
463
+ !webhook.contentTypes.includes(contentTypeName)) {
464
+ return false;
465
+ }
466
+ return true;
467
+ });
468
+ },
469
+ });
470
+ /**
471
+ * Internal query to get unprocessed events for webhook delivery.
472
+ */
473
+ export const getUnprocessedWebhookEvents = internalQuery({
474
+ args: {
475
+ limit: v.optional(v.number()),
476
+ },
477
+ handler: async (ctx, args) => {
478
+ const { limit = 50 } = args;
479
+ // Get unprocessed events
480
+ const events = await ctx.db
481
+ .query("cmsEvents")
482
+ .withIndex("by_processed", (q) => q.eq("processed", false))
483
+ .order("asc")
484
+ .take(limit);
485
+ return events;
486
+ },
487
+ });
488
+ /**
489
+ * Internal query to get pending or retrying deliveries.
490
+ */
491
+ export const getPendingDeliveries = internalQuery({
492
+ args: {
493
+ limit: v.optional(v.number()),
494
+ },
495
+ handler: async (ctx, args) => {
496
+ const { limit = 50 } = args;
497
+ const now = Date.now();
498
+ // Get pending deliveries
499
+ const pending = await ctx.db
500
+ .query("webhookDeliveries")
501
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
502
+ .take(limit);
503
+ // Get retrying deliveries whose retry time has passed
504
+ const retrying = await ctx.db
505
+ .query("webhookDeliveries")
506
+ .withIndex("by_status", (q) => q.eq("status", "retrying"))
507
+ .filter((q) => q.or(q.eq(q.field("nextRetryAt"), undefined), q.lte(q.field("nextRetryAt"), now)))
508
+ .take(limit);
509
+ return [...pending, ...retrying].slice(0, limit);
510
+ },
511
+ });
512
+ /**
513
+ * Internal mutation to create a delivery record for an event-webhook pair.
514
+ */
515
+ export const createDelivery = internalMutation({
516
+ args: {
517
+ webhookId: v.id("webhookConfigs"),
518
+ eventId: v.id("cmsEvents"),
519
+ eventType: v.string(),
520
+ maxAttempts: v.number(),
521
+ payload: v.any(),
522
+ },
523
+ returns: v.id("webhookDeliveries"),
524
+ handler: async (ctx, args) => {
525
+ const { webhookId, eventId, eventType, maxAttempts, payload } = args;
526
+ // Check if delivery already exists for this webhook-event pair
527
+ const existing = await ctx.db
528
+ .query("webhookDeliveries")
529
+ .withIndex("by_event", (q) => q.eq("eventId", eventId))
530
+ .filter((q) => q.eq(q.field("webhookId"), webhookId))
531
+ .first();
532
+ if (existing) {
533
+ // Already exists, return existing ID
534
+ return existing._id;
535
+ }
536
+ const deliveryId = await ctx.db.insert("webhookDeliveries", {
537
+ webhookId,
538
+ eventId,
539
+ eventType,
540
+ status: "pending",
541
+ attemptCount: 0,
542
+ maxAttempts,
543
+ payload,
544
+ });
545
+ return deliveryId;
546
+ },
547
+ });
548
+ /**
549
+ * Internal mutation to update delivery status after an attempt.
550
+ */
551
+ export const updateDeliveryStatus = internalMutation({
552
+ args: {
553
+ deliveryId: v.id("webhookDeliveries"),
554
+ status: webhookDeliveryStatusValidator,
555
+ statusCode: v.optional(v.number()),
556
+ error: v.optional(v.string()),
557
+ responseBody: v.optional(v.string()),
558
+ durationMs: v.optional(v.number()),
559
+ nextRetryAt: v.optional(v.number()),
560
+ },
561
+ handler: async (ctx, args) => {
562
+ const { deliveryId, status, statusCode, error, responseBody, durationMs, nextRetryAt, } = args;
563
+ const delivery = await ctx.db.get(deliveryId);
564
+ if (!delivery) {
565
+ throw new Error(`Delivery not found: ${deliveryId}`);
566
+ }
567
+ const now = Date.now();
568
+ const updates = {
569
+ status,
570
+ lastAttemptAt: now,
571
+ attemptCount: delivery.attemptCount + 1,
572
+ };
573
+ if (statusCode !== undefined) {
574
+ updates.lastStatusCode = statusCode;
575
+ }
576
+ if (error !== undefined) {
577
+ updates.lastError = error;
578
+ }
579
+ if (responseBody !== undefined) {
580
+ updates.lastResponseBody = responseBody.slice(0, MAX_RESPONSE_BODY_LENGTH);
581
+ }
582
+ if (durationMs !== undefined) {
583
+ updates.lastDurationMs = durationMs;
584
+ }
585
+ if (nextRetryAt !== undefined) {
586
+ updates.nextRetryAt = nextRetryAt;
587
+ }
588
+ if (status === "delivered") {
589
+ updates.deliveredAt = now;
590
+ }
591
+ await ctx.db.patch(deliveryId, updates);
592
+ },
593
+ });
594
+ /**
595
+ * Internal mutation to mark delivery as processing.
596
+ */
597
+ export const markDeliveryProcessing = internalMutation({
598
+ args: {
599
+ deliveryId: v.id("webhookDeliveries"),
600
+ },
601
+ handler: async (ctx, args) => {
602
+ await ctx.db.patch(args.deliveryId, {
603
+ status: "processing",
604
+ });
605
+ },
606
+ });
607
+ /**
608
+ * Generate HMAC-SHA256 signature for webhook payload.
609
+ *
610
+ * @param payload - The JSON payload to sign
611
+ * @param secret - The secret key for signing
612
+ * @returns Hex-encoded signature
613
+ */
614
+ async function generateSignature(payload, secret) {
615
+ const encoder = new TextEncoder();
616
+ const keyData = encoder.encode(secret);
617
+ const data = encoder.encode(payload);
618
+ const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
619
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, data);
620
+ const hashArray = Array.from(new Uint8Array(signature));
621
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
622
+ }
623
+ /**
624
+ * Internal action to send a webhook delivery.
625
+ * Actions are needed for HTTP requests to external services.
626
+ */
627
+ export const sendWebhookDelivery = internalAction({
628
+ args: {
629
+ deliveryId: v.id("webhookDeliveries"),
630
+ },
631
+ handler: async (ctx, args) => {
632
+ const { deliveryId } = args;
633
+ // Get delivery and webhook info
634
+ const delivery = await ctx.runQuery(internal.webhookTrigger.getDeliveryWithWebhook, { deliveryId });
635
+ if (!delivery) {
636
+ console.error(`Delivery not found: ${deliveryId}`);
637
+ return;
638
+ }
639
+ const { webhook, ...deliveryData } = delivery;
640
+ // Mark as processing
641
+ await ctx.runMutation(internal.webhookTrigger.markDeliveryProcessing, {
642
+ deliveryId,
643
+ });
644
+ const payload = JSON.stringify(deliveryData.payload);
645
+ const startTime = Date.now();
646
+ try {
647
+ // Build headers
648
+ const headers = {
649
+ "Content-Type": "application/json",
650
+ "X-Webhook-Delivery-Id": deliveryId,
651
+ "X-Webhook-Event-Type": deliveryData.eventType,
652
+ ...(webhook.headers || {}),
653
+ };
654
+ // Add signature if secret is configured
655
+ if (webhook.secret) {
656
+ const signature = await generateSignature(payload, webhook.secret);
657
+ headers["X-Webhook-Signature"] = `sha256=${signature}`;
658
+ }
659
+ // Send request with timeout
660
+ const controller = new AbortController();
661
+ const timeoutMs = webhook.timeoutMs || DEFAULT_WEBHOOK_CONFIG.defaultTimeoutMs;
662
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
663
+ const response = await fetch(webhook.url, {
664
+ method: "POST",
665
+ headers,
666
+ body: payload,
667
+ signal: controller.signal,
668
+ });
669
+ clearTimeout(timeoutId);
670
+ const durationMs = Date.now() - startTime;
671
+ const responseBody = await response.text().catch(() => "");
672
+ if (response.ok) {
673
+ // Success (2xx status)
674
+ await ctx.runMutation(internal.webhookTrigger.updateDeliveryStatus, {
675
+ deliveryId,
676
+ status: "delivered",
677
+ statusCode: response.status,
678
+ responseBody,
679
+ durationMs,
680
+ });
681
+ console.log(`Webhook delivered successfully: ${deliveryId} to ${webhook.url} (${response.status})`);
682
+ }
683
+ else {
684
+ // HTTP error
685
+ await handleDeliveryFailure(ctx, deliveryId, deliveryData.attemptCount + 1, deliveryData.maxAttempts, `HTTP ${response.status}: ${response.statusText}`, response.status, responseBody, durationMs);
686
+ }
687
+ }
688
+ catch (error) {
689
+ const durationMs = Date.now() - startTime;
690
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
691
+ // Handle timeout specifically
692
+ if (errorMessage.includes("aborted")) {
693
+ await handleDeliveryFailure(ctx, deliveryId, deliveryData.attemptCount + 1, deliveryData.maxAttempts, `Request timeout after ${webhook.timeoutMs || DEFAULT_WEBHOOK_CONFIG.defaultTimeoutMs}ms`, undefined, undefined, durationMs);
694
+ }
695
+ else {
696
+ await handleDeliveryFailure(ctx, deliveryId, deliveryData.attemptCount + 1, deliveryData.maxAttempts, errorMessage, undefined, undefined, durationMs);
697
+ }
698
+ }
699
+ },
700
+ });
701
+ /**
702
+ * Handle delivery failure with retry logic.
703
+ */
704
+ async function handleDeliveryFailure(ctx, deliveryId, attemptCount, maxAttempts, error, statusCode, responseBody, durationMs) {
705
+ if (attemptCount < maxAttempts) {
706
+ // Calculate next retry time with exponential backoff
707
+ const delayIndex = Math.min(attemptCount - 1, RETRY_DELAYS_MS.length - 1);
708
+ const delay = RETRY_DELAYS_MS[delayIndex];
709
+ const nextRetryAt = Date.now() + delay;
710
+ await ctx.runMutation(internal.webhookTrigger.updateDeliveryStatus, {
711
+ deliveryId: deliveryId,
712
+ status: "retrying",
713
+ statusCode,
714
+ error,
715
+ responseBody,
716
+ durationMs,
717
+ nextRetryAt,
718
+ });
719
+ console.log(`Webhook delivery ${deliveryId} failed (attempt ${attemptCount}/${maxAttempts}). ` +
720
+ `Retrying in ${delay / 1000}s. Error: ${error}`);
721
+ // Schedule retry
722
+ await ctx.scheduler.runAt(nextRetryAt, internal.webhookTrigger.sendWebhookDelivery, { deliveryId: deliveryId });
723
+ }
724
+ else {
725
+ // Max retries exhausted
726
+ await ctx.runMutation(internal.webhookTrigger.updateDeliveryStatus, {
727
+ deliveryId: deliveryId,
728
+ status: "failed",
729
+ statusCode,
730
+ error,
731
+ responseBody,
732
+ durationMs,
733
+ });
734
+ console.error(`Webhook delivery ${deliveryId} failed permanently after ${maxAttempts} attempts. Error: ${error}`);
735
+ }
736
+ }
737
+ /**
738
+ * Internal query to get delivery with webhook info.
739
+ */
740
+ export const getDeliveryWithWebhook = internalQuery({
741
+ args: {
742
+ deliveryId: v.id("webhookDeliveries"),
743
+ },
744
+ handler: async (ctx, args) => {
745
+ const delivery = await ctx.db.get(args.deliveryId);
746
+ if (!delivery)
747
+ return null;
748
+ const webhook = await ctx.db.get(delivery.webhookId);
749
+ if (!webhook || isDeleted(webhook) || !webhook.enabled) {
750
+ return null;
751
+ }
752
+ return {
753
+ ...delivery,
754
+ webhook,
755
+ };
756
+ },
757
+ });
758
+ // =============================================================================
759
+ // Background Job Scheduling
760
+ // =============================================================================
761
+ /**
762
+ * Process unprocessed events and create webhook deliveries.
763
+ *
764
+ * This mutation:
765
+ * 1. Gets unprocessed events from cmsEvents
766
+ * 2. For each event, finds matching webhook configurations
767
+ * 3. Creates delivery records for each event-webhook pair
768
+ *
769
+ * Call this periodically to queue new deliveries.
770
+ */
771
+ export const processEventsForDelivery = internalMutation({
772
+ args: {
773
+ batchSize: v.optional(v.number()),
774
+ },
775
+ returns: v.object({
776
+ eventsProcessed: v.number(),
777
+ deliveriesCreated: v.number(),
778
+ hasMore: v.boolean(),
779
+ }),
780
+ handler: async (ctx, args) => {
781
+ const { batchSize = DEFAULT_WEBHOOK_CONFIG.batchSize } = args;
782
+ // Get unprocessed events
783
+ const events = await ctx.db
784
+ .query("cmsEvents")
785
+ .withIndex("by_processed", (q) => q.eq("processed", false))
786
+ .order("asc")
787
+ .take(batchSize + 1);
788
+ const hasMore = events.length > batchSize;
789
+ const eventsToProcess = events.slice(0, batchSize);
790
+ let deliveriesCreated = 0;
791
+ for (const event of eventsToProcess) {
792
+ // Get content type name from payload if available
793
+ const payload = event.payload;
794
+ const contentTypeName = payload?.contentTypeName;
795
+ // Get matching webhooks
796
+ const webhooks = await ctx.db
797
+ .query("webhookConfigs")
798
+ .withIndex("by_enabled", (q) => q.eq("enabled", true))
799
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
800
+ .collect();
801
+ // Filter to matching webhooks
802
+ const matchingWebhooks = webhooks.filter((webhook) => {
803
+ // Check event type filter
804
+ if (webhook.eventTypes.length > 0 &&
805
+ !webhook.eventTypes.includes(event.eventType)) {
806
+ return false;
807
+ }
808
+ // Check resource type filter
809
+ if (webhook.resourceTypes &&
810
+ webhook.resourceTypes.length > 0 &&
811
+ !webhook.resourceTypes.includes(event.resourceType)) {
812
+ return false;
813
+ }
814
+ // Check content type filter
815
+ if (event.resourceType === "contentEntry" &&
816
+ webhook.contentTypes &&
817
+ webhook.contentTypes.length > 0 &&
818
+ contentTypeName &&
819
+ !webhook.contentTypes.includes(contentTypeName)) {
820
+ return false;
821
+ }
822
+ return true;
823
+ });
824
+ // Create delivery for each matching webhook
825
+ for (const webhook of matchingWebhooks) {
826
+ // Check if delivery already exists
827
+ const existing = await ctx.db
828
+ .query("webhookDeliveries")
829
+ .withIndex("by_event", (q) => q.eq("eventId", event._id))
830
+ .filter((q) => q.eq(q.field("webhookId"), webhook._id))
831
+ .first();
832
+ if (!existing) {
833
+ // Build webhook payload
834
+ const webhookPayload = {
835
+ deliveryId: "", // Will be updated after creation
836
+ eventType: event.eventType,
837
+ resourceType: event.resourceType,
838
+ resourceId: event.resourceId,
839
+ action: event.action,
840
+ data: event.payload,
841
+ timestamp: new Date(event._creationTime).toISOString(),
842
+ userId: event.userId,
843
+ };
844
+ const deliveryId = await ctx.db.insert("webhookDeliveries", {
845
+ webhookId: webhook._id,
846
+ eventId: event._id,
847
+ eventType: event.eventType,
848
+ status: "pending",
849
+ attemptCount: 0,
850
+ maxAttempts: webhook.maxRetries ?? DEFAULT_WEBHOOK_CONFIG.defaultMaxRetries,
851
+ payload: { ...webhookPayload, deliveryId: "pending" },
852
+ });
853
+ // Update payload with actual delivery ID
854
+ const updatedPayload = { ...webhookPayload, deliveryId };
855
+ await ctx.db.patch(deliveryId, { payload: updatedPayload });
856
+ deliveriesCreated++;
857
+ }
858
+ }
859
+ // Mark event as processed for webhooks
860
+ // Note: We don't use the generic markEventsProcessed because
861
+ // other processors (like RAG indexer) may also need the event
862
+ // Instead, we track webhook processing via delivery records
863
+ }
864
+ return {
865
+ eventsProcessed: eventsToProcess.length,
866
+ deliveriesCreated,
867
+ hasMore,
868
+ };
869
+ },
870
+ });
871
+ /**
872
+ * Trigger webhook deliveries for pending/retrying items.
873
+ *
874
+ * This internal mutation schedules action calls for each pending delivery.
875
+ */
876
+ export const triggerPendingDeliveries = internalMutation({
877
+ args: {
878
+ batchSize: v.optional(v.number()),
879
+ },
880
+ returns: v.object({
881
+ deliveriesTriggered: v.number(),
882
+ hasMore: v.boolean(),
883
+ }),
884
+ handler: async (ctx, args) => {
885
+ const { batchSize = DEFAULT_WEBHOOK_CONFIG.batchSize } = args;
886
+ const now = Date.now();
887
+ // Get pending deliveries
888
+ const pending = await ctx.db
889
+ .query("webhookDeliveries")
890
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
891
+ .take(batchSize);
892
+ // Get retrying deliveries whose retry time has passed
893
+ const retrying = await ctx.db
894
+ .query("webhookDeliveries")
895
+ .withIndex("by_status", (q) => q.eq("status", "retrying"))
896
+ .filter((q) => q.or(q.eq(q.field("nextRetryAt"), undefined), q.lte(q.field("nextRetryAt"), now)))
897
+ .take(batchSize);
898
+ const allDeliveries = [...pending, ...retrying].slice(0, batchSize);
899
+ const hasMore = pending.length >= batchSize || retrying.length >= batchSize;
900
+ // Schedule delivery action for each
901
+ for (const delivery of allDeliveries) {
902
+ await ctx.scheduler.runAfter(0, internal.webhookTrigger.sendWebhookDelivery, { deliveryId: delivery._id });
903
+ }
904
+ return {
905
+ deliveriesTriggered: allDeliveries.length,
906
+ hasMore,
907
+ };
908
+ },
909
+ });
910
+ /**
911
+ * Main scheduled function that processes events and triggers webhooks.
912
+ *
913
+ * This should be called periodically (e.g., every minute) to:
914
+ * 1. Create deliveries for new events
915
+ * 2. Trigger pending/retrying deliveries
916
+ */
917
+ export const processWebhooks = internalMutation({
918
+ args: {
919
+ config: v.optional(v.object({
920
+ batchSize: v.optional(v.number()),
921
+ })),
922
+ },
923
+ returns: v.object({
924
+ eventsProcessed: v.number(),
925
+ deliveriesCreated: v.number(),
926
+ deliveriesTriggered: v.number(),
927
+ hasMore: v.boolean(),
928
+ }),
929
+ handler: async (ctx, args) => {
930
+ const config = { ...DEFAULT_WEBHOOK_CONFIG, ...(args.config || {}) };
931
+ const batchSize = config.batchSize ?? DEFAULT_WEBHOOK_CONFIG.batchSize;
932
+ // Step 1: Process events and create deliveries
933
+ // Inline the logic to avoid calling handler directly
934
+ const events = await ctx.db
935
+ .query("cmsEvents")
936
+ .withIndex("by_processed", (q) => q.eq("processed", false))
937
+ .order("asc")
938
+ .take(batchSize + 1);
939
+ const hasMoreEvents = events.length > batchSize;
940
+ const eventsToProcess = events.slice(0, batchSize);
941
+ let deliveriesCreated = 0;
942
+ for (const event of eventsToProcess) {
943
+ const payload = event.payload;
944
+ const contentTypeName = payload?.contentTypeName;
945
+ const webhooks = await ctx.db
946
+ .query("webhookConfigs")
947
+ .withIndex("by_enabled", (q) => q.eq("enabled", true))
948
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
949
+ .collect();
950
+ const matchingWebhooks = webhooks.filter((webhook) => {
951
+ if (webhook.eventTypes.length > 0 &&
952
+ !webhook.eventTypes.includes(event.eventType)) {
953
+ return false;
954
+ }
955
+ if (webhook.resourceTypes &&
956
+ webhook.resourceTypes.length > 0 &&
957
+ !webhook.resourceTypes.includes(event.resourceType)) {
958
+ return false;
959
+ }
960
+ if (event.resourceType === "contentEntry" &&
961
+ webhook.contentTypes &&
962
+ webhook.contentTypes.length > 0 &&
963
+ contentTypeName &&
964
+ !webhook.contentTypes.includes(contentTypeName)) {
965
+ return false;
966
+ }
967
+ return true;
968
+ });
969
+ for (const webhook of matchingWebhooks) {
970
+ const existing = await ctx.db
971
+ .query("webhookDeliveries")
972
+ .withIndex("by_event", (q) => q.eq("eventId", event._id))
973
+ .filter((q) => q.eq(q.field("webhookId"), webhook._id))
974
+ .first();
975
+ if (!existing) {
976
+ const webhookPayload = {
977
+ deliveryId: "pending",
978
+ eventType: event.eventType,
979
+ resourceType: event.resourceType,
980
+ resourceId: event.resourceId,
981
+ action: event.action,
982
+ data: event.payload,
983
+ timestamp: new Date(event._creationTime).toISOString(),
984
+ userId: event.userId,
985
+ };
986
+ const deliveryId = await ctx.db.insert("webhookDeliveries", {
987
+ webhookId: webhook._id,
988
+ eventId: event._id,
989
+ eventType: event.eventType,
990
+ status: "pending",
991
+ attemptCount: 0,
992
+ maxAttempts: webhook.maxRetries ?? DEFAULT_WEBHOOK_CONFIG.defaultMaxRetries,
993
+ payload: webhookPayload,
994
+ });
995
+ const updatedPayload = { ...webhookPayload, deliveryId };
996
+ await ctx.db.patch(deliveryId, { payload: updatedPayload });
997
+ deliveriesCreated++;
998
+ }
999
+ }
1000
+ }
1001
+ // Step 2: Trigger pending deliveries
1002
+ const now = Date.now();
1003
+ const pending = await ctx.db
1004
+ .query("webhookDeliveries")
1005
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
1006
+ .take(batchSize);
1007
+ const retrying = await ctx.db
1008
+ .query("webhookDeliveries")
1009
+ .withIndex("by_status", (q) => q.eq("status", "retrying"))
1010
+ .filter((q) => q.or(q.eq(q.field("nextRetryAt"), undefined), q.lte(q.field("nextRetryAt"), now)))
1011
+ .take(batchSize);
1012
+ const allDeliveries = [...pending, ...retrying].slice(0, batchSize);
1013
+ const hasMoreDeliveries = pending.length >= batchSize || retrying.length >= batchSize;
1014
+ for (const delivery of allDeliveries) {
1015
+ await ctx.scheduler.runAfter(0, internal.webhookTrigger.sendWebhookDelivery, { deliveryId: delivery._id });
1016
+ }
1017
+ return {
1018
+ eventsProcessed: eventsToProcess.length,
1019
+ deliveriesCreated,
1020
+ deliveriesTriggered: allDeliveries.length,
1021
+ hasMore: hasMoreEvents || hasMoreDeliveries,
1022
+ };
1023
+ },
1024
+ });
1025
+ /**
1026
+ * Schedule the next webhook processing run.
1027
+ *
1028
+ * Call this to set up recurring background processing.
1029
+ *
1030
+ * @param delayMs - Delay before next run in milliseconds
1031
+ */
1032
+ export const scheduleNextWebhookRun = mutation({
1033
+ args: {
1034
+ delayMs: v.optional(v.number()),
1035
+ },
1036
+ returns: v.object({
1037
+ scheduledAt: v.number(),
1038
+ }),
1039
+ handler: async (ctx, args) => {
1040
+ const delayMs = args.delayMs ?? DEFAULT_WEBHOOK_CONFIG.pollingIntervalMs;
1041
+ const runAt = Date.now() + delayMs;
1042
+ await ctx.scheduler.runAt(runAt, internal.webhookTrigger.triggerWebhookCheck, {});
1043
+ return { scheduledAt: runAt };
1044
+ },
1045
+ });
1046
+ /**
1047
+ * Internal mutation triggered by scheduler to process webhooks.
1048
+ * This inlines the processWebhooks logic to avoid calling .handler() directly.
1049
+ */
1050
+ export const triggerWebhookCheck = internalMutation({
1051
+ args: {},
1052
+ handler: async (ctx) => {
1053
+ const batchSize = DEFAULT_WEBHOOK_CONFIG.batchSize;
1054
+ // Step 1: Process events and create deliveries
1055
+ const events = await ctx.db
1056
+ .query("cmsEvents")
1057
+ .withIndex("by_processed", (q) => q.eq("processed", false))
1058
+ .order("asc")
1059
+ .take(batchSize + 1);
1060
+ const hasMoreEvents = events.length > batchSize;
1061
+ const eventsToProcess = events.slice(0, batchSize);
1062
+ let deliveriesCreated = 0;
1063
+ for (const event of eventsToProcess) {
1064
+ const payload = event.payload;
1065
+ const contentTypeName = payload?.contentTypeName;
1066
+ const webhooks = await ctx.db
1067
+ .query("webhookConfigs")
1068
+ .withIndex("by_enabled", (q) => q.eq("enabled", true))
1069
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
1070
+ .collect();
1071
+ const matchingWebhooks = webhooks.filter((webhook) => {
1072
+ if (webhook.eventTypes.length > 0 &&
1073
+ !webhook.eventTypes.includes(event.eventType)) {
1074
+ return false;
1075
+ }
1076
+ if (webhook.resourceTypes &&
1077
+ webhook.resourceTypes.length > 0 &&
1078
+ !webhook.resourceTypes.includes(event.resourceType)) {
1079
+ return false;
1080
+ }
1081
+ if (event.resourceType === "contentEntry" &&
1082
+ webhook.contentTypes &&
1083
+ webhook.contentTypes.length > 0 &&
1084
+ contentTypeName &&
1085
+ !webhook.contentTypes.includes(contentTypeName)) {
1086
+ return false;
1087
+ }
1088
+ return true;
1089
+ });
1090
+ for (const webhook of matchingWebhooks) {
1091
+ const existing = await ctx.db
1092
+ .query("webhookDeliveries")
1093
+ .withIndex("by_event", (q) => q.eq("eventId", event._id))
1094
+ .filter((q) => q.eq(q.field("webhookId"), webhook._id))
1095
+ .first();
1096
+ if (!existing) {
1097
+ const webhookPayload = {
1098
+ deliveryId: "pending",
1099
+ eventType: event.eventType,
1100
+ resourceType: event.resourceType,
1101
+ resourceId: event.resourceId,
1102
+ action: event.action,
1103
+ data: event.payload,
1104
+ timestamp: new Date(event._creationTime).toISOString(),
1105
+ userId: event.userId,
1106
+ };
1107
+ const deliveryId = await ctx.db.insert("webhookDeliveries", {
1108
+ webhookId: webhook._id,
1109
+ eventId: event._id,
1110
+ eventType: event.eventType,
1111
+ status: "pending",
1112
+ attemptCount: 0,
1113
+ maxAttempts: webhook.maxRetries ?? DEFAULT_WEBHOOK_CONFIG.defaultMaxRetries,
1114
+ payload: webhookPayload,
1115
+ });
1116
+ const updatedPayload = { ...webhookPayload, deliveryId };
1117
+ await ctx.db.patch(deliveryId, { payload: updatedPayload });
1118
+ deliveriesCreated++;
1119
+ }
1120
+ }
1121
+ }
1122
+ // Step 2: Trigger pending deliveries
1123
+ const now = Date.now();
1124
+ const pending = await ctx.db
1125
+ .query("webhookDeliveries")
1126
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
1127
+ .take(batchSize);
1128
+ const retrying = await ctx.db
1129
+ .query("webhookDeliveries")
1130
+ .withIndex("by_status", (q) => q.eq("status", "retrying"))
1131
+ .filter((q) => q.or(q.eq(q.field("nextRetryAt"), undefined), q.lte(q.field("nextRetryAt"), now)))
1132
+ .take(batchSize);
1133
+ const allDeliveries = [...pending, ...retrying].slice(0, batchSize);
1134
+ const hasMoreDeliveries = pending.length >= batchSize || retrying.length >= batchSize;
1135
+ for (const delivery of allDeliveries) {
1136
+ await ctx.scheduler.runAfter(0, internal.webhookTrigger.sendWebhookDelivery, { deliveryId: delivery._id });
1137
+ }
1138
+ const result = {
1139
+ eventsProcessed: eventsToProcess.length,
1140
+ deliveriesCreated,
1141
+ deliveriesTriggered: allDeliveries.length,
1142
+ hasMore: hasMoreEvents || hasMoreDeliveries,
1143
+ };
1144
+ console.log(`Webhook processor: processed ${result.eventsProcessed} events, ` +
1145
+ `created ${result.deliveriesCreated} deliveries, ` +
1146
+ `triggered ${result.deliveriesTriggered} deliveries`);
1147
+ // If there's more work, reschedule sooner
1148
+ if (result.hasMore) {
1149
+ await ctx.scheduler.runAfter(5000, // 5 seconds
1150
+ internal.webhookTrigger.triggerWebhookCheck, {});
1151
+ }
1152
+ return result;
1153
+ },
1154
+ });
1155
+ // =============================================================================
1156
+ // Query Functions
1157
+ // =============================================================================
1158
+ /**
1159
+ * Get delivery statistics for a webhook.
1160
+ */
1161
+ export const getWebhookDeliveryStats = query({
1162
+ args: {
1163
+ webhookId: v.id("webhookConfigs"),
1164
+ since: v.optional(v.number()),
1165
+ },
1166
+ returns: v.object({
1167
+ total: v.number(),
1168
+ pending: v.number(),
1169
+ processing: v.number(),
1170
+ delivered: v.number(),
1171
+ failed: v.number(),
1172
+ retrying: v.number(),
1173
+ }),
1174
+ handler: async (ctx, args) => {
1175
+ const { webhookId, since } = args;
1176
+ const deliveries = await ctx.db
1177
+ .query("webhookDeliveries")
1178
+ .withIndex("by_webhook", (q) => q.eq("webhookId", webhookId))
1179
+ .filter((q) => since ? q.gte(q.field("_creationTime"), since) : q.eq(true, true))
1180
+ .collect();
1181
+ const stats = {
1182
+ total: deliveries.length,
1183
+ pending: 0,
1184
+ processing: 0,
1185
+ delivered: 0,
1186
+ failed: 0,
1187
+ retrying: 0,
1188
+ };
1189
+ for (const d of deliveries) {
1190
+ stats[d.status]++;
1191
+ }
1192
+ return stats;
1193
+ },
1194
+ });
1195
+ /**
1196
+ * List recent deliveries for a webhook.
1197
+ */
1198
+ export const listWebhookDeliveries = query({
1199
+ args: {
1200
+ webhookId: v.id("webhookConfigs"),
1201
+ status: v.optional(webhookDeliveryStatusValidator),
1202
+ limit: v.optional(v.number()),
1203
+ },
1204
+ returns: v.array(webhookDeliveryDoc),
1205
+ handler: async (ctx, args) => {
1206
+ const { webhookId, status, limit = 50 } = args;
1207
+ let query = ctx.db
1208
+ .query("webhookDeliveries")
1209
+ .withIndex("by_webhook", (q) => q.eq("webhookId", webhookId));
1210
+ if (status) {
1211
+ query = query.filter((q) => q.eq(q.field("status"), status));
1212
+ }
1213
+ return await query.order("desc").take(limit);
1214
+ },
1215
+ });
1216
+ /**
1217
+ * Get overall webhook statistics.
1218
+ */
1219
+ export const getWebhookStats = query({
1220
+ args: {},
1221
+ returns: v.object({
1222
+ totalWebhooks: v.number(),
1223
+ activeWebhooks: v.number(),
1224
+ pendingDeliveries: v.number(),
1225
+ retryingDeliveries: v.number(),
1226
+ deliveriesLast24h: v.number(),
1227
+ successRateLast24h: v.number(),
1228
+ }),
1229
+ handler: async (ctx) => {
1230
+ // Count webhooks
1231
+ const allWebhooks = await ctx.db
1232
+ .query("webhookConfigs")
1233
+ .filter((q) => q.eq(q.field("deletedAt"), undefined))
1234
+ .collect();
1235
+ const activeWebhooks = allWebhooks.filter((w) => w.enabled).length;
1236
+ // Count pending and retrying deliveries
1237
+ const pending = await ctx.db
1238
+ .query("webhookDeliveries")
1239
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
1240
+ .collect();
1241
+ const retrying = await ctx.db
1242
+ .query("webhookDeliveries")
1243
+ .withIndex("by_status", (q) => q.eq("status", "retrying"))
1244
+ .collect();
1245
+ // Get deliveries in last 24 hours
1246
+ const last24h = Date.now() - 24 * 60 * 60 * 1000;
1247
+ const recentDeliveries = await ctx.db
1248
+ .query("webhookDeliveries")
1249
+ .filter((q) => q.gte(q.field("_creationTime"), last24h))
1250
+ .collect();
1251
+ const successfulRecent = recentDeliveries.filter((d) => d.status === "delivered").length;
1252
+ const completedRecent = recentDeliveries.filter((d) => d.status === "delivered" || d.status === "failed").length;
1253
+ return {
1254
+ totalWebhooks: allWebhooks.length,
1255
+ activeWebhooks,
1256
+ pendingDeliveries: pending.length,
1257
+ retryingDeliveries: retrying.length,
1258
+ deliveriesLast24h: recentDeliveries.length,
1259
+ successRateLast24h: completedRecent > 0
1260
+ ? Math.round((successfulRecent / completedRecent) * 100)
1261
+ : 100,
1262
+ };
1263
+ },
1264
+ });
1265
+ /**
1266
+ * Get delivery details by ID.
1267
+ */
1268
+ export const getDelivery = query({
1269
+ args: {
1270
+ deliveryId: v.id("webhookDeliveries"),
1271
+ },
1272
+ returns: v.union(webhookDeliveryDoc, v.null()),
1273
+ handler: async (ctx, args) => {
1274
+ return await ctx.db.get(args.deliveryId);
1275
+ },
1276
+ });
1277
+ /**
1278
+ * Manually retry a failed delivery.
1279
+ */
1280
+ export const retryDelivery = mutation({
1281
+ args: {
1282
+ deliveryId: v.id("webhookDeliveries"),
1283
+ },
1284
+ returns: v.object({
1285
+ success: v.boolean(),
1286
+ message: v.string(),
1287
+ }),
1288
+ handler: async (ctx, args) => {
1289
+ const { deliveryId } = args;
1290
+ const delivery = await ctx.db.get(deliveryId);
1291
+ if (!delivery) {
1292
+ throw new Error(`Delivery not found: ${deliveryId}`);
1293
+ }
1294
+ if (delivery.status === "delivered") {
1295
+ return {
1296
+ success: false,
1297
+ message: "Delivery already succeeded",
1298
+ };
1299
+ }
1300
+ // Reset for retry
1301
+ await ctx.db.patch(deliveryId, {
1302
+ status: "pending",
1303
+ attemptCount: 0,
1304
+ lastError: undefined,
1305
+ nextRetryAt: undefined,
1306
+ });
1307
+ // Schedule immediate delivery
1308
+ await ctx.scheduler.runAfter(0, internal.webhookTrigger.sendWebhookDelivery, { deliveryId });
1309
+ return {
1310
+ success: true,
1311
+ message: "Delivery scheduled for retry",
1312
+ };
1313
+ },
1314
+ });
1315
+ /**
1316
+ * Clean up old delivery records.
1317
+ */
1318
+ export const cleanupOldDeliveries = mutation({
1319
+ args: {
1320
+ retentionDays: v.optional(v.number()),
1321
+ },
1322
+ returns: v.object({
1323
+ deletedCount: v.number(),
1324
+ }),
1325
+ handler: async (ctx, args) => {
1326
+ const { retentionDays = 30 } = args;
1327
+ const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
1328
+ let deletedCount = 0;
1329
+ // Get old successful deliveries
1330
+ const oldDeliveries = await ctx.db
1331
+ .query("webhookDeliveries")
1332
+ .withIndex("by_status", (q) => q.eq("status", "delivered"))
1333
+ .filter((q) => q.lt(q.field("_creationTime"), cutoffTime))
1334
+ .take(1000);
1335
+ for (const delivery of oldDeliveries) {
1336
+ await ctx.db.delete(delivery._id);
1337
+ deletedCount++;
1338
+ }
1339
+ return { deletedCount };
1340
+ },
1341
+ });
1342
+ // =============================================================================
1343
+ // Test Helpers
1344
+ // =============================================================================
1345
+ /**
1346
+ * Test a webhook by sending a test event.
1347
+ * Useful for verifying webhook configuration before enabling.
1348
+ */
1349
+ export const testWebhook = mutation({
1350
+ args: {
1351
+ webhookId: v.id("webhookConfigs"),
1352
+ },
1353
+ returns: v.object({
1354
+ success: v.boolean(),
1355
+ message: v.string(),
1356
+ deliveryId: v.optional(v.id("webhookDeliveries")),
1357
+ }),
1358
+ handler: async (ctx, args) => {
1359
+ const { webhookId } = args;
1360
+ const webhook = await ctx.db.get(webhookId);
1361
+ if (!webhook) {
1362
+ throw new Error(`Webhook not found: ${webhookId}`);
1363
+ }
1364
+ // Create a test event
1365
+ const testEventId = await ctx.db.insert("cmsEvents", {
1366
+ eventType: "test.webhook",
1367
+ resourceType: "contentEntry",
1368
+ resourceId: "test-resource",
1369
+ action: "created",
1370
+ payload: {
1371
+ test: true,
1372
+ message: "This is a test webhook delivery",
1373
+ timestamp: new Date().toISOString(),
1374
+ },
1375
+ userId: undefined,
1376
+ processed: true, // Mark as processed so it doesn't trigger other handlers
1377
+ });
1378
+ // Create test delivery
1379
+ const testPayload = {
1380
+ deliveryId: "test",
1381
+ eventType: "test.webhook",
1382
+ resourceType: "contentEntry",
1383
+ resourceId: "test-resource",
1384
+ action: "created",
1385
+ data: {
1386
+ test: true,
1387
+ message: "This is a test webhook delivery",
1388
+ },
1389
+ timestamp: new Date().toISOString(),
1390
+ };
1391
+ const deliveryId = await ctx.db.insert("webhookDeliveries", {
1392
+ webhookId,
1393
+ eventId: testEventId,
1394
+ eventType: "test.webhook",
1395
+ status: "pending",
1396
+ attemptCount: 0,
1397
+ maxAttempts: 1,
1398
+ payload: testPayload,
1399
+ });
1400
+ // Update with actual delivery ID
1401
+ await ctx.db.patch(deliveryId, {
1402
+ payload: { ...testPayload, deliveryId },
1403
+ });
1404
+ // Schedule immediate delivery
1405
+ await ctx.scheduler.runAfter(0, internal.webhookTrigger.sendWebhookDelivery, { deliveryId });
1406
+ return {
1407
+ success: true,
1408
+ message: `Test webhook scheduled. Check delivery ${deliveryId} for results.`,
1409
+ deliveryId,
1410
+ };
1411
+ },
1412
+ });
1413
+ //# sourceMappingURL=webhookTrigger.js.map