@kelpie/server 0.5.2 → 0.7.0

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 (406) hide show
  1. package/README.md +5 -2
  2. package/dist/app.d.ts +13 -0
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +9 -0
  5. package/dist/app.js.map +1 -1
  6. package/dist/boot.d.ts +42 -0
  7. package/dist/boot.d.ts.map +1 -0
  8. package/dist/boot.js +57 -0
  9. package/dist/boot.js.map +1 -0
  10. package/dist/index.d.ts +20 -8
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/actor.d.ts +9 -1
  15. package/dist/lib/actor.d.ts.map +1 -1
  16. package/dist/lib/actor.js +13 -0
  17. package/dist/lib/actor.js.map +1 -1
  18. package/dist/lib/config.d.ts +51 -6
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +11 -5
  21. package/dist/lib/config.js.map +1 -1
  22. package/dist/lib/email.d.ts +15 -63
  23. package/dist/lib/email.d.ts.map +1 -1
  24. package/dist/lib/email.js +13 -65
  25. package/dist/lib/email.js.map +1 -1
  26. package/dist/lib/fromEnv.d.ts +60 -0
  27. package/dist/lib/fromEnv.d.ts.map +1 -0
  28. package/dist/lib/fromEnv.js +94 -0
  29. package/dist/lib/fromEnv.js.map +1 -0
  30. package/dist/lib/ids.d.ts +6 -0
  31. package/dist/lib/ids.d.ts.map +1 -1
  32. package/dist/lib/ids.js +6 -0
  33. package/dist/lib/ids.js.map +1 -1
  34. package/dist/lib/kelpieConfigFile.d.ts +129 -0
  35. package/dist/lib/kelpieConfigFile.d.ts.map +1 -0
  36. package/dist/lib/kelpieConfigFile.js +112 -0
  37. package/dist/lib/kelpieConfigFile.js.map +1 -0
  38. package/dist/lib/logger.d.ts +38 -7
  39. package/dist/lib/logger.d.ts.map +1 -1
  40. package/dist/lib/logger.js +58 -20
  41. package/dist/lib/logger.js.map +1 -1
  42. package/dist/modules/agent-tasks/index.d.ts.map +1 -1
  43. package/dist/modules/agent-tasks/index.js +4 -1
  44. package/dist/modules/agent-tasks/index.js.map +1 -1
  45. package/dist/modules/agent-tasks/prompt.d.ts +36 -6
  46. package/dist/modules/agent-tasks/prompt.d.ts.map +1 -1
  47. package/dist/modules/agent-tasks/prompt.js +33 -4
  48. package/dist/modules/agent-tasks/prompt.js.map +1 -1
  49. package/dist/modules/agent-tasks/service.d.ts.map +1 -1
  50. package/dist/modules/agent-tasks/service.js +3 -2
  51. package/dist/modules/agent-tasks/service.js.map +1 -1
  52. package/dist/modules/agent-tasks/wire.d.ts +11 -0
  53. package/dist/modules/agent-tasks/wire.d.ts.map +1 -1
  54. package/dist/modules/agent-tasks/wire.js +1 -0
  55. package/dist/modules/agent-tasks/wire.js.map +1 -1
  56. package/dist/modules/auth/index.d.ts.map +1 -1
  57. package/dist/modules/auth/index.js +8 -7
  58. package/dist/modules/auth/index.js.map +1 -1
  59. package/dist/modules/companies/events.d.ts +22 -0
  60. package/dist/modules/companies/events.d.ts.map +1 -0
  61. package/dist/modules/companies/events.js +8 -0
  62. package/dist/modules/companies/events.js.map +1 -0
  63. package/dist/modules/companies/index.d.ts.map +1 -1
  64. package/dist/modules/companies/index.js +2 -0
  65. package/dist/modules/companies/index.js.map +1 -1
  66. package/dist/modules/companies/service.d.ts +1 -0
  67. package/dist/modules/companies/service.d.ts.map +1 -1
  68. package/dist/modules/companies/service.js +8 -11
  69. package/dist/modules/companies/service.js.map +1 -1
  70. package/dist/modules/core.d.ts.map +1 -1
  71. package/dist/modules/core.js +4 -0
  72. package/dist/modules/core.js.map +1 -1
  73. package/dist/modules/deals/events.d.ts +31 -0
  74. package/dist/modules/deals/events.d.ts.map +1 -0
  75. package/dist/modules/deals/events.js +12 -0
  76. package/dist/modules/deals/events.js.map +1 -0
  77. package/dist/modules/deals/index.d.ts.map +1 -1
  78. package/dist/modules/deals/index.js +2 -0
  79. package/dist/modules/deals/index.js.map +1 -1
  80. package/dist/modules/deals/service.d.ts +1 -0
  81. package/dist/modules/deals/service.d.ts.map +1 -1
  82. package/dist/modules/deals/service.js +9 -18
  83. package/dist/modules/deals/service.js.map +1 -1
  84. package/dist/modules/decisions/events.d.ts +21 -0
  85. package/dist/modules/decisions/events.d.ts.map +1 -0
  86. package/dist/modules/decisions/events.js +11 -0
  87. package/dist/modules/decisions/events.js.map +1 -0
  88. package/dist/modules/decisions/index.d.ts.map +1 -1
  89. package/dist/modules/decisions/index.js +2 -0
  90. package/dist/modules/decisions/index.js.map +1 -1
  91. package/dist/modules/decisions/service.d.ts +1 -0
  92. package/dist/modules/decisions/service.d.ts.map +1 -1
  93. package/dist/modules/decisions/service.js +4 -7
  94. package/dist/modules/decisions/service.js.map +1 -1
  95. package/dist/modules/eventCatalogs.d.ts +35 -0
  96. package/dist/modules/eventCatalogs.d.ts.map +1 -0
  97. package/dist/modules/eventCatalogs.js +35 -0
  98. package/dist/modules/eventCatalogs.js.map +1 -0
  99. package/dist/modules/forms/events.d.ts +37 -0
  100. package/dist/modules/forms/events.d.ts.map +1 -0
  101. package/dist/modules/forms/events.js +15 -0
  102. package/dist/modules/forms/events.js.map +1 -0
  103. package/dist/modules/forms/index.d.ts.map +1 -1
  104. package/dist/modules/forms/index.js +2 -0
  105. package/dist/modules/forms/index.js.map +1 -1
  106. package/dist/modules/forms/service.d.ts +1 -0
  107. package/dist/modules/forms/service.d.ts.map +1 -1
  108. package/dist/modules/forms/service.js +8 -11
  109. package/dist/modules/forms/service.js.map +1 -1
  110. package/dist/modules/forms/submission.d.ts +5 -0
  111. package/dist/modules/forms/submission.d.ts.map +1 -1
  112. package/dist/modules/forms/submission.js +54 -28
  113. package/dist/modules/forms/submission.js.map +1 -1
  114. package/dist/modules/handbook/events.d.ts +28 -0
  115. package/dist/modules/handbook/events.d.ts.map +1 -0
  116. package/dist/modules/handbook/events.js +14 -0
  117. package/dist/modules/handbook/events.js.map +1 -0
  118. package/dist/modules/handbook/index.d.ts.map +1 -1
  119. package/dist/modules/handbook/index.js +2 -0
  120. package/dist/modules/handbook/index.js.map +1 -1
  121. package/dist/modules/handbook/service.d.ts +1 -0
  122. package/dist/modules/handbook/service.d.ts.map +1 -1
  123. package/dist/modules/handbook/service.js +8 -15
  124. package/dist/modules/handbook/service.js.map +1 -1
  125. package/dist/modules/hiring/candidates.d.ts +1 -0
  126. package/dist/modules/hiring/candidates.d.ts.map +1 -1
  127. package/dist/modules/hiring/candidates.js +8 -11
  128. package/dist/modules/hiring/candidates.js.map +1 -1
  129. package/dist/modules/hiring/events.d.ts +35 -0
  130. package/dist/modules/hiring/events.d.ts.map +1 -0
  131. package/dist/modules/hiring/events.js +11 -0
  132. package/dist/modules/hiring/events.js.map +1 -0
  133. package/dist/modules/hiring/index.d.ts.map +1 -1
  134. package/dist/modules/hiring/index.js +2 -0
  135. package/dist/modules/hiring/index.js.map +1 -1
  136. package/dist/modules/hiring/roles.d.ts +1 -0
  137. package/dist/modules/hiring/roles.d.ts.map +1 -1
  138. package/dist/modules/hiring/roles.js +8 -11
  139. package/dist/modules/hiring/roles.js.map +1 -1
  140. package/dist/modules/import-export/drafts.d.ts +9 -0
  141. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  142. package/dist/modules/import-export/drafts.js +25 -0
  143. package/dist/modules/import-export/drafts.js.map +1 -1
  144. package/dist/modules/import-export/events.d.ts +22 -0
  145. package/dist/modules/import-export/events.d.ts.map +1 -0
  146. package/dist/modules/import-export/events.js +12 -0
  147. package/dist/modules/import-export/events.js.map +1 -0
  148. package/dist/modules/import-export/exportRows.d.ts +9 -0
  149. package/dist/modules/import-export/exportRows.d.ts.map +1 -1
  150. package/dist/modules/import-export/exportRows.js +10 -1
  151. package/dist/modules/import-export/exportRows.js.map +1 -1
  152. package/dist/modules/import-export/index.d.ts.map +1 -1
  153. package/dist/modules/import-export/index.js +2 -0
  154. package/dist/modules/import-export/index.js.map +1 -1
  155. package/dist/modules/import-export/plan.d.ts +23 -1
  156. package/dist/modules/import-export/plan.d.ts.map +1 -1
  157. package/dist/modules/import-export/plan.js +62 -3
  158. package/dist/modules/import-export/plan.js.map +1 -1
  159. package/dist/modules/import-export/presets.d.ts.map +1 -1
  160. package/dist/modules/import-export/presets.js +25 -0
  161. package/dist/modules/import-export/presets.js.map +1 -1
  162. package/dist/modules/import-export/repository.d.ts +17 -0
  163. package/dist/modules/import-export/repository.d.ts.map +1 -1
  164. package/dist/modules/import-export/repository.js +21 -1
  165. package/dist/modules/import-export/repository.js.map +1 -1
  166. package/dist/modules/import-export/routes.d.ts.map +1 -1
  167. package/dist/modules/import-export/routes.js +5 -1
  168. package/dist/modules/import-export/routes.js.map +1 -1
  169. package/dist/modules/import-export/schema.d.ts +55 -0
  170. package/dist/modules/import-export/schema.d.ts.map +1 -1
  171. package/dist/modules/import-export/schema.js +13 -1
  172. package/dist/modules/import-export/schema.js.map +1 -1
  173. package/dist/modules/import-export/service.d.ts +9 -1
  174. package/dist/modules/import-export/service.d.ts.map +1 -1
  175. package/dist/modules/import-export/service.js +104 -17
  176. package/dist/modules/import-export/service.js.map +1 -1
  177. package/dist/modules/import-export/tools.d.ts.map +1 -1
  178. package/dist/modules/import-export/tools.js +7 -1
  179. package/dist/modules/import-export/tools.js.map +1 -1
  180. package/dist/modules/import-export/writes.d.ts +16 -0
  181. package/dist/modules/import-export/writes.d.ts.map +1 -1
  182. package/dist/modules/import-export/writes.js +77 -2
  183. package/dist/modules/import-export/writes.js.map +1 -1
  184. package/dist/modules/notes/events.d.ts +22 -0
  185. package/dist/modules/notes/events.d.ts.map +1 -0
  186. package/dist/modules/notes/events.js +12 -0
  187. package/dist/modules/notes/events.js.map +1 -0
  188. package/dist/modules/notes/index.d.ts.map +1 -1
  189. package/dist/modules/notes/index.js +2 -0
  190. package/dist/modules/notes/index.js.map +1 -1
  191. package/dist/modules/notes/service.d.ts +1 -0
  192. package/dist/modules/notes/service.d.ts.map +1 -1
  193. package/dist/modules/notes/service.js +4 -7
  194. package/dist/modules/notes/service.js.map +1 -1
  195. package/dist/modules/opportunities/events.d.ts +31 -0
  196. package/dist/modules/opportunities/events.d.ts.map +1 -0
  197. package/dist/modules/opportunities/events.js +12 -0
  198. package/dist/modules/opportunities/events.js.map +1 -0
  199. package/dist/modules/opportunities/index.d.ts.map +1 -1
  200. package/dist/modules/opportunities/index.js +2 -0
  201. package/dist/modules/opportunities/index.js.map +1 -1
  202. package/dist/modules/opportunities/service.d.ts +1 -0
  203. package/dist/modules/opportunities/service.d.ts.map +1 -1
  204. package/dist/modules/opportunities/service.js +9 -18
  205. package/dist/modules/opportunities/service.js.map +1 -1
  206. package/dist/modules/partnerships/events.d.ts +31 -0
  207. package/dist/modules/partnerships/events.d.ts.map +1 -0
  208. package/dist/modules/partnerships/events.js +12 -0
  209. package/dist/modules/partnerships/events.js.map +1 -0
  210. package/dist/modules/partnerships/index.d.ts.map +1 -1
  211. package/dist/modules/partnerships/index.js +2 -0
  212. package/dist/modules/partnerships/index.js.map +1 -1
  213. package/dist/modules/partnerships/service.d.ts +1 -0
  214. package/dist/modules/partnerships/service.d.ts.map +1 -1
  215. package/dist/modules/partnerships/service.js +9 -18
  216. package/dist/modules/partnerships/service.js.map +1 -1
  217. package/dist/modules/people/events.d.ts +28 -0
  218. package/dist/modules/people/events.d.ts.map +1 -0
  219. package/dist/modules/people/events.js +18 -0
  220. package/dist/modules/people/events.js.map +1 -0
  221. package/dist/modules/people/index.d.ts.map +1 -1
  222. package/dist/modules/people/index.js +2 -0
  223. package/dist/modules/people/index.js.map +1 -1
  224. package/dist/modules/people/service.d.ts +1 -0
  225. package/dist/modules/people/service.d.ts.map +1 -1
  226. package/dist/modules/people/service.js +8 -11
  227. package/dist/modules/people/service.js.map +1 -1
  228. package/dist/modules/pipelines/service.d.ts +4 -0
  229. package/dist/modules/pipelines/service.d.ts.map +1 -1
  230. package/dist/modules/pipelines/service.js +31 -8
  231. package/dist/modules/pipelines/service.js.map +1 -1
  232. package/dist/modules/plans/events.d.ts +21 -0
  233. package/dist/modules/plans/events.d.ts.map +1 -0
  234. package/dist/modules/plans/events.js +11 -0
  235. package/dist/modules/plans/events.js.map +1 -0
  236. package/dist/modules/plans/index.d.ts.map +1 -1
  237. package/dist/modules/plans/index.js +2 -0
  238. package/dist/modules/plans/index.js.map +1 -1
  239. package/dist/modules/plans/service.d.ts +1 -0
  240. package/dist/modules/plans/service.d.ts.map +1 -1
  241. package/dist/modules/plans/service.js +6 -14
  242. package/dist/modules/plans/service.js.map +1 -1
  243. package/dist/modules/positions/events.d.ts +22 -0
  244. package/dist/modules/positions/events.d.ts.map +1 -0
  245. package/dist/modules/positions/events.js +8 -0
  246. package/dist/modules/positions/events.js.map +1 -0
  247. package/dist/modules/positions/index.d.ts.map +1 -1
  248. package/dist/modules/positions/index.js +2 -0
  249. package/dist/modules/positions/index.js.map +1 -1
  250. package/dist/modules/positions/service.d.ts +1 -0
  251. package/dist/modules/positions/service.d.ts.map +1 -1
  252. package/dist/modules/positions/service.js +8 -11
  253. package/dist/modules/positions/service.js.map +1 -1
  254. package/dist/modules/raises/events.d.ts +31 -0
  255. package/dist/modules/raises/events.d.ts.map +1 -0
  256. package/dist/modules/raises/events.js +12 -0
  257. package/dist/modules/raises/events.js.map +1 -0
  258. package/dist/modules/raises/index.d.ts.map +1 -1
  259. package/dist/modules/raises/index.js +2 -0
  260. package/dist/modules/raises/index.js.map +1 -1
  261. package/dist/modules/raises/service.d.ts +1 -0
  262. package/dist/modules/raises/service.d.ts.map +1 -1
  263. package/dist/modules/raises/service.js +9 -18
  264. package/dist/modules/raises/service.js.map +1 -1
  265. package/dist/modules/reseal.d.ts +55 -0
  266. package/dist/modules/reseal.d.ts.map +1 -1
  267. package/dist/modules/reseal.js +90 -1
  268. package/dist/modules/reseal.js.map +1 -1
  269. package/dist/modules/smtp-email/index.d.ts +69 -0
  270. package/dist/modules/smtp-email/index.d.ts.map +1 -0
  271. package/dist/modules/smtp-email/index.js +82 -0
  272. package/dist/modules/smtp-email/index.js.map +1 -0
  273. package/dist/modules/webhooks/index.d.ts.map +1 -1
  274. package/dist/modules/webhooks/index.js +5 -1
  275. package/dist/modules/webhooks/index.js.map +1 -1
  276. package/dist/modules/webhooks/payloads.d.ts +22 -7
  277. package/dist/modules/webhooks/payloads.d.ts.map +1 -1
  278. package/dist/modules/webhooks/payloads.js +63 -44
  279. package/dist/modules/webhooks/payloads.js.map +1 -1
  280. package/dist/modules/workspace/events.d.ts +52 -0
  281. package/dist/modules/workspace/events.d.ts.map +1 -0
  282. package/dist/modules/workspace/events.js +16 -0
  283. package/dist/modules/workspace/events.js.map +1 -0
  284. package/dist/modules/workspace/index.d.ts.map +1 -1
  285. package/dist/modules/workspace/index.js +6 -2
  286. package/dist/modules/workspace/index.js.map +1 -1
  287. package/dist/modules/workspace/service.d.ts +1 -0
  288. package/dist/modules/workspace/service.d.ts.map +1 -1
  289. package/dist/modules/workspace/service.js +13 -15
  290. package/dist/modules/workspace/service.js.map +1 -1
  291. package/dist/runtime/events.d.ts +86 -108
  292. package/dist/runtime/events.d.ts.map +1 -1
  293. package/dist/runtime/events.js +123 -52
  294. package/dist/runtime/events.js.map +1 -1
  295. package/dist/runtime/module.d.ts +54 -2
  296. package/dist/runtime/module.d.ts.map +1 -1
  297. package/dist/runtime/registry.d.ts +23 -0
  298. package/dist/runtime/registry.d.ts.map +1 -1
  299. package/dist/runtime/registry.js +102 -2
  300. package/dist/runtime/registry.js.map +1 -1
  301. package/dist/runtime/transaction.d.ts +29 -4
  302. package/dist/runtime/transaction.d.ts.map +1 -1
  303. package/dist/runtime/transaction.js +48 -7
  304. package/dist/runtime/transaction.js.map +1 -1
  305. package/dist/testing/app.d.ts +5 -1
  306. package/dist/testing/app.d.ts.map +1 -1
  307. package/dist/testing/app.js +13 -3
  308. package/dist/testing/app.js.map +1 -1
  309. package/dist/testing/database.d.ts.map +1 -1
  310. package/dist/testing/database.js +6 -3
  311. package/dist/testing/database.js.map +1 -1
  312. package/dist/testing/services.d.ts +21 -1
  313. package/dist/testing/services.d.ts.map +1 -1
  314. package/dist/testing/services.js +20 -5
  315. package/dist/testing/services.js.map +1 -1
  316. package/migrations/0019_sharp_red_skull.sql +4 -0
  317. package/migrations/0020_early_nemesis.sql +2 -0
  318. package/migrations/meta/0019_snapshot.json +5138 -0
  319. package/migrations/meta/0020_snapshot.json +5138 -0
  320. package/migrations/meta/_journal.json +14 -0
  321. package/package.json +5 -2
  322. package/src/app.ts +26 -0
  323. package/src/boot.ts +89 -0
  324. package/src/index.ts +52 -9
  325. package/src/lib/actor.ts +15 -1
  326. package/src/lib/config.ts +65 -7
  327. package/src/lib/email.ts +22 -92
  328. package/src/lib/fromEnv.ts +143 -0
  329. package/src/lib/ids.ts +6 -0
  330. package/src/lib/kelpieConfigFile.ts +291 -0
  331. package/src/lib/logger.ts +92 -34
  332. package/src/modules/agent-tasks/index.ts +4 -1
  333. package/src/modules/agent-tasks/prompt.ts +57 -9
  334. package/src/modules/agent-tasks/service.ts +3 -2
  335. package/src/modules/agent-tasks/wire.ts +12 -0
  336. package/src/modules/auth/index.ts +11 -9
  337. package/src/modules/companies/events.ts +25 -0
  338. package/src/modules/companies/index.ts +2 -0
  339. package/src/modules/companies/service.ts +8 -11
  340. package/src/modules/core.ts +4 -0
  341. package/src/modules/deals/events.ts +34 -0
  342. package/src/modules/deals/index.ts +2 -0
  343. package/src/modules/deals/service.ts +17 -18
  344. package/src/modules/decisions/events.ts +24 -0
  345. package/src/modules/decisions/index.ts +2 -0
  346. package/src/modules/decisions/service.ts +8 -7
  347. package/src/modules/eventCatalogs.ts +35 -0
  348. package/src/modules/forms/events.ts +37 -0
  349. package/src/modules/forms/index.ts +2 -0
  350. package/src/modules/forms/service.ts +12 -11
  351. package/src/modules/forms/submission.ts +80 -29
  352. package/src/modules/handbook/events.ts +31 -0
  353. package/src/modules/handbook/index.ts +2 -0
  354. package/src/modules/handbook/service.ts +8 -15
  355. package/src/modules/hiring/candidates.ts +8 -11
  356. package/src/modules/hiring/events.ts +36 -0
  357. package/src/modules/hiring/index.ts +2 -0
  358. package/src/modules/hiring/roles.ts +8 -11
  359. package/src/modules/import-export/drafts.ts +27 -0
  360. package/src/modules/import-export/events.ts +25 -0
  361. package/src/modules/import-export/exportRows.ts +10 -1
  362. package/src/modules/import-export/index.ts +2 -0
  363. package/src/modules/import-export/plan.ts +107 -5
  364. package/src/modules/import-export/presets.ts +25 -0
  365. package/src/modules/import-export/repository.ts +35 -1
  366. package/src/modules/import-export/routes.ts +5 -0
  367. package/src/modules/import-export/schema.ts +13 -0
  368. package/src/modules/import-export/service.ts +153 -19
  369. package/src/modules/import-export/tools.ts +9 -0
  370. package/src/modules/import-export/writes.ts +116 -3
  371. package/src/modules/notes/events.ts +25 -0
  372. package/src/modules/notes/index.ts +2 -0
  373. package/src/modules/notes/service.ts +8 -7
  374. package/src/modules/opportunities/events.ts +34 -0
  375. package/src/modules/opportunities/index.ts +2 -0
  376. package/src/modules/opportunities/service.ts +17 -18
  377. package/src/modules/partnerships/events.ts +34 -0
  378. package/src/modules/partnerships/index.ts +2 -0
  379. package/src/modules/partnerships/service.ts +17 -18
  380. package/src/modules/people/events.ts +36 -0
  381. package/src/modules/people/index.ts +2 -0
  382. package/src/modules/people/service.ts +97 -87
  383. package/src/modules/pipelines/service.ts +47 -8
  384. package/src/modules/plans/events.ts +24 -0
  385. package/src/modules/plans/index.ts +2 -0
  386. package/src/modules/plans/service.ts +14 -14
  387. package/src/modules/positions/events.ts +25 -0
  388. package/src/modules/positions/index.ts +2 -0
  389. package/src/modules/positions/service.ts +8 -11
  390. package/src/modules/raises/events.ts +34 -0
  391. package/src/modules/raises/index.ts +2 -0
  392. package/src/modules/raises/service.ts +17 -18
  393. package/src/modules/reseal.ts +144 -1
  394. package/src/modules/smtp-email/index.ts +119 -0
  395. package/src/modules/webhooks/index.ts +5 -1
  396. package/src/modules/webhooks/payloads.ts +94 -48
  397. package/src/modules/workspace/events.ts +46 -0
  398. package/src/modules/workspace/index.ts +6 -2
  399. package/src/modules/workspace/service.ts +37 -15
  400. package/src/runtime/events.ts +268 -107
  401. package/src/runtime/module.ts +55 -2
  402. package/src/runtime/registry.ts +169 -1
  403. package/src/runtime/transaction.ts +94 -17
  404. package/src/testing/app.ts +18 -4
  405. package/src/testing/database.ts +6 -3
  406. package/src/testing/services.ts +34 -5
