@kelpie/server 0.5.1 → 0.6.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 (347) hide show
  1. package/README.md +5 -2
  2. package/dist/index.d.ts +17 -8
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +8 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/actor.d.ts +9 -1
  7. package/dist/lib/actor.d.ts.map +1 -1
  8. package/dist/lib/actor.js +13 -0
  9. package/dist/lib/actor.js.map +1 -1
  10. package/dist/lib/config.d.ts +44 -6
  11. package/dist/lib/config.d.ts.map +1 -1
  12. package/dist/lib/config.js +9 -5
  13. package/dist/lib/config.js.map +1 -1
  14. package/dist/lib/email.d.ts +15 -63
  15. package/dist/lib/email.d.ts.map +1 -1
  16. package/dist/lib/email.js +13 -65
  17. package/dist/lib/email.js.map +1 -1
  18. package/dist/lib/fromEnv.d.ts +60 -0
  19. package/dist/lib/fromEnv.d.ts.map +1 -0
  20. package/dist/lib/fromEnv.js +94 -0
  21. package/dist/lib/fromEnv.js.map +1 -0
  22. package/dist/lib/ids.d.ts +6 -0
  23. package/dist/lib/ids.d.ts.map +1 -1
  24. package/dist/lib/ids.js +6 -0
  25. package/dist/lib/ids.js.map +1 -1
  26. package/dist/lib/kelpieConfigFile.d.ts +123 -0
  27. package/dist/lib/kelpieConfigFile.d.ts.map +1 -0
  28. package/dist/lib/kelpieConfigFile.js +111 -0
  29. package/dist/lib/kelpieConfigFile.js.map +1 -0
  30. package/dist/lib/logger.d.ts +38 -7
  31. package/dist/lib/logger.d.ts.map +1 -1
  32. package/dist/lib/logger.js +58 -20
  33. package/dist/lib/logger.js.map +1 -1
  34. package/dist/modules/agent-tasks/index.d.ts.map +1 -1
  35. package/dist/modules/agent-tasks/index.js +4 -1
  36. package/dist/modules/agent-tasks/index.js.map +1 -1
  37. package/dist/modules/agent-tasks/prompt.d.ts +36 -6
  38. package/dist/modules/agent-tasks/prompt.d.ts.map +1 -1
  39. package/dist/modules/agent-tasks/prompt.js +33 -4
  40. package/dist/modules/agent-tasks/prompt.js.map +1 -1
  41. package/dist/modules/agent-tasks/service.d.ts.map +1 -1
  42. package/dist/modules/agent-tasks/service.js +3 -2
  43. package/dist/modules/agent-tasks/service.js.map +1 -1
  44. package/dist/modules/agent-tasks/wire.d.ts +11 -0
  45. package/dist/modules/agent-tasks/wire.d.ts.map +1 -1
  46. package/dist/modules/agent-tasks/wire.js +1 -0
  47. package/dist/modules/agent-tasks/wire.js.map +1 -1
  48. package/dist/modules/auth/index.d.ts.map +1 -1
  49. package/dist/modules/auth/index.js +8 -7
  50. package/dist/modules/auth/index.js.map +1 -1
  51. package/dist/modules/companies/events.d.ts +22 -0
  52. package/dist/modules/companies/events.d.ts.map +1 -0
  53. package/dist/modules/companies/events.js +8 -0
  54. package/dist/modules/companies/events.js.map +1 -0
  55. package/dist/modules/companies/index.d.ts.map +1 -1
  56. package/dist/modules/companies/index.js +2 -0
  57. package/dist/modules/companies/index.js.map +1 -1
  58. package/dist/modules/companies/service.d.ts +1 -0
  59. package/dist/modules/companies/service.d.ts.map +1 -1
  60. package/dist/modules/companies/service.js +8 -11
  61. package/dist/modules/companies/service.js.map +1 -1
  62. package/dist/modules/core.d.ts.map +1 -1
  63. package/dist/modules/core.js +4 -0
  64. package/dist/modules/core.js.map +1 -1
  65. package/dist/modules/deals/events.d.ts +31 -0
  66. package/dist/modules/deals/events.d.ts.map +1 -0
  67. package/dist/modules/deals/events.js +12 -0
  68. package/dist/modules/deals/events.js.map +1 -0
  69. package/dist/modules/deals/index.d.ts.map +1 -1
  70. package/dist/modules/deals/index.js +2 -0
  71. package/dist/modules/deals/index.js.map +1 -1
  72. package/dist/modules/deals/service.d.ts +1 -0
  73. package/dist/modules/deals/service.d.ts.map +1 -1
  74. package/dist/modules/deals/service.js +9 -18
  75. package/dist/modules/deals/service.js.map +1 -1
  76. package/dist/modules/decisions/events.d.ts +21 -0
  77. package/dist/modules/decisions/events.d.ts.map +1 -0
  78. package/dist/modules/decisions/events.js +11 -0
  79. package/dist/modules/decisions/events.js.map +1 -0
  80. package/dist/modules/decisions/index.d.ts.map +1 -1
  81. package/dist/modules/decisions/index.js +2 -0
  82. package/dist/modules/decisions/index.js.map +1 -1
  83. package/dist/modules/decisions/service.d.ts +1 -0
  84. package/dist/modules/decisions/service.d.ts.map +1 -1
  85. package/dist/modules/decisions/service.js +4 -7
  86. package/dist/modules/decisions/service.js.map +1 -1
  87. package/dist/modules/forms/events.d.ts +37 -0
  88. package/dist/modules/forms/events.d.ts.map +1 -0
  89. package/dist/modules/forms/events.js +15 -0
  90. package/dist/modules/forms/events.js.map +1 -0
  91. package/dist/modules/forms/index.d.ts.map +1 -1
  92. package/dist/modules/forms/index.js +2 -0
  93. package/dist/modules/forms/index.js.map +1 -1
  94. package/dist/modules/forms/service.d.ts +1 -0
  95. package/dist/modules/forms/service.d.ts.map +1 -1
  96. package/dist/modules/forms/service.js +8 -11
  97. package/dist/modules/forms/service.js.map +1 -1
  98. package/dist/modules/forms/submission.d.ts +5 -0
  99. package/dist/modules/forms/submission.d.ts.map +1 -1
  100. package/dist/modules/forms/submission.js +54 -28
  101. package/dist/modules/forms/submission.js.map +1 -1
  102. package/dist/modules/handbook/events.d.ts +28 -0
  103. package/dist/modules/handbook/events.d.ts.map +1 -0
  104. package/dist/modules/handbook/events.js +14 -0
  105. package/dist/modules/handbook/events.js.map +1 -0
  106. package/dist/modules/handbook/index.d.ts.map +1 -1
  107. package/dist/modules/handbook/index.js +2 -0
  108. package/dist/modules/handbook/index.js.map +1 -1
  109. package/dist/modules/handbook/service.d.ts +1 -0
  110. package/dist/modules/handbook/service.d.ts.map +1 -1
  111. package/dist/modules/handbook/service.js +8 -15
  112. package/dist/modules/handbook/service.js.map +1 -1
  113. package/dist/modules/hiring/candidates.d.ts +1 -0
  114. package/dist/modules/hiring/candidates.d.ts.map +1 -1
  115. package/dist/modules/hiring/candidates.js +8 -11
  116. package/dist/modules/hiring/candidates.js.map +1 -1
  117. package/dist/modules/hiring/events.d.ts +35 -0
  118. package/dist/modules/hiring/events.d.ts.map +1 -0
  119. package/dist/modules/hiring/events.js +11 -0
  120. package/dist/modules/hiring/events.js.map +1 -0
  121. package/dist/modules/hiring/index.d.ts.map +1 -1
  122. package/dist/modules/hiring/index.js +2 -0
  123. package/dist/modules/hiring/index.js.map +1 -1
  124. package/dist/modules/hiring/roles.d.ts +1 -0
  125. package/dist/modules/hiring/roles.d.ts.map +1 -1
  126. package/dist/modules/hiring/roles.js +8 -11
  127. package/dist/modules/hiring/roles.js.map +1 -1
  128. package/dist/modules/import-export/events.d.ts +22 -0
  129. package/dist/modules/import-export/events.d.ts.map +1 -0
  130. package/dist/modules/import-export/events.js +12 -0
  131. package/dist/modules/import-export/events.js.map +1 -0
  132. package/dist/modules/import-export/index.d.ts.map +1 -1
  133. package/dist/modules/import-export/index.js +2 -0
  134. package/dist/modules/import-export/index.js.map +1 -1
  135. package/dist/modules/import-export/service.d.ts +5 -0
  136. package/dist/modules/import-export/service.d.ts.map +1 -1
  137. package/dist/modules/import-export/service.js +53 -14
  138. package/dist/modules/import-export/service.js.map +1 -1
  139. package/dist/modules/notes/events.d.ts +22 -0
  140. package/dist/modules/notes/events.d.ts.map +1 -0
  141. package/dist/modules/notes/events.js +12 -0
  142. package/dist/modules/notes/events.js.map +1 -0
  143. package/dist/modules/notes/index.d.ts.map +1 -1
  144. package/dist/modules/notes/index.js +2 -0
  145. package/dist/modules/notes/index.js.map +1 -1
  146. package/dist/modules/notes/service.d.ts +1 -0
  147. package/dist/modules/notes/service.d.ts.map +1 -1
  148. package/dist/modules/notes/service.js +4 -7
  149. package/dist/modules/notes/service.js.map +1 -1
  150. package/dist/modules/opportunities/events.d.ts +31 -0
  151. package/dist/modules/opportunities/events.d.ts.map +1 -0
  152. package/dist/modules/opportunities/events.js +12 -0
  153. package/dist/modules/opportunities/events.js.map +1 -0
  154. package/dist/modules/opportunities/index.d.ts.map +1 -1
  155. package/dist/modules/opportunities/index.js +2 -0
  156. package/dist/modules/opportunities/index.js.map +1 -1
  157. package/dist/modules/opportunities/service.d.ts +1 -0
  158. package/dist/modules/opportunities/service.d.ts.map +1 -1
  159. package/dist/modules/opportunities/service.js +9 -18
  160. package/dist/modules/opportunities/service.js.map +1 -1
  161. package/dist/modules/partnerships/events.d.ts +31 -0
  162. package/dist/modules/partnerships/events.d.ts.map +1 -0
  163. package/dist/modules/partnerships/events.js +12 -0
  164. package/dist/modules/partnerships/events.js.map +1 -0
  165. package/dist/modules/partnerships/index.d.ts.map +1 -1
  166. package/dist/modules/partnerships/index.js +2 -0
  167. package/dist/modules/partnerships/index.js.map +1 -1
  168. package/dist/modules/partnerships/service.d.ts +1 -0
  169. package/dist/modules/partnerships/service.d.ts.map +1 -1
  170. package/dist/modules/partnerships/service.js +9 -18
  171. package/dist/modules/partnerships/service.js.map +1 -1
  172. package/dist/modules/people/events.d.ts +28 -0
  173. package/dist/modules/people/events.d.ts.map +1 -0
  174. package/dist/modules/people/events.js +18 -0
  175. package/dist/modules/people/events.js.map +1 -0
  176. package/dist/modules/people/index.d.ts.map +1 -1
  177. package/dist/modules/people/index.js +2 -0
  178. package/dist/modules/people/index.js.map +1 -1
  179. package/dist/modules/people/service.d.ts +1 -0
  180. package/dist/modules/people/service.d.ts.map +1 -1
  181. package/dist/modules/people/service.js +8 -11
  182. package/dist/modules/people/service.js.map +1 -1
  183. package/dist/modules/pipelines/service.d.ts +4 -0
  184. package/dist/modules/pipelines/service.d.ts.map +1 -1
  185. package/dist/modules/pipelines/service.js +31 -8
  186. package/dist/modules/pipelines/service.js.map +1 -1
  187. package/dist/modules/plans/events.d.ts +21 -0
  188. package/dist/modules/plans/events.d.ts.map +1 -0
  189. package/dist/modules/plans/events.js +11 -0
  190. package/dist/modules/plans/events.js.map +1 -0
  191. package/dist/modules/plans/index.d.ts.map +1 -1
  192. package/dist/modules/plans/index.js +2 -0
  193. package/dist/modules/plans/index.js.map +1 -1
  194. package/dist/modules/plans/service.d.ts +1 -0
  195. package/dist/modules/plans/service.d.ts.map +1 -1
  196. package/dist/modules/plans/service.js +6 -14
  197. package/dist/modules/plans/service.js.map +1 -1
  198. package/dist/modules/positions/events.d.ts +22 -0
  199. package/dist/modules/positions/events.d.ts.map +1 -0
  200. package/dist/modules/positions/events.js +8 -0
  201. package/dist/modules/positions/events.js.map +1 -0
  202. package/dist/modules/positions/index.d.ts.map +1 -1
  203. package/dist/modules/positions/index.js +2 -0
  204. package/dist/modules/positions/index.js.map +1 -1
  205. package/dist/modules/positions/service.d.ts +1 -0
  206. package/dist/modules/positions/service.d.ts.map +1 -1
  207. package/dist/modules/positions/service.js +8 -11
  208. package/dist/modules/positions/service.js.map +1 -1
  209. package/dist/modules/raises/events.d.ts +31 -0
  210. package/dist/modules/raises/events.d.ts.map +1 -0
  211. package/dist/modules/raises/events.js +12 -0
  212. package/dist/modules/raises/events.js.map +1 -0
  213. package/dist/modules/raises/index.d.ts.map +1 -1
  214. package/dist/modules/raises/index.js +2 -0
  215. package/dist/modules/raises/index.js.map +1 -1
  216. package/dist/modules/raises/service.d.ts +1 -0
  217. package/dist/modules/raises/service.d.ts.map +1 -1
  218. package/dist/modules/raises/service.js +9 -18
  219. package/dist/modules/raises/service.js.map +1 -1
  220. package/dist/modules/reseal.d.ts +55 -0
  221. package/dist/modules/reseal.d.ts.map +1 -1
  222. package/dist/modules/reseal.js +90 -1
  223. package/dist/modules/reseal.js.map +1 -1
  224. package/dist/modules/smtp-email/index.d.ts +69 -0
  225. package/dist/modules/smtp-email/index.d.ts.map +1 -0
  226. package/dist/modules/smtp-email/index.js +82 -0
  227. package/dist/modules/smtp-email/index.js.map +1 -0
  228. package/dist/modules/webhooks/index.d.ts.map +1 -1
  229. package/dist/modules/webhooks/index.js +5 -1
  230. package/dist/modules/webhooks/index.js.map +1 -1
  231. package/dist/modules/webhooks/payloads.d.ts +22 -7
  232. package/dist/modules/webhooks/payloads.d.ts.map +1 -1
  233. package/dist/modules/webhooks/payloads.js +63 -44
  234. package/dist/modules/webhooks/payloads.js.map +1 -1
  235. package/dist/modules/workspace/events.d.ts +52 -0
  236. package/dist/modules/workspace/events.d.ts.map +1 -0
  237. package/dist/modules/workspace/events.js +16 -0
  238. package/dist/modules/workspace/events.js.map +1 -0
  239. package/dist/modules/workspace/index.d.ts.map +1 -1
  240. package/dist/modules/workspace/index.js +6 -2
  241. package/dist/modules/workspace/index.js.map +1 -1
  242. package/dist/modules/workspace/service.d.ts +1 -0
  243. package/dist/modules/workspace/service.d.ts.map +1 -1
  244. package/dist/modules/workspace/service.js +13 -15
  245. package/dist/modules/workspace/service.js.map +1 -1
  246. package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -1
  247. package/dist/modules/workspace/workspaceAccessMiddleware.js +11 -3
  248. package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -1
  249. package/dist/runtime/events.d.ts +86 -108
  250. package/dist/runtime/events.d.ts.map +1 -1
  251. package/dist/runtime/events.js +123 -52
  252. package/dist/runtime/events.js.map +1 -1
  253. package/dist/runtime/module.d.ts +54 -2
  254. package/dist/runtime/module.d.ts.map +1 -1
  255. package/dist/runtime/registry.d.ts +23 -0
  256. package/dist/runtime/registry.d.ts.map +1 -1
  257. package/dist/runtime/registry.js +102 -2
  258. package/dist/runtime/registry.js.map +1 -1
  259. package/dist/runtime/transaction.d.ts +29 -4
  260. package/dist/runtime/transaction.d.ts.map +1 -1
  261. package/dist/runtime/transaction.js +48 -7
  262. package/dist/runtime/transaction.js.map +1 -1
  263. package/dist/testing/app.d.ts.map +1 -1
  264. package/dist/testing/app.js +11 -3
  265. package/dist/testing/app.js.map +1 -1
  266. package/dist/testing/database.d.ts.map +1 -1
  267. package/dist/testing/database.js +6 -3
  268. package/dist/testing/database.js.map +1 -1
  269. package/dist/testing/services.d.ts +21 -1
  270. package/dist/testing/services.d.ts.map +1 -1
  271. package/dist/testing/services.js +20 -5
  272. package/dist/testing/services.js.map +1 -1
  273. package/package.json +5 -2
  274. package/src/index.ts +44 -9
  275. package/src/lib/actor.ts +15 -1
  276. package/src/lib/config.ts +56 -7
  277. package/src/lib/email.ts +22 -92
  278. package/src/lib/fromEnv.ts +143 -0
  279. package/src/lib/ids.ts +6 -0
  280. package/src/lib/kelpieConfigFile.ts +283 -0
  281. package/src/lib/logger.ts +92 -34
  282. package/src/modules/agent-tasks/index.ts +4 -1
  283. package/src/modules/agent-tasks/prompt.ts +57 -9
  284. package/src/modules/agent-tasks/service.ts +3 -2
  285. package/src/modules/agent-tasks/wire.ts +12 -0
  286. package/src/modules/auth/index.ts +11 -9
  287. package/src/modules/companies/events.ts +25 -0
  288. package/src/modules/companies/index.ts +2 -0
  289. package/src/modules/companies/service.ts +8 -11
  290. package/src/modules/core.ts +4 -0
  291. package/src/modules/deals/events.ts +34 -0
  292. package/src/modules/deals/index.ts +2 -0
  293. package/src/modules/deals/service.ts +17 -18
  294. package/src/modules/decisions/events.ts +24 -0
  295. package/src/modules/decisions/index.ts +2 -0
  296. package/src/modules/decisions/service.ts +8 -7
  297. package/src/modules/forms/events.ts +37 -0
  298. package/src/modules/forms/index.ts +2 -0
  299. package/src/modules/forms/service.ts +12 -11
  300. package/src/modules/forms/submission.ts +80 -29
  301. package/src/modules/handbook/events.ts +31 -0
  302. package/src/modules/handbook/index.ts +2 -0
  303. package/src/modules/handbook/service.ts +8 -15
  304. package/src/modules/hiring/candidates.ts +8 -11
  305. package/src/modules/hiring/events.ts +36 -0
  306. package/src/modules/hiring/index.ts +2 -0
  307. package/src/modules/hiring/roles.ts +8 -11
  308. package/src/modules/import-export/events.ts +25 -0
  309. package/src/modules/import-export/index.ts +2 -0
  310. package/src/modules/import-export/service.ts +83 -15
  311. package/src/modules/notes/events.ts +25 -0
  312. package/src/modules/notes/index.ts +2 -0
  313. package/src/modules/notes/service.ts +8 -7
  314. package/src/modules/opportunities/events.ts +34 -0
  315. package/src/modules/opportunities/index.ts +2 -0
  316. package/src/modules/opportunities/service.ts +17 -18
  317. package/src/modules/partnerships/events.ts +34 -0
  318. package/src/modules/partnerships/index.ts +2 -0
  319. package/src/modules/partnerships/service.ts +17 -18
  320. package/src/modules/people/events.ts +36 -0
  321. package/src/modules/people/index.ts +2 -0
  322. package/src/modules/people/service.ts +97 -87
  323. package/src/modules/pipelines/service.ts +47 -8
  324. package/src/modules/plans/events.ts +24 -0
  325. package/src/modules/plans/index.ts +2 -0
  326. package/src/modules/plans/service.ts +14 -14
  327. package/src/modules/positions/events.ts +25 -0
  328. package/src/modules/positions/index.ts +2 -0
  329. package/src/modules/positions/service.ts +8 -11
  330. package/src/modules/raises/events.ts +34 -0
  331. package/src/modules/raises/index.ts +2 -0
  332. package/src/modules/raises/service.ts +17 -18
  333. package/src/modules/reseal.ts +144 -1
  334. package/src/modules/smtp-email/index.ts +119 -0
  335. package/src/modules/webhooks/index.ts +5 -1
  336. package/src/modules/webhooks/payloads.ts +94 -48
  337. package/src/modules/workspace/events.ts +46 -0
  338. package/src/modules/workspace/index.ts +6 -2
  339. package/src/modules/workspace/service.ts +37 -15
  340. package/src/modules/workspace/workspaceAccessMiddleware.ts +12 -3
  341. package/src/runtime/events.ts +268 -107
  342. package/src/runtime/module.ts +55 -2
  343. package/src/runtime/registry.ts +169 -1
  344. package/src/runtime/transaction.ts +94 -17
  345. package/src/testing/app.ts +11 -3
  346. package/src/testing/database.ts +6 -3
  347. package/src/testing/services.ts +34 -5
