@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 +1 -1
- package/src/feature.ts +1 -1
- package/src/fields.ts +126 -44
- package/src/screen.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
-
"version": "0.
|
|
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: {
|
|
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
|
|
28
|
-
// the recurring mistake this table exists
|
|
29
|
-
// like the strongest option and is the
|
|
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
|
|
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
|
-
//
|
|
67
|
-
// - `
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// `
|
|
72
|
-
//
|
|
73
|
-
//
|
|
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
|
-
// `
|
|
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
|
-
// `
|
|
83
|
-
// plaintext only in the derived index; purged on
|
|
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. `
|
|
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. `
|
|
92
|
-
// `encrypted: true`: tenant-destroy erases the tenant
|
|
93
|
-
// only the subject path shreds along with it (#800).
|
|
94
|
-
//
|
|
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
|
-
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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
|
-
} &
|
|
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 ---
|