@@ -134,6 +134,20 @@
134
134
  "when": 1786656430307,
135
135
  "tag": "0018_hard_mystique",
136
136
  "breakpoints": true
137
+ },
138
+ {
139
+ "idx": 19,
140
+ "version": "7",
141
+ "when": 1787446086593,
142
+ "tag": "0019_sharp_red_skull",
143
+ "breakpoints": true
144
+ },
145
+ {
146
+ "idx": 20,
147
+ "version": "7",
148
+ "when": 1787447911296,
149
+ "tag": "0020_early_nemesis",
150
+ "breakpoints": true
137
151
  }
138
152
  ]
139
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kelpie/server",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "The Kelpie service as a library: module runtime, core CRM modules, REST API, MCP surface, and the shared migration pipeline.",
5
5
  "keywords": [
6
6
  "kelpie",
@@ -52,13 +52,16 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@hono/node-server": "^2.0.12",
55
- "@kelpie/schemas": "^0.5.2",
55
+ "@kelpie/schemas": "^0.7.0",
56
56
  "@node-rs/argon2": "^2.0.2",
57
57
  "drizzle-orm": "^0.45.2",
58
+ "emittery": "^2.0.0",
58
59
  "hono": "^4.12.32",
59
60
  "nodemailer": "^9.0.5",
60
61
  "postgres": "^3.4.9",
61
62
  "ulid": "^3.0.2",
63
+ "winston": "^3.19.0",
64
+ "winston-transport": "^4.9.0",
62
65
  "zod": "^4.4.3"
63
66
  },
