@cosmicdrift/kumiko-types 0.251.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-types",
3
- "version": "0.251.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 | at rest | searchable | Art. 17 erasure
33
- // -------------------------------|------------|------------|----------------
34
- // (none) | plaintext | yes | no
35
- // allowPlaintext + anonymize | plaintext | yes | read side only
36
- // pii / userOwned / tenantOwned | ciphertext | yes * | yes, key erase
37
- // encrypted: true | ciphertext | no | NO
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
@@ -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 two erase triggers, two subject kinds.
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>") primary key in
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
- return subject.kind === "user"
23
- ? subjectKeyForUser(subject.userId)
24
- : subjectKeyForTenant(subject.tenantId);
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