@digimuza/saas-core 0.0.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 (474) hide show
  1. package/CLAUDE.md +1165 -0
  2. package/LICENSE +21 -0
  3. package/README.md +234 -0
  4. package/dist/api-keys/index.d.ts +9 -0
  5. package/dist/api-keys/index.d.ts.map +1 -0
  6. package/dist/api-keys/index.js +37 -0
  7. package/dist/api-keys/index.js.map +1 -0
  8. package/dist/api-keys/keys.d.ts +55 -0
  9. package/dist/api-keys/keys.d.ts.map +1 -0
  10. package/dist/api-keys/keys.js +92 -0
  11. package/dist/api-keys/keys.js.map +1 -0
  12. package/dist/api-keys/service.d.ts +111 -0
  13. package/dist/api-keys/service.d.ts.map +1 -0
  14. package/dist/api-keys/service.js +210 -0
  15. package/dist/api-keys/service.js.map +1 -0
  16. package/dist/audit/index.d.ts +5 -0
  17. package/dist/audit/index.d.ts.map +1 -0
  18. package/dist/audit/index.js +44 -0
  19. package/dist/audit/index.js.map +1 -0
  20. package/dist/audit/service.d.ts +88 -0
  21. package/dist/audit/service.d.ts.map +1 -0
  22. package/dist/audit/service.js +128 -0
  23. package/dist/audit/service.js.map +1 -0
  24. package/dist/auth/client.d.ts +5 -0
  25. package/dist/auth/client.d.ts.map +1 -0
  26. package/dist/auth/client.js +26 -0
  27. package/dist/auth/client.js.map +1 -0
  28. package/dist/auth/cookies.d.ts +51 -0
  29. package/dist/auth/cookies.d.ts.map +1 -0
  30. package/dist/auth/cookies.js +58 -0
  31. package/dist/auth/cookies.js.map +1 -0
  32. package/dist/auth/index.d.ts +14 -0
  33. package/dist/auth/index.d.ts.map +1 -0
  34. package/dist/auth/index.js +46 -0
  35. package/dist/auth/index.js.map +1 -0
  36. package/dist/auth/service.d.ts +67 -0
  37. package/dist/auth/service.d.ts.map +1 -0
  38. package/dist/auth/service.js +275 -0
  39. package/dist/auth/service.js.map +1 -0
  40. package/dist/auth/types.d.ts +56 -0
  41. package/dist/auth/types.d.ts.map +1 -0
  42. package/dist/auth/types.js +2 -0
  43. package/dist/auth/types.js.map +1 -0
  44. package/dist/authorization/index.d.ts +4 -0
  45. package/dist/authorization/index.d.ts.map +1 -0
  46. package/dist/authorization/index.js +24 -0
  47. package/dist/authorization/index.js.map +1 -0
  48. package/dist/authorization/service.d.ts +92 -0
  49. package/dist/authorization/service.d.ts.map +1 -0
  50. package/dist/authorization/service.js +254 -0
  51. package/dist/authorization/service.js.map +1 -0
  52. package/dist/billing/index.d.ts +16 -0
  53. package/dist/billing/index.d.ts.map +1 -0
  54. package/dist/billing/index.js +47 -0
  55. package/dist/billing/index.js.map +1 -0
  56. package/dist/billing/provider.d.ts +114 -0
  57. package/dist/billing/provider.d.ts.map +1 -0
  58. package/dist/billing/provider.js +2 -0
  59. package/dist/billing/provider.js.map +1 -0
  60. package/dist/billing/revolut.d.ts +68 -0
  61. package/dist/billing/revolut.d.ts.map +1 -0
  62. package/dist/billing/revolut.js +181 -0
  63. package/dist/billing/revolut.js.map +1 -0
  64. package/dist/billing/service.d.ts +124 -0
  65. package/dist/billing/service.d.ts.map +1 -0
  66. package/dist/billing/service.js +470 -0
  67. package/dist/billing/service.js.map +1 -0
  68. package/dist/cli/commands/dev.d.ts +43 -0
  69. package/dist/cli/commands/dev.d.ts.map +1 -0
  70. package/dist/cli/commands/dev.js +212 -0
  71. package/dist/cli/commands/dev.js.map +1 -0
  72. package/dist/cli/commands/project.d.ts +12 -0
  73. package/dist/cli/commands/project.d.ts.map +1 -0
  74. package/dist/cli/commands/project.js +49 -0
  75. package/dist/cli/commands/project.js.map +1 -0
  76. package/dist/cli/docker.d.ts +50 -0
  77. package/dist/cli/docker.d.ts.map +1 -0
  78. package/dist/cli/docker.js +84 -0
  79. package/dist/cli/docker.js.map +1 -0
  80. package/dist/cli/index.d.ts +6 -0
  81. package/dist/cli/index.d.ts.map +1 -0
  82. package/dist/cli/index.js +198 -0
  83. package/dist/cli/index.js.map +1 -0
  84. package/dist/cli/output.d.ts +30 -0
  85. package/dist/cli/output.d.ts.map +1 -0
  86. package/dist/cli/output.js +66 -0
  87. package/dist/cli/output.js.map +1 -0
  88. package/dist/cli/templates.d.ts +52 -0
  89. package/dist/cli/templates.d.ts.map +1 -0
  90. package/dist/cli/templates.js +225 -0
  91. package/dist/cli/templates.js.map +1 -0
  92. package/dist/config/client.d.ts +60 -0
  93. package/dist/config/client.d.ts.map +1 -0
  94. package/dist/config/client.js +94 -0
  95. package/dist/config/client.js.map +1 -0
  96. package/dist/config/config-loader.d.ts +2 -0
  97. package/dist/config/config-loader.d.ts.map +1 -0
  98. package/dist/config/config-loader.js +2 -0
  99. package/dist/config/config-loader.js.map +1 -0
  100. package/dist/config/env.d.ts +24 -0
  101. package/dist/config/env.d.ts.map +1 -0
  102. package/dist/config/env.js +40 -0
  103. package/dist/config/env.js.map +1 -0
  104. package/dist/config/errors.d.ts +17 -0
  105. package/dist/config/errors.d.ts.map +1 -0
  106. package/dist/config/errors.js +26 -0
  107. package/dist/config/errors.js.map +1 -0
  108. package/dist/config/index.d.ts +72 -0
  109. package/dist/config/index.d.ts.map +1 -0
  110. package/dist/config/index.js +106 -0
  111. package/dist/config/index.js.map +1 -0
  112. package/dist/config/runtime.d.ts +15 -0
  113. package/dist/config/runtime.d.ts.map +1 -0
  114. package/dist/config/runtime.js +22 -0
  115. package/dist/config/runtime.js.map +1 -0
  116. package/dist/config/sections.d.ts +226 -0
  117. package/dist/config/sections.d.ts.map +1 -0
  118. package/dist/config/sections.js +141 -0
  119. package/dist/config/sections.js.map +1 -0
  120. package/dist/db/client.d.ts +43 -0
  121. package/dist/db/client.d.ts.map +1 -0
  122. package/dist/db/client.js +77 -0
  123. package/dist/db/client.js.map +1 -0
  124. package/dist/db/errors.d.ts +39 -0
  125. package/dist/db/errors.d.ts.map +1 -0
  126. package/dist/db/errors.js +67 -0
  127. package/dist/db/errors.js.map +1 -0
  128. package/dist/db/index.d.ts +10 -0
  129. package/dist/db/index.d.ts.map +1 -0
  130. package/dist/db/index.js +10 -0
  131. package/dist/db/index.js.map +1 -0
  132. package/dist/db/migrate.d.ts +21 -0
  133. package/dist/db/migrate.d.ts.map +1 -0
  134. package/dist/db/migrate.js +28 -0
  135. package/dist/db/migrate.js.map +1 -0
  136. package/dist/db/schema/_shared.d.ts +30 -0
  137. package/dist/db/schema/_shared.d.ts.map +1 -0
  138. package/dist/db/schema/_shared.js +36 -0
  139. package/dist/db/schema/_shared.js.map +1 -0
  140. package/dist/db/schema/apikeys.d.ts +263 -0
  141. package/dist/db/schema/apikeys.d.ts.map +1 -0
  142. package/dist/db/schema/apikeys.js +70 -0
  143. package/dist/db/schema/apikeys.js.map +1 -0
  144. package/dist/db/schema/authorization.d.ts +465 -0
  145. package/dist/db/schema/authorization.d.ts.map +1 -0
  146. package/dist/db/schema/authorization.js +134 -0
  147. package/dist/db/schema/authorization.js.map +1 -0
  148. package/dist/db/schema/billing.d.ts +1100 -0
  149. package/dist/db/schema/billing.d.ts.map +1 -0
  150. package/dist/db/schema/billing.js +223 -0
  151. package/dist/db/schema/billing.js.map +1 -0
  152. package/dist/db/schema/events.d.ts +161 -0
  153. package/dist/db/schema/events.d.ts.map +1 -0
  154. package/dist/db/schema/events.js +66 -0
  155. package/dist/db/schema/events.js.map +1 -0
  156. package/dist/db/schema/features.d.ts +243 -0
  157. package/dist/db/schema/features.d.ts.map +1 -0
  158. package/dist/db/schema/features.js +87 -0
  159. package/dist/db/schema/features.js.map +1 -0
  160. package/dist/db/schema/files.d.ts +253 -0
  161. package/dist/db/schema/files.d.ts.map +1 -0
  162. package/dist/db/schema/files.js +74 -0
  163. package/dist/db/schema/files.js.map +1 -0
  164. package/dist/db/schema/index.d.ts +38 -0
  165. package/dist/db/schema/index.d.ts.map +1 -0
  166. package/dist/db/schema/index.js +38 -0
  167. package/dist/db/schema/index.js.map +1 -0
  168. package/dist/db/schema/invitations.d.ts +214 -0
  169. package/dist/db/schema/invitations.d.ts.map +1 -0
  170. package/dist/db/schema/invitations.js +80 -0
  171. package/dist/db/schema/invitations.js.map +1 -0
  172. package/dist/db/schema/messaging.d.ts +442 -0
  173. package/dist/db/schema/messaging.d.ts.map +1 -0
  174. package/dist/db/schema/messaging.js +120 -0
  175. package/dist/db/schema/messaging.js.map +1 -0
  176. package/dist/db/schema/users.d.ts +246 -0
  177. package/dist/db/schema/users.d.ts.map +1 -0
  178. package/dist/db/schema/users.js +60 -0
  179. package/dist/db/schema/users.js.map +1 -0
  180. package/dist/db/schema/webhooks.d.ts +693 -0
  181. package/dist/db/schema/webhooks.d.ts.map +1 -0
  182. package/dist/db/schema/webhooks.js +147 -0
  183. package/dist/db/schema/webhooks.js.map +1 -0
  184. package/dist/db/schema/workspaces.d.ts +196 -0
  185. package/dist/db/schema/workspaces.d.ts.map +1 -0
  186. package/dist/db/schema/workspaces.js +57 -0
  187. package/dist/db/schema/workspaces.js.map +1 -0
  188. package/dist/db/types.d.ts +18 -0
  189. package/dist/db/types.d.ts.map +1 -0
  190. package/dist/db/types.js +2 -0
  191. package/dist/db/types.js.map +1 -0
  192. package/dist/email/adapters.d.ts +29 -0
  193. package/dist/email/adapters.d.ts.map +1 -0
  194. package/dist/email/adapters.js +60 -0
  195. package/dist/email/adapters.js.map +1 -0
  196. package/dist/email/index.d.ts +20 -0
  197. package/dist/email/index.d.ts.map +1 -0
  198. package/dist/email/index.js +79 -0
  199. package/dist/email/index.js.map +1 -0
  200. package/dist/email/provider.d.ts +47 -0
  201. package/dist/email/provider.d.ts.map +1 -0
  202. package/dist/email/provider.js +7 -0
  203. package/dist/email/provider.js.map +1 -0
  204. package/dist/email/resend.d.ts +22 -0
  205. package/dist/email/resend.d.ts.map +1 -0
  206. package/dist/email/resend.js +51 -0
  207. package/dist/email/resend.js.map +1 -0
  208. package/dist/email/service.d.ts +55 -0
  209. package/dist/email/service.d.ts.map +1 -0
  210. package/dist/email/service.js +87 -0
  211. package/dist/email/service.js.map +1 -0
  212. package/dist/email/templates.d.ts +81 -0
  213. package/dist/email/templates.d.ts.map +1 -0
  214. package/dist/email/templates.js +84 -0
  215. package/dist/email/templates.js.map +1 -0
  216. package/dist/entitlements/index.d.ts +10 -0
  217. package/dist/entitlements/index.d.ts.map +1 -0
  218. package/dist/entitlements/index.js +28 -0
  219. package/dist/entitlements/index.js.map +1 -0
  220. package/dist/entitlements/service.d.ts +83 -0
  221. package/dist/entitlements/service.d.ts.map +1 -0
  222. package/dist/entitlements/service.js +158 -0
  223. package/dist/entitlements/service.js.map +1 -0
  224. package/dist/errors.d.ts +85 -0
  225. package/dist/errors.d.ts.map +1 -0
  226. package/dist/errors.js +95 -0
  227. package/dist/errors.js.map +1 -0
  228. package/dist/events/core-events.d.ts +120 -0
  229. package/dist/events/core-events.d.ts.map +1 -0
  230. package/dist/events/core-events.js +109 -0
  231. package/dist/events/core-events.js.map +1 -0
  232. package/dist/events/index.d.ts +15 -0
  233. package/dist/events/index.d.ts.map +1 -0
  234. package/dist/events/index.js +58 -0
  235. package/dist/events/index.js.map +1 -0
  236. package/dist/events/publisher.d.ts +46 -0
  237. package/dist/events/publisher.d.ts.map +1 -0
  238. package/dist/events/publisher.js +54 -0
  239. package/dist/events/publisher.js.map +1 -0
  240. package/dist/events/registry.d.ts +87 -0
  241. package/dist/events/registry.d.ts.map +1 -0
  242. package/dist/events/registry.js +34 -0
  243. package/dist/events/registry.js.map +1 -0
  244. package/dist/events/service.d.ts +78 -0
  245. package/dist/events/service.d.ts.map +1 -0
  246. package/dist/events/service.js +182 -0
  247. package/dist/events/service.js.map +1 -0
  248. package/dist/features/bucket.d.ts +22 -0
  249. package/dist/features/bucket.d.ts.map +1 -0
  250. package/dist/features/bucket.js +42 -0
  251. package/dist/features/bucket.js.map +1 -0
  252. package/dist/features/index.d.ts +15 -0
  253. package/dist/features/index.d.ts.map +1 -0
  254. package/dist/features/index.js +78 -0
  255. package/dist/features/index.js.map +1 -0
  256. package/dist/features/registry.d.ts +51 -0
  257. package/dist/features/registry.d.ts.map +1 -0
  258. package/dist/features/registry.js +52 -0
  259. package/dist/features/registry.js.map +1 -0
  260. package/dist/features/service.d.ts +78 -0
  261. package/dist/features/service.d.ts.map +1 -0
  262. package/dist/features/service.js +207 -0
  263. package/dist/features/service.js.map +1 -0
  264. package/dist/files/index.d.ts +6 -0
  265. package/dist/files/index.d.ts.map +1 -0
  266. package/dist/files/index.js +64 -0
  267. package/dist/files/index.js.map +1 -0
  268. package/dist/files/service.d.ts +92 -0
  269. package/dist/files/service.d.ts.map +1 -0
  270. package/dist/files/service.js +303 -0
  271. package/dist/files/service.js.map +1 -0
  272. package/dist/index.d.ts +33 -0
  273. package/dist/index.d.ts.map +1 -0
  274. package/dist/index.js +33 -0
  275. package/dist/index.js.map +1 -0
  276. package/dist/jobs/backend.d.ts +23 -0
  277. package/dist/jobs/backend.d.ts.map +1 -0
  278. package/dist/jobs/backend.js +23 -0
  279. package/dist/jobs/backend.js.map +1 -0
  280. package/dist/jobs/index.d.ts +29 -0
  281. package/dist/jobs/index.d.ts.map +1 -0
  282. package/dist/jobs/index.js +114 -0
  283. package/dist/jobs/index.js.map +1 -0
  284. package/dist/jobs/service.d.ts +67 -0
  285. package/dist/jobs/service.d.ts.map +1 -0
  286. package/dist/jobs/service.js +139 -0
  287. package/dist/jobs/service.js.map +1 -0
  288. package/dist/jobs/types.d.ts +78 -0
  289. package/dist/jobs/types.d.ts.map +1 -0
  290. package/dist/jobs/types.js +4 -0
  291. package/dist/jobs/types.js.map +1 -0
  292. package/dist/logger/default.d.ts +7 -0
  293. package/dist/logger/default.d.ts.map +1 -0
  294. package/dist/logger/default.js +28 -0
  295. package/dist/logger/default.js.map +1 -0
  296. package/dist/logger/index.d.ts +24 -0
  297. package/dist/logger/index.d.ts.map +1 -0
  298. package/dist/logger/index.js +90 -0
  299. package/dist/logger/index.js.map +1 -0
  300. package/dist/logger/redact.d.ts +40 -0
  301. package/dist/logger/redact.d.ts.map +1 -0
  302. package/dist/logger/redact.js +178 -0
  303. package/dist/logger/redact.js.map +1 -0
  304. package/dist/logger/service.d.ts +14 -0
  305. package/dist/logger/service.d.ts.map +1 -0
  306. package/dist/logger/service.js +56 -0
  307. package/dist/logger/service.js.map +1 -0
  308. package/dist/logger/sinks.d.ts +37 -0
  309. package/dist/logger/sinks.d.ts.map +1 -0
  310. package/dist/logger/sinks.js +135 -0
  311. package/dist/logger/sinks.js.map +1 -0
  312. package/dist/logger/types.d.ts +47 -0
  313. package/dist/logger/types.d.ts.map +1 -0
  314. package/dist/logger/types.js +8 -0
  315. package/dist/logger/types.js.map +1 -0
  316. package/dist/notifications/index.d.ts +8 -0
  317. package/dist/notifications/index.d.ts.map +1 -0
  318. package/dist/notifications/index.js +72 -0
  319. package/dist/notifications/index.js.map +1 -0
  320. package/dist/notifications/service.d.ts +79 -0
  321. package/dist/notifications/service.d.ts.map +1 -0
  322. package/dist/notifications/service.js +164 -0
  323. package/dist/notifications/service.js.map +1 -0
  324. package/dist/realtime/channels.d.ts +37 -0
  325. package/dist/realtime/channels.d.ts.map +1 -0
  326. package/dist/realtime/channels.js +58 -0
  327. package/dist/realtime/channels.js.map +1 -0
  328. package/dist/realtime/index.d.ts +19 -0
  329. package/dist/realtime/index.d.ts.map +1 -0
  330. package/dist/realtime/index.js +118 -0
  331. package/dist/realtime/index.js.map +1 -0
  332. package/dist/realtime/postgres.d.ts +48 -0
  333. package/dist/realtime/postgres.d.ts.map +1 -0
  334. package/dist/realtime/postgres.js +151 -0
  335. package/dist/realtime/postgres.js.map +1 -0
  336. package/dist/realtime/service.d.ts +75 -0
  337. package/dist/realtime/service.d.ts.map +1 -0
  338. package/dist/realtime/service.js +103 -0
  339. package/dist/realtime/service.js.map +1 -0
  340. package/dist/realtime/sse.d.ts +55 -0
  341. package/dist/realtime/sse.d.ts.map +1 -0
  342. package/dist/realtime/sse.js +123 -0
  343. package/dist/realtime/sse.js.map +1 -0
  344. package/dist/realtime/transport.d.ts +52 -0
  345. package/dist/realtime/transport.d.ts.map +1 -0
  346. package/dist/realtime/transport.js +2 -0
  347. package/dist/realtime/transport.js.map +1 -0
  348. package/dist/storage/index.d.ts +18 -0
  349. package/dist/storage/index.d.ts.map +1 -0
  350. package/dist/storage/index.js +89 -0
  351. package/dist/storage/index.js.map +1 -0
  352. package/dist/storage/keys.d.ts +50 -0
  353. package/dist/storage/keys.d.ts.map +1 -0
  354. package/dist/storage/keys.js +120 -0
  355. package/dist/storage/keys.js.map +1 -0
  356. package/dist/storage/memory.d.ts +32 -0
  357. package/dist/storage/memory.d.ts.map +1 -0
  358. package/dist/storage/memory.js +84 -0
  359. package/dist/storage/memory.js.map +1 -0
  360. package/dist/storage/provider.d.ts +93 -0
  361. package/dist/storage/provider.d.ts.map +1 -0
  362. package/dist/storage/provider.js +2 -0
  363. package/dist/storage/provider.js.map +1 -0
  364. package/dist/storage/s3.d.ts +34 -0
  365. package/dist/storage/s3.d.ts.map +1 -0
  366. package/dist/storage/s3.js +175 -0
  367. package/dist/storage/s3.js.map +1 -0
  368. package/dist/storage/service.d.ts +86 -0
  369. package/dist/storage/service.d.ts.map +1 -0
  370. package/dist/storage/service.js +107 -0
  371. package/dist/storage/service.js.map +1 -0
  372. package/dist/storage/sigv4.d.ts +58 -0
  373. package/dist/storage/sigv4.d.ts.map +1 -0
  374. package/dist/storage/sigv4.js +114 -0
  375. package/dist/storage/sigv4.js.map +1 -0
  376. package/dist/streams/client.d.ts +91 -0
  377. package/dist/streams/client.d.ts.map +1 -0
  378. package/dist/streams/client.js +128 -0
  379. package/dist/streams/client.js.map +1 -0
  380. package/dist/streams/events.d.ts +68 -0
  381. package/dist/streams/events.d.ts.map +1 -0
  382. package/dist/streams/events.js +81 -0
  383. package/dist/streams/events.js.map +1 -0
  384. package/dist/streams/index.d.ts +47 -0
  385. package/dist/streams/index.d.ts.map +1 -0
  386. package/dist/streams/index.js +47 -0
  387. package/dist/streams/index.js.map +1 -0
  388. package/dist/streams/realtime.d.ts +27 -0
  389. package/dist/streams/realtime.d.ts.map +1 -0
  390. package/dist/streams/realtime.js +50 -0
  391. package/dist/streams/realtime.js.map +1 -0
  392. package/dist/streams/state.d.ts +26 -0
  393. package/dist/streams/state.d.ts.map +1 -0
  394. package/dist/streams/state.js +34 -0
  395. package/dist/streams/state.js.map +1 -0
  396. package/dist/users/index.d.ts +9 -0
  397. package/dist/users/index.d.ts.map +1 -0
  398. package/dist/users/index.js +23 -0
  399. package/dist/users/index.js.map +1 -0
  400. package/dist/users/service.d.ts +43 -0
  401. package/dist/users/service.d.ts.map +1 -0
  402. package/dist/users/service.js +113 -0
  403. package/dist/users/service.js.map +1 -0
  404. package/dist/webhooks/inbound.d.ts +70 -0
  405. package/dist/webhooks/inbound.d.ts.map +1 -0
  406. package/dist/webhooks/inbound.js +146 -0
  407. package/dist/webhooks/inbound.js.map +1 -0
  408. package/dist/webhooks/index.d.ts +12 -0
  409. package/dist/webhooks/index.d.ts.map +1 -0
  410. package/dist/webhooks/index.js +65 -0
  411. package/dist/webhooks/index.js.map +1 -0
  412. package/dist/webhooks/outgoing.d.ts +89 -0
  413. package/dist/webhooks/outgoing.d.ts.map +1 -0
  414. package/dist/webhooks/outgoing.js +375 -0
  415. package/dist/webhooks/outgoing.js.map +1 -0
  416. package/dist/webhooks/signature.d.ts +40 -0
  417. package/dist/webhooks/signature.d.ts.map +1 -0
  418. package/dist/webhooks/signature.js +77 -0
  419. package/dist/webhooks/signature.js.map +1 -0
  420. package/dist/workspaces/context.d.ts +30 -0
  421. package/dist/workspaces/context.d.ts.map +1 -0
  422. package/dist/workspaces/context.js +12 -0
  423. package/dist/workspaces/context.js.map +1 -0
  424. package/dist/workspaces/index.d.ts +5 -0
  425. package/dist/workspaces/index.d.ts.map +1 -0
  426. package/dist/workspaces/index.js +21 -0
  427. package/dist/workspaces/index.js.map +1 -0
  428. package/dist/workspaces/service.d.ts +126 -0
  429. package/dist/workspaces/service.d.ts.map +1 -0
  430. package/dist/workspaces/service.js +495 -0
  431. package/dist/workspaces/service.js.map +1 -0
  432. package/docs/README.md +71 -0
  433. package/docs/api-keys.md +184 -0
  434. package/docs/audit.md +198 -0
  435. package/docs/auth.md +196 -0
  436. package/docs/authorization.md +225 -0
  437. package/docs/billing.md +293 -0
  438. package/docs/cli.md +238 -0
  439. package/docs/config.md +163 -0
  440. package/docs/db.md +219 -0
  441. package/docs/email.md +222 -0
  442. package/docs/entitlements.md +194 -0
  443. package/docs/errors.md +158 -0
  444. package/docs/events.md +279 -0
  445. package/docs/feature-flags.md +237 -0
  446. package/docs/files.md +229 -0
  447. package/docs/jobs.md +288 -0
  448. package/docs/logger.md +269 -0
  449. package/docs/notifications.md +179 -0
  450. package/docs/publishing.md +128 -0
  451. package/docs/realtime.md +360 -0
  452. package/docs/storage.md +279 -0
  453. package/docs/streams.md +296 -0
  454. package/docs/users.md +117 -0
  455. package/docs/webhooks.md +272 -0
  456. package/docs/workspaces.md +269 -0
  457. package/drizzle/0000_init_users.sql +21 -0
  458. package/drizzle/0001_workspaces_roles_permissions.sql +86 -0
  459. package/drizzle/0002_billing_entitlements.sql +97 -0
  460. package/drizzle/0003_events.sql +17 -0
  461. package/drizzle/0004_notifications_audit.sql +64 -0
  462. package/drizzle/0005_api_keys_webhooks.sql +77 -0
  463. package/drizzle/0006_feature_flags.sql +22 -0
  464. package/drizzle/0007_files.sql +23 -0
  465. package/drizzle/meta/0000_snapshot.json +160 -0
  466. package/drizzle/meta/0001_snapshot.json +837 -0
  467. package/drizzle/meta/0002_snapshot.json +1580 -0
  468. package/drizzle/meta/0003_snapshot.json +1751 -0
  469. package/drizzle/meta/0004_snapshot.json +2109 -0
  470. package/drizzle/meta/0005_snapshot.json +2719 -0
  471. package/drizzle/meta/0006_snapshot.json +2909 -0
  472. package/drizzle/meta/0007_snapshot.json +3113 -0
  473. package/drizzle/meta/_journal.json +62 -0
  474. package/package.json +180 -0
