@cosmicdrift/kumiko-bundled-features 0.1.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 (333) hide show
  1. package/package.json +90 -0
  2. package/src/audit/__tests__/audit.integration.ts +328 -0
  3. package/src/audit/constants.ts +7 -0
  4. package/src/audit/feature.ts +23 -0
  5. package/src/audit/handlers/list.query.ts +98 -0
  6. package/src/audit/index.ts +2 -0
  7. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
  8. package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
  9. package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
  10. package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
  11. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
  12. package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
  13. package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
  14. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
  15. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
  16. package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
  17. package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
  18. package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
  19. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
  20. package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
  21. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
  22. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
  23. package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
  24. package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
  25. package/src/auth-email-password/auth-user-row.ts +41 -0
  26. package/src/auth-email-password/constants.ts +101 -0
  27. package/src/auth-email-password/email-templates.ts +283 -0
  28. package/src/auth-email-password/feature.ts +140 -0
  29. package/src/auth-email-password/handlers/change-password.write.ts +58 -0
  30. package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
  31. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
  32. package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
  33. package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
  34. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
  35. package/src/auth-email-password/handlers/login.write.ts +208 -0
  36. package/src/auth-email-password/handlers/logout.write.ts +12 -0
  37. package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
  38. package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
  39. package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
  40. package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
  41. package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
  42. package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
  43. package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
  44. package/src/auth-email-password/i18n.ts +211 -0
  45. package/src/auth-email-password/identity-v3-hash.ts +97 -0
  46. package/src/auth-email-password/index.ts +35 -0
  47. package/src/auth-email-password/invite-token-store.ts +92 -0
  48. package/src/auth-email-password/lockout-store.ts +118 -0
  49. package/src/auth-email-password/password-hashing.ts +43 -0
  50. package/src/auth-email-password/reset-token.ts +28 -0
  51. package/src/auth-email-password/seeding.ts +183 -0
  52. package/src/auth-email-password/signed-token.ts +85 -0
  53. package/src/auth-email-password/signup-token-store.ts +104 -0
  54. package/src/auth-email-password/stream-tenant.ts +31 -0
  55. package/src/auth-email-password/testing.ts +5 -0
  56. package/src/auth-email-password/token-burn-store.ts +57 -0
  57. package/src/auth-email-password/verification-token.ts +27 -0
  58. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
  59. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
  60. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
  61. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
  62. package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
  63. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
  64. package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
  65. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
  66. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
  67. package/src/auth-email-password/web/auth-client.ts +350 -0
  68. package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
  69. package/src/auth-email-password/web/auth-gate.tsx +33 -0
  70. package/src/auth-email-password/web/client-plugin.ts +48 -0
  71. package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
  72. package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
  73. package/src/auth-email-password/web/index.ts +56 -0
  74. package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
  75. package/src/auth-email-password/web/login-screen.tsx +150 -0
  76. package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
  77. package/src/auth-email-password/web/session.tsx +171 -0
  78. package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
  79. package/src/auth-email-password/web/signup-screen.tsx +130 -0
  80. package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
  81. package/src/auth-email-password/web/use-shell-user.ts +34 -0
  82. package/src/auth-email-password/web/user-menu.tsx +89 -0
  83. package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
  84. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
  85. package/src/billing-foundation/__tests__/feature.test.ts +110 -0
  86. package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
  87. package/src/billing-foundation/aggregate-id.ts +21 -0
  88. package/src/billing-foundation/constants.ts +70 -0
  89. package/src/billing-foundation/entities.ts +50 -0
  90. package/src/billing-foundation/events.ts +71 -0
  91. package/src/billing-foundation/feature.ts +122 -0
  92. package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
  93. package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
  94. package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
  95. package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
  96. package/src/billing-foundation/handlers/process-event.write.ts +160 -0
  97. package/src/billing-foundation/index.ts +42 -0
  98. package/src/billing-foundation/projection.ts +135 -0
  99. package/src/billing-foundation/types.ts +157 -0
  100. package/src/billing-foundation/webhook-handler.ts +184 -0
  101. package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
  102. package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
  103. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
  104. package/src/cap-counter/aggregate-id.ts +61 -0
  105. package/src/cap-counter/constants.ts +32 -0
  106. package/src/cap-counter/enforce-cap.ts +404 -0
  107. package/src/cap-counter/entity.ts +48 -0
  108. package/src/cap-counter/feature.ts +90 -0
  109. package/src/cap-counter/handlers/get-counter.query.ts +43 -0
  110. package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
  111. package/src/cap-counter/handlers/increment.write.ts +92 -0
  112. package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
  113. package/src/cap-counter/index.ts +34 -0
  114. package/src/cap-counter/with-cap-enforcement.ts +179 -0
  115. package/src/channel-email/email-channel.ts +48 -0
  116. package/src/channel-email/feature.ts +15 -0
  117. package/src/channel-email/index.ts +4 -0
  118. package/src/channel-email/smtp-transport.ts +65 -0
  119. package/src/channel-email/types.ts +34 -0
  120. package/src/channel-in-app/constants.ts +11 -0
  121. package/src/channel-in-app/feature.ts +30 -0
  122. package/src/channel-in-app/handlers/inbox.query.ts +28 -0
  123. package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
  124. package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
  125. package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
  126. package/src/channel-in-app/in-app-channel.ts +44 -0
  127. package/src/channel-in-app/index.ts +4 -0
  128. package/src/channel-in-app/tables.ts +22 -0
  129. package/src/channel-push/feature.ts +15 -0
  130. package/src/channel-push/index.ts +3 -0
  131. package/src/channel-push/push-channel.ts +33 -0
  132. package/src/channel-push/types.ts +22 -0
  133. package/src/config/__tests__/app-overrides.test.ts +118 -0
  134. package/src/config/__tests__/config.integration.ts +1246 -0
  135. package/src/config/constants.ts +23 -0
  136. package/src/config/feature.ts +117 -0
  137. package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
  138. package/src/config/handlers/reset.write.ts +45 -0
  139. package/src/config/handlers/schema.query.ts +22 -0
  140. package/src/config/handlers/set.write.ts +93 -0
  141. package/src/config/handlers/values.query.ts +43 -0
  142. package/src/config/index.ts +15 -0
  143. package/src/config/resolver.ts +283 -0
  144. package/src/config/table.ts +35 -0
  145. package/src/config/write-helpers.ts +268 -0
  146. package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
  147. package/src/delivery/__tests__/delivery.integration.ts +1405 -0
  148. package/src/delivery/constants.ts +33 -0
  149. package/src/delivery/delivery-service.ts +489 -0
  150. package/src/delivery/events.ts +18 -0
  151. package/src/delivery/feature.ts +70 -0
  152. package/src/delivery/handlers/log.query.ts +21 -0
  153. package/src/delivery/handlers/preferences.query.ts +18 -0
  154. package/src/delivery/handlers/set-preference.write.ts +28 -0
  155. package/src/delivery/index.ts +35 -0
  156. package/src/delivery/tables.ts +74 -0
  157. package/src/delivery/testing.ts +47 -0
  158. package/src/delivery/types.ts +71 -0
  159. package/src/delivery/unsubscribe.ts +99 -0
  160. package/src/delivery/upsert-preference.ts +145 -0
  161. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
  162. package/src/feature-toggles/constants.ts +20 -0
  163. package/src/feature-toggles/events.ts +18 -0
  164. package/src/feature-toggles/feature.ts +98 -0
  165. package/src/feature-toggles/global-feature-state-table.ts +28 -0
  166. package/src/feature-toggles/handlers/list.query.ts +26 -0
  167. package/src/feature-toggles/handlers/registered.query.ts +56 -0
  168. package/src/feature-toggles/handlers/set.write.ts +158 -0
  169. package/src/feature-toggles/index.ts +9 -0
  170. package/src/feature-toggles/toggle-runtime.ts +73 -0
  171. package/src/file-foundation/__tests__/feature.test.ts +35 -0
  172. package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
  173. package/src/file-foundation/feature.ts +123 -0
  174. package/src/file-foundation/index.ts +7 -0
  175. package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
  176. package/src/file-provider-inmemory/feature.ts +73 -0
  177. package/src/file-provider-inmemory/index.ts +3 -0
  178. package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
  179. package/src/file-provider-s3/feature.ts +169 -0
  180. package/src/file-provider-s3/index.ts +3 -0
  181. package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
  182. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
  183. package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
  184. package/src/files-provider-s3/env-helper.ts +49 -0
  185. package/src/files-provider-s3/index.ts +3 -0
  186. package/src/files-provider-s3/s3-provider.ts +114 -0
  187. package/src/foundation-shared/config-helpers.ts +67 -0
  188. package/src/foundation-shared/index.ts +4 -0
  189. package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
  190. package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
  191. package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
  192. package/src/jobs/constants.ts +21 -0
  193. package/src/jobs/events.ts +39 -0
  194. package/src/jobs/feature.ts +150 -0
  195. package/src/jobs/handlers/detail.query.ts +30 -0
  196. package/src/jobs/handlers/list.query.ts +36 -0
  197. package/src/jobs/handlers/retry.write.ts +69 -0
  198. package/src/jobs/handlers/trigger.write.ts +39 -0
  199. package/src/jobs/index.ts +5 -0
  200. package/src/jobs/job-run-logger.ts +213 -0
  201. package/src/jobs/job-run-table.ts +55 -0
  202. package/src/legal-pages/README.md +195 -0
  203. package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
  204. package/src/legal-pages/constants.ts +36 -0
  205. package/src/legal-pages/feature.ts +187 -0
  206. package/src/legal-pages/index.ts +13 -0
  207. package/src/legal-pages/markdown.ts +69 -0
  208. package/src/mail-foundation/__tests__/feature.test.ts +46 -0
  209. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
  210. package/src/mail-foundation/feature.ts +160 -0
  211. package/src/mail-foundation/index.ts +14 -0
  212. package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
  213. package/src/mail-transport-inmemory/feature.ts +90 -0
  214. package/src/mail-transport-inmemory/index.ts +3 -0
  215. package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
  216. package/src/mail-transport-smtp/feature.ts +182 -0
  217. package/src/mail-transport-smtp/index.ts +3 -0
  218. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
  219. package/src/rate-limiting/constants.ts +9 -0
  220. package/src/rate-limiting/feature.ts +16 -0
  221. package/src/rate-limiting/handlers/status.query.ts +52 -0
  222. package/src/rate-limiting/index.ts +2 -0
  223. package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
  224. package/src/renderer-simple/feature.ts +12 -0
  225. package/src/renderer-simple/index.ts +2 -0
  226. package/src/renderer-simple/simple-renderer.ts +72 -0
  227. package/src/secrets/__tests__/rotate.integration.ts +176 -0
  228. package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
  229. package/src/secrets/__tests__/secrets.integration.ts +118 -0
  230. package/src/secrets/feature.ts +84 -0
  231. package/src/secrets/handlers/delete.write.ts +20 -0
  232. package/src/secrets/handlers/list.query.ts +38 -0
  233. package/src/secrets/handlers/rotate.job.ts +193 -0
  234. package/src/secrets/handlers/set.write.ts +50 -0
  235. package/src/secrets/index.ts +16 -0
  236. package/src/secrets/secrets-context.ts +296 -0
  237. package/src/secrets/table.ts +68 -0
  238. package/src/sessions/__tests__/cleanup.integration.ts +175 -0
  239. package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
  240. package/src/sessions/__tests__/sessions.integration.ts +472 -0
  241. package/src/sessions/__tests__/test-helpers.ts +66 -0
  242. package/src/sessions/constants.ts +43 -0
  243. package/src/sessions/feature.ts +84 -0
  244. package/src/sessions/handlers/cleanup.job.ts +109 -0
  245. package/src/sessions/handlers/list.query.ts +35 -0
  246. package/src/sessions/handlers/mine.query.ts +37 -0
  247. package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
  248. package/src/sessions/handlers/revoke.write.ts +76 -0
  249. package/src/sessions/index.ts +17 -0
  250. package/src/sessions/schema/index.ts +5 -0
  251. package/src/sessions/schema/user-session.ts +67 -0
  252. package/src/sessions/session-callbacks.ts +110 -0
  253. package/src/sessions/testing.ts +42 -0
  254. package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
  255. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
  256. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
  257. package/src/subscription-mollie/constants.ts +33 -0
  258. package/src/subscription-mollie/feature.ts +144 -0
  259. package/src/subscription-mollie/index.ts +13 -0
  260. package/src/subscription-mollie/plugin-methods.ts +79 -0
  261. package/src/subscription-mollie/verify-webhook.ts +244 -0
  262. package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
  263. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
  264. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
  265. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
  266. package/src/subscription-stripe/constants.ts +20 -0
  267. package/src/subscription-stripe/feature.ts +120 -0
  268. package/src/subscription-stripe/index.ts +14 -0
  269. package/src/subscription-stripe/plugin-methods.ts +91 -0
  270. package/src/subscription-stripe/verify-webhook.ts +235 -0
  271. package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
  272. package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
  273. package/src/tenant/__tests__/tenant.integration.ts +347 -0
  274. package/src/tenant/command-schemas.ts +37 -0
  275. package/src/tenant/constants.ts +37 -0
  276. package/src/tenant/feature.ts +109 -0
  277. package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
  278. package/src/tenant/handlers/add-member.write.ts +53 -0
  279. package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
  280. package/src/tenant/handlers/create.write.ts +21 -0
  281. package/src/tenant/handlers/disable.write.ts +18 -0
  282. package/src/tenant/handlers/invitations.query.ts +31 -0
  283. package/src/tenant/handlers/list.query.ts +17 -0
  284. package/src/tenant/handlers/me.query.ts +17 -0
  285. package/src/tenant/handlers/members.query.ts +22 -0
  286. package/src/tenant/handlers/memberships.query.ts +24 -0
  287. package/src/tenant/handlers/remove-member.write.ts +40 -0
  288. package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
  289. package/src/tenant/handlers/update-member-roles.write.ts +54 -0
  290. package/src/tenant/handlers/update.write.ts +20 -0
  291. package/src/tenant/index.ts +12 -0
  292. package/src/tenant/invitation-table.ts +93 -0
  293. package/src/tenant/membership-table.ts +35 -0
  294. package/src/tenant/schema/index.ts +5 -0
  295. package/src/tenant/schema/tenant.ts +27 -0
  296. package/src/tenant/seeding.ts +155 -0
  297. package/src/tenant/testing.ts +8 -0
  298. package/src/text-content/README.md +190 -0
  299. package/src/text-content/__tests__/text-content.integration.ts +415 -0
  300. package/src/text-content/api.ts +92 -0
  301. package/src/text-content/constants.ts +19 -0
  302. package/src/text-content/feature.ts +29 -0
  303. package/src/text-content/handlers/by-slug.query.ts +55 -0
  304. package/src/text-content/handlers/set.write.ts +118 -0
  305. package/src/text-content/index.ts +14 -0
  306. package/src/text-content/seeding.ts +91 -0
  307. package/src/text-content/table.ts +45 -0
  308. package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
  309. package/src/tier-engine/__tests__/drift.test.ts +42 -0
  310. package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
  311. package/src/tier-engine/aggregate-id.ts +27 -0
  312. package/src/tier-engine/compose-app.ts +150 -0
  313. package/src/tier-engine/constants.ts +15 -0
  314. package/src/tier-engine/entity.ts +30 -0
  315. package/src/tier-engine/feature.ts +72 -0
  316. package/src/tier-engine/handlers/active-tier.query.ts +23 -0
  317. package/src/tier-engine/index.ts +22 -0
  318. package/src/user/__tests__/seed-testing.integration.ts +127 -0
  319. package/src/user/__tests__/user.integration.ts +198 -0
  320. package/src/user/command-schemas.ts +15 -0
  321. package/src/user/constants.ts +23 -0
  322. package/src/user/feature.ts +32 -0
  323. package/src/user/handlers/create.write.ts +54 -0
  324. package/src/user/handlers/detail.query.ts +9 -0
  325. package/src/user/handlers/find-for-auth.query.ts +38 -0
  326. package/src/user/handlers/list.query.ts +8 -0
  327. package/src/user/handlers/me.query.ts +15 -0
  328. package/src/user/handlers/update.write.ts +54 -0
  329. package/src/user/index.ts +4 -0
  330. package/src/user/schema/index.ts +5 -0
  331. package/src/user/schema/user.ts +69 -0
  332. package/src/user/seeding.ts +93 -0
  333. package/src/user/testing.ts +5 -0
