@cosmicdrift/kumiko-bundled-features 0.252.1 → 0.253.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 (76) hide show
  1. package/package.json +9 -9
  2. package/src/agent-tools/__tests__/agent-manifest.test.ts +2 -2
  3. package/src/agent-tools/__tests__/agent-optout.test.ts +22 -3
  4. package/src/agent-tools/__tests__/agent-tools.integration.test.ts +7 -1
  5. package/src/agent-tools/__tests__/tool-catalog.test.ts +11 -4
  6. package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +13 -2
  7. package/src/audit/__tests__/audit-security.integration.test.ts +1 -1
  8. package/src/audit/__tests__/audit.integration.test.ts +2 -2
  9. package/src/billing-foundation/entities.ts +30 -5
  10. package/src/cap-counter/entity.ts +8 -1
  11. package/src/config/table.ts +4 -2
  12. package/src/crypto-shredding/__tests__/forget-subject-record.integration.test.ts +141 -1
  13. package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +1 -1
  14. package/src/crypto-shredding/constants.ts +1 -0
  15. package/src/crypto-shredding/handlers/forget-subject.write.ts +42 -0
  16. package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
  17. package/src/custom-fields/__tests__/cross-tenant-field-delete.integration.test.ts +1 -1
  18. package/src/custom-fields/__tests__/cross-tenant-set-write.integration.test.ts +1 -1
  19. package/src/custom-fields/__tests__/custom-fields.integration.test.ts +1 -1
  20. package/src/custom-fields/__tests__/field-access.integration.test.ts +1 -1
  21. package/src/custom-fields/__tests__/quota.integration.test.ts +1 -1
  22. package/src/custom-fields/__tests__/retention.integration.test.ts +1 -1
  23. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +1 -1
  24. package/src/custom-fields/__tests__/wire-for-entity.test.ts +1 -1
  25. package/src/custom-fields/entity.ts +24 -4
  26. package/src/data-retention/__tests__/resolver.test.ts +6 -4
  27. package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +12 -4
  28. package/src/delivery/__tests__/delivery.integration.test.ts +3 -3
  29. package/src/delivery/tables.ts +6 -2
  30. package/src/document-ingest-foundation/entity.ts +6 -2
  31. package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +12 -2
  32. package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -1
  33. package/src/folders/__tests__/folders-parent-visibility.integration.test.ts +12 -2
  34. package/src/folders/__tests__/folders.integration.test.ts +8 -1
  35. package/src/folders/entity.ts +27 -5
  36. package/src/folders-user-data/__tests__/hooks.integration.test.ts +8 -1
  37. package/src/form-draft/entity.ts +6 -1
  38. package/src/inbound-mail-foundation/entities.ts +57 -12
  39. package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +111 -1
  40. package/src/jobs/__tests__/projection-rebuild-job.integration.test.ts +2 -2
  41. package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +6 -1
  42. package/src/jobs/db/queries/stale-run-sweep.ts +36 -14
  43. package/src/jobs/feature.ts +5 -0
  44. package/src/jobs/job-run-logger.ts +7 -2
  45. package/src/jobs/job-run-table.ts +11 -6
  46. package/src/ledger/entity.ts +27 -7
  47. package/src/managed-pages/screens/branding-screen.ts +27 -5
  48. package/src/managed-pages/table.ts +23 -4
  49. package/src/notes-history/__tests__/add-note-author-name-kms.integration.test.ts +8 -1
  50. package/src/notes-history/__tests__/notes-history-ownership.integration.test.ts +8 -1
  51. package/src/notes-history/__tests__/notes-history-parent-visibility.integration.test.ts +20 -3
  52. package/src/notes-history/__tests__/notes-history.integration.test.ts +8 -1
  53. package/src/notes-history/entity.ts +12 -2
  54. package/src/notes-history-user-data/__tests__/hooks.integration.test.ts +8 -1
  55. package/src/personal-access-tokens/schema/api-token.ts +29 -1
  56. package/src/secrets/table.ts +1 -1
  57. package/src/sessions/schema/user-session.ts +4 -0
  58. package/src/tags/__tests__/tags-parent-visibility.integration.test.ts +12 -2
  59. package/src/tags/__tests__/tags.integration.test.ts +8 -1
  60. package/src/tags/entity.ts +20 -5
  61. package/src/template-resolver/table.ts +8 -7
  62. package/src/template-resolver/user-content-table.ts +6 -4
  63. package/src/tenant/invitation-table.ts +6 -1
  64. package/src/tenant/membership-table.ts +2 -2
  65. package/src/tenant/schema/tenant.ts +22 -3
  66. package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +1 -1
  67. package/src/tier-engine/entity.ts +12 -2
  68. package/src/user/schema/user.ts +15 -1
  69. package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +2 -2
  70. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +1 -1
  71. package/src/user-data-rights/__tests__/forget-cleanup-search-purge-ordering.integration.test.ts +1 -1
  72. package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +3 -1
  73. package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +3 -1
  74. package/src/user-data-rights/schema/download-attempt.ts +25 -7
  75. package/src/user-data-rights/schema/download-token.ts +4 -0
  76. package/src/user-data-rights/schema/export-job.ts +6 -0
