@kelpie/server 0.12.0 → 0.13.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 (383) hide show
  1. package/dist/app.d.ts +13 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +8 -1
  4. package/dist/app.js.map +1 -1
  5. package/dist/boot.d.ts.map +1 -1
  6. package/dist/boot.js +1 -0
  7. package/dist/boot.js.map +1 -1
  8. package/dist/lib/apiKeyScopes.d.ts.map +1 -1
  9. package/dist/lib/apiKeyScopes.js +6 -0
  10. package/dist/lib/apiKeyScopes.js.map +1 -1
  11. package/dist/lib/columns.d.ts +6 -0
  12. package/dist/lib/columns.d.ts.map +1 -1
  13. package/dist/lib/columns.js +5 -3
  14. package/dist/lib/columns.js.map +1 -1
  15. package/dist/lib/config.d.ts +17 -0
  16. package/dist/lib/config.d.ts.map +1 -1
  17. package/dist/lib/config.js +3 -0
  18. package/dist/lib/config.js.map +1 -1
  19. package/dist/lib/egress.d.ts.map +1 -1
  20. package/dist/lib/egress.js +8 -4
  21. package/dist/lib/egress.js.map +1 -1
  22. package/dist/lib/errors.d.ts +3 -3
  23. package/dist/lib/errors.d.ts.map +1 -1
  24. package/dist/lib/errors.js +13 -5
  25. package/dist/lib/errors.js.map +1 -1
  26. package/dist/lib/ids.d.ts +11 -0
  27. package/dist/lib/ids.d.ts.map +1 -1
  28. package/dist/lib/ids.js +11 -0
  29. package/dist/lib/ids.js.map +1 -1
  30. package/dist/lib/kelpieConfigFile.d.ts +15 -1
  31. package/dist/lib/kelpieConfigFile.d.ts.map +1 -1
  32. package/dist/lib/kelpieConfigFile.js +33 -1
  33. package/dist/lib/kelpieConfigFile.js.map +1 -1
  34. package/dist/modules/agent-tasks/catalog.d.ts.map +1 -1
  35. package/dist/modules/agent-tasks/catalog.js +32 -1
  36. package/dist/modules/agent-tasks/catalog.js.map +1 -1
  37. package/dist/modules/agent-tasks/index.d.ts.map +1 -1
  38. package/dist/modules/agent-tasks/index.js +1 -0
  39. package/dist/modules/agent-tasks/index.js.map +1 -1
  40. package/dist/modules/agent-tasks/resolve.d.ts.map +1 -1
  41. package/dist/modules/agent-tasks/resolve.js +5 -0
  42. package/dist/modules/agent-tasks/resolve.js.map +1 -1
  43. package/dist/modules/agent-tasks/routes.d.ts +2 -0
  44. package/dist/modules/agent-tasks/routes.d.ts.map +1 -1
  45. package/dist/modules/attachedRecords.d.ts +22 -8
  46. package/dist/modules/attachedRecords.d.ts.map +1 -1
  47. package/dist/modules/attachedRecords.js +42 -5
  48. package/dist/modules/attachedRecords.js.map +1 -1
  49. package/dist/modules/auth/index.d.ts.map +1 -1
  50. package/dist/modules/auth/index.js +1 -0
  51. package/dist/modules/auth/index.js.map +1 -1
  52. package/dist/modules/auth/repository.d.ts +14 -0
  53. package/dist/modules/auth/repository.d.ts.map +1 -1
  54. package/dist/modules/auth/repository.js +20 -2
  55. package/dist/modules/auth/repository.js.map +1 -1
  56. package/dist/modules/auth/routes.d.ts.map +1 -1
  57. package/dist/modules/auth/routes.js +30 -0
  58. package/dist/modules/auth/routes.js.map +1 -1
  59. package/dist/modules/auth/service.d.ts +28 -0
  60. package/dist/modules/auth/service.d.ts.map +1 -1
  61. package/dist/modules/auth/service.js +72 -4
  62. package/dist/modules/auth/service.js.map +1 -1
  63. package/dist/modules/companies/repository.d.ts.map +1 -1
  64. package/dist/modules/companies/repository.js +1 -1
  65. package/dist/modules/companies/repository.js.map +1 -1
  66. package/dist/modules/companies/routes.d.ts +2 -4
  67. package/dist/modules/companies/routes.d.ts.map +1 -1
  68. package/dist/modules/companies/routes.js +6 -11
  69. package/dist/modules/companies/routes.js.map +1 -1
  70. package/dist/modules/companies/schema.d.ts +13 -28
  71. package/dist/modules/companies/schema.d.ts.map +1 -1
  72. package/dist/modules/companies/schema.js +2 -2
  73. package/dist/modules/companies/schema.js.map +1 -1
  74. package/dist/modules/companies/service.d.ts +2 -3
  75. package/dist/modules/companies/service.d.ts.map +1 -1
  76. package/dist/modules/companies/service.js +4 -7
  77. package/dist/modules/companies/service.js.map +1 -1
  78. package/dist/modules/core.d.ts.map +1 -1
  79. package/dist/modules/core.js +2 -0
  80. package/dist/modules/core.js.map +1 -1
  81. package/dist/modules/custom-fields/repository.d.ts.map +1 -1
  82. package/dist/modules/custom-fields/repository.js +2 -0
  83. package/dist/modules/custom-fields/repository.js.map +1 -1
  84. package/dist/modules/custom-fields/routes.d.ts +1 -0
  85. package/dist/modules/custom-fields/routes.d.ts.map +1 -1
  86. package/dist/modules/dashboard/index.d.ts.map +1 -1
  87. package/dist/modules/dashboard/index.js +1 -0
  88. package/dist/modules/dashboard/index.js.map +1 -1
  89. package/dist/modules/dashboard/repository.d.ts +13 -0
  90. package/dist/modules/dashboard/repository.d.ts.map +1 -1
  91. package/dist/modules/dashboard/repository.js +36 -0
  92. package/dist/modules/dashboard/repository.js.map +1 -1
  93. package/dist/modules/dashboard/routes.d.ts.map +1 -1
  94. package/dist/modules/dashboard/routes.js +13 -0
  95. package/dist/modules/dashboard/routes.js.map +1 -1
  96. package/dist/modules/dashboard/service.d.ts +11 -0
  97. package/dist/modules/dashboard/service.d.ts.map +1 -1
  98. package/dist/modules/dashboard/service.js +24 -1
  99. package/dist/modules/dashboard/service.js.map +1 -1
  100. package/dist/modules/decisions/repository.d.ts.map +1 -1
  101. package/dist/modules/decisions/repository.js +2 -0
  102. package/dist/modules/decisions/repository.js.map +1 -1
  103. package/dist/modules/decisions/routes.d.ts +2 -0
  104. package/dist/modules/decisions/routes.d.ts.map +1 -1
  105. package/dist/modules/events/attendances.d.ts +35 -0
  106. package/dist/modules/events/attendances.d.ts.map +1 -0
  107. package/dist/modules/events/attendances.js +157 -0
  108. package/dist/modules/events/attendances.js.map +1 -0
  109. package/dist/modules/events/catalog.d.ts +35 -0
  110. package/dist/modules/events/catalog.d.ts.map +1 -0
  111. package/dist/modules/events/catalog.js +11 -0
  112. package/dist/modules/events/catalog.js.map +1 -0
  113. package/dist/modules/events/index.d.ts +10 -0
  114. package/dist/modules/events/index.d.ts.map +1 -0
  115. package/dist/modules/events/index.js +47 -0
  116. package/dist/modules/events/index.js.map +1 -0
  117. package/dist/modules/events/repository.d.ts +67 -0
  118. package/dist/modules/events/repository.d.ts.map +1 -0
  119. package/dist/modules/events/repository.js +212 -0
  120. package/dist/modules/events/repository.js.map +1 -0
  121. package/dist/modules/events/routes.d.ts +136 -0
  122. package/dist/modules/events/routes.d.ts.map +1 -0
  123. package/dist/modules/events/routes.js +280 -0
  124. package/dist/modules/events/routes.js.map +1 -0
  125. package/dist/modules/events/schema.d.ts +626 -0
  126. package/dist/modules/events/schema.d.ts.map +1 -0
  127. package/dist/modules/events/schema.js +93 -0
  128. package/dist/modules/events/schema.js.map +1 -0
  129. package/dist/modules/events/service.d.ts +68 -0
  130. package/dist/modules/events/service.d.ts.map +1 -0
  131. package/dist/modules/events/service.js +262 -0
  132. package/dist/modules/events/service.js.map +1 -0
  133. package/dist/modules/events/tools.d.ts +8 -0
  134. package/dist/modules/events/tools.d.ts.map +1 -0
  135. package/dist/modules/events/tools.js +80 -0
  136. package/dist/modules/events/tools.js.map +1 -0
  137. package/dist/modules/forms/applySubmissionFields.d.ts.map +1 -1
  138. package/dist/modules/forms/applySubmissionFields.js +7 -5
  139. package/dist/modules/forms/applySubmissionFields.js.map +1 -1
  140. package/dist/modules/forms/fillAddresses.d.ts +4 -0
  141. package/dist/modules/forms/fillAddresses.d.ts.map +1 -0
  142. package/dist/modules/forms/fillAddresses.js +93 -0
  143. package/dist/modules/forms/fillAddresses.js.map +1 -0
  144. package/dist/modules/forms/repository.d.ts +2 -2
  145. package/dist/modules/forms/repository.d.ts.map +1 -1
  146. package/dist/modules/forms/repository.js.map +1 -1
  147. package/dist/modules/forms/routes.d.ts +2 -0
  148. package/dist/modules/forms/routes.d.ts.map +1 -1
  149. package/dist/modules/forms/routes.js +2 -2
  150. package/dist/modules/forms/routes.js.map +1 -1
  151. package/dist/modules/forms/schema.js +2 -2
  152. package/dist/modules/forms/schema.js.map +1 -1
  153. package/dist/modules/forms/service.js +2 -2
  154. package/dist/modules/forms/service.js.map +1 -1
  155. package/dist/modules/forms/submission.d.ts +1 -0
  156. package/dist/modules/forms/submission.d.ts.map +1 -1
  157. package/dist/modules/forms/submission.js +27 -4
  158. package/dist/modules/forms/submission.js.map +1 -1
  159. package/dist/modules/hiring/roles.d.ts.map +1 -1
  160. package/dist/modules/hiring/roles.js +2 -1
  161. package/dist/modules/hiring/roles.js.map +1 -1
  162. package/dist/modules/import-export/addresses.d.ts +22 -0
  163. package/dist/modules/import-export/addresses.d.ts.map +1 -0
  164. package/dist/modules/import-export/addresses.js +83 -0
  165. package/dist/modules/import-export/addresses.js.map +1 -0
  166. package/dist/modules/import-export/drafts.d.ts +3 -4
  167. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  168. package/dist/modules/import-export/drafts.js +4 -5
  169. package/dist/modules/import-export/drafts.js.map +1 -1
  170. package/dist/modules/import-export/exportRows.d.ts.map +1 -1
  171. package/dist/modules/import-export/exportRows.js +3 -3
  172. package/dist/modules/import-export/exportRows.js.map +1 -1
  173. package/dist/modules/import-export/plan.d.ts.map +1 -1
  174. package/dist/modules/import-export/plan.js +17 -2
  175. package/dist/modules/import-export/plan.js.map +1 -1
  176. package/dist/modules/import-export/presets.d.ts.map +1 -1
  177. package/dist/modules/import-export/presets.js +52 -7
  178. package/dist/modules/import-export/presets.js.map +1 -1
  179. package/dist/modules/import-export/repository.d.ts +3 -4
  180. package/dist/modules/import-export/repository.d.ts.map +1 -1
  181. package/dist/modules/import-export/repository.js +2 -3
  182. package/dist/modules/import-export/repository.js.map +1 -1
  183. package/dist/modules/import-export/writes.d.ts.map +1 -1
  184. package/dist/modules/import-export/writes.js +8 -4
  185. package/dist/modules/import-export/writes.js.map +1 -1
  186. package/dist/modules/lists/routes.d.ts +4 -0
  187. package/dist/modules/lists/routes.d.ts.map +1 -1
  188. package/dist/modules/notes/routes.d.ts +2 -0
  189. package/dist/modules/notes/routes.d.ts.map +1 -1
  190. package/dist/modules/people/repository.d.ts.map +1 -1
  191. package/dist/modules/people/repository.js +1 -1
  192. package/dist/modules/people/repository.js.map +1 -1
  193. package/dist/modules/people/routes.d.ts +2 -2
  194. package/dist/modules/people/routes.js +6 -6
  195. package/dist/modules/people/routes.js.map +1 -1
  196. package/dist/modules/people/schema.d.ts +17 -14
  197. package/dist/modules/people/schema.d.ts.map +1 -1
  198. package/dist/modules/people/schema.js +6 -4
  199. package/dist/modules/people/schema.js.map +1 -1
  200. package/dist/modules/people/service.d.ts +2 -2
  201. package/dist/modules/people/service.d.ts.map +1 -1
  202. package/dist/modules/people/service.js +5 -4
  203. package/dist/modules/people/service.js.map +1 -1
  204. package/dist/modules/plans/repository.d.ts +2 -2
  205. package/dist/modules/plans/repository.d.ts.map +1 -1
  206. package/dist/modules/plans/routes.d.ts +1 -0
  207. package/dist/modules/plans/routes.d.ts.map +1 -1
  208. package/dist/modules/plans/routes.js +4 -4
  209. package/dist/modules/plans/routes.js.map +1 -1
  210. package/dist/modules/plans/schema.d.ts +2 -2
  211. package/dist/modules/plans/schema.d.ts.map +1 -1
  212. package/dist/modules/plans/schema.js +3 -3
  213. package/dist/modules/plans/schema.js.map +1 -1
  214. package/dist/modules/plans/service.d.ts +2 -2
  215. package/dist/modules/plans/service.d.ts.map +1 -1
  216. package/dist/modules/plans/service.js.map +1 -1
  217. package/dist/modules/plans/tools.js +2 -2
  218. package/dist/modules/plans/tools.js.map +1 -1
  219. package/dist/modules/recordTargets.d.ts.map +1 -1
  220. package/dist/modules/recordTargets.js +11 -0
  221. package/dist/modules/recordTargets.js.map +1 -1
  222. package/dist/modules/references.d.ts.map +1 -1
  223. package/dist/modules/references.js +2 -0
  224. package/dist/modules/references.js.map +1 -1
  225. package/dist/modules/sample-data/fixture.d.ts +26 -4
  226. package/dist/modules/sample-data/fixture.d.ts.map +1 -1
  227. package/dist/modules/sample-data/fixture.js +157 -23
  228. package/dist/modules/sample-data/fixture.js.map +1 -1
  229. package/dist/modules/sample-data/index.d.ts +5 -2
  230. package/dist/modules/sample-data/index.d.ts.map +1 -1
  231. package/dist/modules/sample-data/index.js +7 -2
  232. package/dist/modules/sample-data/index.js.map +1 -1
  233. package/dist/modules/sample-data/routes.d.ts.map +1 -1
  234. package/dist/modules/sample-data/routes.js +2 -0
  235. package/dist/modules/sample-data/routes.js.map +1 -1
  236. package/dist/modules/sample-data/service.d.ts +11 -3
  237. package/dist/modules/sample-data/service.d.ts.map +1 -1
  238. package/dist/modules/sample-data/service.js +452 -363
  239. package/dist/modules/sample-data/service.js.map +1 -1
  240. package/dist/modules/sample-data/tools.d.ts.map +1 -1
  241. package/dist/modules/sample-data/tools.js +4 -3
  242. package/dist/modules/sample-data/tools.js.map +1 -1
  243. package/dist/modules/search/index.d.ts +1 -1
  244. package/dist/modules/search/index.d.ts.map +1 -1
  245. package/dist/modules/search/index.js +2 -1
  246. package/dist/modules/search/index.js.map +1 -1
  247. package/dist/modules/search/repository.d.ts +1 -0
  248. package/dist/modules/search/repository.d.ts.map +1 -1
  249. package/dist/modules/search/repository.js +22 -0
  250. package/dist/modules/search/repository.js.map +1 -1
  251. package/dist/modules/search/service.d.ts +2 -2
  252. package/dist/modules/search/service.d.ts.map +1 -1
  253. package/dist/modules/search/service.js +4 -3
  254. package/dist/modules/search/service.js.map +1 -1
  255. package/dist/modules/smtp-email/index.d.ts +12 -4
  256. package/dist/modules/smtp-email/index.d.ts.map +1 -1
  257. package/dist/modules/smtp-email/index.js +25 -5
  258. package/dist/modules/smtp-email/index.js.map +1 -1
  259. package/dist/modules/workspace/repository.d.ts +2 -0
  260. package/dist/modules/workspace/repository.d.ts.map +1 -1
  261. package/dist/modules/workspace/repository.js +12 -0
  262. package/dist/modules/workspace/repository.js.map +1 -1
  263. package/dist/modules/workspace/routes.d.ts.map +1 -1
  264. package/dist/modules/workspace/routes.js +13 -0
  265. package/dist/modules/workspace/routes.js.map +1 -1
  266. package/dist/modules/workspace/service.d.ts +10 -0
  267. package/dist/modules/workspace/service.d.ts.map +1 -1
  268. package/dist/modules/workspace/service.js +34 -10
  269. package/dist/modules/workspace/service.js.map +1 -1
  270. package/dist/modules/workspace/starters.d.ts +17 -2
  271. package/dist/modules/workspace/starters.d.ts.map +1 -1
  272. package/dist/modules/workspace/starters.js +141 -14
  273. package/dist/modules/workspace/starters.js.map +1 -1
  274. package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspaceAccessMiddleware.js +3 -4
  276. package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -1
  277. package/dist/runtime/events.d.ts +1 -1
  278. package/dist/runtime/events.d.ts.map +1 -1
  279. package/dist/runtime/events.js +2 -0
  280. package/dist/runtime/events.js.map +1 -1
  281. package/dist/runtime/module.d.ts +8 -0
  282. package/dist/runtime/module.d.ts.map +1 -1
  283. package/dist/runtime/registry.d.ts.map +1 -1
  284. package/dist/runtime/registry.js +102 -7
  285. package/dist/runtime/registry.js.map +1 -1
  286. package/dist/schema/index.d.ts +1 -0
  287. package/dist/schema/index.d.ts.map +1 -1
  288. package/dist/schema/index.js +1 -0
  289. package/dist/schema/index.js.map +1 -1
  290. package/dist/testing/app.d.ts +8 -0
  291. package/dist/testing/app.d.ts.map +1 -1
  292. package/dist/testing/app.js +6 -1
  293. package/dist/testing/app.js.map +1 -1
  294. package/dist/testing/client.d.ts.map +1 -1
  295. package/dist/testing/client.js +9 -1
  296. package/dist/testing/client.js.map +1 -1
  297. package/migrations/0044_events.sql +79 -0
  298. package/migrations/0045_addresses.sql +66 -0
  299. package/migrations/0046_drop_company_website.sql +66 -0
  300. package/migrations/meta/_journal.json +21 -0
  301. package/package.json +3 -3
  302. package/src/app.ts +25 -1
  303. package/src/boot.ts +1 -0
  304. package/src/lib/apiKeyScopes.ts +6 -0
  305. package/src/lib/columns.ts +12 -3
  306. package/src/lib/config.ts +21 -0
  307. package/src/lib/egress.ts +8 -4
  308. package/src/lib/errors.ts +16 -9
  309. package/src/lib/ids.ts +11 -0
  310. package/src/lib/kelpieConfigFile.ts +69 -1
  311. package/src/modules/agent-tasks/catalog.ts +33 -1
  312. package/src/modules/agent-tasks/index.ts +1 -0
  313. package/src/modules/agent-tasks/resolve.ts +11 -0
  314. package/src/modules/attachedRecords.ts +85 -13
  315. package/src/modules/auth/index.ts +1 -0
  316. package/src/modules/auth/repository.ts +34 -3
  317. package/src/modules/auth/routes.ts +44 -1
  318. package/src/modules/auth/service.ts +118 -4
  319. package/src/modules/companies/repository.ts +1 -0
  320. package/src/modules/companies/routes.ts +6 -11
  321. package/src/modules/companies/schema.ts +3 -3
  322. package/src/modules/companies/service.ts +6 -10
  323. package/src/modules/core.ts +2 -0
  324. package/src/modules/custom-fields/repository.ts +2 -0
  325. package/src/modules/dashboard/index.ts +1 -0
  326. package/src/modules/dashboard/repository.ts +64 -0
  327. package/src/modules/dashboard/routes.ts +15 -0
  328. package/src/modules/dashboard/service.ts +48 -0
  329. package/src/modules/decisions/repository.ts +2 -0
  330. package/src/modules/events/attendances.ts +245 -0
  331. package/src/modules/events/catalog.ts +36 -0
  332. package/src/modules/events/index.ts +54 -0
  333. package/src/modules/events/repository.ts +396 -0
  334. package/src/modules/events/routes.ts +405 -0
  335. package/src/modules/events/schema.ts +148 -0
  336. package/src/modules/events/service.ts +443 -0
  337. package/src/modules/events/tools.ts +118 -0
  338. package/src/modules/forms/applySubmissionFields.ts +7 -5
  339. package/src/modules/forms/fillAddresses.ts +185 -0
  340. package/src/modules/forms/repository.ts +3 -3
  341. package/src/modules/forms/routes.ts +2 -2
  342. package/src/modules/forms/schema.ts +2 -2
  343. package/src/modules/forms/service.ts +2 -2
  344. package/src/modules/forms/submission.ts +34 -4
  345. package/src/modules/hiring/roles.ts +2 -1
  346. package/src/modules/import-export/addresses.ts +150 -0
  347. package/src/modules/import-export/drafts.ts +7 -9
  348. package/src/modules/import-export/exportRows.ts +3 -3
  349. package/src/modules/import-export/plan.ts +30 -2
  350. package/src/modules/import-export/presets.ts +55 -7
  351. package/src/modules/import-export/repository.ts +5 -7
  352. package/src/modules/import-export/writes.ts +8 -4
  353. package/src/modules/people/repository.ts +1 -0
  354. package/src/modules/people/routes.ts +6 -6
  355. package/src/modules/people/schema.ts +7 -5
  356. package/src/modules/people/service.ts +7 -5
  357. package/src/modules/plans/repository.ts +2 -2
  358. package/src/modules/plans/routes.ts +6 -6
  359. package/src/modules/plans/schema.ts +4 -4
  360. package/src/modules/plans/service.ts +3 -3
  361. package/src/modules/plans/tools.ts +2 -2
  362. package/src/modules/recordTargets.ts +14 -0
  363. package/src/modules/references.ts +2 -0
  364. package/src/modules/sample-data/fixture.ts +208 -27
  365. package/src/modules/sample-data/index.ts +7 -2
  366. package/src/modules/sample-data/routes.ts +2 -0
  367. package/src/modules/sample-data/service.ts +166 -49
  368. package/src/modules/sample-data/tools.ts +4 -3
  369. package/src/modules/search/index.ts +2 -1
  370. package/src/modules/search/repository.ts +32 -1
  371. package/src/modules/search/service.ts +6 -4
  372. package/src/modules/smtp-email/index.ts +32 -10
  373. package/src/modules/workspace/repository.ts +18 -0
  374. package/src/modules/workspace/routes.ts +16 -0
  375. package/src/modules/workspace/service.ts +59 -14
  376. package/src/modules/workspace/starters.ts +275 -14
  377. package/src/modules/workspace/workspaceAccessMiddleware.ts +3 -4
  378. package/src/runtime/events.ts +2 -0
  379. package/src/runtime/module.ts +8 -0
  380. package/src/runtime/registry.ts +122 -8
  381. package/src/schema/index.ts +1 -0
  382. package/src/testing/app.ts +14 -1
  383. package/src/testing/client.ts +11 -1
