@cosmicdrift/kumiko-bundled-features 0.166.0 → 0.167.1
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 +7 -9
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +4 -2
- package/src/auth-email-password/__tests__/login-gates.test.ts +35 -1
- package/src/auth-email-password/auth-user-row.ts +2 -0
- package/src/auth-email-password/handlers/login.write.ts +9 -1
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +5 -6
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +1 -1
- package/src/config/__tests__/pii-encrypted.integration.test.ts +1 -1
- package/src/config/__tests__/tz-resolution.integration.test.ts +101 -0
- package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +2 -6
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +1 -1
- package/src/document-ingest-foundation/__tests__/pages-crypto-shredding.integration.test.ts +1 -1
- package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +2 -5
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +1 -2
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +2 -5
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +2 -6
- package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +2 -5
- package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +1 -2
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +6 -3
- package/src/sessions/__tests__/sessions.integration.test.ts +2 -2
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +1 -2
- package/src/tier-engine/compose-app.ts +14 -0
- package/src/user/__tests__/user.integration.test.ts +2 -1
- package/src/user/handlers/create.write.ts +2 -0
- package/src/user/handlers/update.write.ts +2 -0
- package/src/user/i18n.ts +1 -0
- package/src/user/schema/user.ts +6 -1
- package/src/user/screens.ts +1 -1
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +5 -3
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +5 -6
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +5 -2
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.167.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -119,11 +119,12 @@
|
|
|
119
119
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
123
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
124
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
125
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
126
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
122
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.167.1",
|
|
123
|
+
"@cosmicdrift/kumiko-framework": "0.167.1",
|
|
124
|
+
"@cosmicdrift/kumiko-headless": "0.167.1",
|
|
125
|
+
"@cosmicdrift/kumiko-renderer": "0.167.1",
|
|
126
|
+
"@cosmicdrift/kumiko-renderer-web": "0.167.1",
|
|
127
|
+
"@cosmicdrift/kumiko-types": "0.167.1",
|
|
127
128
|
"@mollie/api-client": "^4.5.0",
|
|
128
129
|
"imapflow": "^1.3.3",
|
|
129
130
|
"mailparser": "^3.9.8",
|
|
@@ -147,9 +148,6 @@
|
|
|
147
148
|
"README.md",
|
|
148
149
|
"LICENSE"
|
|
149
150
|
],
|
|
150
|
-
"peerDependencies": {
|
|
151
|
-
"@cosmicdrift/kumiko-types": "^0.166.0"
|
|
152
|
-
},
|
|
153
151
|
"devDependencies": {
|
|
154
152
|
"@testing-library/user-event": "^14.6.1",
|
|
155
153
|
"@types/mailparser": "^3.4.6",
|
|
@@ -13,8 +13,6 @@ import {
|
|
|
13
13
|
decryptPiiFieldValues,
|
|
14
14
|
InMemoryKmsAdapter,
|
|
15
15
|
isPiiCiphertext,
|
|
16
|
-
resetBlindIndexKeyForTests,
|
|
17
|
-
resetPiiSubjectKmsForTests,
|
|
18
16
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
19
17
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
18
|
import {
|
|
@@ -23,6 +21,10 @@ import {
|
|
|
23
21
|
unsafeCreateEntityTable,
|
|
24
22
|
unsafePushTables,
|
|
25
23
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
+
import {
|
|
25
|
+
resetBlindIndexKeyForTests,
|
|
26
|
+
resetPiiSubjectKmsForTests,
|
|
27
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
26
28
|
import { createChannelEmailFeature, createInMemoryTransport } from "../../channel-email";
|
|
27
29
|
import { createConfigFeature } from "../../config";
|
|
28
30
|
import { createConfigResolver } from "../../config/resolver";
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { USER_STATUS } from "../../user";
|
|
3
4
|
import type { AuthUserRow } from "../auth-user-row";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
gateBuildSession,
|
|
7
|
+
gateEnforceAccountStatus,
|
|
8
|
+
gateEnforceEmailVerified,
|
|
9
|
+
} from "../handlers/login.write";
|
|
10
|
+
|
|
11
|
+
// resolveAuthClaims is the only ctx member gateBuildSession touches.
|
|
12
|
+
const authClaimsCtx = {
|
|
13
|
+
resolveAuthClaims: async () => ({}),
|
|
14
|
+
} as unknown as HandlerContext;
|
|
5
15
|
|
|
6
16
|
function row(over: Partial<AuthUserRow> = {}): AuthUserRow {
|
|
7
17
|
return { id: "00000000-0000-4000-8000-000000000001", passwordHash: "x", ...over };
|
|
@@ -36,4 +46,28 @@ describe("login.write gates (fw#1284)", () => {
|
|
|
36
46
|
test("gateEnforceAccountStatus passes active", () => {
|
|
37
47
|
expect(gateEnforceAccountStatus(row({ status: USER_STATUS.Active })).ok).toBe(true);
|
|
38
48
|
});
|
|
49
|
+
|
|
50
|
+
test("gateBuildSession carries the user's timezone into the session", async () => {
|
|
51
|
+
const g = await gateBuildSession(
|
|
52
|
+
authClaimsCtx,
|
|
53
|
+
"00000000-0000-4000-8000-000000000001",
|
|
54
|
+
"00000000-0000-4000-8000-000000000002",
|
|
55
|
+
["User"],
|
|
56
|
+
"Asia/Tokyo",
|
|
57
|
+
);
|
|
58
|
+
expect(g.ok).toBe(true);
|
|
59
|
+
if (g.ok) expect(g.value.session.timezone).toBe("Asia/Tokyo");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("gateBuildSession omits timezone when the user never set one (fw#1636)", async () => {
|
|
63
|
+
const g = await gateBuildSession(
|
|
64
|
+
authClaimsCtx,
|
|
65
|
+
"00000000-0000-4000-8000-000000000001",
|
|
66
|
+
"00000000-0000-4000-8000-000000000002",
|
|
67
|
+
["User"],
|
|
68
|
+
null,
|
|
69
|
+
);
|
|
70
|
+
expect(g.ok).toBe(true);
|
|
71
|
+
if (g.ok) expect(g.value.session.timezone).toBeUndefined();
|
|
72
|
+
});
|
|
39
73
|
});
|
|
@@ -17,6 +17,8 @@ export type AuthUserRow = {
|
|
|
17
17
|
readonly isDeleted?: boolean | null;
|
|
18
18
|
readonly emailVerified?: boolean | null;
|
|
19
19
|
readonly lastActiveTenantId?: TenantId | string | null;
|
|
20
|
+
// IANA zone — threaded into SessionUser.timezone at login (fw#1636).
|
|
21
|
+
readonly timezone?: string | null;
|
|
20
22
|
// JSON-encoded string[] — globale Rollen die parallel zu tenant-membership-
|
|
21
23
|
// roles gelten (z.B. SystemAdmin, BillingAdmin). Caller deserialisiert via
|
|
22
24
|
// parseRoles() vor dem Merge in die Session.
|
|
@@ -224,11 +224,13 @@ export async function gateBuildSession(
|
|
|
224
224
|
userId: string,
|
|
225
225
|
tenantId: TenantId,
|
|
226
226
|
mergedRoles: readonly string[],
|
|
227
|
+
timezone?: string | null,
|
|
227
228
|
): Promise<GateOutcome<{ readonly kind: "auth-session"; readonly session: SessionUser }>> {
|
|
228
229
|
const baseSession: SessionUser = {
|
|
229
230
|
id: userId,
|
|
230
231
|
tenantId,
|
|
231
232
|
roles: mergedRoles,
|
|
233
|
+
...(timezone !== null && timezone !== undefined && { timezone }),
|
|
232
234
|
};
|
|
233
235
|
const claims = await ctx.resolveAuthClaims(baseSession);
|
|
234
236
|
const session: SessionUser =
|
|
@@ -294,7 +296,13 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
|
|
|
294
296
|
return { isSuccess: true, data: mfaGate.value };
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
const sessionGate = await gateBuildSession(
|
|
299
|
+
const sessionGate = await gateBuildSession(
|
|
300
|
+
ctx,
|
|
301
|
+
found.id,
|
|
302
|
+
chosen.tenantId,
|
|
303
|
+
mergedRoles,
|
|
304
|
+
found.timezone,
|
|
305
|
+
);
|
|
298
306
|
if (!sessionGate.ok) return sessionGate.result;
|
|
299
307
|
return { isSuccess: true, data: sessionGate.value };
|
|
300
308
|
},
|
|
@@ -10,11 +10,7 @@
|
|
|
10
10
|
// non-string userOwned field. enable-confirm failed for EVERY real deployment
|
|
11
11
|
// with crypto-shredding configured, while every test here stayed green.
|
|
12
12
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
-
import {
|
|
14
|
-
configurePiiSubjectKms,
|
|
15
|
-
InMemoryKmsAdapter,
|
|
16
|
-
resetPiiSubjectKmsForTests,
|
|
17
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
18
14
|
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
19
15
|
import {
|
|
20
16
|
createTestUser,
|
|
@@ -23,7 +19,10 @@ import {
|
|
|
23
19
|
unsafeCreateEntityTable,
|
|
24
20
|
unsafePushTables,
|
|
25
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
26
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createTestEnvelopeCipher,
|
|
24
|
+
resetPiiSubjectKmsForTests,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
27
26
|
import { createConfigFeature } from "../../config";
|
|
28
27
|
import { createConfigResolver } from "../../config/resolver";
|
|
29
28
|
import { configValuesTable } from "../../config/table";
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
InMemoryKmsAdapter,
|
|
21
21
|
isPiiCiphertext,
|
|
22
22
|
PII_ERASED_SENTINEL,
|
|
23
|
-
resetPiiSubjectKmsForTests,
|
|
24
23
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
25
24
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
26
25
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
testTenantId,
|
|
37
36
|
unsafeCreateEntityTable,
|
|
38
37
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
38
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
39
39
|
import {
|
|
40
40
|
ComplianceProfileHandlers,
|
|
41
41
|
createComplianceProfilesFeature,
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
14
|
PII_CIPHERTEXT_PREFIX,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
15
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
16
|
import {
|
|
18
17
|
access,
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
TestUsers,
|
|
28
27
|
unsafePushTables,
|
|
29
28
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
29
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
30
30
|
import { ConfigHandlers, ConfigQueries } from "../constants";
|
|
31
31
|
import { createConfigAccessorFactory, createConfigFeature } from "../feature";
|
|
32
32
|
import { createConfigResolver } from "../resolver";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// buildHandlerContext resolves ctx.tz.tenant from tenant:config:timezone and
|
|
2
|
+
// ctx.tz.user from SessionUser.timezone (fw#1636). "tenant" here is a
|
|
3
|
+
// standalone probe feature registering the same qualified config key the
|
|
4
|
+
// real bundled `tenant` feature uses — decoupled from that feature's
|
|
5
|
+
// entities/handlers, but exercising the identical runtime path.
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
7
|
+
import { access, createTenantConfig, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import {
|
|
9
|
+
createTestUser,
|
|
10
|
+
setupTestStack,
|
|
11
|
+
type TestStack,
|
|
12
|
+
unsafePushTables,
|
|
13
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { createConfigAccessorFactory, createConfigFeature } from "../feature";
|
|
16
|
+
import { createConfigResolver } from "../resolver";
|
|
17
|
+
import { configValuesTable } from "../table";
|
|
18
|
+
|
|
19
|
+
const tenantFeature = defineFeature("tenant", (r) => {
|
|
20
|
+
r.requires("config");
|
|
21
|
+
r.config({
|
|
22
|
+
keys: {
|
|
23
|
+
timezone: createTenantConfig("select", {
|
|
24
|
+
default: "UTC",
|
|
25
|
+
options: ["UTC", "Europe/Berlin", "Asia/Tokyo"],
|
|
26
|
+
write: access.roles("Admin"),
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const probeFeature = defineFeature("probe", (r) => {
|
|
33
|
+
r.requires("tenant");
|
|
34
|
+
r.writeHandler(
|
|
35
|
+
"read-tz",
|
|
36
|
+
z.object({}),
|
|
37
|
+
async (_event, ctx) => ({
|
|
38
|
+
isSuccess: true,
|
|
39
|
+
data: { tenant: ctx.tz.tenant, user: ctx.tz.user },
|
|
40
|
+
}),
|
|
41
|
+
{ access: { openToAll: true } },
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("buildHandlerContext ctx.tz resolution", () => {
|
|
46
|
+
let stack: TestStack;
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
const resolver = createConfigResolver();
|
|
50
|
+
stack = await setupTestStack({
|
|
51
|
+
features: [createConfigFeature(), tenantFeature, probeFeature],
|
|
52
|
+
extraContext: ({ registry }) => ({
|
|
53
|
+
configResolver: resolver,
|
|
54
|
+
_configAccessorFactory: createConfigAccessorFactory(registry, resolver),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
await unsafePushTables(stack.db, { configValuesTable });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
await stack.cleanup();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("defaults to UTC when no tenant config value is set and SessionUser has no timezone", async () => {
|
|
65
|
+
const user = createTestUser({ id: 10 });
|
|
66
|
+
const res = await stack.http.writeOk<{ tenant: string; user: string }>(
|
|
67
|
+
"probe:write:read-tz",
|
|
68
|
+
{},
|
|
69
|
+
user,
|
|
70
|
+
);
|
|
71
|
+
expect(res).toEqual({ tenant: "UTC", user: "UTC" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("ctx.tz.tenant reads tenant:config:timezone", async () => {
|
|
75
|
+
const admin = createTestUser({ id: 11, roles: ["Admin"] });
|
|
76
|
+
await stack.http.writeOk(
|
|
77
|
+
"config:write:set",
|
|
78
|
+
{ key: "tenant:config:timezone", value: "Europe/Berlin" },
|
|
79
|
+
admin,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const res = await stack.http.writeOk<{ tenant: string; user: string }>(
|
|
83
|
+
"probe:write:read-tz",
|
|
84
|
+
{},
|
|
85
|
+
admin,
|
|
86
|
+
);
|
|
87
|
+
expect(res).toEqual({ tenant: "Europe/Berlin", user: "Europe/Berlin" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("ctx.tz.user reads SessionUser.timezone independently of tenant", async () => {
|
|
91
|
+
// Same tenant as the previous test (Europe/Berlin already set) — proves
|
|
92
|
+
// user overrides without needing to touch tenant config.
|
|
93
|
+
const user = createTestUser({ id: 12, timezone: "Asia/Tokyo" });
|
|
94
|
+
const res = await stack.http.writeOk<{ tenant: string; user: string }>(
|
|
95
|
+
"probe:write:read-tz",
|
|
96
|
+
{},
|
|
97
|
+
user,
|
|
98
|
+
);
|
|
99
|
+
expect(res).toEqual({ tenant: "Europe/Berlin", user: "Asia/Tokyo" });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -9,15 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
11
11
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
12
|
-
import {
|
|
13
|
-
configurePiiSubjectKms,
|
|
14
|
-
InMemoryKmsAdapter,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
12
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
13
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
14
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
19
15
|
import { setupTestStack, type TestStack, testTenantId } from "@cosmicdrift/kumiko-framework/stack";
|
|
20
|
-
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
16
|
+
import { resetPiiSubjectKmsForTests, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
21
17
|
import { SUBJECT_FORGOTTEN_EVENT_NAME } from "../constants";
|
|
22
18
|
import { createCryptoShreddingFeature } from "../feature";
|
|
23
19
|
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
14
|
PII_ERASED_SENTINEL,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
15
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
16
|
import {
|
|
18
17
|
defineFeature,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
TestUsers,
|
|
29
28
|
unsafePushTables,
|
|
30
29
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
30
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
31
31
|
import { z } from "zod";
|
|
32
32
|
import { createChannelEmailFeature } from "../../channel-email/feature";
|
|
33
33
|
import { createInMemoryTransport } from "../../channel-email/types";
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
configurePiiSubjectKms,
|
|
13
13
|
InMemoryKmsAdapter,
|
|
14
14
|
PII_ERASED_SENTINEL,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
15
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
16
|
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
18
17
|
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
testTenantId,
|
|
23
22
|
unsafeCreateEntityTable,
|
|
24
23
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
25
|
import { createConfigFeature } from "../../config";
|
|
26
26
|
import { documentExtractEntity, documentExtractsTable } from "../entity";
|
|
27
27
|
import { readIngestPages, writeIngestPages } from "../pages";
|
|
@@ -7,11 +7,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
9
9
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
-
import {
|
|
11
|
-
configurePiiSubjectKms,
|
|
12
|
-
InMemoryKmsAdapter,
|
|
13
|
-
resetPiiSubjectKmsForTests,
|
|
14
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
10
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
15
11
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
16
12
|
import {
|
|
17
13
|
createSystemUser,
|
|
@@ -31,6 +27,7 @@ import {
|
|
|
31
27
|
import {
|
|
32
28
|
createMutableMasterKeyProvider,
|
|
33
29
|
type MutableMasterKeyProvider,
|
|
30
|
+
resetPiiSubjectKmsForTests,
|
|
34
31
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
35
32
|
import { Hono } from "hono";
|
|
36
33
|
import {
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
InMemoryKmsAdapter,
|
|
21
21
|
isPiiCiphertext,
|
|
22
22
|
PII_ERASED_SENTINEL,
|
|
23
|
-
resetPiiSubjectKmsForTests,
|
|
24
23
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
25
24
|
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
26
25
|
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -39,7 +38,7 @@ import {
|
|
|
39
38
|
testTenantId,
|
|
40
39
|
unsafeCreateEntityTable,
|
|
41
40
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
42
|
-
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing";
|
|
41
|
+
import { bridgeStub, resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
43
42
|
import {
|
|
44
43
|
createComplianceProfilesFeature,
|
|
45
44
|
tenantComplianceProfileEntity,
|
|
@@ -7,11 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
9
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
-
import {
|
|
11
|
-
configurePiiSubjectKms,
|
|
12
|
-
InMemoryKmsAdapter,
|
|
13
|
-
resetPiiSubjectKmsForTests,
|
|
14
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
10
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
15
11
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
16
12
|
import { createEventsTable, loadAggregate } from "@cosmicdrift/kumiko-framework/event-store";
|
|
17
13
|
import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
@@ -22,6 +18,7 @@ import {
|
|
|
22
18
|
testTenantId,
|
|
23
19
|
unsafeCreateEntityTable,
|
|
24
20
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
22
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
26
23
|
import {
|
|
27
24
|
createComplianceProfilesFeature,
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
7
7
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
8
8
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
|
-
import {
|
|
10
|
-
configurePiiSubjectKms,
|
|
11
|
-
InMemoryKmsAdapter,
|
|
12
|
-
resetPiiSubjectKmsForTests,
|
|
13
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
9
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
14
10
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
15
11
|
import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
12
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -21,7 +17,7 @@ import {
|
|
|
21
17
|
testTenantId,
|
|
22
18
|
unsafeCreateEntityTable,
|
|
23
19
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
|
|
20
|
+
import { resetPiiSubjectKmsForTests, waitFor } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
21
|
import {
|
|
26
22
|
createComplianceProfilesFeature,
|
|
27
23
|
tenantComplianceProfileEntity,
|
|
@@ -16,11 +16,7 @@ import { randomBytes } from "node:crypto";
|
|
|
16
16
|
import { connect } from "node:net";
|
|
17
17
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
18
18
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
-
import {
|
|
20
|
-
configurePiiSubjectKms,
|
|
21
|
-
InMemoryKmsAdapter,
|
|
22
|
-
resetPiiSubjectKmsForTests,
|
|
23
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
19
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
24
20
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
25
21
|
import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
22
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -36,6 +32,7 @@ import {
|
|
|
36
32
|
import {
|
|
37
33
|
createMutableMasterKeyProvider,
|
|
38
34
|
type MutableMasterKeyProvider,
|
|
35
|
+
resetPiiSubjectKmsForTests,
|
|
39
36
|
waitFor,
|
|
40
37
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
41
38
|
import { createTransport } from "nodemailer";
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
InMemoryKmsAdapter,
|
|
14
14
|
isPiiCiphertext,
|
|
15
15
|
PII_ERASED_SENTINEL,
|
|
16
|
-
resetPiiSubjectKmsForTests,
|
|
17
16
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
18
17
|
import { createRegistry } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
18
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
type TestRedis,
|
|
25
24
|
unsafePushTables,
|
|
26
25
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
27
|
-
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
26
|
+
import { resetPiiSubjectKmsForTests, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
28
27
|
import { createJobsFeature } from "../feature";
|
|
29
28
|
import { createJobRunLogger, JOB_RUN_STARTED_EVENT } from "../job-run-logger";
|
|
30
29
|
import { jobRunLogsTable, jobRunsTable } from "../job-run-table";
|
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
configurePiiSubjectKms,
|
|
8
8
|
InMemoryKmsAdapter,
|
|
9
9
|
isPiiCiphertext,
|
|
10
|
-
resetBlindIndexKeyForTests,
|
|
11
|
-
resetPiiSubjectKmsForTests,
|
|
12
10
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
11
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
12
|
import {
|
|
@@ -18,7 +16,12 @@ import {
|
|
|
18
16
|
unsafeCreateEntityTable,
|
|
19
17
|
unsafePushTables,
|
|
20
18
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
createTestEnvelopeCipher,
|
|
21
|
+
deleteRows,
|
|
22
|
+
resetBlindIndexKeyForTests,
|
|
23
|
+
resetPiiSubjectKmsForTests,
|
|
24
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
22
25
|
import { Temporal } from "temporal-polyfill";
|
|
23
26
|
import { AuthHandlers } from "../../auth-email-password/constants";
|
|
24
27
|
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
configurePiiSubjectKms,
|
|
8
8
|
InMemoryKmsAdapter,
|
|
9
9
|
isPiiCiphertext,
|
|
10
|
-
resetBlindIndexKeyForTests,
|
|
11
|
-
resetPiiSubjectKmsForTests,
|
|
12
10
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
11
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
12
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -24,6 +22,8 @@ import {
|
|
|
24
22
|
createLateBoundHolder,
|
|
25
23
|
createTestEnvelopeCipher,
|
|
26
24
|
deleteRows,
|
|
25
|
+
resetBlindIndexKeyForTests,
|
|
26
|
+
resetPiiSubjectKmsForTests,
|
|
27
27
|
resetTestTables,
|
|
28
28
|
updateRows,
|
|
29
29
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
configurePiiSubjectKms,
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
|
-
resetPiiSubjectKmsForTests,
|
|
15
14
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
16
15
|
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
16
|
import {
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
unsafeCreateEntityTable,
|
|
22
21
|
unsafePushTables,
|
|
23
22
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
23
|
+
import { resetPiiSubjectKmsForTests, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
24
|
import { createConfigFeature } from "../../config";
|
|
26
25
|
import { configValuesTable } from "../../config/table";
|
|
27
26
|
import { createUserFeature } from "../../user/feature";
|
|
@@ -84,6 +84,20 @@ export type ComposedApp<TCaps extends Readonly<Record<string, unknown>>> = {
|
|
|
84
84
|
* delivered something the platform doesn't understand. Failing loud at boot
|
|
85
85
|
* is safer than silently mounting the wrong feature-set and hoping nobody
|
|
86
86
|
* notices when "Pro" turns out to mean "Free".
|
|
87
|
+
*
|
|
88
|
+
* **No production callers, and that is not rot.** This is the BOOT-TIME half
|
|
89
|
+
* of tier composition — which features get mounted at all. The RUNTIME half,
|
|
90
|
+
* which features a given tenant may see out of everything mounted, is
|
|
91
|
+
* `createTierEngineFeature`'s `resolver(tenantId) => ReadonlySet<string>`
|
|
92
|
+
* (feature.ts). Both read the same tierMap at opposite ends. Apps use the
|
|
93
|
+
* resolver today; nobody composes per-tenant feature sets at boot yet, so an
|
|
94
|
+
* app that only needs caps merges them locally rather than constructing a
|
|
95
|
+
* featureRegistry it has no other use for (kumiko-studio's
|
|
96
|
+
* `resolvePlatformCaps`, studio#154/1 → PR #162 — a deliberate, typed copy of
|
|
97
|
+
* step 4, not a workaround). Whether the boot-time half ever gets a caller is
|
|
98
|
+
* a product question: kumiko-studio's BYO pivot (Sprint 8a Phase 3a)
|
|
99
|
+
* differentiates tiers by CAPS, not by feature set. Delete this only once no
|
|
100
|
+
* app plans to differentiate by feature set — the engine is app-agnostic.
|
|
87
101
|
*/
|
|
88
102
|
export function composeApp<TCaps extends Readonly<Record<string, unknown>>>(
|
|
89
103
|
input: ComposeAppInput<TCaps>,
|
|
@@ -64,8 +64,9 @@ describe("scenario 1: create + me", () => {
|
|
|
64
64
|
id: created.id,
|
|
65
65
|
email: "marc@example.com",
|
|
66
66
|
displayName: "Marc",
|
|
67
|
-
locale: "de", // comes from the entity's default — client didn't send it
|
|
68
67
|
});
|
|
68
|
+
// No entity-level default — client didn't send a locale, so it stays unset.
|
|
69
|
+
expect(me["locale"] == null).toBe(true);
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
test("normal user cannot create another user", async () => {
|
|
@@ -2,6 +2,7 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { ConflictError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
|
+
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { UserErrors } from "../constants";
|
|
7
8
|
import { userEntity, userTable } from "../schema/user";
|
|
@@ -23,6 +24,7 @@ export const createWrite = defineWriteHandler({
|
|
|
23
24
|
passwordHash: z.string().optional(),
|
|
24
25
|
displayName: z.string().min(1).max(100),
|
|
25
26
|
locale: z.string().min(2).max(10).optional(),
|
|
27
|
+
timezone: z.string().max(64).refine(isValidIanaTimeZone, "invalid IANA time zone").optional(),
|
|
26
28
|
// Globale Rollen — JSON-encoded string[]. Optional weil der Default
|
|
27
29
|
// im Entity-Schema "[]" ist; setzen wenn man einen SystemAdmin (oder
|
|
28
30
|
// andere globale Rollen) anlegt. Field-Access (write: privileged) auf
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { access, defineWriteHandler, hasAccess } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { UserErrors } from "../constants";
|
|
6
7
|
import { userEntity, userTable } from "../schema/user";
|
|
@@ -22,6 +23,7 @@ export const updateWrite = defineWriteHandler({
|
|
|
22
23
|
changes: z.object({
|
|
23
24
|
displayName: z.string().min(1).max(100).optional(),
|
|
24
25
|
locale: z.string().min(2).max(10).optional(),
|
|
26
|
+
timezone: z.string().max(64).refine(isValidIanaTimeZone, "invalid IANA time zone").optional(),
|
|
25
27
|
email: z.email().optional(),
|
|
26
28
|
passwordHash: z.string().optional(),
|
|
27
29
|
lastActiveTenantId: z.string().optional(),
|
package/src/user/i18n.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const USER_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
|
8
8
|
"user:entity:user:field:status": { de: "Status", en: "Status" },
|
|
9
9
|
"user:entity:user:field:emailVerified": { de: "E-Mail bestätigt", en: "Email verified" },
|
|
10
10
|
"user:entity:user:field:locale": { de: "Sprache", en: "Locale" },
|
|
11
|
+
"user:entity:user:field:timezone": { de: "Zeitzone", en: "Timezone" },
|
|
11
12
|
"user:entity:user:field:status:option:active": { de: "Aktiv", en: "Active" },
|
|
12
13
|
"user:entity:user:field:status:option:restricted": { de: "Eingeschränkt", en: "Restricted" },
|
|
13
14
|
"user:entity:user:field:status:option:deletionRequested": {
|
package/src/user/schema/user.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createSelectField,
|
|
7
7
|
createTextField,
|
|
8
8
|
createTimestampField,
|
|
9
|
+
createTzField,
|
|
9
10
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -87,7 +88,11 @@ export const userEntity = createEntity({
|
|
|
87
88
|
pii: true,
|
|
88
89
|
searchable: true,
|
|
89
90
|
}),
|
|
90
|
-
|
|
91
|
+
// No default here — tenant-settings owns the app-wide locale default; see #1637.
|
|
92
|
+
locale: createTextField({ maxLength: 10 }),
|
|
93
|
+
// No default here — mirrors locale: ctx.tz falls back to tenant.timezone
|
|
94
|
+
// (then "UTC") when unset, see buildHandlerContext (fw#1636).
|
|
95
|
+
timezone: createTzField(),
|
|
91
96
|
|
|
92
97
|
// Which tenant should this user land in on next login. Set by the login
|
|
93
98
|
// handler (SYSTEM), read by the login flow + UI for deep-linking.
|
package/src/user/screens.ts
CHANGED
|
@@ -11,8 +11,6 @@ import {
|
|
|
11
11
|
configurePiiSubjectKms,
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
|
-
resetBlindIndexKeyForTests,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
14
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
15
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
18
16
|
import {
|
|
@@ -21,7 +19,11 @@ import {
|
|
|
21
19
|
testTenantId,
|
|
22
20
|
unsafeCreateEntityTable,
|
|
23
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
resetBlindIndexKeyForTests,
|
|
24
|
+
resetPiiSubjectKmsForTests,
|
|
25
|
+
resetTestTables,
|
|
26
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
25
27
|
import {
|
|
26
28
|
createComplianceProfilesFeature,
|
|
27
29
|
tenantComplianceProfileEntity,
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
7
7
|
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
8
8
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
|
-
import {
|
|
10
|
-
buildEntityTable,
|
|
11
|
-
configureEntityFieldEncryption,
|
|
12
|
-
resetEntityFieldEncryptionCacheForTests,
|
|
13
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
+
import { buildEntityTable, configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
14
10
|
import {
|
|
15
11
|
createEntity,
|
|
16
12
|
createRegistry,
|
|
@@ -20,7 +16,10 @@ import {
|
|
|
20
16
|
type UserDataExportHook,
|
|
21
17
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
22
18
|
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
createTestEnvelopeCipher,
|
|
21
|
+
resetEntityFieldEncryptionCacheForTests,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
24
23
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
25
24
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
26
25
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
configurePiiSubjectKms,
|
|
22
22
|
encryptPiiFieldValues,
|
|
23
23
|
InMemoryKmsAdapter,
|
|
24
|
-
resetPiiSubjectKmsForTests,
|
|
25
24
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
26
25
|
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
27
26
|
import {
|
|
@@ -30,7 +29,11 @@ import {
|
|
|
30
29
|
unsafeCreateEntityTable,
|
|
31
30
|
unsafePushTables,
|
|
32
31
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
33
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
resetPiiSubjectKmsForTests,
|
|
34
|
+
resetTestTables,
|
|
35
|
+
seedRow,
|
|
36
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
34
37
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
35
38
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
36
39
|
import { createDataRetentionFeature } from "../../data-retention";
|