@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,241 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
4
+ import {
5
+ createEntityTable,
6
+ createTestUser,
7
+ pushTables,
8
+ setupTestStack,
9
+ type TestStack,
10
+ TestUsers,
11
+ testTenantId,
12
+ } from "@cosmicdrift/kumiko-framework/stack";
13
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
14
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
15
+ import { createConfigFeature } from "../../config";
16
+ import { type ConfigResolver, createConfigResolver } from "../../config/resolver";
17
+ import { configValuesTable } from "../../config/table";
18
+ import { createTenantFeature } from "../../tenant/feature";
19
+ import { tenantEntity } from "../../tenant/schema/tenant";
20
+ import { TierEngineHandlers, TierEngineQueries } from "../constants";
21
+ import { tierAssignmentEntity } from "../entity";
22
+ import { tierEngineFeature } from "../feature";
23
+
24
+ // --- Setup ---
25
+ //
26
+ // Test-Isolation-Pattern: jeder Test nutzt einen eigenen Tenant über
27
+ // `createTestUser({ id: N, tenantId: testTenantId(N) })`. Dadurch teilen
28
+ // die Tests keinen Zustand — wenn ein Test fehlschlägt, bleiben die anderen
29
+ // aussagekräftig. Zusätzlicher Vorteil: list-Test sieht garantiert genau
30
+ // 1 Row, weil sonst keine andere Test-Aktivität in seinem Tenant läuft.
31
+
32
+ let stack: TestStack;
33
+ let db: DbConnection;
34
+ let resolver: ConfigResolver;
35
+
36
+ const configFeature = createConfigFeature();
37
+ const tenantFeature = createTenantFeature();
38
+ const testEncryptionKey = randomBytes(32).toString("base64");
39
+
40
+ beforeAll(async () => {
41
+ const encryption = createEncryptionProvider(testEncryptionKey);
42
+ resolver = createConfigResolver({ encryption });
43
+
44
+ stack = await setupTestStack({
45
+ features: [configFeature, tenantFeature, tierEngineFeature],
46
+ extraContext: { configResolver: resolver, configEncryption: encryption },
47
+ });
48
+ db = stack.db;
49
+
50
+ await createEntityTable(db, tenantEntity);
51
+ await createEntityTable(db, tierAssignmentEntity);
52
+ await pushTables(db, { configValuesTable });
53
+ await createEventsTable(db);
54
+ });
55
+
56
+ afterAll(async () => {
57
+ await stack.cleanup();
58
+ });
59
+
60
+ // Per-test admin-user factory — fresh tenant per scenario.
61
+ function adminFor(tenantNumber: number) {
62
+ return createTestUser({
63
+ id: tenantNumber,
64
+ tenantId: testTenantId(tenantNumber),
65
+ roles: ["TenantAdmin", "SystemAdmin"],
66
+ });
67
+ }
68
+
69
+ // --- Scenario 1: create ---
70
+
71
+ describe("scenario 1: create", () => {
72
+ test("admin creates a tier-assignment for the calling tenant", async () => {
73
+ const admin = adminFor(101);
74
+
75
+ const result = await stack.http.writeOk(TierEngineHandlers.create, { tier: "pro" }, admin);
76
+
77
+ const data = result!["data"] as Record<string, unknown>;
78
+ expect(data["tier"]).toBe("pro");
79
+ expect(typeof data["id"]).toBe("string");
80
+ expect(result!["isNew"]).toBe(true);
81
+ });
82
+ });
83
+
84
+ // --- Scenario 2: update ---
85
+
86
+ describe("scenario 2: update", () => {
87
+ test("admin updates the tier value via {id, version, changes}", async () => {
88
+ const admin = adminFor(102);
89
+
90
+ const created = await stack.http.writeOk(TierEngineHandlers.create, { tier: "pro" }, admin);
91
+ const id = (created!["data"] as Record<string, unknown>)["id"] as string;
92
+
93
+ const updated = await stack.http.writeOk(
94
+ TierEngineHandlers.update,
95
+ { id, version: 1, changes: { tier: "business" } },
96
+ admin,
97
+ );
98
+
99
+ const data = updated!["data"] as Record<string, unknown>;
100
+ expect(data["tier"]).toBe("business");
101
+ expect(updated!["isNew"]).toBe(false);
102
+ });
103
+ });
104
+
105
+ // --- Scenario 3: get-active-tier ---
106
+
107
+ describe("scenario 3: get-active-tier", () => {
108
+ test("returns the current tier for the calling tenant", async () => {
109
+ const admin = adminFor(103);
110
+ await stack.http.writeOk(TierEngineHandlers.create, { tier: "starter" }, admin);
111
+
112
+ const result = await stack.http.queryOk<Record<string, unknown> | null>(
113
+ TierEngineQueries.getActiveTier,
114
+ {},
115
+ admin,
116
+ );
117
+
118
+ expect(result).not.toBeNull();
119
+ expect(result!["tier"]).toBe("starter");
120
+ });
121
+
122
+ test("returns null when no tier is set for the calling tenant", async () => {
123
+ const adminWithoutTier = adminFor(104);
124
+
125
+ const result = await stack.http.queryOk<Record<string, unknown> | null>(
126
+ TierEngineQueries.getActiveTier,
127
+ {},
128
+ adminWithoutTier,
129
+ );
130
+
131
+ expect(result).toBeNull();
132
+ });
133
+ });
134
+
135
+ // --- Scenario 4: list (auto tenant-scoped) ---
136
+
137
+ describe("scenario 4: list", () => {
138
+ test("returns the tier-assignment(s) for the calling tenant only", async () => {
139
+ const admin = adminFor(105);
140
+ await stack.http.writeOk(TierEngineHandlers.create, { tier: "team" }, admin);
141
+
142
+ const result = await stack.http.queryOk<{
143
+ rows: Record<string, unknown>[];
144
+ nextCursor: string | null;
145
+ }>(TierEngineQueries.list, {}, admin);
146
+
147
+ expect(Array.isArray(result.rows)).toBe(true);
148
+ expect(result.rows.length).toBe(1);
149
+ expect(result.rows[0]!["tier"]).toBe("team");
150
+ });
151
+ });
152
+
153
+ // --- Scenario 5: tenant isolation (Kern-Versprechen) ---
154
+
155
+ describe("scenario 5: tenant isolation", () => {
156
+ test("two tenants have independent tier-assignments — no cross-bleed", async () => {
157
+ const adminA = adminFor(201);
158
+ const adminB = adminFor(202);
159
+
160
+ await stack.http.writeOk(TierEngineHandlers.create, { tier: "pro" }, adminA);
161
+ await stack.http.writeOk(TierEngineHandlers.create, { tier: "business" }, adminB);
162
+
163
+ const tierA = await stack.http.queryOk<Record<string, unknown> | null>(
164
+ TierEngineQueries.getActiveTier,
165
+ {},
166
+ adminA,
167
+ );
168
+ const tierB = await stack.http.queryOk<Record<string, unknown> | null>(
169
+ TierEngineQueries.getActiveTier,
170
+ {},
171
+ adminB,
172
+ );
173
+
174
+ expect(tierA!["tier"]).toBe("pro");
175
+ expect(tierB!["tier"]).toBe("business");
176
+
177
+ // List as A returns only A's row, never B's.
178
+ const listA = await stack.http.queryOk<{
179
+ rows: Record<string, unknown>[];
180
+ nextCursor: string | null;
181
+ }>(TierEngineQueries.list, {}, adminA);
182
+ expect(listA.rows.length).toBe(1);
183
+ expect(listA.rows[0]!["tier"]).toBe("pro");
184
+
185
+ const listB = await stack.http.queryOk<{
186
+ rows: Record<string, unknown>[];
187
+ nextCursor: string | null;
188
+ }>(TierEngineQueries.list, {}, adminB);
189
+ expect(listB.rows.length).toBe(1);
190
+ expect(listB.rows[0]!["tier"]).toBe("business");
191
+ });
192
+ });
193
+
194
+ // --- Scenario 6: access control (negative tests) ---
195
+
196
+ describe("scenario 6: access control", () => {
197
+ test("normal User (no admin role) cannot create a tier-assignment", async () => {
198
+ const normalUser = TestUsers.user; // tenant 1, role: User
199
+
200
+ const error = await stack.http.writeErr(TierEngineHandlers.create, { tier: "pro" }, normalUser);
201
+
202
+ expectErrorIncludes(error, "access_denied");
203
+ });
204
+
205
+ test("normal User cannot update a tier-assignment", async () => {
206
+ const admin = adminFor(301);
207
+ const created = await stack.http.writeOk(TierEngineHandlers.create, { tier: "pro" }, admin);
208
+ const id = (created!["data"] as Record<string, unknown>)["id"] as string;
209
+
210
+ // Same tenant as admin (tenant 301), but a User role instead of admin.
211
+ const normalUserSameTenant = createTestUser({
212
+ id: 302,
213
+ tenantId: testTenantId(301),
214
+ roles: ["User"],
215
+ });
216
+
217
+ const error = await stack.http.writeErr(
218
+ TierEngineHandlers.update,
219
+ { id, version: 1, changes: { tier: "business" } },
220
+ normalUserSameTenant,
221
+ );
222
+
223
+ expectErrorIncludes(error, "access_denied");
224
+ });
225
+
226
+ test("query handlers carry the admin-only access rule (config-level check)", () => {
227
+ // Read-access is enforced by the same role-rule set on the query handler.
228
+ // We assert the rule is registered correctly — covers regression when
229
+ // someone changes adminAccess to openToAll without noticing.
230
+ const listRule = stack.registry.getQueryHandler(TierEngineQueries.list)?.access;
231
+ const activeTierRule = stack.registry.getQueryHandler(TierEngineQueries.getActiveTier)?.access;
232
+
233
+ expect(listRule).toBeDefined();
234
+ expect(activeTierRule).toBeDefined();
235
+ // Roles array contains TenantAdmin + SystemAdmin (no anonymous, no User).
236
+ expect(JSON.stringify(listRule)).toMatch(/TenantAdmin/);
237
+ expect(JSON.stringify(listRule)).toMatch(/SystemAdmin/);
238
+ expect(JSON.stringify(activeTierRule)).toMatch(/TenantAdmin/);
239
+ expect(JSON.stringify(activeTierRule)).toMatch(/SystemAdmin/);
240
+ });
241
+ });
@@ -0,0 +1,27 @@
1
+ import { v5 as uuidv5 } from "uuid";
2
+
3
+ // Fixed UUID-namespace für die tier-assignment-aggregate-id-Ableitung.
4
+ // Generiert einmalig (2026-05-02), in Stein gemeißelt: ein Wechsel würde
5
+ // jeden existing aggregate-Stream re-keyen → kaputter event-replay,
6
+ // kaputte projection-rebuilds, verlorener Tier-Wechsel-Audit. Der
7
+ // drift-pin-Test in feature.test.ts pinnt diese Konstante.
8
+ const TIER_ASSIGNMENT_NAMESPACE = "8e91d2fc-8b7a-4d3e-9f4a-1c5d6e7f8a9b";
9
+
10
+ /**
11
+ * Deterministic aggregate-id für ein Tier-Assignment aus dem tenantId.
12
+ * Pro Plattform-Tenant existiert genau ein Aggregat — uuidv5 ist
13
+ * namespace-deterministic, identische Eingabe ergibt identischen UUID.
14
+ *
15
+ * **Wann nutzen:** Sprint 5 (`stripe-sync`-Feature) wrapt einen
16
+ * idempotent-set-tier-Handler darum: zweiter Stripe-Webhook-Retry mit
17
+ * derselben tenantId → derselbe aggregate-Stream → version_conflict
18
+ * vom Event-Store statt pg-23505 von der Read-Model-DB. ES-saubere
19
+ * Path-Uniqueness statt DB-Constraint.
20
+ *
21
+ * **Sprint 1 nutzt das nicht aktiv** — Standard-CRUD-Handlers vergeben
22
+ * UUID via `gen_random_uuid()`. Die Funktion lebt hier als Utility-
23
+ * Export bereit für Sprint 5.
24
+ */
25
+ export function tierAssignmentAggregateId(tenantId: string): string {
26
+ return uuidv5(tenantId, TIER_ASSIGNMENT_NAMESPACE);
27
+ }
@@ -0,0 +1,150 @@
1
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ /**
4
+ * Tier definition — a named bundle of features + caps.
5
+ *
6
+ * **Generic über `TCaps`:** die App definiert ihren Cap-Shape als konkreten
7
+ * Type (z.B. `{ apps: number, mailsPerMonth: number, aiTokens: number }`)
8
+ * und der composeApp-Aufruf liefert exakt diesen Shape zurück. Keine
9
+ * `as Record<string, unknown>`-Casts in App-Code, alle Cap-Reads sind
10
+ * compile-time-checked.
11
+ *
12
+ * Die Tier-Engine selbst ist **agnostisch** zu konkreten Tier-Werten und
13
+ * Cap-Dimensionen. Jede App definiert ihre TierMap: kumiko.so hat
14
+ * free/pro/business/enterprise/self-host, PublicStatus hat
15
+ * free/starter/team/agency. Die Engine speichert nur den Tier-Namen als
16
+ * String und vertraut der App's TierMap, was das beim Boot bedeutet.
17
+ */
18
+ export type TierDefinition<TCaps extends Readonly<Record<string, unknown>>> = {
19
+ /** Feature names to mount on top of the base set (no add-ons applied). */
20
+ readonly features: readonly string[];
21
+ /** Cap-Definition als app-spezifischer typed shape. */
22
+ readonly caps: TCaps;
23
+ };
24
+
25
+ /**
26
+ * Add-On definition — a tier-orthogonal feature bundle that can be added
27
+ * to any tier (BYOK-Encryption, Dedicated-Stack, Custom-SLA, ...).
28
+ *
29
+ * `capOverrides` ist `Partial<TCaps>` weil ein Add-On nur einzelne Cap-
30
+ * Werte überschreibt (z.B. „Dedicated-Stack erhöht mailsPerMonth auf
31
+ * 100k"), nicht den ganzen Shape neu definiert.
32
+ */
33
+ export type AddOnDefinition<TCaps extends Readonly<Record<string, unknown>>> = {
34
+ /** Feature names to mount additionally when this add-on is active. */
35
+ readonly features: readonly string[];
36
+ /** Cap-Overrides (replaces matching keys in the tier's cap set). */
37
+ readonly capOverrides?: Partial<TCaps>;
38
+ };
39
+
40
+ export type TierMap<TCaps extends Readonly<Record<string, unknown>>> = Readonly<
41
+ Record<string, TierDefinition<TCaps>>
42
+ >;
43
+ export type AddOnMap<TCaps extends Readonly<Record<string, unknown>>> = Readonly<
44
+ Record<string, AddOnDefinition<TCaps>>
45
+ >;
46
+
47
+ export type ComposeAppInput<TCaps extends Readonly<Record<string, unknown>>> = {
48
+ /** Features that mount unconditionally — auth, tenant, secrets, tier-engine itself. */
49
+ readonly base: readonly FeatureDefinition[];
50
+ /** All app-specific features keyed by their feature-name. */
51
+ readonly featureRegistry: Readonly<Record<string, FeatureDefinition>>;
52
+ /** App's tier definitions. */
53
+ readonly tierMap: TierMap<TCaps>;
54
+ /** App's add-on definitions. */
55
+ readonly addOnMap: AddOnMap<TCaps>;
56
+ /** Active tier name for the current platform-tenant. */
57
+ readonly tier: string;
58
+ /** Active add-on names for the current platform-tenant. */
59
+ readonly addOns: readonly string[];
60
+ };
61
+
62
+ export type ComposedApp<TCaps extends Readonly<Record<string, unknown>>> = {
63
+ /** Final feature list to pass to runProdApp / setupTestStack. */
64
+ readonly features: readonly FeatureDefinition[];
65
+ /** Effective caps after tier + add-on overrides — same shape as input.tierMap[*].caps. */
66
+ readonly caps: TCaps;
67
+ };
68
+
69
+ /**
70
+ * composeApp — compute the feature-set + caps for a platform-tenant.
71
+ *
72
+ * **Contract:**
73
+ * 1. base features always mount (Tier-Engine itself ist Teil davon)
74
+ * 2. plus tier-specific features (from tierMap[tier].features)
75
+ * 3. plus add-on features (from addOnMap[addOn].features for each active add-on)
76
+ * 4. caps = tier.caps ⊕ add-on.capOverrides (later add-ons override earlier)
77
+ *
78
+ * **Failure modes:**
79
+ * - tier not in tierMap → throws (config error, app must register valid tiers)
80
+ * - addOn not in addOnMap → throws (same)
81
+ * - feature-name not in featureRegistry → throws (same)
82
+ *
83
+ * **Why throw vs silently ignore:** unknown tier/add-on means a Stripe-webhook
84
+ * delivered something the platform doesn't understand. Failing loud at boot
85
+ * is safer than silently mounting the wrong feature-set and hoping nobody
86
+ * notices when "Pro" turns out to mean "Free".
87
+ */
88
+ export function composeApp<TCaps extends Readonly<Record<string, unknown>>>(
89
+ input: ComposeAppInput<TCaps>,
90
+ ): ComposedApp<TCaps> {
91
+ const tierDef = input.tierMap[input.tier];
92
+ if (!tierDef) {
93
+ throw new Error(
94
+ `composeApp: unknown tier "${input.tier}". Known tiers: ` +
95
+ `${Object.keys(input.tierMap).join(", ")}`,
96
+ );
97
+ }
98
+
99
+ const addOnDefs = input.addOns.map((name) => {
100
+ const def = input.addOnMap[name];
101
+ if (!def) {
102
+ throw new Error(
103
+ `composeApp: unknown add-on "${name}". Known add-ons: ` +
104
+ `${Object.keys(input.addOnMap).join(", ")}`,
105
+ );
106
+ }
107
+ return def;
108
+ });
109
+
110
+ const tierFeatureNames = tierDef.features;
111
+ const addOnFeatureNames = addOnDefs.flatMap((d) => d.features);
112
+ const allFeatureNames = [...tierFeatureNames, ...addOnFeatureNames];
113
+
114
+ const additionalFeatures = allFeatureNames.map((name) => {
115
+ const feature = input.featureRegistry[name];
116
+ if (!feature) {
117
+ throw new Error(
118
+ `composeApp: unknown feature "${name}". Registered features: ` +
119
+ `${Object.keys(input.featureRegistry).join(", ")}`,
120
+ );
121
+ }
122
+ return feature;
123
+ });
124
+
125
+ // Dedupe — a feature listed in both the tier and an add-on should only
126
+ // mount once. Order-preserving: first occurrence wins.
127
+ const seen = new Set<string>();
128
+ const dedupedFeatures = additionalFeatures.filter((f) => {
129
+ if (seen.has(f.name)) return false;
130
+ seen.add(f.name);
131
+ return true;
132
+ });
133
+
134
+ // Caps merge: tier.caps as base, each add-on's overrides applied on top.
135
+ // Later add-ons win — order in input.addOns matters for conflicting overrides.
136
+ // Object.assign mutates the local `merged` (allocated once, not in
137
+ // an accumulating-spread loop — Biome's noAccumulatingSpread is happy
138
+ // and we still avoid an `as TCaps` cast: TS narrows the assign-result
139
+ // back to TCaps when source is Partial<TCaps>.
140
+ const merged: TCaps = { ...tierDef.caps };
141
+ for (const def of addOnDefs) {
142
+ if (def.capOverrides) Object.assign(merged, def.capOverrides);
143
+ }
144
+ const effectiveCaps = merged;
145
+
146
+ return {
147
+ features: [...input.base, ...dedupedFeatures],
148
+ caps: effectiveCaps,
149
+ };
150
+ }
@@ -0,0 +1,15 @@
1
+ // Feature name
2
+ export const TIER_ENGINE_FEATURE = "tier-engine" as const;
3
+
4
+ // Qualified write handler names (QN format: scope:type:name).
5
+ // Auto-generated by defineEntityCreateHandler / defineEntityUpdateHandler.
6
+ export const TierEngineHandlers = {
7
+ create: "tier-engine:write:tier-assignment:create",
8
+ update: "tier-engine:write:tier-assignment:update",
9
+ } as const;
10
+
11
+ // Qualified query handler names.
12
+ export const TierEngineQueries = {
13
+ list: "tier-engine:query:tier-assignment:list",
14
+ getActiveTier: "tier-engine:query:get-active-tier",
15
+ } as const;
@@ -0,0 +1,30 @@
1
+ import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ // tier-assignment — pro Plattform-Tenant genau ein Aggregat. Aggregate-ID
4
+ // wird deterministisch aus tenantId abgeleitet (uuidv5, siehe aggregate-id.ts).
5
+ //
6
+ // **Tenant-Scope:** automatisch via Kumiko's tenant-scoped projection — die
7
+ // `tenantId`-Spalte wird vom Framework als Base-Column hinzugefügt, der
8
+ // CrudExecutor befüllt sie aus `event.user.tenantId`. Pro Plattform-Tenant
9
+ // gibt es genau eine Tier-Assignment-Row.
10
+ //
11
+ // **Felder**
12
+ // - tier: TierName-String (z.B. "free", "pro", "business", "enterprise",
13
+ // "self-host"). Welche Tier-Werte gültig sind, definiert die App in
14
+ // ihrer TierMap (siehe compose-app.ts) — die Engine selbst hat keine
15
+ // enumerierte Tier-Liste.
16
+ //
17
+ // **Was bewusst NICHT in der Entity steht**
18
+ // - tenantId: kommt automatisch als Base-Column.
19
+ // - validFrom/validTo: ES-redundant (Events tragen Timestamps nativ,
20
+ // time-travel über `ctx.loadAggregate({ asOf })`).
21
+ // - addOns: Sprint 1 hat keine Add-Ons. Sprint 4 fügt sie als separate
22
+ // `tier-add-on`-Entity hinzu (1:n Relation, ES-saubere Add/Remove-Events
23
+ // mit klarer Audit-Granularität statt JSON-Array-Replace).
24
+ // - Caps-Werte: pro-Tier-Cap-Definitionen leben in der TierMap der App.
25
+ export const tierAssignmentEntity = createEntity({
26
+ table: "read_tier_assignments",
27
+ fields: {
28
+ tier: createTextField({ required: true, maxLength: 50 }),
29
+ },
30
+ });
@@ -0,0 +1,72 @@
1
+ // tier-engine — Pricing-Tier-Mechanik als reguläres Bundled-Feature.
2
+ //
3
+ // **Was diese Feature macht:**
4
+ // Speichert pro Plattform-Tenant ein Tier-Assignment (welcher Tier ist
5
+ // aktiv). composeApp-Helper liest diesen Stand und leitet daraus ab,
6
+ // welche Features für den Tenant gemountet werden.
7
+ //
8
+ // **Generic über Tier-Werte:** das Feature kennt keine "free"/"pro"/etc.
9
+ // konkreten Tier-Werte. Es speichert nur den Tier-Namen als String. Die
10
+ // App definiert ihre TierMap (siehe compose-app.ts), damit kumiko.so,
11
+ // PublicStatus, und andere Kumiko-Apps je eigene Tier-Sets nutzen können.
12
+ //
13
+ // **Standard-CRUD-Handler in Sprint 1:** Create/Update/List/Detail per
14
+ // `defineEntityXxxHandler`. Idempotente set-tier-Logic (deterministic
15
+ // aggregate-id, create-or-update-Routing) kommt im stripe-sync-Feature
16
+ // in Sprint 5 als Wrapper darum.
17
+ //
18
+ // **Tenant-Scope:** tier-engine ist tenant-scoped. Plattform-Tenant verwaltet
19
+ // seinen eigenen Tier (Self-Service-Upgrade-UI). Stripe-Webhook (Sprint 5)
20
+ // wird im stripe-sync-Feature die tenant-resolution machen und den
21
+ // tier-assignment:create/update-Handler mit dem aufgelösten Context
22
+ // aufrufen.
23
+ //
24
+ // **Was Sprint 1 NICHT macht:**
25
+ // - Custom Domain-Events (`tier-changed`) — emittiert werden derzeit nur
26
+ // die CRUD-Auto-Events. Sprint 4 (Add-On-Marketplace) erweitert das auf
27
+ // semantische Domain-Events.
28
+ // - Add-Ons im Schema — Sprint 4 fügt sie als separate
29
+ // `tier-add-on`-Entity hinzu (1:n Relation, ES-saubere Add/Remove-Events).
30
+ // - Cap-Counter-Integration — kommt mit `cap-counter`-Feature in Sprint 3.
31
+ // - Stripe-Sync + Idempotent-Set-Tier-Wrapper — kommt mit `stripe-sync`-
32
+ // Feature in Sprint 5.
33
+ //
34
+ // **Boot-Dependencies:**
35
+ // r.requires("config") — transitiv für tenant.
36
+ // r.requires("tenant") — tier-assignment lebt im Plattform-Tenant-Kontext.
37
+
38
+ import {
39
+ defineEntityCreateHandler,
40
+ defineEntityListHandler,
41
+ defineEntityUpdateHandler,
42
+ defineFeature,
43
+ type FeatureDefinition,
44
+ } from "@cosmicdrift/kumiko-framework/engine";
45
+ import { TIER_ENGINE_FEATURE } from "./constants";
46
+ import { tierAssignmentEntity } from "./entity";
47
+ import { getActiveTierQuery } from "./handlers/active-tier.query";
48
+
49
+ const adminAccess = { access: { roles: ["TenantAdmin", "SystemAdmin"] } } as const;
50
+
51
+ export const tierEngineFeature: FeatureDefinition = defineFeature(TIER_ENGINE_FEATURE, (r) => {
52
+ r.requires("config");
53
+ r.requires("tenant");
54
+
55
+ r.entity("tier-assignment", tierAssignmentEntity);
56
+
57
+ // Standard-CRUD via Helper. Sprint 5 wraps these in a custom set-tier
58
+ // handler with deterministic aggregate-id for Stripe-Webhook idempotency.
59
+ r.writeHandler(defineEntityCreateHandler("tier-assignment", tierAssignmentEntity, adminAccess));
60
+ r.writeHandler(defineEntityUpdateHandler("tier-assignment", tierAssignmentEntity, adminAccess));
61
+
62
+ // Reads.
63
+ // - list: cross-tenant view for SystemAdmin (debug/migration-tooling)
64
+ // and per-tenant 0-or-1-row view for TenantAdmin (auto-tenant-scoped)
65
+ // - get-active-tier: convenience-wrapper for the only sensible per-tenant
66
+ // query — returns the single row or null. composeApp consumes this.
67
+ //
68
+ // Detail-by-id-handler bewusst weggelassen — kein Use-Case, weil pro Tenant
69
+ // genau eine Row existiert; get-active-tier ist die richtige Lookup-Form.
70
+ r.queryHandler(defineEntityListHandler("tier-assignment", tierAssignmentEntity, adminAccess));
71
+ r.queryHandler(getActiveTierQuery);
72
+ });
@@ -0,0 +1,23 @@
1
+ import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { z } from "zod";
3
+
4
+ // get-active-tier — return the current tier-assignment for the calling tenant.
5
+ //
6
+ // **Convenience-wrapper** über `ctx.queryProjection` für die häufigste Frage
7
+ // gegen die Engine: „welcher Tier ist gerade aktiv?". Returns null wenn
8
+ // noch kein Tier gesetzt — der Caller (composeApp) mappt null → Default-Tier.
9
+ //
10
+ // **Tenant-scope:** ctx.queryProjection filtert automatisch nach tenantId.
11
+ // Pro Plattform-Tenant existiert per Konvention genau eine
12
+ // tier-assignment-Row.
13
+ export const getActiveTierQuery: QueryHandlerDef = {
14
+ name: "get-active-tier",
15
+ schema: z.object({}),
16
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
17
+ handler: async (_query, ctx) => {
18
+ const rows = await ctx.queryProjection<Record<string, unknown>>(
19
+ "tier-engine:projection:tier-assignment-entity",
20
+ );
21
+ return rows[0] ?? null;
22
+ },
23
+ };
@@ -0,0 +1,22 @@
1
+ // Public API of the tier-engine bundled-feature.
2
+ //
3
+ // **What downstream apps import:**
4
+ // - `tierEngineFeature` — register at app boot via runProdApp/setupTestStack
5
+ // - `composeApp` — call to derive feature-set + caps from tier+addOns
6
+ // - `TierMap` / `AddOnMap` — types for the app's own tier/add-on definitions
7
+ // - `tierAssignmentEntity` — for migrations + drizzle-schema-generation
8
+ // - `TierEngineHandlers` / `TierEngineQueries` — qualified handler names
9
+
10
+ export { tierAssignmentAggregateId } from "./aggregate-id";
11
+ export {
12
+ type AddOnDefinition,
13
+ type AddOnMap,
14
+ type ComposeAppInput,
15
+ type ComposedApp,
16
+ composeApp,
17
+ type TierDefinition,
18
+ type TierMap,
19
+ } from "./compose-app";
20
+ export { TIER_ENGINE_FEATURE, TierEngineHandlers, TierEngineQueries } from "./constants";
21
+ export { tierAssignmentEntity } from "./entity";
22
+ export { tierEngineFeature } from "./feature";