@@ -21,10 +21,12 @@ import { seedStarterForms } from './seedStarterForms.ts'
21
21
  import { parseInvitableRole, parseMemberRole, roleAllows } from './roles.ts'
22
22
  import type { InvitableRole, InviteStatus, MemberRole } from './roles.ts'
23
23
  import {
24
- STARTER_HANDBOOK_PAGES,
24
+ getHandbookPagesForTemplate,
25
+ isStarterHandbookBody,
25
26
  STARTER_PIPELINE_STAGES,
26
27
  starterHandbookBody,
27
28
  } from './starters.ts'
29
+ import type { HandbookTemplateId } from './starters.ts'
28
30
 
29
31
  /**
30
32
  * Workspaces and who belongs to them.
@@ -89,6 +91,15 @@ export interface CreateWorkspaceInput {
89
91
  readonly timezone: string
90
92
  }
91
93
 
94
+ export interface SeedHandbookInput {
95
+ readonly handbookTemplate: HandbookTemplateId
96
+ readonly replace?: boolean | undefined
97
+ }
98
+
99
+ export interface SeedHandbookResult {
100
+ readonly handbookPages: number
101
+ }
102
+
92
103
  /**
93
104
  * A partial update, on `api.md`'s rule: an absent field is not being changed,
94
105
  * and `null` clears one.
@@ -108,6 +119,8 @@ export interface UpdateWorkspaceInput {
108
119
  export interface WorkspaceService {
109
120
  /** Creates the workspace, makes the caller its owner, and seeds its starters. */
