@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type { DeliveryStatusValue } from "./constants";
|
|
2
|
+
export {
|
|
3
|
+
DELIVERY_FEATURE,
|
|
4
|
+
DeliveryErrors,
|
|
5
|
+
DeliveryHandlers,
|
|
6
|
+
DeliveryQueries,
|
|
7
|
+
DeliveryStatus,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
export {
|
|
10
|
+
collectChannels,
|
|
11
|
+
collectRenderers,
|
|
12
|
+
createDeliveryService,
|
|
13
|
+
type DeliveryServiceOptions,
|
|
14
|
+
type KillSwitchResolver,
|
|
15
|
+
type RateLimitConfig,
|
|
16
|
+
} from "./delivery-service";
|
|
17
|
+
export { createDeliveryFeature } from "./feature";
|
|
18
|
+
export { deliveryAttemptsTable, notificationPreferencesTable } from "./tables";
|
|
19
|
+
export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing";
|
|
20
|
+
export type {
|
|
21
|
+
ChannelContext,
|
|
22
|
+
ChannelMessage,
|
|
23
|
+
ChannelResult,
|
|
24
|
+
DeliveryChannel,
|
|
25
|
+
DeliveryLogEntry,
|
|
26
|
+
DeliveryService,
|
|
27
|
+
NotificationRenderer,
|
|
28
|
+
RendererInput,
|
|
29
|
+
} from "./types";
|
|
30
|
+
export {
|
|
31
|
+
createUnsubscribeRoute,
|
|
32
|
+
signUnsubscribeToken,
|
|
33
|
+
type UnsubscribeRouteOptions,
|
|
34
|
+
type UnsubscribeTokenPayload,
|
|
35
|
+
} from "./unsubscribe";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boolean,
|
|
3
|
+
buildBaseColumns,
|
|
4
|
+
instant,
|
|
5
|
+
table as pgTable,
|
|
6
|
+
text,
|
|
7
|
+
uniqueIndex,
|
|
8
|
+
uuid,
|
|
9
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
10
|
+
import {
|
|
11
|
+
createBooleanField,
|
|
12
|
+
createEntity,
|
|
13
|
+
createTextField,
|
|
14
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
+
import { sql } from "drizzle-orm";
|
|
16
|
+
|
|
17
|
+
// Delivery-log is an append-only stream of per-attempt records. The stream
|
|
18
|
+
// of truth lives in the events-Tabelle (one aggregate per attempt, event
|
|
19
|
+
// type `delivery:event:attempt`). An INLINE projection materialises each
|
|
20
|
+
// event into a row of deliveryAttemptsTable for the log-query handler —
|
|
21
|
+
// same TX as the event-append, so callers can read their own writes
|
|
22
|
+
// synchronously. No r.entity is registered for `deliveryAttempt`: the
|
|
23
|
+
// boot-validator accepts events-only projection sources as long as every
|
|
24
|
+
// apply-key is a registered domain-event (see registry.ts).
|
|
25
|
+
//
|
|
26
|
+
// PK = event aggregate-id (uuid). Keeps the projection row linked back to
|
|
27
|
+
// its event stream 1:1 — same convention as jobRunsTable + tenantSecretsTable.
|
|
28
|
+
// Event replays stay idempotent (primary-key conflict instead of duplicate rows).
|
|
29
|
+
export const deliveryAttemptsTable = pgTable("read_delivery_attempts", {
|
|
30
|
+
id: uuid("id").primaryKey(),
|
|
31
|
+
tenantId: uuid("tenant_id").notNull(),
|
|
32
|
+
notificationType: text("notification_type").notNull(),
|
|
33
|
+
channel: text("channel").notNull(),
|
|
34
|
+
// User-IDs as UUID-strings post-ES migration.
|
|
35
|
+
recipientId: text("recipient_id"),
|
|
36
|
+
recipientAddress: text("recipient_address"),
|
|
37
|
+
status: text("status").notNull().$type<"sent" | "failed" | "skipped">(),
|
|
38
|
+
error: text("error"),
|
|
39
|
+
createdAt: instant("created_at").default(sql`now()`).notNull(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// User-scoped opt-in/opt-out for (notificationType, channel) pairs. Post-ES
|
|
43
|
+
// refactor: each row is a notificationPreference aggregate with
|
|
44
|
+
// `.created / .updated / .deleted` lifecycle events written via the
|
|
45
|
+
// event-store executor. The unique index on (tenant, user, type, channel)
|
|
46
|
+
// is the effective natural key; the uuid PK is the aggregate id.
|
|
47
|
+
export const notificationPreferenceEntity = createEntity({
|
|
48
|
+
table: "read_notification_preferences",
|
|
49
|
+
fields: {
|
|
50
|
+
userId: createTextField({ required: true }),
|
|
51
|
+
notificationType: createTextField({ required: true }), // qualified name or "*"
|
|
52
|
+
channel: createTextField({ required: true }), // "inApp", "email", "push", or "*"
|
|
53
|
+
enabled: createBooleanField({ default: true }),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const notificationPreferencesTable = pgTable(
|
|
58
|
+
"read_notification_preferences",
|
|
59
|
+
{
|
|
60
|
+
...buildBaseColumns(false, "uuid"),
|
|
61
|
+
userId: text("user_id").notNull(),
|
|
62
|
+
notificationType: text("notification_type").notNull(),
|
|
63
|
+
channel: text("channel").notNull(),
|
|
64
|
+
enabled: boolean("enabled").default(true).notNull(),
|
|
65
|
+
},
|
|
66
|
+
(table) => [
|
|
67
|
+
uniqueIndex("read_notification_preferences_unique").on(
|
|
68
|
+
table.tenantId,
|
|
69
|
+
table.userId,
|
|
70
|
+
table.notificationType,
|
|
71
|
+
table.channel,
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { Redis } from "ioredis";
|
|
5
|
+
import type { KillSwitchResolver, RateLimitConfig } from "./delivery-service";
|
|
6
|
+
import { collectChannels, createDeliveryService } from "./delivery-service";
|
|
7
|
+
import type { DeliveryService } from "./types";
|
|
8
|
+
|
|
9
|
+
export type CreateDeliveryTestContextOptions = {
|
|
10
|
+
readonly tenantUserIdsQuery?: string;
|
|
11
|
+
readonly rateLimit?: RateLimitConfig;
|
|
12
|
+
readonly isChannelKilled?: KillSwitchResolver;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
|
|
17
|
+
* Abstracts the boilerplate that every Delivery-using test needs.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* setupTestStack({
|
|
21
|
+
* features: [...],
|
|
22
|
+
* extraContext: (deps) => createDeliveryTestContext(deps, { tenantUserIdsQuery: TenantQueries.resolveUserIds }),
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
export function createDeliveryTestContext(
|
|
26
|
+
deps: { registry: Registry; db: DbConnection; sseBroker: SseBroker; redis: Redis },
|
|
27
|
+
options: CreateDeliveryTestContextOptions = {},
|
|
28
|
+
): Record<string, unknown> & { deliveryService: DeliveryService } {
|
|
29
|
+
const { registry, db, sseBroker } = deps;
|
|
30
|
+
const channels = collectChannels(registry);
|
|
31
|
+
const deliveryService = createDeliveryService({
|
|
32
|
+
db,
|
|
33
|
+
registry,
|
|
34
|
+
sseBroker,
|
|
35
|
+
channels,
|
|
36
|
+
...options,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
deliveryService, // exposed so tests can inspect/call directly if needed
|
|
41
|
+
_notifyFactory:
|
|
42
|
+
(user: { id: number; tenantId: TenantId }, tenantId: TenantId) =>
|
|
43
|
+
(notificationType: string, notifyOptions: Record<string, unknown>) =>
|
|
44
|
+
// @cast-boundary engine-bridge — generic test-helper → typed entity-specific notify()
|
|
45
|
+
deliveryService.notify(notificationType, notifyOptions as never, user as never, tenantId),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type {
|
|
4
|
+
NotifyOptions,
|
|
5
|
+
Registry,
|
|
6
|
+
SessionUser,
|
|
7
|
+
TenantId,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
|
|
10
|
+
// --- Channel Interface ---
|
|
11
|
+
|
|
12
|
+
export type ChannelContext = {
|
|
13
|
+
readonly db: DbConnection;
|
|
14
|
+
readonly registry: Registry;
|
|
15
|
+
readonly sseBroker: SseBroker | undefined;
|
|
16
|
+
readonly tenantId: TenantId;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ChannelMessage = {
|
|
20
|
+
readonly notificationType: string;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly body: string | undefined;
|
|
23
|
+
readonly data: Readonly<Record<string, unknown>> | undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ChannelResult = {
|
|
27
|
+
readonly status: "sent" | "failed" | "skipped";
|
|
28
|
+
readonly error?: string;
|
|
29
|
+
readonly address?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type DeliveryChannel = {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
resolve(userId: string, ctx: ChannelContext): Promise<string | null>;
|
|
35
|
+
send(address: string, message: ChannelMessage, ctx: ChannelContext): Promise<ChannelResult>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// --- Notification Renderer ---
|
|
39
|
+
|
|
40
|
+
export type RendererInput = {
|
|
41
|
+
readonly template: string;
|
|
42
|
+
readonly variables: Readonly<Record<string, unknown>>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type NotificationRenderer = {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
render(input: RendererInput): Promise<string>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// --- Delivery Log Entry ---
|
|
51
|
+
|
|
52
|
+
export type DeliveryLogEntry = {
|
|
53
|
+
readonly tenantId: TenantId;
|
|
54
|
+
readonly notificationType: string;
|
|
55
|
+
readonly channel: string;
|
|
56
|
+
readonly recipientId: string | null;
|
|
57
|
+
readonly recipientAddress: string | null;
|
|
58
|
+
readonly status: "sent" | "failed" | "skipped";
|
|
59
|
+
readonly error: string | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// --- Delivery Service ---
|
|
63
|
+
|
|
64
|
+
export type DeliveryService = {
|
|
65
|
+
notify(
|
|
66
|
+
notificationType: string,
|
|
67
|
+
options: NotifyOptions,
|
|
68
|
+
user: SessionUser,
|
|
69
|
+
tenantId: TenantId,
|
|
70
|
+
): Promise<void>;
|
|
71
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import * as jose from "jose";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { upsertPreference } from "./upsert-preference";
|
|
7
|
+
|
|
8
|
+
// Shape des verified-JWT-payloads. tenantId kommt als string aus jose und
|
|
9
|
+
// wird NACH erfolgreichem parse() zur Branded TenantId — kein blind-cast.
|
|
10
|
+
const unsubscribeJwtPayloadSchema = z.object({
|
|
11
|
+
sub: z.string().min(1),
|
|
12
|
+
tenantId: z.string().min(1),
|
|
13
|
+
notificationType: z.string().min(1),
|
|
14
|
+
channel: z.string().min(1),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type UnsubscribeTokenPayload = {
|
|
18
|
+
readonly userId: string;
|
|
19
|
+
readonly tenantId: TenantId;
|
|
20
|
+
readonly notificationType: string;
|
|
21
|
+
readonly channel: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type UnsubscribeRouteOptions = {
|
|
25
|
+
readonly db: DbConnection;
|
|
26
|
+
readonly jwtSecret: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const UNSUBSCRIBE_EXPIRY = "7d";
|
|
30
|
+
// The route runs outside the dispatcher — no SessionUser, no JWT middleware.
|
|
31
|
+
// Bill the event against the token-subject (the user owns their preference)
|
|
32
|
+
// and attribute it as a system-role action. This mirrors the way jobs and
|
|
33
|
+
// seeds attribute their out-of-band writes.
|
|
34
|
+
const SYSTEM_ROLES = ["system"] as const;
|
|
35
|
+
|
|
36
|
+
export async function signUnsubscribeToken(
|
|
37
|
+
payload: UnsubscribeTokenPayload,
|
|
38
|
+
secret: string,
|
|
39
|
+
): Promise<string> {
|
|
40
|
+
const encodedSecret = new TextEncoder().encode(secret);
|
|
41
|
+
return new jose.SignJWT({
|
|
42
|
+
tenantId: payload.tenantId,
|
|
43
|
+
notificationType: payload.notificationType,
|
|
44
|
+
channel: payload.channel,
|
|
45
|
+
})
|
|
46
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
47
|
+
.setSubject(String(payload.userId))
|
|
48
|
+
.setIssuer("kumiko:unsubscribe")
|
|
49
|
+
.setIssuedAt()
|
|
50
|
+
.setExpirationTime(UNSUBSCRIBE_EXPIRY)
|
|
51
|
+
.sign(encodedSecret);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createUnsubscribeRoute(options: UnsubscribeRouteOptions): Hono {
|
|
55
|
+
const { db, jwtSecret } = options;
|
|
56
|
+
const encodedSecret = new TextEncoder().encode(jwtSecret);
|
|
57
|
+
const app = new Hono();
|
|
58
|
+
|
|
59
|
+
app.get("/unsubscribe", async (c) => {
|
|
60
|
+
const token = c.req.query("token");
|
|
61
|
+
if (!token) {
|
|
62
|
+
return c.text("Missing token", 400);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let userId: string;
|
|
66
|
+
let tenantId: TenantId;
|
|
67
|
+
let notificationType: string;
|
|
68
|
+
let channel: string;
|
|
69
|
+
try {
|
|
70
|
+
const { payload } = await jose.jwtVerify(token, encodedSecret, {
|
|
71
|
+
issuer: "kumiko:unsubscribe",
|
|
72
|
+
});
|
|
73
|
+
const parsed = unsubscribeJwtPayloadSchema.parse(payload);
|
|
74
|
+
userId = parsed.sub;
|
|
75
|
+
// @cast-boundary engine-bridge — string post-zod → branded TenantId
|
|
76
|
+
tenantId = parsed.tenantId as TenantId;
|
|
77
|
+
notificationType = parsed.notificationType;
|
|
78
|
+
channel = parsed.channel;
|
|
79
|
+
} catch {
|
|
80
|
+
return c.text("Invalid or expired token", 400);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Token-verify passed — everything below is a legitimate write. Don't
|
|
84
|
+
// swallow write-errors as "invalid token", that would mask real bugs
|
|
85
|
+
// (e.g. events-table missing, DB down) behind a misleading 400.
|
|
86
|
+
const actor = { id: userId, tenantId, roles: SYSTEM_ROLES };
|
|
87
|
+
const tdb = createTenantDb(db, tenantId, "system");
|
|
88
|
+
await upsertPreference(tdb, actor, {
|
|
89
|
+
tenantId,
|
|
90
|
+
userId,
|
|
91
|
+
notificationType,
|
|
92
|
+
channel,
|
|
93
|
+
enabled: false,
|
|
94
|
+
});
|
|
95
|
+
return c.text("You have been unsubscribed.", 200);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return app;
|
|
99
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Race-safe upsert for notification-preferences. Pre-ES this was a single
|
|
2
|
+
// `onConflictDoUpdate` statement on the preferences table; post-ES we go
|
|
3
|
+
// through the event-store executor, which doesn't offer a built-in upsert.
|
|
4
|
+
// Splitting into lookup + create|update re-opens the race window for
|
|
5
|
+
// concurrent requests — typical case: a user clicks the same "unsubscribe"
|
|
6
|
+
// email link three times in a second.
|
|
7
|
+
//
|
|
8
|
+
// The fix: try the optimistic path (lookup + create|update), and on a
|
|
9
|
+
// unique-index-violation race from a parallel create, re-lookup and fall
|
|
10
|
+
// through to update. Worst case: one extra roundtrip for the loser of
|
|
11
|
+
// the race. Happy path: same number of queries as the pre-ES upsert.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createEventStoreExecutor,
|
|
15
|
+
fetchOne,
|
|
16
|
+
type TenantDb,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
18
|
+
import type { SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
+
import { eq } from "drizzle-orm";
|
|
20
|
+
import { notificationPreferenceEntity, notificationPreferencesTable } from "./tables";
|
|
21
|
+
|
|
22
|
+
const executor = createEventStoreExecutor(
|
|
23
|
+
notificationPreferencesTable,
|
|
24
|
+
notificationPreferenceEntity,
|
|
25
|
+
{ entityName: "notification-preference" },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Konkretes Lookup-Result-Shape — nur die zwei Felder die der upsert
|
|
29
|
+
// tatsächlich für den Update-Path braucht. Vermeidet `row["x"] as T` index-
|
|
30
|
+
// access casts; der Generic-Param am fetchOne macht den Cast zentralisiert
|
|
31
|
+
// im Helper (db-row-boundary), nicht 4× pro Callsite.
|
|
32
|
+
type PreferenceLookupRow = { readonly id: string; readonly version: number };
|
|
33
|
+
|
|
34
|
+
async function lookup(
|
|
35
|
+
db: TenantDb,
|
|
36
|
+
tenantId: TenantId,
|
|
37
|
+
userId: string,
|
|
38
|
+
notificationType: string,
|
|
39
|
+
channel: string,
|
|
40
|
+
): Promise<PreferenceLookupRow | undefined> {
|
|
41
|
+
return fetchOne<PreferenceLookupRow>(
|
|
42
|
+
db,
|
|
43
|
+
notificationPreferencesTable,
|
|
44
|
+
eq(notificationPreferencesTable.tenantId, tenantId),
|
|
45
|
+
eq(notificationPreferencesTable.userId, userId),
|
|
46
|
+
eq(notificationPreferencesTable.notificationType, notificationType),
|
|
47
|
+
eq(notificationPreferencesTable.channel, channel),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type UpsertPreferenceInput = {
|
|
52
|
+
readonly tenantId: TenantId;
|
|
53
|
+
readonly userId: string;
|
|
54
|
+
readonly notificationType: string;
|
|
55
|
+
readonly channel: string;
|
|
56
|
+
readonly enabled: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Idempotent "set-this-preference-to-enabled-state" against the preferences
|
|
61
|
+
* aggregate stream. Emits either `.created` (first time) or `.updated`
|
|
62
|
+
* (subsequent) and catches the race-induced unique-index violation as a
|
|
63
|
+
* fallback to update.
|
|
64
|
+
*/
|
|
65
|
+
export async function upsertPreference(
|
|
66
|
+
db: TenantDb,
|
|
67
|
+
actor: SessionUser,
|
|
68
|
+
input: UpsertPreferenceInput,
|
|
69
|
+
): Promise<WriteResult<UpsertPreferenceInput>> {
|
|
70
|
+
const existing = await lookup(
|
|
71
|
+
db,
|
|
72
|
+
input.tenantId,
|
|
73
|
+
input.userId,
|
|
74
|
+
input.notificationType,
|
|
75
|
+
input.channel,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (existing) {
|
|
79
|
+
const result = await executor.update(
|
|
80
|
+
{
|
|
81
|
+
id: existing.id,
|
|
82
|
+
version: existing.version,
|
|
83
|
+
changes: { enabled: input.enabled },
|
|
84
|
+
},
|
|
85
|
+
actor,
|
|
86
|
+
db,
|
|
87
|
+
);
|
|
88
|
+
if (!result.isSuccess) return result;
|
|
89
|
+
return { isSuccess: true, data: input };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result = await executor.create(
|
|
94
|
+
{
|
|
95
|
+
userId: input.userId,
|
|
96
|
+
notificationType: input.notificationType,
|
|
97
|
+
channel: input.channel,
|
|
98
|
+
enabled: input.enabled,
|
|
99
|
+
},
|
|
100
|
+
actor,
|
|
101
|
+
db,
|
|
102
|
+
);
|
|
103
|
+
if (!result.isSuccess) return result;
|
|
104
|
+
return { isSuccess: true, data: input };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Race-fallback: another request beat us to the insert between our
|
|
107
|
+
// lookup and the executor.create. The unique-index on
|
|
108
|
+
// (tenant, user, type, channel) fires Postgres error 23505. Only that
|
|
109
|
+
// specific error triggers the retry; DB-disconnect or any other
|
|
110
|
+
// failure must bubble up unchanged so callers see the real cause.
|
|
111
|
+
if (!isUniqueViolation(err)) throw err;
|
|
112
|
+
const afterRace = await lookup(
|
|
113
|
+
db,
|
|
114
|
+
input.tenantId,
|
|
115
|
+
input.userId,
|
|
116
|
+
input.notificationType,
|
|
117
|
+
input.channel,
|
|
118
|
+
);
|
|
119
|
+
if (!afterRace) throw err;
|
|
120
|
+
const result = await executor.update(
|
|
121
|
+
{
|
|
122
|
+
id: afterRace.id,
|
|
123
|
+
version: afterRace.version,
|
|
124
|
+
changes: { enabled: input.enabled },
|
|
125
|
+
},
|
|
126
|
+
actor,
|
|
127
|
+
db,
|
|
128
|
+
);
|
|
129
|
+
if (!result.isSuccess) return result;
|
|
130
|
+
return { isSuccess: true, data: input };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Narrow detection for Postgres "duplicate key violates unique constraint"
|
|
135
|
+
// (SQLSTATE 23505). Drivers wrap the DB error in varying envelopes; we
|
|
136
|
+
// check the `code` field plus a string-match fallback so the match survives
|
|
137
|
+
// minor driver-version shifts without drifting wide.
|
|
138
|
+
function isUniqueViolation(err: unknown): boolean {
|
|
139
|
+
if (typeof err !== "object" || err === null) return false;
|
|
140
|
+
const e = err as { code?: unknown; cause?: { code?: unknown }; message?: unknown };
|
|
141
|
+
if (e.code === "23505") return true;
|
|
142
|
+
if (e.cause && typeof e.cause === "object" && e.cause.code === "23505") return true;
|
|
143
|
+
if (typeof e.message === "string" && e.message.includes("23505")) return true;
|
|
144
|
+
return false;
|
|
145
|
+
}
|