@cosmicdrift/kumiko-types 0.209.1 → 0.211.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-types",
3
- "version": "0.209.1",
3
+ "version": "0.211.0",
4
4
  "description": "Framework-Type-Definitions für Kumiko — FeatureDefinition, BootCheck-Types und die reinen Engine-Types. Erlaubt Downstream-Konsumenten, gegen die Type-Contracts zu bauen, ohne das ganze Framework-Package zu importieren. Enthaelt keine identitaets-sensitiven Runtime-Werte mehr (Error-Klassen leben seit #1629 in kumiko-framework, Brand-Symbole nutzen Symbol.for) und ist deshalb eine plain dependency, keine peerDependency.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
package/src/feature.ts CHANGED
@@ -667,7 +667,7 @@ export type FeatureRegistrar<TFeature extends string = string> = {
667
667
  *
668
668
  * ```ts
669
669
  * defineFeature("prompt-store", (r) => {
670
- * const promptFields = { text: { type: "text", pii: true } };
670
+ * const promptFields = { text: createTextField({ personal: "self", find: "none" }) };
671
671
  * r.entity("prompt", { fields: promptFields });
672
672
  * r.bootCheck(({ features }) => {
673
673
  * // Conditional on this feature's own shape (has a pii field) —
package/src/fields.ts CHANGED
@@ -24,11 +24,12 @@ export type FieldAccess = {
24
24
 
25
25
  // --- PII / Subject-Key Annotations (GDPR Art. 17) ---
26
26
  //
27
- // Four independent mechanisms, NOT interchangeable. Picking the wrong one is
28
- // the recurring mistake this table exists to prevent: `encrypted: true` looks
29
- // like the strongest option and is the only one with NO erasure guarantee.
27
+ // Four independent mechanisms, NOT interchangeable. Picking the wrong
28
+ // `personal`/`find` combination is the recurring mistake this table exists
29
+ // to prevent: `encrypted: true` looks like the strongest option and is the
30
+ // only one with NO erasure guarantee.
30
31
  //
31
- // flag | at rest | searchable | Art. 17 erasure
32
+ // resolved flag | at rest | searchable | Art. 17 erasure
32
33
  // -------------------------------|------------|------------|----------------
33
34
  // (none) | plaintext | yes | no
34
35
  // allowPlaintext + anonymize | plaintext | yes | read side only
@@ -62,36 +63,52 @@ export type FieldAccess = {
62
63
  // access — an access + retention question, not a crypto one. Choose it
63
64
  // deliberately, not by forgetting to annotate.
64
65
  //
65
- // Which subject owns the field:
66
- // - `pii: true` — the entity itself. user.email.
67
- // - `userOwned: { ownerField }` the user referenced in that field.
68
- // comment.bodycomment.authorId.
69
- // - `tenantOwned: true` — the current tenant (ctx.tenantId at write
70
- // time). tenantBranding.brandColor.
71
- // `piiEncrypted: true` is a declarative alias over these for values the user
72
- // may legitimately see back (their own IBAN), unlike `r.secret()`. Text
73
- // fields only. `sortable` + subject annotation still throws; `searchable` is allowed (#1610). `sensitive` + `searchable` throws.
66
+ // Which subject owns the field — expressed via `personal` on the field
67
+ // factory, resolved to these flags:
68
+ // - `personal: "self"` → `pii: true`. the entity itself, user.email.
69
+ // - `personal: { of: "<f>" }` `userOwned: { ownerField: "<f>" }`. the
70
+ // user referenced in that field.
71
+ // comment.body → comment.authorId.
72
+ // - `personal: "tenant"` `tenantOwned: true`. the current tenant
73
+ // (ctx.tenantId at write time).
74
+ // tenantBranding.brandColor.
75
+ // - `personal: "ref"` → `subjectRef: true`. FK into `user` with no
76
+ // annotated content of its own (authorId,
77
+ // assigneeId).
78
+ // - `personal: false, reason` → `allowPlaintext: "<reason>"`. deliberately
79
+ // plaintext; `reason` is mandatory snake_case.
80
+ //
81
+ // How findable it must stay — `find`, mandatory whenever `personal` names a
82
+ // subject ("self" / { of } / "tenant"):
83
+ // - `find: "exact"` → `lookupable: true`. equality only, via blind index.
84
+ // - `find: "fuzzy"` → `lookupable: true, searchable: true`. substring/
85
+ // full-text via the Meilisearch consumer (#1610).
86
+ // - `find: "none"` → neither.
87
+ // - `find: "secret"` → `sensitive: true`. longText fields only allow
88
+ // "none" | "secret" — no lookup/search machinery there.
74
89
  //
75
90
  // Worked examples, all live in this repo:
76
91
  //
77
92
  // user.email (user/schema/user.ts) — login identifier, must stay findable.
78
- // `pii: true, lookupable: true`. Ciphertext everywhere, exact lookup via
79
- // the blind index, gone when the user's subject key dies.
93
+ // `personal: "self", find: "exact"`. Ciphertext everywhere, exact lookup
94
+ // via the blind index, gone when the user's subject key dies.
80
95
  //
81
96
  // user.displayName (same file) — real name; substring search via Meili.
82
- // `pii: true, searchable: true` (#1610). Ciphertext in events/projection;
83
- // plaintext only in the derived index; purged on forget.
97
+ // `personal: "self", find: "fuzzy"` (#1610). Ciphertext in
98
+ // events/projection; plaintext only in the derived index; purged on
99
+ // forget.
84
100
  //
85
101
  // ledger.description (ledger/entity.ts) — "Miete Januar" is accounting
86
102
  // data that happens to read like PII, and full-text search over it is the
87
- // point. `allowPlaintext: "is-business-data"`, cleaned up via retention,
88
- // event log keeps the original. Accepted deliberately.
103
+ // point. `personal: false, reason: "is_business_data"`, cleaned up via
104
+ // retention, event log keeps the original. Accepted deliberately.
89
105
  //
90
106
  // subscription.providerCustomerId (billing-foundation/entities.ts) — a
91
- // Mollie/Stripe id, never searched. `tenantOwned: true`, explicitly NOT
92
- // `encrypted: true`: tenant-destroy erases the tenant subject key, and
93
- // only the subject path shreds along with it (#800). Budget maxLength for
94
- // ciphertext (`kumiko-pii:v1:<subject>:<blob>`, roughly plaintext × 2.3).
107
+ // Mollie/Stripe id, never searched. `personal: "tenant", find: "none"`,
108
+ // explicitly NOT `encrypted: true`: tenant-destroy erases the tenant
109
+ // subject key, and only the subject path shreds along with it (#800).
110
+ // Budget maxLength for ciphertext
111
+ // (`kumiko-pii:v1:<subject>:<blob>`, roughly plaintext × 2.3).
95
112
  //
96
113
  // `sensitive: true` is orthogonal to all of this — see its own note above.
97
114
  //
@@ -102,7 +119,10 @@ export type FieldAccess = {
102
119
  //
103
120
  // Background specs (both shipped, kumiko-platform/docs/archive/plans/):
104
121
  // datenschutz/crypto-shredding.md, datenschutz/blind-index.md.
105
- export type PiiAnnotations = {
122
+ // Internal, expanded flag shape a field definition carries after a factory
123
+ // (createTextField, createTimestampField, ...) resolves the author-facing
124
+ // `personal`/`find` annotations below. No field author sets this directly.
125
+ export type ResolvedPiiFlags = {
106
126
  readonly pii?: boolean;
107
127
  readonly userOwned?: { readonly ownerField: string };
108
128
  readonly tenantOwned?: boolean;
@@ -121,6 +141,74 @@ export type PiiAnnotations = {
121
141
  readonly subjectRef?: true;
122
142
  };
123
143
 
144
+ // --- Author-facing PII API (kumiko-framework#2250) ---
145
+ //
146
+ // Two questions instead of the twelve raw flags above: whose data is this
147
+ // (`personal`, resolved to a subject key) and how findable must it stay
148
+ // (`find`, resolved to the search/lookup flags). Field factories expand
149
+ // these into `ResolvedPiiFlags` — see packages/framework/src/engine/factories.ts.
150
+ export type PersonalSubject = "self" | { readonly of: string } | "tenant";
151
+ export type Findability = "exact" | "fuzzy" | "none" | "secret";
152
+
153
+ // Marks "no personal annotation given" — plain `Record<string, never>` would
154
+ // force every OTHER intersected property (e.g. a factory's `required?: R`)
155
+ // to `never` too, since its index signature applies to all string keys.
156
+ // Naming the three annotation keys explicitly avoids that trap.
157
+ type NoPersonalAnnotation = {
158
+ readonly personal?: never;
159
+ readonly find?: never;
160
+ readonly reason?: never;
161
+ readonly anonymize?: never;
162
+ };
163
+
164
+ export type PersonalAnnotations =
165
+ | {
166
+ readonly personal: PersonalSubject;
167
+ readonly find: Findability;
168
+ readonly anonymize?: () => unknown | Promise<unknown>;
169
+ }
170
+ | { readonly personal: "ref" }
171
+ | {
172
+ readonly personal: false;
173
+ readonly reason: string;
174
+ readonly anonymize?: () => unknown | Promise<unknown>;
175
+ }
176
+ | NoPersonalAnnotation;
177
+
178
+ // longText has no lookupable/searchable machinery ("Kein searchable" — see
179
+ // LongTextFieldDef doc) — only "does this need to stay a secret" applies.
180
+ // Kept mandatory like `find` on PersonalAnnotations: no silent default.
181
+ export type LongTextFindability = "none" | "secret";
182
+
183
+ export type PersonalAnnotationsLongText =
184
+ | {
185
+ readonly personal: PersonalSubject;
186
+ readonly find: LongTextFindability;
187
+ readonly anonymize?: () => unknown | Promise<unknown>;
188
+ }
189
+ | { readonly personal: "ref" }
190
+ | {
191
+ readonly personal: false;
192
+ readonly reason: string;
193
+ readonly anonymize?: () => unknown | Promise<unknown>;
194
+ }
195
+ | NoPersonalAnnotation;
196
+
197
+ // Same as PersonalAnnotations but without `find` — for field types with no
198
+ // full-text/blind-index machinery (everything except text and longText).
199
+ export type PersonalAnnotationsNoFind =
200
+ | {
201
+ readonly personal: PersonalSubject;
202
+ readonly anonymize?: () => unknown | Promise<unknown>;
203
+ }
204
+ | { readonly personal: "ref" }
205
+ | {
206
+ readonly personal: false;
207
+ readonly reason: string;
208
+ readonly anonymize?: () => unknown | Promise<unknown>;
209
+ }
210
+ | NoPersonalAnnotation;
211
+
124
212
  // --- Retention (DSGVO Art. 5(1)(e) + HGB/AO Aufbewahrungspflichten) ---
125
213
  //
126
214
  // Pro Entity definiert der Author eine Default-Retention-Policy. Tenant-
@@ -158,12 +246,6 @@ export type TextFieldDef = {
158
246
  * Default: false — analog zu `sortable`, opt-in. */
159
247
  readonly filterable?: boolean;
160
248
  readonly encrypted?: boolean;
161
- /** User/admin may legitimately see the value (their own IBAN, passport
162
- * number) — unlike `r.secret()`, which only server code reads. A
163
- * declarative alias over the subject-KMS (pii/userOwned/tenantOwned):
164
- * only allowed on `type: "text"`, can't combine with `searchable`/
165
- * `sortable` (boot validator, kumiko-platform#231/#456). */
166
- readonly piiEncrypted?: boolean;
167
249
  readonly sensitive?: boolean;
168
250
  readonly format?: "email" | "url" | "phone";
169
251
  readonly default?: string;
@@ -173,7 +255,7 @@ export type TextFieldDef = {
173
255
  * explizite Höhe. Search/sort/encrypt verhalten sich unverändert
174
256
  * identisch zu single-line — nur die Render-Surface wechselt. */
175
257
  readonly multiline?: boolean | { readonly rows?: number };
176
- } & PiiAnnotations;
258
+ } & ResolvedPiiFlags;
177
259
 
178
260
  /**
179
261
  * Long-form text content — source-code, markdown, blog-posts, email-
@@ -207,7 +289,7 @@ export type LongTextFieldDef = {
207
289
  readonly default?: string;
208
290
  readonly access?: FieldAccess;
209
291
  readonly multiline?: boolean | { readonly rows?: number };
210
- } & PiiAnnotations;
292
+ } & ResolvedPiiFlags;
211
293
 
212
294
  export type BooleanFieldDef = {
213
295
  readonly type: "boolean";
@@ -228,7 +310,7 @@ export type SelectFieldDef<TOptions extends readonly string[] = readonly string[
228
310
  readonly sensitive?: boolean;
229
311
  readonly default?: TOptions[number];
230
312
  readonly access?: FieldAccess;
231
- } & PiiAnnotations;
313
+ } & ResolvedPiiFlags;
232
314
 
233
315
  // Mehrere Werte aus einer festen Options-Liste — UI rendert als
234
316
  // Checkbox-/Multi-Select-Kontrolle. Storage: jsonb-Array<string>;
@@ -252,7 +334,7 @@ export type MultiSelectFieldDef<TOptions extends readonly string[] = readonly st
252
334
  /** Default-Auswahl. Jeder Eintrag muss in `options` sein (Boot-Validator). */
253
335
  readonly default?: readonly TOptions[number][];
254
336
  readonly access?: FieldAccess;
255
- } & PiiAnnotations;
337
+ } & ResolvedPiiFlags;
256
338
 
257
339
  /**
258
340
  * Storage: `integer` flag decides the Postgres column type, not just Zod
@@ -276,7 +358,7 @@ export type NumberFieldDef = {
276
358
  /** `true` → `integer` column + `.int()` Zod validation. Omitted/`false` →
277
359
  * `double precision` column, fractional values allowed. */
278
360
  readonly integer?: boolean;
279
- } & PiiAnnotations;
361
+ } & ResolvedPiiFlags;
280
362
 
281
363
  /**
282
364
  * 64-bit-Integer-Spalte fuer Audit-Counter, Byte-Sizes, Event-IDs und
@@ -298,7 +380,7 @@ export type BigIntFieldDef = {
298
380
  readonly sensitive?: boolean;
299
381
  readonly default?: number;
300
382
  readonly access?: FieldAccess;
301
- } & PiiAnnotations;
383
+ } & ResolvedPiiFlags;
302
384
 
303
385
  /**
304
386
  * Exact decimal — Postgres `numeric(precision, scale)`. For values that need
@@ -322,7 +404,7 @@ export type DecimalFieldDef = {
322
404
  readonly sensitive?: boolean;
323
405
  readonly default?: number;
324
406
  readonly access?: FieldAccess;
325
- } & PiiAnnotations;
407
+ } & ResolvedPiiFlags;
326
408
 
327
409
  export type MoneyFieldDef = {
328
410
  readonly type: "money";
@@ -361,7 +443,7 @@ export type ReferenceFieldDef = {
361
443
  * statt single UUID. Storage als jsonb-Array<uuid>. UI rendert
362
444
  * Multi-Select-Combobox mit Tag-Anzeige der gewählten Items. */
363
445
  readonly multiple?: boolean;
364
- } & PiiAnnotations;
446
+ } & ResolvedPiiFlags;
365
447
 
366
448
  // --- Currency ---
367
449
 
@@ -463,7 +545,7 @@ export type EmbeddedFieldDef = {
463
545
  * rounded `derived` cells, i.e. "sum-of-rounded" not "round-of-sum" —
464
546
  * see kumiko-framework#1866. */
465
547
  readonly totalsMatch?: Readonly<Record<string, string>>;
466
- } & PiiAnnotations;
548
+ } & ResolvedPiiFlags;
467
549
 
468
550
  // Free-form jsonb — keys/shape NOT validated at write-time. Use for:
469
551
  // - Tenant-defined extension data (custom-fields-bundle uses this for
@@ -478,7 +560,7 @@ export type JsonbFieldDef = {
478
560
  readonly type: "jsonb";
479
561
  readonly sensitive?: boolean;
480
562
  readonly access?: FieldAccess;
481
- } & PiiAnnotations;
563
+ } & ResolvedPiiFlags;
482
564
 
483
565
  // Legacy "date" — JS-Date-Object, semantisch unklar (Wall-Clock vs Instant).
484
566
  // Für neue Felder bevorzuge:
@@ -502,7 +584,7 @@ export type DateFieldDef = {
502
584
  /** Format/Locale-Override für Anzeige und Eingabe-Parsing (z.B.
503
585
  * "de-DE"). Default = App-Locale. */
504
586
  readonly locale?: string;
505
- } & PiiAnnotations;
587
+ } & ResolvedPiiFlags;
506
588
 
507
589
  // UTC-Instant (Temporal.Instant). Für Ereignisse die zu einem bestimmten
508
590
  // Augenblick passieren, ohne Location-Bezug: createdAt, loginAt, actualPickupAt.
@@ -539,7 +621,7 @@ export type TimestampFieldDef = {
539
621
  /** Format/Locale-Override für Anzeige und Eingabe-Parsing. Default =
540
622
  * App-Locale. */
541
623
  readonly locale?: string;
542
- } & PiiAnnotations;
624
+ } & ResolvedPiiFlags;
543
625
 
544
626
  // IANA-Zonenname (z.B. "Europe/Berlin", "America/Los_Angeles").
545
627
  // Wird via `Intl.supportedValuesOf("timeZone")` validiert (kommt im
@@ -550,7 +632,7 @@ export type TzFieldDef = {
550
632
  readonly required?: boolean;
551
633
  readonly sensitive?: boolean;
552
634
  readonly access?: FieldAccess;
553
- } & PiiAnnotations;
635
+ } & ResolvedPiiFlags;
554
636
 
555
637
  // Wall-Clock-Termin an einem Ort als ATOMARES Konzept.
556
638
  // EIN Feld in der Schema-Definition, ZWEI Spalten in der DB
@@ -585,7 +667,7 @@ export type LocatedTimestampFieldDef = {
585
667
  /** Format/Locale-Override für Anzeige und Eingabe-Parsing. Default =
586
668
  * App-Locale. */
587
669
  readonly locale?: string;
588
- } & PiiAnnotations;
670
+ } & ResolvedPiiFlags;
589
671
 
590
672
  export type FileFieldDef = {
591
673
  readonly type: "file";
package/src/screen.ts CHANGED
@@ -426,13 +426,6 @@ export type ProjectionDetailScreenDefinition = {
426
426
  * readOnly anyway (there is no write path, see the shim doc above).
427
427
  * Set "form" to opt back into the disabled-Input look (fw#2245). */
428
428
  readonly valueDisplay?: "form" | "text";
429
- /** Hides RenderEdit's action-bar footer (effectively just the "Cancel"
430
- * button here — this screen type never has Save/Delete). Default false,
431
- * matching the pre-fw#2245 behavior: Cancel is shown whenever
432
- * `listScreenId` is set. `resolveDetailBreadcrumb` (renderer-web) also
433
- * reads `listScreenId` on this screen type, so the breadcrumb still
434
- * offers "back" when the footer is hidden. */
435
- readonly hideActions?: boolean;
436
429
  };
437
430
 
438
431
  // --- dashboard ---