110
121
  create(actor: SessionActor, input: CreateWorkspaceInput): Promise<WorkspaceView>
122
+ /** Seeds starter handbook pages for the chosen organisation template. */
123
+ seedHandbook(actor: Actor, workspaceId: string, input: SeedHandbookInput): Promise<SeedHandbookResult>
111
124
  get(actor: Actor, workspaceId: string): Promise<WorkspaceView>
112
125
  update(actor: Actor, workspaceId: string, changes: UpdateWorkspaceInput): Promise<WorkspaceView>
113
126
  /**
@@ -439,19 +452,6 @@ export function createWorkspaceService(dependencies: WorkspaceDependencies): Wor
439
452
  joinedAt: now,
440
453
  })
441
454
 
442
- await repository.insertHandbookPages(
443
- tx,
444
- STARTER_HANDBOOK_PAGES.map((page, index) => ({
445
- id: dependencies.createId('handbookPage'),
446
- workspaceId,
447
- title: page.title,
448
- slug: page.slug,
449
- sortOrder: index,
450
- body: starterHandbookBody(page.title),
451
- updatedBy: memberId,
452
- })),
453
- )
454
-
455
455
  await repository.insertPipelineStages(
456
456
  tx,
457
457
  Object.entries(STARTER_PIPELINE_STAGES).flatMap(([kind, stages]) =>
@@ -511,6 +511,51 @@ export function createWorkspaceService(dependencies: WorkspaceDependencies): Wor
511
511
  }, { workspaceId, actor: toEventActor(actor) })
512
512
  },
513
513
 
514
+ async seedHandbook(actor, workspaceId, input) {
515
+ const { memberId } = await requireMembership(actor, workspaceId, 'admin')
516
+
517
+ if (memberId === null) {
518
+ throw new AppError('forbidden', 'This action needs a workspace member')
519
+ }
520
+
521
+ const existing = await repository.listHandbookPages(dependencies.db, workspaceId)
522
+
523
+ if (existing.length > 0) {
524
+ if (input.replace !== true) {
525
+ throw AppError.conflict('This workspace already has handbook pages', [
526
+ { field: 'replace', message: 'Set replace to true to replace starter pages' },
527
+ ])
528
+ }
529
+
530
+ if (!existing.every((page) => isStarterHandbookBody(page))) {
531
+ throw AppError.conflict('Handbook pages have been edited and cannot be replaced')
532
+ }
533
+ }
534
+
535
+ const templatePages = getHandbookPagesForTemplate(input.handbookTemplate)
536
+
537
+ return dependencies.transaction(async ({ tx }) => {
538
+ if (existing.length > 0) {
539
+ await repository.deleteAllHandbookPages(tx, workspaceId)
540
+ }
541
+
542
+ await repository.insertHandbookPages(
543
+ tx,
544
+ templatePages.map((page, index) => ({
545
+ id: dependencies.createId('handbookPage'),
546
+ workspaceId,
547
+ title: page.title,
548
+ slug: page.slug,
549
+ sortOrder: index,
550
+ body: starterHandbookBody(page),
551
+ updatedBy: memberId,
552
+ })),
553
+ )
554
+
555
+ return { handbookPages: templatePages.length }
556
+ })
557
+ },
558
+
514
559
  async get(actor, workspaceId) {
515
560
  await requireMembership(actor, workspaceId, 'member')
516
561
  const workspace = await repository.findWorkspace(dependencies.db, workspaceId)
@@ -6,27 +6,288 @@
6
6
  * owner can then rename, reorder, or delete.
7
7
  */
