@cosmicdrift/kumiko-bundled-features 0.170.0 → 0.171.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.171.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -119,12 +119,12 @@
|
|
|
119
119
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
123
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
124
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
125
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
126
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
127
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
122
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.171.1",
|
|
123
|
+
"@cosmicdrift/kumiko-framework": "0.171.1",
|
|
124
|
+
"@cosmicdrift/kumiko-headless": "0.171.1",
|
|
125
|
+
"@cosmicdrift/kumiko-renderer": "0.171.1",
|
|
126
|
+
"@cosmicdrift/kumiko-renderer-web": "0.171.1",
|
|
127
|
+
"@cosmicdrift/kumiko-types": "0.171.1",
|
|
128
128
|
"@mollie/api-client": "^4.5.0",
|
|
129
129
|
"imapflow": "^1.3.3",
|
|
130
130
|
"mailparser": "^3.9.8",
|
|
@@ -102,6 +102,28 @@ describe("GDPR-storage boot guards V2-V4 (via r.bootCheck)", () => {
|
|
|
102
102
|
expect(() => validateBoot([...baseFeatures(), bad])).toThrow(/EXT_USER_DATA hook.*Art\.17 gap/);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
test("V3: subjectRef field (FK to user, no pii/userOwned annotation) without EXT_USER_DATA hook → throws", () => {
|
|
106
|
+
const bad = defineFeature("tasks", (r) => {
|
|
107
|
+
r.entity(
|
|
108
|
+
"task",
|
|
109
|
+
createEntity({ fields: { assigneeId: createTextField({ subjectRef: true }) } }),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
expect(() => validateBoot([...baseFeatures(), bad])).toThrow(/EXT_USER_DATA hook.*Art\.17 gap/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("V3: subjectRef field WITH EXT_USER_DATA hook registered → no throw", () => {
|
|
116
|
+
const hooked = defineFeature("tasks-hooked", (r) => {
|
|
117
|
+
r.requires("user-data-rights");
|
|
118
|
+
r.entity(
|
|
119
|
+
"task",
|
|
120
|
+
createEntity({ fields: { assigneeId: createTextField({ subjectRef: true }) } }),
|
|
121
|
+
);
|
|
122
|
+
r.useExtension(EXT_USER_DATA, "task", { export: async () => null, delete: async () => {} });
|
|
123
|
+
});
|
|
124
|
+
expect(() => validateBoot([...baseFeatures(), hooked])).not.toThrow();
|
|
125
|
+
});
|
|
126
|
+
|
|
105
127
|
test("V3: projection-only pii entity WITH EXT_USER_DATA hook registered for its entity name → no throw", () => {
|
|
106
128
|
const projectionTable = projectionProbeTable("crm_contact_summary_hooked");
|
|
107
129
|
const hooked = defineFeature("crm-hooked", (r) => {
|
|
@@ -35,8 +35,9 @@ export function validateGdprHookCompleteness(features: readonly FeatureDefinitio
|
|
|
35
35
|
|
|
36
36
|
// V3: PII-entity-without-hook gate. V2 checks registered hooks for
|
|
37
37
|
// completeness; V3 catches the entity nobody registered at all — fields
|
|
38
|
-
// annotated as user-subject data (pii / userOwned) yet
|
|
39
|
-
// Art.15/20 export and Art.17 forget pipeline. Hard boot
|
|
38
|
+
// annotated as user-subject data (pii / userOwned / subjectRef) yet
|
|
39
|
+
// invisible to the Art.15/20 export and Art.17 forget pipeline. Hard boot
|
|
40
|
+
// failure: a
|
|
40
41
|
// subject-data entity that skips the pipeline is exactly the "feature built
|
|
41
42
|
// past GDPR" leak this gate exists to stop. No "is user-data-rights mounted"
|
|
42
43
|
// guard needed here (unlike the framework-internal predecessor) — this runs
|
|
@@ -58,7 +59,7 @@ export function validateGdprPiiHookCoverage(features: readonly FeatureDefinition
|
|
|
58
59
|
const subjectFields = Object.entries(entity.fields)
|
|
59
60
|
.filter(([, field]) => {
|
|
60
61
|
const annot = field as PiiAnnotations; // @cast-boundary schema-walk
|
|
61
|
-
return Boolean(annot.pii) || Boolean(annot.userOwned);
|
|
62
|
+
return Boolean(annot.pii) || Boolean(annot.userOwned) || Boolean(annot.subjectRef);
|
|
62
63
|
})
|
|
63
64
|
.map(([name]) => name);
|
|
64
65
|
if (subjectFields.length === 0) continue;
|