@cosmicdrift/kumiko-bundled-features 0.170.0 → 0.171.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.170.0",
3
+ "version": "0.171.0",
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.170.0",
123
- "@cosmicdrift/kumiko-framework": "0.170.0",
124
- "@cosmicdrift/kumiko-headless": "0.170.0",
125
- "@cosmicdrift/kumiko-renderer": "0.170.0",
126
- "@cosmicdrift/kumiko-renderer-web": "0.170.0",
127
- "@cosmicdrift/kumiko-types": "0.170.0",
122
+ "@cosmicdrift/kumiko-dispatcher-live": "0.171.0",
123
+ "@cosmicdrift/kumiko-framework": "0.171.0",
124
+ "@cosmicdrift/kumiko-headless": "0.171.0",
125
+ "@cosmicdrift/kumiko-renderer": "0.171.0",
126
+ "@cosmicdrift/kumiko-renderer-web": "0.171.0",
127
+ "@cosmicdrift/kumiko-types": "0.171.0",
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 invisible to the
39
- // Art.15/20 export and Art.17 forget pipeline. Hard boot failure: a
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;