@@ -0,0 +1,296 @@
1
+ # Streams (RxJS)
2
+
3
+ ```ts
4
+ import { fromEvents, ofType, inWorkspace, latestByKey } from "@digimuza/saas-core/streams";
5
+ import { fromEventStream } from "@digimuza/saas-core/streams/client";
6
+ ```
7
+
8
+ > Orientation and recipes: [`src/streams/README.md`](../src/streams/README.md).
9
+
10
+ RxJS is a first-class dependency, used for **application-level reactive
11
+ behaviour** — and nothing else:
12
+
13
+ ```
14
+ event sources → application events → Observable → reactor → application/client
15
+ ```
16
+
17
+ `rxjs` is an **optional peer dependency**. Install it only if you use this module.
18
+
19
+ ---
20
+
21
+ ## What this module deliberately is not
22
+
23
+ | Not | Because |
24
+ | --- | --- |
25
+ | the database | A one-shot read stays a promise. Wrapping it adds a lifecycle to manage for no benefit. |
26
+ | the durable job system | A crash loses a reaction, not an event. Durable work belongs in [jobs.md](./jobs.md). |
27
+ | the realtime transport | That is [realtime.md](./realtime.md); this consumes it. |
28
+ | persistence | State that must survive a restart belongs in PostgreSQL, never in an operator's accumulator. |
29
+
30
+ The rule throughout the package: **promises for one-time work, Observables for
31
+ sequences over time.**
32
+
33
+ ---
34
+
35
+ ## Server-side
36
+
37
+ ### `fromEvents(events)`
38
+
39
+ ```ts
40
+ function fromEvents<S extends EventSchemaMap>(events: EventsService<S>): Observable<SaasEvent<S>>
41
+ ```
42
+
43
+ An Observable of application events, from the in-process dispatcher.
44
+
45
+ ```ts
46
+ const subscription = fromEvents(events)
47
+ .pipe(
48
+ inWorkspace(context.workspaceId),
49
+ ofType("workspace.member.created"),
50
+ debounceTime(500),
51
+ takeUntil(shutdown$),
52
+ )
53
+ .subscribe(reindexWorkspace);
54
+ ```
55
+
56
+ **Lifecycle.** The events handler is registered on subscribe and removed on
57
+ unsubscribe. Nothing exists before the first subscriber and nothing lingers after
58
+ the last — so forgetting `takeUntil` leaks one handler, not a growing set. Each
59
+ subscriber gets its own handler; one leaving does not silence another.
60
+
61
+ **It never completes on its own.** Bound it with `takeUntil` or `take(n)`.
62
+
63
+ ### `fromRealtime(realtime, options)`
64
+
65
+ ```ts
66
+ function fromRealtime(realtime: RealtimeService, options: {
67
+ userId: string;
68
+ channels: readonly string[];
69
+ }): Observable<RealtimeMessage>
70
+ ```
71
+
72
+ Authorized realtime messages, for a *server* reacting to delivery. A browser
73
+ should use `fromEventStream` instead.
74
+
75
+ Authorization runs once, on subscribe. Because that check is asynchronous and the
76
+ Observable constructor is not, a subscriber that unsubscribes mid-check is
77
+ honoured — the transport subscription closes immediately rather than sitting open
78
+ with nobody listening.
79
+
80
+ ---
81
+
82
+ ## Operators
83
+
84
+ Five helpers. Everything else is plain RxJS, because re-exporting existing
85
+ operators under new names adds vocabulary without adding behaviour.
86
+
87
+ ### `ofType(...types)`
88
+
89
+ ```ts
90
+ function ofType<S, const K extends readonly EventTypeOf<S>[]>(
91
+ ...types: K
92
+ ): OperatorFunction<SaasEvent<S>, SaasEventOf<S, K[number]>>
93
+ ```
94
+
95
+ Narrows in the **type system** as well as at runtime:
96
+
97
+ ```ts
98
+ fromEvents(events).pipe(
99
+ ofType("project.created", "project.deleted"),
100
+ map((event) => event.data.projectId), // narrowed to those two payloads
101
+ );
102
+ ```
103
+
104
+ A plain `filter((e) => e.type === "project.created")` filters correctly but leaves
105
+ `data` as the union of every payload in the registry, so every downstream operator
106
+ needs a cast.
107
+
108
+ ### `inWorkspace(workspaceId)` / `byActor(actorId)`
109
+
110
+ ```ts
111
+ function inWorkspace<T extends { workspaceId: string | null }>(id: string): MonoTypeOperatorFunction<T>
112
+ function byActor<T extends { actorId: string | null }>(id: string): MonoTypeOperatorFunction<T>
113
+ ```
114
+
115
+ The in-process dispatcher is process-wide, so a server stream sees every
116
+ workspace's events. `inWorkspace` is what makes a per-tenant stream out of it.
117
+
118
+ **Not an authorization boundary.** The caller must already hold a verified
119
+ `WorkspaceContext` — see [workspaces.md](./workspaces.md#workspacecontext).
120
+
121
+ ### `toData()`
122
+
123
+ ```ts
124
+ function toData<E extends { data: unknown }>(): OperatorFunction<E, E["data"]>
125
+ ```
126
+
127
+ Projects each event to its payload. The type parameter is inferred from the
128
+ *source*; declaring it over the registry and event key would leave both unbound at
129
+ the call site and collapse `data` to `unknown`.
130
+
131
+ ### `latestByKey(source, keyOf)`
132
+
133
+ ```ts
134
+ function latestByKey<T>(source: Observable<T>, keyOf: (value: T) => string):
135
+ Observable<ReadonlyMap<string, T>>
136
+ ```
137
+
138
+ "Newest value per key" — reactive state.
139
+
140
+ ```ts
141
+ const progress$ = latestByKey(jobEvents$, (event) => event.data.runId);
142
+ ```
143
+
144
+ The one state shape worth wrapping, because the sharing is easy to miss: without
145
+ `shareReplay`, every new subscriber re-runs the scan from an empty map and sees
146
+ nothing until the next event.
147
+
148
+ Each emission is a **new** Map, so a subscriber holding an earlier value is not
149
+ mutated underneath it.
150
+
151
+ `refCount: true` discards the accumulated map when the last subscriber leaves.
152
+ Deliberate — a cache outliving its subscribers is a leak, and this is a *view* of
153
+ durable data.
154
+
155
+ ---
156
+
157
+ ## Browser: `fromEventStream(options)`
158
+
159
+ ```ts
160
+ function fromEventStream(options: {
161
+ url: string;
162
+ channels: readonly string[];
163
+ types: readonly string[];
164
+ createSource?: (url: string) => EventSourceLike;
165
+ initialRetryDelayMs?: number; // 1_000
166
+ maxRetryDelayMs?: number; // 30_000
167
+ onError?: (error: unknown) => void;
168
+ setTimeoutImpl?: (handler: () => void, ms: number) => unknown;
169
+ clearTimeoutImpl?: (handle: unknown) => void;
170
+ }): Observable<ClientRealtimeMessage>
171
+ ```
172
+
173
+ ```ts
174
+ const events$ = fromEventStream({
175
+ url: "/api/realtime",
176
+ channels: [`workspace:${workspaceId}`],
177
+ types: ["job.progress", "project.created"],
178
+ });
179
+
180
+ const subscription = events$
181
+ .pipe(
182
+ filter((message) => message.type === "job.progress"),
183
+ map((message) => message.data as { percent?: number }),
184
+ distinctUntilChanged((a, b) => a.percent === b.percent),
185
+ )
186
+ .subscribe(render);
187
+
188
+ return () => subscription.unsubscribe(); // React effect cleanup
189
+ ```
190
+
191
+ **`types` is required.** SSE dispatches by event name and the server names each
192
+ frame after the event type, so listening for `message` alone receives nothing.
193
+
194
+ **One stream, shared.** Every component piping this beats each opening its own
195
+ `EventSource`: one connection, one reconnect policy, one ordering to reason about.
196
+
197
+ **No server imports.** This module pulls in nothing but RxJS — a test asserts it,
198
+ because a shared import would drag config and the database driver into the bundle.
199
+
200
+ ### Reconnect behaviour
201
+
202
+ Errors do **not** terminate the stream. A dropped connection is expected, not
203
+ exceptional: it is reported through `onError` and retried, because surfacing it as
204
+ an RxJS error would tear down every downstream operator and lose the subscriber's
205
+ accumulated state on a transient blip.
206
+
207
+ Backoff doubles from `initialRetryDelayMs`, capped at `maxRetryDelayMs`
208
+ (1s → 2s → 4s → 8s → … → 30s), and **resets once a frame arrives** — a message
209
+ proves the connection is healthy. Unbounded retries would hammer a server that is
210
+ already struggling.
211
+
212
+ A pending retry is cancelled on unsubscribe, so a torn-down component cannot
213
+ reconnect behind the caller's back.
214
+
215
+ Only one failure is fatal: being unable to construct a source at all (no
216
+ `EventSource` and no `createSource`). That is a configuration fault, not a blip.
217
+
218
+ ### Handle `truncated`
219
+
220
+ ```ts
221
+ source.addEventListener("project.created", (message) => {
222
+ const { eventId, data, truncated } = JSON.parse(message.data);
223
+ if (truncated) return refetchEvent(eventId);
224
+ applyUpdate(data);
225
+ });
226
+ ```
227
+
228
+ `pg_notify` caps payloads at 8000 bytes; oversized `data` is dropped and `eventId`
229
+ retained so the client can re-read. See
230
+ [realtime.md](./realtime.md#the-default-transport-postgresql-listennotify).
231
+
232
+ Frames are validated, not cast — this crossed the network.
233
+
234
+ ### `buildStreamUrl(url, channels)`
235
+
236
+ ```ts
237
+ function buildStreamUrl(url: string, channels: readonly string[]): string
238
+ ```
239
+
240
+ How `fromEventStream` builds its endpoint — each channel appended as a repeated
241
+ `channel` parameter. Exported because a caller sometimes needs the same URL: to
242
+ prefetch it, to pass it to a service worker, or to assert on it in a test.
243
+
244
+ ```ts
245
+ buildStreamUrl("/api/realtime", ["workspace:abc", "user:def"]);
246
+ // "/api/realtime?channel=workspace%3Aabc&channel=user%3Adef"
247
+ ```
248
+
249
+ A relative endpoint stays relative — browsers call relative endpoints, and forcing
250
+ an absolute URL would break that.
251
+
252
+ ---
253
+
254
+ ## Errors: the gotcha worth reading twice
255
+
256
+ **A throwing subscriber is invisible to the events error sink.** RxJS catches an
257
+ exception thrown in a `next` callback and reports it to its *own* global unhandled
258
+ handler. It never propagates back to the events service, so `onHandlerError` never
259
+ sees it. There is a test asserting exactly this so it stays documented.
260
+
261
+ ```ts
262
+ // Wrong: the failure vanishes.
263
+ stream.subscribe((event) => { throw new Error("boom"); });
264
+
265
+ // Right: handle it, or let the chain raise it.
266
+ stream.subscribe({
267
+ next: apply,
268
+ error: (error) => logger.error({ error }),
269
+ });
270
+ ```
271
+
272
+ Publishing is never affected either way — the event is already committed.
273
+
274
+ ---
275
+
276
+ ## Testing streams
277
+
278
+ Virtual time for anything time-based. Real timers make these tests slow and flaky,
279
+ and the point is the operator's semantics, not the clock's.
280
+
281
+ ```ts
282
+ import { TestScheduler } from "rxjs/testing";
283
+
284
+ new TestScheduler((actual, expected) => expect(actual).toEqual(expected)).run(
285
+ ({ cold, expectObservable, expectSubscriptions }) => {
286
+ const source = cold("a-b-c---|");
287
+ expectObservable(source.pipe(debounceTime(3))).toBe("-------c|");
288
+ // Assert the source is actually released, not just that values stop.
289
+ expectSubscriptions(source.subscriptions).toBe("^-------!");
290
+ },
291
+ );
292
+ ```
293
+
294
+ For the client stream, inject `createSource`, `setTimeoutImpl` and
295
+ `clearTimeoutImpl` — the core's own tests drive reconnect and assert the exact
296
+ backoff schedule with no waiting.
package/docs/users.md ADDED
@@ -0,0 +1,117 @@
1
+ # Users & profiles
2
+
3
+ ```ts
4
+ import { users } from "@digimuza/saas-core/users";
5
+ ```
6
+
7
+ > Orientation and recipes: [`src/users/README.md`](../src/users/README.md).
8
+
9
+
10
+ Identity and profile are separate tables but a single concern here, because callers
11
+ almost always want both.
12
+
13
+ - **`users`** — the link to WorkOS. No email, no credentials.
14
+ - **`user_profiles`** — email, name, avatar. Keyed by `user_id`.
15
+
16
+ Email lives on the profile because duplicating it on the identity row would create
17
+ two sources of truth that drift. Credentials are never stored: WorkOS owns
18
+ authentication.
19
+
20
+ ---
21
+
22
+ ## `users.getById(userId)`
23
+
24
+ ```ts
25
+ function getById(userId: string): Promise<UserWithProfile | undefined>
26
+ ```
27
+
28
+ ```ts
29
+ interface UserWithProfile {
30
+ id: string;
31
+ workosUserId: string;
32
+ createdAt: Date;
33
+ updatedAt: Date;
34
+ profile: UserProfile | null;
35
+ }
36
+ ```
37
+
38
+ The profile is nullable because the join is a left join — a user row can exist
39
+ without a profile if a sync was interrupted.
40
+
41
+ ## `users.requireById(userId)`
42
+
43
+ ```ts
44
+ function requireById(userId: string): Promise<UserWithProfile>
45
+ ```
46
+
47
+ Throws `NotFoundError`.
48
+
49
+ ## `users.getByWorkosId(workosUserId)`
50
+
51
+ ```ts
52
+ function getByWorkosId(workosUserId: string): Promise<UserWithProfile | undefined>
53
+ ```
54
+
55
+ Used by the auth module to map a session back to a local user.
56
+
57
+ ## `users.syncFromWorkos(attributes)`
58
+
59
+ ```ts
60
+ function syncFromWorkos(attributes: {
61
+ workosUserId: string;
62
+ email: string;
63
+ firstName?: string | null;
64
+ lastName?: string | null;
65
+ profilePictureUrl?: string | null;
66
+ }): Promise<UserWithProfile>
67
+ ```
68
+
69
+ Creates or updates the local record for a WorkOS identity. Called on every
70
+ successful authentication, so it must be idempotent and concurrency-safe — and is:
71
+
72
+ - Uses `onConflictDoUpdate` rather than select-then-insert. Two concurrent
73
+ sign-ins for the same identity would otherwise race and one would fail on the
74
+ unique index. There is a test firing three concurrent syncs.
75
+ - Tracks email changes from the provider.
76
+ - **Never overwrites `display_name`.** The user may have set it themselves, and
77
+ the identity provider must not clobber it. Also tested.
78
+
79
+ Emits `user.created` once per identity, keyed on the WorkOS id — a re-login is not
80
+ a new user. See [events.md](./events.md).
81
+
82
+ ## `users.updateProfile(userId, update)`
83
+
84
+ ```ts
85
+ function updateProfile(userId: string, update: {
86
+ firstName?: string | null;
87
+ lastName?: string | null;
88
+ displayName?: string | null;
89
+ avatarUrl?: string | null;
90
+ }): Promise<UserProfile>
91
+ ```
92
+
93
+ Throws `NotFoundError` if there is no profile row.
94
+
95
+ ---
96
+
97
+ ## Email is indexed, not unique
98
+
99
+ `user_profiles.email` has a **case-insensitive, non-unique** index. WorkOS owns
100
+ identity uniqueness, and its account-linking flows can transiently present two
101
+ identities sharing an email. A unique constraint here would turn legitimate
102
+ provider state into a failed write inside the core.
103
+
104
+ If your product needs one email to mean one account, enforce it in WorkOS, or add
105
+ a unique index in your own migration having decided how to handle linking.
106
+
107
+ ---
108
+
109
+ ## Custom construction
110
+
111
+ ```ts
112
+ import { createUsersService } from "@digimuza/saas-core/users";
113
+
114
+ const users = createUsersService({ db, events }); // events optional
115
+ ```
116
+
117
+ Without `events`, nothing is emitted and behaviour is otherwise identical.
@@ -0,0 +1,272 @@
1
+ # Webhooks
2
+
3
+ ```ts
4
+ import {
5
+ webhooks, signWebhookPayload, verifyWebhookSignature,
6
+ } from "@digimuza/saas-core/webhooks";
7
+ ```
8
+
9
+ > Orientation and recipes: [`src/webhooks/README.md`](../src/webhooks/README.md).
10
+
11
+ Both directions, sharing one signature scheme so there is a single implementation
12
+ to audit.
13
+
14
+ ---
15
+
16
+ ## Signing
17
+
18
+ HMAC-SHA256 over `v1.{timestampMs}.{rawBody}`, presented as `v1=<hex>`. The same
19
+ scheme the core already verifies from Revolut — which means receivers can follow
20
+ instructions Stripe and Revolut users already know.
21
+
22
+ ```ts
23
+ function signWebhookPayload(input: { rawBody: string; secret: string; timestampMs: number }): string
24
+
25
+ function verifyWebhookSignature(input: {
26
+ rawBody: string;
27
+ signature: string;
28
+ timestamp: string | number;
29
+ secret: string | readonly string[]; // several during rotation
30
+ toleranceSeconds?: number; // 300
31
+ now?: () => number;
32
+ }): boolean
33
+ ```
34
+
35
+ **Give it the body exactly as received.** Parsing and re-serialising JSON changes
36
+ key order and whitespace, which invalidates the signature.
37
+
38
+ Stale **and** future timestamps are rejected: a signature is valid forever, so
39
+ without a window a captured request could be replayed indefinitely — and a clock
40
+ skewed forward would widen that window rather than narrow it.
41
+
42
+ Comparison is constant-time, and comma-separated signatures are accepted so a
43
+ sender can present both secrets while rotating.
44
+
45
+ Headers the core sends: `x-saas-core-signature`, `x-saas-core-timestamp`, plus
46
+ `X-Saas-Core-Event-Id`, `-Event-Type`, `-Delivery-Id` and `-Attempt`.
47
+
48
+ ### `generateWebhookSecret()`
49
+
50
+ ```ts
51
+ function generateWebhookSecret(): string // "whsec_<43 base64url chars>"
52
+ ```
53
+
54
+ 256 bits, with a recognisable prefix so a leaked secret is identifiable in a log or
55
+ a paste. `endpoints.create` and `rotateSecret` call it for you; it is exported for
56
+ inbound endpoints you register with a sender yourself, where you need to generate
57
+ and store the shared secret on your own side.
58
+
59
+ `signingPayload(timestampMs, rawBody)` is exported too, if you need to reproduce
60
+ the exact signed string — for a receiver written in another language, say.
61
+
62
+ ---
63
+
64
+ ## Outgoing
65
+
66
+ ### Endpoints
67
+
68
+ ```ts
69
+ webhooks.outgoing.endpoints.create(context, {
70
+ url: string;
71
+ description?: string;
72
+ eventTypes?: readonly string[]; // empty means every type
73
+ }): Promise<{ endpoint: WebhookEndpoint; secret: string }>
74
+
75
+ webhooks.outgoing.endpoints.list(context): Promise<WebhookEndpoint[]>
76
+ webhooks.outgoing.endpoints.get(context, id): Promise<{ endpoint; secret }>
77
+ webhooks.outgoing.endpoints.update(context, id, patch): Promise<WebhookEndpoint>
78
+ webhooks.outgoing.endpoints.rotateSecret(context, id): Promise<{ endpoint; secret }>
79
+ webhooks.outgoing.endpoints.remove(context, id): Promise<void>
80
+ ```
81
+
82
+ The signing secret is **readable**, unlike an API key. It has to be: the core signs
83
+ with it and the receiver verifies with the same value, so a one-way hash would make
84
+ both impossible. Encryption at rest is the mitigation; `rotateSecret` handles a
85
+ leak without recreating the endpoint.
86
+
87
+ Re-enabling an endpoint clears its failure state — otherwise it would disable again
88
+ on the next failure regardless of what was fixed.
89
+
90
+ ### URL safety
91
+
92
+ HTTPS only. A signed payload over plain HTTP still exposes its contents, and the
93
+ signature does not stop an interceptor reading it.
94
+
95
+ Private and loopback addresses are refused: loopback, RFC1918, link-local
96
+ (`169.254.*`), `.internal`, `.local`. A tenant registering
97
+ `https://169.254.169.254/latest/meta-data` would otherwise have your server fetch
98
+ its own cloud credentials and post them back.
99
+
100
+ **This is a first pass, not a complete defence.** DNS can resolve a public name to
101
+ a private address at connect time. A production deployment should route webhook
102
+ egress through a proxy that enforces this too — which is why `WebhookSender` is
103
+ injectable.
104
+
105
+ ### `dispatch(event, tx?)`
106
+
107
+ ```ts
108
+ function dispatch(event: SaasEvent, tx?: CoreDatabase): Promise<WebhookDelivery[]>
109
+ ```
110
+
111
+ Queues a delivery per active, subscribed endpoint. **Records rows; sends nothing.**
112
+
113
+ Idempotent per `(endpoint, event)`: a retried job or a replayed event cannot
114
+ produce a second delivery. The payload is frozen at dispatch, so a later change to
115
+ the event cannot alter what was already queued.
116
+
117
+ Platform-wide events (null workspace) are skipped — there are no endpoints that
118
+ should receive them, and picking a workspace would leak across tenants.
119
+
120
+ ### Driving delivery
121
+
122
+ The core runs no loop. Retries must survive a deploy, so that is
123
+ [Trigger.dev's](./jobs.md) job:
124
+
125
+ ```ts
126
+ export const drainWebhooks = schedules.task({
127
+ id: "drain-webhooks",
128
+ cron: "* * * * *",
129
+ run: async () => {
130
+ for (const delivery of await webhooks.outgoing.claimDue({ limit: 100 })) {
131
+ await webhooks.outgoing.attempt(delivery);
132
+ }
133
+ },
134
+ });
135
+ ```
136
+
137
+ ```ts
138
+ function claimDue(options?: { limit?: number }): Promise<WebhookDelivery[]>
139
+ function attempt(delivery: WebhookDelivery): Promise<DeliveryAttemptResult>
140
+ function listDeliveries(context, options?): Promise<WebhookDelivery[]>
141
+ ```
142
+
143
+ ### Retries and failure
144
+
145
+ | Setting | Default |
146
+ | --- | --- |
147
+ | `maxAttempts` | 6 |
148
+ | `initialRetryDelayMs` | 30s, doubling |
149
+ | `maxRetryDelayMs` | 6h |
150
+ | `failureThreshold` | 20 consecutive failures |
151
+
152
+ A sender that *throws* is a failed attempt, not a crashed worker. An exhausted
153
+ delivery has `next_attempt_at` nulled, so it is never re-claimed.
154
+
155
+ An endpoint that fails `failureThreshold` times consecutively is set inactive and
156
+ stops receiving dispatches — it should not keep consuming retry budget. Any success
157
+ resets the streak.
158
+
159
+ ### `createFetchWebhookSender(options?)`
160
+
161
+ ```ts
162
+ createFetchWebhookSender({ timeoutMs?: 10_000, fetchImpl?: typeof fetch })
163
+ ```
164
+
165
+ `redirect: "manual"`, because a redirect could land somewhere the URL check
166
+ rejected. The abort signal matters: without a timeout one slow receiver holds a
167
+ worker slot indefinitely.
168
+
169
+ ---
170
+
171
+ ## Inbound
172
+
173
+ ```ts
174
+ function receive(input: {
175
+ source: string; // "github"
176
+ externalId: string; // the sender's event id
177
+ type: string;
178
+ payload: unknown;
179
+ workspaceId?: string | null;
180
+ }, tx?: CoreDatabase): Promise<ReceiveOutcome>
181
+
182
+ type ReceiveOutcome =
183
+ | { status: "claimed"; id: string }
184
+ | { status: "duplicate"; id: string | undefined };
185
+ ```
186
+
187
+ **The insert is the claim.** `(source, external_id)` is unique, so at-least-once
188
+ delivery and concurrent redeliveries are handled by PostgreSQL rather than by a
189
+ check-then-act two requests could both pass. A test fires three concurrent
190
+ receipts: one claims, two dedupe.
191
+
192
+ A retry re-claims an event whose previous attempt **failed**, but never one already
193
+ `processed` or currently `pending`. The raw payload is retained so a failed handler
194
+ can be replayed without asking the sender to redeliver.
195
+
196
+ `source` and `externalId` are required — without them there is nothing to
197
+ deduplicate on, and accepting the event would risk applying it repeatedly.
198
+
199
+ ### `process(input, handler)`
200
+
201
+ The shape most callers want — claim, handle, record:
202
+
203
+ ```ts
204
+ await webhooks.inbound.process(
205
+ { source: "github", externalId: eventId, type: eventType, payload },
206
+ async (webhook) => {
207
+ await handle(webhook);
208
+ // return "ignored" for something you deliberately do not handle
209
+ },
210
+ );
211
+ ```
212
+
213
+ Marks `processed`, or `ignored` if the handler says so, or `failed` **and rethrows**
214
+ so the sender sees a non-2xx and retries. The handler does not run for a duplicate.
215
+
216
+ ### Status meanings
217
+
218
+ | Status | Means |
219
+ | --- | --- |
220
+ | `pending` | claimed, being handled |
221
+ | `processed` | done |
222
+ | `ignored` | verified but not actionable — unknown tenant, unhandled type |
223
+ | `failed` | needs attention; re-claimable by a redelivery |
224
+
225
+ `ignored` and `failed` are distinct on purpose. Sharing one provider account across
226
+ environments makes unactionable events routine, and conflating them would bury real
227
+ failures.
228
+
229
+ ```ts
230
+ function markProcessed(id): Promise<void>
231
+ function markFailed(id, error): Promise<void>
232
+ function markIgnored(id): Promise<void>
233
+ function get(source, externalId): Promise<InboundWebhook | undefined>
234
+ function listFailed(options?): Promise<InboundWebhook[]>
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Relationship to billing webhooks
240
+
241
+ [Billing](./billing.md) has its own ingestion against `billing_events`, predating
242
+ this module and using the same claim-on-insert mechanism. It stays separate because
243
+ it also applies subscription state — this module is the general case for everything
244
+ else.
245
+
246
+ ---
247
+
248
+ ## Testing
249
+
250
+ ```ts
251
+ const outgoing = createOutgoingWebhooksService({
252
+ db,
253
+ sender: { async send(input) { calls.push(input); return { delivered: true, responseStatus: 200 }; } },
254
+ now: () => clock,
255
+ maxAttempts: 3,
256
+ initialRetryDelayMs: 1000,
257
+ });
258
+
259
+ const inbound = createInboundWebhooksService({ db, now: () => clock });
260
+ ```
261
+
262
+ `WebhookSender` is one method, so retries, backoff and endpoint disabling are all
263
+ testable with no network. Inject `now` and both retry scheduling and signature
264
+ freshness become deterministic.
265
+
266
+ The default `webhooks` export composes the two, bound to the configured database
267
+ and a `fetch`-based sender:
268
+
269
+ ```ts
270
+ webhooks.outgoing; // OutgoingWebhooksService
271
+ webhooks.inbound; // InboundWebhooksService
272
+ ```