@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.
- package/package.json +9 -9
- package/src/agent-tools/__tests__/agent-manifest.test.ts +2 -2
- package/src/agent-tools/__tests__/agent-optout.test.ts +22 -3
- package/src/agent-tools/__tests__/agent-tools.integration.test.ts +7 -1
- package/src/agent-tools/__tests__/tool-catalog.test.ts +11 -4
- package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +13 -2
- package/src/audit/__tests__/audit-security.integration.test.ts +1 -1
- package/src/audit/__tests__/audit.integration.test.ts +2 -2
- package/src/billing-foundation/entities.ts +30 -5
- package/src/cap-counter/entity.ts +8 -1
- package/src/config/table.ts +4 -2
- package/src/crypto-shredding/__tests__/forget-subject-record.integration.test.ts +141 -1
- package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +1 -1
- package/src/crypto-shredding/constants.ts +1 -0
- package/src/crypto-shredding/handlers/forget-subject.write.ts +42 -0
- package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/cross-tenant-field-delete.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/cross-tenant-set-write.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/quota.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/retention.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +1 -1
- package/src/custom-fields/entity.ts +24 -4
- package/src/data-retention/__tests__/resolver.test.ts +6 -4
- package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +12 -4
- package/src/delivery/__tests__/delivery.integration.test.ts +3 -3
- package/src/delivery/tables.ts +6 -2
- package/src/document-ingest-foundation/entity.ts +6 -2
- package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +12 -2
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -1
- package/src/folders/__tests__/folders-parent-visibility.integration.test.ts +12 -2
- package/src/folders/__tests__/folders.integration.test.ts +8 -1
- package/src/folders/entity.ts +27 -5
- package/src/folders-user-data/__tests__/hooks.integration.test.ts +8 -1
- package/src/form-draft/entity.ts +6 -1
- package/src/inbound-mail-foundation/entities.ts +57 -12
- package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +111 -1
- package/src/jobs/__tests__/projection-rebuild-job.integration.test.ts +2 -2
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +6 -1
- package/src/jobs/db/queries/stale-run-sweep.ts +36 -14
- package/src/jobs/feature.ts +5 -0
- package/src/jobs/job-run-logger.ts +7 -2
- package/src/jobs/job-run-table.ts +11 -6
- package/src/ledger/entity.ts +27 -7
- package/src/managed-pages/screens/branding-screen.ts +27 -5
- package/src/managed-pages/table.ts +23 -4
- package/src/notes-history/__tests__/add-note-author-name-kms.integration.test.ts +8 -1
- package/src/notes-history/__tests__/notes-history-ownership.integration.test.ts +8 -1
- package/src/notes-history/__tests__/notes-history-parent-visibility.integration.test.ts +20 -3
- package/src/notes-history/__tests__/notes-history.integration.test.ts +8 -1
- package/src/notes-history/entity.ts +12 -2
- package/src/notes-history-user-data/__tests__/hooks.integration.test.ts +8 -1
- package/src/personal-access-tokens/schema/api-token.ts +29 -1
- package/src/secrets/table.ts +1 -1
- package/src/sessions/schema/user-session.ts +4 -0
- package/src/tags/__tests__/tags-parent-visibility.integration.test.ts +12 -2
- package/src/tags/__tests__/tags.integration.test.ts +8 -1
- package/src/tags/entity.ts +20 -5
- package/src/template-resolver/table.ts +8 -7
- package/src/template-resolver/user-content-table.ts +6 -4
- package/src/tenant/invitation-table.ts +6 -1
- package/src/tenant/membership-table.ts +2 -2
- package/src/tenant/schema/tenant.ts +22 -3
- package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +1 -1
- package/src/tier-engine/entity.ts +12 -2
- package/src/user/schema/user.ts +15 -1
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +2 -2
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +1 -1
- package/src/user-data-rights/__tests__/forget-cleanup-search-purge-ordering.integration.test.ts +1 -1
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +3 -1
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +3 -1
- package/src/user-data-rights/schema/download-attempt.ts +25 -7
- package/src/user-data-rights/schema/download-token.ts +4 -0
- 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({
|
|
29
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
40
|
+
source: createTextField({
|
|
41
|
+
required: false,
|
|
42
|
+
maxLength: 20,
|
|
43
|
+
personal: false,
|
|
44
|
+
reason: "technical_reference",
|
|
45
|
+
}),
|
|
36
46
|
},
|
|
37
47
|
});
|
|
38
48
|
|
package/src/user/schema/user.ts
CHANGED
|
@@ -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
|
});
|
package/src/user-data-rights/__tests__/forget-cleanup-search-purge-ordering.integration.test.ts
CHANGED
|
@@ -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: {
|
|
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: {
|
|
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({
|
|
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({
|
|
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
|
-
|
|
29
|
-
|
|
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
|