@cosmicdrift/kumiko-bundled-features 0.113.1 → 1.0.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 +6 -8
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +10 -9
- package/src/auth-email-password/__tests__/auth.integration.test.ts +5 -5
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +4 -7
- package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +4 -3
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -3
- package/src/auth-email-password/constants.ts +0 -4
- package/src/auth-email-password/feature.ts +4 -8
- package/src/auth-email-password/handlers/change-password.write.ts +11 -11
- package/src/auth-email-password/handlers/confirm-token-flow.ts +104 -33
- package/src/auth-email-password/handlers/login.write.ts +1 -4
- package/src/auth-email-password/password-hashing.ts +0 -11
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +1 -18
- package/src/auth-email-password/web/auth-gate.tsx +4 -3
- package/src/auth-email-password/web/invite-accept-screen.tsx +2 -2
- package/src/auth-email-password/web/reset-password-screen.tsx +11 -6
- package/src/auth-email-password/web/signup-complete-screen.tsx +5 -5
- package/src/auth-email-password/web/user-menu.tsx +1 -3
- package/src/auth-email-password/web/verify-email-screen.tsx +7 -2
- package/src/config/__tests__/config.integration.test.ts +8 -7
- package/src/config/__tests__/deserialize-value.test.ts +6 -3
- package/src/config/__tests__/inherited-redaction.integration.test.ts +3 -4
- package/src/config/feature.ts +8 -14
- package/src/config/handlers/set.write.ts +2 -2
- package/src/config/index.ts +3 -4
- package/src/config/resolver.ts +10 -34
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +0 -65
- package/src/custom-fields/constants.ts +0 -1
- package/src/custom-fields/events.ts +0 -6
- package/src/custom-fields/handlers/delete-system-field.write.ts +1 -2
- package/src/custom-fields/handlers/delete-tenant-field.write.ts +1 -2
- package/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -10
- package/src/custom-fields/wire-for-entity.ts +77 -116
- package/src/data-retention/__tests__/parse-override.test.ts +13 -13
- package/src/file-foundation/__tests__/file-foundation.integration.test.ts +3 -4
- package/src/folders/__tests__/folders.integration.test.ts +0 -3
- package/src/folders/web/__tests__/tree.test.ts +0 -8
- package/src/folders/web/folder-manager.tsx +3 -1
- package/src/folders/web/i18n.ts +2 -0
- package/src/folders/web/tree.ts +2 -13
- package/src/folders-user-data/__tests__/hooks.integration.test.ts +0 -1
- package/src/folders-user-data/index.ts +4 -4
- package/src/jobs/__tests__/job-system-user.integration.test.ts +8 -4
- package/src/jobs/handlers/projection-rebuild.job.ts +4 -14
- package/src/ledger/__tests__/ledger.integration.test.ts +3 -8
- package/src/ledger/handlers/confirm-schedule-period.write.ts +4 -17
- package/src/ledger/recurring.ts +8 -17
- package/src/ledger/web/index.ts +2 -3
- package/src/legal-pages/feature.ts +3 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +3 -4
- package/src/managed-pages/__tests__/branding-coerce.test.ts +6 -2
- package/src/managed-pages/feature.ts +3 -1
- package/src/readiness/__tests__/readiness.integration.test.ts +3 -4
- package/src/secrets/handlers/rotate.job.ts +98 -77
- package/src/secrets/secrets-context.ts +38 -7
- package/src/secrets/table.ts +7 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +4 -6
- package/src/sessions/__tests__/sessions.integration.test.ts +3 -3
- package/src/sessions/feature.ts +18 -51
- package/src/sessions/index.ts +2 -2
- package/src/subscription-stripe/__tests__/runtime.test.ts +24 -14
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +5 -6
- package/src/tags/__tests__/tags.integration.test.ts +1 -12
- package/src/tags/entity.ts +1 -6
- package/src/tags/schemas.ts +2 -9
- package/src/tags/web/tag-manager.tsx +1 -1
- package/src/template-resolver/testing.ts +1 -2
- package/src/tenant/__tests__/tenant.integration.test.ts +4 -8
- package/src/tier-engine/__tests__/tier-engine.integration.test.ts +4 -7
- package/src/user/__tests__/user.integration.test.ts +0 -26
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/index.ts +0 -4
- package/src/user-data-rights/__tests__/download.integration.test.ts +4 -7
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +3 -3
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +3 -3
- package/src/user-data-rights/feature.ts +3 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +6 -4
- package/src/user-data-rights/lib/update-user-lifecycle.ts +24 -8
- package/src/user-data-rights/run-forget-cleanup.ts +0 -1
- package/src/user-data-rights/run-user-export.ts +4 -43
- package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +1 -7
- package/src/user-data-rights/web/client-plugin.tsx +1 -6
- package/src/user-data-rights/web/i18n.ts +10 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +9 -31
- package/src/user-profile/__tests__/profile-screen.test.tsx +0 -4
- package/src/user-profile/handlers/change-email.write.ts +8 -11
- package/src/user-profile/web/profile-screen.tsx +1 -12
- package/src/auth-email-password/__tests__/feature-options.test.ts +0 -33
- package/src/auth-email-password/password-hashing.test.ts +0 -35
- package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +0 -29
- package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +0 -179
- package/src/config/handlers/reencrypt.job.ts +0 -197
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +0 -204
- package/src/personal-access-tokens/__tests__/scopes.test.ts +0 -42
- package/src/personal-access-tokens/constants.ts +0 -33
- package/src/personal-access-tokens/feature.ts +0 -78
- package/src/personal-access-tokens/handlers/available-scopes.query.ts +0 -21
- package/src/personal-access-tokens/handlers/create.write.ts +0 -41
- package/src/personal-access-tokens/handlers/list.query.ts +0 -47
- package/src/personal-access-tokens/handlers/revoke.write.ts +0 -37
- package/src/personal-access-tokens/hash.ts +0 -17
- package/src/personal-access-tokens/index.ts +0 -45
- package/src/personal-access-tokens/resolver.ts +0 -58
- package/src/personal-access-tokens/roles.ts +0 -29
- package/src/personal-access-tokens/schema/api-token.ts +0 -56
- package/src/personal-access-tokens/scopes.ts +0 -43
- package/src/personal-access-tokens/web/client-plugin.tsx +0 -29
- package/src/personal-access-tokens/web/i18n.ts +0 -69
- package/src/personal-access-tokens/web/index.ts +0 -7
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +0 -251
- package/src/sessions/__tests__/auto-revoke-binding.test.ts +0 -71
- package/src/shared/chunked-entity-migration.ts +0 -89
- package/src/shared/index.ts +0 -7
- package/src/tags/__tests__/schemas.test.ts +0 -34
- package/src/tags/web/__tests__/tag-manager.test.tsx +0 -64
- package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +0 -186
- package/src/user/db/queries/stream-tenant-backfill.ts +0 -112
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +0 -152
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +0 -108
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// user-feature runs with r.systemScope() but events land on a concrete
|
|
2
|
+
// tenant stream. The row.version tracks whichever stream the user's last
|
|
3
|
+
// modification wrote to — and that tenant is NOT discoverable from the
|
|
4
|
+
// row alone.
|
|
5
|
+
//
|
|
6
|
+
// Strategy: prioritize lastActiveTenantId (most likely holds the latest
|
|
7
|
+
// event), fall through to the remaining memberships in insertion order,
|
|
8
|
+
// and let the caller try each one in sequence. The first stream whose
|
|
9
|
+
// version matches row.version wins; the rest are bypassed.
|
|
10
|
+
//
|
|
11
|
+
// This is pragmatic — the real fix is to scope user events to
|
|
12
|
+
// SYSTEM_TENANT_ID when the feature is r.systemScope(), which is a
|
|
13
|
+
// framework-level change tracked separately. Until then, "try each
|
|
14
|
+
// tenant the user belongs to" is robust against non-deterministic
|
|
15
|
+
// memberships-query ordering (tenant:query:memberships has no ORDER BY).
|
|
16
|
+
|
|
17
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
+
|
|
19
|
+
export function orderTenantsByPreference(
|
|
20
|
+
memberships: readonly { readonly tenantId: TenantId }[],
|
|
21
|
+
lastActiveTenantId: string | null | undefined,
|
|
22
|
+
): TenantId[] {
|
|
23
|
+
if (memberships.length === 0) return [];
|
|
24
|
+
const ids = memberships.map((m) => m.tenantId);
|
|
25
|
+
if (!lastActiveTenantId) return ids;
|
|
26
|
+
// Move lastActiveTenantId to the front; preserve relative order of rest.
|
|
27
|
+
const preferred = ids.find((id) => id === lastActiveTenantId);
|
|
28
|
+
if (!preferred) return ids;
|
|
29
|
+
const rest = ids.filter((id) => id !== preferred);
|
|
30
|
+
return [preferred, ...rest];
|
|
31
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
19
19
|
import { BareFormProvider, cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
20
|
-
import { createContext, type ReactNode, useContext
|
|
20
|
+
import { createContext, type ReactNode, useContext } from "react";
|
|
21
21
|
|
|
22
22
|
// Wrappt die zentrierte Auth-Card in ihre Umgebung. Default = Fullscreen-
|
|
23
23
|
// zentriert (Standalone-Auth-Page). Eine Apex-/Marketing-Chrome reicht über
|
|
@@ -106,20 +106,3 @@ export function parseUrlToken(paramName = "token"): string {
|
|
|
106
106
|
if (typeof window === "undefined") return "";
|
|
107
107
|
return new URLSearchParams(window.location.search).get(paramName) ?? "";
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
// Reads the magic-link token from `?<paramName>=` once on mount, then strips
|
|
111
|
-
// that param from the URL via history.replaceState so the single-use token
|
|
112
|
-
// doesn't linger in browser history / Referer (#774). An explicit `override`
|
|
113
|
-
// (server-injected token) short-circuits both the URL read and the scrub.
|
|
114
|
-
export function useUrlToken(override?: string, paramName = "token"): string {
|
|
115
|
-
const [token] = useState(() => override ?? parseUrlToken(paramName));
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
if (override !== undefined) return;
|
|
118
|
-
if (typeof window === "undefined") return;
|
|
119
|
-
const url = new URL(window.location.href);
|
|
120
|
-
if (!url.searchParams.has(paramName)) return;
|
|
121
|
-
url.searchParams.delete(paramName);
|
|
122
|
-
window.history.replaceState(window.history.state, "", url.toString());
|
|
123
|
-
}, [override, paramName]);
|
|
124
|
-
return token;
|
|
125
|
-
}
|
|
@@ -32,9 +32,10 @@ export function makeAuthGate(
|
|
|
32
32
|
return AuthGate;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/** SessionProvider + AuthGate als ein Gate — damit öffentliche Gates davor
|
|
36
|
+
* (z.B. /rechner) den Session-Bootstrap nicht mounten. createKumikoApp
|
|
37
|
+
* stackt providers außerhalb aller gates; SessionProvider darf deshalb
|
|
38
|
+
* kein provider mehr sein. */
|
|
38
39
|
export function makeSessionAuthGate(
|
|
39
40
|
LoginComponent: ComponentType<LoginScreenProps> = LoginScreen,
|
|
40
41
|
loginProps?: LoginScreenProps,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
20
20
|
import { type FormEvent, type ReactNode, useContext, useState } from "react";
|
|
21
21
|
import { csrfHeader } from "./auth-client";
|
|
22
|
-
import { AuthCard, authMutedLinkClass,
|
|
22
|
+
import { AuthCard, authMutedLinkClass, parseUrlToken } from "./auth-form-primitives";
|
|
23
23
|
import { SessionContext, UNAUTHENTICATED } from "./session";
|
|
24
24
|
|
|
25
25
|
export type InviteAcceptScreenProps = {
|
|
@@ -47,7 +47,7 @@ export function InviteAcceptScreen({
|
|
|
47
47
|
// <SessionProvider> ancestor. Read the context directly and fall back to
|
|
48
48
|
// the anonymous state when no provider wraps this screen (632/1).
|
|
49
49
|
const session = useContext(SessionContext) ?? UNAUTHENTICATED;
|
|
50
|
-
const token =
|
|
50
|
+
const [token] = useState(() => tokenProp ?? parseUrlToken());
|
|
51
51
|
const [mode, setMode] = useState<Mode>(() =>
|
|
52
52
|
session.status === "authenticated" ? "loggedin" : "anon-existing",
|
|
53
53
|
);
|
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
// Code (anti-enumeration); UI zeigt unified "Link ungültig oder
|
|
6
6
|
// abgelaufen"-message.
|
|
7
7
|
//
|
|
8
|
-
// Token-Quelle ist read-once via
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// Token-Quelle ist read-once: wir lesen via parseUrlToken() einmalig
|
|
9
|
+
// im useState-Initializer. Apps die das anders brauchen (server-
|
|
10
|
+
// injected Token-Prop, andere Parameter-Namen) reichen einen
|
|
11
|
+
// expliziten `token` als Prop durch.
|
|
12
12
|
|
|
13
13
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
14
14
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
15
15
|
import { resetPassword } from "./auth-client";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
AuthCard,
|
|
18
|
+
authButtonClass,
|
|
19
|
+
authMutedLinkClass,
|
|
20
|
+
parseUrlToken,
|
|
21
|
+
} from "./auth-form-primitives";
|
|
17
22
|
|
|
18
23
|
export type ResetPasswordScreenProps = {
|
|
19
24
|
readonly title?: string;
|
|
@@ -32,7 +37,7 @@ export function ResetPasswordScreen({
|
|
|
32
37
|
}: ResetPasswordScreenProps): ReactNode {
|
|
33
38
|
const t = useTranslation();
|
|
34
39
|
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
35
|
-
const token =
|
|
40
|
+
const [token] = useState(() => tokenProp ?? parseUrlToken());
|
|
36
41
|
const [newPassword, setNewPassword] = useState("");
|
|
37
42
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
38
43
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// Server JWT + Cookies (Auto-Login!) und liefert tenantKey für den
|
|
7
7
|
// Post-Signup-Redirect.
|
|
8
8
|
//
|
|
9
|
-
// Token-Quelle ist read-once
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// Token-Quelle ist read-once: parseUrlToken im useState-Initializer.
|
|
10
|
+
// Apps die einen anderen URL-Param nutzen, reichen `token` als Prop
|
|
11
|
+
// durch.
|
|
12
12
|
//
|
|
13
13
|
// Nach success: redirect via window.location.assign zu loggedInHref.
|
|
14
14
|
// Default-Pattern ist "/<tenantKey>/" — die App reicht ein Template
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
19
19
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
20
20
|
import { confirmSignup } from "./auth-client";
|
|
21
|
-
import { AuthCard, authMutedLinkClass,
|
|
21
|
+
import { AuthCard, authMutedLinkClass, parseUrlToken } from "./auth-form-primitives";
|
|
22
22
|
|
|
23
23
|
export type SignupCompleteScreenProps = {
|
|
24
24
|
readonly title?: string;
|
|
@@ -44,7 +44,7 @@ export function SignupCompleteScreen({
|
|
|
44
44
|
}: SignupCompleteScreenProps): ReactNode {
|
|
45
45
|
const t = useTranslation();
|
|
46
46
|
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
47
|
-
const token =
|
|
47
|
+
const [token] = useState(() => tokenProp ?? parseUrlToken());
|
|
48
48
|
const [password, setPassword] = useState("");
|
|
49
49
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
50
50
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -33,9 +33,7 @@ export type UserMenuProps = {
|
|
|
33
33
|
* controlliert der Caller — wir packen nur den Frame drumrum. */
|
|
34
34
|
readonly children?: ReactNode;
|
|
35
35
|
/** "pill" (Default) = kompakter Topbar-Trigger; "sidebar" = volle NavUser-
|
|
36
|
-
* Row (Avatar + Name + Email) für den `sidebarFooter`-Slot der App-Shell.
|
|
37
|
-
* Requires a `SidebarProvider` ancestor (547/3) — the default App-Shell
|
|
38
|
-
* `sidebarFooter` slot already provides one. */
|
|
36
|
+
* Row (Avatar + Name + Email) für den `sidebarFooter`-Slot der App-Shell. */
|
|
39
37
|
readonly variant?: UserMenuVariant;
|
|
40
38
|
};
|
|
41
39
|
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
14
14
|
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
15
15
|
import { verifyEmail } from "./auth-client";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
AuthCard,
|
|
18
|
+
authButtonClass,
|
|
19
|
+
authMutedLinkClass,
|
|
20
|
+
parseUrlToken,
|
|
21
|
+
} from "./auth-form-primitives";
|
|
17
22
|
|
|
18
23
|
export type VerifyEmailScreenProps = {
|
|
19
24
|
readonly title?: string;
|
|
@@ -31,7 +36,7 @@ export function VerifyEmailScreen({
|
|
|
31
36
|
loginHref = "/login",
|
|
32
37
|
}: VerifyEmailScreenProps): ReactNode {
|
|
33
38
|
const t = useTranslation();
|
|
34
|
-
const token =
|
|
39
|
+
const [token] = useState(() => tokenProp ?? parseUrlToken());
|
|
35
40
|
const [status, setStatus] = useState<Status>(token === "" ? "missing-token" : "verifying");
|
|
36
41
|
const startedRef = useRef(false);
|
|
37
42
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createEncryptionProvider,
|
|
6
|
+
type DbConnection,
|
|
7
|
+
seedConfigValues,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
5
9
|
import {
|
|
6
10
|
access,
|
|
7
11
|
createSeed,
|
|
@@ -20,10 +24,7 @@ import {
|
|
|
20
24
|
TestUsers,
|
|
21
25
|
unsafePushTables,
|
|
22
26
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
-
import {
|
|
24
|
-
createTestEnvelopeCipher,
|
|
25
|
-
expectErrorIncludes,
|
|
26
|
-
} from "@cosmicdrift/kumiko-framework/testing";
|
|
27
|
+
import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
|
|
27
28
|
import { z } from "zod";
|
|
28
29
|
import { ConfigHandlers, ConfigQueries } from "../constants";
|
|
29
30
|
import { createConfigAccessor, createConfigAccessorFactory, createConfigFeature } from "../feature";
|
|
@@ -256,8 +257,8 @@ const transportFeature = defineFeature("transport", (r) => {
|
|
|
256
257
|
const testEncryptionKey = randomBytes(32).toString("base64");
|
|
257
258
|
|
|
258
259
|
beforeAll(async () => {
|
|
259
|
-
const encryption =
|
|
260
|
-
resolver = createConfigResolver({
|
|
260
|
+
const encryption = createEncryptionProvider(testEncryptionKey);
|
|
261
|
+
resolver = createConfigResolver({ encryption });
|
|
261
262
|
|
|
262
263
|
stack = await setupTestStack({
|
|
263
264
|
features: [
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { deserializeValue } from "../resolver";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// deserializeValue is the read boundary for every config value: the DB stores
|
|
5
|
+
// the JSON-encoded raw string, this turns it back into a typed primitive per the
|
|
6
|
+
// key's declared `type`. The coercion has non-obvious paths worth pinning so a
|
|
7
|
+
// refactor can't quietly change them: a stored value whose JSON type disagrees
|
|
8
|
+
// with the declared type is NOT rejected — it is coerced (number via Number(),
|
|
9
|
+
// boolean only via literal true / the string "true", text via String()).
|
|
7
10
|
|
|
8
11
|
describe("deserializeValue", () => {
|
|
9
12
|
test("null raw short-circuits to undefined before any parse", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import type
|
|
3
|
+
import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import {
|
|
5
5
|
access,
|
|
6
6
|
type ConfigCascade,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
TestUsers,
|
|
15
15
|
unsafePushTables,
|
|
16
16
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
17
|
-
import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
|
|
18
17
|
import { ConfigHandlers, ConfigQueries } from "../constants";
|
|
19
18
|
import { createConfigAccessorFactory, createConfigFeature } from "../feature";
|
|
20
19
|
import { type ConfigResolver, createConfigResolver } from "../resolver";
|
|
@@ -77,8 +76,8 @@ const platformFeature = defineFeature("platform", (r) => {
|
|
|
77
76
|
});
|
|
78
77
|
|
|
79
78
|
beforeAll(async () => {
|
|
80
|
-
const encryption =
|
|
81
|
-
resolver = createConfigResolver({
|
|
79
|
+
const encryption = createEncryptionProvider(randomBytes(32).toString("base64"));
|
|
80
|
+
resolver = createConfigResolver({ encryption });
|
|
82
81
|
|
|
83
82
|
stack = await setupTestStack({
|
|
84
83
|
features: [configFeature, platformFeature],
|
package/src/config/feature.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import type { DbConnection, EncryptionProvider, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import {
|
|
3
3
|
type ConfigAccessor,
|
|
4
4
|
type ConfigAccessorFactory,
|
|
@@ -13,10 +13,8 @@ import {
|
|
|
13
13
|
type TenantId,
|
|
14
14
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
15
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
16
|
-
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
17
16
|
import { cascadeQuery } from "./handlers/cascade.query";
|
|
18
17
|
import { readinessQuery } from "./handlers/readiness.query";
|
|
19
|
-
import { reencryptJob } from "./handlers/reencrypt.job";
|
|
20
18
|
import { resetWrite } from "./handlers/reset.write";
|
|
21
19
|
import { schemaQuery } from "./handlers/schema.query";
|
|
22
20
|
import { setWrite } from "./handlers/set.write";
|
|
@@ -29,7 +27,7 @@ export type ConfigContext = { readonly config: ConfigAccessor };
|
|
|
29
27
|
export function createConfigFeature(): FeatureDefinition {
|
|
30
28
|
return defineFeature("config", (r) => {
|
|
31
29
|
r.describe(
|
|
32
|
-
"Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through
|
|
30
|
+
"Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through an `EncryptionProvider`. Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.",
|
|
33
31
|
);
|
|
34
32
|
r.uiHints({
|
|
35
33
|
displayLabel: "Tenant Config Store",
|
|
@@ -57,12 +55,6 @@ export function createConfigFeature(): FeatureDefinition {
|
|
|
57
55
|
readiness: r.queryHandler(readinessQuery),
|
|
58
56
|
};
|
|
59
57
|
|
|
60
|
-
// Migration + KEK-rotation for encrypted values (legacy-format →
|
|
61
|
-
// envelope, old kekVersion → current). Manual trigger — ops runs it
|
|
62
|
-
// once after adding a new master key version or before retiring the
|
|
63
|
-
// legacy CONFIG_ENCRYPTION_KEY.
|
|
64
|
-
r.job("reencrypt", { trigger: { manual: true } }, reencryptJob);
|
|
65
|
-
|
|
66
58
|
return { handlers, queries };
|
|
67
59
|
});
|
|
68
60
|
}
|
|
@@ -124,14 +116,16 @@ export function requireConfigResolver(ctx: HandlerContext, handlerName: string):
|
|
|
124
116
|
// lazily so apps that never wire encryption still boot (and only crash
|
|
125
117
|
// if a handler tries to write an encrypted key without the provider in
|
|
126
118
|
// place, pointing at the exact wiring gap).
|
|
127
|
-
export function requireConfigEncryption(
|
|
119
|
+
export function requireConfigEncryption(
|
|
120
|
+
ctx: HandlerContext,
|
|
121
|
+
handlerName: string,
|
|
122
|
+
): EncryptionProvider {
|
|
128
123
|
if (!ctx.configEncryption) {
|
|
129
124
|
throw new InternalError({
|
|
130
125
|
message:
|
|
131
126
|
`[${handlerName}] ctx.configEncryption missing — at least one config key declares ` +
|
|
132
|
-
`encrypted: true, so the boot wiring must pass an
|
|
133
|
-
`extraContext.configEncryption (
|
|
134
|
-
`KUMIKO_SECRETS_MASTER_KEY_V<n>; same instance the resolver was built with).`,
|
|
127
|
+
`encrypted: true, so the boot wiring must pass an EncryptionProvider via ` +
|
|
128
|
+
`extraContext.configEncryption (same instance the resolver was built with).`,
|
|
135
129
|
});
|
|
136
130
|
}
|
|
137
131
|
return ctx.configEncryption;
|
|
@@ -88,8 +88,8 @@ export const setWrite = defineWriteHandler({
|
|
|
88
88
|
|
|
89
89
|
let serialized = JSON.stringify(event.payload.value);
|
|
90
90
|
if (keyDef.encrypted) {
|
|
91
|
-
const
|
|
92
|
-
serialized =
|
|
91
|
+
const encryption = requireConfigEncryption(ctx, "config:write:set");
|
|
92
|
+
serialized = encryption.encrypt(serialized);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const existing = await findConfigRow(db, event.payload.key, tenantId, userId);
|
package/src/config/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import type { DbConnection, EncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { seedConfigValues } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
4
|
import { configValueEntity, configValuesTable } from "./table";
|
|
6
5
|
|
|
7
6
|
export {
|
|
@@ -31,8 +30,8 @@ export { configValuesTable } from "./table";
|
|
|
31
30
|
export function seedAllConfigValues(
|
|
32
31
|
registry: Registry,
|
|
33
32
|
db: DbConnection,
|
|
34
|
-
|
|
33
|
+
encryption?: EncryptionProvider,
|
|
35
34
|
): Promise<{ created: number; skipped: number }> {
|
|
36
35
|
const seeds = registry.getAllConfigSeeds();
|
|
37
|
-
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db,
|
|
36
|
+
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db, encryption);
|
|
38
37
|
}
|
package/src/config/resolver.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { DbConnection, EncryptionProvider, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type {
|
|
4
4
|
ConfigCascade,
|
|
5
5
|
ConfigCascadeLevel,
|
|
@@ -12,7 +12,6 @@ import type {
|
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
13
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
14
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
15
|
-
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
16
15
|
import { assertUnreachable, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
17
16
|
import { selectConfigRowsForKeys, selectConfigRowsForScope } from "./db/queries/resolver";
|
|
18
17
|
import { configValuesTable } from "./table";
|
|
@@ -64,33 +63,10 @@ export function deserializeValue(
|
|
|
64
63
|
export type AppConfigOverrides = ReadonlyMap<string, string | number | boolean>;
|
|
65
64
|
|
|
66
65
|
export type ConfigResolverOptions = {
|
|
67
|
-
|
|
68
|
-
// automatically from the secrets master key — apps only pass their own
|
|
69
|
-
// to override (tests, custom providers).
|
|
70
|
-
cipher?: EnvelopeCipher;
|
|
66
|
+
encryption?: EncryptionProvider;
|
|
71
67
|
appOverrides?: AppConfigOverrides;
|
|
72
68
|
};
|
|
73
69
|
|
|
74
|
-
// Decrypt gate for encrypted keys: an encrypted row without a cipher MUST
|
|
75
|
-
// throw — passing the ciphertext through as the "value" (the pre-envelope
|
|
76
|
-
// behavior) silently feeds base64 garbage to whatever consumes the key
|
|
77
|
-
// (e.g. an SMTP password) and is a debugging nightmare.
|
|
78
|
-
async function decryptEncrypted(
|
|
79
|
-
cipher: EnvelopeCipher | undefined,
|
|
80
|
-
raw: string,
|
|
81
|
-
qualifiedKey: string,
|
|
82
|
-
): Promise<string> {
|
|
83
|
-
if (!cipher) {
|
|
84
|
-
throw new InternalError({
|
|
85
|
-
message:
|
|
86
|
-
`[config] key "${qualifiedKey}" is encrypted but no cipher is wired — the boot must ` +
|
|
87
|
-
`provide a master key (KUMIKO_SECRETS_MASTER_KEY_V<n>) or pass ConfigResolverOptions.cipher.`,
|
|
88
|
-
i18nKey: "config.errors.cipher_missing",
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
return cipher.decrypt(raw);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
70
|
// backing="secrets" keys store their value in the secrets store (flat per
|
|
95
71
|
// (tenant,key) at SYSTEM_TENANT_ID), not in config_values. Both read paths
|
|
96
72
|
// (getWithSource + buildCascade) route the system rung here. Missing reader =
|
|
@@ -125,7 +101,7 @@ async function buildCascade(
|
|
|
125
101
|
userId: string | null,
|
|
126
102
|
) => Promise<ConfigRow | null> | ConfigRow | null,
|
|
127
103
|
appOverrides: AppConfigOverrides | undefined,
|
|
128
|
-
|
|
104
|
+
encryption: EncryptionProvider | undefined,
|
|
129
105
|
secretsReader: ConfigSecretsReader | undefined,
|
|
130
106
|
): Promise<ConfigCascade> {
|
|
131
107
|
type Lookup = {
|
|
@@ -203,8 +179,8 @@ async function buildCascade(
|
|
|
203
179
|
const row = await fetchRow(lookup.tenantId, lookup.userId);
|
|
204
180
|
if (row?.value !== null && row?.value !== undefined) {
|
|
205
181
|
let raw = row.value;
|
|
206
|
-
if (keyDef.encrypted) {
|
|
207
|
-
raw =
|
|
182
|
+
if (keyDef.encrypted && encryption) {
|
|
183
|
+
raw = encryption.decrypt(raw);
|
|
208
184
|
}
|
|
209
185
|
if (activeIndex === -1) activeIndex = levels.length;
|
|
210
186
|
levels.push({
|
|
@@ -289,7 +265,7 @@ async function buildCascade(
|
|
|
289
265
|
}
|
|
290
266
|
|
|
291
267
|
export function createConfigResolver(options: ConfigResolverOptions = {}): ConfigResolver {
|
|
292
|
-
const {
|
|
268
|
+
const { encryption, appOverrides } = options;
|
|
293
269
|
async function findRow(
|
|
294
270
|
key: string,
|
|
295
271
|
tenantId: string,
|
|
@@ -380,8 +356,8 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
380
356
|
const row = await findRow(qualifiedKey, lookup.tenantId, lookup.userId, db);
|
|
381
357
|
if (row?.value !== null && row?.value !== undefined) {
|
|
382
358
|
let raw = row.value;
|
|
383
|
-
if (keyDef.encrypted) {
|
|
384
|
-
raw =
|
|
359
|
+
if (keyDef.encrypted && encryption) {
|
|
360
|
+
raw = encryption.decrypt(raw);
|
|
385
361
|
}
|
|
386
362
|
return { value: deserializeValue(raw, keyDef.type), source: lookup.source };
|
|
387
363
|
}
|
|
@@ -494,7 +470,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
494
470
|
db,
|
|
495
471
|
(tid, uid) => findRow(qualifiedKey, tid, uid, db),
|
|
496
472
|
appOverrides,
|
|
497
|
-
|
|
473
|
+
encryption,
|
|
498
474
|
secretsReader,
|
|
499
475
|
);
|
|
500
476
|
},
|
|
@@ -536,7 +512,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
536
512
|
(tid, uid) =>
|
|
537
513
|
keyRows.find((r) => r.tenantId === tid && (r.userId ?? null) === uid) ?? null,
|
|
538
514
|
appOverrides,
|
|
539
|
-
|
|
515
|
+
encryption,
|
|
540
516
|
secretsReader,
|
|
541
517
|
);
|
|
542
518
|
result.set(key, cascade);
|
|
@@ -22,10 +22,6 @@ import {
|
|
|
22
22
|
SYSTEM_TENANT_ID,
|
|
23
23
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
24
24
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
25
|
-
import {
|
|
26
|
-
createProjectionStateTable,
|
|
27
|
-
rebuildProjection,
|
|
28
|
-
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
29
25
|
import {
|
|
30
26
|
createTestUser,
|
|
31
27
|
setupTestStack,
|
|
@@ -88,7 +84,6 @@ beforeAll(async () => {
|
|
|
88
84
|
await unsafeCreateEntityTable(stack.db, fieldDefinitionEntity);
|
|
89
85
|
await unsafeCreateEntityTable(stack.db, propertyEntity);
|
|
90
86
|
await createEventsTable(stack.db);
|
|
91
|
-
await createProjectionStateTable(stack.db);
|
|
92
87
|
});
|
|
93
88
|
|
|
94
89
|
afterAll(async () => {
|
|
@@ -924,63 +919,3 @@ describe("custom-fields integration — update-tenant-field (Bug-Bash D2)", () =
|
|
|
924
919
|
expect(causeMessage).toContain("update-tenant-field");
|
|
925
920
|
});
|
|
926
921
|
});
|
|
927
|
-
|
|
928
|
-
describe("custom-fields integration — official host-entity rebuild (#759)", () => {
|
|
929
|
-
test("rebuildProjection replays customField.set/cleared + fieldDefinition.deleted", async () => {
|
|
930
|
-
await defineField("property", "color", "text");
|
|
931
|
-
await defineField("property", "doomed", "text");
|
|
932
|
-
const propId = "dddddddd-dddd-4000-8000-00000000000d";
|
|
933
|
-
await createProperty(propId, "RebuildOfficial");
|
|
934
|
-
await setCustomField("property", propId, "color", "blue");
|
|
935
|
-
await setCustomField("property", propId, "doomed", "bye");
|
|
936
|
-
// fieldDef-delete cascade removes the key — the rebuild must preserve the
|
|
937
|
-
// removal (fieldDefinition.deleted rides the extra "field-definition" source).
|
|
938
|
-
await stack.http.writeOk(
|
|
939
|
-
"custom-fields:write:delete-tenant-field",
|
|
940
|
-
{ entityName: "property", fieldKey: "doomed" },
|
|
941
|
-
admin,
|
|
942
|
-
);
|
|
943
|
-
await stack.eventDispatcher?.runOnce();
|
|
944
|
-
|
|
945
|
-
let row = (await listProperties()).rows.find((r) => r["id"] === propId);
|
|
946
|
-
expect(row?.["color"]).toBe("blue");
|
|
947
|
-
expect(row?.["doomed"]).toBeUndefined();
|
|
948
|
-
|
|
949
|
-
// The routine schema-migration path: full official rebuild of the host
|
|
950
|
-
// entity's implicit projection. Pre-#759 this reset every custom_fields
|
|
951
|
-
// jsonb to its default '{}' with no recovery path.
|
|
952
|
-
const result = await rebuildProjection("property-test:projection:property-entity", {
|
|
953
|
-
db: stack.db,
|
|
954
|
-
registry: stack.registry,
|
|
955
|
-
});
|
|
956
|
-
expect(result.eventsProcessed).toBeGreaterThan(0);
|
|
957
|
-
|
|
958
|
-
row = (await listProperties()).rows.find((r) => r["id"] === propId);
|
|
959
|
-
expect(row?.["name"]).toBe("RebuildOfficial");
|
|
960
|
-
expect(row?.["color"]).toBe("blue");
|
|
961
|
-
expect(row?.["doomed"]).toBeUndefined();
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
test("clear-custom-field removal survives the official rebuild", async () => {
|
|
965
|
-
await defineField("property", "flag", "text");
|
|
966
|
-
await defineField("property", "keep", "text");
|
|
967
|
-
const propId = "eeeeeeee-eeee-4000-8000-00000000000e";
|
|
968
|
-
await createProperty(propId, "ClearSurvives");
|
|
969
|
-
await setCustomField("property", propId, "flag", "on");
|
|
970
|
-
await setCustomField("property", propId, "keep", "yes");
|
|
971
|
-
await clearCustomField("property", propId, "flag");
|
|
972
|
-
await stack.eventDispatcher?.runOnce();
|
|
973
|
-
|
|
974
|
-
await rebuildProjection("property-test:projection:property-entity", {
|
|
975
|
-
db: stack.db,
|
|
976
|
-
registry: stack.registry,
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
const row = (await listProperties()).rows.find((r) => r["id"] === propId);
|
|
980
|
-
// Kontrollfeld beweist dass der Rebuild die set-Events real replayt hat —
|
|
981
|
-
// ein leeres {} wuerde "flag undefined" trivial erfuellen.
|
|
982
|
-
expect(row?.["keep"]).toBe("yes");
|
|
983
|
-
expect(row?.["flag"]).toBeUndefined();
|
|
984
|
-
expect(row?.["name"]).toBe("ClearSurvives");
|
|
985
|
-
});
|
|
986
|
-
});
|
|
@@ -31,7 +31,6 @@ export const CUSTOM_FIELDS_FORM_EXTENSION_NAME = "CustomFieldsFormSection";
|
|
|
31
31
|
// Entity-CRUD auto-events for the `field-definition` entity. registry.ts emits
|
|
32
32
|
// these as `${entityName}.created`/`.updated` (dot form) — they do NOT run
|
|
33
33
|
// through r.defineEvent/toKebab, so the dot MUST stay.
|
|
34
|
-
export const FIELD_DEFINITION_AGGREGATE_TYPE = "field-definition";
|
|
35
34
|
export const FIELD_DEFINITION_CREATED_EVENT = "field-definition.created";
|
|
36
35
|
export const FIELD_DEFINITION_UPDATED_EVENT = "field-definition.updated";
|
|
37
36
|
// Qualified at registration via r.defineEvent — final name is
|
|
@@ -15,12 +15,6 @@ export const customFieldSetSchema = z.object({
|
|
|
15
15
|
// self-projected into the host row by the write handler and must never enter
|
|
16
16
|
// the immutable log. Non-sensitive sets always carry the value.
|
|
17
17
|
value: z.unknown().optional(),
|
|
18
|
-
// Explicit protocol marker (527/1) — `value === undefined` alone is also a
|
|
19
|
-
// valid-looking "clear" shape; `_sensitive` names the reason so a future
|
|
20
|
-
// reader can't mistake one for the other. Optional so historical events
|
|
21
|
-
// (persisted before this field existed) still parse: apply-side readers
|
|
22
|
-
// keep `value === undefined` as their actual branch condition.
|
|
23
|
-
_sensitive: z.literal(true).optional(),
|
|
24
18
|
});
|
|
25
19
|
export type CustomFieldSetPayload = z.infer<typeof customFieldSetSchema>;
|
|
26
20
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { fieldDefinitionAggregateId } from "../aggregate-id";
|
|
3
|
-
import { FIELD_DEFINITION_AGGREGATE_TYPE } from "../constants";
|
|
4
3
|
import { fieldDefinitionExecutor } from "../executor";
|
|
5
4
|
import { customFieldsFeature } from "../feature";
|
|
6
5
|
import { type DeleteFieldPayload, deleteFieldPayloadSchema } from "../schemas";
|
|
@@ -31,7 +30,7 @@ export const deleteSystemFieldHandler: WriteHandlerDef = {
|
|
|
31
30
|
if (result.isSuccess) {
|
|
32
31
|
await ctx.unsafeAppendEvent({
|
|
33
32
|
aggregateId,
|
|
34
|
-
aggregateType:
|
|
33
|
+
aggregateType: "field-definition",
|
|
35
34
|
type: customFieldsFeature.exports.fieldDefinitionDeletedEvent.name,
|
|
36
35
|
payload: {
|
|
37
36
|
entityName: payload.entityName,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isSystemTenant, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { fieldDefinitionAggregateId } from "../aggregate-id";
|
|
3
|
-
import { FIELD_DEFINITION_AGGREGATE_TYPE } from "../constants";
|
|
4
3
|
import { fieldDefinitionExecutor } from "../executor";
|
|
5
4
|
import { customFieldsFeature } from "../feature";
|
|
6
5
|
import { type DeleteFieldPayload, deleteFieldPayloadSchema } from "../schemas";
|
|
@@ -39,7 +38,7 @@ export const deleteTenantFieldHandler: WriteHandlerDef = {
|
|
|
39
38
|
if (result.isSuccess) {
|
|
40
39
|
await ctx.unsafeAppendEvent({
|
|
41
40
|
aggregateId,
|
|
42
|
-
aggregateType:
|
|
41
|
+
aggregateType: "field-definition",
|
|
43
42
|
type: customFieldsFeature.exports.fieldDefinitionDeletedEvent.name,
|
|
44
43
|
payload: { entityName: payload.entityName, fieldKey: payload.fieldKey, tenantId },
|
|
45
44
|
});
|
|
@@ -135,7 +135,7 @@ export const setCustomFieldHandler: WriteHandlerDef = {
|
|
|
135
135
|
aggregateType: payload.entityName,
|
|
136
136
|
type: customFieldsFeature.exports.setEvent.name,
|
|
137
137
|
payload: sensitive
|
|
138
|
-
? { fieldKey: payload.fieldKey
|
|
138
|
+
? { fieldKey: payload.fieldKey }
|
|
139
139
|
: { fieldKey: payload.fieldKey, value: payload.value },
|
|
140
140
|
});
|
|
141
141
|
|