@@ -0,0 +1,283 @@
1
+ import type { KelpieModule } from '../runtime/module.ts'
2
+ import { ConfigurationError, type Environment, type KelpieConfig, type LogLevel, type RuntimeMode } from './config.ts'
3
+ import type { EmailConfig } from './email.ts'
4
+ import { type ConfigValue, resolveMarkers } from './fromEnv.ts'
5
+ import type { LoggingDestination } from './logger.ts'
6
+ import type { RateLimitConfig } from './rateLimit.ts'
7
+ import type { SecretEncryptionConfig } from './secrets.ts'
8
+
9
+ /**
10
+ * The shape a `kelpie.config.ts` file declares.
11
+ *
12
+ * Every leaf is either a literal value the assembly commits to git, or a
13
+ * `fromEnv(...)` marker the deployment fills in. The resolver walks this
14
+ * object, replaces markers with parsed values, and returns a fully-populated
15
+ * `KelpieConfig`.
16
+ *
17
+ * The file is the map. `fromEnv(...)` marks the leaves a deployment overrides.
18
+ * Nothing in the app reads `process.env` for these fields after boot.
19
+ */
20
+
21
+ export interface KelpieConfigInput {
22
+ readonly runtimeMode: ConfigValue<RuntimeMode>
23
+ readonly port: ConfigValue<number>
24
+ readonly databaseUrl: ConfigValue<string>
25
+ readonly logging: LoggingInput
26
+ /** Optional; unset in development, set to the built web bundle in a deployment. */
27
+ readonly webBundleDirectory?: ConfigValue<string | undefined>
28
+ /** Optional; unset in development, set to the deploy-time module override file. */
29
+ readonly moduleConfigPath?: ConfigValue<string | undefined>
30
+ /** Optional; defaults to 0 (no proxy in front). */
31
+ readonly trustedProxyHopCount?: ConfigValue<number>
32
+ /**
33
+ * The deployment's base URL. Every emailed link (password reset, email
34
+ * verification, invite) is built from it. Required by the workspace and auth
35
+ * modules. Optional here so an older assembly can omit it and let those
36
+ * modules fall back to the `context.config(appUrlConfigSchema)` path.
37
+ */
38
+ readonly appBaseUrl?: ConfigValue<string>
39
+ /**
40
+ * Keys that seal stored secrets (webhook signing secrets, agent-task auth
41
+ * headers). Required by the webhooks and agent-tasks modules. Optional here
42
+ * for the same reason `appBaseUrl` is.
43
+ */
44
+ readonly secretEncryption?: SecretEncryptionInput
45
+ readonly email: EmailInput
46
+ /** Optional; each unset field falls back to the same defaults `loadConfig` used. */
47
+ readonly rateLimit?: RateLimitInput
48
+ /**
49
+ * Env-keyed values the assembly can lock in code, or defer to the
50
+ * environment. Modules read this through `context.config(schema)`; each
51
+ * module's own Zod schema still names the keys and validates them.
52
+ *
53
+ * A key present here overrides the same key in `process.env`. A key absent
54
+ * here falls through to `process.env` unchanged, so the section is a
55
+ * progressive migration: lock what you want to lock, leave the rest.
56
+ *
57
+ * Literal: `SECRET_ENCRYPTION_KEY: 'a-committed-key'` (don't do this for
58
+ * secrets — the point is you can, if the value is not a secret).
59
+ * Marker: `SECRET_ENCRYPTION_KEY: fromEnv('SECRET_ENCRYPTION_KEY', z.string())`
60
+ * defers to the environment, same as today.
61
+ */
62
+ readonly env?: Readonly<Record<string, ConfigValue<string | undefined>>>
63
+ /** The module list the assembly composes. Same shape it had before. */
64
+ readonly modules: readonly KelpieModule[]
65
+ }
66
+
67
+ /**
68
+ * The `logging` sub-tree of the assembly config.
69
+ *
70
+ * `level` is the minimum severity to emit. `destinations` is the ordered list
71
+ * of destinations each line writes to; stdout is the default entry in the
72
+ * template assembly, and a self-hoster adds another by extending
73
+ * `LoggingDestination` in `@kelpie/server` and putting a matching entry here.
74
+ */
75
+ export interface LoggingInput {
76
+ readonly level: ConfigValue<LogLevel>
77
+ readonly destinations: readonly LoggingDestination[]
78
+ }
79
+
80
+ /**
81
+ * The `email` sub-tree of the assembly config.
82
+ *
83
+ * `provider` names one entry in the runtime's provider registry. `'log'` is a
84
+ * built-in the runtime always registers; `'smtp'` is registered by the
85
+ * built-in `smtp-email` core module; third-party provider modules register
86
+ * their own names (an API-based provider might register `'resend'` or
87
+ * `'postmark'`, and so on). Free-string so a self-hoster can install a
88
+ * module core has never heard of.
89
+ *
90
+ * `from` is the address on every outgoing message. Provider-specific config
91
+ * (SMTP host and credentials, an API key) belongs to the provider module,
92
+ * which reads it through `context.config(...)`; putting it here would tie the
93
+ * assembly config to a single transport.
94
+ */
95
+ export interface EmailInput {
96
+ readonly provider: ConfigValue<string>
97
+ readonly from: ConfigValue<string>
98
+ }
99
+
100
+ export interface SecretEncryptionInput {
101
+ readonly key: ConfigValue<string>
102
+ /** Absent (or blank) is normal; only set while rotating away from an older key. */
103
+ readonly previousKey?: ConfigValue<string | undefined>
104
+ }
105
+
106
+ export interface RateLimitBudgetInput {
107
+ readonly limit?: ConfigValue<number>
108
+ readonly windowSeconds?: ConfigValue<number>
109
+ }
110
+
111
+ export interface RateLimitInput {
112
+ readonly forms?: RateLimitBudgetInput
113
+ readonly auth?: RateLimitBudgetInput
114
+ readonly loginAccount?: RateLimitBudgetInput
115
+ readonly api?: RateLimitBudgetInput
116
+ }
117
+
118
+ /**
119
+ * The defaults for each rate-limit budget, from `lib/rateLimit.ts`. Kept in
120
+ * this file so the assembly can leave `rateLimit` unset and still get the same
121
+ * numbers `loadConfig` produced.
122
+ */
123
+ const RATE_LIMIT_DEFAULTS = {
124
+ forms: { limit: 20, windowSeconds: 60 },
125
+ auth: { limit: 10, windowSeconds: 60 },
126
+ loginAccount: { limit: 10, windowSeconds: 900 },
127
+ api: { limit: 600, windowSeconds: 60 },
128
+ } as const
129
+
130
+ /**
131
+ * Identity helper for the assembly's config file. It exists for the type-check
132
+ * only: `defineKelpieConfig({...})` makes TypeScript check the object against
133
+ * `KelpieConfigInput` at the call site, so a wrong-shape field is caught in
134
+ * the assembly rather than at boot.
135
+ */
136
+ export function defineKelpieConfig(input: KelpieConfigInput): KelpieConfigInput {
137
+ return input
138
+ }
139
+
140
+ /**
141
+ * Resolves an assembly's config against an environment.
142
+ *
143
+ * Every `fromEnv` marker is resolved. Missing required variables and parse
144
+ * failures are collected together, so a self-hoster fixes them in one pass.
145
+ *
146
+ * @throws ConfigurationError listing every problem.
147
+ */
148
+ export function resolveKelpieConfig(input: KelpieConfigInput, environment: Environment): KelpieConfig {
149
+ const walked = resolveMarkers(input, environment)
150
+ const problems = [...walked.problems.map((problem) => (problem.path.length > 0 ? `${problem.path}: ${problem.message}` : problem.message))]
151
+
152
+ // Even when problems exist, the walk still produced a partial object. Reshape
153
+ // it now: any field a problem covered will be undefined, which is fine, since
154
+ // we throw before returning.
155
+ const resolved = walked.value as ResolvedInput
156
+
157
+ const email = buildEmailConfig(resolved.email)
158
+ const rateLimit = buildRateLimitConfig(resolved.rateLimit)
159
+ const env = mergeEnv(environment, resolved.env)
160
+ const secretEncryption = buildSecretEncryptionConfig(resolved.secretEncryption)
161
+
162
+ if (problems.length > 0) {
163
+ throw new ConfigurationError(problems)
164
+ }
165
+
166
+ return {
167
+ runtimeMode: resolved.runtimeMode,
168
+ port: resolved.port,
169
+ databaseUrl: resolved.databaseUrl,
170
+ logging: {
171
+ level: resolved.logging.level,
172
+ destinations: resolved.logging.destinations,
173
+ },
174
+ email,
175
+ moduleConfigPath: resolved.moduleConfigPath,
176
+ webBundleDirectory: resolved.webBundleDirectory,
177
+ rateLimit,
178
+ trustedProxyHopCount: resolved.trustedProxyHopCount ?? 0,
179
+ env,
180
+ appBaseUrl: resolved.appBaseUrl,
181
+ secretEncryption,
182
+ }
183
+ }
184
+
185
+ /**
186
+ * `kelpie.config.ts` env wins per-key; unset keys pass through from `environment`.
187
+ * Absent env section returns `environment` unchanged, so a self-hoster who has
188
+ * not opted in gets the same behaviour they had before pass 2.
189
+ */
190
+ function mergeEnv(
191
+ environment: Environment,
192
+ resolvedEnv: Readonly<Record<string, string | undefined>> | undefined,
193
+ ): Environment {
194
+ if (resolvedEnv === undefined) {
195
+ return environment
196
+ }
197
+
198
+ return { ...environment, ...resolvedEnv }
199
+ }
200
+
201
+ interface ResolvedInput {
202
+ readonly runtimeMode: RuntimeMode
203
+ readonly port: number
204
+ readonly databaseUrl: string
205
+ readonly logging: {
206
+ readonly level: LogLevel
207
+ readonly destinations: readonly LoggingDestination[]
208
+ }
209
+ readonly webBundleDirectory?: string | undefined
210
+ readonly moduleConfigPath?: string | undefined
211
+ readonly trustedProxyHopCount?: number
212
+ readonly appBaseUrl?: string
213
+ readonly secretEncryption?: ResolvedSecretEncryption
214
+ readonly email: ResolvedEmail
215
+ readonly rateLimit?: ResolvedRateLimit
216
+ readonly env?: Readonly<Record<string, string | undefined>>
217
+ readonly modules: readonly KelpieModule[]
218
+ }
219
+
220
+ interface ResolvedSecretEncryption {
221
+ readonly key: string
222
+ readonly previousKey?: string
223
+ }
224
+
225
+ interface ResolvedEmail {
226
+ readonly provider: string
227
+ readonly from: string
228
+ }
229
+
230
+ interface ResolvedRateLimit {
231
+ readonly forms?: { readonly limit?: number; readonly windowSeconds?: number }
232
+ readonly auth?: { readonly limit?: number; readonly windowSeconds?: number }
233
+ readonly loginAccount?: { readonly limit?: number; readonly windowSeconds?: number }
234
+ readonly api?: { readonly limit?: number; readonly windowSeconds?: number }
235
+ }
236
+
237
+ function buildEmailConfig(resolved: ResolvedEmail | undefined): EmailConfig {
238
+ // Missing `email.provider` or `email.from` is already recorded by the
239
+ // walker; return a placeholder so `resolveKelpieConfig` reaches its throw
240
+ // below.
241
+ return {
242
+ EMAIL_PROVIDER: resolved?.provider ?? '',
243
+ EMAIL_FROM: resolved?.from ?? '',
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Reshape the resolved leaves into the `SecretEncryptionConfig` shape modules
249
+ * already consume. Returns undefined when the config file omits the field, in
250
+ * which case a module falls back to reading `context.config(...)`.
251
+ */
252
+ function buildSecretEncryptionConfig(
253
+ resolved: ResolvedSecretEncryption | undefined,
254
+ ): SecretEncryptionConfig | undefined {
255
+ if (resolved === undefined) {
256
+ return undefined
257
+ }
258
+
259
+ return resolved.previousKey === undefined
260
+ ? { SECRET_ENCRYPTION_KEY: resolved.key }
261
+ : { SECRET_ENCRYPTION_KEY: resolved.key, SECRET_ENCRYPTION_KEY_PREVIOUS: resolved.previousKey }
262
+ }
263
+
264
+ function buildRateLimitConfig(resolved: ResolvedRateLimit | undefined): RateLimitConfig {
265
+ return {
266
+ forms: budget('forms', resolved?.forms),
267
+ auth: budget('auth', resolved?.auth),
268
+ loginAccount: budget('loginAccount', resolved?.loginAccount),
269
+ api: budget('api', resolved?.api),
270
+ }
271
+ }
272
+
273
+ function budget(
274
+ key: keyof typeof RATE_LIMIT_DEFAULTS,
275
+ input: { readonly limit?: number; readonly windowSeconds?: number } | undefined,
276
+ ): { readonly limit: number; readonly windowMs: number } {
277
+ const defaults = RATE_LIMIT_DEFAULTS[key]
278
+
279
+ return {
280
+ limit: input?.limit ?? defaults.limit,
281
+ windowMs: (input?.windowSeconds ?? defaults.windowSeconds) * 1000,
282
+ }
283
+ }
package/src/lib/logger.ts CHANGED
@@ -1,8 +1,18 @@
1
+ import winston from 'winston'
2
+ import Transport from 'winston-transport'
3
+
1
4
  import type { LogLevel } from './config.ts'
2
5
 
3
6
  /**
4
- * Structured logging as JSON lines. The sink is injected so tests capture output
5
- * instead of writing to stdout, and so a deployment can redirect it.
7
+ * Structured logging as JSON lines. Every line goes through one or more
8
+ * transports, each of which is a named destination (`createStdoutTransport`,
9
+ * `createCaptureTransport`, and whatever a deployment adds). Nothing defaults:
10
+ * an empty transport list means the logger writes nowhere, which the assembly
11
+ * must notice.
12
+ *
13
+ * Winston handles level filtering and fan-out to transports. The JSON line
14
+ * itself is built in this file and passed through as the message, so the
15
+ * envelope order (`time`, `level`, `message` last) is preserved exactly.
6
16
  */
7
17
 
8
18
  export type LogFields = Readonly<Record<string, unknown>>
@@ -16,48 +26,96 @@ export interface Logger {
16
26
  child(fields: LogFields): Logger
17
27
  }
18
28
 
19
- export type LogSink = (line: string) => void
29
+ /**
30
+ * A destination declared in `kelpie.config.ts`. Extend the union to add a new
31
+ * kind; the switch in `createTransportForDestination` fails to compile until it
32
+ * covers every new variant, so a destination cannot be added in config without
33
+ * a matching transport factory.
34
+ */
35
+ export type LoggingDestination = { readonly kind: 'stdout' }
36
+
37
+ const LEVELS: Readonly<Record<LogLevel, number>> = {
38
+ error: 0,
39
+ warn: 1,
40
+ info: 2,
41
+ debug: 3,
42
+ }
43
+
44
+ class LineTransport extends Transport {
45
+ private readonly writeLine: (line: string) => void
46
+
47
+ constructor(writeLine: (line: string) => void) {
48
+ super()
49
+ this.writeLine = writeLine
50
+ }
51
+
52
+ override log(info: { readonly message: string }, next: () => void): void {
53
+ this.writeLine(info.message)
54
+ // Winston expects the transport to emit 'logged' after handling.
55
+ setImmediate(() => this.emit('logged', info))
56
+ next()
57
+ }
58
+ }
59
+
60
+ /** Writes each line plus a trailing newline directly to stdout. */
61
+ export function createStdoutTransport(): Transport {
62
+ return new LineTransport((line) => {
63
+ process.stdout.write(`${line}\n`)
64
+ })
65
+ }
20
66
 
21
- const severityByLevel: Readonly<Record<LogLevel, number>> = {
22
- debug: 10,
23
- info: 20,
24
- warn: 30,
25
- error: 40,
67
+ /** Hands each line to a callback. Tests use it to capture output. */
68
+ export function createCaptureTransport(onLine: (line: string) => void): Transport {
69
+ return new LineTransport(onLine)
70
+ }
71
+
72
+ /**
73
+ * Builds the transport for one declared destination. The switch is exhaustive
74
+ * so adding a new `LoggingDestination` kind without a factory clause fails to
75
+ * compile.
76
+ */
77
+ export function createTransportForDestination(destination: LoggingDestination): Transport {
78
+ switch (destination.kind) {
79
+ case 'stdout':
80
+ return createStdoutTransport()
81
+ }
26
82
  }
27
83
 
28
- function writeToStdout(line: string): void {
29
- process.stdout.write(`${line}\n`)
84
+ export interface CreateLoggerOptions {
85
+ readonly level: LogLevel
86
+ readonly transports: readonly Transport[]
87
+ /** Injected so tests get deterministic timestamps. Defaults to the wall clock. */
88
+ readonly now?: () => Date
30
89
  }
31
90
 
32
91
  /**
33
- * @param level Minimum level to emit. Lines below it are dropped.
34
- * @param sink Receives one complete JSON line per log call.
35
- * @param now Injected so tests get deterministic timestamps.
92
+ * Builds a logger. `transports` is required: an empty list writes nowhere and
93
+ * that is the assembly's decision to make, not a silent default.
36
94
  */
37
- export function createLogger(
38
- level: LogLevel,
39
- sink: LogSink = writeToStdout,
40
- now: () => Date = () => new Date(),
41
- ): Logger {
42
- const threshold = severityByLevel[level]
95
+ export function createLogger(options: CreateLoggerOptions): Logger {
96
+ const now = options.now ?? ((): Date => new Date())
97
+ const winstonLogger = winston.createLogger({
98
+ levels: LEVELS,
99
+ level: options.level,
100
+ transports: [...options.transports],
101
+ })
43
102
 
44
103
  function build(boundFields: LogFields): Logger {
45
104
  function emit(lineLevel: LogLevel, message: string, fields?: LogFields): void {
46
- if (severityByLevel[lineLevel] < threshold) {
47
- return
48
- }
49
- // The envelope is written last so a field called `message`, `level`, or
50
- // `time` cannot overwrite it. A colliding field is dropped; a corrupted
51
- // envelope would make the whole line untrustworthy.
52
- sink(
53
- JSON.stringify({
54
- ...boundFields,
55
- ...fields,
56
- time: now().toISOString(),
57
- level: lineLevel,
58
- message,
59
- }),
60
- )
105
+ // The envelope is written last so a bound or per-call field called
106
+ // `message`, `level`, or `time` cannot overwrite it. A colliding field
107
+ // is dropped; a corrupted envelope would make the whole line untrustworthy.
108
+ const line = JSON.stringify({
109
+ ...boundFields,
110
+ ...fields,
111
+ time: now().toISOString(),
112
+ level: lineLevel,
113
+ message,
114
+ })
115
+ // The full JSON line is passed as `message` and each transport writes it
116
+ // verbatim. Winston still filters by level and fans out to transports;
117
+ // it does not re-format the line.
118
+ winstonLogger.log({ level: lineLevel, message: line })
61
119
  }
62
120
 
63
121
  return {
@@ -48,7 +48,10 @@ export function createAgentTasksModule(
48
48
  // Validated at boot rather than on the first run: a missing or malformed
49
49
  // key means no stored auth header can ever be read back, and finding that
50
50
  // out when a dispatch quietly fails is far too late.
51
- const cipher = createSecretCipher(context.config(secretEncryptionConfigSchema))
51
+ //
52
+ // Prefer the top-level `secretEncryption` from the assembly's
53
+ // kelpie.config.ts; fall back to the schema read for older assemblies.
54
+ const cipher = createSecretCipher(context.secretEncryption ?? context.config(secretEncryptionConfigSchema))
52
55
  const egress = createEgressGuard(context.config(egressConfigSchema))
53
56
 
54
57
  const engine = createDispatchEngine({
@@ -3,12 +3,22 @@ import type { AgentTaskDefinition, AgentTaskTargetType } from '@kelpie/schemas'
3
3
  /**
4
4
  * Renders the markdown prompt a ResolvedTask carries. Pure: every fact arrives
5
5
  * as an argument, so the template is unit-testable and Copy and Run cannot
6
- * drift — both read the one string this produces.
6
+ * drift — both read the strings this file produces.
7
7
  *
8
- * The shape is the mockup's (`mockups/src/data/agentTasks.ts`), with two
9
- * server-era changes: workspace-scoped tasks are pointed at `GET /v1/dashboard`
10
- * instead of a single record, and the sweep tasks carry a "Workspace signals"
11
- * section computed at resolve time.
8
+ * Two renderers, one shared body.
9
+ *
10
+ * - {@link renderBasePrompt} returns the general request: the task title,
11
+ * the target, the required reads, the handbook pages, related ids,
12
+ * workspace signals, the task instructions, and the write policy. It
13
+ * says nothing about *how* the receiver should apply the changes.
14
+ * - {@link renderExternalAgentPrompt} wraps the base with framing for an
15
+ * agent that calls the CRM itself — the opening line about MCP / the
16
+ * public API and the closing "Done when… applied allowed updates" tail.
17
+ *
18
+ * The resolved-task and dispatch payloads carry both. The copy-prompt UI
19
+ * flow serves the external-agent version by default; the hosted AI in the
20
+ * cloud reads the base and adds its own structured-output framing. Two
21
+ * consumers, two wrappers, one body.
12
22
  */
13
23
 
14
24
  /** A handbook page the task requires, resolved to this workspace's own copy. */
@@ -99,7 +109,13 @@ function firstRead(targetType: AgentTaskTargetType): string {
99
109
  return 'Load the target record and agent fields.'
100
110
  }
101
111
 
102
- export function renderPrompt(
112
+ /**
113
+ * The shared body. Renders everything a receiver needs to *understand* the
114
+ * task, without telling it how to apply results. Callers wrap this with an
115
+ * opening and closing that matches how the receiver will execute (call the
116
+ * CRM, or return structured data for a caller to apply).
117
+ */
118
+ export function renderBasePrompt(
103
119
  definition: AgentTaskDefinition,
104
120
  targetType: AgentTaskTargetType,
105
121
  targetId: string,
@@ -107,9 +123,6 @@ export function renderPrompt(
107
123
  ): string {
108
124
  return `# Agent task: ${definition.label}
109
125
 
110
- You are operating on the Kelpie workspace **${inputs.workspaceName}** via MCP / the public API.
111
- Bring your own model. Kelpie does not bundle AI.
112
-
113
126
  ## Task
114
127
  - **Id:** \`${definition.id}\`
115
128
  - **Intent:** ${definition.description}
@@ -133,8 +146,43 @@ ${definition.instructions}
133
146
 
134
147
  ## Write policy
135
148
  ${definition.writePolicy}
149
+ `
150
+ }
136
151
 
152
+ /**
153
+ * The external-agent-framed prompt. Adds the opening line about MCP / the
154
+ * public API and the closing "Done when" tail to the base. This is what the
155
+ * Copy button hands to the user and what dispatch sends by default; an
156
+ * agent that runs its own tool loop against the CRM reads this and works
157
+ * from it directly.
158
+ */
159
+ export function renderExternalAgentPrompt(
160
+ definition: AgentTaskDefinition,
161
+ targetType: AgentTaskTargetType,
162
+ targetId: string,
163
+ inputs: PromptInputs,
164
+ ): string {
165
+ const opening = `# Agent task: ${definition.label}
166
+
167
+ You are operating on the Kelpie workspace **${inputs.workspaceName}** via MCP / the public API.
168
+ Bring your own model. Kelpie does not bundle AI.
169
+ `
170
+
171
+ // The base opens with its own `# Agent task:` header; strip it so the
172
+ // combined prompt has one heading rather than two.
173
+ const base = renderBasePrompt(definition, targetType, targetId, inputs)
174
+ const bodyWithoutHeading = base.replace(/^# Agent task:[^\n]*\n\n?/u, '')
175
+
176
+ return `${opening}
177
+ ${bodyWithoutHeading}
137
178
  ## Done when
138
179
  You have applied allowed updates via MCP/API (or asked the human when blocked), and summarised what changed.
139
180
  `
140
181
  }
182
+
183
+ /**
184
+ * Backwards-compatible alias for the pre-split renderer. Existing callers
185
+ * that produced the "wrapped" prompt keep working. New callers pick
186
+ * {@link renderBasePrompt} or {@link renderExternalAgentPrompt} directly.
187
+ */
188
+ export const renderPrompt = renderExternalAgentPrompt
@@ -14,7 +14,7 @@ import { requireWorkspaceId } from '../auth/actor.ts'
14
14
  import { roleAllows } from '../workspace/roles.ts'
15
15
  import { AGENT_TASK_DEFINITIONS, findTask, tasksFor } from './catalog.ts'
16
16
  import type { DispatchEngine } from './dispatch.ts'
17
- import { renderPrompt } from './prompt.ts'
17
+ import { renderBasePrompt, renderExternalAgentPrompt } from './prompt.ts'
18
18
  import * as repository from './repository.ts'
19
19
  import { AGENT_SORTS, DEFAULT_AGENT_SORT, DEFAULT_RUN_SORT, RUN_SORTS } from './repository.ts'
20
20
  import type { AgentRecord, RunFilters, RunRecord } from './repository.ts'
@@ -184,7 +184,8 @@ export function createAgentTasksService(dependencies: AgentTasksDependencies): A
184
184
  taskId: definition.id,
185
185
  targetType: target.targetType,
186
186
  targetId: target.targetId,
187
- prompt: renderPrompt(definition, target.targetType, target.targetId, inputs),
187
+ prompt: renderExternalAgentPrompt(definition, target.targetType, target.targetId, inputs),
188
+ basePrompt: renderBasePrompt(definition, target.targetType, target.targetId, inputs),
188
189
  context: {
189
190
  targetLabel: inputs.targetLabel,
190
191
  deepLink: inputs.deepLink,
@@ -21,7 +21,18 @@ export interface ResolvedTaskView {
21
21
  readonly taskId: string
22
22
  readonly targetType: AgentTaskTargetType
23
23
  readonly targetId: string
24
+ /**
25
+ * The external-agent-framed prompt: opens with "operating via MCP / the
26
+ * public API", ends with "Done when… applied allowed updates". This is
27
+ * what Copy hands to a user and what a customer agent reads.
28
+ */
24
29
  readonly prompt: string
30
+ /**
31
+ * The shared body without the framing. What an agent that returns
32
+ * structured data (the hosted AI is the one that ships) reads before
33
+ * adding its own instructions.
34
+ */
35
+ readonly basePrompt: string
25
36
  readonly context: ContextPackView
26
37
  }
27
38
 
@@ -31,6 +42,7 @@ export function resolvedTaskResponse(resolved: ResolvedTaskView): Record<string,
31
42
  target_type: resolved.targetType,
32
43
  target_id: resolved.targetId,
33
44
  prompt: resolved.prompt,
45
+ base_prompt: resolved.basePrompt,
34
46
  context: {
35
47
  target_label: resolved.context.targetLabel,
36
48
  deep_link: resolved.context.deepLink,
@@ -13,12 +13,10 @@ import { createAuthService } from './service.ts'
13
13
  * `users`.
14
14
  */
15
15
 
16
- const authConfigSchema = z
17
- .object({
18
- /** Cookies go out `Secure` everywhere except development. */
19
- NODE_ENV: z.enum(['development', 'test', 'production']),
20
- })
21
- .merge(appUrlConfigSchema)
16
+ const nodeEnvSchema = z.object({
17
+ /** Cookies go out `Secure` everywhere except development. */
18
+ NODE_ENV: z.enum(['development', 'test', 'production']),
19
+ })
22
20
 
23
21
  export function createAuthModule(migrationsDirectory: string): KelpieModule {
24
22
  return {
@@ -26,14 +24,18 @@ export function createAuthModule(migrationsDirectory: string): KelpieModule {
26
24
  structural: true,
27
25
 
28
26
  register(context) {
29
- const config = context.config(authConfigSchema)
27
+ const { NODE_ENV } = context.config(nodeEnvSchema)
28
+ // Prefer the top-level `appBaseUrl` from the assembly's kelpie.config.ts;
29
+ // fall back to the schema read for older assemblies that don't declare it.
30
+ const appBaseUrl = context.appBaseUrl ?? context.config(appUrlConfigSchema).APP_BASE_URL
31
+
30
32
  const service = createAuthService({
31
33
  db: context.db,
32
34
  transaction: context.transaction,
33
35
  email: context.email,
34
36
  createId: context.createId,
35
37
  now: context.now,
36
- appBaseUrl: config.APP_BASE_URL,
38
+ appBaseUrl,
37
39
  })
38
40
 
39
41
  context.schema(schema, migrationsDirectory)
@@ -46,7 +48,7 @@ export function createAuthModule(migrationsDirectory: string): KelpieModule {
46
48
  // `Secure` everywhere except development. A test host reaches the API
47
49
  // over http through a test client, which stores the cookie regardless
48
50
  // of the flag, so test is not excluded.
49
- cookie: { secure: config.NODE_ENV !== 'development' },
51
+ cookie: { secure: NODE_ENV !== 'development' },
50
52
  })
51
53
  })
52
54
 
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { ModuleEventCatalog } from '../../runtime/module.ts'
4
+
5
+ /** Domain events published by the companies module. */
6
+
7
+ export const companiesEvents = {
8
+ 'companies.company.created': z.object({}).strict(),
9
+ 'companies.company.updated': z.object({ changed: z.array(z.string()).readonly() }),
10
+ 'companies.company.deleted': z.object({}).strict(),
11
+ } satisfies ModuleEventCatalog
12
+
13
+ export type CompanyCreatedData = Record<string, never>
14
+ export interface CompanyUpdatedData {
15
+ readonly changed: readonly string[]
16
+ }
17
+ export type CompanyDeletedData = Record<string, never>
18
+
19
+ declare module '../../runtime/events.ts' {
20
+ interface KelpieEventMap {
21
+ 'companies.company.created': CompanyCreatedData
22
+ 'companies.company.updated': CompanyUpdatedData
23
+ 'companies.company.deleted': CompanyDeletedData
24
+ }
25
+ }
@@ -1,5 +1,6 @@
1
1
  import type { KelpieModule } from '../../runtime/module.ts'
2
2
  import { createActivityRecorder } from '../activities/index.ts'
3
+ import { companiesEvents } from './events.ts'
3
4
  import { mountCompaniesRoutes } from './routes.ts'
4
5
  import * as schema from './schema.ts'
5
6
  import { createCompaniesService } from './service.ts'
@@ -16,6 +17,7 @@ export function createCompaniesModule(migrationsDirectory: string): KelpieModule
16
17
  id: 'companies',
17
18
  requires: ['workspace', 'activities'],
18
19
  structural: true,
20
+ events: companiesEvents,
19
21
 
20
22
  register(context) {
21
23
  const service = createCompaniesService({