8
8
 
9
+ export const HANDBOOK_TEMPLATE_IDS = [
10
+ 'startup',
11
+ 'agency',
12
+ 'nonprofit',
13
+ 'community',
14
+ 'professional-services',
15
+ 'creator',
16
+ ] as const
17
+
18
+ export type HandbookTemplateId = (typeof HANDBOOK_TEMPLATE_IDS)[number]
19
+
9
20
  export interface StarterHandbookPage {
10
21
  readonly title: string
11
22
  readonly slug: string
23
+ readonly bodyPrompt: string
24
+ }
25
+
26
+ export interface HandbookTemplate {
27
+ readonly id: HandbookTemplateId
28
+ readonly label: string
29
+ readonly description: string
30
+ readonly pages: readonly StarterHandbookPage[]
31
+ }
32
+
33
+ function page(title: string, slug: string, bodyPrompt: string): StarterHandbookPage {
34
+ return { title, slug, bodyPrompt }
12
35
  }
13
36
 
14
- /** Bodies start as stubs. Teams write them; agents read whatever is there. */
15
- export const STARTER_HANDBOOK_PAGES: readonly StarterHandbookPage[] = [
16
- { title: 'About us', slug: 'about-us' },
17
- { title: 'Product', slug: 'product' },
18
- { title: 'Ideal customer profile', slug: 'ideal-customer-profile' },
19
- { title: 'Voice and tone', slug: 'voice-and-tone' },
20
- { title: 'Pricing', slug: 'pricing' },
21
- { title: 'How we sell', slug: 'how-we-sell' },
22
- { title: 'Competitive landscape', slug: 'competitive-landscape' },
23
- { title: 'Team and roles', slug: 'team-and-roles' },
24
- { title: 'Tools and stack', slug: 'tools-and-stack' },
25
- { title: 'Agent FAQ', slug: 'agent-faq' },
37
+ const STARTUP_PAGES: readonly StarterHandbookPage[] = [
38
+ page('About us', 'about-us', 'What does your company do, and why does it exist?'),
39
+ page('Product', 'product', 'What do you sell, and what problem does it solve?'),
40
+ page(
41
+ 'Ideal customer profile',
42
+ 'ideal-customer-profile',
43
+ 'Who is your best-fit customer? Size, industry, pain points, and buying signals.',
44
+ ),
45
+ page('Voice and tone', 'voice-and-tone', 'How should Kelpie and your agents sound when they write on your behalf?'),
46
+ page('Pricing', 'pricing', 'How is your product priced? Plans, tiers, and what is included.'),
47
+ page('How we sell', 'how-we-sell', 'Your sales motion: inbound, outbound, trials, demos, and typical cycle.'),
48
+ page(
49
+ 'Competitive landscape',
50
+ 'competitive-landscape',
51
+ 'Who else solves this problem, and why do customers choose you over them?',
52
+ ),
53
+ page('Team and roles', 'team-and-roles', 'Who does what, and who owns which relationships or decisions?'),
54
+ page('Tools and stack', 'tools-and-stack', 'The systems your team and agents should know about.'),
55
+ page(
56
+ 'Agent FAQ',
57
+ 'agent-faq',
58
+ 'Standing rules for agents: what they may promise, what they must escalate, and what never to invent.',
59
+ ),
60
+ ]
61
+
62
+ const AGENCY_PAGES: readonly StarterHandbookPage[] = [
63
+ page('About us', 'about-us', 'What does your agency do, and what makes your team distinctive?'),
64
+ page(
65
+ 'Services & capabilities',
66
+ 'product',
67
+ 'What services do you offer, and what kinds of clients or projects fit best?',
68
+ ),
69
+ page(
70
+ 'Ideal client profile',
71
+ 'ideal-customer-profile',
72
+ 'Who is your best-fit client? Size, sector, budget, and the problems you solve best.',
73
+ ),
74
+ page('Voice and tone', 'voice-and-tone', 'How should Kelpie and your agents sound in client-facing writing?'),
75
+ page(
76
+ 'Pricing & engagement models',
77
+ 'pricing',
78
+ 'How you price work: retainers, fixed bids, day rates, and typical scopes.',
79
+ ),
80
+ page('How we win work', 'how-we-sell', 'How you find and close clients: referrals, pitches, RFPs, and follow-up.'),
81
+ page(
82
+ 'Competitive landscape',
83
+ 'competitive-landscape',
84
+ 'Who you compete with, and why clients choose you over alternatives.',
85
+ ),
86
+ page(
87
+ 'Case studies & proof points',
88
+ 'case-studies',
89
+ 'Outcomes you can cite: client wins, metrics, and stories agents may reference.',
90
+ ),
91
+ page(
92
+ 'How we deliver work',
93
+ 'delivery-process',
94
+ 'How a project runs from kickoff to handoff: phases, checkpoints, and client touchpoints.',
95
+ ),
96
+ page('Team and roles', 'team-and-roles', 'Who leads delivery, sales, and client relationships?'),
97
+ page('Tools and stack', 'tools-and-stack', 'The systems your team and agents should know about.'),
98
+ page(
99
+ 'Agent FAQ',
100
+ 'agent-faq',
101
+ 'Standing rules for agents: what they may promise, what they must escalate, and what never to invent.',
102
+ ),
103
+ ]
104
+
105
+ const NONPROFIT_PAGES: readonly StarterHandbookPage[] = [
106
+ page('About us & mission', 'about-us', 'Your mission, vision, and the change you exist to make.'),
107
+ page(
108
+ 'Programs & impact',
109
+ 'product',
110
+ 'What programs or services you run, and what outcomes they aim for.',
111
+ ),
112
+ page(
113
+ 'Who we serve',
114
+ 'ideal-customer-profile',
115
+ 'The communities, beneficiaries, or stakeholders your programs exist for.',
116
+ ),
117
+ page('Voice and tone', 'voice-and-tone', 'How should Kelpie and your agents sound when they write on your behalf?'),
118
+ page(
119
+ 'How we engage supporters',
120
+ 'how-we-sell',
121
+ 'How you build relationships with donors, volunteers, partners, and the public.',
122
+ ),
123
+ page(
124
+ 'Funding & donations',
125
+ 'funding-and-donations',
126
+ 'Revenue streams: donations, grants, memberships, and major-gift approaches.',
127
+ ),
128
+ page('Impact & outcomes', 'impact-metrics', 'How you measure success: outputs, outcomes, and stories you can cite.'),
129
+ page(
130
+ 'Grant applications',
131
+ 'grant-applications',
132
+ 'How you find, qualify, and apply for grants; common requirements and deadlines.',
133
+ ),
134
+ page('Team and roles', 'team-and-roles', 'Who does what, and who owns programs, fundraising, and comms?'),
135
+ page('Tools and stack', 'tools-and-stack', 'The systems your team and agents should know about.'),
136
+ page(
137
+ 'Agent FAQ',
138
+ 'agent-faq',
139
+ 'Standing rules for agents: what they may promise, what they must escalate, and what never to invent.',
140
+ ),
141
+ ]
142
+
143
+ const COMMUNITY_PAGES: readonly StarterHandbookPage[] = [
144
+ page('About us', 'about-us', 'What your organisation is, its purpose, and who it serves.'),
145
+ page('What we offer', 'product', 'What members get: benefits, services, events, and community value.'),
146
+ page(
147
+ 'Ideal member profile',
148
+ 'ideal-customer-profile',
149
+ 'Who should join, and what makes someone a good fit for your community?',
150
+ ),
151
+ page('Voice and tone', 'voice-and-tone', 'How should Kelpie and your agents sound when they write on your behalf?'),
152
+ page('Membership & fees', 'pricing', 'Membership tiers, fees, and what each level includes.'),
153
+ page(
154
+ 'How we grow membership',
155
+ 'how-we-sell',
156
+ 'How you attract and retain members: outreach, events, referrals, and renewals.',
157
+ ),
158
+ page('Membership benefits', 'membership-benefits', 'What members receive: access, discounts, resources, and perks.'),
159
+ page(
160
+ 'Events & programming',
161
+ 'events-and-programming',
162
+ 'Regular events, programs, and how members can participate.',
163
+ ),
164
+ page('Team and roles', 'team-and-roles', 'Who runs the organisation, and who owns membership and events?'),
165
+ page('Tools and stack', 'tools-and-stack', 'The systems your team and agents should know about.'),
166
+ page(
167
+ 'Agent FAQ',
168
+ 'agent-faq',
169
+ 'Standing rules for agents: what they may promise, what they must escalate, and what never to invent.',
170
+ ),
26
171
  ]
27
172
 
28
- export function starterHandbookBody(title: string): string {
29
- return `# ${title}\n\nWrite this page.`
173
+ const PROFESSIONAL_SERVICES_PAGES: readonly StarterHandbookPage[] = [
174
+ page('About the practice', 'about-us', 'What your practice does, its history, and what distinguishes it.'),
175
+ page('Practice areas', 'product', 'The services and matters you handle, and what you do not take on.'),
176
+ page(
177
+ 'Ideal client profile',
178
+ 'ideal-customer-profile',
179
+ 'Who your best-fit clients are: size, sector, matter types, and engagement style.',
180
+ ),
181
+ page('Voice and tone', 'voice-and-tone', 'How should Kelpie and your agents sound in client-facing writing?'),
182
+ page('Fee structures', 'pricing', 'How you charge: hourly, fixed, value-based, and typical ranges.'),
183
+ page(
184
+ 'Business development',
185
+ 'how-we-sell',
186
+ 'How you win clients: referrals, networking, pitches, and follow-up.',
187
+ ),
188
+ page('Market position', 'competitive-landscape', 'How you compare to peer firms and why clients choose you.'),
189
+ page(
190
+ 'Conflicts & ethics',
191
+ 'conflicts-and-ethics',
192
+ 'Conflict checks, ethical boundaries, and what agents must never assume or promise.',
193
+ ),
194
+ page('Team and roles', 'team-and-roles', 'Partners, associates, and who owns client relationships.'),
195
+ page('Tools and stack', 'tools-and-stack', 'The systems your team and agents should know about.'),
196
+ page(
197
+ 'Agent FAQ',
198
+ 'agent-faq',
199
+ 'Standing rules for agents: what they may promise, what they must escalate, and what never to invent.',
200
+ ),
201
+ ]
202
+
203
+ const CREATOR_PAGES: readonly StarterHandbookPage[] = [
204
+ page('About me', 'about-us', 'Who you are, what you create, and what your brand stands for.'),
205
+ page('What I create', 'product', 'Your content, products, or services, and what makes them distinctive.'),
206
+ page(
207
+ 'Audience profile',
208
+ 'ideal-customer-profile',
209
+ 'Who follows you, who buys from you, and what they care about.',
210
+ ),
211
+ page('Voice and tone', 'voice-and-tone', 'How you sound across content, email, and brand partnerships.'),
212
+ page('Rates & packages', 'pricing', 'What you charge: sponsorship rates, consulting packages, and deliverables.'),
213
+ page(
214
+ 'How I work with clients',
215
+ 'how-we-sell',
216
+ 'How inbound leads become paid work: discovery, scope, contracts, and delivery.',
217
+ ),
218
+ page(
219
+ 'Brand partnerships & sponsorships',
220
+ 'brand-partnerships',
221
+ 'What you offer sponsors, your rates, and brands you will or will not work with.',
222
+ ),
223
+ page('Tools and stack', 'tools-and-stack', 'The systems you and your agents should know about.'),
224
+ page(
225
+ 'Agent FAQ',
226
+ 'agent-faq',
227
+ 'Standing rules for agents: what they may promise, what they must escalate, and what never to invent.',
228
+ ),
229
+ ]
230
+
231
+ export const HANDBOOK_TEMPLATES: Readonly<Record<HandbookTemplateId, HandbookTemplate>> = {
232
+ startup: {
233
+ id: 'startup',
234
+ label: 'Startup',
235
+ description: 'Product company, SaaS, or tech startup selling software or services.',
236
+ pages: STARTUP_PAGES,
237
+ },
238
+ agency: {
239
+ id: 'agency',
240
+ label: 'Agency',
241
+ description: 'Consultancy, design shop, dev shop, or marketing agency.',
242
+ pages: AGENCY_PAGES,
243
+ },
244
+ nonprofit: {
245
+ id: 'nonprofit',
246
+ label: 'Nonprofit',
247
+ description: 'Charity, foundation, or social enterprise.',
248
+ pages: NONPROFIT_PAGES,
249
+ },
250
+ community: {
251
+ id: 'community',
252
+ label: 'Community',
253
+ description: 'Club, association, co-op, or membership organisation.',
254
+ pages: COMMUNITY_PAGES,
255
+ },
256
+ 'professional-services': {
257
+ id: 'professional-services',
258
+ label: 'Professional services',
259
+ description: 'Law, accounting, architecture, or advisory practice.',
260
+ pages: PROFESSIONAL_SERVICES_PAGES,
261
+ },
262
+ creator: {
263
+ id: 'creator',
264
+ label: 'Creator',
265
+ description: 'Solo creator, freelancer, or personal brand.',
266
+ pages: CREATOR_PAGES,
267
+ },
268
+ }
269
+
270
+ /** Default startup template pages — same list `onboarding.md` names. */
271
+ export const STARTER_HANDBOOK_PAGES: readonly StarterHandbookPage[] = STARTUP_PAGES
272
+
273
+ export function isStarterHandbookBody(page: { readonly slug: string; readonly body: string }): boolean {
274
+ for (const template of Object.values(HANDBOOK_TEMPLATES)) {
275
+ for (const def of template.pages) {
276
+ if (def.slug === page.slug && starterHandbookBody(def) === page.body) {
277
+ return true
278
+ }
279
+ }
280
+ }
281
+
282
+ return false
283
+ }
284
+
285
+ export function getHandbookPagesForTemplate(templateId: HandbookTemplateId): readonly StarterHandbookPage[] {
286
+ return HANDBOOK_TEMPLATES[templateId].pages
287
+ }
288
+
289
+ export function starterHandbookBody(page: StarterHandbookPage): string {
290
+ return `# ${page.title}\n\n${page.bodyPrompt}`
30
291
  }
31
292
 
32
293
  export interface StarterStage {
@@ -23,10 +23,9 @@ import { WORKSPACE_ACCESS } from './capabilities.ts'
23
23
  *
24
24
  * Exempts `/v1/auth/*` and `/v1/account*`: they manage the *account*, not
25
25
  * the workspace, and a member of a workspace this gate has locked out
26
- * still needs to sign in, sign out, and see their own account to have any
27
- * chance of understanding why. There is no `/v1` endpoint to switch a
28
- * session to a different workspace membership a session is bound to one
29
- * at login — so there is nothing further to exempt for that case.
26
+ * still needs to sign in, sign out, see their own account, list the
27
+ * workspaces they belong to (`GET /v1/auth/workspaces`), and switch this
28
+ * session to a different membership (`POST /v1/auth/workspace`).
30
29
  *
31
30
  * Also exempts `DELETE /v1/workspaces/:id` itself, and only that exact
32
31
  * path: a suspension must never trap an owner into a workspace they can no
@@ -35,6 +35,8 @@ export const RECORD_OBJECT_TYPES = [
35
35
  'raise',
36
36
  'role',
37
37
  'candidate',
38
+ 'event',
39
+ 'attendance',
38
40
  'handbook_page',
39
41
  'form',
40
42
  ] as const
@@ -133,6 +133,14 @@ export interface ModuleServices {
133
133
  * `secretEncryption`. Same fallback pattern as `appBaseUrl`.
134
134
  */
135
135
  readonly secretEncryption?: SecretEncryptionConfig | undefined
136
+ /**
137
+ * Whether the instance accepts a brand-new account, from `kelpie.config.ts`'s
138
+ * `signups` (or `SIGNUPS`). Undefined defaults to `true` (open), the same
139
+ * default `loadConfig` and `resolveKelpieConfig` use, so a caller that has
140
+ * not thought about it — a test, an older assembly — sees no change in
141
+ * behaviour.
142
+ */
143
+ readonly signupsEnabled?: boolean
136
144
  }
137
145
 
138
146
  export interface ModuleContext extends ModuleServices {
@@ -87,6 +87,117 @@ function assertMountablePath(moduleId: string, kind: string, path: string): void
87
87
  }
88
88
  }
89
89
 
90
+ /** Verb methods a module can call on the router — full Hono set. */
91
+ const ROUTER_VERB_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'all'] as const
92
+ type RouterVerbMethod = (typeof ROUTER_VERB_METHODS)[number]
93
+
94
+ function isRouterVerbMethod(name: string): name is RouterVerbMethod {
95
+ return (ROUTER_VERB_METHODS as readonly string[]).includes(name)
96
+ }
97
+
98
+ /**
99
+ * Wraps a Hono router so every route the module registers runs `gate` first,
100
+ * and only that module's own routes. The straightforward alternative —
101
+ * `router.use('*', gate)` — attaches at `/v1/*` on the app once the router is
102
+ * mounted (Hono flattens sub-routers onto the parent prefix), so the gate
103
+ * fires for every module's routes, not just this module's. Prepending the gate
104
+ * to each declared route keeps the check local.
105
+ *
106
+ * The full router surface is covered so a future contributor cannot silently
107
+ * reintroduce the leak by reaching for a method that was not wrapped:
108
+ *
109
+ * - HTTP verbs and `on`: prepend `gate`, then the module's handlers.
110
+ * - `use(pattern, ...)` at a specific path: prepend `gate`, then the mw.
111
+ * - `route(subPath, subApp)`: recursively wrap `subApp` before mounting, so
112
+ * every route it contributes still runs the gate first.
113
+ *
114
+ * And the calls that cannot be made module-local through wrapping fail at
115
+ * boot with a message naming the module, rather than shipping a subtle leak:
116
+ *
117
+ * - `use()` with no path, `use('*')`, `use('/*')`, `use('/')`: any of these
118
+ * register a middleware at `/v1/*` on the parent app once the router is
119
+ * mounted — the exact bug this wrapper prevents. A module that wants to
120
+ * share middleware across many of its routes must name a specific path
121
+ * (`/things/*`) or attach the middleware per route.
122
+ * - `notFound` / `onError`: fire for every unmatched or failing `/v1`
123
+ * request once the router is flattened, so a module's handler would run
124
+ * for other modules' traffic. Handle failures inside the module's own
125
+ * route handlers instead.
126
+ */
127
+ function gateRouter(router: Hono, moduleId: string, gate: MiddlewareHandler): Hono {
128
+ const proxy: Hono = new Proxy(router, {
129
+ get(target, property, receiver) {
130
+ if (typeof property !== 'string') {
131
+ return Reflect.get(target, property, receiver)
132
+ }
133
+
134
+ if (isRouterVerbMethod(property)) {
135
+ return (path: string, ...handlers: Handler[]): Hono => {
136
+ const method = target[property] as (path: string, ...handlers: Handler[]) => Hono
137
+ method.call(target, path, gate, ...handlers)
138
+ return proxy
139
+ }
140
+ }
141
+
142
+ if (property === 'on') {
143
+ return (
144
+ method: string | string[],
145
+ path: string | string[],
146
+ ...handlers: Handler[]
147
+ ): Hono => {
148
+ target.on(method as never, path as never, gate, ...handlers)
149
+ return proxy
150
+ }
151
+ }
152
+
153
+ if (property === 'use') {
154
+ return (...args: unknown[]): Hono => {
155
+ const first = args[0]
156
+ const isAppWide =
157
+ typeof first !== 'string' || first === '*' || first === '/*' || first === '/'
158
+
159
+ if (isAppWide) {
160
+ throw new ModuleBootError([
161
+ `module "${moduleId}" called router.use() with an app-wide pattern (${
162
+ typeof first === 'string' ? `"${first}"` : 'no path'
163
+ }). Hono flattens the module router onto "/v1", so this would fire for every module's routes. Name a specific path ("/things/*") or attach middleware per route.`,
164
+ ])
165
+ }
166
+
167
+ const [pattern, ...handlers] = args as [string, ...MiddlewareHandler[]]
168
+ target.use(pattern, gate, ...handlers)
169
+ return proxy
170
+ }
171
+ }
172
+
173
+ if (property === 'route') {
174
+ return (path: string, subApp: Hono): Hono => {
175
+ // The sub-app's routes were registered on it before this call, so a
176
+ // proxy over `subApp` cannot retroactively prepend the gate to them.
177
+ // Instead attach the gate as middleware scoped to the sub-router's
178
+ // mount prefix on this router — once flattened onto `/v1`, the
179
+ // middleware covers exactly `/v1${path}/*`, which is the module's
180
+ // own routes. `use` must land before `route` so composition order
181
+ // runs the gate first.
182
+ target.use(`${path.replace(/\/+$/, '')}/*`, gate)
183
+ target.route(path, subApp)
184
+ return proxy
185
+ }
186
+ }
187
+
188
+ if (property === 'notFound' || property === 'onError') {
189
+ throw new ModuleBootError([
190
+ `module "${moduleId}" called router.${property}(). A module router's ${property} fires for every unmatched or failing "/v1" request once the router is mounted; handle failures inside the module's own route handlers instead.`,
191
+ ])
192
+ }
193
+
194
+ return Reflect.get(target, property, receiver)
195
+ },
196
+ }) as Hono
197
+
198
+ return proxy
199
+ }
200
+
90
201
  /**
91
202
  * What `registerModules` needs.
92
203
  *
@@ -282,22 +393,25 @@ function createModuleContext(
282
393
  routes(mount) {
283
394
  const router = new Hono()
284
395
 
285
- // Gate first, so it runs before anything `mount` adds: Hono composes a
286
- // path's handlers in the order they were registered on this router,
287
- // middleware included, and registering after the routes it should guard
288
- // would run it too late to block them.
396
+ // Gate must run before anything `mount` adds. A `router.use('*', gate)`
397
+ // here would attach at `/v1/*` on the app after mounting (Hono flattens
398
+ // the sub-router onto the parent prefix), which would fire the gate for
399
+ // every module's routes, not just this one's. So the gate is prepended
400
+ // to each individual route through `gateRouter` instead.
289
401
  if (module.structural !== true && options.resolveActor !== undefined) {
290
402
  const { resolveActor } = options
291
-
292
- router.use('*', async (context, next) => {
403
+ const gate: MiddlewareHandler = async (context, next) => {
293
404
  const actor = await resolveActor(context)
294
405
 
295
406
  await requireCapability(entitlements, requireWorkspaceId(actor), moduleCapabilityName(module.id))
296
407
  await next()
297
- })
408
+ }
409
+
410
+ mount(gateRouter(router, module.id, gate))
411
+ } else {
412
+ mount(router)
298
413
  }
299
414
 
300
- mount(router)
301
415
  accumulator.routers.push({ moduleId: module.id, router })
302
416
  },
303
417
 
@@ -20,6 +20,7 @@ export * from '../modules/opportunities/schema.ts'
20
20
  export * from '../modules/partnerships/schema.ts'
21
21
  export * from '../modules/raises/schema.ts'
22
22
  export * from '../modules/hiring/schema.ts'
23
+ export * from '../modules/events/schema.ts'
23
24
  export * from '../modules/plans/schema.ts'
24
25
  export * from '../modules/decisions/schema.ts'
25
26
  export * from '../modules/notes/schema.ts'
@@ -1,3 +1,4 @@
1
+ import type { PublicRegion } from '@kelpie/schemas'
1
2
  import type { Context, Hono } from 'hono'
2
3
 
3
4
  import { createApp } from '../app.ts'
@@ -61,6 +62,13 @@ export interface TestAppOptions {
61
62
  readonly runtimeMode?: RuntimeMode
62
63
  /** Reported through `GET /v1/public/config`. Defaults to unset. */
63
64
  readonly siteName?: string
65
+ /**
66
+ * Reported through `GET /v1/public/config` and consulted by the `auth`
67
+ * module's `signUp` and `completeExternalSignIn`. Defaults to `true` (open).
68
+ */
69
+ readonly signupsEnabled?: boolean
70
+ /** Reported through `GET /v1/public/config`. Defaults to `[]`. */
71
+ readonly regions?: readonly PublicRegion[]
64
72
  }
