@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.
- package/package.json +90 -0
- package/src/audit/__tests__/audit.integration.ts +328 -0
- package/src/audit/constants.ts +7 -0
- package/src/audit/feature.ts +23 -0
- package/src/audit/handlers/list.query.ts +98 -0
- package/src/audit/index.ts +2 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
- package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
- package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
- package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
- package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
- package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
- package/src/auth-email-password/auth-user-row.ts +41 -0
- package/src/auth-email-password/constants.ts +101 -0
- package/src/auth-email-password/email-templates.ts +283 -0
- package/src/auth-email-password/feature.ts +140 -0
- package/src/auth-email-password/handlers/change-password.write.ts +58 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
- package/src/auth-email-password/handlers/login.write.ts +208 -0
- package/src/auth-email-password/handlers/logout.write.ts +12 -0
- package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
- package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
- package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
- package/src/auth-email-password/i18n.ts +211 -0
- package/src/auth-email-password/identity-v3-hash.ts +97 -0
- package/src/auth-email-password/index.ts +35 -0
- package/src/auth-email-password/invite-token-store.ts +92 -0
- package/src/auth-email-password/lockout-store.ts +118 -0
- package/src/auth-email-password/password-hashing.ts +43 -0
- package/src/auth-email-password/reset-token.ts +28 -0
- package/src/auth-email-password/seeding.ts +183 -0
- package/src/auth-email-password/signed-token.ts +85 -0
- package/src/auth-email-password/signup-token-store.ts +104 -0
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/testing.ts +5 -0
- package/src/auth-email-password/token-burn-store.ts +57 -0
- package/src/auth-email-password/verification-token.ts +27 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
- package/src/auth-email-password/web/auth-client.ts +350 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
- package/src/auth-email-password/web/auth-gate.tsx +33 -0
- package/src/auth-email-password/web/client-plugin.ts +48 -0
- package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
- package/src/auth-email-password/web/index.ts +56 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
- package/src/auth-email-password/web/login-screen.tsx +150 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
- package/src/auth-email-password/web/session.tsx +171 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
- package/src/auth-email-password/web/signup-screen.tsx +130 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
- package/src/auth-email-password/web/use-shell-user.ts +34 -0
- package/src/auth-email-password/web/user-menu.tsx +89 -0
- package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
- package/src/billing-foundation/__tests__/feature.test.ts +110 -0
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
- package/src/billing-foundation/aggregate-id.ts +21 -0
- package/src/billing-foundation/constants.ts +70 -0
- package/src/billing-foundation/entities.ts +50 -0
- package/src/billing-foundation/events.ts +71 -0
- package/src/billing-foundation/feature.ts +122 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
- package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
- package/src/billing-foundation/handlers/process-event.write.ts +160 -0
- package/src/billing-foundation/index.ts +42 -0
- package/src/billing-foundation/projection.ts +135 -0
- package/src/billing-foundation/types.ts +157 -0
- package/src/billing-foundation/webhook-handler.ts +184 -0
- package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
- package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
- package/src/cap-counter/aggregate-id.ts +61 -0
- package/src/cap-counter/constants.ts +32 -0
- package/src/cap-counter/enforce-cap.ts +404 -0
- package/src/cap-counter/entity.ts +48 -0
- package/src/cap-counter/feature.ts +90 -0
- package/src/cap-counter/handlers/get-counter.query.ts +43 -0
- package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
- package/src/cap-counter/handlers/increment.write.ts +92 -0
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
- package/src/cap-counter/index.ts +34 -0
- package/src/cap-counter/with-cap-enforcement.ts +179 -0
- package/src/channel-email/email-channel.ts +48 -0
- package/src/channel-email/feature.ts +15 -0
- package/src/channel-email/index.ts +4 -0
- package/src/channel-email/smtp-transport.ts +65 -0
- package/src/channel-email/types.ts +34 -0
- package/src/channel-in-app/constants.ts +11 -0
- package/src/channel-in-app/feature.ts +30 -0
- package/src/channel-in-app/handlers/inbox.query.ts +28 -0
- package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
- package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
- package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
- package/src/channel-in-app/in-app-channel.ts +44 -0
- package/src/channel-in-app/index.ts +4 -0
- package/src/channel-in-app/tables.ts +22 -0
- package/src/channel-push/feature.ts +15 -0
- package/src/channel-push/index.ts +3 -0
- package/src/channel-push/push-channel.ts +33 -0
- package/src/channel-push/types.ts +22 -0
- package/src/config/__tests__/app-overrides.test.ts +118 -0
- package/src/config/__tests__/config.integration.ts +1246 -0
- package/src/config/constants.ts +23 -0
- package/src/config/feature.ts +117 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
- package/src/config/handlers/reset.write.ts +45 -0
- package/src/config/handlers/schema.query.ts +22 -0
- package/src/config/handlers/set.write.ts +93 -0
- package/src/config/handlers/values.query.ts +43 -0
- package/src/config/index.ts +15 -0
- package/src/config/resolver.ts +283 -0
- package/src/config/table.ts +35 -0
- package/src/config/write-helpers.ts +268 -0
- package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
- package/src/delivery/__tests__/delivery.integration.ts +1405 -0
- package/src/delivery/constants.ts +33 -0
- package/src/delivery/delivery-service.ts +489 -0
- package/src/delivery/events.ts +18 -0
- package/src/delivery/feature.ts +70 -0
- package/src/delivery/handlers/log.query.ts +21 -0
- package/src/delivery/handlers/preferences.query.ts +18 -0
- package/src/delivery/handlers/set-preference.write.ts +28 -0
- package/src/delivery/index.ts +35 -0
- package/src/delivery/tables.ts +74 -0
- package/src/delivery/testing.ts +47 -0
- package/src/delivery/types.ts +71 -0
- package/src/delivery/unsubscribe.ts +99 -0
- package/src/delivery/upsert-preference.ts +145 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
- package/src/feature-toggles/constants.ts +20 -0
- package/src/feature-toggles/events.ts +18 -0
- package/src/feature-toggles/feature.ts +98 -0
- package/src/feature-toggles/global-feature-state-table.ts +28 -0
- package/src/feature-toggles/handlers/list.query.ts +26 -0
- package/src/feature-toggles/handlers/registered.query.ts +56 -0
- package/src/feature-toggles/handlers/set.write.ts +158 -0
- package/src/feature-toggles/index.ts +9 -0
- package/src/feature-toggles/toggle-runtime.ts +73 -0
- package/src/file-foundation/__tests__/feature.test.ts +35 -0
- package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
- package/src/file-foundation/feature.ts +123 -0
- package/src/file-foundation/index.ts +7 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
- package/src/file-provider-inmemory/feature.ts +73 -0
- package/src/file-provider-inmemory/index.ts +3 -0
- package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
- package/src/file-provider-s3/feature.ts +169 -0
- package/src/file-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
- package/src/files-provider-s3/env-helper.ts +49 -0
- package/src/files-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/s3-provider.ts +114 -0
- package/src/foundation-shared/config-helpers.ts +67 -0
- package/src/foundation-shared/index.ts +4 -0
- package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
- package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
- package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
- package/src/jobs/constants.ts +21 -0
- package/src/jobs/events.ts +39 -0
- package/src/jobs/feature.ts +150 -0
- package/src/jobs/handlers/detail.query.ts +30 -0
- package/src/jobs/handlers/list.query.ts +36 -0
- package/src/jobs/handlers/retry.write.ts +69 -0
- package/src/jobs/handlers/trigger.write.ts +39 -0
- package/src/jobs/index.ts +5 -0
- package/src/jobs/job-run-logger.ts +213 -0
- package/src/jobs/job-run-table.ts +55 -0
- package/src/legal-pages/README.md +195 -0
- package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
- package/src/legal-pages/constants.ts +36 -0
- package/src/legal-pages/feature.ts +187 -0
- package/src/legal-pages/index.ts +13 -0
- package/src/legal-pages/markdown.ts +69 -0
- package/src/mail-foundation/__tests__/feature.test.ts +46 -0
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
- package/src/mail-foundation/feature.ts +160 -0
- package/src/mail-foundation/index.ts +14 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
- package/src/mail-transport-inmemory/feature.ts +90 -0
- package/src/mail-transport-inmemory/index.ts +3 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
- package/src/mail-transport-smtp/feature.ts +182 -0
- package/src/mail-transport-smtp/index.ts +3 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
- package/src/rate-limiting/constants.ts +9 -0
- package/src/rate-limiting/feature.ts +16 -0
- package/src/rate-limiting/handlers/status.query.ts +52 -0
- package/src/rate-limiting/index.ts +2 -0
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
- package/src/renderer-simple/feature.ts +12 -0
- package/src/renderer-simple/index.ts +2 -0
- package/src/renderer-simple/simple-renderer.ts +72 -0
- package/src/secrets/__tests__/rotate.integration.ts +176 -0
- package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
- package/src/secrets/__tests__/secrets.integration.ts +118 -0
- package/src/secrets/feature.ts +84 -0
- package/src/secrets/handlers/delete.write.ts +20 -0
- package/src/secrets/handlers/list.query.ts +38 -0
- package/src/secrets/handlers/rotate.job.ts +193 -0
- package/src/secrets/handlers/set.write.ts +50 -0
- package/src/secrets/index.ts +16 -0
- package/src/secrets/secrets-context.ts +296 -0
- package/src/secrets/table.ts +68 -0
- package/src/sessions/__tests__/cleanup.integration.ts +175 -0
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
- package/src/sessions/__tests__/sessions.integration.ts +472 -0
- package/src/sessions/__tests__/test-helpers.ts +66 -0
- package/src/sessions/constants.ts +43 -0
- package/src/sessions/feature.ts +84 -0
- package/src/sessions/handlers/cleanup.job.ts +109 -0
- package/src/sessions/handlers/list.query.ts +35 -0
- package/src/sessions/handlers/mine.query.ts +37 -0
- package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
- package/src/sessions/handlers/revoke.write.ts +76 -0
- package/src/sessions/index.ts +17 -0
- package/src/sessions/schema/index.ts +5 -0
- package/src/sessions/schema/user-session.ts +67 -0
- package/src/sessions/session-callbacks.ts +110 -0
- package/src/sessions/testing.ts +42 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
- package/src/subscription-mollie/constants.ts +33 -0
- package/src/subscription-mollie/feature.ts +144 -0
- package/src/subscription-mollie/index.ts +13 -0
- package/src/subscription-mollie/plugin-methods.ts +79 -0
- package/src/subscription-mollie/verify-webhook.ts +244 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
- package/src/subscription-stripe/constants.ts +20 -0
- package/src/subscription-stripe/feature.ts +120 -0
- package/src/subscription-stripe/index.ts +14 -0
- package/src/subscription-stripe/plugin-methods.ts +91 -0
- package/src/subscription-stripe/verify-webhook.ts +235 -0
- package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
- package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
- package/src/tenant/__tests__/tenant.integration.ts +347 -0
- package/src/tenant/command-schemas.ts +37 -0
- package/src/tenant/constants.ts +37 -0
- package/src/tenant/feature.ts +109 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
- package/src/tenant/handlers/add-member.write.ts +53 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
- package/src/tenant/handlers/create.write.ts +21 -0
- package/src/tenant/handlers/disable.write.ts +18 -0
- package/src/tenant/handlers/invitations.query.ts +31 -0
- package/src/tenant/handlers/list.query.ts +17 -0
- package/src/tenant/handlers/me.query.ts +17 -0
- package/src/tenant/handlers/members.query.ts +22 -0
- package/src/tenant/handlers/memberships.query.ts +24 -0
- package/src/tenant/handlers/remove-member.write.ts +40 -0
- package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
- package/src/tenant/handlers/update-member-roles.write.ts +54 -0
- package/src/tenant/handlers/update.write.ts +20 -0
- package/src/tenant/index.ts +12 -0
- package/src/tenant/invitation-table.ts +93 -0
- package/src/tenant/membership-table.ts +35 -0
- package/src/tenant/schema/index.ts +5 -0
- package/src/tenant/schema/tenant.ts +27 -0
- package/src/tenant/seeding.ts +155 -0
- package/src/tenant/testing.ts +8 -0
- package/src/text-content/README.md +190 -0
- package/src/text-content/__tests__/text-content.integration.ts +415 -0
- package/src/text-content/api.ts +92 -0
- package/src/text-content/constants.ts +19 -0
- package/src/text-content/feature.ts +29 -0
- package/src/text-content/handlers/by-slug.query.ts +55 -0
- package/src/text-content/handlers/set.write.ts +118 -0
- package/src/text-content/index.ts +14 -0
- package/src/text-content/seeding.ts +91 -0
- package/src/text-content/table.ts +45 -0
- package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
- package/src/tier-engine/__tests__/drift.test.ts +42 -0
- package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
- package/src/tier-engine/aggregate-id.ts +27 -0
- package/src/tier-engine/compose-app.ts +150 -0
- package/src/tier-engine/constants.ts +15 -0
- package/src/tier-engine/entity.ts +30 -0
- package/src/tier-engine/feature.ts +72 -0
- package/src/tier-engine/handlers/active-tier.query.ts +23 -0
- package/src/tier-engine/index.ts +22 -0
- package/src/user/__tests__/seed-testing.integration.ts +127 -0
- package/src/user/__tests__/user.integration.ts +198 -0
- package/src/user/command-schemas.ts +15 -0
- package/src/user/constants.ts +23 -0
- package/src/user/feature.ts +32 -0
- package/src/user/handlers/create.write.ts +54 -0
- package/src/user/handlers/detail.query.ts +9 -0
- package/src/user/handlers/find-for-auth.query.ts +38 -0
- package/src/user/handlers/list.query.ts +8 -0
- package/src/user/handlers/me.query.ts +15 -0
- package/src/user/handlers/update.write.ts +54 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/index.ts +5 -0
- package/src/user/schema/user.ts +69 -0
- package/src/user/seeding.ts +93 -0
- package/src/user/testing.ts +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
|
+
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/cosmicdriftgamestudio/kumiko-framework.git",
|
|
10
|
+
"directory": "packages/bundled-features"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cosmicdriftgamestudio/kumiko-framework/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://kumiko.so",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"kumiko": {
|
|
18
|
+
"runtime": "runtime"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
"./audit": "./src/audit/index.ts",
|
|
22
|
+
"./config": "./src/config/index.ts",
|
|
23
|
+
"./jobs": "./src/jobs/index.ts",
|
|
24
|
+
"./tier-engine": "./src/tier-engine/index.ts",
|
|
25
|
+
"./cap-counter": "./src/cap-counter/index.ts",
|
|
26
|
+
"./billing-foundation": "./src/billing-foundation/index.ts",
|
|
27
|
+
"./subscription-stripe": "./src/subscription-stripe/index.ts",
|
|
28
|
+
"./subscription-mollie": "./src/subscription-mollie/index.ts",
|
|
29
|
+
"./foundation-shared": "./src/foundation-shared/index.ts",
|
|
30
|
+
"./mail-foundation": "./src/mail-foundation/index.ts",
|
|
31
|
+
"./mail-transport-smtp": "./src/mail-transport-smtp/index.ts",
|
|
32
|
+
"./mail-transport-inmemory": "./src/mail-transport-inmemory/index.ts",
|
|
33
|
+
"./file-foundation": "./src/file-foundation/index.ts",
|
|
34
|
+
"./file-provider-s3": "./src/file-provider-s3/index.ts",
|
|
35
|
+
"./file-provider-inmemory": "./src/file-provider-inmemory/index.ts",
|
|
36
|
+
"./tenant": "./src/tenant/index.ts",
|
|
37
|
+
"./tenant/constants": "./src/tenant/constants.ts",
|
|
38
|
+
"./tenant/seeding": "./src/tenant/seeding.ts",
|
|
39
|
+
"./tenant/testing": "./src/tenant/testing.ts",
|
|
40
|
+
"./user": "./src/user/index.ts",
|
|
41
|
+
"./user/seeding": "./src/user/seeding.ts",
|
|
42
|
+
"./user/testing": "./src/user/testing.ts",
|
|
43
|
+
"./auth-email-password": "./src/auth-email-password/index.ts",
|
|
44
|
+
"./auth-email-password/constants": "./src/auth-email-password/constants.ts",
|
|
45
|
+
"./auth-email-password/seeding": "./src/auth-email-password/seeding.ts",
|
|
46
|
+
"./auth-email-password/testing": "./src/auth-email-password/testing.ts",
|
|
47
|
+
"./auth-email-password/web": "./src/auth-email-password/web/index.ts",
|
|
48
|
+
"./delivery": "./src/delivery/index.ts",
|
|
49
|
+
"./channel-in-app": "./src/channel-in-app/index.ts",
|
|
50
|
+
"./channel-email": "./src/channel-email/index.ts",
|
|
51
|
+
"./channel-push": "./src/channel-push/index.ts",
|
|
52
|
+
"./renderer-simple": "./src/renderer-simple/index.ts",
|
|
53
|
+
"./files-provider-s3": "./src/files-provider-s3/index.ts",
|
|
54
|
+
"./rate-limiting": "./src/rate-limiting/index.ts",
|
|
55
|
+
"./secrets": "./src/secrets/index.ts",
|
|
56
|
+
"./sessions": "./src/sessions/index.ts",
|
|
57
|
+
"./sessions/testing": "./src/sessions/testing.ts",
|
|
58
|
+
"./feature-toggles": "./src/feature-toggles/index.ts",
|
|
59
|
+
"./text-content": "./src/text-content/index.ts",
|
|
60
|
+
"./text-content/seeding": "./src/text-content/seeding.ts",
|
|
61
|
+
"./legal-pages": "./src/legal-pages/index.ts"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@aws-sdk/client-s3": "^3.700.0",
|
|
65
|
+
"@aws-sdk/s3-request-presigner": "^3.700.0",
|
|
66
|
+
"@cosmicdrift/kumiko-dispatcher-live": "workspace:*",
|
|
67
|
+
"@cosmicdrift/kumiko-framework": "workspace:*",
|
|
68
|
+
"@cosmicdrift/kumiko-renderer": "workspace:*",
|
|
69
|
+
"@cosmicdrift/kumiko-renderer-web": "workspace:*",
|
|
70
|
+
"@mollie/api-client": "^4.5.0",
|
|
71
|
+
"@node-rs/argon2": "^2.0.2",
|
|
72
|
+
"@types/nodemailer": "^8.0.0",
|
|
73
|
+
"clsx": "^2.1.1",
|
|
74
|
+
"lucide-react": "^1.11.0",
|
|
75
|
+
"marked": "^14.1.3",
|
|
76
|
+
"nodemailer": "^8.0.7",
|
|
77
|
+
"react": "^19.2.0",
|
|
78
|
+
"stripe": "^22.1.0",
|
|
79
|
+
"tailwind-merge": "^3.0.2"
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"registry": "https://registry.npmjs.org",
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"files": [
|
|
86
|
+
"src",
|
|
87
|
+
"README.md",
|
|
88
|
+
"LICENSE"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// Audit query — filter-by-example coverage over the event-store. The event
|
|
2
|
+
// log IS the audit trail; this suite proves the query handler exposes the
|
|
3
|
+
// right slices of it (tenant-isolated, filtered, paginated, content-intact).
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createEntity,
|
|
7
|
+
createTextField,
|
|
8
|
+
defineEntityWriteHandler,
|
|
9
|
+
defineFeature,
|
|
10
|
+
type SessionUser,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
12
|
+
import {
|
|
13
|
+
createEntityTable,
|
|
14
|
+
createTestUser,
|
|
15
|
+
resetEventStore,
|
|
16
|
+
setupTestStack,
|
|
17
|
+
type TestStack,
|
|
18
|
+
TestUsers,
|
|
19
|
+
testTenantId,
|
|
20
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
+
import { sql } from "drizzle-orm";
|
|
22
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
23
|
+
import { AuditQueries } from "../constants";
|
|
24
|
+
import { createAuditFeature } from "../feature";
|
|
25
|
+
|
|
26
|
+
const widgetEntity = createEntity({
|
|
27
|
+
table: "audit_widgets",
|
|
28
|
+
fields: {
|
|
29
|
+
name: createTextField({ required: true }),
|
|
30
|
+
color: createTextField(),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const widgetFeature = defineFeature("widgets", (r) => {
|
|
35
|
+
r.entity("widget", widgetEntity);
|
|
36
|
+
for (const verb of ["create", "update", "delete"] as const) {
|
|
37
|
+
r.writeHandler(
|
|
38
|
+
defineEntityWriteHandler(`widget:${verb}`, widgetEntity, {
|
|
39
|
+
access: { roles: ["Admin", "User", "SystemAdmin"] },
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let stack: TestStack;
|
|
46
|
+
|
|
47
|
+
const admin = TestUsers.systemAdmin;
|
|
48
|
+
const regularUser: SessionUser = createTestUser({
|
|
49
|
+
id: 7,
|
|
50
|
+
tenantId: testTenantId(1),
|
|
51
|
+
roles: ["User"],
|
|
52
|
+
});
|
|
53
|
+
const otherTenantAdmin: SessionUser = createTestUser({
|
|
54
|
+
id: 8,
|
|
55
|
+
tenantId: testTenantId(2),
|
|
56
|
+
roles: ["Admin"],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
beforeAll(async () => {
|
|
60
|
+
stack = await setupTestStack({
|
|
61
|
+
features: [widgetFeature, createAuditFeature()],
|
|
62
|
+
});
|
|
63
|
+
await createEntityTable(stack.db, widgetEntity);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterAll(async () => {
|
|
67
|
+
await stack.cleanup();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
beforeEach(async () => {
|
|
71
|
+
// Fresh event log per test — the audit query reads the events table
|
|
72
|
+
// directly, so stale events from previous tests would leak into results.
|
|
73
|
+
await resetEventStore(stack);
|
|
74
|
+
await stack.db.execute(sql`TRUNCATE audit_widgets`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
async function createWidget(user: SessionUser, name: string, color?: string): Promise<string> {
|
|
78
|
+
const res = await stack.http.writeOk<{ id: string }>(
|
|
79
|
+
"widgets:write:widget:create",
|
|
80
|
+
{ name, ...(color && { color }) },
|
|
81
|
+
user,
|
|
82
|
+
);
|
|
83
|
+
return res.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type AuditRow = {
|
|
87
|
+
id: string;
|
|
88
|
+
aggregateId: string;
|
|
89
|
+
aggregateType: string;
|
|
90
|
+
type: string;
|
|
91
|
+
createdBy: string;
|
|
92
|
+
createdAt: string;
|
|
93
|
+
payload: Record<string, unknown>;
|
|
94
|
+
metadata: Record<string, unknown>;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type AuditResponse = { rows: AuditRow[]; nextBefore: string | null };
|
|
98
|
+
|
|
99
|
+
describe("audit: list query", () => {
|
|
100
|
+
test("returns events of the caller's tenant, newest first", async () => {
|
|
101
|
+
await createWidget(admin, "A");
|
|
102
|
+
await createWidget(admin, "B");
|
|
103
|
+
await createWidget(admin, "C");
|
|
104
|
+
|
|
105
|
+
const res = await stack.http.queryOk<AuditResponse>(AuditQueries.list, {}, admin);
|
|
106
|
+
expect(res.rows.length).toBeGreaterThanOrEqual(3);
|
|
107
|
+
const names = res.rows.map((r) => r.type);
|
|
108
|
+
expect(names).toContain("widget.created");
|
|
109
|
+
// Descending by id (bigserial) ⇒ newest first.
|
|
110
|
+
for (let i = 1; i < res.rows.length; i++) {
|
|
111
|
+
const prev = BigInt(res.rows[i - 1]!.id);
|
|
112
|
+
const curr = BigInt(res.rows[i]!.id);
|
|
113
|
+
expect(prev > curr).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("tenant isolation: admin on tenant-1 sees NO events from tenant-2", async () => {
|
|
118
|
+
await createWidget(admin, "on-tenant-1");
|
|
119
|
+
await stack.http.writeOk<{ id: string }>(
|
|
120
|
+
"widgets:write:widget:create",
|
|
121
|
+
{ name: "on-tenant-2" },
|
|
122
|
+
otherTenantAdmin,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const res = await stack.http.queryOk<AuditResponse>(AuditQueries.list, {}, admin);
|
|
126
|
+
for (const r of res.rows) {
|
|
127
|
+
// Only admin's rows come back — the cross-tenant event's createdBy
|
|
128
|
+
// would be the other-tenant admin's id.
|
|
129
|
+
expect(r.createdBy).toBe(admin.id);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("filter by eventType", async () => {
|
|
134
|
+
const id1 = await createWidget(admin, "X");
|
|
135
|
+
await stack.http.writeOk(
|
|
136
|
+
"widgets:write:widget:update",
|
|
137
|
+
{ id: id1, version: 1, changes: { color: "red" } },
|
|
138
|
+
admin,
|
|
139
|
+
);
|
|
140
|
+
await stack.http.writeOk("widgets:write:widget:delete", { id: id1 }, admin);
|
|
141
|
+
|
|
142
|
+
const updates = await stack.http.queryOk<AuditResponse>(
|
|
143
|
+
AuditQueries.list,
|
|
144
|
+
{ eventType: "widget.updated" },
|
|
145
|
+
admin,
|
|
146
|
+
);
|
|
147
|
+
expect(updates.rows).toHaveLength(1);
|
|
148
|
+
expect(updates.rows[0]?.type).toBe("widget.updated");
|
|
149
|
+
|
|
150
|
+
const deletes = await stack.http.queryOk<AuditResponse>(
|
|
151
|
+
AuditQueries.list,
|
|
152
|
+
{ eventType: "widget.deleted" },
|
|
153
|
+
admin,
|
|
154
|
+
);
|
|
155
|
+
expect(deletes.rows).toHaveLength(1);
|
|
156
|
+
expect(deletes.rows[0]?.type).toBe("widget.deleted");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("filter by aggregateId pins the event chain for one entity", async () => {
|
|
160
|
+
const a = await createWidget(admin, "A");
|
|
161
|
+
const b = await createWidget(admin, "B");
|
|
162
|
+
await stack.http.writeOk(
|
|
163
|
+
"widgets:write:widget:update",
|
|
164
|
+
{ id: a, version: 1, changes: { color: "blue" } },
|
|
165
|
+
admin,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const res = await stack.http.queryOk<AuditResponse>(
|
|
169
|
+
AuditQueries.list,
|
|
170
|
+
{ aggregateId: a },
|
|
171
|
+
admin,
|
|
172
|
+
);
|
|
173
|
+
expect(res.rows).toHaveLength(2);
|
|
174
|
+
expect(res.rows.every((r) => r.aggregateId === a)).toBe(true);
|
|
175
|
+
expect(res.rows.some((r) => r.aggregateId === b)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("filter by userId", async () => {
|
|
179
|
+
await createWidget(admin, "by admin");
|
|
180
|
+
await createWidget(regularUser, "by user");
|
|
181
|
+
|
|
182
|
+
const res = await stack.http.queryOk<AuditResponse>(
|
|
183
|
+
AuditQueries.list,
|
|
184
|
+
{ userId: regularUser.id },
|
|
185
|
+
admin,
|
|
186
|
+
);
|
|
187
|
+
expect(res.rows).toHaveLength(1);
|
|
188
|
+
expect(res.rows[0]?.createdBy).toBe(regularUser.id);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("filter by from/to date range (inclusive bounds, outside-range rows excluded)", async () => {
|
|
192
|
+
// Events are written with server-now at ms precision. Delays between
|
|
193
|
+
// writes + anchor timestamps give us clean sort-order. The anchors are
|
|
194
|
+
// captured OUTSIDE the write bursts so precision-truncation on the
|
|
195
|
+
// db side (ms) can't blur anchor vs event.
|
|
196
|
+
await createWidget(admin, "before-window");
|
|
197
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
198
|
+
const t1 = Temporal.Now.instant();
|
|
199
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
200
|
+
await createWidget(admin, "in-window-1");
|
|
201
|
+
await createWidget(admin, "in-window-2");
|
|
202
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
203
|
+
const t2 = Temporal.Now.instant();
|
|
204
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
205
|
+
await createWidget(admin, "after-window");
|
|
206
|
+
|
|
207
|
+
// Slice strictly to the [t1, t2] window — should return exactly 2 rows.
|
|
208
|
+
const inWindow = await stack.http.queryOk<AuditResponse>(
|
|
209
|
+
AuditQueries.list,
|
|
210
|
+
{ from: t1.toString(), to: t2.toString() },
|
|
211
|
+
admin,
|
|
212
|
+
);
|
|
213
|
+
expect(inWindow.rows).toHaveLength(2);
|
|
214
|
+
const names = inWindow.rows.map((r) => (r.payload as { name?: string }).name).sort();
|
|
215
|
+
expect(names).toEqual(["in-window-1", "in-window-2"]);
|
|
216
|
+
|
|
217
|
+
// From-only: everything at or after t1 → 3 rows (2 in-window + 1 after).
|
|
218
|
+
const sinceT1 = await stack.http.queryOk<AuditResponse>(
|
|
219
|
+
AuditQueries.list,
|
|
220
|
+
{ from: t1.toString() },
|
|
221
|
+
admin,
|
|
222
|
+
);
|
|
223
|
+
expect(sinceT1.rows).toHaveLength(3);
|
|
224
|
+
|
|
225
|
+
// To-only: everything at or before t1 → just the before-window row.
|
|
226
|
+
const untilT1 = await stack.http.queryOk<AuditResponse>(
|
|
227
|
+
AuditQueries.list,
|
|
228
|
+
{ to: t1.toString() },
|
|
229
|
+
admin,
|
|
230
|
+
);
|
|
231
|
+
expect(untilT1.rows).toHaveLength(1);
|
|
232
|
+
expect((untilT1.rows[0]?.payload as { name?: string }).name).toBe("before-window");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("rejects inverted from/to range with validation_error", async () => {
|
|
236
|
+
// from > to would silently return empty — confusing. Schema-level refine
|
|
237
|
+
// turns it into a clean 400 at the gate.
|
|
238
|
+
const res = await stack.http.query(
|
|
239
|
+
AuditQueries.list,
|
|
240
|
+
{ from: "2030-01-01T00:00:00Z", to: "2020-01-01T00:00:00Z" },
|
|
241
|
+
admin,
|
|
242
|
+
);
|
|
243
|
+
expect(res.status).toBe(400);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("rejects non-numeric cursor with validation_error (no PG crash path)", async () => {
|
|
247
|
+
// Pre-fix the handler interpolated `before` directly as bigint, so
|
|
248
|
+
// "abc" would raise an uncaught invalid_text_representation from PG.
|
|
249
|
+
// The schema regex catches it at the gate.
|
|
250
|
+
const res = await stack.http.query(AuditQueries.list, { before: "not-a-number" }, admin);
|
|
251
|
+
expect(res.status).toBe(400);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("pagination: limit + nextBefore cursor walks the log", async () => {
|
|
255
|
+
for (let i = 0; i < 5; i++) {
|
|
256
|
+
await createWidget(admin, `W${i}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const page1 = await stack.http.queryOk<AuditResponse>(AuditQueries.list, { limit: 2 }, admin);
|
|
260
|
+
expect(page1.rows).toHaveLength(2);
|
|
261
|
+
expect(page1.nextBefore).not.toBeNull();
|
|
262
|
+
|
|
263
|
+
const page2 = await stack.http.queryOk<AuditResponse>(
|
|
264
|
+
AuditQueries.list,
|
|
265
|
+
{ limit: 2, before: page1.nextBefore },
|
|
266
|
+
admin,
|
|
267
|
+
);
|
|
268
|
+
expect(page2.rows).toHaveLength(2);
|
|
269
|
+
const page1Ids = page1.rows.map((r) => r.id);
|
|
270
|
+
const page2Ids = page2.rows.map((r) => r.id);
|
|
271
|
+
for (const id of page2Ids) expect(page1Ids).not.toContain(id);
|
|
272
|
+
|
|
273
|
+
const page3 = await stack.http.queryOk<AuditResponse>(
|
|
274
|
+
AuditQueries.list,
|
|
275
|
+
{ limit: 2, before: page2.nextBefore },
|
|
276
|
+
admin,
|
|
277
|
+
);
|
|
278
|
+
// 5 events, 2+2+1: final page is partial ⇒ nextBefore null.
|
|
279
|
+
expect(page3.rows).toHaveLength(1);
|
|
280
|
+
expect(page3.nextBefore).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("response carries the full event payload + metadata (the audit-relevant detail)", async () => {
|
|
284
|
+
const id = await createWidget(admin, "Auditable", "green");
|
|
285
|
+
await stack.http.writeOk(
|
|
286
|
+
"widgets:write:widget:update",
|
|
287
|
+
{ id, version: 1, changes: { color: "yellow" } },
|
|
288
|
+
admin,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const res = await stack.http.queryOk<AuditResponse>(
|
|
292
|
+
AuditQueries.list,
|
|
293
|
+
{ aggregateId: id },
|
|
294
|
+
admin,
|
|
295
|
+
);
|
|
296
|
+
// Two events on this stream: created + updated. Newest first.
|
|
297
|
+
expect(res.rows).toHaveLength(2);
|
|
298
|
+
const [updated, created] = res.rows;
|
|
299
|
+
|
|
300
|
+
// created: payload IS the initial entity snapshot.
|
|
301
|
+
expect(created?.type).toBe("widget.created");
|
|
302
|
+
expect(created?.payload).toMatchObject({ name: "Auditable", color: "green" });
|
|
303
|
+
// metadata carries the actor (userId) for the write.
|
|
304
|
+
expect(created?.metadata).toMatchObject({ userId: admin.id });
|
|
305
|
+
|
|
306
|
+
// updated: payload is { changes, previous } — both halves matter for audit.
|
|
307
|
+
expect(updated?.type).toBe("widget.updated");
|
|
308
|
+
expect(updated?.payload).toMatchObject({
|
|
309
|
+
changes: { color: "yellow" },
|
|
310
|
+
previous: expect.objectContaining({ color: "green", name: "Auditable" }),
|
|
311
|
+
});
|
|
312
|
+
expect(updated?.metadata).toMatchObject({ userId: admin.id });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("access denied for non-admin roles", async () => {
|
|
316
|
+
await createWidget(admin, "A");
|
|
317
|
+
// regularUser has role "User" — the handler requires Admin/SystemAdmin.
|
|
318
|
+
const res = await stack.http.query(AuditQueries.list, {}, regularUser);
|
|
319
|
+
expect(res.status).toBe(403);
|
|
320
|
+
const body = (await res.json()) as {
|
|
321
|
+
error?: { code?: string; details?: { reason?: string } };
|
|
322
|
+
};
|
|
323
|
+
// Pin the specific failure class. The framework raises AccessDeniedError
|
|
324
|
+
// with code=access_denied; asserting on `code` beats a status-only check
|
|
325
|
+
// (a 403 could also come from ownership-denied, for example).
|
|
326
|
+
expect(body.error?.code).toBe("access_denied");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { listQuery } from "./handlers/list.query";
|
|
3
|
+
|
|
4
|
+
// Audit feature — exposes a filtered read over the framework's event log.
|
|
5
|
+
//
|
|
6
|
+
// Design: the event-store IS the audit trail (every entity write produces
|
|
7
|
+
// an event with who/when/what/where/delta). This feature adds no persistence,
|
|
8
|
+
// no projection, no cursor — it's a single privileged query handler over
|
|
9
|
+
// the existing `events` table. See handlers/list.query.ts for the filter
|
|
10
|
+
// surface.
|
|
11
|
+
//
|
|
12
|
+
// Retention lives elsewhere. Events are kept indefinitely as the source of
|
|
13
|
+
// truth for state; archive or compress policies are a separate concern
|
|
14
|
+
// (tracked with the snapshot/archive infrastructure that already exists in
|
|
15
|
+
// the framework).
|
|
16
|
+
export function createAuditFeature(): FeatureDefinition {
|
|
17
|
+
return defineFeature("audit", (r) => {
|
|
18
|
+
const queries = {
|
|
19
|
+
list: r.queryHandler(listQuery),
|
|
20
|
+
};
|
|
21
|
+
return { queries };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Audit query — reads the event-store's `events` table directly. The event-
|
|
2
|
+
// log IS the audit trail by construction: every entity write appends at least
|
|
3
|
+
// one event with createdBy (who), createdAt (when), tenantId (where),
|
|
4
|
+
// aggregateType + aggregateId (what), type (action), and payload (delta).
|
|
5
|
+
//
|
|
6
|
+
// No projection, no separate audit table. Queryable with the same filter
|
|
7
|
+
// surface any audit UI needs; tenant-isolated at the WHERE level so cross-
|
|
8
|
+
// tenant peeking is structurally impossible for non-SystemAdmin callers.
|
|
9
|
+
//
|
|
10
|
+
// Sensitive field-values are already stripped out of payloads at event-
|
|
11
|
+
// append time (see event-store-executor → stripSensitive), so this query
|
|
12
|
+
// can't surface PII that the entity definition marked as sensitive.
|
|
13
|
+
|
|
14
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
+
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
16
|
+
import { and, desc, eq, gte, lt, lte } from "drizzle-orm";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
// Per-page cap. 100 keeps a single page payload bounded while being enough
|
|
20
|
+
// for a humans-browse UI — clients that need exports iterate by `before`.
|
|
21
|
+
const MAX_LIMIT = 100;
|
|
22
|
+
|
|
23
|
+
export const listQuery = defineQueryHandler({
|
|
24
|
+
name: "list",
|
|
25
|
+
schema: z
|
|
26
|
+
.object({
|
|
27
|
+
// Cursor-style pagination: pass the `id` from the last row of the
|
|
28
|
+
// previous page as `before`. bigserial ids are monotonic, so `< before`
|
|
29
|
+
// reliably returns "the next older page". Beats OFFSET on large tables.
|
|
30
|
+
// The regex pins the input to digits-only — otherwise an invalid value
|
|
31
|
+
// would surface as a raw PG `invalid_text_representation` instead of a
|
|
32
|
+
// clean 400 at the schema gate.
|
|
33
|
+
before: z.string().regex(/^\d+$/, "cursor must be a positive integer").optional(),
|
|
34
|
+
limit: z.number().int().min(1).max(MAX_LIMIT).default(50),
|
|
35
|
+
// Filters — all optional. Combined via AND.
|
|
36
|
+
aggregateType: z.string().optional(),
|
|
37
|
+
aggregateId: z.uuid().optional(),
|
|
38
|
+
eventType: z.string().optional(),
|
|
39
|
+
// createdBy is stored as text on the events table (it accepts both UUIDs
|
|
40
|
+
// and system actor strings like "SYSTEM"), so the filter is a plain
|
|
41
|
+
// equality check on the raw value.
|
|
42
|
+
userId: z.string().optional(),
|
|
43
|
+
// Inclusive bounds. Clients pass ISO-8601; we parse to Temporal.Instant
|
|
44
|
+
// and compare via the `instant()` column type.
|
|
45
|
+
from: z.iso.datetime().optional(),
|
|
46
|
+
to: z.iso.datetime().optional(),
|
|
47
|
+
})
|
|
48
|
+
.refine((v) => !v.from || !v.to || v.from <= v.to, {
|
|
49
|
+
message: "`from` must be less than or equal to `to`",
|
|
50
|
+
path: ["from"],
|
|
51
|
+
}),
|
|
52
|
+
access: { roles: ["Admin", "SystemAdmin"] },
|
|
53
|
+
handler: async (query, ctx) => {
|
|
54
|
+
const p = query.payload;
|
|
55
|
+
const tenantId = query.user.tenantId;
|
|
56
|
+
|
|
57
|
+
const conditions = [eq(eventsTable.tenantId, tenantId)];
|
|
58
|
+
if (p.aggregateType) conditions.push(eq(eventsTable.aggregateType, p.aggregateType));
|
|
59
|
+
if (p.aggregateId) conditions.push(eq(eventsTable.aggregateId, p.aggregateId));
|
|
60
|
+
if (p.eventType) conditions.push(eq(eventsTable.type, p.eventType));
|
|
61
|
+
if (p.userId) conditions.push(eq(eventsTable.createdBy, p.userId));
|
|
62
|
+
if (p.from) conditions.push(gte(eventsTable.createdAt, Temporal.Instant.from(p.from)));
|
|
63
|
+
if (p.to) conditions.push(lte(eventsTable.createdAt, Temporal.Instant.from(p.to)));
|
|
64
|
+
// `before` = last seen id from the previous page. bigserial so `<` walks
|
|
65
|
+
// backwards in chronological order. Schema-regex guarantees the string
|
|
66
|
+
// is digits-only, so BigInt(...) can't throw.
|
|
67
|
+
if (p.before) conditions.push(lt(eventsTable.id, BigInt(p.before)));
|
|
68
|
+
|
|
69
|
+
const rows = await ctx.db
|
|
70
|
+
.select({
|
|
71
|
+
id: eventsTable.id,
|
|
72
|
+
aggregateId: eventsTable.aggregateId,
|
|
73
|
+
aggregateType: eventsTable.aggregateType,
|
|
74
|
+
version: eventsTable.version,
|
|
75
|
+
type: eventsTable.type,
|
|
76
|
+
payload: eventsTable.payload,
|
|
77
|
+
metadata: eventsTable.metadata,
|
|
78
|
+
createdAt: eventsTable.createdAt,
|
|
79
|
+
createdBy: eventsTable.createdBy,
|
|
80
|
+
})
|
|
81
|
+
.from(eventsTable)
|
|
82
|
+
.where(and(...conditions))
|
|
83
|
+
.orderBy(desc(eventsTable.id))
|
|
84
|
+
.limit(p.limit);
|
|
85
|
+
|
|
86
|
+
// bigint ids need serialisation — JSON can't carry a plain BigInt, and
|
|
87
|
+
// clients pass the cursor back as a string via `before`. Stringified once
|
|
88
|
+
// here so the response shape matches what the caller will re-submit.
|
|
89
|
+
const serialised = rows.map((r) => ({ ...r, id: String(r["id"]) }));
|
|
90
|
+
const last = serialised[serialised.length - 1];
|
|
91
|
+
return {
|
|
92
|
+
rows: serialised,
|
|
93
|
+
// Cursor for the NEXT page. Null when this page is partial (we hit
|
|
94
|
+
// the start of the log) so clients know to stop.
|
|
95
|
+
nextBefore: serialised.length === p.limit && last ? last["id"] : null,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|