@cosmicdrift/kumiko-bundled-features 0.2.3 → 0.3.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 +60 -0
- package/package.json +17 -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/feature.ts +1 -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 +42 -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-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/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/constants.ts +2 -0
- package/src/text-content/feature.ts +20 -4
- package/src/text-content/handlers/by-tenant.query.ts +56 -0
- package/src/text-content/handlers/set.write.ts +1 -1
- package/src/text-content/web/client-plugin.ts +113 -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
|
@@ -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,
|
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({
|
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,42 @@
|
|
|
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 = (_ctx) => (emit) => {
|
|
24
|
+
const allBlocks = [...LEGAL_REQUIRED_BLOCKS, ...LEGAL_OPTIONAL_BLOCKS];
|
|
25
|
+
const nodes: readonly TreeNode[] = allBlocks.map((b) => ({
|
|
26
|
+
label: `${b.slug} (${b.lang})`,
|
|
27
|
+
target: {
|
|
28
|
+
featureId: "text-content",
|
|
29
|
+
action: "edit",
|
|
30
|
+
args: { slug: b.slug, lang: b.lang },
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
emit(nodes);
|
|
34
|
+
return () => {};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function legalPagesClient(): ClientFeatureDefinition {
|
|
38
|
+
return {
|
|
39
|
+
name: "legal-pages",
|
|
40
|
+
treeProvider,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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,
|
|
@@ -38,7 +38,7 @@ export const simpleRenderer: NotificationRenderer = {
|
|
|
38
38
|
name: "simple",
|
|
39
39
|
|
|
40
40
|
async render(input) {
|
|
41
|
-
const data = input.variables as EmailTemplateData;
|
|
41
|
+
const data = input.variables as EmailTemplateData; // @cast-boundary render-helper
|
|
42
42
|
|
|
43
43
|
// Fallback: if no structured fields, use title + body as header + single text section
|
|
44
44
|
const header = data.header ?? data.title;
|
|
@@ -51,7 +51,7 @@ export type RotateJobResult = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export const rotateJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> => {
|
|
54
|
-
const payload = rawPayload as RotateJobPayload;
|
|
54
|
+
const payload = rawPayload as RotateJobPayload; // @cast-boundary engine-payload
|
|
55
55
|
if (!ctx.masterKeyProvider) {
|
|
56
56
|
throw new InternalError({
|
|
57
57
|
message:
|
|
@@ -64,7 +64,7 @@ export const rotateJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> =>
|
|
|
64
64
|
message: "[secrets:rotate] ctx.db missing — job context requires a database connection.",
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
const db = ctx.db as DbConnection;
|
|
67
|
+
const db = ctx.db as DbConnection; // @cast-boundary db-operator
|
|
68
68
|
const batchSize = payload.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
69
69
|
const maxFailures = payload.maxFailures ?? DEFAULT_MAX_FAILURES;
|
|
70
70
|
const deadline = payload.maxDurationMs
|
|
@@ -39,13 +39,13 @@ export type SessionCleanupResult = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export const cleanupJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> => {
|
|
42
|
-
const payload = rawPayload as SessionCleanupPayload;
|
|
42
|
+
const payload = rawPayload as SessionCleanupPayload; // @cast-boundary engine-payload
|
|
43
43
|
if (!ctx.db) {
|
|
44
44
|
throw new InternalError({
|
|
45
45
|
message: "[sessions:cleanup] ctx.db missing — job context requires a database connection.",
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
const db = ctx.db as DbConnection;
|
|
48
|
+
const db = ctx.db as DbConnection; // @cast-boundary db-operator
|
|
49
49
|
|
|
50
50
|
// Coerce-and-validate: BullMQ payloads arrive as opaque JSON, so TS types
|
|
51
51
|
// don't survive. Guard before the value is interpolated into SQL.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// step-dispatcher — bundled-feature that drains deferred Tier-2 step
|
|
2
|
+
// requests (webhook.send, mail.send, ...) after their TX commits.
|
|
3
|
+
//
|
|
4
|
+
// Listens on the `kumiko:system:step.dispatch-requested` system event
|
|
5
|
+
// (registry-bypassed, see append-event-core.ts SYSTEM_EVENT_PREFIX).
|
|
6
|
+
// Performs the side-effect and emits `kumiko:system:step.dispatched`
|
|
7
|
+
// or `kumiko:system:step.dispatch-failed` back onto the same stream so
|
|
8
|
+
// the audit trail lives in the event log only — no separate status table.
|
|
9
|
+
|
|
10
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
|
+
import { type MailSpec, performMailDispatch } from "./mail-runner";
|
|
12
|
+
import { performWebhookDispatch, type WebhookSpec } from "./webhook-runner";
|
|
13
|
+
|
|
14
|
+
export const STEP_DISPATCH_AGGREGATE_TYPE = "step-dispatch";
|
|
15
|
+
export const STEP_DISPATCH_REQUESTED_TYPE = "kumiko:system:step.dispatch-requested";
|
|
16
|
+
export const STEP_DISPATCHED_TYPE = "kumiko:system:step.dispatched";
|
|
17
|
+
export const STEP_DISPATCH_FAILED_TYPE = "kumiko:system:step.dispatch-failed";
|
|
18
|
+
|
|
19
|
+
type DispatchRequestedPayload =
|
|
20
|
+
| {
|
|
21
|
+
readonly stepKind: "webhook.send";
|
|
22
|
+
readonly spec: WebhookSpec;
|
|
23
|
+
readonly retry?: { readonly times: number; readonly backoff: "exponential" | "linear" };
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
readonly stepKind: "mail.send";
|
|
27
|
+
readonly spec: MailSpec;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createStepDispatcherFeature(): FeatureDefinition {
|
|
31
|
+
return defineFeature("step-dispatcher", (r) => {
|
|
32
|
+
r.systemScope();
|
|
33
|
+
|
|
34
|
+
r.multiStreamProjection({
|
|
35
|
+
name: "step-dispatcher",
|
|
36
|
+
apply: {
|
|
37
|
+
[STEP_DISPATCH_REQUESTED_TYPE]: async (event, _tx, ctx) => {
|
|
38
|
+
const payload = event.payload as DispatchRequestedPayload;
|
|
39
|
+
const result =
|
|
40
|
+
payload.stepKind === "webhook.send"
|
|
41
|
+
? await performWebhookDispatch(payload.spec)
|
|
42
|
+
: await performMailDispatch(payload.spec);
|
|
43
|
+
if (result.ok) {
|
|
44
|
+
await ctx.unsafeAppendEvent({
|
|
45
|
+
aggregateId: event.aggregateId,
|
|
46
|
+
aggregateType: STEP_DISPATCH_AGGREGATE_TYPE,
|
|
47
|
+
type: STEP_DISPATCHED_TYPE,
|
|
48
|
+
payload: { stepKind: payload.stepKind, status: result.status },
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
await ctx.unsafeAppendEvent({
|
|
52
|
+
aggregateId: event.aggregateId,
|
|
53
|
+
aggregateType: STEP_DISPATCH_AGGREGATE_TYPE,
|
|
54
|
+
type: STEP_DISPATCH_FAILED_TYPE,
|
|
55
|
+
payload: { stepKind: payload.stepKind, error: result.error, attempt: 1 },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { createStepDispatcherFeature, STEP_DISPATCH_AGGREGATE_TYPE } from "./feature";
|
|
2
|
+
export {
|
|
3
|
+
type MailDispatchResult,
|
|
4
|
+
type MailSpec,
|
|
5
|
+
mailSpecSchema,
|
|
6
|
+
performMailDispatch,
|
|
7
|
+
setMailRunner,
|
|
8
|
+
} from "./mail-runner";
|
|
9
|
+
export {
|
|
10
|
+
performWebhookDispatch,
|
|
11
|
+
setWebhookFetch,
|
|
12
|
+
setWebhookSecretResolver,
|
|
13
|
+
type WebhookDispatchResult,
|
|
14
|
+
type WebhookSpec,
|
|
15
|
+
webhookSpecSchema,
|
|
16
|
+
} from "./webhook-runner";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Mail execution logic — separated from feature.ts and tests-injectable.
|
|
2
|
+
// Production wiring (mail-foundation transport) is a follow-up; the
|
|
3
|
+
// default impl throws so a missing setMailRunner is loud, not silent.
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
export const mailSpecSchema = z.object({
|
|
8
|
+
to: z.union([z.string(), z.array(z.string())]),
|
|
9
|
+
subject: z.string(),
|
|
10
|
+
body: z.string(),
|
|
11
|
+
from: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type MailSpec = z.infer<typeof mailSpecSchema>;
|
|
15
|
+
|
|
16
|
+
export type MailDispatchResult =
|
|
17
|
+
| { readonly ok: true; readonly status: number }
|
|
18
|
+
| { readonly ok: false; readonly error: string };
|
|
19
|
+
|
|
20
|
+
let mailRunner: (spec: MailSpec) => Promise<MailDispatchResult> = async () => ({
|
|
21
|
+
ok: false,
|
|
22
|
+
error:
|
|
23
|
+
"no mail-runner configured — call setMailRunner() with a mail-foundation transport adapter",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function setMailRunner(fn: (spec: MailSpec) => Promise<MailDispatchResult>): void {
|
|
27
|
+
mailRunner = fn;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function performMailDispatch(spec: MailSpec): Promise<MailDispatchResult> {
|
|
31
|
+
return mailRunner(spec);
|
|
32
|
+
}
|