@cosmicdrift/kumiko-bundled-features 0.145.1 → 0.146.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 +7 -6
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +15 -55
- package/src/custom-fields/__tests__/parse-serialized-field.test.ts +11 -3
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +24 -79
- package/src/custom-fields/db/queries/user-data-rights.ts +0 -20
- package/src/custom-fields/events.ts +4 -9
- package/src/custom-fields/feature.ts +8 -0
- package/src/custom-fields/handlers/set-custom-field.write.ts +3 -44
- package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -27
- package/src/custom-fields/lib/parse-serialized-field.ts +12 -4
- package/src/custom-fields/lib/value-schema.ts +1 -1
- package/src/custom-fields/schemas.ts +9 -9
- package/src/custom-fields/wire-for-entity.ts +8 -15
- package/src/custom-fields/wire-user-data-rights.ts +5 -54
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +199 -0
- package/src/inbound-mail-foundation/feature.ts +58 -0
- package/src/inbound-mail-foundation/retention-sweep.ts +155 -0
- package/src/managed-pages/__tests__/managed-pages.integration.test.ts +26 -0
- package/src/managed-pages/feature.ts +3 -1
- package/src/managed-pages/handlers/by-tenant-published.query.ts +52 -0
- package/src/page-render/__tests__/layout.test.ts +53 -0
- package/src/page-render/index.ts +1 -1
- package/src/page-render/layout.ts +15 -1
- package/src/seo/__tests__/llms-txt.test.ts +38 -0
- package/src/seo/__tests__/robots-txt.test.ts +18 -0
- package/src/seo/__tests__/schema-builders.test.ts +57 -0
- package/src/seo/__tests__/seo.integration.test.ts +258 -0
- package/src/seo/__tests__/sitemap.test.ts +38 -0
- package/src/seo/constants.ts +60 -0
- package/src/seo/feature.ts +313 -0
- package/src/seo/handlers/seo-config.query.ts +35 -0
- package/src/seo/index.ts +19 -0
- package/src/seo/llms-txt.ts +33 -0
- package/src/seo/robots-txt.ts +13 -0
- package/src/seo/schema-builders.ts +57 -0
- package/src/seo/sitemap.ts +30 -0
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +39 -62
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
// DSGVO forget hook-ordering —
|
|
2
|
-
// before any host hook that
|
|
3
|
-
// of feature registration
|
|
1
|
+
// DSGVO forget hook-ordering — a redaction hook with order
|
|
2
|
+
// EXT_USER_DATA_ORDER.REDACT_BEFORE_OWNER must run before any host hook that
|
|
3
|
+
// nulls the owner column it filters on, regardless of feature registration
|
|
4
|
+
// order. (Originally proven via the custom-fields sensitive strip; that strip
|
|
5
|
+
// was removed in #972, so this test pins the generic runner guarantee with a
|
|
6
|
+
// synthetic redact hook.)
|
|
4
7
|
//
|
|
5
8
|
// Two host entities are wired with the OPPOSITE EXT_USER_DATA registration
|
|
6
|
-
// order: one registers the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// This drives the full runner on purpose: the existing custom-fields
|
|
14
|
-
// user-data-rights test invokes the strip hook in isolation
|
|
15
|
-
// (getExtensionUsages + direct call) and is structurally blind to ordering.
|
|
9
|
+
// order: one registers the redact hook first (the order that happened to be
|
|
10
|
+
// safe), the other registers the owner-nulling host hook first (the order
|
|
11
|
+
// that exposed the bug). Both run through the real `runForgetCleanup` with
|
|
12
|
+
// strategy=anonymize. With the order-sentinel both strip the jsonb key;
|
|
13
|
+
// without it the host-first entity would leave it behind (the redact matches
|
|
14
|
+
// 0 rows after the owner column is nulled).
|
|
16
15
|
|
|
17
16
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
17
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
-
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
20
18
|
import {
|
|
21
19
|
createEntity,
|
|
22
20
|
createTextField,
|
|
23
21
|
defineFeature,
|
|
24
22
|
EXT_USER_DATA,
|
|
23
|
+
EXT_USER_DATA_ORDER,
|
|
25
24
|
type UserDataDeleteHook,
|
|
26
25
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
27
26
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
28
27
|
import {
|
|
29
|
-
createTestUser,
|
|
30
28
|
resetEventStore,
|
|
31
29
|
setupTestStack,
|
|
32
30
|
type TestStack,
|
|
@@ -34,10 +32,7 @@ import {
|
|
|
34
32
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
35
33
|
import { seedRow as seedProjectionRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
36
34
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
37
|
-
import { fieldDefinitionEntity } from "../../custom-fields/entity";
|
|
38
|
-
import { createCustomFieldsFeature } from "../../custom-fields/feature";
|
|
39
35
|
import { customFieldsField } from "../../custom-fields/wire-for-entity";
|
|
40
|
-
import { wireCustomFieldsUserDataRightsFor } from "../../custom-fields/wire-user-data-rights";
|
|
41
36
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
|
|
42
37
|
import { tenantRetentionOverrideTable } from "../../data-retention/schema/tenant-retention-override";
|
|
43
38
|
import { createSessionsFeature } from "../../sessions";
|
|
@@ -70,6 +65,20 @@ function makeHostDeleteHook(tableName: string): UserDataDeleteHook {
|
|
|
70
65
|
};
|
|
71
66
|
}
|
|
72
67
|
|
|
68
|
+
// Owner-keyed jsonb redaction with the REDACT_BEFORE_OWNER order sentinel —
|
|
69
|
+
// the shape the removed custom-fields strip had, kept synthetic here.
|
|
70
|
+
function makeRedactHook(tableName: string): UserDataDeleteHook {
|
|
71
|
+
return async (ctx, strategy) => {
|
|
72
|
+
// skip: delete strategy removes rows wholesale — redaction N/A.
|
|
73
|
+
if (strategy === "delete") return;
|
|
74
|
+
await asRawClient(ctx.db).unsafe(
|
|
75
|
+
`UPDATE ${tableName} SET custom_fields = custom_fields - 'ssn'
|
|
76
|
+
WHERE inserted_by_id = $1 AND tenant_id = $2`,
|
|
77
|
+
[ctx.userId, ctx.tenantId],
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
interface HostSpec {
|
|
74
83
|
readonly entityName: string;
|
|
75
84
|
readonly tableName: string;
|
|
@@ -99,19 +108,16 @@ function makeEntity(tableName: string) {
|
|
|
99
108
|
|
|
100
109
|
const cfFirstEntity = makeEntity(CF_FIRST.tableName);
|
|
101
110
|
const hostFirstEntity = makeEntity(HOST_FIRST.tableName);
|
|
102
|
-
const cfFirstTable = buildEntityTable(CF_FIRST.entityName, cfFirstEntity);
|
|
103
|
-
const hostFirstTable = buildEntityTable(HOST_FIRST.entityName, hostFirstEntity);
|
|
104
111
|
|
|
105
|
-
// Registration order is the whole point: cfFirst registers the
|
|
106
|
-
// owner-nulling host hook; hostFirst registers the host hook FIRST
|
|
107
|
-
// that exposed the bug). The order-sentinel must make both correct.
|
|
112
|
+
// Registration order is the whole point: cfFirst registers the redact hook
|
|
113
|
+
// BEFORE the owner-nulling host hook; hostFirst registers the host hook FIRST
|
|
114
|
+
// (the order that exposed the bug). The order-sentinel must make both correct.
|
|
108
115
|
const cfFirstFeature = defineFeature(CF_FIRST.featureName, (r) => {
|
|
109
116
|
r.entity(CF_FIRST.entityName, cfFirstEntity);
|
|
110
|
-
r.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
userIdColumn: "inserted_by_id",
|
|
117
|
+
r.useExtension(EXT_USER_DATA, CF_FIRST.entityName, {
|
|
118
|
+
export: async () => null,
|
|
119
|
+
delete: makeRedactHook(CF_FIRST.tableName),
|
|
120
|
+
order: EXT_USER_DATA_ORDER.REDACT_BEFORE_OWNER,
|
|
115
121
|
});
|
|
116
122
|
r.useExtension(EXT_USER_DATA, CF_FIRST.entityName, {
|
|
117
123
|
export: async () => null,
|
|
@@ -121,19 +127,17 @@ const cfFirstFeature = defineFeature(CF_FIRST.featureName, (r) => {
|
|
|
121
127
|
|
|
122
128
|
const hostFirstFeature = defineFeature(HOST_FIRST.featureName, (r) => {
|
|
123
129
|
r.entity(HOST_FIRST.entityName, hostFirstEntity);
|
|
124
|
-
r.requires("custom-fields");
|
|
125
130
|
r.useExtension(EXT_USER_DATA, HOST_FIRST.entityName, {
|
|
126
131
|
export: async () => null,
|
|
127
132
|
delete: makeHostDeleteHook(HOST_FIRST.tableName),
|
|
128
133
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
r.useExtension(EXT_USER_DATA, HOST_FIRST.entityName, {
|
|
135
|
+
export: async () => null,
|
|
136
|
+
delete: makeRedactHook(HOST_FIRST.tableName),
|
|
137
|
+
order: EXT_USER_DATA_ORDER.REDACT_BEFORE_OWNER,
|
|
133
138
|
});
|
|
134
139
|
});
|
|
135
140
|
|
|
136
|
-
const admin = createTestUser({ id: 1, roles: ["TenantAdmin"], tenantId: TENANT });
|
|
137
141
|
const FORGET_USER = "cccccccc-cccc-4ccc-8ccc-0000000000a1";
|
|
138
142
|
|
|
139
143
|
let stack: TestStack;
|
|
@@ -147,14 +151,12 @@ beforeAll(async () => {
|
|
|
147
151
|
createSessionsFeature(),
|
|
148
152
|
createDataRetentionFeature(),
|
|
149
153
|
createComplianceProfilesFeature(),
|
|
150
|
-
createCustomFieldsFeature(),
|
|
151
154
|
createUserDataRightsFeature(),
|
|
152
155
|
cfFirstFeature,
|
|
153
156
|
hostFirstFeature,
|
|
154
157
|
],
|
|
155
158
|
});
|
|
156
159
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
157
|
-
await unsafeCreateEntityTable(stack.db, fieldDefinitionEntity);
|
|
158
160
|
await unsafeCreateEntityTable(stack.db, tenantRetentionOverrideEntity);
|
|
159
161
|
await unsafeCreateEntityTable(stack.db, cfFirstEntity);
|
|
160
162
|
await unsafeCreateEntityTable(stack.db, hostFirstEntity);
|
|
@@ -166,21 +168,6 @@ afterAll(async () => {
|
|
|
166
168
|
await stack.cleanup();
|
|
167
169
|
});
|
|
168
170
|
|
|
169
|
-
async function defineSensitiveField(entityName: string, fieldKey: string, sensitive: boolean) {
|
|
170
|
-
await stack.http.writeOk(
|
|
171
|
-
"custom-fields:write:define-tenant-field",
|
|
172
|
-
{
|
|
173
|
-
entityName,
|
|
174
|
-
fieldKey,
|
|
175
|
-
serializedField: { type: "text", sensitive },
|
|
176
|
-
required: false,
|
|
177
|
-
searchable: false,
|
|
178
|
-
displayOrder: 0,
|
|
179
|
-
},
|
|
180
|
-
admin,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
171
|
async function seedAnonymizeOverride(entityName: string) {
|
|
185
172
|
await seedProjectionRow(stack.db, tenantRetentionOverrideTable, {
|
|
186
173
|
entityName,
|
|
@@ -217,25 +204,15 @@ beforeEach(async () => {
|
|
|
217
204
|
await resetEventStore(stack);
|
|
218
205
|
await asRawClient(stack.db).unsafe(`DELETE FROM ${CF_FIRST.tableName}`);
|
|
219
206
|
await asRawClient(stack.db).unsafe(`DELETE FROM ${HOST_FIRST.tableName}`);
|
|
220
|
-
await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
|
|
221
207
|
await asRawClient(stack.db).unsafe(`DELETE FROM read_tenant_retention_overrides`);
|
|
222
208
|
await asRawClient(stack.db).unsafe(`DELETE FROM read_tenant_memberships`);
|
|
223
209
|
});
|
|
224
210
|
|
|
225
211
|
describe("forget hook-ordering :: redaction runs before owner-nulling regardless of registration order", () => {
|
|
226
|
-
test("both registration orders strip the
|
|
227
|
-
// Field defs (sensitive ssn, non-sensitive safe) for both entities.
|
|
212
|
+
test("both registration orders strip the redacted jsonb key and null the owner column", async () => {
|
|
228
213
|
for (const spec of [CF_FIRST, HOST_FIRST]) {
|
|
229
|
-
await defineSensitiveField(spec.entityName, "ssn", true);
|
|
230
|
-
await defineSensitiveField(spec.entityName, "safe", false);
|
|
231
214
|
await seedAnonymizeOverride(spec.entityName);
|
|
232
215
|
}
|
|
233
|
-
// Project the field definitions. Assert the pass had no failures: a silent
|
|
234
|
-
// projection failure would leave read_custom_field_definitions empty, make
|
|
235
|
-
// loadSensitiveFieldKeys return [], and turn the strip into a no-op —
|
|
236
|
-
// verifying the test exercises the strip for the right reason.
|
|
237
|
-
const pass = await stack.eventDispatcher?.runOnce();
|
|
238
|
-
expect(pass?.failed ?? 0).toBe(0);
|
|
239
216
|
|
|
240
217
|
const cfRowId = "dddddddd-dddd-4ddd-8ddd-000000000001";
|
|
241
218
|
const hostRowId = "dddddddd-dddd-4ddd-8ddd-000000000002";
|