65
73
 
66
74
  export interface TestApp {
@@ -94,7 +102,10 @@ export async function createTestApp(options: TestAppOptions = {}): Promise<TestA
94
102
  ...(options.entitlements === undefined ? {} : { entitlements: options.entitlements }),
95
103
  ...(options.moduleConfig === undefined ? {} : { moduleConfig: options.moduleConfig }),
96
104
  ...(options.resolveActor === undefined ? {} : { resolveActor: options.resolveActor }),
97
- services,
105
+ services: {
106
+ ...services,
107
+ ...(options.signupsEnabled === undefined ? {} : { signupsEnabled: options.signupsEnabled }),
108
+ },
98
109
  })
99
110
 
100
111
  const app = createApp({
@@ -108,6 +119,8 @@ export async function createTestApp(options: TestAppOptions = {}): Promise<TestA
108
119
  resolveClientIp: options.resolveClientIp ?? testClientIp,
109
120
  ...(options.runtimeMode === undefined ? {} : { runtimeMode: options.runtimeMode }),
110
121
  ...(options.siteName === undefined ? {} : { siteName: options.siteName }),
122
+ ...(options.signupsEnabled === undefined ? {} : { signupsEnabled: options.signupsEnabled }),
123
+ ...(options.regions === undefined ? {} : { regions: options.regions }),
111
124
  })
112
125
 
113
126
  return { app, contributions, logLines, services }
@@ -132,7 +132,17 @@ export function createTestClient(app: Hono<AppBindings>, db: Database): TestClie
132
132
  throw new Error(`Creating workspace ${slug} answered ${String(created.status)}`)
133
133
  }
134
134
 
135
- return { cookie, workspaceId: readString(await created.json(), 'id') }
135
+ const workspaceId = readString(await created.json(), 'id')
136
+ const seeded = await send('POST', `/v1/workspaces/${workspaceId}/handbook/seed`, {
137
+ body: { handbook_template: 'startup' },
138
+ cookie,
139
+ })
140
+
141
+ if (seeded.status !== 201) {
142
+ throw new Error(`Seeding handbook for ${slug} answered ${String(seeded.status)}`)
143
+ }
144
+
145
+ return { cookie, workspaceId }
136
146
  },
137
147
  }
138
148
  }