64
67
  "devDependencies": {
package/src/app.ts CHANGED
@@ -2,6 +2,7 @@ import { Hono } from 'hono'
2
2
  import type { Context } from 'hono'
3
3
  import { cors } from 'hono/cors'
4
4
 
5
+ import type { RuntimeMode } from './lib/config.ts'
5
6
  import type { DatabaseProbe } from './lib/database.ts'
6
7
  import { AppError, internalErrorBody, toErrorBody } from './lib/errors.ts'
7
8
  import { PUBLIC_ROUTE_PREFIX } from './lib/http.ts'
@@ -51,6 +52,18 @@ export interface AppDependencies {
51
52
  * control (`testing/app.ts`).
52
53
  */
53
54
  readonly resolveClientIp: (context: Context) => string
55
+ /**
56
+ * Reported through `GET /v1/public/config` so the browser knows which
57
+ * runtime it is talking to. Defaults to `'production'` so a caller that has
58
+ * not thought about it fails safe: no banner shows.
59
+ */
60
+ readonly runtimeMode?: RuntimeMode
61
+ /**
62
+ * Reported through `GET /v1/public/config` so a non-production UI can name
63
+ * the site it is on. Undefined is fine and means "this deployment did not
64
+ * name itself".
65
+ */
66
+ readonly siteName?: string | undefined
54
67
  }
55
68
 
56
69
  /** Per-request values the middleware chain sets and handlers read. */
@@ -161,6 +174,19 @@ export function createApp(dependencies: AppDependencies): Hono<AppBindings> {
161
174
  app.route(PUBLIC_ROUTE_PREFIX, router)
162
175
  }
163
176
 
177
+ // Public deployment metadata the browser reads once at boot. Sits under the
178
+ // public prefix so the CORS and rate-limit layers above cover it, and takes
179
+ // no credentials for the same reason /healthz does not: the pages that read
180
+ // it include the sign-in page, which runs before any session exists. The
181
+ // runtime mode is already visible in error messages, and the site name
182
+ // exists to be visible; nothing here is sensitive.
183
+ const runtimeMode: RuntimeMode = dependencies.runtimeMode ?? 'production'
184
+ const siteName = dependencies.siteName ?? null
185
+
186
+ app.get(`${PUBLIC_ROUTE_PREFIX}/config`, (context) =>
187
+ context.json({ runtime_mode: runtimeMode, site_name: siteName }, 200),
188
+ )
189
+
164
190
  for (const { router } of dependencies.contributions.routers) {
165
191
  app.route('/v1', router)
166
192
  }
package/src/boot.ts ADDED
@@ -0,0 +1,89 @@
1
+ import type { Environment, KelpieConfig } from './lib/config.ts'
2
+ import { connectDatabase } from './lib/database.ts'
3
+ import type { DatabaseConnection } from './lib/database.ts'
4
+ import { createIdFactory } from './lib/ids.ts'
5
+ import type { IdFactory } from './lib/ids.ts'
6
+ import { resolveKelpieConfig } from './lib/kelpieConfigFile.ts'
7
+ import type { KelpieConfigInput } from './lib/kelpieConfigFile.ts'
8
+ import { createLogger, createTransportForDestination } from './lib/logger.ts'
9
+ import type { Logger } from './lib/logger.ts'
10
+ import { readModuleConfigFile } from './lib/moduleConfig.ts'
11
+ import { resolveActorFrom } from './modules/auth/credentials.ts'
12
+ import type { CredentialDependencies } from './modules/auth/credentials.ts'
13
+ import { createEventBus } from './runtime/events.ts'
14
+ import { registerModules } from './runtime/registry.ts'
15
+ import type { ModuleContributions } from './runtime/registry.ts'
16
+ import { createTransactionScope } from './runtime/transaction.ts'
17
+
18
+ /**
19
+ * A resolved assembly: config parsed, database connected, modules registered.
20
+ *
21
+ * This is the common prelude every entry point needs before it does its own
22
+ * work. `server.ts` goes on to build the app and serve; `migrate.ts` goes on to
23
+ * apply migrations and exit. Both start here, so the wiring lives in one place
24
+ * rather than drifting between two copies.
25
+ *
26
+ * The database is connected but not opened: postgres.js connects lazily, and
27
+ * registration issues no query, so a caller that only needs the contributions
28
+ * pays for no connection. The caller owns `database.close()`.
29
+ */
30
+ export interface AssemblyBoot {
31
+ readonly config: KelpieConfig
32
+ readonly logger: Logger
33
+ readonly database: DatabaseConnection
34
+ readonly createId: IdFactory
35
+ readonly credentials: CredentialDependencies
36
+ readonly contributions: ModuleContributions
37
+ }
38
+
39
+ /**
40
+ * Resolves an assembly's config, connects the database, and runs the module
41
+ * registration pass.
42
+ *
43
+ * Registration touches no database. It only collects what the modules
44
+ * contribute, migrations directories included, which is why an entry point can
45
+ * call this and then decide whether to serve, migrate, or both.
46
+ *
47
+ * @throws ConfigurationError when a required variable is missing or malformed.
48
+ * @throws ModuleConfigFileError when the module override file cannot be read.
49
+ * @throws ModuleBootError when the module list is invalid or a module fails to
50
+ * register. The caller maps these to a fix hint and an exit code.
51
+ */
52
+ export async function bootAssembly(
53
+ kelpieConfig: KelpieConfigInput,
54
+ environment: Environment,
55
+ ): Promise<AssemblyBoot> {
56
+ const config = resolveKelpieConfig(kelpieConfig, environment)
57
+ const logger = createLogger({
58
+ level: config.logging.level,
59
+ transports: config.logging.destinations.map(createTransportForDestination),
60
+ })
61
+ const database = connectDatabase(config.databaseUrl, logger)
62
+ const events = createEventBus(logger)
63
+ const createId = createIdFactory()
64
+ const credentials: CredentialDependencies = { db: database.db, now: () => new Date() }
65
+ const moduleConfig = readModuleConfigFile(config.moduleConfigPath)
66
+ const contributions = await registerModules({
67
+ modules: kelpieConfig.modules,
68
+ environment: config.env,
69
+ logger,
70
+ events,
71
+ moduleConfig,
72
+ resolveActor: (context) => resolveActorFrom(credentials, context),
73
+ services: {
74
+ db: database.db,
75
+ transaction: createTransactionScope({ db: database.db, bus: events, logger, createId }),
76
+ createId,
77
+ now: () => new Date(),
78
+ appBaseUrl: config.appBaseUrl,
79
+ secretEncryption: config.secretEncryption,
80
+ },
81
+ // `provider` picks a named sender from the runtime's registry. `'log'` is
82
+ // built in; `'smtp'` is registered by the built-in `smtp-email` core
83
+ // module; other names come from third-party provider modules listed in
84
+ // `kelpie.config.ts`. `from` is the address on every outgoing message.
85
+ email: { provider: config.email.EMAIL_PROVIDER, from: config.email.EMAIL_FROM },
86
+ })
87
+
88
+ return { config, logger, database, createId, credentials, contributions }
89
+ }
package/src/index.ts CHANGED
@@ -1,12 +1,35 @@
1
+ // Carries each module's `KelpieEventMap` augmentation into the emitted
2
+ // `index.d.ts` so consumers installed from npm get a typed `event.data`. See
3
+ // `modules/eventCatalogs.ts` for why the entry point has to reference them.
4
+ import './modules/eventCatalogs.ts'
5
+
1
6
  export { createApp } from './app.ts'
2
7
  export type { AppDependencies, AppBindings } from './app.ts'
3
8
 
9
+ export { bootAssembly } from './boot.ts'
10
+ export type { AssemblyBoot } from './boot.ts'
11
+
4
12
  export { WebBundleError, serveWebBundle } from './webBundle.ts'
5
13
  export type { WebBundleOptions } from './webBundle.ts'
6
14
 
7
15
  export { ConfigurationError, loadConfig } from './lib/config.ts'
8
16
  export type { Environment, KelpieConfig, LogLevel, RuntimeMode } from './lib/config.ts'
9
17
 
18
+ export { fromEnv, isFromEnvMarker, resolveMarker, resolveMarkers } from './lib/fromEnv.ts'
19
+ export type { ConfigValue, FromEnvMarker, FromEnvProblem, ResolveResult } from './lib/fromEnv.ts'
20
+
21
+ export { defineKelpieConfig, resolveKelpieConfig } from './lib/kelpieConfigFile.ts'
22
+ export type {
23
+ EmailInput,
24
+ KelpieConfigInput,
25
+ RateLimitBudgetInput,
26
+ RateLimitInput,
27
+ SecretEncryptionInput,
28
+ } from './lib/kelpieConfigFile.ts'
29
+
30
+ export { appUrlConfigSchema } from './lib/appUrl.ts'
31
+ export type { AppUrlConfig } from './lib/appUrl.ts'
32
+
10
33
  export { resolveClientIpFrom } from './lib/clientIp.ts'
11
34
 
12
35
  export { ModuleConfigFileError, readModuleConfigFile } from './lib/moduleConfig.ts'
@@ -23,9 +46,12 @@ export type { Database, DatabaseConnection, DatabaseProbe } from './lib/database
23
46
 
24
47
  export * as schema from './schema/index.ts'
25
48
 
26
- export { createEmailSender, createLogEmailSender, emailConfigSchema } from './lib/email.ts'
49
+ export { createLogEmailSender, emailConfigSchema } from './lib/email.ts'
27
50
  export type { EmailConfig, EmailMessage, EmailSender } from './lib/email.ts'
28
51
 
52
+ export { SMTP_EMAIL_PROVIDER, createSmtpEmailModule, createSmtpEmailSender } from './modules/smtp-email/index.ts'
53
+ export type { SmtpEmailConfig, SmtpEmailModuleOptions, SmtpTransport } from './modules/smtp-email/index.ts'
54
+
29
55
  export { MINIMUM_PASSWORD_LENGTH, hashPassword, isPasswordStrongEnough, verifyPassword } from './lib/passwords.ts'
30
56
  export { generateToken, hashToken, tokenHashesMatch } from './lib/tokens.ts'
31
57
 
@@ -49,8 +75,13 @@ export type { ErrorBody, ErrorCode, ErrorDetail, ErrorStatus, ValidationIssue }
49
75
  export { createIdFactory, idPrefixes } from './lib/ids.ts'
50
76
  export type { IdFactory, ObjectKind } from './lib/ids.ts'
51
77
 
52
- export { createLogger } from './lib/logger.ts'
53
- export type { LogFields, Logger, LogSink } from './lib/logger.ts'
78
+ export {
79
+ createCaptureTransport,
80
+ createLogger,
81
+ createStdoutTransport,
82
+ createTransportForDestination,
83
+ } from './lib/logger.ts'
84
+ export type { CreateLoggerOptions, LogFields, Logger, LoggingDestination } from './lib/logger.ts'
54
85
 
55
86
  export type {
56
87
  KelpieModule,
@@ -76,21 +107,33 @@ export type {
76
107
 
77
108
  export { SEATS_LIMIT } from './modules/workspace/capabilities.ts'
78
109
 
79
- export { DOMAIN_EVENT_NAMES, RECORD_OBJECT_TYPES, createEventBus } from './runtime/events.ts'
110
+ export {
111
+ RECORD_OBJECT_TYPES,
112
+ checkEventCycle,
113
+ createEventBus,
114
+ currentEventChain,
115
+ } from './runtime/events.ts'
80
116
  export type {
81
- DomainEventName,
82
- DomainEvents,
117
+ AnyEventHandler,
118
+ CycleGuardOutcome,
83
119
  EventBus,
120
+ EventBusOptions,
121
+ EventCatalog,
122
+ EventChainEntry,
84
123
  EventHandler,
124
+ EventName,
125
+ KelpieEventMap,
85
126
  RecordObjectType,
86
- StagedObjectType,
127
+ SubscribeOptions,
87
128
  } from './runtime/events.ts'
129
+ export type { ModuleEventCatalog } from './runtime/module.ts'
88
130
 
89
131
  export { createTransactionScope } from './runtime/transaction.ts'
90
132
  export type {
91
133
  BufferedEvents,
92
134
  Transaction,
93
135
  TransactionContext,
136
+ TransactionOptions,
94
137
  TransactionScope,
95
138
  TransactionScopeDependencies,
96
139
  } from './runtime/transaction.ts'
@@ -100,8 +143,8 @@ export type { MigrationPlanStep } from './runtime/migrate.ts'
100
143
 
101
144
  export { coreMigrationsDirectory, coreModules } from './modules/core.ts'
102
145
 
103
- export { resealStoredSecrets } from './modules/reseal.ts'
104
- export type { ResealColumnOutcome, ResealOutcome } from './modules/reseal.ts'
146
+ export { resealStoredSecrets, runReseal } from './modules/reseal.ts'
147
+ export type { ResealColumnOutcome, ResealOutcome, ResealPass, RunResealOptions } from './modules/reseal.ts'
105
148
 
106
149
  export { KEY_KINDS, kindOfSecret, mintKey, parseKeyKind, readBearerToken } from './modules/api-keys/keys.ts'
107
150
  export type { KeyKind, MintedKey } from './modules/api-keys/keys.ts'
package/src/lib/actor.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MemberRole } from '@kelpie/schemas'
1
+ import type { EventActor, MemberRole } from '@kelpie/schemas'
2
2
 
3
3
  import { AppError } from './errors.ts'
4
4
 
@@ -97,3 +97,17 @@ export function actorUserId(actor: Actor): string | null {
97
97
  export function actorMemberId(actor: Actor): string | null {
98
98
  return actor.memberId
99
99
  }
100
+
101
+ /**
102
+ * Maps the request actor to the actor stamped on domain events.
103
+ *
104
+ * A signed-in user becomes `user`. A workspace key (no user behind it) becomes
105
+ * `system`, because the event was caused by the workspace itself rather than
106
+ * any one person.
107
+ */
108
+ export function toEventActor(actor: Actor): EventActor {
109
+ if (actor.userId !== null) {
110
+ return { kind: 'user', id: actor.userId }
111
+ }
112
+ return { kind: 'system' }
113
+ }
package/src/lib/config.ts CHANGED
@@ -3,8 +3,14 @@ import { z } from 'zod'
3
3
  import { emailConfigSchema } from './email.ts'
4
4
  import type { EmailConfig } from './email.ts'
5
5
  import { describeValidationIssue } from './errors.ts'
6
+ // `EmailConfig` is `{ EMAIL_PROVIDER, EMAIL_FROM }`. `EMAIL_PROVIDER` names one
7
+ // of the registered senders; `'log'` is always available, other names come
8
+ // from provider modules. Provider-specific config (SMTP settings, an API key)
9
+ // belongs to the module that reads it through `context.config(...)`.
10
+ import type { LoggingDestination } from './logger.ts'
6
11
  import { rateLimitConfigFrom, rateLimitConfigSchema } from './rateLimit.ts'
7
12
  import type { RateLimitConfig } from './rateLimit.ts'
13
+ import type { SecretEncryptionConfig } from './secrets.ts'
8
14
 
9
15
  /**
10
16
  * The single place the service reads environment variables. Every other module
@@ -18,12 +24,36 @@ export type RuntimeMode = 'development' | 'test' | 'production'
18
24
 
19
25
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
20
26
 
27
+ /**
28
+ * Everything the logger needs to boot: the minimum severity to emit, and the
29
+ * ordered list of destinations each line is written to. Stdout is one such
30
+ * destination; adding a file or a hosted collector means adding an entry to
31
+ * `destinations` and extending `LoggingDestination` in `lib/logger.ts`.
32
+ */
33
+ export interface LoggingConfig {
34
+ readonly level: LogLevel
35
+ readonly destinations: readonly LoggingDestination[]
36
+ }
37
+
21
38
  export interface KelpieConfig {
22
39
  readonly runtimeMode: RuntimeMode
23
40
  readonly port: number
24
41
  readonly databaseUrl: string
25
- readonly logLevel: LogLevel
26
- /** Transactional mail only. Roadmap decision 4: configured, never hardcoded. */
42
+ /**
43
+ * Human-readable name of this deployment ("dev", "demo", "cloud", or
44
+ * whatever the self-hoster picks). Undefined is normal. Exposed to the
45
+ * browser through `GET /v1/public/config` so a non-production UI can name
46
+ * the site it is on. Ignored when `runtimeMode` is `production`.
47
+ */
48
+ readonly siteName: string | undefined
49
+ readonly logging: LoggingConfig
50
+ /**
51
+ * The provider name the module runtime looks up in its registry, and the
52
+ * from address every outgoing message uses. See `lib/email.ts` for the
53
+ * shape and rules; `'log'` is always available, `'smtp'` is registered by
54
+ * the built-in `smtp-email` core module, and other names come from
55
+ * third-party provider modules.
56
+ */
27
57
  readonly email: EmailConfig
28
58
  /**
29
59
  * Path to the deploy-time module override file (`lib/moduleConfig.ts`).
@@ -44,6 +74,28 @@ export interface KelpieConfig {
44
74
  * client IP is read from `X-Forwarded-For` (`lib/clientIp.ts`).
45
75
  */
46
76
  readonly trustedProxyHopCount: number
77
+ /**
78
+ * What modules see through `context.config(schema)`. `resolveKelpieConfig`
79
+ * produces this by merging any `env` section in `kelpie.config.ts` over
80
+ * `process.env`, so a key in the config file wins for that key and unset
81
+ * keys pass through. `loadConfig` sets it to the environment it was given,
82
+ * so an old caller keeps the same behaviour.
83
+ */
84
+ readonly env: Environment
85
+ /**
86
+ * The deployment's base URL, source of every emailed link. Optional so an
87
+ * older assembly that omits it from `kelpie.config.ts` still boots: modules
88
+ * that need it fall back to `context.config(appUrlConfigSchema)` when unset.
89
+ * A new assembly declares it as a first-class field.
90
+ */
91
+ readonly appBaseUrl: string | undefined
92
+ /**
93
+ * The key(s) that seal stored secrets. Optional for the same reason
94
+ * `appBaseUrl` is: modules that need it fall back to
95
+ * `context.config(secretEncryptionConfigSchema)`. The base assembly declares
96
+ * it, so both webhooks and agent-tasks receive it through `services`.
97
+ */
98
+ readonly secretEncryption: SecretEncryptionConfig | undefined
47
99
  }
48
100
 
49
101
  /** Thrown at boot when the environment cannot produce a valid configuration. */
@@ -74,6 +126,7 @@ const environmentSchema = z.object({
74
126
  .refine(isPostgresUrl, { message: 'must be a postgres:// or postgresql:// connection string' }),
75
127
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']),
76
128
  KELPIE_MODULE_CONFIG_PATH: z.string().min(1).optional(),
129
+ KELPIE_SITE_NAME: z.string().min(1).optional(),
77
130
  WEB_BUNDLE_DIR: z.string().min(1).optional(),
78
131
  TRUSTED_PROXY_HOP_COUNT: z.coerce.number().int().nonnegative().default(0),
79
132
  ...rateLimitConfigSchema.shape,
@@ -82,10 +135,8 @@ const environmentSchema = z.object({
82
135
  /**
83
136
  * Parses an environment into a validated config.
84
137
  *
85
- * `emailConfigSchema` is a discriminated union, so it is parsed separately
86
- * from the rest: a union has no flat `.shape` to spread into `environmentSchema`,
87
- * only its own `EMAIL_PROVIDER`-keyed branches. Problems from both parses are
88
- * combined into one error, preserving the "every missing variable at once" rule.
138
+ * The email variables are parsed separately so a caller sees every missing
139
+ * variable at once, rather than one at a time.
89
140
  *
90
141
  * @param environment Raw variables, normally `process.env`.
91
142
  * @throws ConfigurationError listing every invalid or missing variable.
@@ -107,11 +158,18 @@ export function loadConfig(environment: Environment): KelpieConfig {
107
158
  runtimeMode: environmentResult.data.NODE_ENV,
108
159
  port: environmentResult.data.PORT,
109
160
  databaseUrl: environmentResult.data.DATABASE_URL,
110
- logLevel: environmentResult.data.LOG_LEVEL,
161
+ logging: {
162
+ level: environmentResult.data.LOG_LEVEL,
163
+ destinations: [{ kind: 'stdout' }],
164
+ },
111
165
  email: emailResult.data,
112
166
  moduleConfigPath: environmentResult.data.KELPIE_MODULE_CONFIG_PATH,
167
+ siteName: environmentResult.data.KELPIE_SITE_NAME,
113
168
  webBundleDirectory: environmentResult.data.WEB_BUNDLE_DIR,
114
169
  rateLimit: rateLimitConfigFrom(environmentResult.data),
115
170
  trustedProxyHopCount: environmentResult.data.TRUSTED_PROXY_HOP_COUNT,
171
+ env: environment,
172
+ appBaseUrl: undefined,
173
+ secretEncryption: undefined,
116
174
  }
117
175
  }
package/src/lib/email.ts CHANGED
@@ -1,7 +1,5 @@
1
- import nodemailer from 'nodemailer'
2
1
  import { z } from 'zod'
3
2
 
4
- import { describeThrown } from './errors.ts'
5
3
  import type { Logger } from './logger.ts'
6
4
 
7
5
  /**
@@ -9,15 +7,15 @@ import type { Logger } from './logger.ts'
9
7
  * and account-change notifications, nothing else. Kelpie never sends outreach
10
8
  * email.
11
9
  *
12
- * Roadmap decision 4: the provider is configured, never hardcoded. Core ships
13
- * the port and two providers, `log` and `smtp`. Both need no vendor account a
14
- * deployment doesn't already have, which is what keeps them in core rather than
15
- * a module, per `modules.md`'s split test. A provider that does need one
16
- * (Resend, Postmark, Mailtrap, SendGrid) is a commercial integration under that
17
- * same test and belongs in a module: it supplies its own `EmailSender` from its
18
- * own config, and an assembly's entry point wires it into `services.email` in
19
- * place of `createEmailSender`, the same way `kelpie-cloud/src/server.ts` wires
20
- * this one today.
10
+ * The assembly names one named provider in `kelpie.config.ts`'s
11
+ * `email.provider`. The module runtime resolves the name against a registry
12
+ * every module can contribute to via `context.provideEmailSender(name, sender)`.
13
+ * `'log'` is a built-in name the runtime always registers, so a bare install
14
+ * has something to point at. `'smtp'` is registered by the built-in
15
+ * `smtp-email` core module. Other names come from third-party provider modules
16
+ * (Resend, Postmark, SES) that follow the same registry pattern. Transport is
17
+ * arbitrary: the module builds one behind the `EmailSender` interface and
18
+ * hands it to core, which knows only how to `send`.
21
19
  */
22
20
 
23
21
  export interface EmailMessage {
@@ -30,40 +28,29 @@ export interface EmailSender {
30
28
  send(message: EmailMessage): Promise<void>
31
29
  }
32
30
 
33
- const logEmailConfigSchema = z.object({
34
- EMAIL_PROVIDER: z.literal('log'),
31
+ export const emailConfigSchema = z.object({
32
+ /**
33
+ * The named provider `registerModules` resolves against its registry. `'log'`
34
+ * is a built-in, always present. Any other name needs a module that
35
+ * registered it via `context.provideEmailSender(name, sender)`. Free-string
36
+ * so a self-hoster can add a module core has never heard of.
37
+ */
38
+ EMAIL_PROVIDER: z.string().min(1),
35
39
  EMAIL_FROM: z.string().min(1),
36
40
  })
37
41
 
38
- const smtpEmailConfigSchema = z.object({
39
- EMAIL_PROVIDER: z.literal('smtp'),
40
- EMAIL_FROM: z.string().min(1),
41
- SMTP_HOST: z.string().min(1),
42
- SMTP_PORT: z.coerce.number().int().positive().max(65535),
43
- SMTP_SECURE: z.enum(['true', 'false']).transform((value) => value === 'true'),
44
- SMTP_USER: z.string().min(1),
45
- SMTP_PASSWORD: z.string().min(1),
46
- })
47
-
48
- export const emailConfigSchema = z.discriminatedUnion('EMAIL_PROVIDER', [
49
- logEmailConfigSchema,
50
- smtpEmailConfigSchema,
51
- ])
52
-
53
42
  export type EmailConfig = z.infer<typeof emailConfigSchema>
54
- export type SmtpEmailConfig = z.infer<typeof smtpEmailConfigSchema>
55
43
 
56
44
  /**
57
- * Writes the message to the log instead of sending it. For self-hosted
58
- * development, where the reset link in the log is exactly what you want.
59
- *
60
- * This is a deliberate choice, not a fallback: `EMAIL_PROVIDER` has no default,
61
- * so nobody reaches this provider without naming it.
45
+ * Writes the message to the log instead of sending it. The registry default,
46
+ * swapped out for a real sender by picking a different provider name in
47
+ * `email.provider` (`'smtp'` for the built-in SMTP module, or any name a
48
+ * third-party provider module registers).
62
49
  */
63
50
  export function createLogEmailSender(logger: Logger, from: string): EmailSender {
64
51
  return {
65
52
  send(message) {
66
- logger.info('email not sent: provider is "log"', {
53
+ logger.info('email not sent: no provider module registered', {
67
54
  from,
68
55
  to: message.to,
69
56
  subject: message.subject,
@@ -74,60 +61,3 @@ export function createLogEmailSender(logger: Logger, from: string): EmailSender
74
61
  },
75
62
  }
76
63
  }
77
-
78
- /**
79
- * What `createSmtpEmailSender` sends a message through. A real deployment gets
80
- * one backed by `nodemailer`; a test injects one that records calls, so the
81
- * sender is verifiable without a container running a real SMTP conversation.
82
- */
83
- export interface SmtpTransport {
84
- sendMail(message: { from: string; to: string; subject: string; text: string }): Promise<unknown>
85
- }
86
-
87
- function createNodemailerTransport(config: SmtpEmailConfig): SmtpTransport {
88
- return nodemailer.createTransport({
89
- host: config.SMTP_HOST,
90
- port: config.SMTP_PORT,
91
- secure: config.SMTP_SECURE,
92
- auth: { user: config.SMTP_USER, pass: config.SMTP_PASSWORD },
93
- })
94
- }
95
-
96
- /** Sends over SMTP. `transport` defaults to a real connection built from `config`. */
97
- export function createSmtpEmailSender(
98
- config: SmtpEmailConfig,
99
- logger: Logger,
100
- transport: SmtpTransport = createNodemailerTransport(config),
101
- ): EmailSender {
102
- return {
103
- async send(message) {
104
- try {
105
- await transport.sendMail({
106
- from: config.EMAIL_FROM,
107
- to: message.to,
108
- subject: message.subject,
109
- text: message.body,
110
- })
111
- } catch (cause) {
112
- const reason = describeThrown(cause)
113
- logger.error('smtp send failed', { to: message.to, reason })
114
- throw new Error(`Failed to send email to ${message.to} over SMTP: ${reason}`, { cause })
115
- }
116
- },
117
- }
118
- }
119
-
120
- /**
121
- * Builds the configured sender.
122
- *
123
- * @throws Never. An unknown provider cannot reach here: the config schema is a
124
- * discriminated union, so boot rejects it first.
125
- */
126
- export function createEmailSender(config: EmailConfig, logger: Logger): EmailSender {
127
- switch (config.EMAIL_PROVIDER) {
128
- case 'log':
129
- return createLogEmailSender(logger, config.EMAIL_FROM)
130
- case 'smtp':
131
- return createSmtpEmailSender(config, logger)
132
- }
133
- }