@cosmicdrift/kumiko-bundled-features 0.277.0 → 0.280.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +8 -2
- package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +6 -6
- package/src/auth-mfa-user-data/hooks.ts +4 -6
- package/src/cap-counter/__tests__/enforce-cap.test.ts +7 -10
- package/src/cap-counter/__tests__/stock-cap-guard.integration.test.ts +123 -6
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +135 -1
- package/src/cap-counter/book-cap-usage.ts +158 -0
- package/src/cap-counter/changes.json +16 -1
- package/src/cap-counter/enforce-cap.ts +5 -37
- package/src/cap-counter/handlers/increment.write.ts +7 -47
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +6 -33
- package/src/cap-counter/index.ts +8 -0
- package/src/cap-counter/stock-cap-guard.ts +14 -19
- package/src/cap-counter/with-cap-enforcement.ts +9 -11
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +10 -3
- package/src/custom-fields/wire-user-data-rights.ts +40 -24
- package/src/data-retention/resolve-for-tenant.ts +18 -6
- package/src/delivery/db/queries/preferences.ts +1 -0
- package/src/files-tenant-data/hooks.ts +3 -5
- package/src/folders/__tests__/folders.integration.test.ts +4 -3
- package/src/folders-user-data/__tests__/hooks.integration.test.ts +2 -1
- package/src/folders-user-data/hooks.ts +5 -10
- package/src/form-draft-user-data/__tests__/hooks-delete-failure.integration.test.ts +7 -1
- package/src/form-draft-user-data/__tests__/hooks.integration.test.ts +16 -5
- package/src/form-draft-user-data/hooks.ts +3 -6
- package/src/notes-history-user-data/__tests__/hooks.integration.test.ts +9 -3
- package/src/notes-history-user-data/hooks.ts +3 -4
- package/src/personal-access-tokens/handlers/create.write.ts +1 -0
- package/src/template-resolver-user-data/hooks.ts +1 -2
- package/src/tenant-lifecycle/feature.ts +2 -0
- package/src/tenant-lifecycle/run-tenant-destroy.ts +9 -0
- package/src/tenant-lifecycle/stages.ts +26 -2
- package/src/tier-engine/__tests__/tier-resolver.integration.test.ts +138 -0
- package/src/tier-engine/changes.json +7 -0
- package/src/tier-engine/tier-resolver.ts +7 -6
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +5 -14
- package/src/user-data-rights/__tests__/escape-hatch-audit.integration.test.ts +133 -0
- package/src/user-data-rights/__tests__/escape-hatch-declaration.integration.test.ts +198 -0
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +15 -3
- package/src/user-data-rights/__tests__/forget-cleanup-search-purge-ordering.integration.test.ts +10 -2
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +3 -1
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +8 -3
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +8 -2
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +8 -4
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +15 -5
- package/src/user-data-rights/feature.ts +4 -0
- package/src/user-data-rights/run-export-jobs.ts +16 -1
- package/src/user-data-rights/run-forget-cleanup.ts +44 -2
- package/src/user-data-rights/run-user-export.ts +43 -3
- package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +7 -6
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +64 -18
- package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +2 -3
- package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +4 -6
- package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +1 -2
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +5 -11
- package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +2 -3
- package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +1 -2
- package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +4 -6
- package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +8 -10
- package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +2 -3
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +9 -10
|
@@ -49,7 +49,11 @@ import { addDurationSpec } from "@cosmicdrift/kumiko-framework/compliance";
|
|
|
49
49
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
50
50
|
import type { DbConnection, DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
51
51
|
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
52
|
-
import type {
|
|
52
|
+
import type {
|
|
53
|
+
EscapeHatchAuditSink,
|
|
54
|
+
Registry,
|
|
55
|
+
TenantId,
|
|
56
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
53
57
|
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
54
58
|
import {
|
|
55
59
|
createZipStream,
|
|
@@ -155,6 +159,11 @@ export interface RunExportJobsArgs {
|
|
|
155
159
|
* a URL fragment (not a query param — see issue #1271) and passes that
|
|
156
160
|
* Callback durch. Required wenn sendExportReadyEmail gesetzt. */
|
|
157
161
|
readonly appExportDownloadUrl?: string;
|
|
162
|
+
|
|
163
|
+
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
164
|
+
// systemUser.id); threaded into runUserExport's per-hook TenantDb.
|
|
165
|
+
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
166
|
+
readonly actor?: string;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
export interface ExportJobError {
|
|
@@ -219,6 +228,8 @@ export async function runExportJobs(args: RunExportJobsArgs): Promise<RunExportJ
|
|
|
219
228
|
buildStorageProvider,
|
|
220
229
|
now,
|
|
221
230
|
job,
|
|
231
|
+
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
232
|
+
actor: args.actor,
|
|
222
233
|
});
|
|
223
234
|
if (outcome.kind === "done") {
|
|
224
235
|
completedJobIds.push(job.id);
|
|
@@ -337,6 +348,8 @@ async function processJob(args: {
|
|
|
337
348
|
buildStorageProvider: (tenantId: TenantId) => Promise<FileStorageProvider>;
|
|
338
349
|
now: Instant;
|
|
339
350
|
job: JobRow;
|
|
351
|
+
escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
352
|
+
actor?: string;
|
|
340
353
|
}): Promise<ProcessOutcome> {
|
|
341
354
|
const { db, registry, buildStorageProvider, now, job } = args;
|
|
342
355
|
const executor = createSystemUser(job.requestedFromTenantId);
|
|
@@ -381,6 +394,8 @@ async function processJob(args: {
|
|
|
381
394
|
registry,
|
|
382
395
|
userId: job.userId,
|
|
383
396
|
now,
|
|
397
|
+
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
398
|
+
actor: args.actor,
|
|
384
399
|
});
|
|
385
400
|
|
|
386
401
|
const profile = await resolveProfileForTenant({
|
|
@@ -39,14 +39,17 @@ import {
|
|
|
39
39
|
subjectIdToKey,
|
|
40
40
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
41
41
|
import {
|
|
42
|
+
createTenantDb,
|
|
42
43
|
type DbRunner,
|
|
43
44
|
entityEventName,
|
|
44
45
|
executeRawQuery,
|
|
45
46
|
nullBlindIndexesForSubject,
|
|
46
47
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
47
48
|
import {
|
|
49
|
+
type EscapeHatchAuditSink,
|
|
48
50
|
EXT_USER_DATA,
|
|
49
51
|
EXT_USER_DATA_ORDER,
|
|
52
|
+
extensionUsageEscapeHatchReason,
|
|
50
53
|
type Registry,
|
|
51
54
|
type TenantId,
|
|
52
55
|
type TenantUserModel,
|
|
@@ -54,6 +57,7 @@ import {
|
|
|
54
57
|
type UserDataDeleteStrategy,
|
|
55
58
|
type UserDataStorageProvider,
|
|
56
59
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
60
|
+
import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
57
61
|
import {
|
|
58
62
|
purgeSearchDocumentsForSubject,
|
|
59
63
|
type SearchAdapter,
|
|
@@ -140,6 +144,12 @@ export interface RunForgetCleanupArgs {
|
|
|
140
144
|
* (no tenant-scoped erasure).
|
|
141
145
|
*/
|
|
142
146
|
readonly tenantModel?: TenantUserModel;
|
|
147
|
+
|
|
148
|
+
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
149
|
+
// systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
|
|
150
|
+
// escapeHatch when its delete hook calls ctx.db.unsafeRaw(reason).
|
|
151
|
+
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
152
|
+
readonly actor?: string;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
export interface ForgetCleanupError {
|
|
@@ -184,6 +194,26 @@ interface HookEntry {
|
|
|
184
194
|
/** Lower runs first. Owner-column-preserving redaction declares a negative
|
|
185
195
|
* order so it precedes owner-nulling hooks on the same entity (see sort below). */
|
|
186
196
|
readonly order: number;
|
|
197
|
+
readonly escapeHatchReason: string | undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildHookDb(
|
|
201
|
+
db: DbRunner,
|
|
202
|
+
tenantId: TenantId,
|
|
203
|
+
entry: HookEntry,
|
|
204
|
+
args: { readonly escapeHatchAuditSink?: EscapeHatchAuditSink; readonly actor?: string },
|
|
205
|
+
) {
|
|
206
|
+
const report = createEscapeHatchReporter({
|
|
207
|
+
handler: `${EXT_USER_DATA}:${entry.entityName}`,
|
|
208
|
+
tenantId,
|
|
209
|
+
actor: args.actor ?? "system",
|
|
210
|
+
sink: args.escapeHatchAuditSink,
|
|
211
|
+
});
|
|
212
|
+
return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
213
|
+
unsafeRaw:
|
|
214
|
+
entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
|
|
215
|
+
report,
|
|
216
|
+
});
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
// EXT_USER_DATA delete-hooks default here; a hook that redacts data keyed on an
|
|
@@ -226,7 +256,12 @@ export async function runForgetCleanup(
|
|
|
226
256
|
const opts = (u.options ?? {}) as { delete?: UserDataDeleteHook; order?: number }; // @cast-boundary engine-payload
|
|
227
257
|
if (!opts.delete) return null;
|
|
228
258
|
const order = typeof opts.order === "number" ? opts.order : HOOK_ORDER_DEFAULT;
|
|
229
|
-
return {
|
|
259
|
+
return {
|
|
260
|
+
entityName: u.entityName,
|
|
261
|
+
deleteHook: opts.delete,
|
|
262
|
+
order,
|
|
263
|
+
escapeHatchReason: extensionUsageEscapeHatchReason(u),
|
|
264
|
+
};
|
|
230
265
|
})
|
|
231
266
|
.filter((x): x is HookEntry => x !== null)
|
|
232
267
|
// Order ascending. Array.sort is ES2019-stable, so equal orders keep
|
|
@@ -252,6 +287,8 @@ export async function runForgetCleanup(
|
|
|
252
287
|
appTenantModel,
|
|
253
288
|
kms,
|
|
254
289
|
searchAdapter: args.searchAdapter,
|
|
290
|
+
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
291
|
+
actor: args.actor,
|
|
255
292
|
});
|
|
256
293
|
hookCallsAttempted += userResult.hookCallsAttempted;
|
|
257
294
|
errors.push(...userResult.errors);
|
|
@@ -316,6 +353,8 @@ async function processUser(args: {
|
|
|
316
353
|
appTenantModel: TenantUserModel;
|
|
317
354
|
kms?: KmsAdapter;
|
|
318
355
|
searchAdapter?: SearchAdapter;
|
|
356
|
+
escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
357
|
+
actor?: string;
|
|
319
358
|
}): Promise<ProcessUserResult> {
|
|
320
359
|
const { db, registry, userId, hookEntries, buildStorageProvider, appTenantModel, kms } = args;
|
|
321
360
|
const errors: ForgetCleanupError[] = [];
|
|
@@ -404,7 +443,10 @@ async function processUser(args: {
|
|
|
404
443
|
hookCallsAttempted++;
|
|
405
444
|
const hookResult = await entry.deleteHook(
|
|
406
445
|
{
|
|
407
|
-
db: tx,
|
|
446
|
+
db: buildHookDb(tx, tenantId, entry, {
|
|
447
|
+
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
448
|
+
actor: args.actor,
|
|
449
|
+
}),
|
|
408
450
|
registry,
|
|
409
451
|
tenantId,
|
|
410
452
|
userId,
|
|
@@ -30,15 +30,19 @@ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
|
30
30
|
import {
|
|
31
31
|
collectEncryptedFieldNames,
|
|
32
32
|
configuredEntityFieldEncryption,
|
|
33
|
+
createTenantDb,
|
|
33
34
|
decryptEntityFieldValues,
|
|
34
35
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
35
36
|
import {
|
|
37
|
+
type EscapeHatchAuditSink,
|
|
36
38
|
EXT_USER_DATA,
|
|
39
|
+
extensionUsageEscapeHatchReason,
|
|
37
40
|
type Registry,
|
|
38
41
|
type TenantId,
|
|
39
42
|
type UserDataExportHook,
|
|
40
43
|
type UserDataExportSnippet,
|
|
41
44
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
45
|
+
import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
42
46
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
43
47
|
import { tenantMembershipsTable } from "../tenant";
|
|
44
48
|
import { buildFileRefZipPath } from "./zip-path";
|
|
@@ -50,6 +54,11 @@ export interface RunUserExportArgs {
|
|
|
50
54
|
readonly registry: Registry;
|
|
51
55
|
readonly userId: string;
|
|
52
56
|
readonly now: Instant;
|
|
57
|
+
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
58
|
+
// systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
|
|
59
|
+
// escapeHatch when its export hook calls ctx.db.unsafeRaw(reason).
|
|
60
|
+
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
61
|
+
readonly actor?: string;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export interface UserExportFileRef {
|
|
@@ -90,6 +99,26 @@ export interface UserExportBundle {
|
|
|
90
99
|
interface HookEntry {
|
|
91
100
|
readonly entityName: string;
|
|
92
101
|
readonly exportHook: UserDataExportHook;
|
|
102
|
+
readonly escapeHatchReason: string | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildHookDb(
|
|
106
|
+
db: DbRunner,
|
|
107
|
+
tenantId: TenantId,
|
|
108
|
+
entry: HookEntry,
|
|
109
|
+
args: Pick<RunUserExportArgs, "escapeHatchAuditSink" | "actor">,
|
|
110
|
+
) {
|
|
111
|
+
const report = createEscapeHatchReporter({
|
|
112
|
+
handler: `${EXT_USER_DATA}:${entry.entityName}`,
|
|
113
|
+
tenantId,
|
|
114
|
+
actor: args.actor ?? "system",
|
|
115
|
+
sink: args.escapeHatchAuditSink,
|
|
116
|
+
});
|
|
117
|
+
return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
118
|
+
unsafeRaw:
|
|
119
|
+
entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
|
|
120
|
+
report,
|
|
121
|
+
});
|
|
93
122
|
}
|
|
94
123
|
|
|
95
124
|
/**
|
|
@@ -137,7 +166,13 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
|
|
|
137
166
|
const hookEntries: HookEntry[] = usages
|
|
138
167
|
.map((u): HookEntry | null => {
|
|
139
168
|
const opts = (u.options ?? {}) as { export?: UserDataExportHook }; // @cast-boundary engine-payload
|
|
140
|
-
return opts.export
|
|
169
|
+
return opts.export
|
|
170
|
+
? {
|
|
171
|
+
entityName: u.entityName,
|
|
172
|
+
exportHook: opts.export,
|
|
173
|
+
escapeHatchReason: extensionUsageEscapeHatchReason(u),
|
|
174
|
+
}
|
|
175
|
+
: null;
|
|
141
176
|
})
|
|
142
177
|
.filter((x): x is HookEntry => x !== null);
|
|
143
178
|
|
|
@@ -147,7 +182,12 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
|
|
|
147
182
|
for (const tenantId of tenantList) {
|
|
148
183
|
const entities: UserDataExportSnippet[] = [];
|
|
149
184
|
for (const entry of hookEntries) {
|
|
150
|
-
const rawSnippet = await entry.exportHook({
|
|
185
|
+
const rawSnippet = await entry.exportHook({
|
|
186
|
+
db: buildHookDb(db, tenantId, entry, args),
|
|
187
|
+
registry,
|
|
188
|
+
tenantId,
|
|
189
|
+
userId,
|
|
190
|
+
});
|
|
151
191
|
if (rawSnippet === null) continue;
|
|
152
192
|
const snippet = await decryptSnippetFields(registry, entry.entityName, rawSnippet);
|
|
153
193
|
entities.push(snippet);
|
|
@@ -181,7 +221,7 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
|
|
|
181
221
|
const orphanEntities: UserDataExportSnippet[] = [];
|
|
182
222
|
for (const entry of hookEntries) {
|
|
183
223
|
const rawSnippet = await entry.exportHook({
|
|
184
|
-
db,
|
|
224
|
+
db: buildHookDb(db, SYSTEM_TENANT_ID_FOR_ORPHANS, entry, args),
|
|
185
225
|
registry,
|
|
186
226
|
tenantId: SYSTEM_TENANT_ID_FOR_ORPHANS,
|
|
187
227
|
userId,
|
package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
14
14
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
15
|
+
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
16
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
17
|
import {
|
|
16
18
|
setupTestStack,
|
|
17
19
|
type TestStack,
|
|
@@ -117,7 +119,7 @@ function uuid(suffix: number): string {
|
|
|
117
119
|
|
|
118
120
|
function ctx(userId: string, overrides: Record<string, unknown> = {}) {
|
|
119
121
|
return {
|
|
120
|
-
db: full.db,
|
|
122
|
+
db: createTenantDb(full.db, TENANT_A, "tenant"),
|
|
121
123
|
registry: full.registry,
|
|
122
124
|
tenantId: TENANT_A,
|
|
123
125
|
userId,
|
|
@@ -132,10 +134,9 @@ async function rawSelect(sql: string, params: unknown[]) {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
async function seedUser(id: string): Promise<void> {
|
|
135
|
-
const SYSTEM_TENANT = "00000000-0000-4000-8000-000000000001";
|
|
136
137
|
await seedRow(full.db, userTable, {
|
|
137
138
|
id,
|
|
138
|
-
tenantId:
|
|
139
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
139
140
|
email: `user-${id}@example.com`,
|
|
140
141
|
passwordHash: "hashed-password",
|
|
141
142
|
displayName: `User ${id}`,
|
|
@@ -404,11 +405,11 @@ describe("job-run userData-hooks (#799)", () => {
|
|
|
404
405
|
`INSERT INTO store_job_runs
|
|
405
406
|
(id, tenant_id, job_name, bull_job_id, status, payload, attempt, started_at, triggered_by_id)
|
|
406
407
|
VALUES ($1::uuid, $2, 'app:job:export', $3, 'completed', '{"scope":"mine"}', 1, now(), $4)`,
|
|
407
|
-
[id,
|
|
408
|
+
[id, SYSTEM_TENANT_ID, `bull-${id}`, triggeredById],
|
|
408
409
|
);
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
test("export filters by triggeredById only (
|
|
412
|
+
test("export filters by triggeredById only (rows live on SYSTEM_TENANT_ID, ctx bound to TENANT_A)", async () => {
|
|
412
413
|
await seedRun(uuid(70), "jr-user-1");
|
|
413
414
|
await seedRun(uuid(71), "jr-user-2");
|
|
414
415
|
await seedRun(uuid(72), null);
|
|
@@ -428,7 +429,7 @@ describe("job-run userData-hooks (#799)", () => {
|
|
|
428
429
|
describe("Presence-Gating: Source-Feature nicht gemountet → null/no-op statt Crash", () => {
|
|
429
430
|
test("alle gegateten Hooks no-open auf dem Minimal-Stack", async () => {
|
|
430
431
|
const minCtx = {
|
|
431
|
-
db: minimal.db,
|
|
432
|
+
db: createTenantDb(minimal.db, TENANT_A, "tenant"),
|
|
432
433
|
registry: minimal.registry,
|
|
433
434
|
tenantId: TENANT_A,
|
|
434
435
|
userId: "any-user",
|
package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
14
14
|
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
15
15
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
16
|
+
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
16
17
|
import { variantSuffix } from "@cosmicdrift/kumiko-framework/derivatives";
|
|
18
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
19
|
import {
|
|
18
20
|
createInMemoryFileProvider,
|
|
19
21
|
deriveKey,
|
|
@@ -87,10 +89,9 @@ async function seedUser(id: string, overrides: Record<string, unknown> = {}): Pr
|
|
|
87
89
|
// user-Entity ist tenant-agnostisch im Domain-Sinn, aber das DB-
|
|
88
90
|
// Schema hat tenant_id-Spalte automatisch (Framework-Default).
|
|
89
91
|
// Pragmatisch: SYSTEM_TENANT_ID fuer User-Rows in Tests.
|
|
90
|
-
const SYSTEM_TENANT = "00000000-0000-4000-8000-000000000001";
|
|
91
92
|
await seedRow(stack.db, userTable, {
|
|
92
93
|
id,
|
|
93
|
-
tenantId:
|
|
94
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
94
95
|
email: `user-${id}@example.com`,
|
|
95
96
|
passwordHash: "hashed-password",
|
|
96
97
|
displayName: `User ${id}`,
|
|
@@ -168,7 +169,7 @@ describe("S2.H1 :: userExportHook", () => {
|
|
|
168
169
|
await seedUser(uuid(1001), { displayName: "Marc" });
|
|
169
170
|
|
|
170
171
|
const result = await userExportHook({
|
|
171
|
-
db: stack.db,
|
|
172
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
172
173
|
registry: stack.registry,
|
|
173
174
|
tenantId: TENANT_A,
|
|
174
175
|
userId: uuid(1001),
|
|
@@ -189,7 +190,7 @@ describe("S2.H1 :: userExportHook", () => {
|
|
|
189
190
|
|
|
190
191
|
test("returns null wenn User nicht existiert", async () => {
|
|
191
192
|
const result = await userExportHook({
|
|
192
|
-
db: stack.db,
|
|
193
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
193
194
|
registry: stack.registry,
|
|
194
195
|
tenantId: TENANT_A,
|
|
195
196
|
userId: uuid(1002),
|
|
@@ -203,7 +204,12 @@ describe("S2.H1 :: userDeleteHook", () => {
|
|
|
203
204
|
await seedUser(uuid(1003));
|
|
204
205
|
|
|
205
206
|
await userDeleteHook(
|
|
206
|
-
{
|
|
207
|
+
{
|
|
208
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
209
|
+
registry: stack.registry,
|
|
210
|
+
tenantId: TENANT_A,
|
|
211
|
+
userId: uuid(1003),
|
|
212
|
+
},
|
|
207
213
|
"delete",
|
|
208
214
|
);
|
|
209
215
|
|
|
@@ -222,7 +228,12 @@ describe("S2.H1 :: userDeleteHook", () => {
|
|
|
222
228
|
await seedUser(uuid(1004));
|
|
223
229
|
|
|
224
230
|
await userDeleteHook(
|
|
225
|
-
{
|
|
231
|
+
{
|
|
232
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
233
|
+
registry: stack.registry,
|
|
234
|
+
tenantId: TENANT_A,
|
|
235
|
+
userId: uuid(1004),
|
|
236
|
+
},
|
|
226
237
|
"anonymize",
|
|
227
238
|
);
|
|
228
239
|
|
|
@@ -238,7 +249,12 @@ describe("S2.H1 :: userDeleteHook", () => {
|
|
|
238
249
|
await seedUser(uuid(1005));
|
|
239
250
|
|
|
240
251
|
await userDeleteHook(
|
|
241
|
-
{
|
|
252
|
+
{
|
|
253
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
254
|
+
registry: stack.registry,
|
|
255
|
+
tenantId: TENANT_A,
|
|
256
|
+
userId: uuid(1005),
|
|
257
|
+
},
|
|
242
258
|
"delete",
|
|
243
259
|
);
|
|
244
260
|
const afterFirst = await fetchUser(uuid(1005));
|
|
@@ -247,7 +263,12 @@ describe("S2.H1 :: userDeleteHook", () => {
|
|
|
247
263
|
// Zweiter Call: kein Crash + State unverändert
|
|
248
264
|
await expect(
|
|
249
265
|
userDeleteHook(
|
|
250
|
-
{
|
|
266
|
+
{
|
|
267
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
268
|
+
registry: stack.registry,
|
|
269
|
+
tenantId: TENANT_A,
|
|
270
|
+
userId: uuid(1005),
|
|
271
|
+
},
|
|
251
272
|
"delete",
|
|
252
273
|
),
|
|
253
274
|
).resolves.toBeUndefined();
|
|
@@ -269,7 +290,7 @@ describe("S2.H2 :: fileRefExportHook", () => {
|
|
|
269
290
|
await seedFileRef(uuid(102), TENANT_A, "user-files-1", "anschreiben.pdf");
|
|
270
291
|
|
|
271
292
|
const result = await fileRefExportHook({
|
|
272
|
-
db: stack.db,
|
|
293
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
273
294
|
registry: stack.registry,
|
|
274
295
|
tenantId: TENANT_A,
|
|
275
296
|
userId: "user-files-1",
|
|
@@ -284,7 +305,7 @@ describe("S2.H2 :: fileRefExportHook", () => {
|
|
|
284
305
|
|
|
285
306
|
test("returns null wenn User keine Files hat", async () => {
|
|
286
307
|
const result = await fileRefExportHook({
|
|
287
|
-
db: stack.db,
|
|
308
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
288
309
|
registry: stack.registry,
|
|
289
310
|
tenantId: TENANT_A,
|
|
290
311
|
userId: "ghost-user-no-files",
|
|
@@ -299,7 +320,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
|
|
|
299
320
|
await seedFileRef(uuid(202), TENANT_A, "user-delete-files", "f2.pdf");
|
|
300
321
|
|
|
301
322
|
await fileRefDeleteHook(
|
|
302
|
-
{
|
|
323
|
+
{
|
|
324
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
325
|
+
registry: stack.registry,
|
|
326
|
+
tenantId: TENANT_A,
|
|
327
|
+
userId: "user-delete-files",
|
|
328
|
+
},
|
|
303
329
|
"delete",
|
|
304
330
|
);
|
|
305
331
|
|
|
@@ -311,7 +337,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
|
|
|
311
337
|
await seedFileRef(uuid(203), TENANT_A, "user-anon-files", "shared.pdf");
|
|
312
338
|
|
|
313
339
|
await fileRefDeleteHook(
|
|
314
|
-
{
|
|
340
|
+
{
|
|
341
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
342
|
+
registry: stack.registry,
|
|
343
|
+
tenantId: TENANT_A,
|
|
344
|
+
userId: "user-anon-files",
|
|
345
|
+
},
|
|
315
346
|
"anonymize",
|
|
316
347
|
);
|
|
317
348
|
|
|
@@ -329,7 +360,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
|
|
|
329
360
|
|
|
330
361
|
// Tenant A loescht alle Files von "shared-user"
|
|
331
362
|
await fileRefDeleteHook(
|
|
332
|
-
{
|
|
363
|
+
{
|
|
364
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
365
|
+
registry: stack.registry,
|
|
366
|
+
tenantId: TENANT_A,
|
|
367
|
+
userId: "shared-user",
|
|
368
|
+
},
|
|
333
369
|
"delete",
|
|
334
370
|
);
|
|
335
371
|
|
|
@@ -345,7 +381,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
|
|
|
345
381
|
await seedFileRef(uuid(401), TENANT_A, "user-idem-files", "f.pdf");
|
|
346
382
|
|
|
347
383
|
await fileRefDeleteHook(
|
|
348
|
-
{
|
|
384
|
+
{
|
|
385
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
386
|
+
registry: stack.registry,
|
|
387
|
+
tenantId: TENANT_A,
|
|
388
|
+
userId: "user-idem-files",
|
|
389
|
+
},
|
|
349
390
|
"delete",
|
|
350
391
|
);
|
|
351
392
|
const afterFirst = await fetchFileRefs(TENANT_A, "user-idem-files");
|
|
@@ -354,7 +395,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
|
|
|
354
395
|
// Zweiter Call: kein Crash + State weiter 0 Files
|
|
355
396
|
await expect(
|
|
356
397
|
fileRefDeleteHook(
|
|
357
|
-
{
|
|
398
|
+
{
|
|
399
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
400
|
+
registry: stack.registry,
|
|
401
|
+
tenantId: TENANT_A,
|
|
402
|
+
userId: "user-idem-files",
|
|
403
|
+
},
|
|
358
404
|
"delete",
|
|
359
405
|
),
|
|
360
406
|
).resolves.toBeUndefined();
|
|
@@ -398,7 +444,7 @@ describe("S2.H2 :: fileRefDeleteHook — GDPR derivatives survive forget (issue
|
|
|
398
444
|
|
|
399
445
|
await fileRefDeleteHook(
|
|
400
446
|
{
|
|
401
|
-
db: stack.db,
|
|
447
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
402
448
|
registry: stack.registry,
|
|
403
449
|
tenantId: TENANT_A,
|
|
404
450
|
userId,
|
|
@@ -428,7 +474,7 @@ describe("S2.H2 :: fileRefDeleteHook — GDPR derivatives survive forget (issue
|
|
|
428
474
|
|
|
429
475
|
await fileRefDeleteHook(
|
|
430
476
|
{
|
|
431
|
-
db: stack.db,
|
|
477
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
432
478
|
registry: stack.registry,
|
|
433
479
|
tenantId: TENANT_A,
|
|
434
480
|
userId,
|
|
@@ -459,7 +505,7 @@ describe("S2.H2 :: fileRefDeleteHook — GDPR derivatives survive forget (issue
|
|
|
459
505
|
await expect(
|
|
460
506
|
fileRefDeleteHook(
|
|
461
507
|
{
|
|
462
|
-
db: stack.db,
|
|
508
|
+
db: createTenantDb(stack.db, TENANT_A, "tenant"),
|
|
463
509
|
registry: stack.registry,
|
|
464
510
|
tenantId: TENANT_A,
|
|
465
511
|
userId,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { apiTokenTable } from "../../personal-access-tokens";
|
|
4
3
|
import { featureMounted } from "./feature-mounted";
|
|
@@ -11,7 +10,7 @@ import { featureMounted } from "./feature-mounted";
|
|
|
11
10
|
|
|
12
11
|
export const apiTokenExportHook: UserDataExportHook = async (ctx) => {
|
|
13
12
|
if (!featureMounted(ctx, "personal-access-tokens")) return null;
|
|
14
|
-
const rows = await
|
|
13
|
+
const rows = await ctx.db.selectMany(apiTokenTable, {
|
|
15
14
|
userId: ctx.userId,
|
|
16
15
|
tenantId: ctx.tenantId,
|
|
17
16
|
});
|
|
@@ -33,5 +32,5 @@ export const apiTokenExportHook: UserDataExportHook = async (ctx) => {
|
|
|
33
32
|
export const apiTokenDeleteHook: UserDataDeleteHook = async (ctx) => {
|
|
34
33
|
// skip: personal-access-tokens not mounted — its table doesn't exist, nothing to erase.
|
|
35
34
|
if (!featureMounted(ctx, "personal-access-tokens")) return;
|
|
36
|
-
await
|
|
35
|
+
await ctx.db.deleteMany(apiTokenTable, { userId: ctx.userId, tenantId: ctx.tenantId });
|
|
37
36
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
2
|
import {
|
|
4
3
|
createSystemUser,
|
|
5
4
|
type UserDataDeleteHook,
|
|
@@ -21,7 +20,7 @@ const crud = createEventStoreExecutor(configValuesTable, configValueEntity, {
|
|
|
21
20
|
|
|
22
21
|
export const configValueExportHook: UserDataExportHook = async (ctx) => {
|
|
23
22
|
if (!featureMounted(ctx, "config")) return null;
|
|
24
|
-
const rows = await selectMany<Record<string, unknown>>(
|
|
23
|
+
const rows = await ctx.db.selectMany<Record<string, unknown>>(configValuesTable, {
|
|
25
24
|
tenantId: ctx.tenantId,
|
|
26
25
|
userId: ctx.userId,
|
|
27
26
|
});
|
|
@@ -39,14 +38,13 @@ export const configValueDeleteHook: UserDataDeleteHook = async (ctx) => {
|
|
|
39
38
|
// skip: config not mounted — its table doesn't exist, nothing to erase.
|
|
40
39
|
if (!featureMounted(ctx, "config")) return;
|
|
41
40
|
const systemUser = createSystemUser(ctx.tenantId);
|
|
42
|
-
const
|
|
43
|
-
const rows = await selectMany<Record<string, unknown>>(ctx.db, configValuesTable, {
|
|
41
|
+
const rows = await ctx.db.selectMany<Record<string, unknown>>(configValuesTable, {
|
|
44
42
|
tenantId: ctx.tenantId,
|
|
45
43
|
userId: ctx.userId,
|
|
46
44
|
});
|
|
47
45
|
for (const row of rows) {
|
|
48
46
|
const id = row["id"]; // @cast-boundary db-row
|
|
49
47
|
if (typeof id !== "string") continue;
|
|
50
|
-
assertErased(await crud.forget({ id }, systemUser,
|
|
48
|
+
assertErased(await crud.forget({ id }, systemUser, ctx.db), "config-value", id);
|
|
51
49
|
}
|
|
52
50
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { deliveryAttemptsTable } from "../../delivery";
|
|
4
3
|
import { featureMounted } from "./feature-mounted";
|
|
@@ -11,7 +10,7 @@ import { featureMounted } from "./feature-mounted";
|
|
|
11
10
|
|
|
12
11
|
export const deliveryAttemptExportHook: UserDataExportHook = async (ctx) => {
|
|
13
12
|
if (!featureMounted(ctx, "delivery")) return null;
|
|
14
|
-
const rows = await
|
|
13
|
+
const rows = await ctx.db.selectMany(deliveryAttemptsTable, {
|
|
15
14
|
tenantId: ctx.tenantId,
|
|
16
15
|
recipientId: ctx.userId,
|
|
17
16
|
});
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
createEventStoreExecutor,
|
|
4
|
-
createTenantDb,
|
|
5
|
-
type TenantDb,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
7
2
|
import { derivativeListPrefix, isDerivativeKeyOf } from "@cosmicdrift/kumiko-framework/derivatives";
|
|
8
3
|
import {
|
|
9
4
|
createSystemUser,
|
|
@@ -69,7 +64,7 @@ const crud = createEventStoreExecutor(fileRefsTable, fileRefEntity, { entityName
|
|
|
69
64
|
export const fileRefExportHook: UserDataExportHook = async (ctx) => {
|
|
70
65
|
// isDeleted:false — soft-deleted (trashed) Files gehören nicht ins
|
|
71
66
|
// Auskunfts-Bundle. Forget (delete-Hook unten) erfasst sie trotzdem.
|
|
72
|
-
const rawRows = await
|
|
67
|
+
const rawRows = await ctx.db.selectMany(fileRefsTable, {
|
|
73
68
|
tenantId: ctx.tenantId,
|
|
74
69
|
insertedById: ctx.userId,
|
|
75
70
|
isDeleted: false,
|
|
@@ -218,8 +213,7 @@ async function severPersonLink(
|
|
|
218
213
|
|
|
219
214
|
export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
|
|
220
215
|
const systemUser = createSystemUser(ctx.tenantId);
|
|
221
|
-
const
|
|
222
|
-
const rows = await selectMany<Record<string, unknown>>(ctx.db, fileRefsTable, {
|
|
216
|
+
const rows = await ctx.db.selectMany<Record<string, unknown>>(fileRefsTable, {
|
|
223
217
|
tenantId: ctx.tenantId,
|
|
224
218
|
insertedById: ctx.userId,
|
|
225
219
|
});
|
|
@@ -227,7 +221,7 @@ export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
|
|
|
227
221
|
if (strategy !== "delete") {
|
|
228
222
|
// anonymize: insertedById=null, FileRef + binary bleiben. Use-case: shared
|
|
229
223
|
// chat-Attachment im Multi-User-Channel — Author-ID raus, Datei bleibt sichtbar.
|
|
230
|
-
await severPersonLink(
|
|
224
|
+
await severPersonLink(ctx.db, systemUser, rows);
|
|
231
225
|
// skip: anonymize is complete — the hard-delete path below runs only for strategy "delete".
|
|
232
226
|
return;
|
|
233
227
|
}
|
|
@@ -259,6 +253,6 @@ export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
|
|
|
259
253
|
for (const row of rows) {
|
|
260
254
|
const id = row["id"]; // @cast-boundary db-row
|
|
261
255
|
if (typeof id !== "string") continue;
|
|
262
|
-
assertErased(await crud.forget({ id }, systemUser,
|
|
256
|
+
assertErased(await crud.forget({ id }, systemUser, ctx.db), "fileRef", id);
|
|
263
257
|
}
|
|
264
258
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { inAppMessagesTable } from "../../channel-in-app";
|
|
4
3
|
import { featureMounted } from "./feature-mounted";
|
|
@@ -14,7 +13,7 @@ import { featureMounted } from "./feature-mounted";
|
|
|
14
13
|
|
|
15
14
|
export const inAppMessageExportHook: UserDataExportHook = async (ctx) => {
|
|
16
15
|
if (!featureMounted(ctx, "channel-in-app")) return null;
|
|
17
|
-
const rows = await
|
|
16
|
+
const rows = await ctx.db.selectMany(inAppMessagesTable, {
|
|
18
17
|
userId: ctx.userId,
|
|
19
18
|
tenantId: ctx.tenantId,
|
|
20
19
|
});
|
|
@@ -34,5 +33,5 @@ export const inAppMessageExportHook: UserDataExportHook = async (ctx) => {
|
|
|
34
33
|
export const inAppMessageDeleteHook: UserDataDeleteHook = async (ctx) => {
|
|
35
34
|
// skip: channel-in-app not mounted — its table doesn't exist, nothing to erase.
|
|
36
35
|
if (!featureMounted(ctx, "channel-in-app")) return;
|
|
37
|
-
await
|
|
36
|
+
await ctx.db.deleteMany(inAppMessagesTable, { userId: ctx.userId, tenantId: ctx.tenantId });
|
|
38
37
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { jobRunsTable } from "../../jobs";
|
|
4
3
|
import { featureMounted } from "./feature-mounted";
|
|
@@ -11,7 +10,7 @@ import { featureMounted } from "./feature-mounted";
|
|
|
11
10
|
|
|
12
11
|
export const jobRunExportHook: UserDataExportHook = async (ctx) => {
|
|
13
12
|
if (!featureMounted(ctx, "jobs")) return null;
|
|
14
|
-
const rows = await selectMany<Record<string, unknown>>(
|
|
13
|
+
const rows = await ctx.db.selectMany<Record<string, unknown>>(jobRunsTable, {
|
|
15
14
|
triggeredById: ctx.userId,
|
|
16
15
|
});
|
|
17
16
|
if (rows.length === 0) return null;
|