@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,39 @@
|
|
|
1
|
+
import type { DbRow } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
export const triggerWrite = defineWriteHandler({
|
|
8
|
+
name: "trigger",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
jobName: z.string(),
|
|
11
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
12
|
+
}),
|
|
13
|
+
access: { roles: ["SystemAdmin"] },
|
|
14
|
+
handler: async (event, ctx) => {
|
|
15
|
+
const registry = ctx.registry;
|
|
16
|
+
// `jobRunner` is a dynamic context extension — not a core HandlerContext field.
|
|
17
|
+
const jobRunner = ctx["jobRunner"] as JobRunner;
|
|
18
|
+
|
|
19
|
+
const jobDef = registry.getJob(event.payload.jobName);
|
|
20
|
+
if (!jobDef) {
|
|
21
|
+
return writeFailure(
|
|
22
|
+
new NotFoundError("job", event.payload.jobName, {
|
|
23
|
+
i18nKey: "jobs.errors.unknownJob",
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const payload = (event.payload.payload ?? {}) as DbRow;
|
|
29
|
+
const bullJobId = await jobRunner.dispatch(event.payload.jobName, payload, {
|
|
30
|
+
triggeredById: event.user.id,
|
|
31
|
+
payload: JSON.stringify(payload),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
isSuccess: true,
|
|
36
|
+
data: { jobName: event.payload.jobName, bullJobId },
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createJobsFeature } from "./feature";
|
|
2
|
+
export type { JobRunLoggerCallbacks } from "./job-run-logger";
|
|
3
|
+
export { createJobRunLogger } from "./job-run-logger";
|
|
4
|
+
export type { JobLogLevel, JobRunStatus } from "./job-run-table";
|
|
5
|
+
export { jobRunLogsTable, jobRunsTable } from "./job-run-table";
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { type Registry, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { append, getStreamVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
|
+
import type { JobLogEntry, JobMeta, JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
|
|
5
|
+
import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
6
|
+
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
7
|
+
import { eq } from "drizzle-orm";
|
|
8
|
+
import { runCompletedSchema, runFailedSchema, runStartedSchema } from "./events";
|
|
9
|
+
import { jobRunsTable } from "./job-run-table";
|
|
10
|
+
|
|
11
|
+
// ES job-run lifecycle:
|
|
12
|
+
// - onJobStart → jobs:event:run-started (first append, version 0→1)
|
|
13
|
+
// - onJobComplete → jobs:event:run-completed (append at current version,
|
|
14
|
+
// payload carries the batched logs)
|
|
15
|
+
// - onJobFailed → jobs:event:run-failed (same shape as completed + error)
|
|
16
|
+
//
|
|
17
|
+
// BullMQ callbacks don't carry a tenantId (jobs are cross-tenant). We
|
|
18
|
+
// anchor every run on SYSTEM_TENANT_ID — mirrors how config system-scope
|
|
19
|
+
// rows use the sentinel. The stream still works per-run because
|
|
20
|
+
// aggregate_id is a fresh UUID per run.
|
|
21
|
+
|
|
22
|
+
export const JOB_RUN_STARTED_EVENT = "jobs:event:run-started" as const;
|
|
23
|
+
export const JOB_RUN_COMPLETED_EVENT = "jobs:event:run-completed" as const;
|
|
24
|
+
export const JOB_RUN_FAILED_EVENT = "jobs:event:run-failed" as const;
|
|
25
|
+
|
|
26
|
+
export type JobRunLoggerOptions = {
|
|
27
|
+
readonly db: DbConnection;
|
|
28
|
+
readonly registry: Registry;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type JobRunLoggerCallbacks = Pick<
|
|
32
|
+
JobRunnerOptions,
|
|
33
|
+
"onJobStart" | "onJobComplete" | "onJobFailed"
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
// Default cap on the bullJobId → runId cache. A worker that starts jobs
|
|
37
|
+
// without ever seeing complete/failed callbacks (e.g. crashes mid-run)
|
|
38
|
+
// would otherwise leak entries indefinitely. 10k fits ~1 hour of
|
|
39
|
+
// high-throughput jobs; past that we evict oldest. DB-lookup recovers
|
|
40
|
+
// evicted entries, so correctness isn't at stake — only memory bounds.
|
|
41
|
+
const DEFAULT_CACHE_MAX_ENTRIES = 10_000;
|
|
42
|
+
// Entry TTL. A run that hangs longer than this is either a real stuck
|
|
43
|
+
// worker (ops should alert) or a test-environment run that never fired
|
|
44
|
+
// complete/failed; either way the cache entry has no value. Falls back
|
|
45
|
+
// to DB-lookup if actually needed.
|
|
46
|
+
const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
47
|
+
|
|
48
|
+
export function createJobRunLogger(opts: JobRunLoggerOptions): JobRunLoggerCallbacks {
|
|
49
|
+
const { db, registry } = opts;
|
|
50
|
+
|
|
51
|
+
// bullJobId → aggregate uuid. BullMQ hands us the bullJobId on every
|
|
52
|
+
// callback, but our aggregate stream is keyed by a fresh UUID we mint
|
|
53
|
+
// on start. The cache threads that UUID from onJobStart through to
|
|
54
|
+
// onJobComplete/onJobFailed so the completion-event lands on the same
|
|
55
|
+
// stream as the start-event.
|
|
56
|
+
//
|
|
57
|
+
// Bounded cache (LRU-ish with TTL) — worker-crash between start and
|
|
58
|
+
// complete would otherwise leak entries. DB-lookup recovers evicted
|
|
59
|
+
// entries via bull_job_id on the projection.
|
|
60
|
+
type CacheEntry = { readonly runId: string; readonly expiresAt: number };
|
|
61
|
+
const runIdByBullJobId = new Map<string, CacheEntry>();
|
|
62
|
+
|
|
63
|
+
function cachePut(bullJobId: string, runId: string): void {
|
|
64
|
+
// Enforce max-size BEFORE insert. Map iteration returns insertion
|
|
65
|
+
// order, so dropping the first entry is the oldest.
|
|
66
|
+
if (runIdByBullJobId.size >= DEFAULT_CACHE_MAX_ENTRIES) {
|
|
67
|
+
const oldest = runIdByBullJobId.keys().next().value;
|
|
68
|
+
if (oldest !== undefined) runIdByBullJobId.delete(oldest);
|
|
69
|
+
}
|
|
70
|
+
runIdByBullJobId.set(bullJobId, {
|
|
71
|
+
runId,
|
|
72
|
+
expiresAt: Date.now() + DEFAULT_CACHE_TTL_MS,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cacheGet(bullJobId: string): string | undefined {
|
|
77
|
+
const entry = runIdByBullJobId.get(bullJobId);
|
|
78
|
+
if (!entry) return undefined;
|
|
79
|
+
if (Date.now() >= entry.expiresAt) {
|
|
80
|
+
runIdByBullJobId.delete(bullJobId); // immediate cleanup on terminal callback
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return entry.runId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function resolveRunId(bullJobId: string): Promise<string | undefined> {
|
|
87
|
+
const cached = cacheGet(bullJobId);
|
|
88
|
+
if (cached) return cached;
|
|
89
|
+
const [row] = await db
|
|
90
|
+
.select({ id: jobRunsTable.id })
|
|
91
|
+
.from(jobRunsTable)
|
|
92
|
+
.where(eq(jobRunsTable.bullJobId, bullJobId));
|
|
93
|
+
// buildBaseColumns's signature types `id` as `string | number` because
|
|
94
|
+
// it returns both branches of the idType union. We know this table
|
|
95
|
+
// was built with idType: "uuid" (see job-run-table.ts), so narrowing
|
|
96
|
+
// via String() is safe runtime-wise. A proper framework-level fix
|
|
97
|
+
// would overload buildBaseColumns per idType — scoped out of this
|
|
98
|
+
// follow-up as its return type has four branches (with/without
|
|
99
|
+
// softDelete × serial/uuid).
|
|
100
|
+
const id = row ? String(row.id) : undefined;
|
|
101
|
+
if (id) cachePut(bullJobId, id);
|
|
102
|
+
return id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
onJobStart: async (jobName: string, bullJobId: string, meta: JobMeta) => {
|
|
107
|
+
const runId = generateId();
|
|
108
|
+
cachePut(bullJobId, runId);
|
|
109
|
+
// Parse against the registered schema so out-of-dispatcher writes
|
|
110
|
+
// get the same validation guarantee as ctx.appendEvent. A shape
|
|
111
|
+
// drift between feature + logger fails loudly at the source
|
|
112
|
+
// instead of silently landing on the events-table.
|
|
113
|
+
const payload = runStartedSchema.parse({
|
|
114
|
+
jobName,
|
|
115
|
+
bullJobId,
|
|
116
|
+
status: "running",
|
|
117
|
+
payload: meta.payload ?? null,
|
|
118
|
+
triggeredById: meta.triggeredById ?? null,
|
|
119
|
+
startedAt: Temporal.Now.instant().toString(),
|
|
120
|
+
attempt: meta.attempt ?? 1,
|
|
121
|
+
});
|
|
122
|
+
const event = await append(db, {
|
|
123
|
+
aggregateId: runId,
|
|
124
|
+
aggregateType: "jobRun",
|
|
125
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
126
|
+
expectedVersion: 0,
|
|
127
|
+
type: JOB_RUN_STARTED_EVENT,
|
|
128
|
+
payload,
|
|
129
|
+
metadata: { userId: "system" },
|
|
130
|
+
});
|
|
131
|
+
await runProjectionsForEvent(event, registry, db);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
onJobComplete: async (
|
|
135
|
+
_jobName: string,
|
|
136
|
+
bullJobId: string,
|
|
137
|
+
duration: number,
|
|
138
|
+
logs: JobLogEntry[],
|
|
139
|
+
) => {
|
|
140
|
+
const runId = await resolveRunId(bullJobId);
|
|
141
|
+
// skip: state loss between start + complete (worker restart, cache
|
|
142
|
+
// evicted AND DB has no matching bull_job_id). Rare edge case; we
|
|
143
|
+
// drop the completion event rather than forging a jobRun aggregate
|
|
144
|
+
// from scratch — forensics still has the original BullMQ lifecycle.
|
|
145
|
+
if (!runId) return;
|
|
146
|
+
const currentVersion = await getStreamVersion(db, runId, SYSTEM_TENANT_ID);
|
|
147
|
+
const payload = runCompletedSchema.parse({
|
|
148
|
+
duration,
|
|
149
|
+
finishedAt: Temporal.Now.instant().toString(),
|
|
150
|
+
logs: logs.map((l) => ({
|
|
151
|
+
level: l.level,
|
|
152
|
+
message: l.message,
|
|
153
|
+
timestamp: l.timestamp.toString(),
|
|
154
|
+
})),
|
|
155
|
+
});
|
|
156
|
+
const event = await append(db, {
|
|
157
|
+
aggregateId: runId,
|
|
158
|
+
aggregateType: "jobRun",
|
|
159
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
160
|
+
expectedVersion: currentVersion,
|
|
161
|
+
type: JOB_RUN_COMPLETED_EVENT,
|
|
162
|
+
payload,
|
|
163
|
+
metadata: { userId: "system" },
|
|
164
|
+
});
|
|
165
|
+
await runProjectionsForEvent(event, registry, db);
|
|
166
|
+
runIdByBullJobId.delete(bullJobId); // immediate cleanup on terminal callback
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
onJobFailed: async (
|
|
170
|
+
_jobName: string,
|
|
171
|
+
bullJobId: string,
|
|
172
|
+
error: string,
|
|
173
|
+
logs: JobLogEntry[],
|
|
174
|
+
) => {
|
|
175
|
+
const runId = await resolveRunId(bullJobId);
|
|
176
|
+
// skip: same rare state-loss case as in onJobComplete — drop the
|
|
177
|
+
// failure event rather than forge a jobRun aggregate from scratch.
|
|
178
|
+
if (!runId) return;
|
|
179
|
+
const currentVersion = await getStreamVersion(db, runId, SYSTEM_TENANT_ID);
|
|
180
|
+
// Read started_at off the projection so we can compute duration
|
|
181
|
+
// symmetrically to onJobComplete (which gets duration from the
|
|
182
|
+
// worker). The projection already has started_at from the
|
|
183
|
+
// run-started inline-apply.
|
|
184
|
+
const [row] = await db
|
|
185
|
+
.select({ startedAt: jobRunsTable.startedAt })
|
|
186
|
+
.from(jobRunsTable)
|
|
187
|
+
.where(eq(jobRunsTable.id, runId));
|
|
188
|
+
const now = Temporal.Now.instant();
|
|
189
|
+
const duration = row ? Number(now.since(row.startedAt).total({ unit: "millisecond" })) : 0;
|
|
190
|
+
const payload = runFailedSchema.parse({
|
|
191
|
+
duration,
|
|
192
|
+
finishedAt: now.toString(),
|
|
193
|
+
error,
|
|
194
|
+
logs: logs.map((l) => ({
|
|
195
|
+
level: l.level,
|
|
196
|
+
message: l.message,
|
|
197
|
+
timestamp: l.timestamp.toString(),
|
|
198
|
+
})),
|
|
199
|
+
});
|
|
200
|
+
const event = await append(db, {
|
|
201
|
+
aggregateId: runId,
|
|
202
|
+
aggregateType: "jobRun",
|
|
203
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
204
|
+
expectedVersion: currentVersion,
|
|
205
|
+
type: JOB_RUN_FAILED_EVENT,
|
|
206
|
+
payload,
|
|
207
|
+
metadata: { userId: "system" },
|
|
208
|
+
});
|
|
209
|
+
await runProjectionsForEvent(event, registry, db);
|
|
210
|
+
runIdByBullJobId.delete(bullJobId); // immediate cleanup on terminal callback
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildBaseColumns,
|
|
3
|
+
instant,
|
|
4
|
+
integer,
|
|
5
|
+
table as pgTable,
|
|
6
|
+
serial,
|
|
7
|
+
text,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
+
|
|
10
|
+
export type JobRunStatus = "queued" | "running" | "completed" | "failed";
|
|
11
|
+
export type JobLogLevel = "info" | "warn" | "error";
|
|
12
|
+
|
|
13
|
+
// jobRun is a system-scoped events-only aggregate: every job execution is
|
|
14
|
+
// its own stream, driven entirely by BullMQ-callbacks (onJobStart /
|
|
15
|
+
// -Complete / -Failed) via the low-level append() path. Three domain-
|
|
16
|
+
// events cover the lifecycle:
|
|
17
|
+
// - `jobs:event:run-started` (when BullMQ picks a job off its queue)
|
|
18
|
+
// - `jobs:event:run-completed` (duration + batched log entries)
|
|
19
|
+
// - `jobs:event:run-failed` (error + duration + batched log entries)
|
|
20
|
+
//
|
|
21
|
+
// Logs ride the completed/failed event as an array — "Option B" from the
|
|
22
|
+
// design discussion: one event per run instead of N events per log line,
|
|
23
|
+
// no log duplication across status transitions. The inline projection
|
|
24
|
+
// expands the batch into N rows in jobRunLogsTable, keeping the pre-ES
|
|
25
|
+
// detail-query-shape intact.
|
|
26
|
+
//
|
|
27
|
+
// No r.entity is registered for `jobRun` — the boot-validator accepts
|
|
28
|
+
// events-only projection sources where every apply-key is a registered
|
|
29
|
+
// domain-event (see registry.ts).
|
|
30
|
+
export const jobRunsTable = pgTable("read_job_runs", {
|
|
31
|
+
...buildBaseColumns(false, "uuid"),
|
|
32
|
+
jobName: text("job_name").notNull(),
|
|
33
|
+
bullJobId: text("bull_job_id").notNull(),
|
|
34
|
+
status: text("status").notNull().$type<JobRunStatus>(),
|
|
35
|
+
payload: text("payload"),
|
|
36
|
+
error: text("error"),
|
|
37
|
+
attempt: integer("attempt").default(1).notNull(),
|
|
38
|
+
startedAt: instant("started_at").notNull(),
|
|
39
|
+
finishedAt: instant("finished_at"),
|
|
40
|
+
duration: integer("duration"),
|
|
41
|
+
triggeredById: text("triggered_by_id"),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Child projection keyed by the jobRun aggregate id. Pre-ES used a serial
|
|
45
|
+
// PK + integer runId; post-ES runId is still exposed but now holds the
|
|
46
|
+
// uuid of the parent jobRun. Existing detail-query callers treat it as an
|
|
47
|
+
// opaque identifier, so the type-switch is backward-compatible at the
|
|
48
|
+
// query surface.
|
|
49
|
+
export const jobRunLogsTable = pgTable("read_job_run_logs", {
|
|
50
|
+
id: serial("id").primaryKey(),
|
|
51
|
+
runId: text("run_id").notNull(),
|
|
52
|
+
level: text("level").notNull().$type<JobLogLevel>(),
|
|
53
|
+
message: text("message").notNull(),
|
|
54
|
+
timestamp: instant("timestamp").notNull(),
|
|
55
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# legal-pages
|
|
2
|
+
|
|
3
|
+
Opt-in wrapper around [`text-content`](../text-content/) for
|
|
4
|
+
DACH compliance. Ships four fixed public HTML routes
|
|
5
|
+
(`/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`,
|
|
6
|
+
`/legal/privacy`) with Markdown→HTML rendering and a boot check that
|
|
7
|
+
hard-fails in production when the DE required blocks aren't seeded.
|
|
8
|
+
|
|
9
|
+
**Opt-in.** Internal tools, US apps without an imprint requirement,
|
|
10
|
+
or hobby projects without public access simply don't activate the
|
|
11
|
+
feature.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createLegalPagesFeature } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
|
|
19
|
+
import {
|
|
20
|
+
createTextContentApi,
|
|
21
|
+
createTextContentFeature,
|
|
22
|
+
} from "@cosmicdrift/kumiko-bundled-features/text-content";
|
|
23
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
24
|
+
|
|
25
|
+
runProdApp({
|
|
26
|
+
features: [
|
|
27
|
+
createTextContentFeature(), // legal-pages requires text-content
|
|
28
|
+
createLegalPagesFeature(),
|
|
29
|
+
/* ... */
|
|
30
|
+
],
|
|
31
|
+
// Two wirings are required:
|
|
32
|
+
// 1. anonymousAccess for /legal/* routes (run without a JWT)
|
|
33
|
+
// 2. extraContext.textContent for the boot check (cross-feature
|
|
34
|
+
// decoupling — legal-pages imports no code from text-content,
|
|
35
|
+
// only uses the API via ctx)
|
|
36
|
+
anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
|
|
37
|
+
extraContext: ({ db }) => ({
|
|
38
|
+
textContent: createTextContentApi(db),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### Production table setup
|
|
46
|
+
|
|
47
|
+
legal-pages doesn't have its own table — it uses text-content's
|
|
48
|
+
`read_text_blocks`. Table setup therefore goes through text-content:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
yarn kumiko migrate generate # text-block entity is detected
|
|
52
|
+
yarn kumiko migrate apply
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See [text-content/README.md](../text-content/README.md#production-table-setup).
|
|
56
|
+
|
|
57
|
+
## Routes
|
|
58
|
+
|
|
59
|
+
| Path | Slug + lang | Title fallback (when block empty) |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `GET /legal/impressum` | `imprint` / `de` | "Impressum" |
|
|
62
|
+
| `GET /legal/datenschutz` | `privacy` / `de` | "Datenschutzerklärung" |
|
|
63
|
+
| `GET /legal/imprint` | `imprint` / `en` | "Imprint" |
|
|
64
|
+
| `GET /legal/privacy` | `privacy` / `en` | "Privacy Policy" |
|
|
65
|
+
|
|
66
|
+
Response:
|
|
67
|
+
- `200 text/html` — block exists + has body. Cache header `public, max-age=300`.
|
|
68
|
+
- `404 text/plain` — block missing. Hint: "Tenant admin must set this text block".
|
|
69
|
+
- `503 text/plain` — `app.fetch` to `/api/query` failed (anonymousAccess missing?).
|
|
70
|
+
|
|
71
|
+
Layout: a minimal HTML5 skeleton with inline CSS — apps that want to
|
|
72
|
+
integrate into their own layout use `text-content:query:by-slug`
|
|
73
|
+
directly and render themselves.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Boot check
|
|
78
|
+
|
|
79
|
+
`r.job` with `runOnBoot: true` checks at app start whether the DE
|
|
80
|
+
required blocks exist in SYSTEM_TENANT:
|
|
81
|
+
|
|
82
|
+
| Slug + lang | What happens when missing |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `imprint` / `de` | **Production:** `throw new Error(...)` blocks app start. **Dev:** `ctx.log.warn(...)` |
|
|
85
|
+
| `privacy` / `de` | as above |
|
|
86
|
+
|
|
87
|
+
EN versions are **not** boot-fail-relevant (`LEGAL_OPTIONAL_BLOCKS`).
|
|
88
|
+
Routes return `404` if an EN block is missing.
|
|
89
|
+
|
|
90
|
+
→ Apps that activate the feature must seed both DE blocks before a
|
|
91
|
+
production deploy — either via a bootstrap script (`seedTextBlock`) or
|
|
92
|
+
manually via the TenantAdmin API.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## TenantAdmin maintenance via the API
|
|
97
|
+
|
|
98
|
+
Tenant admins (or platform SystemAdmin for SYSTEM_TENANT texts) can
|
|
99
|
+
update content at any time through the standard write handler:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// From the tenant admin frontend (or admin curl):
|
|
103
|
+
await fetch("/api/write", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
type: "text-content:write:set",
|
|
108
|
+
payload: {
|
|
109
|
+
slug: "imprint",
|
|
110
|
+
lang: "de",
|
|
111
|
+
title: "Impressum",
|
|
112
|
+
body: "## Angaben gemäß § 5 TMG\n\n...",
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
→ Idempotent: a second call with the same `(slug, lang)` updates the block.
|
|
119
|
+
ACL: `roles: ["TenantAdmin", "SystemAdmin"]` — SystemAdmin (a global
|
|
120
|
+
role) may set SYSTEM_TENANT texts, TenantAdmin only tenant-owned ones.
|
|
121
|
+
|
|
122
|
+
→ The route's cache header is `public, max-age=300` — after an update,
|
|
123
|
+
visitors see new content within 5 minutes at most. If you need
|
|
124
|
+
instant visibility, you can help things along with a CDN purge.
|
|
125
|
+
|
|
126
|
+
## Seeding
|
|
127
|
+
|
|
128
|
+
On first app boot or via migration:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { seedTextBlock } from "@cosmicdrift/kumiko-bundled-features/text-content/seeding";
|
|
132
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
133
|
+
|
|
134
|
+
await seedTextBlock(db, {
|
|
135
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
136
|
+
slug: "imprint",
|
|
137
|
+
lang: "de",
|
|
138
|
+
title: "Impressum",
|
|
139
|
+
body: `## Angaben gemäß § 5 TMG
|
|
140
|
+
|
|
141
|
+
**Marc Frost**
|
|
142
|
+
|
|
143
|
+
Slevogtstr. 10
|
|
144
|
+
04159 Leipzig
|
|
145
|
+
|
|
146
|
+
## Kontakt
|
|
147
|
+
|
|
148
|
+
E-Mail: hello@example.com`,
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Templates for imprint + privacy policy: see
|
|
153
|
+
[docs/plans/datenschutz/legal-artifacts.md](../../../../docs/plans/datenschutz/legal-artifacts.md)
|
|
154
|
+
and vetted external generators (e-recht24.de,
|
|
155
|
+
datenschutz-generator.de).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## XSS — currently not secured by design
|
|
160
|
+
|
|
161
|
+
`marked` renders HTML tags 1:1, so a malicious tenant admin could in
|
|
162
|
+
theory put `<script>` into the body.
|
|
163
|
+
|
|
164
|
+
Currently accepted because:
|
|
165
|
+
- only `roles: ["TenantAdmin"]` may set texts
|
|
166
|
+
- multi-author setups don't exist yet
|
|
167
|
+
- self-hosted tier without unknown tenant admins
|
|
168
|
+
|
|
169
|
+
**Phase-2 hardening:** `DOMPurify` or `isomorphic-dompurify`
|
|
170
|
+
sanitization step between `marked.parse()` and the response.
|
|
171
|
+
Documented when a customer with a multi-author setup shows up.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Tenant model
|
|
176
|
+
|
|
177
|
+
**1 app = X tenants = 1 imprint.** All subdomains/tenant hosts of a
|
|
178
|
+
Kumiko app share the SYSTEM_TENANT version of the legal pages. If you
|
|
179
|
+
need per-tenant imprints (rare — typical case: the platform operator
|
|
180
|
+
is the responsible party, not the tenant customer), call
|
|
181
|
+
text-content's by-slug query directly with a tenant-specific TenantId
|
|
182
|
+
and put your own routes in front.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Architecture cross-refs
|
|
187
|
+
|
|
188
|
+
- [docs/plans/datenschutz/](../../../../docs/plans/datenschutz/)
|
|
189
|
+
— consolidated privacy plan index
|
|
190
|
+
- [docs/plans/datenschutz/legal-artifacts.md](../../../../docs/plans/datenschutz/legal-artifacts.md)
|
|
191
|
+
— templates + where-is-what for imprint/AVV/TOMs/RoPA
|
|
192
|
+
- [docs/plans/datenschutz/compliance-as-product.md](../../../../docs/plans/datenschutz/compliance-as-product.md)
|
|
193
|
+
— roadmap for auto-generation (sub-processor list, TOMs, data-breach workflow)
|
|
194
|
+
- [samples/recipes/legal-pages/](../../../../samples/recipes/legal-pages/)
|
|
195
|
+
— live sample with both features wired up
|