@@ -18,11 +18,11 @@ import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/eng
18
18
  export const tenantMembershipEntity = createEntity({
19
19
  table: "read_tenant_memberships",
20
20
  fields: {
21
- userId: createTextField({ required: true }),
21
+ userId: createTextField({ required: true, personal: false, reason: "pseudonymous_fk" }),
22
22
  // JSON-encoded string[] — parseRoles() deserializes at read time.
23
23
  // Mirrors how roles were stored under the pre-ES row model so the
24
24
  // read-side stays byte-compatible and no MSP/consumer needs rewrites.
25
- roles: createTextField({ required: true }),
25
+ roles: createTextField({ required: true, personal: false, reason: "technical_reference" }),
26
26
  },
27
27
  indexes: [
28
28
  { unique: true, columns: ["userId", "tenantId"], name: "read_tenant_memberships_unique" },
@@ -25,8 +25,23 @@ export const tenantEntity = createEntity({
25
25
  // Alle tenantId-Spalten sind UUID (Migration 2026-04-16) → tenant.id muss
26
26
  // UUID sein, sonst findet der tenants-Lookup nie. Default gen_random_uuid().
27
27
  fields: {
28
- key: createTextField({ required: true, maxLength: 50 }),
29
- name: createTextField({ required: true, maxLength: 200, searchable: true, sortable: true }),
28
+ key: createTextField({
29
+ required: true,
30
+ maxLength: 50,
31
+ personal: false,
32
+ reason: "technical_reference",
33
+ }),
34
+ // Workspace/company display name, not an individual's identity —
35
+ // same class as the `company.legalName` example in the boot-validator's
36
+ // own PII-heuristic doc-comment.
37
+ name: createTextField({
38
+ required: true,
39
+ maxLength: 200,
40
+ searchable: true,
41
+ sortable: true,
42
+ personal: false,
43
+ reason: "is_business_data",
44
+ }),
30
45
  isEnabled: createBooleanField({ default: true, filterable: true }),
31
46
  // Tenant-destroy lifecycle (tenant-lifecycle feature). Defaults keep
32
47
  // existing tenants valid when the feature is not mounted.
@@ -37,7 +52,11 @@ export const tenantEntity = createEntity({
37
52
  filterable: true,
38
53
  }),
39
54
  destroyRequestedAt: createTimestampField({}),
40
- destroyRequestedBy: createTextField({ maxLength: 36 }),
55
+ destroyRequestedBy: createTextField({
56
+ maxLength: 36,
57
+ personal: false,
58
+ reason: "pseudonymous_fk",
59
+ }),
41
60
  gracePeriodEnd: createTimestampField({}),
42
61
  destroyStartedAt: createTimestampField({}),
43
62
  destroyedAt: createTimestampField({}),
@@ -30,7 +30,7 @@ import { defineCreateWithTenantDefaults } from "../tenant-defaults";
30
30
  const invoiceEntity = createEntity({
31
31
  table: "read_invoices",
32
32
  fields: {
33
- title: createTextField({ required: true }),
33
+ title: createTextField({ required: true, personal: false, reason: "is_business_data" }),
34
34
  amount: createMoneyField({ required: true }),
35
35
  language: createSelectField({ options: ["en", "de"] as const }),
36
36
  },
@@ -27,12 +27,22 @@ export const tierAssignmentEntity = createEntity({
27
27
  description:
28
28
  "The pricing tier currently assigned to a tenant, plus where the assignment came from (manual grant, billing sync or signup default) — one row per tenant.",
29
29
  fields: {
30
- tier: createTextField({ required: true, maxLength: 50 }),
30
+ tier: createTextField({
31
+ required: true,
32
+ maxLength: 50,
33
+ personal: false,
34
+ reason: "catalog_label",
35
+ }),
31
36
  // Woher das Assignment stammt: "manual" (Admin-Grant via tier-admin-Screen),
32
37
  // "stripe" (future Billing-Sync), "default" (auto-default-on-signup-Hook).
33
38
  // Optional für Back-Compat zu bestehenden Rows ohne source. Schützt manuelle
34
39
  // Grants davor, von einem späteren Stripe→Tier-Sync geplättet zu werden.
35
- source: createTextField({ required: false, maxLength: 20 }),
40
+ source: createTextField({
41
+ required: false,
42
+ maxLength: 20,
43
+ personal: false,
44
+ reason: "technical_reference",
45
+ }),
36
46
  },
37
47
  });
38
48
 
@@ -81,9 +81,19 @@ export const userEntity = createEntity({
81
81
  // Password material: only SYSTEM/SystemAdmin can read or write it.
82
82
  // auth-email-password reads it during login, writes it during registration
83
83
  // and password changes. Stripped from ordinary responses via read-access.
84
+ // Kept Klasse 1 deliberately: find-for-auth.query.ts reads this row via a
85
+ // raw fetchOne (bypassing the executor's decrypt on purpose, so a
86
+ // SystemAdmin session's ordinary query path never sees it) — a subject
87
+ // annotation would make the executor encrypt it on write while this
88
+ // login-critical read stays raw, so verifyPassword would compare a
89
+ // plaintext password against ciphertext and every login would fail once
90
+ // a KMS is active. Already an irreversible one-way hash, access-gated to
91
+ // privileged read/write — see PR body "Offene Fragen".
84
92
  passwordHash: createTextField({
85
93
  maxLength: 255,
86
94
  access: { read: access.privileged, write: access.privileged },
95
+ personal: false,
96
+ reason: "credential_hash_read_without_decrypt_2809",
87
97
  }),
88
98
 
89
99
  // Profile — user-editable. Real name in most apps → PII. Searchable via
@@ -95,7 +105,7 @@ export const userEntity = createEntity({
95
105
  find: "fuzzy",
96
106
  }),
97
107
  // No default here — tenant-settings owns the app-wide locale default; see #1637.
98
- locale: createTextField({ maxLength: 10 }),
108
+ locale: createTextField({ maxLength: 10, personal: false, reason: "technical_reference" }),
99
109
  // No default here — mirrors locale: ctx.tz falls back to tenant.timezone
100
110
  // (then "UTC") when unset, see buildHandlerContext (fw#1636).
101
111
  timezone: createTzField(),
@@ -106,6 +116,8 @@ export const userEntity = createEntity({
106
116
  lastActiveTenantId: createTextField({
107
117
  maxLength: 36,
108
118
  access: { write: access.privileged },
119
+ personal: false,
120
+ reason: "technical_reference",
109
121
  }),
110
122
 
111
123
  // Email-verification flag — flipped to true by the verify-email handler
@@ -171,6 +183,8 @@ export const userEntity = createEntity({
171
183
  pendingDeletionRequestId: createTextField({
172
184
  maxLength: 36,
173
185
  access: { write: access.privileged },
186
+ personal: false,
187
+ reason: "technical_reference",
174
188
  }),
175
189
  },
176
190
  // fw#2134 — email is `personal: "self", find: "exact"`, so the column holds
@@ -37,8 +37,8 @@ const cipher = createTestEnvelopeCipher();
37
37
  const vaultNoteEntity = createEntity({
38
38
  table: "read_vault_notes",
39
39
  fields: {
40
- ownerId: createTextField({ required: true }),
41
- note: createTextField({ encrypted: true }),
40
+ ownerId: createTextField({ required: true, personal: false, reason: "pseudonymous_fk" }),
41
+ note: createTextField({ encrypted: true, personal: false, reason: "technical_reference" }),
42
42
  },
43
43
  });
44
44
 
@@ -101,7 +101,7 @@ function makeEntity(tableName: string) {
101
101
  return createEntity({
102
102
  table: tableName,
103
103
  fields: {
104
- name: createTextField({ required: true }),
104
+ name: createTextField({ required: true, personal: false, reason: "technical_reference" }),
105
105
  customFields: customFieldsField(),
106
106
  },
107
107
  });
@@ -43,7 +43,7 @@ const NOTE_ID = "bbbbbbbb-bbbb-4bbb-8bbb-0000000000f1";
43
43
  const noteEntity = createEntity({
44
44
  table: "read_forget_purge_notes",
45
45
  fields: {
46
- authorId: createTextField({ required: true }),
46
+ authorId: createTextField({ required: true, personal: false, reason: "pseudonymous_fk" }),
47
47
  body: createTextField({
48
48
  required: true,
49
49
  personal: { of: "authorId" },
@@ -46,7 +46,9 @@ const captureHook: UserDataDeleteHook = async (ctx) => {
46
46
 
47
47
  const probeEntity = createEntity({
48
48
  table: `read_${PROBE.replace(/-/g, "_")}`,
49
- fields: { name: createTextField({ required: true }) },
49
+ fields: {
50
+ name: createTextField({ required: true, personal: false, reason: "technical_reference" }),
51
+ },
50
52
  });
51
53
 
52
54
  const probeFeature = defineFeature(PROBE, (r) => {
@@ -72,7 +72,9 @@ const tenantScopedDeleteHook: UserDataDeleteHook = async (ctx) => {
72
72
 
73
73
  const scopedEntity = createEntity({
74
74
  table: TABLE,
75
- fields: { name: createTextField({ required: true }) },
75
+ fields: {
76
+ name: createTextField({ required: true, personal: false, reason: "technical_reference" }),
77
+ },
76
78
  });
77
79
 
78
80
  const contributorFeature = defineFeature("dsgvo-tenant-scoped", (r) => {
@@ -16,17 +16,35 @@ export const downloadAttemptEntity = createEntity({
16
16
  "One rejected attempt to fetch a GDPR data-export download, recording the failure result, the path used (magic-link token or logged-in job id), the source IP, user agent and time, so brute-force probing of download links is visible.",
17
17
  fields: {
18
18
  // notFound | expired | failed | signedUrlNotSupported
19
- result: createTextField({ required: true, maxLength: 32 }),
19
+ result: createTextField({
20
+ required: true,
21
+ maxLength: 32,
22
+ personal: false,
23
+ reason: "technical_reference",
24
+ }),
20
25
  // Welcher Pfad: "token" | "job"
21
- via: createTextField({ required: true, maxLength: 16 }),
26
+ via: createTextField({
27
+ required: true,
28
+ maxLength: 16,
29
+ personal: false,
30
+ reason: "technical_reference",
31
+ }),
22
32
  // Token-Hash (token-Pfad) oder NULL (job-Pfad / unbekannter Token).
23
- tokenHash: createTextField({ maxLength: 64 }),
33
+ tokenHash: createTextField({ maxLength: 64, personal: false, reason: "technical_reference" }),
24
34
  // Job-ID wenn der attempt einen kannte. NULL bei unbekanntem Token.
25
- jobId: createTextField({}),
35
+ jobId: createTextField({ personal: false, reason: "technical_reference" }),
26
36
  // User-ID wenn auth-Pfad (job). NULL bei anonymous (token-Pfad).
27
- attemptedByUserId: createTextField({}),
28
- ip: createTextField({ maxLength: 64 }),
29
- userAgent: createTextField({ maxLength: 256 }),
37
+ attemptedByUserId: createTextField({ personal: false, reason: "pseudonymous_fk" }),
38
+ // Same reasoning as download-token.ts's lastUsedFromIp/lastUsedUserAgent
39
+ // (sibling entity, same feature): DPO brute-force-audit data belongs to
40
+ // the tenant operator, not the (often anonymous) requester. Also avoids
41
+ // a real hazard on this buildEntityTable-managed entity: attemptedByUserId
42
+ // is null-by-design for the anonymous token path, so a subject annotation
43
+ // (`personal: { of: "attemptedByUserId" }`) would throw
44
+ // SubjectResolutionError on every such row once a KMS is configured
45
+ // (crypto/subject-resolver.ts resolveUserOwnedSubject).
46
+ ip: createTextField({ maxLength: 64, personal: false, reason: "is_business_data" }),
47
+ userAgent: createTextField({ maxLength: 256, personal: false, reason: "is_business_data" }),
30
48
  attemptedAt: createTimestampField({ required: true }),
31
49
  },
32
50
  // 90d hardDelete: unbounded growth = disk-bomb genau gegen das System
@@ -45,6 +45,8 @@ export const exportDownloadTokenEntity = createEntity({
45
45
  // Job gehoeren.
46
46
  jobId: createTextField({
47
47
  required: true,
48
+ personal: false,
49
+ reason: "technical_reference",
48
50
  }),
49
51
 
50
52
  // **Hash NICHT plain.** SHA256-hex (64 chars). Atom 4b verifiziert
@@ -52,6 +54,8 @@ export const exportDownloadTokenEntity = createEntity({
52
54
  tokenHash: createTextField({
53
55
  required: true,
54
56
  maxLength: 64,
57
+ personal: false,
58
+ reason: "technical_reference",
55
59
  }),
56
60
 
57
61
  // Wann wurde das Token ausgegeben (= Job-Done-Flip-Zeit).
@@ -76,6 +76,8 @@ export const exportJobEntity = createEntity({
76
76
  userId: createTextField({
77
77
  required: true,
78
78
  searchable: true,
79
+ personal: false,
80
+ reason: "pseudonymous_fk",
79
81
  }),
80
82
 
81
83
  // **requestedFromTenantId** — der Tenant aus dem der User den Antrag
@@ -93,6 +95,8 @@ export const exportJobEntity = createEntity({
93
95
  // Tenant-Context ein falsches Profile.
94
96
  requestedFromTenantId: createTextField({
95
97
  required: true,
98
+ personal: false,
99
+ reason: "technical_reference",
96
100
  }),
97
101
 
98
102
  status: createSelectField({
@@ -123,6 +127,8 @@ export const exportJobEntity = createEntity({
123
127
  // signed-URL on demand vom Storage-Provider (Atom 4).
124
128
  downloadStorageKey: createTextField({
125
129
  maxLength: 500,
130
+ personal: false,
131
+ reason: "technical_reference",
126
132
  }),
127
133
 
128
134
  // Ab wann ist der Download nicht mehr abrufbar. Worker setzt