@cosmicdrift/kumiko-types 0.250.0 → 0.252.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/fields.ts +11 -6
- package/src/kms-adapter-types.ts +30 -7
- package/src/screen.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.252.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/fields.ts
CHANGED
|
@@ -29,12 +29,12 @@ export type FieldAccess = {
|
|
|
29
29
|
// to prevent: `encrypted: true` looks like the strongest option and is the
|
|
30
30
|
// only one with NO erasure guarantee.
|
|
31
31
|
//
|
|
32
|
-
// resolved flag
|
|
33
|
-
//
|
|
34
|
-
// (none)
|
|
35
|
-
// allowPlaintext + anonymize
|
|
36
|
-
// pii / userOwned / tenantOwned
|
|
37
|
-
// encrypted: true
|
|
32
|
+
// resolved flag | at rest | searchable | Art. 17 erasure
|
|
33
|
+
// --------------------------------------------|------------|------------|---------------
|
|
34
|
+
// (none) | plaintext | yes | no
|
|
35
|
+
// allowPlaintext + anonymize | plaintext | yes | read side only
|
|
36
|
+
// pii / userOwned / tenantOwned / recordOwned | ciphertext | yes * | yes, key erase
|
|
37
|
+
// encrypted: true | ciphertext | no | NO
|
|
38
38
|
//
|
|
39
39
|
// * Subject-annotated + `searchable: true` (#1610): search consumer
|
|
40
40
|
// decrypts into Meilisearch. Events/projection stay ciphertext.
|
|
@@ -75,6 +75,10 @@ export type FieldAccess = {
|
|
|
75
75
|
// - `personal: "ref"` → `subjectRef: true`. FK into `user` with no
|
|
76
76
|
// annotated content of its own (authorId,
|
|
77
77
|
// assigneeId).
|
|
78
|
+
// - `personal: { of: "id" }` → `recordOwned: true`. the row itself is the
|
|
79
|
+
// subject — free-text content with no user
|
|
80
|
+
// reference of its own (a support note keyed
|
|
81
|
+
// only by its own row id).
|
|
78
82
|
// - `personal: false, reason` → `allowPlaintext: "<reason>"`. deliberately
|
|
79
83
|
// plaintext; `reason` is mandatory snake_case.
|
|
80
84
|
//
|
|
@@ -126,6 +130,7 @@ export type ResolvedPiiFlags = {
|
|
|
126
130
|
readonly pii?: boolean;
|
|
127
131
|
readonly userOwned?: { readonly ownerField: string };
|
|
128
132
|
readonly tenantOwned?: boolean;
|
|
133
|
+
readonly recordOwned?: true;
|
|
129
134
|
readonly anonymize?: () => unknown | Promise<unknown>;
|
|
130
135
|
readonly allowPlaintext?: string;
|
|
131
136
|
/** Equality-Lookups (fetchOne/filter eq) bleiben trotz Verschluesselung
|
package/src/kms-adapter-types.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { TenantId } from "./identifiers";
|
|
2
2
|
|
|
3
3
|
// The subject a DEK belongs to. User data is shredded on user-forget,
|
|
4
|
-
// tenant data on tenant-destroy
|
|
4
|
+
// tenant data on tenant-destroy, record data on a row-scoped forget —
|
|
5
|
+
// three erase triggers, three subject kinds.
|
|
5
6
|
export type SubjectId =
|
|
6
7
|
| { readonly kind: "user"; readonly userId: string }
|
|
7
|
-
| { readonly kind: "tenant"; readonly tenantId: TenantId }
|
|
8
|
+
| { readonly kind: "tenant"; readonly tenantId: TenantId }
|
|
9
|
+
| { readonly kind: "record"; readonly entity: string; readonly id: string };
|
|
8
10
|
|
|
9
|
-
// Compact storage key ("user:<uuid>" / "tenant:<uuid>"
|
|
10
|
-
// adapter backends and cache key in the request-level DEK cache.
|
|
11
|
+
// Compact storage key ("user:<uuid>" / "tenant:<uuid>" / "record:<entity>:<id>")
|
|
12
|
+
// — primary key in adapter backends and cache key in the request-level DEK cache.
|
|
11
13
|
export type SubjectKey = string;
|
|
12
14
|
|
|
13
15
|
export function subjectKeyForUser(userId: string): SubjectKey {
|
|
@@ -18,10 +20,23 @@ export function subjectKeyForTenant(tenantId: TenantId): SubjectKey {
|
|
|
18
20
|
return `tenant:${tenantId}`;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
export function subjectKeyForRecord(entity: string, id: string): SubjectKey {
|
|
24
|
+
// The key is parsed back on exactly one ":" — an entity containing ":" would break the round-trip.
|
|
25
|
+
if (entity === "" || entity.includes(":"))
|
|
26
|
+
throw new Error(`Invalid record entity for subject key: ${entity}`);
|
|
27
|
+
if (id === "") throw new Error("Invalid record id for subject key: empty");
|
|
28
|
+
return `record:${entity}:${id}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export function subjectIdToKey(subject: SubjectId): SubjectKey {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
switch (subject.kind) {
|
|
33
|
+
case "user":
|
|
34
|
+
return subjectKeyForUser(subject.userId);
|
|
35
|
+
case "tenant":
|
|
36
|
+
return subjectKeyForTenant(subject.tenantId);
|
|
37
|
+
case "record":
|
|
38
|
+
return subjectKeyForRecord(subject.entity, subject.id);
|
|
39
|
+
}
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
export function subjectIdFromKey(key: SubjectKey): SubjectId {
|
|
@@ -29,6 +44,14 @@ export function subjectIdFromKey(key: SubjectKey): SubjectId {
|
|
|
29
44
|
if (key.startsWith("tenant:")) {
|
|
30
45
|
return { kind: "tenant", tenantId: key.slice("tenant:".length) as TenantId }; // @cast-boundary parse of a key this module minted
|
|
31
46
|
}
|
|
47
|
+
if (key.startsWith("record:")) {
|
|
48
|
+
const rest = key.slice("record:".length);
|
|
49
|
+
const separatorIndex = rest.indexOf(":");
|
|
50
|
+
const entity = separatorIndex === -1 ? "" : rest.slice(0, separatorIndex);
|
|
51
|
+
const id = separatorIndex === -1 ? "" : rest.slice(separatorIndex + 1);
|
|
52
|
+
if (entity === "" || id === "") throw new Error(`Invalid subject key: ${key}`);
|
|
53
|
+
return { kind: "record", entity, id };
|
|
54
|
+
}
|
|
32
55
|
throw new Error(`Invalid subject key: ${key}`);
|
|
33
56
|
}
|
|
34
57
|
|
package/src/screen.ts
CHANGED
|
@@ -835,6 +835,20 @@ export type EditRelatedListSection = {
|
|
|
835
835
|
* `field` must name a column with `sortable: true` — boot-validator pins
|
|
836
836
|
* that, same as `entityList.defaultSort`. */
|
|
837
837
|
readonly defaultSort?: ListSortSpec;
|
|
838
|
+
/** Renders a search box whose debounced term rides along as
|
|
839
|
+
* `payload.search` on this section's own query. Opt-in, unlike
|
|
840
|
+
* `projectionList.searchable` (derived from the bound query's schema): a
|
|
841
|
+
* relatedList query is an ordinary child-rows handler, so a `search`
|
|
842
|
+
* parameter in its schema is not by itself a statement that this section
|
|
843
|
+
* should offer search. The boot-validator rejects `true` when the query's
|
|
844
|
+
* Zod schema has no `search` parameter. */
|
|
845
|
+
readonly searchable?: boolean;
|
|
846
|
+
/** User-toggleable facet dropdowns — same explicit-label `ListFacetSpec` as
|
|
847
|
+
* `projectionList.facets` (a relatedList has no entity to derive filterable
|
|
848
|
+
* fields from). `field` must be a declared column and the bound query
|
|
849
|
+
* handler must accept `filters` in its Zod schema — the boot-validator
|
|
850
|
+
* checks both. Selected values ride along as `payload.filters`. */
|
|
851
|
+
readonly facets?: readonly ListFacetSpec[];
|
|
838
852
|
/** Row click opens the target entity's detail screen via ObjectTarget —
|
|
839
853
|
* the `detailFor` lookup owns the entity→screen mapping, so no screenId
|
|
840
854
|
* is named here. `idColumn` names the row key holding that id (default
|