@@ -0,0 +1,28 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { and, desc, eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ import { inAppMessagesTable } from "../tables";
5
+
6
+ export const inboxQuery = defineQueryHandler({
7
+ name: "inbox",
8
+ schema: z.object({
9
+ limit: z.number().min(1).max(100).default(50),
10
+ unreadOnly: z.boolean().default(false),
11
+ }),
12
+ access: { openToAll: true },
13
+ handler: async (query, ctx) => {
14
+ const conditions = [eq(inAppMessagesTable.userId, query.user.id)];
15
+ if (query.payload.unreadOnly) {
16
+ conditions.push(eq(inAppMessagesTable.isRead, false));
17
+ }
18
+
19
+ const rows = await ctx.db
20
+ .select()
21
+ .from(inAppMessagesTable)
22
+ .where(and(...conditions))
23
+ .orderBy(desc(inAppMessagesTable.createdAt))
24
+ .limit(query.payload.limit);
25
+
26
+ return { rows };
27
+ },
28
+ });
@@ -0,0 +1,21 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ import { inAppMessagesTable } from "../tables";
5
+
6
+ export const markAllReadWrite = defineWriteHandler({
7
+ name: "markAllRead",
8
+ schema: z.object({}),
9
+ access: { openToAll: true },
10
+ handler: async (event, ctx) => {
11
+ const rows = await ctx.db
12
+ .update(inAppMessagesTable)
13
+ .set({ isRead: true, readAt: Temporal.Now.instant() })
14
+ .where(
15
+ and(eq(inAppMessagesTable.userId, event.user.id), eq(inAppMessagesTable.isRead, false)),
16
+ )
17
+ .returning();
18
+
19
+ return { isSuccess: true, data: { marked: rows.length } };
20
+ },
21
+ });
@@ -0,0 +1,32 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+ import { inAppMessagesTable } from "../tables";
6
+
7
+ export const markReadWrite = defineWriteHandler({
8
+ name: "markRead",
9
+ schema: z.object({
10
+ // inAppMessages.id is a serial integer (table is infra, not an ES aggregate).
11
+ id: z.number().int(),
12
+ }),
13
+ access: { openToAll: true },
14
+ handler: async (event, ctx) => {
15
+ const rows = await ctx.db
16
+ .update(inAppMessagesTable)
17
+ .set({ isRead: true, readAt: Temporal.Now.instant() })
18
+ .where(
19
+ and(
20
+ eq(inAppMessagesTable.id, event.payload.id),
21
+ eq(inAppMessagesTable.userId, event.user.id),
22
+ ),
23
+ )
24
+ .returning();
25
+
26
+ if (rows.length === 0) {
27
+ return writeFailure(new NotFoundError("inAppMessage", event.payload.id));
28
+ }
29
+
30
+ return { isSuccess: true, data: { id: rows[0]?.["id"] } };
31
+ },
32
+ });
@@ -0,0 +1,20 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { and, count, eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ import { inAppMessagesTable } from "../tables";
5
+
6
+ export const unreadCountQuery = defineQueryHandler({
7
+ name: "unreadCount",
8
+ schema: z.object({}),
9
+ access: { openToAll: true },
10
+ handler: async (query, ctx) => {
11
+ const rows = await ctx.db
12
+ .select({ value: count() })
13
+ .from(inAppMessagesTable)
14
+ .where(
15
+ and(eq(inAppMessagesTable.userId, query.user.id), eq(inAppMessagesTable.isRead, false)),
16
+ );
17
+
18
+ return { count: rows[0]?.["value"] ?? 0 };
19
+ },
20
+ });
@@ -0,0 +1,44 @@
1
+ import { tenantChannel } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DeliveryChannel } from "../delivery";
3
+ import { inAppMessagesTable } from "./tables";
4
+
5
+ export const inAppChannel: DeliveryChannel = {
6
+ name: "inApp",
7
+
8
+ async resolve(userId) {
9
+ // InApp always resolves — the userId IS the address
10
+ return String(userId);
11
+ },
12
+
13
+ async send(address, message, ctx) {
14
+ // address is the user-id string after the ES migration — keep it as-is.
15
+ const userId = address;
16
+
17
+ const rows = await ctx.db
18
+ .insert(inAppMessagesTable)
19
+ .values({
20
+ tenantId: ctx.tenantId,
21
+ userId,
22
+ notificationType: message.notificationType,
23
+ title: message.title,
24
+ body: message.body ?? null,
25
+ data: message.data ? JSON.stringify(message.data) : null,
26
+ })
27
+ .returning();
28
+
29
+ if (ctx.sseBroker) {
30
+ ctx.sseBroker.pushToChannel(tenantChannel(ctx.tenantId), {
31
+ type: "channel-in-app:event:delivered",
32
+ data: {
33
+ id: rows[0]?.["id"],
34
+ userId,
35
+ notificationType: message.notificationType,
36
+ title: message.title,
37
+ body: message.body,
38
+ },
39
+ });
40
+ }
41
+
42
+ return { status: "sent", address };
43
+ },
44
+ };
@@ -0,0 +1,4 @@
1
+ export { CHANNEL_IN_APP_FEATURE, InAppHandlers, InAppQueries } from "./constants";
2
+ export { createChannelInAppFeature } from "./feature";
3
+ export { inAppChannel } from "./in-app-channel";
4
+ export { inAppMessagesTable } from "./tables";
@@ -0,0 +1,22 @@
1
+ import {
2
+ boolean,
3
+ instant,
4
+ table as pgTable,
5
+ serial,
6
+ text,
7
+ uuid,
8
+ } from "@cosmicdrift/kumiko-framework/db";
9
+ import { sql } from "drizzle-orm";
10
+
11
+ export const inAppMessagesTable = pgTable("in_app_messages", {
12
+ id: serial("id").primaryKey(),
13
+ tenantId: uuid("tenant_id").notNull(),
14
+ userId: text("user_id").notNull(),
15
+ notificationType: text("notification_type").notNull(),
16
+ title: text("title").notNull(),
17
+ body: text("body"),
18
+ data: text("data"), // JSON string for action, screen, etc.
19
+ isRead: boolean("is_read").default(false).notNull(),
20
+ readAt: instant("read_at"),
21
+ createdAt: instant("created_at").default(sql`now()`).notNull(),
22
+ });
@@ -0,0 +1,15 @@
1
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { createPushChannel, type PushChannelOptions } from "./push-channel";
3
+
4
+ export function createChannelPushFeature(options: PushChannelOptions): FeatureDefinition {
5
+ const channel = createPushChannel(options);
6
+
7
+ return defineFeature("channelPush", (r) => {
8
+ r.requires("delivery");
9
+
10
+ r.useExtension("deliveryChannel", "push", {
11
+ resolve: channel.resolve,
12
+ send: channel.send,
13
+ });
14
+ });
15
+ }
@@ -0,0 +1,3 @@
1
+ export { createChannelPushFeature } from "./feature";
2
+ export { createPushChannel, type PushChannelOptions } from "./push-channel";
3
+ export { createInMemoryPushTransport, type PushMessage, type PushTransport } from "./types";
@@ -0,0 +1,33 @@
1
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DeliveryChannel } from "../delivery";
3
+ import type { PushTransport } from "./types";
4
+
5
+ export type PushChannelOptions = {
6
+ readonly transport: PushTransport;
7
+ readonly resolveToken: (
8
+ userId: string,
9
+ ctx: { db: unknown; tenantId: TenantId },
10
+ ) => Promise<string | null>;
11
+ };
12
+
13
+ export function createPushChannel(options: PushChannelOptions): DeliveryChannel {
14
+ const { transport, resolveToken } = options;
15
+
16
+ return {
17
+ name: "push",
18
+
19
+ async resolve(userId, ctx) {
20
+ return resolveToken(userId, ctx);
21
+ },
22
+
23
+ async send(address, message, _ctx) {
24
+ await transport.send({
25
+ token: address,
26
+ title: message.title,
27
+ body: message.body,
28
+ data: message.data,
29
+ });
30
+ return { status: "sent", address };
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,22 @@
1
+ export type PushMessage = {
2
+ readonly token: string;
3
+ readonly title: string;
4
+ readonly body: string | undefined;
5
+ readonly data: Readonly<Record<string, unknown>> | undefined;
6
+ };
7
+
8
+ export type PushTransport = {
9
+ send(message: PushMessage): Promise<void>;
10
+ };
11
+
12
+ export function createInMemoryPushTransport(): PushTransport & {
13
+ readonly sent: PushMessage[];
14
+ } {
15
+ const sent: PushMessage[] = [];
16
+ return {
17
+ sent,
18
+ async send(message) {
19
+ sent.push(message);
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,118 @@
1
+ import { createTenantConfig, createUserConfig } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { describe, expect, test } from "vitest";
3
+ import { validateAppOverrides } from "../resolver";
4
+
5
+ // Minimal registry stub — validateAppOverrides only reads getConfigKey.
6
+ // Cast keeps the test isolated from the rest of the Registry surface.
7
+ function registryStub(keys: Record<string, unknown>) {
8
+ return {
9
+ getConfigKey: (key: string) => keys[key] as never,
10
+ };
11
+ }
12
+
13
+ describe("validateAppOverrides", () => {
14
+ test("accepts a typed override that matches an existing key", () => {
15
+ const reg = registryStub({
16
+ "files:config:max-size": createTenantConfig("number"),
17
+ });
18
+ const validated = validateAppOverrides(reg, {
19
+ "files:config:max-size": 50,
20
+ });
21
+ expect(validated.get("files:config:max-size")).toBe(50);
22
+ expect(validated.size).toBe(1);
23
+ });
24
+
25
+ test("throws on unknown key", () => {
26
+ const reg = registryStub({});
27
+ expect(() =>
28
+ validateAppOverrides(reg, {
29
+ "missing:config:x": 1,
30
+ }),
31
+ ).toThrow(/unknown config key.*missing:config:x/i);
32
+ });
33
+
34
+ test("throws on type mismatch — number key, string value", () => {
35
+ const reg = registryStub({
36
+ "a:config:x": createTenantConfig("number"),
37
+ });
38
+ expect(() =>
39
+ validateAppOverrides(reg, {
40
+ "a:config:x": "not-a-number",
41
+ }),
42
+ ).toThrow(/expected number, got string/i);
43
+ });
44
+
45
+ test("throws on type mismatch — boolean key, number value", () => {
46
+ const reg = registryStub({
47
+ "a:config:flag": createTenantConfig("boolean"),
48
+ });
49
+ expect(() =>
50
+ validateAppOverrides(reg, {
51
+ "a:config:flag": 1,
52
+ }),
53
+ ).toThrow(/expected boolean, got number/i);
54
+ });
55
+
56
+ test("select key — accepts value in options, rejects anything else", () => {
57
+ const reg = registryStub({
58
+ "a:config:theme": createTenantConfig("select", { options: ["light", "dark", "auto"] }),
59
+ });
60
+ // ok
61
+ expect(() => validateAppOverrides(reg, { "a:config:theme": "dark" })).not.toThrow();
62
+ // not in options
63
+ expect(() => validateAppOverrides(reg, { "a:config:theme": "purple" })).toThrow(
64
+ /not in options/i,
65
+ );
66
+ });
67
+
68
+ test("throws on bounds violation — below min", () => {
69
+ const reg = registryStub({
70
+ "a:config:n": createTenantConfig("number", { bounds: { min: 1, max: 100 } }),
71
+ });
72
+ expect(() => validateAppOverrides(reg, { "a:config:n": 0 })).toThrow(/below bounds\.min/i);
73
+ });
74
+
75
+ test("throws on bounds violation — above max", () => {
76
+ const reg = registryStub({
77
+ "a:config:n": createTenantConfig("number", { bounds: { min: 1, max: 100 } }),
78
+ });
79
+ expect(() => validateAppOverrides(reg, { "a:config:n": 101 })).toThrow(/above bounds\.max/i);
80
+ });
81
+
82
+ test("passes multiple overrides at once and preserves all", () => {
83
+ const reg = registryStub({
84
+ "a:config:n": createTenantConfig("number"),
85
+ "a:config:s": createTenantConfig("text"),
86
+ "a:config:b": createUserConfig("boolean"),
87
+ });
88
+ const result = validateAppOverrides(reg, {
89
+ "a:config:n": 42,
90
+ "a:config:s": "hello",
91
+ "a:config:b": true,
92
+ });
93
+ expect(result.size).toBe(3);
94
+ expect(result.get("a:config:n")).toBe(42);
95
+ expect(result.get("a:config:s")).toBe("hello");
96
+ expect(result.get("a:config:b")).toBe(true);
97
+ });
98
+
99
+ test("empty overrides map passes through without validation cost", () => {
100
+ const reg = registryStub({});
101
+ const result = validateAppOverrides(reg, {});
102
+ expect(result.size).toBe(0);
103
+ });
104
+
105
+ test("rejects override for a computed key — plan-logic may not be silently bypassed", () => {
106
+ const reg = registryStub({
107
+ "plan:config:quota": createTenantConfig("number", {
108
+ default: 10,
109
+ computed: async () => 100,
110
+ }),
111
+ });
112
+ expect(() =>
113
+ validateAppOverrides(reg, {
114
+ "plan:config:quota": 999,
115
+ }),
116
+ ).toThrow(/computed resolver.*app-overrides would silently bypass/i);
117
+ });
118
+ });