@cosmicdrift/kumiko-bundled-features 0.2.3 → 0.4.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/CHANGELOG.md +109 -0
- package/package.json +19 -14
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
- package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
- package/src/auth-email-password/handlers/login.write.ts +1 -1
- package/src/auth-email-password/handlers/logout.write.ts +2 -2
- package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/src/auth-email-password/web/auth-client.ts +1 -1
- package/src/billing-foundation/events.ts +1 -1
- package/src/billing-foundation/feature.ts +44 -47
- package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
- package/src/billing-foundation/handlers/process-event.write.ts +3 -3
- package/src/billing-foundation/projection.ts +1 -1
- package/src/billing-foundation/webhook-handler.ts +1 -1
- package/src/cap-counter/constants.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +1 -1
- package/src/cap-counter/feature.ts +3 -7
- package/src/cap-counter/handlers/get-counter.query.ts +1 -1
- package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
- package/src/cap-counter/handlers/increment.write.ts +3 -3
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-email/email-channel.ts +1 -1
- package/src/channel-email/types.ts +1 -1
- package/src/compliance-profiles/handlers/for-tenant.query.ts +7 -6
- package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +6 -8
- package/src/compliance-profiles/resolve-for-tenant.ts +7 -5
- package/src/compliance-profiles/seeding.ts +1 -1
- package/src/config/resolver.ts +1 -1
- package/src/data-retention/_internal/parse-override.ts +3 -2
- package/src/data-retention/handlers/policy-for.query.ts +1 -1
- package/src/data-retention/keep-for.ts +1 -1
- package/src/data-retention/presets.ts +1 -1
- package/src/data-retention/resolve-for-tenant.ts +1 -1
- package/src/delivery/__tests__/delivery.integration.ts +6 -0
- package/src/delivery/delivery-service.ts +4 -12
- package/src/delivery/feature.ts +7 -5
- package/src/delivery/index.ts +0 -1
- package/src/delivery/testing.ts +1 -2
- package/src/delivery/upsert-preference.ts +1 -1
- package/src/feature-toggles/feature.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +9 -2
- package/src/feature-toggles/handlers/set.write.ts +3 -3
- package/src/file-foundation/feature.ts +1 -1
- package/src/file-provider-s3/feature.ts +2 -2
- package/src/files-provider-s3/s3-provider.ts +2 -2
- package/src/jobs/handlers/list.query.ts +3 -3
- package/src/jobs/handlers/trigger.write.ts +1 -1
- package/src/legal-pages/constants.ts +1 -0
- package/src/legal-pages/web/client-plugin.ts +82 -0
- package/src/legal-pages/web/index.ts +4 -0
- package/src/mail-foundation/feature.ts +1 -1
- package/src/mail-transport-smtp/feature.ts +2 -2
- package/src/renderer-foundation/README.md +86 -0
- package/src/renderer-foundation/__tests__/api.test.ts +188 -0
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +101 -0
- package/src/renderer-foundation/api.ts +106 -0
- package/src/renderer-foundation/constants.ts +21 -0
- package/src/renderer-foundation/feature.ts +47 -0
- package/src/renderer-foundation/index.ts +25 -0
- package/src/renderer-foundation/types.ts +109 -0
- package/src/renderer-simple/__tests__/adapter.test.ts +50 -0
- package/src/renderer-simple/feature.ts +28 -3
- package/src/renderer-simple/simple-renderer.ts +1 -1
- package/src/secrets/handlers/rotate.job.ts +2 -2
- package/src/sessions/handlers/cleanup.job.ts +2 -2
- package/src/step-dispatcher/feature.ts +62 -0
- package/src/step-dispatcher/index.ts +16 -0
- package/src/step-dispatcher/mail-runner.ts +32 -0
- package/src/step-dispatcher/webhook-runner.ts +67 -0
- package/src/subscription-mollie/plugin-methods.ts +1 -1
- package/src/subscription-mollie/verify-webhook.ts +9 -5
- package/src/subscription-stripe/verify-webhook.ts +3 -3
- package/src/template-resolver/README.md +89 -0
- package/src/template-resolver/__tests__/handlers.integration.ts +403 -0
- package/src/template-resolver/__tests__/template-resolver.integration.ts +570 -0
- package/src/template-resolver/api.ts +189 -0
- package/src/template-resolver/constants.ts +28 -0
- package/src/template-resolver/feature.ts +36 -0
- package/src/template-resolver/handlers/archive.write.ts +42 -0
- package/src/template-resolver/handlers/find-by-id.query.ts +45 -0
- package/src/template-resolver/handlers/list.query.ts +69 -0
- package/src/template-resolver/handlers/publish.write.ts +45 -0
- package/src/template-resolver/handlers/shared.ts +41 -0
- package/src/template-resolver/handlers/upsert-system.write.ts +75 -0
- package/src/template-resolver/handlers/upsert-tenant.write.ts +98 -0
- package/src/template-resolver/index.ts +28 -0
- package/src/template-resolver/qualified-names.ts +24 -0
- package/src/template-resolver/table.ts +67 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/src/tenant/handlers/remove-member.write.ts +1 -1
- package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/src/tenant/handlers/update-member-roles.write.ts +3 -3
- package/src/text-content/__tests__/text-content.integration.ts +54 -0
- package/src/text-content/constants.ts +2 -0
- package/src/text-content/feature.ts +20 -4
- package/src/text-content/handlers/by-slug.query.ts +1 -0
- package/src/text-content/handlers/by-tenant.query.ts +58 -0
- package/src/text-content/handlers/set.write.ts +24 -1
- package/src/text-content/seeding.ts +9 -1
- package/src/text-content/table.ts +6 -0
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +125 -0
- package/src/text-content/web/__tests__/group-blocks.test.ts +221 -0
- package/src/text-content/web/client-plugin.tsx +378 -0
- package/src/text-content/web/index.ts +8 -0
- package/src/tier-engine/feature.ts +8 -8
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/seeding.ts +2 -2
- package/src/user-data-rights/feature.ts +4 -3
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +8 -11
- package/src/user-data-rights/handlers/download-by-token.query.ts +14 -16
- package/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/src/user-data-rights/handlers/request-export.write.ts +2 -2
- package/src/user-data-rights/run-export-jobs.ts +2 -2
- package/src/user-data-rights/run-forget-cleanup.ts +27 -28
- package/src/user-data-rights/run-user-export.ts +1 -1
- package/src/user-data-rights/token-helpers.ts +2 -2
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +1 -1
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
|
@@ -25,7 +25,7 @@ export const markSoftWarnedHandler: WriteHandlerDef = {
|
|
|
25
25
|
schema: markSoftWarnedSchema,
|
|
26
26
|
access: { roles: ["SystemAdmin"] },
|
|
27
27
|
handler: async (event, ctx) => {
|
|
28
|
-
const payload = event.payload as z.infer<typeof markSoftWarnedSchema>;
|
|
28
|
+
const payload = event.payload as z.infer<typeof markSoftWarnedSchema>; // @cast-boundary engine-payload
|
|
29
29
|
const aggregateId = capCounterAggregateId(
|
|
30
30
|
event.user.tenantId,
|
|
31
31
|
payload.capName,
|
|
@@ -42,7 +42,7 @@ export const markSoftWarnedHandler: WriteHandlerDef = {
|
|
|
42
42
|
if (!row) {
|
|
43
43
|
throw new Error("cap-counter:mark-soft-warned: row vanished between length-check and read");
|
|
44
44
|
}
|
|
45
|
-
const currentVersion = row["version"] as number;
|
|
45
|
+
const currentVersion = row["version"] as number; // @cast-boundary db-row
|
|
46
46
|
|
|
47
47
|
return executor.update(
|
|
48
48
|
{
|
|
@@ -34,7 +34,7 @@ export function createEmailChannel(options: EmailChannelOptions): DeliveryChanne
|
|
|
34
34
|
template: message.notificationType,
|
|
35
35
|
variables,
|
|
36
36
|
});
|
|
37
|
-
const subject = (variables["subject"] as string) ?? message.title;
|
|
37
|
+
const subject = (variables["subject"] as string) ?? message.title; // @cast-boundary dynamic-key
|
|
38
38
|
|
|
39
39
|
await transport.send({
|
|
40
40
|
to: address,
|
|
@@ -20,7 +20,7 @@ export function createInMemoryTransport(): EmailTransport & {
|
|
|
20
20
|
const sent: EmailMessage[] = [];
|
|
21
21
|
const transport = {
|
|
22
22
|
sent,
|
|
23
|
-
failNext: null as null | { message: string },
|
|
23
|
+
failNext: null as null | { message: string }, // @cast-boundary generic-record
|
|
24
24
|
async send(message: EmailMessage) {
|
|
25
25
|
if (transport.failNext) {
|
|
26
26
|
const err = new Error(transport.failNext.message);
|
|
@@ -26,7 +26,7 @@ export const forTenantQuery = defineQueryHandler({
|
|
|
26
26
|
ctx.db,
|
|
27
27
|
tenantComplianceProfileTable,
|
|
28
28
|
eq(tenantComplianceProfileTable["tenantId"], query.user.tenantId),
|
|
29
|
-
)) as { profileKey: string; override: string | null } | null;
|
|
29
|
+
)) as { profileKey: string; override: string | null } | null; // @cast-boundary db-runner
|
|
30
30
|
|
|
31
31
|
if (!row) {
|
|
32
32
|
return resolveComplianceProfile({});
|
|
@@ -34,7 +34,7 @@ export const forTenantQuery = defineQueryHandler({
|
|
|
34
34
|
|
|
35
35
|
const override = parseOverride(row.override, query.user.tenantId);
|
|
36
36
|
return resolveComplianceProfile({
|
|
37
|
-
selection: row.profileKey as ComplianceProfileKey,
|
|
37
|
+
selection: row.profileKey as ComplianceProfileKey, // @cast-boundary engine-payload
|
|
38
38
|
override,
|
|
39
39
|
});
|
|
40
40
|
},
|
|
@@ -46,9 +46,10 @@ function parseOverride(
|
|
|
46
46
|
): ComplianceProfileOverride | undefined {
|
|
47
47
|
if (!raw || raw.trim() === "") return undefined;
|
|
48
48
|
try {
|
|
49
|
-
const parsed = JSON.parse(raw)
|
|
50
|
-
return parsed;
|
|
51
|
-
} catch (e) {
|
|
49
|
+
const parsed: unknown = JSON.parse(raw);
|
|
50
|
+
return parsed as ComplianceProfileOverride; // @cast-boundary engine-payload
|
|
51
|
+
} catch (e: unknown) {
|
|
52
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
52
53
|
// Defensiv: ungültiges JSON wird als "kein Override" behandelt. Der
|
|
53
54
|
// set-profile-Handler validiert Zod das Override schon — invalides
|
|
54
55
|
// JSON in der DB ist also nur möglich bei manueller DB-Manipulation
|
|
@@ -56,7 +57,7 @@ function parseOverride(
|
|
|
56
57
|
// Operator-Sichtbarkeit via console.warn — Telemetry-Hook spaeter.
|
|
57
58
|
// biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
|
|
58
59
|
console.warn(
|
|
59
|
-
`[compliance-profiles:for-tenant] tenant ${tenantId}: stored override is not valid JSON, falling back to base profile. Reason: ${
|
|
60
|
+
`[compliance-profiles:for-tenant] tenant ${tenantId}: stored override is not valid JSON, falling back to base profile. Reason: ${reason}`,
|
|
60
61
|
);
|
|
61
62
|
return undefined;
|
|
62
63
|
}
|
|
@@ -27,7 +27,7 @@ export const needsProfileQuery = defineQueryHandler({
|
|
|
27
27
|
ctx.db,
|
|
28
28
|
tenantComplianceProfileTable,
|
|
29
29
|
eq(tenantComplianceProfileTable["tenantId"], query.user.tenantId),
|
|
30
|
-
)) as { profileKey: ComplianceProfileKey } | null;
|
|
30
|
+
)) as { profileKey: ComplianceProfileKey } | null; // @cast-boundary db-runner
|
|
31
31
|
|
|
32
32
|
if (!row) {
|
|
33
33
|
return {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
2
2
|
import {
|
|
3
|
-
type ComplianceProfileKey,
|
|
4
3
|
complianceProfileOverrideSchema,
|
|
5
4
|
SELECTABLE_PROFILE_KEYS,
|
|
6
5
|
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
@@ -27,9 +26,7 @@ const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantCompli
|
|
|
27
26
|
// X1) — minimal-no-region ist Default-Fallback fuer "noch keine Wahl",
|
|
28
27
|
// nicht eine waehlbare Production-Option. Symmetrisch zu
|
|
29
28
|
// SELECTABLE_PROFILE_KEYS aus der framework/compliance-Liste.
|
|
30
|
-
const profileKeySchema = z.enum(
|
|
31
|
-
SELECTABLE_PROFILE_KEYS as readonly [ComplianceProfileKey, ...ComplianceProfileKey[]],
|
|
32
|
-
);
|
|
29
|
+
const profileKeySchema = z.enum(SELECTABLE_PROFILE_KEYS);
|
|
33
30
|
|
|
34
31
|
// Tenant-Admin setzt Profile-Key + optional Override-JSON.
|
|
35
32
|
//
|
|
@@ -69,7 +66,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
69
66
|
}),
|
|
70
67
|
);
|
|
71
68
|
}
|
|
72
|
-
const tenantId = (tenantOverride ?? event.user.tenantId) as TenantId;
|
|
69
|
+
const tenantId = (tenantOverride ?? event.user.tenantId) as TenantId; // @cast-boundary engine-payload
|
|
73
70
|
const executorUser = tenantOverride !== undefined ? { ...event.user, tenantId } : event.user;
|
|
74
71
|
|
|
75
72
|
// Override-Validation: muss parseables JSON-Object sein UND dem
|
|
@@ -85,12 +82,13 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
85
82
|
let parsed: unknown;
|
|
86
83
|
try {
|
|
87
84
|
parsed = JSON.parse(event.payload.override);
|
|
88
|
-
} catch (e) {
|
|
85
|
+
} catch (e: unknown) {
|
|
86
|
+
const parseError = e instanceof Error ? e.message : String(e);
|
|
89
87
|
return writeFailure(
|
|
90
88
|
new UnprocessableError("compliance_override_invalid_json", {
|
|
91
89
|
details: {
|
|
92
90
|
reason: "compliance_override_invalid_json",
|
|
93
|
-
parseError
|
|
91
|
+
parseError,
|
|
94
92
|
},
|
|
95
93
|
}),
|
|
96
94
|
);
|
|
@@ -106,7 +104,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
106
104
|
ctx.db,
|
|
107
105
|
tenantComplianceProfileTable,
|
|
108
106
|
eq(tenantComplianceProfileTable["tenantId"], tenantId),
|
|
109
|
-
)) as { id: string; version: number } | null;
|
|
107
|
+
)) as { id: string; version: number } | null; // @cast-boundary db-runner
|
|
110
108
|
|
|
111
109
|
if (existing) {
|
|
112
110
|
const result = await crud.update(
|
|
@@ -31,7 +31,7 @@ export async function resolveProfileForTenant(
|
|
|
31
31
|
args.db,
|
|
32
32
|
tenantComplianceProfileTable,
|
|
33
33
|
eq(tenantComplianceProfileTable["tenantId"], args.tenantId),
|
|
34
|
-
)) as { profileKey: string; override: string | null } | null;
|
|
34
|
+
)) as { profileKey: string; override: string | null } | null; // @cast-boundary db-runner
|
|
35
35
|
|
|
36
36
|
if (!row) {
|
|
37
37
|
return resolveComplianceProfile({});
|
|
@@ -39,7 +39,7 @@ export async function resolveProfileForTenant(
|
|
|
39
39
|
|
|
40
40
|
const override = parseOverride(row.override, args.tenantId);
|
|
41
41
|
return resolveComplianceProfile({
|
|
42
|
-
selection: row.profileKey as ComplianceProfileKey,
|
|
42
|
+
selection: row.profileKey as ComplianceProfileKey, // @cast-boundary engine-payload
|
|
43
43
|
override,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -50,11 +50,13 @@ function parseOverride(
|
|
|
50
50
|
): ComplianceProfileOverride | undefined {
|
|
51
51
|
if (!raw || raw.trim() === "") return undefined;
|
|
52
52
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const parsed: unknown = JSON.parse(raw);
|
|
54
|
+
return parsed as ComplianceProfileOverride; // @cast-boundary engine-payload
|
|
55
|
+
} catch (e: unknown) {
|
|
56
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
55
57
|
// biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
|
|
56
58
|
console.warn(
|
|
57
|
-
`[compliance-profiles:resolve-for-tenant] tenant ${tenantId}: stored override is not valid JSON, ignoring. Reason: ${
|
|
59
|
+
`[compliance-profiles:resolve-for-tenant] tenant ${tenantId}: stored override is not valid JSON, ignoring. Reason: ${reason}`,
|
|
58
60
|
);
|
|
59
61
|
return undefined;
|
|
60
62
|
}
|
|
@@ -56,7 +56,7 @@ export async function seedComplianceProfile(
|
|
|
56
56
|
db,
|
|
57
57
|
tenantComplianceProfileTable,
|
|
58
58
|
eq(tenantComplianceProfileTable["tenantId"], opts.tenantId),
|
|
59
|
-
)) as { id: string; version: number } | null;
|
|
59
|
+
)) as { id: string; version: number } | null; // @cast-boundary db-runner
|
|
60
60
|
|
|
61
61
|
if (existing) {
|
|
62
62
|
const result = await executor.update(
|
package/src/config/resolver.ts
CHANGED
|
@@ -179,7 +179,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
179
179
|
|
|
180
180
|
const result = new Map<string, ConfigRow>();
|
|
181
181
|
for (const row of rows) {
|
|
182
|
-
const r = row as ConfigRow;
|
|
182
|
+
const r = row as ConfigRow; // @cast-boundary db-row
|
|
183
183
|
// Higher specificity wins: user > tenant > system. Under the ES
|
|
184
184
|
// schema system rows carry SYSTEM_TENANT_ID instead of NULL, so the
|
|
185
185
|
// "tenant set" check compares against the sentinel rather than null.
|
|
@@ -14,10 +14,11 @@ export function parseRetentionOverrideOrNull(
|
|
|
14
14
|
let parsed: unknown;
|
|
15
15
|
try {
|
|
16
16
|
parsed = JSON.parse(raw);
|
|
17
|
-
} catch (e) {
|
|
17
|
+
} catch (e: unknown) {
|
|
18
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
18
19
|
// biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
|
|
19
20
|
console.warn(
|
|
20
|
-
`[${callerLabel}] tenant ${tenantId}: stored override is not valid JSON, ignoring. Reason: ${
|
|
21
|
+
`[${callerLabel}] tenant ${tenantId}: stored override is not valid JSON, ignoring. Reason: ${reason}`,
|
|
21
22
|
);
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
@@ -33,7 +33,7 @@ export const policyForQuery = defineQueryHandler({
|
|
|
33
33
|
tenantRetentionOverrideTable,
|
|
34
34
|
eq(tenantRetentionOverrideTable["tenantId"], query.user.tenantId),
|
|
35
35
|
eq(tenantRetentionOverrideTable["entityName"], entityName),
|
|
36
|
-
)) as { config: string | null } | null;
|
|
36
|
+
)) as { config: string | null } | null; // @cast-boundary db-runner
|
|
37
37
|
|
|
38
38
|
const tenantOverride = parseRetentionOverrideOrNull(
|
|
39
39
|
overrideRow?.config ?? null,
|
|
@@ -58,7 +58,7 @@ export const RETENTION_PRESETS: Readonly<Record<RetentionPresetKey, RetentionPre
|
|
|
58
58
|
session: { keepFor: "30d", strategy: "hardDelete", reference: "lastSeenAt" },
|
|
59
59
|
invoice: { keepFor: "10y", strategy: "blockDelete", reference: "createdAt" },
|
|
60
60
|
},
|
|
61
|
-
}
|
|
61
|
+
} satisfies Readonly<Record<RetentionPresetKey, RetentionPreset>>;
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Auswählbare Presets für den Onboarding-Banner. "default" ist Migration-
|
|
@@ -28,7 +28,7 @@ export async function resolveRetentionPolicyForTenant(
|
|
|
28
28
|
tenantRetentionOverrideTable,
|
|
29
29
|
eq(tenantRetentionOverrideTable["tenantId"], args.tenantId),
|
|
30
30
|
eq(tenantRetentionOverrideTable["entityName"], args.entityName),
|
|
31
|
-
)) as { config: string | null } | null;
|
|
31
|
+
)) as { config: string | null } | null; // @cast-boundary db-runner
|
|
32
32
|
|
|
33
33
|
const tenantOverride = parseRetentionOverrideOrNull(
|
|
34
34
|
overrideRow?.config ?? null,
|
|
@@ -27,8 +27,10 @@ import { createChannelPushFeature } from "../../channel-push/feature";
|
|
|
27
27
|
import { createInMemoryPushTransport } from "../../channel-push/types";
|
|
28
28
|
import { createConfigFeature } from "../../config/feature";
|
|
29
29
|
import { configValuesTable } from "../../config/table";
|
|
30
|
+
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
30
31
|
import { createRendererSimpleFeature } from "../../renderer-simple/feature";
|
|
31
32
|
import { simpleRenderer } from "../../renderer-simple/simple-renderer";
|
|
33
|
+
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
32
34
|
import { TenantQueries } from "../../tenant/constants";
|
|
33
35
|
import { createTenantFeature } from "../../tenant/feature";
|
|
34
36
|
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
@@ -241,6 +243,8 @@ const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
241
243
|
|
|
242
244
|
const configFeature = createConfigFeature();
|
|
243
245
|
const tenantFeature = createTenantFeature();
|
|
246
|
+
const templateResolverFeature = createTemplateResolverFeature();
|
|
247
|
+
const rendererFoundationFeature = createRendererFoundationFeature();
|
|
244
248
|
const deliveryFeature = createDeliveryFeature();
|
|
245
249
|
const channelInAppFeature = createChannelInAppFeature();
|
|
246
250
|
const rendererSimpleFeature = createRendererSimpleFeature();
|
|
@@ -256,6 +260,8 @@ const channelPushFeature = createChannelPushFeature({
|
|
|
256
260
|
const features = [
|
|
257
261
|
configFeature,
|
|
258
262
|
tenantFeature,
|
|
263
|
+
templateResolverFeature,
|
|
264
|
+
rendererFoundationFeature,
|
|
259
265
|
deliveryFeature,
|
|
260
266
|
channelInAppFeature,
|
|
261
267
|
rendererSimpleFeature,
|
|
@@ -18,7 +18,6 @@ import type {
|
|
|
18
18
|
DeliveryChannel,
|
|
19
19
|
DeliveryLogEntry,
|
|
20
20
|
DeliveryService,
|
|
21
|
-
NotificationRenderer,
|
|
22
21
|
} from "./types";
|
|
23
22
|
|
|
24
23
|
export type RateLimitConfig = {
|
|
@@ -56,17 +55,10 @@ export function collectChannels(registry: Registry): DeliveryChannel[] {
|
|
|
56
55
|
});
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for (const usage of usages) {
|
|
64
|
-
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
65
|
-
const opts = usage.options as { render: NotificationRenderer["render"] };
|
|
66
|
-
map.set(usage.entityName, { name: usage.entityName, render: opts.render });
|
|
67
|
-
}
|
|
68
|
-
return map;
|
|
69
|
-
}
|
|
58
|
+
// `collectRenderers` entfernt 2026-05-19: notificationRenderer-Extension-Point
|
|
59
|
+
// wurde nie konsumiert (channel-email nimmt renderer als Konstruktor-Option,
|
|
60
|
+
// nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
|
|
61
|
+
// `renderer-foundation`-Bundle via `collectRendererPlugins`.
|
|
70
62
|
|
|
71
63
|
export function createDeliveryService(options: DeliveryServiceOptions): DeliveryService {
|
|
72
64
|
const {
|
package/src/delivery/feature.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function createDeliveryFeature(): FeatureDefinition {
|
|
|
31
31
|
table: deliveryAttemptsTable,
|
|
32
32
|
apply: {
|
|
33
33
|
[DELIVERY_ATTEMPT_EVENT]: async (event, tx) => {
|
|
34
|
-
const p = event.payload as z.infer<typeof deliveryAttemptSchema>;
|
|
34
|
+
const p = event.payload as z.infer<typeof deliveryAttemptSchema>; // @cast-boundary engine-payload
|
|
35
35
|
// PK = aggregateId — replaying the same event twice conflicts on
|
|
36
36
|
// the PK rather than silently duplicating the log row.
|
|
37
37
|
await tx.insert(deliveryAttemptsTable).values({
|
|
@@ -48,13 +48,15 @@ export function createDeliveryFeature(): FeatureDefinition {
|
|
|
48
48
|
},
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
// Extension
|
|
51
|
+
// Extension point: delivery-channels (email/in-app/push). Renderer-
|
|
52
|
+
// Extension-Point lebt jetzt im `renderer-foundation`-Bundle als
|
|
53
|
+
// `renderer` (Multi-Kind-Plugin-Contract). delivery hostet keinen
|
|
54
|
+
// eigenen mehr — channel-email nimmt renderer als direkte
|
|
55
|
+
// Konstruktor-Option (siehe email-channel.ts), nicht via Extension-
|
|
56
|
+
// Usage. Migration 2026-05-19.
|
|
52
57
|
r.extendsRegistrar("deliveryChannel", {
|
|
53
58
|
onRegister: () => {},
|
|
54
59
|
});
|
|
55
|
-
r.extendsRegistrar("notificationRenderer", {
|
|
56
|
-
onRegister: () => {},
|
|
57
|
-
});
|
|
58
60
|
|
|
59
61
|
const handlers = {
|
|
60
62
|
setPreference: r.writeHandler(setPreferenceWrite),
|
package/src/delivery/index.ts
CHANGED
package/src/delivery/testing.ts
CHANGED
|
@@ -41,7 +41,6 @@ export function createDeliveryTestContext(
|
|
|
41
41
|
_notifyFactory:
|
|
42
42
|
(user: { id: number; tenantId: TenantId }, tenantId: TenantId) =>
|
|
43
43
|
(notificationType: string, notifyOptions: Record<string, unknown>) =>
|
|
44
|
-
// @cast-boundary engine-bridge
|
|
45
|
-
deliveryService.notify(notificationType, notifyOptions as never, user as never, tenantId),
|
|
44
|
+
deliveryService.notify(notificationType, notifyOptions as never, user as never, tenantId), // @cast-boundary engine-bridge
|
|
46
45
|
};
|
|
47
46
|
}
|
|
@@ -137,7 +137,7 @@ export async function upsertPreference(
|
|
|
137
137
|
// minor driver-version shifts without drifting wide.
|
|
138
138
|
function isUniqueViolation(err: unknown): boolean {
|
|
139
139
|
if (typeof err !== "object" || err === null) return false;
|
|
140
|
-
const e = err as { code?: unknown; cause?: { code?: unknown }; message?: unknown };
|
|
140
|
+
const e = err as { code?: unknown; cause?: { code?: unknown }; message?: unknown }; // @cast-boundary error-details
|
|
141
141
|
if (e.code === "23505") return true;
|
|
142
142
|
if (e.cause && typeof e.cause === "object" && e.cause.code === "23505") return true;
|
|
143
143
|
if (typeof e.message === "string" && e.message.includes("23505")) return true;
|
|
@@ -78,7 +78,7 @@ export function createFeatureTogglesFeature(options: FeatureTogglesOptions): Fea
|
|
|
78
78
|
// (validated on append). Shallow-cast to a typed shape rather
|
|
79
79
|
// than re-parsing — the payload round-trips through JSON and is
|
|
80
80
|
// fixed at the source.
|
|
81
|
-
const payload = event.payload as { featureName: string; enabled: boolean };
|
|
81
|
+
const payload = event.payload as { featureName: string; enabled: boolean }; // @cast-boundary engine-payload
|
|
82
82
|
options.getRuntime().apply(payload.featureName, payload.enabled);
|
|
83
83
|
},
|
|
84
84
|
},
|
|
@@ -12,7 +12,7 @@ export const listQuery = defineQueryHandler({
|
|
|
12
12
|
access: { roles: ["SystemAdmin", "Admin"] },
|
|
13
13
|
handler: async (_event, ctx) => {
|
|
14
14
|
type Row = typeof globalFeatureStateTable.$inferSelect;
|
|
15
|
-
const rows = (await ctx.db.select().from(globalFeatureStateTable)) as Row[];
|
|
15
|
+
const rows = (await ctx.db.select().from(globalFeatureStateTable)) as Row[]; // @cast-boundary db-row
|
|
16
16
|
return {
|
|
17
17
|
items: rows.map((r) => ({
|
|
18
18
|
featureName: r.featureName,
|
|
@@ -21,7 +21,7 @@ export const registeredQuery = defineQueryHandler({
|
|
|
21
21
|
featureName: globalFeatureStateTable.featureName,
|
|
22
22
|
enabled: globalFeatureStateTable.enabled,
|
|
23
23
|
})
|
|
24
|
-
.from(globalFeatureStateTable)) as OverrideRow[];
|
|
24
|
+
.from(globalFeatureStateTable)) as OverrideRow[]; // @cast-boundary db-row
|
|
25
25
|
const overrides = new Map(overrideRows.map((r) => [r.featureName, r.enabled]));
|
|
26
26
|
|
|
27
27
|
// SystemAdmin operator-tooling: das listing soll die PLATTFORM-truth
|
|
@@ -31,7 +31,14 @@ export const registeredQuery = defineQueryHandler({
|
|
|
31
31
|
// dokumentiert in DispatcherOptions.effectiveFeatures.
|
|
32
32
|
const effective = ctx.effectiveFeatures?.(SYSTEM_TENANT_ID);
|
|
33
33
|
|
|
34
|
-
const items
|
|
34
|
+
const items: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
toggleable: boolean;
|
|
37
|
+
default: boolean | null;
|
|
38
|
+
override: boolean | null;
|
|
39
|
+
requires: readonly string[];
|
|
40
|
+
effective: boolean | null;
|
|
41
|
+
}> = [];
|
|
35
42
|
for (const feature of ctx.registry.features.values()) {
|
|
36
43
|
const toggleable = feature.toggleableDefault !== undefined;
|
|
37
44
|
const override = overrides.get(feature.name);
|
|
@@ -71,7 +71,7 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
|
|
|
71
71
|
.select()
|
|
72
72
|
.from(globalFeatureStateTable)
|
|
73
73
|
.where(eq(globalFeatureStateTable.featureName, featureName))
|
|
74
|
-
.limit(1)) as StateRow[];
|
|
74
|
+
.limit(1)) as StateRow[]; // @cast-boundary db-row
|
|
75
75
|
|
|
76
76
|
const previousEnabled = existing?.enabled ?? null;
|
|
77
77
|
|
|
@@ -123,11 +123,11 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
|
|
|
123
123
|
// and filtering by payload.featureName is trivial at query time.
|
|
124
124
|
// This mirrors how `config` handles the same constraint for
|
|
125
125
|
// its config-changed events.
|
|
126
|
-
//
|
|
126
|
+
// unsafeAppendEvent — bundled-features ohne lokalen Wrapper. Apps
|
|
127
127
|
// mit `yarn kumiko codegen` kriegen `.kumiko/define.ts` als strict-
|
|
128
128
|
// path; bundled-features bleibt bei der unsafe-Variante. Schema-
|
|
129
129
|
// Validation läuft trotzdem via r.defineEvent("toggle-set", ...).
|
|
130
|
-
await ctx.
|
|
130
|
+
await ctx.unsafeAppendEvent({
|
|
131
131
|
aggregateId: SYSTEM_TENANT_ID,
|
|
132
132
|
aggregateType: FEATURE_TOGGLE_AGGREGATE_TYPE,
|
|
133
133
|
type: FEATURE_TOGGLE_SET_EVENT_NAME,
|
|
@@ -137,7 +137,7 @@ export async function createFileProviderForTenant(
|
|
|
137
137
|
await ctxConfig(fileFoundationFeature.exports.configKeys.provider),
|
|
138
138
|
FEATURE_NAME,
|
|
139
139
|
"provider",
|
|
140
|
-
) as string;
|
|
140
|
+
) as string; // @cast-boundary engine-payload
|
|
141
141
|
if (provider.length === 0) {
|
|
142
142
|
const usages = ctx.registry.getExtensionUsages("fileProvider");
|
|
143
143
|
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
@@ -129,13 +129,13 @@ async function buildS3Provider(
|
|
|
129
129
|
await ctxConfig(fileProviderS3Feature.exports.configKeys.endpoint),
|
|
130
130
|
FEATURE_NAME,
|
|
131
131
|
"endpoint",
|
|
132
|
-
) as string;
|
|
132
|
+
) as string; // @cast-boundary engine-payload
|
|
133
133
|
const endpoint = endpointRaw.length > 0 ? endpointRaw : undefined;
|
|
134
134
|
const forcePathStyle = requireDefined(
|
|
135
135
|
await ctxConfig(fileProviderS3Feature.exports.configKeys.forcePathStyle),
|
|
136
136
|
FEATURE_NAME,
|
|
137
137
|
"forcePathStyle",
|
|
138
|
-
) as boolean;
|
|
138
|
+
) as boolean; // @cast-boundary engine-payload
|
|
139
139
|
const accessKeyId = requireNonEmpty(
|
|
140
140
|
await ctxConfig(fileProviderS3Feature.exports.configKeys.accessKeyId),
|
|
141
141
|
FEATURE_NAME,
|
|
@@ -154,7 +154,7 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
154
154
|
}
|
|
155
155
|
// SdkStream is AsyncIterable<Buffer> on node. Buffer extends
|
|
156
156
|
// Uint8Array; cast sichert die Surface ohne neue runtime-deps.
|
|
157
|
-
const body = response.Body as AsyncIterable<Uint8Array>;
|
|
157
|
+
const body = response.Body as AsyncIterable<Uint8Array>; // @cast-boundary engine-bridge
|
|
158
158
|
for await (const chunk of body) {
|
|
159
159
|
yield chunk;
|
|
160
160
|
}
|
|
@@ -174,7 +174,7 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
174
174
|
// S3 SDK throws either NotFound or a generic 404. Check both the
|
|
175
175
|
// `.name` property (newer SDKs) and the `$metadata.httpStatusCode`
|
|
176
176
|
// (what the SDK guarantees on every error).
|
|
177
|
-
const err = error as { name?: string; $metadata?: { httpStatusCode?: number } };
|
|
177
|
+
const err = error as { name?: string; $metadata?: { httpStatusCode?: number } }; // @cast-boundary error-details
|
|
178
178
|
if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
|
|
179
179
|
return false;
|
|
180
180
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
2
|
+
import { and, desc, eq, type SQL } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { type JobRunStatus, jobRunsTable } from "../job-run-table";
|
|
5
5
|
|
|
@@ -13,13 +13,13 @@ export const listQuery = defineQueryHandler({
|
|
|
13
13
|
access: { roles: ["SystemAdmin"] },
|
|
14
14
|
handler: async (query, ctx) => {
|
|
15
15
|
const db = ctx.db;
|
|
16
|
-
const conditions = [];
|
|
16
|
+
const conditions: SQL[] = [];
|
|
17
17
|
|
|
18
18
|
if (query.payload.jobName) {
|
|
19
19
|
conditions.push(eq(jobRunsTable.jobName, query.payload.jobName));
|
|
20
20
|
}
|
|
21
21
|
if (query.payload.status) {
|
|
22
|
-
conditions.push(eq(jobRunsTable.status, query.payload.status as JobRunStatus));
|
|
22
|
+
conditions.push(eq(jobRunsTable.status, query.payload.status as JobRunStatus)); // @cast-boundary engine-payload
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const limit = query.payload.limit ?? 50;
|
|
@@ -14,7 +14,7 @@ export const triggerWrite = defineWriteHandler({
|
|
|
14
14
|
handler: async (event, ctx) => {
|
|
15
15
|
const registry = ctx.registry;
|
|
16
16
|
// `jobRunner` is a dynamic context extension — not a core HandlerContext field.
|
|
17
|
-
const jobRunner = ctx["jobRunner"] as JobRunner;
|
|
17
|
+
const jobRunner = ctx["jobRunner"] as JobRunner; // @cast-boundary dynamic-key
|
|
18
18
|
|
|
19
19
|
const jobDef = registry.getJob(event.payload.jobName);
|
|
20
20
|
if (!jobDef) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Client-Feature-Factory für legal-pages Visual-Tree. Liefert statische
|
|
3
|
+
// Tree-Knoten für die DACH-Compliance-Blöcke (imprint, privacy in de/en).
|
|
4
|
+
// Jeder Knoten linkt auf text-content's edit-Action — reines Cross-Feature-
|
|
5
|
+
// Linking, kein eigener State oder Fetch nötig.
|
|
6
|
+
//
|
|
7
|
+
// **Static statt fetch**: legal-pages weiß out-of-the-box welche Blocks
|
|
8
|
+
// existieren (LEGAL_REQUIRED_BLOCKS + LEGAL_OPTIONAL_BLOCKS aus constants).
|
|
9
|
+
// Anders als text-content's Provider (der alle Slugs des Tenants holt)
|
|
10
|
+
// ist diese Liste bekannt zur Build-Zeit — kein /api/query-Round-trip nötig.
|
|
11
|
+
//
|
|
12
|
+
// **Content-State unbekannt**: V.1.2 setzt keine state-Markierung; alle
|
|
13
|
+
// Knoten erscheinen "filled" (default). V.1.3+ könnte via by-slug-Query
|
|
14
|
+
// ermitteln ob ein Block tatsächlich body hat und „stub" markieren wenn
|
|
15
|
+
// leer (Provider-Author-Hinweis dass Block existiert aber befüllt werden
|
|
16
|
+
// muss). Aktuell ist legal-pages's Boot-Check der primäre Wächter für
|
|
17
|
+
// fehlende Pflicht-Blocks.
|
|
18
|
+
|
|
19
|
+
import type { TreeChildrenSubscribe, TreeNode } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
21
|
+
import { LEGAL_OPTIONAL_BLOCKS, LEGAL_REQUIRED_BLOCKS } from "../constants";
|
|
22
|
+
|
|
23
|
+
const treeProvider: TreeChildrenSubscribe = () => (emit) => {
|
|
24
|
+
// V.1.5d Slug-first Verschachtelung (Variante C):
|
|
25
|
+
// 📁 Legal
|
|
26
|
+
// 📁 imprint
|
|
27
|
+
// de
|
|
28
|
+
// en
|
|
29
|
+
// 📁 privacy
|
|
30
|
+
// de
|
|
31
|
+
// en
|
|
32
|
+
//
|
|
33
|
+
// Slug ist der Übersetzungs-Anker — User pflegt DE+EN-Versionen
|
|
34
|
+
// desselben Inhalts zusammen statt nach Sprache zu gruppieren.
|
|
35
|
+
// Sub-Items sind reine Sprach-Leaves; Label = Sprache, target zeigt
|
|
36
|
+
// auf text-content:edit mit slug+lang.
|
|
37
|
+
|
|
38
|
+
// Group all blocks by slug, collect set of langs per slug.
|
|
39
|
+
const bySlug = new Map<string, string[]>();
|
|
40
|
+
for (const b of [...LEGAL_REQUIRED_BLOCKS, ...LEGAL_OPTIONAL_BLOCKS]) {
|
|
41
|
+
const langs = bySlug.get(b.slug) ?? [];
|
|
42
|
+
if (!langs.includes(b.lang)) langs.push(b.lang);
|
|
43
|
+
bySlug.set(b.slug, langs);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const slugFolders: TreeNode[] = [];
|
|
47
|
+
for (const slug of [...bySlug.keys()].sort()) {
|
|
48
|
+
const langs = bySlug.get(slug);
|
|
49
|
+
if (langs === undefined) continue;
|
|
50
|
+
const langLeaves: TreeNode[] = langs.sort().map((lang) => ({
|
|
51
|
+
label: lang,
|
|
52
|
+
target: {
|
|
53
|
+
featureId: "text-content",
|
|
54
|
+
action: "edit",
|
|
55
|
+
args: { slug, lang },
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
slugFolders.push({
|
|
59
|
+
label: slug,
|
|
60
|
+
icon: "folder",
|
|
61
|
+
state: "filled",
|
|
62
|
+
children: langLeaves,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
emit([
|
|
67
|
+
{
|
|
68
|
+
label: "Legal",
|
|
69
|
+
icon: "folder",
|
|
70
|
+
state: "filled",
|
|
71
|
+
children: slugFolders,
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
return () => {};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function legalPagesClient(): ClientFeatureDefinition {
|
|
78
|
+
return {
|
|
79
|
+
name: "legal-pages",
|
|
80
|
+
treeProvider,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -134,7 +134,7 @@ export async function createTransportForTenant(
|
|
|
134
134
|
await ctxConfig(mailFoundationFeature.exports.configKeys.provider),
|
|
135
135
|
FEATURE_NAME,
|
|
136
136
|
"provider",
|
|
137
|
-
) as string;
|
|
137
|
+
) as string; // @cast-boundary engine-payload
|
|
138
138
|
if (provider.length === 0) {
|
|
139
139
|
const usages = ctx.registry.getExtensionUsages("mailTransport");
|
|
140
140
|
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
@@ -140,12 +140,12 @@ async function buildSmtpTransport(ctx: HandlerContext, tenantId: string): Promis
|
|
|
140
140
|
await ctxConfig(mailTransportSmtpFeature.exports.configKeys.port),
|
|
141
141
|
FEATURE_NAME,
|
|
142
142
|
"port",
|
|
143
|
-
) as number;
|
|
143
|
+
) as number; // @cast-boundary engine-payload
|
|
144
144
|
const secure = requireDefined(
|
|
145
145
|
await ctxConfig(mailTransportSmtpFeature.exports.configKeys.secure),
|
|
146
146
|
FEATURE_NAME,
|
|
147
147
|
"secure",
|
|
148
|
-
) as boolean;
|
|
148
|
+
) as boolean; // @cast-boundary engine-payload
|
|
149
149
|
const from = requireNonEmpty(
|
|
150
150
|
await ctxConfig(mailTransportSmtpFeature.exports.configKeys.from),
|
|
151
151
|
FEATURE_NAME,
|