@cosmicdrift/kumiko-bundled-features 0.116.1 → 0.119.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.
Files changed (46) hide show
  1. package/package.json +7 -6
  2. package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +283 -0
  3. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +5 -1
  4. package/src/auth-email-password/handlers/invite-accept.write.ts +5 -2
  5. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +5 -1
  6. package/src/auth-email-password/handlers/token-request-handler.ts +4 -2
  7. package/src/channel-email/__tests__/pii-guard.test.ts +56 -0
  8. package/src/channel-email/email-channel.ts +2 -1
  9. package/src/channel-email/index.ts +1 -0
  10. package/src/channel-email/pii-guard.ts +36 -0
  11. package/src/config/table.ts +1 -1
  12. package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +148 -0
  13. package/src/crypto-shredding/constants.ts +5 -0
  14. package/src/crypto-shredding/feature.ts +18 -0
  15. package/src/crypto-shredding/handlers/forget-subject.write.ts +91 -0
  16. package/src/crypto-shredding/index.ts +11 -0
  17. package/src/delivery/tables.ts +1 -1
  18. package/src/mail-foundation/feature.ts +5 -2
  19. package/src/personal-access-tokens/__tests__/feature.test.ts +24 -0
  20. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +39 -1
  21. package/src/personal-access-tokens/feature.ts +11 -1
  22. package/src/personal-access-tokens/handlers/create.write.ts +21 -15
  23. package/src/personal-access-tokens/handlers/list.query.ts +12 -9
  24. package/src/personal-access-tokens/schema/api-token.ts +1 -1
  25. package/src/sessions/__tests__/sessions.integration.test.ts +49 -0
  26. package/src/sessions/feature.ts +2 -0
  27. package/src/sessions/handlers/list.query.ts +12 -9
  28. package/src/sessions/handlers/mine.query.ts +11 -8
  29. package/src/sessions/schema/user-session.ts +4 -2
  30. package/src/sessions/session-callbacks.ts +19 -10
  31. package/src/shared/decrypt-stored-pii.ts +19 -0
  32. package/src/shared/encrypt-for-direct-write.ts +23 -0
  33. package/src/shared/index.ts +2 -0
  34. package/src/tenant/handlers/invitations.query.ts +10 -2
  35. package/src/tenant/invitation-table.ts +3 -2
  36. package/src/user/schema/user.ts +5 -2
  37. package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +143 -0
  38. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +79 -0
  39. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +73 -0
  40. package/src/user-data-rights/handlers/deletion-grace-period.ts +4 -1
  41. package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +3 -1
  42. package/src/user-data-rights/run-export-jobs.ts +5 -1
  43. package/src/user-data-rights/run-forget-cleanup.ts +51 -3
  44. package/src/user-data-rights/run-user-export.ts +39 -12
  45. package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +6 -1
  46. package/src/user-profile/handlers/change-email.write.ts +3 -1
@@ -21,6 +21,11 @@
21
21
  // expliziert KEIN passwordHash + KEINE roles (privileged columns).
22
22
 
