@cosmicdrift/kumiko-bundled-features 0.158.2 → 0.159.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 -6
- package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +21 -0
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +15 -1
- package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +12 -0
- package/src/auth-email-password/constants.ts +15 -0
- package/src/auth-email-password/email-templates.ts +32 -0
- package/src/auth-email-password/errors.ts +9 -0
- package/src/auth-email-password/feature.ts +37 -5
- package/src/auth-email-password/handlers/change-password.write.ts +2 -1
- package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +6 -2
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +1 -0
- package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +2 -1
- package/src/auth-email-password/handlers/signup-confirm.write.ts +2 -1
- package/src/auth-email-password/i18n.ts +42 -0
- package/src/auth-email-password/index.ts +2 -0
- package/src/auth-email-password/lockout-store.ts +10 -5
- package/src/auth-email-password/password-policy.ts +2108 -0
- package/src/auth-email-password/signed-token.ts +1 -0
- package/src/auth-email-password/unlock-token.ts +28 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +65 -3
- package/src/auth-email-password/web/auth-client.ts +33 -0
- package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
- package/src/auth-email-password/web/index.ts +6 -0
- package/src/auth-email-password/web/login-screen.tsx +15 -0
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
- package/src/auth-foundation/__tests__/feature.test.ts +33 -0
- package/src/auth-foundation/__tests__/session-store.test.ts +78 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +117 -0
- package/src/auth-foundation/boot-checks.ts +91 -0
- package/src/auth-foundation/feature.ts +103 -0
- package/src/auth-foundation/index.ts +23 -0
- package/src/auth-foundation/types.ts +107 -0
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +8 -0
- package/src/auth-mfa/__tests__/verify.integration.test.ts +20 -0
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +118 -0
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +173 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +100 -0
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +69 -0
- package/src/channel-email/pii-guard.ts +6 -3
- package/src/config/__tests__/config.integration.test.ts +14 -0
- package/src/config/__tests__/pii-encrypted.integration.test.ts +3 -2
- package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +20 -3
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +2 -3
- package/src/config/feature.ts +2 -4
- package/src/config/handlers/set.write.ts +7 -1
- package/src/config/resolver.ts +1 -1
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +2 -0
- package/src/delivery/handlers/log.query.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +7 -0
- package/src/folders/web/__tests__/folder-manager.test.tsx +27 -2
- package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +57 -6
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +93 -0
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +60 -50
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +12 -0
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +48 -0
- package/src/inbound-provider-inmemory/feature.ts +2 -2
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +53 -1
- package/src/jobs/handlers/detail.query.ts +1 -1
- package/src/jobs/handlers/list.query.ts +1 -1
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +42 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +19 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +60 -0
- package/src/managed-pages/handlers/branding.query.ts +5 -1
- package/src/personal-access-tokens/__tests__/feature.test.ts +18 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +8 -11
- package/src/personal-access-tokens/feature.ts +22 -7
- package/src/personal-access-tokens/handlers/list.query.ts +1 -1
- package/src/personal-access-tokens/index.ts +0 -14
- package/src/personal-access-tokens/resolver.ts +2 -2
- package/src/secrets/__tests__/rotate.integration.test.ts +31 -0
- package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +5 -3
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +11 -2
- package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +10 -1
- package/src/sessions/__tests__/sessions.integration.test.ts +28 -17
- package/src/sessions/__tests__/test-helpers.ts +15 -2
- package/src/sessions/feature.ts +31 -3
- package/src/sessions/handlers/detail.query.ts +2 -2
- package/src/sessions/handlers/list.query.ts +4 -2
- package/src/sessions/handlers/mine.query.ts +4 -2
- package/src/sessions/testing.ts +14 -1
- package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
- package/src/shared/decrypt-stored-pii.ts +12 -5
- package/src/subscription-mollie/__tests__/feature.test.ts +87 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +120 -1
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
- package/src/tenant/handlers/invitations.query.ts +4 -2
- package/src/tenant/handlers/members.query.ts +2 -2
- package/src/tenant/seeding.ts +2 -2
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +2 -0
- package/src/tenant-lifecycle/boot-checks.ts +53 -0
- package/src/tenant-lifecycle/feature.ts +7 -0
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/src/tier-engine/feature.ts +2 -2
- package/src/user-data-rights/COMPLIANCE.md +20 -0
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +172 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +59 -4
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-retention.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +10 -1
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +15 -7
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +48 -15
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
- package/src/user-data-rights/boot-checks.ts +86 -0
- package/src/user-data-rights/feature.ts +116 -6
- package/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
- package/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/src/user-data-rights/handlers/lift-restriction.write.ts +13 -19
- package/src/user-data-rights/handlers/restrict-account.write.ts +30 -8
- package/src/user-data-rights/run-export-jobs.ts +3 -3
- package/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +3 -0
- package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +2 -0
- package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +1 -1
- package/src/user-profile/__tests__/change-email.integration.test.ts +2 -0
- package/src/user-profile/handlers/change-email.write.ts +1 -1
- package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +0 -179
package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
15
15
|
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
16
17
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
18
|
import {
|
|
18
19
|
createRegistry,
|
|
@@ -58,7 +59,7 @@ import { createDataRetentionFeature } from "../../data-retention";
|
|
|
58
59
|
import { createSessionsFeature } from "../../sessions";
|
|
59
60
|
import { userSessionEntity, userSessionTable } from "../../sessions/schema/user-session";
|
|
60
61
|
import { createSessionCallbacks, type SessionCallbacks } from "../../sessions/session-callbacks";
|
|
61
|
-
import { sessionCallbacksFromLateBound } from "../../sessions/testing";
|
|
62
|
+
import { sessionCallbacksFromLateBound, withMintedSession } from "../../sessions/testing";
|
|
62
63
|
import { hashPassword } from "../../shared";
|
|
63
64
|
import { createTenantFeature, tenantMembershipsTable } from "../../tenant";
|
|
64
65
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -91,6 +92,7 @@ function buildFeatures() {
|
|
|
91
92
|
createDataRetentionFeature(),
|
|
92
93
|
createComplianceProfilesFeature(),
|
|
93
94
|
createAuthEmailPasswordFeature(),
|
|
95
|
+
authFoundationFeature,
|
|
94
96
|
createSessionsFeature(),
|
|
95
97
|
createUserDataRightsFeature(),
|
|
96
98
|
];
|
|
@@ -137,6 +139,12 @@ beforeEach(async () => {
|
|
|
137
139
|
]);
|
|
138
140
|
});
|
|
139
141
|
|
|
142
|
+
// Any actor hitting a write handler needs a live sid once sessionChecker
|
|
143
|
+
// is wired.
|
|
144
|
+
function mintActor(user: { id: string; tenantId: TenantId; roles: readonly string[] }) {
|
|
145
|
+
return withMintedSession((u, meta) => callbacks.get().sessionCreator(u, meta), user);
|
|
146
|
+
}
|
|
147
|
+
|
|
140
148
|
describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
|
|
141
149
|
test("Restricted-Status ueberlebt einen Projection-Rebuild (T_create != T_active)", async () => {
|
|
142
150
|
// Diskriminierende Praemisse: user.created landet auf systemAdmin.tenantId
|
|
@@ -149,7 +157,7 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
|
|
|
149
157
|
const created = await stack.http.writeOk<{ id: string }>(
|
|
150
158
|
UserHandlers.create,
|
|
151
159
|
{ email: ALICE_EMAIL, passwordHash: hash, displayName: "Alice" },
|
|
152
|
-
TestUsers.systemAdmin,
|
|
160
|
+
await mintActor(TestUsers.systemAdmin),
|
|
153
161
|
);
|
|
154
162
|
// Mitgliedschaft + Lifecycle auf einem ANDEREN, aktiven Tenant.
|
|
155
163
|
await seedTenantMembership(stack.db, {
|
|
@@ -158,7 +166,7 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
|
|
|
158
166
|
roles: ["Member"],
|
|
159
167
|
});
|
|
160
168
|
|
|
161
|
-
const aliceActive = { id: created.id, tenantId: T_ACTIVE, roles: ["Member"] };
|
|
169
|
+
const aliceActive = await mintActor({ id: created.id, tenantId: T_ACTIVE, roles: ["Member"] });
|
|
162
170
|
const restricted = await stack.http.writeOk<{ status: string }>(RESTRICT, {}, aliceActive);
|
|
163
171
|
expect(restricted.status).toBe(USER_STATUS.Restricted);
|
|
164
172
|
|
|
@@ -187,7 +195,7 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
|
|
|
187
195
|
const created = await stack.http.writeOk<{ id: string }>(
|
|
188
196
|
UserHandlers.create,
|
|
189
197
|
{ email: "grace.rebuild@example.com", passwordHash: hash, displayName: "Grace" },
|
|
190
|
-
TestUsers.systemAdmin,
|
|
198
|
+
await mintActor(TestUsers.systemAdmin),
|
|
191
199
|
);
|
|
192
200
|
|
|
193
201
|
const T = getTemporal();
|
|
@@ -218,7 +226,7 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
|
|
|
218
226
|
const created = await stack.http.writeOk<{ id: string }>(
|
|
219
227
|
UserHandlers.create,
|
|
220
228
|
{ email: "legacy.rebuild@example.com", passwordHash: hash, displayName: "Legacy" },
|
|
221
|
-
TestUsers.systemAdmin,
|
|
229
|
+
await mintActor(TestUsers.systemAdmin),
|
|
222
230
|
);
|
|
223
231
|
|
|
224
232
|
// Prae-Fix-Zustand simulieren: roher Write OHNE Event.
|
|
@@ -256,14 +264,14 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
|
|
|
256
264
|
const goodRow = await stack.http.writeOk<{ id: string }>(
|
|
257
265
|
UserHandlers.create,
|
|
258
266
|
{ email: "healthy.rebuild@example.com", passwordHash: hash, displayName: "Healthy" },
|
|
259
|
-
TestUsers.systemAdmin,
|
|
267
|
+
await mintActor(TestUsers.systemAdmin),
|
|
260
268
|
);
|
|
261
269
|
await updateRows(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: goodRow.id });
|
|
262
270
|
|
|
263
271
|
const badRow = await stack.http.writeOk<{ id: string }>(
|
|
264
272
|
UserHandlers.create,
|
|
265
273
|
{ email: "corrupt.rebuild@example.com", passwordHash: hash, displayName: "Corrupt" },
|
|
266
|
-
TestUsers.systemAdmin,
|
|
274
|
+
await mintActor(TestUsers.systemAdmin),
|
|
267
275
|
);
|
|
268
276
|
await updateRows(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: badRow.id });
|
|
269
277
|
// userEntity is systemStream:true (schema/user.ts) — its event stream
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// hier; der Frist-Ablauf-Cleanup folgt mit S2.U5b.
|
|
11
11
|
|
|
12
12
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
13
14
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
14
15
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
15
16
|
import {
|
|
@@ -57,6 +58,7 @@ const features = [
|
|
|
57
58
|
createUserFeature(),
|
|
58
59
|
createDataRetentionFeature(),
|
|
59
60
|
createComplianceProfilesFeature(),
|
|
61
|
+
authFoundationFeature,
|
|
60
62
|
createSessionsFeature(),
|
|
61
63
|
|
|
62
64
|
createUserDataRightsFeature(),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Properties — dieser Test verifiziert sie end-to-end.
|
|
9
9
|
|
|
10
10
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
11
12
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
12
13
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
13
14
|
import {
|
|
@@ -64,6 +65,7 @@ beforeAll(async () => {
|
|
|
64
65
|
createUserFeature(),
|
|
65
66
|
createDataRetentionFeature(),
|
|
66
67
|
createComplianceProfilesFeature(),
|
|
68
|
+
authFoundationFeature,
|
|
67
69
|
createSessionsFeature(),
|
|
68
70
|
|
|
69
71
|
createUserDataRightsFeature({ sendDeletionRequestedEmail }),
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// hasJob=false wenn nichts da
|
|
19
19
|
|
|
20
20
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
21
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
21
22
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
22
23
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
23
24
|
import {
|
|
@@ -53,6 +54,7 @@ beforeAll(async () => {
|
|
|
53
54
|
createUserFeature(),
|
|
54
55
|
createDataRetentionFeature(),
|
|
55
56
|
createComplianceProfilesFeature(),
|
|
57
|
+
authFoundationFeature,
|
|
56
58
|
createSessionsFeature(),
|
|
57
59
|
|
|
58
60
|
createUserDataRightsFeature(),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
14
14
|
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
15
16
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
16
17
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
18
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -44,7 +45,7 @@ import { createSessionsFeature } from "../../sessions";
|
|
|
44
45
|
import { SessionHandlers } from "../../sessions/constants";
|
|
45
46
|
import { userSessionEntity, userSessionTable } from "../../sessions/schema/user-session";
|
|
46
47
|
import { createSessionCallbacks, type SessionCallbacks } from "../../sessions/session-callbacks";
|
|
47
|
-
import { sessionCallbacksFromLateBound } from "../../sessions/testing";
|
|
48
|
+
import { sessionCallbacksFromLateBound, withMintedSession } from "../../sessions/testing";
|
|
48
49
|
import { hashPassword } from "../../shared";
|
|
49
50
|
import { createTenantFeature, tenantMembershipsTable } from "../../tenant";
|
|
50
51
|
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
@@ -77,6 +78,7 @@ beforeAll(async () => {
|
|
|
77
78
|
createDataRetentionFeature(),
|
|
78
79
|
createComplianceProfilesFeature(),
|
|
79
80
|
createAuthEmailPasswordFeature(),
|
|
81
|
+
authFoundationFeature,
|
|
80
82
|
createSessionsFeature(),
|
|
81
83
|
createUserDataRightsFeature(),
|
|
82
84
|
],
|
|
@@ -115,10 +117,14 @@ async function seedAliceWithMembership(
|
|
|
115
117
|
status: string = USER_STATUS.Active,
|
|
116
118
|
): Promise<{ userId: string }> {
|
|
117
119
|
const hash = await hashPassword(ALICE_PW);
|
|
120
|
+
const actor = await withMintedSession(
|
|
121
|
+
(user, meta) => callbacks.get().sessionCreator(user, meta),
|
|
122
|
+
TestUsers.systemAdmin,
|
|
123
|
+
);
|
|
118
124
|
const created = await stack.http.writeOk<{ id: string }>(
|
|
119
125
|
UserHandlers.create,
|
|
120
126
|
{ email: ALICE_EMAIL, passwordHash: hash, displayName: "Alice" },
|
|
121
|
-
|
|
127
|
+
actor,
|
|
122
128
|
);
|
|
123
129
|
if (status !== USER_STATUS.Active) {
|
|
124
130
|
await updateRows(stack.db, userTable, { status }, { id: created.id });
|
|
@@ -136,6 +142,12 @@ async function login(email: string, password: string): Promise<{ status: number;
|
|
|
136
142
|
return { status: res.status, body: await res.json() };
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
// Any actor hitting a write handler needs a live sid once sessionChecker
|
|
146
|
+
// is wired (same gate as seedAliceWithMembership's bootstrap actor above).
|
|
147
|
+
function mintActor(user: { id: string; tenantId: TenantId; roles: readonly string[] }) {
|
|
148
|
+
return withMintedSession((u, meta) => callbacks.get().sessionCreator(u, meta), user);
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
function reasonOf(err: { details?: unknown }): string | undefined {
|
|
140
152
|
return (err.details as { reason?: string } | undefined)?.reason;
|
|
141
153
|
}
|
|
@@ -156,11 +168,11 @@ describe("S2.U6 :: restrict-account state-transitions", () => {
|
|
|
156
168
|
expect(sessionsBefore.every((s) => s.revokedAt === null)).toBe(true);
|
|
157
169
|
|
|
158
170
|
// Restrict-Account.
|
|
159
|
-
const aliceUser = {
|
|
171
|
+
const aliceUser = await mintActor({
|
|
160
172
|
id: userId,
|
|
161
173
|
tenantId: TENANT,
|
|
162
174
|
roles: ["Member"],
|
|
163
|
-
};
|
|
175
|
+
});
|
|
164
176
|
const result = await stack.http.writeOk<{ userId: string; status: string }>(
|
|
165
177
|
RESTRICT,
|
|
166
178
|
{},
|
|
@@ -182,17 +194,36 @@ describe("S2.U6 :: restrict-account state-transitions", () => {
|
|
|
182
194
|
expect(sessionsAfter.every((s) => s.revokedAt !== null)).toBe(true);
|
|
183
195
|
});
|
|
184
196
|
|
|
197
|
+
test("non-admin targets another user's account: 403 admin_required_for_other_user", async () => {
|
|
198
|
+
const { userId: targetId } = await seedAliceWithMembership();
|
|
199
|
+
const attacker = await mintActor({
|
|
200
|
+
id: crypto.randomUUID(),
|
|
201
|
+
tenantId: TENANT,
|
|
202
|
+
roles: ["Member"],
|
|
203
|
+
});
|
|
204
|
+
const err = await stack.http.writeErr(RESTRICT, { userId: targetId }, attacker);
|
|
205
|
+
expect(err.httpStatus).toBe(403);
|
|
206
|
+
expect(reasonOf(err)).toBe("admin_required_for_other_user");
|
|
207
|
+
|
|
208
|
+
const userRow = (await selectMany(stack.db, userTable, { id: targetId })) as Array<{
|
|
209
|
+
status: string;
|
|
210
|
+
}>;
|
|
211
|
+
expect(userRow[0]?.status).toBe(USER_STATUS.Active);
|
|
212
|
+
});
|
|
213
|
+
|
|
185
214
|
test("Restricted → Restricted (Idempotenz-Guard): 422 already_restricted", async () => {
|
|
186
215
|
const { userId } = await seedAliceWithMembership(USER_STATUS.Restricted);
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
// Alice's own session is blocked while Restricted — an admin targets
|
|
217
|
+
// her account instead, same as production would have to.
|
|
218
|
+
const admin = await mintActor(TestUsers.admin);
|
|
219
|
+
const err = await stack.http.writeErr(RESTRICT, { userId }, admin);
|
|
189
220
|
expect(err.httpStatus).toBe(422);
|
|
190
221
|
expect(reasonOf(err)).toBe("already_restricted");
|
|
191
222
|
});
|
|
192
223
|
|
|
193
224
|
test("DeletionRequested → restrict-account: 422 user_not_in_active_state", async () => {
|
|
194
225
|
const { userId } = await seedAliceWithMembership(USER_STATUS.DeletionRequested);
|
|
195
|
-
const aliceUser = { id: userId, tenantId: TENANT, roles: ["Member"] };
|
|
226
|
+
const aliceUser = await mintActor({ id: userId, tenantId: TENANT, roles: ["Member"] });
|
|
196
227
|
const err = await stack.http.writeErr(RESTRICT, {}, aliceUser);
|
|
197
228
|
expect(reasonOf(err)).toBe("user_not_in_active_state");
|
|
198
229
|
});
|
|
@@ -201,9 +232,11 @@ describe("S2.U6 :: restrict-account state-transitions", () => {
|
|
|
201
232
|
describe("S2.U6 :: lift-restriction state-transitions", () => {
|
|
202
233
|
test("Restricted → Active: status flippt zurueck", async () => {
|
|
203
234
|
const { userId } = await seedAliceWithMembership(USER_STATUS.Restricted);
|
|
204
|
-
|
|
235
|
+
// lift-restriction is operator-only (access.admin) — Alice's own
|
|
236
|
+
// session is blocked while Restricted anyway.
|
|
237
|
+
const admin = await mintActor(TestUsers.admin);
|
|
205
238
|
|
|
206
|
-
const result = await stack.http.writeOk<{ status: string }>(LIFT, {},
|
|
239
|
+
const result = await stack.http.writeOk<{ status: string }>(LIFT, { userId }, admin);
|
|
207
240
|
expect(result.status).toBe(USER_STATUS.Active);
|
|
208
241
|
|
|
209
242
|
const userRow = (await selectMany(stack.db, userTable, { id: userId })) as Array<{
|
|
@@ -214,15 +247,15 @@ describe("S2.U6 :: lift-restriction state-transitions", () => {
|
|
|
214
247
|
|
|
215
248
|
test("Active → lift-restriction: 422 not_restricted (Idempotenz-Guard)", async () => {
|
|
216
249
|
const { userId } = await seedAliceWithMembership(USER_STATUS.Active);
|
|
217
|
-
const
|
|
218
|
-
const err = await stack.http.writeErr(LIFT, {},
|
|
250
|
+
const admin = await mintActor(TestUsers.admin);
|
|
251
|
+
const err = await stack.http.writeErr(LIFT, { userId }, admin);
|
|
219
252
|
expect(reasonOf(err)).toBe("not_restricted");
|
|
220
253
|
});
|
|
221
254
|
|
|
222
255
|
test("DeletionRequested → lift-restriction: 422 not_restricted", async () => {
|
|
223
256
|
const { userId } = await seedAliceWithMembership(USER_STATUS.DeletionRequested);
|
|
224
|
-
const
|
|
225
|
-
const err = await stack.http.writeErr(LIFT, {},
|
|
257
|
+
const admin = await mintActor(TestUsers.admin);
|
|
258
|
+
const err = await stack.http.writeErr(LIFT, { userId }, admin);
|
|
226
259
|
expect(reasonOf(err)).toBe("not_restricted");
|
|
227
260
|
});
|
|
228
261
|
});
|
|
@@ -286,11 +319,11 @@ describe("S2.U6 :: Cross-Feature sessions.revokeAllForUser direct", () => {
|
|
|
286
319
|
expect(liveBefore.length).toBe(2);
|
|
287
320
|
|
|
288
321
|
// System-Caller.
|
|
289
|
-
const systemUser = {
|
|
322
|
+
const systemUser = await mintActor({
|
|
290
323
|
id: "00000000-0000-4000-8000-000000000000",
|
|
291
324
|
tenantId: TENANT,
|
|
292
325
|
roles: ["SystemAdmin"],
|
|
293
|
-
};
|
|
326
|
+
});
|
|
294
327
|
const result = await stack.http.writeOk<{ count: number; userId: string }>(
|
|
295
328
|
SessionHandlers.revokeAllForUser,
|
|
296
329
|
{ userId },
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// jeden Export auf "failed" setzte).
|
|
11
11
|
|
|
12
12
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
13
14
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
14
15
|
import { type JobContext, SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
16
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -65,6 +66,7 @@ beforeAll(async () => {
|
|
|
65
66
|
fileProviderInMemoryFeature,
|
|
66
67
|
mailFoundationFeature,
|
|
67
68
|
mailTransportInMemoryFeature,
|
|
69
|
+
authFoundationFeature,
|
|
68
70
|
createSessionsFeature(),
|
|
69
71
|
// appExportDownloadUrl set → the default export-ready mail is enabled, so
|
|
70
72
|
// this also proves the export cron's mail bridge end-to-end (C6).
|
|
@@ -17,6 +17,7 @@ import { spawn } from "node:child_process";
|
|
|
17
17
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
20
21
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
21
22
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
22
23
|
import {
|
|
@@ -59,6 +60,7 @@ beforeAll(async () => {
|
|
|
59
60
|
createUserFeature(),
|
|
60
61
|
createDataRetentionFeature(),
|
|
61
62
|
createComplianceProfilesFeature(),
|
|
63
|
+
authFoundationFeature,
|
|
62
64
|
createSessionsFeature(),
|
|
63
65
|
|
|
64
66
|
createUserDataRightsFeature(),
|
|
@@ -803,6 +805,7 @@ describe("runExportJobs :: Atom 3c file-binaries", () => {
|
|
|
803
805
|
createUserFeature(),
|
|
804
806
|
createDataRetentionFeature(),
|
|
805
807
|
createComplianceProfilesFeature(),
|
|
808
|
+
authFoundationFeature,
|
|
806
809
|
createSessionsFeature(),
|
|
807
810
|
|
|
808
811
|
createUserDataRightsFeature(),
|
|
@@ -1063,7 +1066,7 @@ describe("runExportJobs :: Atom 5 notification-callbacks", () => {
|
|
|
1063
1066
|
expect(sent.userEmail).toBe("alice@example.com");
|
|
1064
1067
|
expect(sent.jobId).toBe(jobId);
|
|
1065
1068
|
expect(sent.downloadUrl).toMatch(
|
|
1066
|
-
/^https:\/\/app\.example\.com\/user-export\/by-token
|
|
1069
|
+
/^https:\/\/app\.example\.com\/user-export\/by-token#token=[A-Za-z0-9_%-]+$/,
|
|
1067
1070
|
);
|
|
1068
1071
|
// plain-token aus dem callback-arg entspricht dem result.tokenByJobId
|
|
1069
1072
|
const plainFromResult = result.tokenByJobId.get(jobId);
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// der end-to-end-Default-Pfad (delete).
|
|
17
17
|
|
|
18
18
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
19
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
19
20
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
20
21
|
import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
21
22
|
import type { JobContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -67,6 +68,7 @@ beforeAll(async () => {
|
|
|
67
68
|
createFilesFeature(),
|
|
68
69
|
createDataRetentionFeature(),
|
|
69
70
|
createComplianceProfilesFeature(),
|
|
71
|
+
authFoundationFeature,
|
|
70
72
|
createSessionsFeature(),
|
|
71
73
|
|
|
72
74
|
createUserDataRightsFeature(),
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// ueber Pseudo-Tenant.
|
|
16
16
|
|
|
17
17
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
18
19
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
20
|
import {
|
|
20
21
|
configurePiiSubjectKms,
|
|
@@ -61,6 +62,7 @@ beforeAll(async () => {
|
|
|
61
62
|
createFilesFeature(),
|
|
62
63
|
createDataRetentionFeature(),
|
|
63
64
|
createComplianceProfilesFeature(),
|
|
65
|
+
authFoundationFeature,
|
|
64
66
|
createSessionsFeature(),
|
|
65
67
|
|
|
66
68
|
createUserDataRightsFeature(),
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// value (the failure mode that shipped the ctx.config export bug).
|
|
14
14
|
|
|
15
15
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
16
17
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
18
|
import {
|
|
18
19
|
createEntity,
|
|
@@ -82,6 +83,7 @@ beforeAll(async () => {
|
|
|
82
83
|
stack = await setupTestStack({
|
|
83
84
|
features: [
|
|
84
85
|
createUserFeature(),
|
|
86
|
+
authFoundationFeature,
|
|
85
87
|
createSessionsFeature(),
|
|
86
88
|
createDataRetentionFeature(),
|
|
87
89
|
createComplianceProfilesFeature(),
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Sprint-2-H1/H2-Hooks) kommt in S2.T1 (Cross-Data-Matrix).
|
|
8
8
|
|
|
9
9
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
10
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
10
11
|
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
11
12
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
12
13
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
@@ -24,7 +25,14 @@ const userDataRights = createUserDataRightsFeature();
|
|
|
24
25
|
|
|
25
26
|
beforeAll(async () => {
|
|
26
27
|
stack = await setupTestStack({
|
|
27
|
-
features: [
|
|
28
|
+
features: [
|
|
29
|
+
authFoundationFeature,
|
|
30
|
+
userFeature,
|
|
31
|
+
sessionsFeature,
|
|
32
|
+
dataRetention,
|
|
33
|
+
complianceProfiles,
|
|
34
|
+
userDataRights,
|
|
35
|
+
],
|
|
28
36
|
});
|
|
29
37
|
});
|
|
30
38
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EntityDefinition,
|
|
3
|
+
EXT_USER_DATA,
|
|
4
|
+
type FeatureDefinition,
|
|
5
|
+
type PiiAnnotations,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
|
|
8
|
+
// r.entity(...) is not the only way a feature exposes an entity shape:
|
|
9
|
+
// r.projection(...) can carry an optional `entity` too (raw read-models with
|
|
10
|
+
// no executor, e.g. billing-foundation's subscription table). V3 below needs
|
|
11
|
+
// every entity a feature declares, not just the r.entity ones, or a pii/
|
|
12
|
+
// userOwned field on a projection-only entity is invisible to the guard it
|
|
13
|
+
// was annotated for.
|
|
14
|
+
function entitiesOf(
|
|
15
|
+
feature: FeatureDefinition,
|
|
16
|
+
): ReadonlyArray<readonly [string, EntityDefinition]> {
|
|
17
|
+
const fromEntities = Object.entries(feature.entities ?? {});
|
|
18
|
+
const fromProjections = Object.values(feature.projections ?? {})
|
|
19
|
+
.filter((p): p is typeof p & { entity: EntityDefinition } => p.entity !== undefined)
|
|
20
|
+
.map((p) => [p.name, p.entity] as const);
|
|
21
|
+
return [...fromEntities, ...fromProjections];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// V2: export-without-erase gate. A feature that registers an EXT_USER_DATA
|
|
25
|
+
// export hook without a matching delete hook exports data under Art.20 but
|
|
26
|
+
// never erases it on forget — an Art.17 violation. Hard boot failure: no app
|
|
27
|
+
// should ship a GDPR export path with no erase path. Registry-level signal
|
|
28
|
+
// only; runtime no-ops (a delete hook that silently skips) are not detectable
|
|
29
|
+
// here — those are covered by the export/forget integration tests.
|
|
30
|
+
//
|
|
31
|
+
// Registered as an `r.bootCheck()` by this feature (#1314) — EXT_USER_DATA is
|
|
32
|
+
// owned by user-data-rights (r.extendsRegistrar(EXT_USER_DATA, {})), so any
|
|
33
|
+
// r.useExtension(EXT_USER_DATA, ...) usage across the app already requires
|
|
34
|
+
// user-data-rights to be mounted (boot-validator's extension-provider check
|
|
35
|
+
// fails otherwise); running this from user-data-rights' own bootCheck is
|
|
36
|
+
// equivalent to the framework-internal call it replaces.
|
|
37
|
+
export function validateGdprHookCompleteness(features: readonly FeatureDefinition[]): void {
|
|
38
|
+
for (const feature of features) {
|
|
39
|
+
for (const usage of feature.extensionUsages) {
|
|
40
|
+
if (usage.extensionName !== EXT_USER_DATA) continue;
|
|
41
|
+
const hasExport = typeof usage.options?.["export"] === "function";
|
|
42
|
+
const hasDelete = typeof usage.options?.["delete"] === "function";
|
|
43
|
+
if (hasExport && !hasDelete) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 violation). Add a delete hook. If erasure is intentionally handled elsewhere (e.g. crypto-shredding key-erase, parent cascade), register a no-op delete: async () => {} with a comment explaining why.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// V3: PII-entity-without-hook gate. V2 checks registered hooks for
|
|
53
|
+
// completeness; V3 catches the entity nobody registered at all — fields
|
|
54
|
+
// annotated as user-subject data (pii / userOwned) yet invisible to the
|
|
55
|
+
// Art.15/20 export and Art.17 forget pipeline. Hard boot failure: a
|
|
56
|
+
// subject-data entity that skips the pipeline is exactly the "feature built
|
|
57
|
+
// past GDPR" leak this gate exists to stop. No "is user-data-rights mounted"
|
|
58
|
+
// guard needed here (unlike the framework-internal predecessor) — this runs
|
|
59
|
+
// as user-data-rights' own bootCheck, so it's only reachable when mounted.
|
|
60
|
+
// Matching is by entity name across all features (usage.entityName is
|
|
61
|
+
// unqualified); a same-named entity in another feature can mask a gap —
|
|
62
|
+
// accepted, as the common case is a distinct entity name.
|
|
63
|
+
export function validateGdprPiiHookCoverage(features: readonly FeatureDefinition[]): void {
|
|
64
|
+
const hookedEntities = new Set<string>();
|
|
65
|
+
for (const f of features) {
|
|
66
|
+
for (const usage of f.extensionUsages) {
|
|
67
|
+
if (usage.extensionName === EXT_USER_DATA) hookedEntities.add(usage.entityName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const feature of features) {
|
|
72
|
+
for (const [entityName, entity] of entitiesOf(feature)) {
|
|
73
|
+
if (hookedEntities.has(entityName)) continue;
|
|
74
|
+
const subjectFields = Object.entries(entity.fields)
|
|
75
|
+
.filter(([, field]) => {
|
|
76
|
+
const annot = field as PiiAnnotations; // @cast-boundary schema-walk
|
|
77
|
+
return Boolean(annot.pii) || Boolean(annot.userOwned);
|
|
78
|
+
})
|
|
79
|
+
.map(([name]) => name);
|
|
80
|
+
if (subjectFields.length === 0) continue;
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[kumiko:boot] Entity "${entityName}" (feature "${feature.name}") has user-subject fields (${subjectFields.join(", ")}) but no feature registers an EXT_USER_DATA hook for it — the data never appears in Art.15/20 exports and is never erased on forget (Art.17 gap). Register r.useExtension(EXT_USER_DATA, "${entityName}", { export, delete }) in the owning feature or a defaults feature. If this entity is intentionally out of the pipeline (e.g. crypto-shredding key-erase covers it), register a no-op hook { export: async () => null, delete: async () => {} } with a comment explaining why.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|