@cosmicdrift/kumiko-bundled-features 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/package.json +12 -6
- package/src/auth-email-password/auth-user-row.ts +6 -0
- package/src/auth-email-password/constants.ts +11 -0
- package/src/auth-email-password/handlers/login.write.ts +31 -1
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/compliance-profiles/README.md +88 -0
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +308 -0
- package/src/compliance-profiles/__tests__/seeding.integration.ts +93 -0
- package/src/compliance-profiles/feature.ts +51 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +63 -0
- package/src/compliance-profiles/handlers/list-profiles.query.ts +44 -0
- package/src/compliance-profiles/handlers/needs-profile.query.ts +56 -0
- package/src/compliance-profiles/handlers/set-profile.write.ts +146 -0
- package/src/compliance-profiles/handlers/sub-processors.query.ts +43 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/resolve-for-tenant.ts +61 -0
- package/src/compliance-profiles/schema/profile-selection.ts +52 -0
- package/src/compliance-profiles/seeding.ts +96 -0
- package/src/data-retention/__tests__/data-retention.integration.ts +49 -0
- package/src/data-retention/__tests__/keep-for.test.ts +77 -0
- package/src/data-retention/__tests__/override-schema.test.ts +96 -0
- package/src/data-retention/__tests__/policy-for.integration.ts +172 -0
- package/src/data-retention/__tests__/resolver.test.ts +201 -0
- package/src/data-retention/_internal/parse-override.ts +33 -0
- package/src/data-retention/feature.ts +57 -0
- package/src/data-retention/handlers/policy-for.query.ts +57 -0
- package/src/data-retention/index.ts +18 -0
- package/src/data-retention/keep-for.ts +75 -0
- package/src/data-retention/override-schema.ts +37 -0
- package/src/data-retention/presets.ts +72 -0
- package/src/data-retention/resolve-for-tenant.ts +50 -0
- package/src/data-retention/resolver.ts +107 -0
- package/src/data-retention/schema/tenant-retention-override.ts +47 -0
- package/src/file-foundation/feature.ts +43 -3
- package/src/file-foundation/index.ts +1 -0
- package/src/file-provider-inmemory/feature.ts +6 -3
- package/src/file-provider-s3/feature.ts +8 -10
- package/src/files/README.md +50 -0
- package/src/files/__tests__/files.integration.ts +157 -0
- package/src/files/feature.ts +34 -0
- package/src/files/index.ts +1 -0
- package/src/files/schema/file-ref.ts +58 -0
- package/src/files-provider-s3/s3-provider.ts +89 -0
- package/src/secrets/__tests__/require-secrets-context.test.ts +81 -0
- package/src/secrets/feature.ts +10 -6
- package/src/sessions/constants.ts +4 -0
- package/src/sessions/feature.ts +3 -0
- package/src/sessions/handlers/revoke-all-for-user.write.ts +42 -0
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +118 -0
- package/src/tier-engine/feature.ts +16 -6
- package/src/user/__tests__/user-status.test.ts +39 -0
- package/src/user/index.ts +11 -1
- package/src/user/schema/user.ts +76 -0
- package/src/user-data-rights/COMPLIANCE.md +182 -0
- package/src/user-data-rights/README.md +109 -0
- package/src/user-data-rights/__tests__/audit-log.integration.ts +199 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +349 -0
- package/src/user-data-rights/__tests__/download.integration.ts +565 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +244 -0
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +163 -0
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +30 -0
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +370 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +179 -0
- package/src/user-data-rights/__tests__/request-export.integration.ts +269 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +309 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +1124 -0
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +703 -0
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +291 -0
- package/src/user-data-rights/__tests__/token-helpers.test.ts +63 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +57 -0
- package/src/user-data-rights/__tests__/zip-path.test.ts +119 -0
- package/src/user-data-rights/audit-download.ts +125 -0
- package/src/user-data-rights/feature.ts +309 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +84 -0
- package/src/user-data-rights/handlers/download-by-job.query.ts +209 -0
- package/src/user-data-rights/handlers/download-by-token.query.ts +257 -0
- package/src/user-data-rights/handlers/export-status.query.ts +76 -0
- package/src/user-data-rights/handlers/lift-restriction.write.ts +68 -0
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +53 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +63 -0
- package/src/user-data-rights/handlers/request-deletion.write.ts +123 -0
- package/src/user-data-rights/handlers/request-export.write.ts +155 -0
- package/src/user-data-rights/handlers/restrict-account.write.ts +81 -0
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +61 -0
- package/src/user-data-rights/i18n.ts +37 -0
- package/src/user-data-rights/index.ts +19 -0
- package/src/user-data-rights/run-export-jobs.ts +878 -0
- package/src/user-data-rights/run-forget-cleanup.ts +334 -0
- package/src/user-data-rights/run-user-export.ts +211 -0
- package/src/user-data-rights/schema/download-attempt.ts +37 -0
- package/src/user-data-rights/schema/download-token.ts +111 -0
- package/src/user-data-rights/schema/export-job.ts +166 -0
- package/src/user-data-rights/token-helpers.ts +67 -0
- package/src/user-data-rights/zip-path.ts +94 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +337 -0
- package/src/user-data-rights-defaults/feature.ts +40 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +109 -0
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +91 -0
- package/src/user-data-rights-defaults/index.ts +6 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// auto-default-tier hook regression test — beweist dass beim
|
|
2
|
+
// `tenant:write:create` der postSave-Hook von createTierEngineFeature
|
|
3
|
+
// fired und automatisch ein tier-assignment-row mit `defaultTier`
|
|
4
|
+
// schreibt.
|
|
5
|
+
//
|
|
6
|
+
// **Warum dieser Test existiert (2026-05-10):**
|
|
7
|
+
// Der auto-default-tier-Hook wurde in Sprint 8a Phase 2 hinzugefügt aber
|
|
8
|
+
// nie direkt getestet. Bei Sprint 8c (Studio-Mount mit auto-default-
|
|
9
|
+
// compliance-companion-hook) flog der Bug auf: `ctx.db as DbConnection`
|
|
10
|
+
// war ein Type-Lie — TenantDb exposed select/insert/update/delete, NICHT
|
|
11
|
+
// execute(). Der event-store-append (event-store.ts:102) ruft
|
|
12
|
+
// `db.execute(sql\`SELECT pg_notify(...)\`)` → TypeError. Fix:
|
|
13
|
+
// `ctx.db.raw as DbConnection` (Pattern aus signup-confirm.write.ts:107).
|
|
14
|
+
//
|
|
15
|
+
// Pin-Verträge:
|
|
16
|
+
// 1. tenant:write:create fired postSave-Hook mit isNew=true
|
|
17
|
+
// 2. Hook erstellt tier-assignment-row im NEUEN tenant (nicht im caller-
|
|
18
|
+
// tenant — Memory `feedback_event_store_tenant_consistency`)
|
|
19
|
+
// 3. Idempotency: tenant-update fired keinen weiteren row
|
|
20
|
+
|
|
21
|
+
import { composeFeatures } from "@cosmicdrift/kumiko-dev-server/compose-features";
|
|
22
|
+
import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
|
|
23
|
+
import {
|
|
24
|
+
createTestUser,
|
|
25
|
+
setupTestStack,
|
|
26
|
+
type TestStack,
|
|
27
|
+
unsafePushTables,
|
|
28
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
29
|
+
import { eq } from "drizzle-orm";
|
|
30
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
31
|
+
import { configValuesTable } from "../../config";
|
|
32
|
+
import { TenantHandlers, tenantMembershipsTable, tenantTable } from "../../tenant";
|
|
33
|
+
import { userTable } from "../../user";
|
|
34
|
+
import type { TierMap } from "../compose-app";
|
|
35
|
+
import { tierAssignmentEntity } from "../entity";
|
|
36
|
+
import { createTierEngineFeature } from "../feature";
|
|
37
|
+
|
|
38
|
+
const TEST_TIER_MAP: TierMap<{ readonly maxItems: number }> = {
|
|
39
|
+
free: { features: [], caps: { maxItems: 1 } },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const tierAssignmentTable = buildDrizzleTable("tier-assignment", tierAssignmentEntity);
|
|
43
|
+
|
|
44
|
+
const features = composeFeatures(
|
|
45
|
+
[createTierEngineFeature({ defaultTier: "free", tierMap: TEST_TIER_MAP })],
|
|
46
|
+
{ includeBundled: true },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
let stack: TestStack;
|
|
50
|
+
const PLATFORM_TENANT = "00000000-0000-4000-8000-000000000001";
|
|
51
|
+
const sysadmin = createTestUser({
|
|
52
|
+
id: "platform-sysadmin",
|
|
53
|
+
tenantId: PLATFORM_TENANT,
|
|
54
|
+
roles: ["SystemAdmin"],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeAll(async () => {
|
|
58
|
+
stack = await setupTestStack({ features });
|
|
59
|
+
await unsafePushTables(stack.db, {
|
|
60
|
+
config_values: configValuesTable,
|
|
61
|
+
users: userTable,
|
|
62
|
+
tenants: tenantTable,
|
|
63
|
+
tenant_memberships: tenantMembershipsTable,
|
|
64
|
+
tier_assignments: tierAssignmentTable,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => stack?.cleanup());
|
|
69
|
+
|
|
70
|
+
describe("auto-default-tier postSave hook on tenant-create", () => {
|
|
71
|
+
test("sysadmin creates a new tenant → free tier-assignment-row angelegt", async () => {
|
|
72
|
+
const data = (await stack.http.writeOk<Record<string, unknown>>(
|
|
73
|
+
TenantHandlers.create,
|
|
74
|
+
{ key: "test-tenant-1", name: "Test Tenant One" },
|
|
75
|
+
sysadmin,
|
|
76
|
+
))!;
|
|
77
|
+
const newTenantId = data["id"] as string;
|
|
78
|
+
expect(typeof newTenantId).toBe("string");
|
|
79
|
+
|
|
80
|
+
const rows = await stack.db
|
|
81
|
+
.select()
|
|
82
|
+
.from(tierAssignmentTable)
|
|
83
|
+
.where(eq(tierAssignmentTable["tenantId"], newTenantId));
|
|
84
|
+
expect(rows.length).toBe(1);
|
|
85
|
+
expect((rows[0] as Record<string, unknown>)["tier"]).toBe("free");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("idempotency: tenant-update fired keinen weiteren tier-assignment-row", async () => {
|
|
89
|
+
const created = (await stack.http.writeOk<Record<string, unknown>>(
|
|
90
|
+
TenantHandlers.create,
|
|
91
|
+
{ key: "test-tenant-2", name: "Test Tenant Two" },
|
|
92
|
+
sysadmin,
|
|
93
|
+
))!;
|
|
94
|
+
const tenantId = created["id"] as string;
|
|
95
|
+
|
|
96
|
+
const existing = (await stack.db
|
|
97
|
+
.select()
|
|
98
|
+
.from(tenantTable)
|
|
99
|
+
.where(eq(tenantTable["id"], tenantId))) as Array<{ id: string; version: number }>;
|
|
100
|
+
const currentVersion = existing[0]!.version;
|
|
101
|
+
|
|
102
|
+
await stack.http.writeOk(
|
|
103
|
+
TenantHandlers.update,
|
|
104
|
+
{
|
|
105
|
+
id: tenantId,
|
|
106
|
+
version: currentVersion,
|
|
107
|
+
changes: { name: "Test Tenant Two — Renamed" },
|
|
108
|
+
},
|
|
109
|
+
sysadmin,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const rows = await stack.db
|
|
113
|
+
.select()
|
|
114
|
+
.from(tierAssignmentTable)
|
|
115
|
+
.where(eq(tierAssignmentTable["tenantId"], tenantId));
|
|
116
|
+
expect(rows.length).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -253,12 +253,22 @@ export function createTierEngineFeature<
|
|
|
253
253
|
if (!ctx.db) return;
|
|
254
254
|
|
|
255
255
|
// ctx.db ist im inTransaction-phase eine TenantDb (tenant-scoped
|
|
256
|
-
// proxy auf die echte TX). Für event-store-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
256
|
+
// proxy auf die echte TX). Für event-store-Pfade brauchen wir
|
|
257
|
+
// die rohe DbConnection — TenantDb exposes nur select/insert/
|
|
258
|
+
// update/delete, NICHT execute (event-store-append.ts:102 ruft
|
|
259
|
+
// db.execute(sql`SELECT pg_notify(...)`) → TypeError sonst).
|
|
260
|
+
// Pattern matched signup-confirm.write.ts:107 (.raw), nicht
|
|
261
|
+
// `as DbConnection` — das ist Type-Lie der erst beim ersten
|
|
262
|
+
// .execute()-Call crashed.
|
|
263
|
+
//
|
|
264
|
+
// AppContext.db ist union (DbConnection | TenantDb). Im
|
|
265
|
+
// inTransaction-phase garantiert TenantDb — der dispatcher
|
|
266
|
+
// wrapped vorher (siehe pipeline/dispatcher.ts createTenantDb-
|
|
267
|
+
// Aufruf). TypeGuard via `"raw" in ...` ist robuster als
|
|
268
|
+
// `as TenantDb` gegen future refactor.
|
|
269
|
+
// skip: defensive — sollte im inTransaction nie greifen.
|
|
270
|
+
if (!("raw" in ctx.db)) return;
|
|
271
|
+
const rawDb = ctx.db.raw as DbConnection;
|
|
262
272
|
|
|
263
273
|
// Idempotency: stream-existence-check vor create. Pattern aus
|
|
264
274
|
// seedTenant.ts. Bei re-replay (rebuild) nicht versionsbumpen.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Drift-Guard fuer USER_STATUS (S2.D2.5 N1).
|
|
2
|
+
//
|
|
3
|
+
// Plus: USER_STATUS_OPTIONS wird nicht direkt exportiert (private im
|
|
4
|
+
// schema/user.ts), wird aber via createSelectField in der Entity
|
|
5
|
+
// referenziert. Wenn USER_STATUS-Object erweitert wird ohne dass das
|
|
6
|
+
// Tuple synchron mitwaechst, faengt der Test es ab — entity.fields.status
|
|
7
|
+
// liefert die options-Liste.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import { USER_STATUS, userEntity } from "../schema/user";
|
|
11
|
+
|
|
12
|
+
describe("USER_STATUS — Drift-Guard (S2.D2.5 N1)", () => {
|
|
13
|
+
test("Snapshot-Vergleich: USER_STATUS-Object und entity.fields.status.options synchron", () => {
|
|
14
|
+
const objectValues = Object.values(USER_STATUS).sort();
|
|
15
|
+
// entity.fields.status ist createSelectField — options ist die Tuple
|
|
16
|
+
const statusField = userEntity.fields["status"] as { options: readonly string[] };
|
|
17
|
+
const optionValues = [...statusField.options].sort();
|
|
18
|
+
|
|
19
|
+
expect(optionValues).toEqual(objectValues);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("USER_STATUS-Snapshot — explizit zu updaten bei Aenderungen", () => {
|
|
23
|
+
expect(USER_STATUS).toMatchInlineSnapshot(`
|
|
24
|
+
{
|
|
25
|
+
"Active": "active",
|
|
26
|
+
"Deleted": "deleted",
|
|
27
|
+
"DeletionRequested": "deletionRequested",
|
|
28
|
+
"Restricted": "restricted",
|
|
29
|
+
}
|
|
30
|
+
`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("USER_STATUS-Werte sind camelCase (Convention fuer status-Strings)", () => {
|
|
34
|
+
// Erwartung: alle Werte starten mit lowercase und enthalten kein Leerzeichen
|
|
35
|
+
for (const value of Object.values(USER_STATUS)) {
|
|
36
|
+
expect(value).toMatch(/^[a-z][a-zA-Z]*$/);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/user/index.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export { UserCommandSchemas } from "./command-schemas";
|
|
2
2
|
export { USER_FEATURE, UserErrors, UserHandlers, UserQueries } from "./constants";
|
|
3
3
|
export { createUserFeature } from "./feature";
|
|
4
|
-
export {
|
|
4
|
+
export type { UserStatus } from "./schema/user";
|
|
5
|
+
export {
|
|
6
|
+
USER_ANONYMIZED_DISPLAY_NAME,
|
|
7
|
+
USER_ANONYMIZED_EMAIL_DOMAIN,
|
|
8
|
+
USER_ANONYMIZED_EMAIL_PREFIX,
|
|
9
|
+
USER_DELETED_DISPLAY_NAME,
|
|
10
|
+
USER_DELETED_EMAIL_PREFIX,
|
|
11
|
+
USER_STATUS,
|
|
12
|
+
userEntity,
|
|
13
|
+
userTable,
|
|
14
|
+
} from "./schema/user";
|
package/src/user/schema/user.ts
CHANGED
|
@@ -3,9 +3,54 @@ import {
|
|
|
3
3
|
access,
|
|
4
4
|
createBooleanField,
|
|
5
5
|
createEntity,
|
|
6
|
+
createSelectField,
|
|
6
7
|
createTextField,
|
|
8
|
+
createTimestampField,
|
|
7
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* User-Lifecycle-Status (S2.U1). Single source of truth — Auth-Middleware
|
|
13
|
+
* (S2.U6), Forget-Job (S2.U5) und Restriction-Handler nutzen diese
|
|
14
|
+
* Constants statt Magic-Strings (Memory feedback_role_naming_drift —
|
|
15
|
+
* gleiches Pattern wie ROLES.SystemAdmin).
|
|
16
|
+
*/
|
|
17
|
+
export const USER_STATUS = {
|
|
18
|
+
Active: "active",
|
|
19
|
+
Restricted: "restricted",
|
|
20
|
+
DeletionRequested: "deletionRequested",
|
|
21
|
+
Deleted: "deleted",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type UserStatus = (typeof USER_STATUS)[keyof typeof USER_STATUS];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Anonymize-Display-Strings fuer userDeleteHook (S2.H1). Constants statt
|
|
28
|
+
* Magic-Strings damit i18n-Mapping moeglich + drift-fest. Default-DE,
|
|
29
|
+
* App-Author kann via i18n-System uebersetzen wenn gewuenscht.
|
|
30
|
+
*/
|
|
31
|
+
export const USER_DELETED_DISPLAY_NAME = "[Geloescht]";
|
|
32
|
+
export const USER_ANONYMIZED_DISPLAY_NAME = "[Anonymisiert]";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Email-Pseudonyme nach Forget. `<prefix>-<userId>@anonymized.invalid`
|
|
36
|
+
* — der userId-Suffix ist als Pseudo-Audit-Marker fuer Operator
|
|
37
|
+
* (Tracing-fall) erlaubt; user-id selbst ist UUID, kein PII.
|
|
38
|
+
* `.invalid`-TLD ist RFC2606-reserviert — niemals deliverbare Email.
|
|
39
|
+
*/
|
|
40
|
+
export const USER_DELETED_EMAIL_PREFIX = "deleted";
|
|
41
|
+
export const USER_ANONYMIZED_EMAIL_PREFIX = "anonymized";
|
|
42
|
+
export const USER_ANONYMIZED_EMAIL_DOMAIN = "anonymized.invalid";
|
|
43
|
+
|
|
44
|
+
// Tuple form fuer createSelectField (erfordert non-empty readonly tuple).
|
|
45
|
+
// Object.values(USER_STATUS) waere string[] — statisches Tuple ist
|
|
46
|
+
// type-sicher.
|
|
47
|
+
const USER_STATUS_OPTIONS = [
|
|
48
|
+
USER_STATUS.Active,
|
|
49
|
+
USER_STATUS.Restricted,
|
|
50
|
+
USER_STATUS.DeletionRequested,
|
|
51
|
+
USER_STATUS.Deleted,
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
9
54
|
// User entity — tenant-agnostic. A single user can belong to multiple tenants
|
|
10
55
|
// via tenantMemberships. No tenantId column on this table.
|
|
11
56
|
export const userEntity = createEntity({
|
|
@@ -63,6 +108,37 @@ export const userEntity = createEntity({
|
|
|
63
108
|
default: "[]",
|
|
64
109
|
access: { write: access.privileged },
|
|
65
110
|
}),
|
|
111
|
+
|
|
112
|
+
// S2.U1: User-Lifecycle-Status für user-data-rights (Sprint 2).
|
|
113
|
+
// - "active": Normaler State, alle Operationen erlaubt
|
|
114
|
+
// - "restricted": Art. 18 Restriction — Auth-Middleware blockiert
|
|
115
|
+
// Schreib-Endpoints, Read bleibt erlaubt damit
|
|
116
|
+
// User das Banner sieht + lift-restriction klicken kann
|
|
117
|
+
// - "deletionRequested": delete-account aufgerufen, gracePeriodEnd
|
|
118
|
+
// gesetzt, User kann via cancel-deletion zurueck
|
|
119
|
+
// auf "active". Auth-Middleware blockiert wie
|
|
120
|
+
// "restricted".
|
|
121
|
+
// - "deleted": Forget executed nach Grace, Row anonymisiert via
|
|
122
|
+
// softDelete. Auth-Middleware blockt Login.
|
|
123
|
+
//
|
|
124
|
+
// Schreibrecht privileged: nur die request-deletion / restrict / lift /
|
|
125
|
+
// execute-forget-Handler (alle SYSTEM-context) duerfen status flippen.
|
|
126
|
+
status: createSelectField({
|
|
127
|
+
required: true,
|
|
128
|
+
default: USER_STATUS.Active,
|
|
129
|
+
options: USER_STATUS_OPTIONS,
|
|
130
|
+
access: { write: access.privileged },
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
// Wann darf der pending-Forget tatsaechlich ausgefuehrt werden?
|
|
134
|
+
// Cron-Job in user-data-rights checkt taeglich gracePeriodEnd < now()
|
|
135
|
+
// und triggert dann die EXT_USER_DATA-Hooks. NULL solange kein
|
|
136
|
+
// Forget pending — wird beim delete-account-Call gesetzt
|
|
137
|
+
// (= now() + Compliance-Profile.userRights.gracePeriod), beim
|
|
138
|
+
// cancel-deletion zurueckgesetzt.
|
|
139
|
+
gracePeriodEnd: createTimestampField({
|
|
140
|
+
access: { write: access.privileged },
|
|
141
|
+
}),
|
|
66
142
|
},
|
|
67
143
|
});
|
|
68
144
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# DSGVO Compliance — Operator-Guide
|
|
2
|
+
|
|
3
|
+
Dieses Dokument bündelt die **technischen Fakten** zur DSGVO-Pipeline
|
|
4
|
+
(Art. 15/17/18/20) als Grundlage für:
|
|
5
|
+
|
|
6
|
+
- **Verarbeitungsverzeichnis** (Art. 30) — was wird wo wie lange gespeichert
|
|
7
|
+
- **Datenschutzerklärung** — welche Endpoints decken welchen Artikel ab
|
|
8
|
+
- **AVV mit Sub-Processors** — welche TOM sind eingebaut
|
|
9
|
+
- **Operator-Runbook** — was triggert wann, wer kann was sehen
|
|
10
|
+
|
|
11
|
+
> Juristische Texte (AVV-Wortlaut, Datenschutzerklärung-Bausteine,
|
|
12
|
+
> Verarbeitungsverzeichnis-Strukturierung) gehören zu Marc + Anwalt —
|
|
13
|
+
> dieses Dokument liefert nur die technischen Fakten dafür.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Endpoint → Artikel-Mapping
|
|
18
|
+
|
|
19
|
+
| Artikel | Endpoint / Runner | Wer ruft auf | Was passiert |
|
|
20
|
+
|---------|-------------------|--------------|--------------|
|
|
21
|
+
| **Art. 15 (Auskunft, light)** | `user-data-rights:query:my-audit-log` | User | Liest eigene Framework-Events aus `kumiko_events` (account-weit über alle Memberships). Filterbar nach `eventType`, `aggregateType`, `from`/`to`. Domain-Entities ohne `ctx.appendEvent` erscheinen NICHT — die kommen im Export-Bundle (Art. 20). |
|
|
22
|
+
| **Art. 15 + 20 (Auskunft + Portabilität)** | `user-data-rights:write:request-export` | User | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten (cross-tenant) + signed Magic-Link per Email. Idempotent: nur 1 active Job pro User (`ACTIVE_JOB_CONSTRAINT`). |
|
|
23
|
+
| **Art. 15 + 20** | `GET /user-export/by-token?token=…` | Anonym (Magic-Link-Pfad) | Token-Hash-Lookup → 302-Redirect auf signed Storage-URL. Multi-use innerhalb TTL. Audit: `lastUsedAt`, `lastUsedFromIp`, `lastUsedUserAgent`. |
|
|
24
|
+
| **Art. 15 + 20** | `GET /user-export/by-job/:jobId` | User (Session-Auth) | UI-Klick-Pfad. Cross-tenant-same-user: User in Tenant B kann Job aus Tenant A laden wenn er der Owner ist. |
|
|
25
|
+
| **Art. 17 (Löschung)** | `user-data-rights:write:request-deletion` | User | Soft-Delete: `status=DeletionRequested` + `gracePeriodEnd = now + profile.gracePeriod`. Cron `run-forget-cleanup` führt nach Grace die Anonymisierung durch. |
|
|
26
|
+
| **Art. 17** | `user-data-rights:write:cancel-deletion` | User | Während Grace: status zurück auf `Active`. |
|
|
27
|
+
| **Art. 17** | `run-forget-cleanup` (Cron) | System | Findet User mit `status=DeletionRequested AND gracePeriodEnd < now`. Pro User Sub-TX über alle EXT_USER_DATA-Provider mit Strategy aus `retention.policyFor`. user-Hook anonymisiert (PII raus, Sentinel-Email). |
|
|
28
|
+
| **Art. 18 (Restriction)** | `user-data-rights:write:restrict-account` | Admin / SystemAdmin | Status-Flip → Auth-Middleware-Guard blockt Logins. `sessions.revokeAllForUser` killt aktive Sessions. |
|
|
29
|
+
| **Art. 18** | `user-data-rights:write:lift-restriction` | Admin / SystemAdmin | Restriction aufheben. |
|
|
30
|
+
| **Operator (DPO)** | `user-data-rights:query:list-download-attempts` | Admin / SystemAdmin | Brute-Force-Detection: zeigt invalid Download-Versuche (notFound / expired / failed / signedUrlNotSupported) gefiltert nach Result, IP, Zeitraum. |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Speicherorte (Verarbeitungsverzeichnis Art. 30)
|
|
35
|
+
|
|
36
|
+
| Tabelle | Inhalt | Personenbezug | Retention | Zweck |
|
|
37
|
+
|---------|--------|---------------|-----------|-------|
|
|
38
|
+
| `read_users` | User-Profil (email, displayName, passwordHash, locale, status, gracePeriodEnd, roles) | direkt | per Domain (typ. blockDelete bei Aufbewahrungspflicht, sonst hardDelete via Forget-Pipeline) | Authentifizierung, Nutzer-Profil |
|
|
39
|
+
| `read_tenant_memberships` | User↔Tenant-Verknüpfung + Rollen | direkt (via userId) | per Tenant-Lifecycle | Mehrmandant-Zuordnung |
|
|
40
|
+
| `kumiko_events` | Event-Store (alle write-Events) | direkt (`createdBy = userId`) | per Domain-Policy via `data-retention` | Audit-Trail (Art. 15-Selbstauskunft Quelle), Event-Sourcing |
|
|
41
|
+
| `read_export_jobs` | Async Export-Status (queued/running/done/failed), userId, requestedAt, doneAt, storageKey | direkt (userId) | per `compliance-profiles` Profil-Default | Idempotenz + Status-Polling |
|
|
42
|
+
| `read_export_download_tokens` | Magic-Link-Hash (SHA-256), TTL, useCount, lastUsedAt, lastUsedFromIp, lastUsedUserAgent | indirekt (Token→Job→User) | `compliance-profiles.userRights.exportDownloadTtl` (default 7d) | Magic-Link-Auth + Multi-Use-Audit |
|
|
43
|
+
| `read_download_attempts` | Invalid Download-Versuche: result, via, tokenHash, ip, userAgent, attemptedAt | indirekt (IP) | **90d hardDelete** (Entity-Default, Disk-Bomb-Schutz) | DPO-Brute-Force-Detection |
|
|
44
|
+
| `read_tenant_compliance_profiles` | Per-Tenant Profile-Wahl + Override | nein | unbounded (Konfiguration) | Region-/Branchen-Defaults |
|
|
45
|
+
| `read_tenant_retention_overrides` | Per-Tenant Retention-Override pro Entity | nein | unbounded (Konfiguration) | Aufbewahrungspflicht-Edge-Cases |
|
|
46
|
+
| Storage-Provider (Local / S3) | Export-ZIPs + File-Binaries | indirekt (Inhalt) | Local: per `exportDownloadTtl` Cleanup. S3: lifecycle-Policy (App-Author-Verantwortung) | Daten-Export-Auslieferung |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 3. Compliance-Profile
|
|
51
|
+
|
|
52
|
+
| Profil | gracePeriod | exportDownloadTtl | Stale-After | Sub-Processors |
|
|
53
|
+
|--------|-------------|-------------------|-------------|----------------|
|
|
54
|
+
| `eu-dsgvo` | 30d | 7d | 30d | per Tenant konfigurierbar |
|
|
55
|
+
| `swiss-dsg` | 30d | 7d | 30d | + EDÖB-Meldepfad |
|
|
56
|
+
| `de-hr-dsgvo-hgb` | 30d | 7d | 30d | + HR-Aufbewahrung 10y für HR-Entities (anonymize statt delete) |
|
|
57
|
+
| `minimal-no-region` | 30d | 7d | 30d | Migration-Edge-Case ohne Region |
|
|
58
|
+
|
|
59
|
+
Tenant kann via `compliance-profiles:write:set-profile` + Override (`override.userRights.gracePeriod={ days: N }` etc.) eigene Werte setzen.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 4. Technische und Organisatorische Maßnahmen (TOM)
|
|
64
|
+
|
|
65
|
+
Eingebaute Schutzmaßnahmen — können 1:1 in den AVV-Anhang "Technische
|
|
66
|
+
und Organisatorische Maßnahmen" übernommen werden:
|
|
67
|
+
|
|
68
|
+
### Vertraulichkeit / Zugriffskontrolle
|
|
69
|
+
|
|
70
|
+
- **Magic-Link-Token-Hashing**: Plain-Token landet NIE in DB / Event-Store. SHA-256-Hash via `crypto.subtle.digest` (Web-Crypto-API). Plain-Token kommt nur ephemeral via Email-Callback an die App-Author-Implementation.
|
|
71
|
+
- **Download-Token Multi-Use within TTL**: kein consume-on-use (Pattern Google Takeout) — User kann ZIP mehrfach laden, aber TTL gilt absolut.
|
|
72
|
+
- **Audit-Felder am Token**: useCount, lastUsedAt, lastUsedFromIp, lastUsedUserAgent. Operator sieht ob Token mehrfach verwendet wurde, von welcher IP.
|
|
73
|
+
- **Account-weite Auskunft via `ctx.db.raw`**: `my-audit-log` umgeht TenantDb-Auto-Filter explizit (Account-weite Sicht für Art. 15 ist Pflicht); Sicherung über hard-coded `WHERE createdBy = ctx.user.id` (kein userId-Parameter, kein Cross-User-Snooping möglich).
|
|
74
|
+
- **Cross-User-Schutz im Download-Pfad**: `download-by-job` checkt `jobRow.userId === session.user.id` — User kann nur eigene Jobs laden.
|
|
75
|
+
- **Restriction killt Sessions**: `restrict-account` triggert `sessions.revokeAllForUser` — restricted User kann existierende Tabs nicht weiternutzen.
|
|
76
|
+
|
|
77
|
+
### Integrität
|
|
78
|
+
|
|
79
|
+
- **Event-Sourcing first-class**: alle DSGVO-Schreib-Operationen (request-deletion, restrict, lift, request-export) sind Events im `kumiko_events`-Store mit `version_conflict`-Schutz.
|
|
80
|
+
- **Forget-Strategy aus `data-retention`**: Cleanup-Runner konsultiert `retention.policyFor` pro Entity — `blockDelete` für gesetzliche Aufbewahrungspflicht (HR/HGB), `anonymize` als Alternative zu `hardDelete`.
|
|
81
|
+
- **Strategy-respect-Pattern in Default-Hooks**: user-Hook anonymisiert mit Sentinel-Pattern `deleted-<id>@anonymized.invalid` — Unique-Constraint + FK-Refs bleiben intakt.
|
|
82
|
+
|
|
83
|
+
### Verfügbarkeit / Belastbarkeit
|
|
84
|
+
|
|
85
|
+
- **Best-Effort-Audit beim Download**: Audit-INSERT in `read_download_attempts` ist `try/catch` swallowed — Audit-Failure killt nicht den User-facing 4xx.
|
|
86
|
+
- **Idempotenz im Export-Job**: `ACTIVE_JOB_CONSTRAINT` (UNIQUE-Index auf `(userId, status='active')`) verhindert Doppel-Jobs. Worker-Crash → Job bleibt `running`, Recovery-Pfad via Job-Run-Tracking aus `jobs`-Feature.
|
|
87
|
+
- **Per-User Sub-TX im Forget-Runner**: Ein User-Hook-Throw rollback'd nur diesen User; andere User im Batch laufen weiter.
|
|
88
|
+
- **Best-Effort-Notification-Callbacks**: send-Throw für Job A killt nicht Batch B/C (Memory: Atom 5.fix3).
|
|
89
|
+
|
|
90
|
+
### Brute-Force-Schutz
|
|
91
|
+
|
|
92
|
+
- **Edge-Rate-Limit auf Download-Endpoint**: `rateLimit: { per: "ip", limit: 30, windowSeconds: 60 }` für `download-by-token`.
|
|
93
|
+
- **Download-Attempt-Audit** mit 90d hardDelete: invalid Versuche werden persistiert für DPO-Detection, Tabelle ist begrenzt → kein Disk-Bomb durch Brute-Force.
|
|
94
|
+
- **Token-Hash-Suchraum**: 32-Byte-Random = 256 Bit, Brute-Force über Edge-Limit praktisch nicht möglich.
|
|
95
|
+
|
|
96
|
+
### Zweckbindung / Datenminimierung
|
|
97
|
+
|
|
98
|
+
- **Export-Bundle Default-PII-Filter**: user-Hook entfernt `passwordHash`, `roles`, `status` aus dem Bundle (App-Author kann das per Custom-Hook überschreiben).
|
|
99
|
+
- **fileRefs separat**: Export-Bundle enthält Datei-Metadaten (id, fileName, mimeType, size); Binaries werden via signed-URL separat ins ZIP gepackt — kein Inline-Base64-Memory-Druck.
|
|
100
|
+
|
|
101
|
+
### Auftragskontrolle (gegenüber Sub-Processors)
|
|
102
|
+
|
|
103
|
+
- **Storage-Provider als Plugin** (`file-foundation` + `file-provider-{s3,inmemory}`): App-Author wählt Provider; AVV mit S3-Hoster (Hetzner / AWS) ist vom Provider abhängig.
|
|
104
|
+
- **Email-Transport als Plugin** (`mail-foundation` + `mail-transport-{smtp,inmemory}`): SMTP / SES / Resend per Plugin austauschbar.
|
|
105
|
+
- **`compliance-profiles:query:sub-processors`**: Per-Tenant Liste der aktiven Sub-Processors, abrufbar für AVV-Anhang.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 5. Cron-Jobs / Operationale Trigger
|
|
110
|
+
|
|
111
|
+
| Job | Trigger | Was passiert | Operator-Sichtbarkeit |
|
|
112
|
+
|-----|---------|--------------|------------------------|
|
|
113
|
+
| `run-forget-cleanup` | Cron (per App-Author konfigurierbar, typisch täglich) | Findet User mit abgelaufener Grace, ruft `EXT_USER_DATA.delete` pro Provider, anonymisiert User, sendet `sendDeletionExecutedEmail`-Callback | `read_job_runs` (success/fail), Hook-Errors als `errors[]` im Result |
|
|
114
|
+
| `run-export-jobs` | Cron + Event-getriggert | Findet pending Export-Jobs, ruft `runUserExport` → ZIP-Bau → Storage-Upload → Magic-Link-Token → `sendExportReadyEmail` | `read_job_runs`, `read_export_jobs.status` |
|
|
115
|
+
| `data-retention-cleanup` | Cron (in `data-retention`) | Cleant abgelaufene Rows pro Entity per `retention.keepFor` | `read_job_runs` |
|
|
116
|
+
| Token-Cleanup (implizit) | Per `compliance-profiles.userRights.exportDownloadTtl` | Worker-Job entfernt expired Magic-Link-Tokens + Storage-Binaries | `read_export_download_tokens` |
|
|
117
|
+
| Download-Attempt-Cleanup | Per Entity-Default 90d | Cleant `read_download_attempts` | — |
|
|
118
|
+
|
|
119
|
+
App-Author registriert die Cron-Trigger im run-config; `jobs`-Feature
|
|
120
|
+
persistiert Run-Tracking + Retry-Pfad.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 6. Doku-Snippets für Marc
|
|
125
|
+
|
|
126
|
+
### Datenschutzerklärung — Betroffenenrechte
|
|
127
|
+
|
|
128
|
+
> **Recht auf Auskunft (Art. 15 DSGVO):** Sie können jederzeit eine
|
|
129
|
+
> Kopie aller bei uns über Sie gespeicherten Daten anfordern. Über die
|
|
130
|
+
> Funktion "Daten exportieren" in den Account-Einstellungen erhalten
|
|
131
|
+
> Sie ein vollständiges JSON-/ZIP-Bundle Ihrer Profildaten, hochgeladenen
|
|
132
|
+
> Dateien und [App-Author: Domain-Daten ergänzen] per signed Magic-Link
|
|
133
|
+
> auf Ihre hinterlegte E-Mail-Adresse. Der Link ist 7 Tage gültig.
|
|
134
|
+
|
|
135
|
+
> **Recht auf Löschung (Art. 17 DSGVO):** Über "Account löschen" können
|
|
136
|
+
> Sie Ihren Account jederzeit zur Löschung vormerken. Es gilt eine
|
|
137
|
+
> Karenzzeit von 30 Tagen, in der Sie den Antrag widerrufen können
|
|
138
|
+
> (Funktion "Löschung widerrufen"). Nach Ablauf werden Ihre Daten
|
|
139
|
+
> automatisch gelöscht oder anonymisiert. Daten mit gesetzlicher
|
|
140
|
+
> Aufbewahrungspflicht (z. B. Rechnungen nach §147 AO) bleiben bis zum
|
|
141
|
+
> Fristablauf gesperrt erhalten und werden danach automatisch gelöscht.
|
|
142
|
+
|
|
143
|
+
> **Recht auf Einschränkung (Art. 18 DSGVO):** Auf begründeten Antrag
|
|
144
|
+
> kann Ihr Account temporär gesperrt werden. Während der Sperre können
|
|
145
|
+
> Sie sich nicht mehr einloggen, Ihre Daten werden aber nicht gelöscht
|
|
146
|
+
> oder verändert.
|
|
147
|
+
|
|
148
|
+
### AVV-TOM-Anhang
|
|
149
|
+
|
|
150
|
+
Für den AVV-Anhang "Technische und Organisatorische Maßnahmen" siehe
|
|
151
|
+
**Abschnitt 4** dieses Dokuments — komplette Liste mit Verweisen auf
|
|
152
|
+
die Code-Implementierung.
|
|
153
|
+
|
|
154
|
+
### Verarbeitungsverzeichnis
|
|
155
|
+
|
|
156
|
+
Spalte "Speicherorte / Empfänger" → siehe **Abschnitt 2** (Tabellen-
|
|
157
|
+
Übersicht). Spalte "Löschfristen" → siehe **Abschnitt 3** + Spalte
|
|
158
|
+
"Retention" in Abschnitt 2.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 7. Was NICHT in diesem Framework abgedeckt ist
|
|
163
|
+
|
|
164
|
+
Bewusst ausserhalb — App-Author-Verantwortung:
|
|
165
|
+
|
|
166
|
+
- **Auswahl + AVV mit konkretem Storage-Provider** (Hetzner / AWS / etc.)
|
|
167
|
+
- **Auswahl + AVV mit konkretem Email-Provider** (SMTP-Host / SES / Resend)
|
|
168
|
+
- **Datenpannen-Meldung** an Aufsichtsbehörde (organisatorisch, nicht technisch)
|
|
169
|
+
- **DSFA** (Datenschutz-Folgenabschätzung) — Framework liefert die TOM-Inputs, App-Author macht die Bewertung
|
|
170
|
+
- **Cookie-Consent-Layer** (das ist Frontend-Sache + separate consent-Feature, nicht Teil von user-data-rights)
|
|
171
|
+
- **Tenant-Lifecycle-Destroy** (Account-Löschung des Tenants selbst, nicht der User darin) — kommt als separates `tenant-lifecycle`-Feature in Sprint 5
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Referenzen
|
|
176
|
+
|
|
177
|
+
- Code: `packages/bundled-features/src/user-data-rights/`
|
|
178
|
+
- Default-Hooks: `packages/bundled-features/src/user-data-rights-defaults/`
|
|
179
|
+
- Compliance-Profile: `packages/bundled-features/src/compliance-profiles/`
|
|
180
|
+
- Retention-Engine: `packages/bundled-features/src/data-retention/`
|
|
181
|
+
- Sample-App: `samples/apps/user-data-rights-demo/`
|
|
182
|
+
- Tests: `packages/bundled-features/src/user-data-rights/__tests__/` (188 Tests)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# user-data-rights
|
|
2
|
+
|
|
3
|
+
DSGVO Art. 15 (Auskunft) + Art. 17 (Löschung) + Art. 18 (Restriction) +
|
|
4
|
+
Art. 20 (Portabilität) als Core-Feature.
|
|
5
|
+
|
|
6
|
+
**Status:** S2 abgeschlossen — alle Endpoints, Hooks, Default-Provider,
|
|
7
|
+
Cron-Pipeline, Tests + Sample wired.
|
|
8
|
+
|
|
9
|
+
## Pattern
|
|
10
|
+
|
|
11
|
+
Statt jedes Feature seine eigene Forget-/Export-Logik schreibt, hängt
|
|
12
|
+
es sich via `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })`
|
|
13
|
+
an. user-data-rights orchestriert Export- und Forget-Pipeline:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
defineFeature("tasks", (r) => {
|
|
17
|
+
r.requires("user-data-rights");
|
|
18
|
+
r.useExtension(EXT_USER_DATA, "task", {
|
|
19
|
+
export: async (ctx) => ({
|
|
20
|
+
entity: "task",
|
|
21
|
+
rows: await ctx.db.select().from(tasksTable)
|
|
22
|
+
.where(eq(tasksTable.authorId, ctx.userId)),
|
|
23
|
+
}),
|
|
24
|
+
delete: async (ctx, strategy) => {
|
|
25
|
+
if (strategy === "anonymize") {
|
|
26
|
+
await ctx.db.update(tasksTable).set({ authorId: null })
|
|
27
|
+
.where(eq(tasksTable.authorId, ctx.userId));
|
|
28
|
+
} else {
|
|
29
|
+
await ctx.db.delete(tasksTable)
|
|
30
|
+
.where(eq(tasksTable.authorId, ctx.userId));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Hook-Signaturen in `framework/src/engine/extensions/user-data.ts`:
|
|
38
|
+
|
|
39
|
+
- `UserDataExportHook(ctx) => Promise<UserDataExportSnippet | null>`
|
|
40
|
+
- `UserDataDeleteHook(ctx, strategy) => Promise<void>`
|
|
41
|
+
- `UserDataDeleteStrategy = "delete" | "anonymize"`
|
|
42
|
+
|
|
43
|
+
## Endpoints
|
|
44
|
+
|
|
45
|
+
| Article | Handler | Zweck |
|
|
46
|
+
|---------|---------|-------|
|
|
47
|
+
| Art. 15 | `user-data-rights:query:my-audit-log` | User sieht eigene Framework-Events (account-weit über alle Memberships). Domain-Entities ohne `ctx.appendEvent` erscheinen NICHT — nur im Export-Bundle. |
|
|
48
|
+
| Art. 15+20 | `user-data-rights:write:request-export` | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten + signed Magic-Link |
|
|
49
|
+
| Art. 17 | `user-data-rights:write:request-deletion` | Soft-Delete mit Grace-Period, anschließend Cron anonymisiert User + cleant Domain-Entities |
|
|
50
|
+
| Art. 17 | `user-data-rights:write:cancel-deletion` | User widerruft seinen Forget-Request während der Grace |
|
|
51
|
+
| Art. 18 | `user-data-rights:write:restrict-account` | Auth-Middleware blockt Logins bis Lift |
|
|
52
|
+
| Art. 18 | `user-data-rights:write:lift-restriction` | Admin/SystemAdmin hebt Restriction auf |
|
|
53
|
+
| Operator | `user-data-rights:query:list-download-attempts` | DPO-Sicht auf invalid Download-Versuche (Brute-Force-Detection, Admin/SystemAdmin only) |
|
|
54
|
+
|
|
55
|
+
Plus 2 anonyme HTTP-Routes für Export-Download (Magic-Link-Pfad +
|
|
56
|
+
session-auth-Pfad), siehe `handlers/download-by-{token,job}.query.ts`.
|
|
57
|
+
|
|
58
|
+
## Cross-Feature-API
|
|
59
|
+
|
|
60
|
+
**Exposes:**
|
|
61
|
+
- `userDataRights.runExport` — über die public Runner-Exports
|
|
62
|
+
(`runUserExport`, `runForgetCleanup`)
|
|
63
|
+
- `userDataRights.runForget`
|
|
64
|
+
|
|
65
|
+
**Uses:**
|
|
66
|
+
- `compliance.forTenant` (Grace-Period aus Profile)
|
|
67
|
+
- `retention.policyFor` (blockDelete-Konsultation, anonymize statt delete)
|
|
68
|
+
- `sessions.revokeAllForUser` (Restriction killt aktive Sessions)
|
|
69
|
+
|
|
70
|
+
## Default-Hooks (`user-data-rights-defaults`)
|
|
71
|
+
|
|
72
|
+
Optional-mountbares Sub-Feature liefert Default-Hooks für
|
|
73
|
+
Core-Entities `user` (anonymize: email→`deleted-<id>@anonymized.invalid`,
|
|
74
|
+
displayName→`(deleted)`, passwordHash=null) und `fileRef` (delete: row +
|
|
75
|
+
storage-binary; anonymize: insertedById=null). App-Author kann es
|
|
76
|
+
weglassen wenn er Custom-Hooks registrieren will.
|
|
77
|
+
|
|
78
|
+
## Audit-Trail
|
|
79
|
+
|
|
80
|
+
| Tabelle | Zweck | Retention |
|
|
81
|
+
|---------|-------|-----------|
|
|
82
|
+
| `kumiko_events` | Framework-Event-Store, Quelle für `my-audit-log` | per Domain-Policy |
|
|
83
|
+
| `read_export_jobs` | Async Export-Status (queued / done / failed) | per `compliance-profiles` |
|
|
84
|
+
| `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit | per `compliance-profiles` (default `exportDownloadTtl`) |
|
|
85
|
+
| `read_download_attempts` | Invalid-Download-Versuche für DPO-Brute-Force-Detection | **90d hardDelete** (Entity-Default — schützt vor Disk-Bomb bei aktiven Angriffen) |
|
|
86
|
+
|
|
87
|
+
## Tests
|
|
88
|
+
|
|
89
|
+
18 Testdateien, 188 Tests, alle grün:
|
|
90
|
+
|
|
91
|
+
| Datei | Pinst |
|
|
92
|
+
|-------|-------|
|
|
93
|
+
| `audit-log.integration.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
|
|
94
|
+
| `cross-data-matrix.integration.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
|
|
95
|
+
| `download.integration.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
|
|
96
|
+
| `request-export.integration.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
|
|
97
|
+
| `request-deletion-callback.integration.ts` + `request-cancel-deletion.integration.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
|
|
98
|
+
| `restriction-flow.integration.ts` | Status-Flip + Auth-Middleware-Block + Lift |
|
|
99
|
+
| `run-{export-jobs,forget-cleanup,user-export}.integration.ts` | Worker-Logic + Idempotency + Email-Callbacks |
|
|
100
|
+
| `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
|
|
101
|
+
| `user-data-rights.integration.ts` | Boot-Smoke + Feature-Meta |
|
|
102
|
+
| `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
|
|
103
|
+
| `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
|
|
104
|
+
|
|
105
|
+
## Sample
|
|
106
|
+
|
|
107
|
+
`samples/apps/user-data-rights-demo` — runnable Demo mit todos-Domain,
|
|
108
|
+
EXT_USER_DATA-Hook für strategy-aware delete (anonymize → authorId=null,
|
|
109
|
+
delete → DROP), 3 living-doc Integration-Tests.
|