23
23
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
24
+ import {
25
+ configuredPiiSubjectKms,
26
+ decryptPiiFieldValues,
27
+ isPiiCiphertext,
28
+ } from "@cosmicdrift/kumiko-framework/crypto";
24
29
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
25
30
  import {
26
31
  collectEncryptedFieldNames,
@@ -223,20 +228,42 @@ async function decryptSnippetFields(
223
228
  hookEntityName: string,
224
229
  snippet: UserDataExportSnippet,
225
230
  ): Promise<UserDataExportSnippet> {
231
+ let rows = snippet.rows;
232
+
226
233
  const entity = registry.getEntity(snippet.entity) ?? registry.getEntity(hookEntityName);
227
- if (!entity) return snippet;
228
- const encryptedFields = collectEncryptedFieldNames(entity);
229
- if (encryptedFields.size === 0) return snippet;
230
-
231
- const cipher = configuredEntityFieldEncryption();
232
- const rows = await Promise.all(
233
- snippet.rows.map(async (row) => {
234
- if (cipher) return decryptEntityFieldValues(row, encryptedFields, cipher);
235
- const out = { ...row };
236
- for (const name of encryptedFields) {
237
- if (typeof out[name] === "string") out[name] = ENCRYPTED_UNAVAILABLE;
234
+ if (entity) {
235
+ const encryptedFields = collectEncryptedFieldNames(entity);
236
+ if (encryptedFields.size > 0) {
237
+ const cipher = configuredEntityFieldEncryption();
238
+ rows = await Promise.all(
239
+ rows.map(async (row) => {
240
+ if (cipher) return decryptEntityFieldValues(row, encryptedFields, cipher);
241
+ const out = { ...row };
242
+ for (const name of encryptedFields) {
243
+ if (typeof out[name] === "string") out[name] = ENCRYPTED_UNAVAILABLE;
244
+ }
245
+ return out;
246
+ }),
247
+ );
248
+ }
249
+ }
250
+
251
+ // PII-Subject-Ciphertexte (kumiko-pii:) sind self-describing — kein
252
+ // Entity-Kontext noetig, deckt jeden heutigen und kuenftigen Hook ab.
253
+ // Erased Subject → Sentinel (ehrlich: der Wert ist geshreddet).
254
+ const kms = configuredPiiSubjectKms();
255
+ rows = await Promise.all(
256
+ rows.map(async (row) => {
257
+ const piiKeys = Object.keys(row).filter((k) => isPiiCiphertext(row[k]));
258
+ if (piiKeys.length === 0) return row;
259
+ if (!kms) {
260
+ const out = { ...row };
261
+ for (const k of piiKeys) out[k] = ENCRYPTED_UNAVAILABLE;
262
+ return out;
238
263
  }
239
- return out;
264
+ return decryptPiiFieldValues(row, piiKeys, kms, {
265
+ requestId: "user-data-rights:export",
266
+ });
240
267
  }),
241
268
  );
242
269
  return { ...snippet, rows };
@@ -6,6 +6,7 @@ import {
6
6
  type UserDataExportHook,
7
7
  type UserDataHookCtx,
8
8
  } from "@cosmicdrift/kumiko-framework/engine";
9
+ import { decryptStoredPii } from "../../shared";
9
10
  import { tenantInvitationEntity, tenantInvitationsTable } from "../../tenant";
10
11
  import { userTable } from "../../user";
11
12
  import { featureMounted } from "./feature-mounted";
@@ -38,7 +39,11 @@ async function resolveUserEmail(ctx: UserDataHookCtx): Promise<string | null> {
38
39
  const row = (await fetchOne(ctx.db, userTable, { id: ctx.userId })) as {
39
40
  email: string;
40
41
  } | null; // @cast-boundary db-runner
41
- return row ? row.email.toLowerCase() : null;
42
+ if (!row) return null;
43
+ // Stored value is ciphertext with an active KMS; lowercasing a base64
44
+ // blob would break both lookup arms — decrypt first.
45
+ const email = await decryptStoredPii(row.email, "user-data-rights:invitation-hook");
46
+ return email.toLowerCase();
42
47
  }
43
48
 
44
49
  export const tenantInvitationExportHook: UserDataExportHook = async (ctx) => {
@@ -7,6 +7,7 @@ import {
7
7
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
8
8
  import { z } from "zod";
9
9
  import { AuthErrors, verifyPassword } from "../../auth-email-password";
10
+ import { decryptStoredPii } from "../../shared";
10
11
  import { UserErrors, UserHandlers, UserQueries } from "../../user";
11
12
  import { UserProfileErrors } from "../constants";
12
13
 
@@ -53,7 +54,8 @@ export const changeEmailWrite = defineWriteHandler({
53
54
  }
54
55
 
55
56
  const newEmail = event.payload.newEmail.toLowerCase();
56
- if (newEmail === me.email.toLowerCase()) {
57
+ const currentEmail = await decryptStoredPii(me.email, "user-profile:change-email");
58
+ if (newEmail === currentEmail.toLowerCase()) {
57
59
  return writeFailure(
58
60
  new UnprocessableError(UserProfileErrors.emailUnchanged, {
59
61
  i18nKey: "profile.errors.emailUnchanged",