@cosmicdrift/kumiko-types 0.260.0 → 0.262.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.260.0",
3
+ "version": "0.262.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>",
@@ -122,6 +122,10 @@
122
122
  "types": "./src/executor-brand.ts",
123
123
  "default": "./src/executor-brand.ts"
124
124
  },
125
+ "./tenancy-brand": {
126
+ "types": "./src/tenancy-brand.ts",
127
+ "default": "./src/tenancy-brand.ts"
128
+ },
125
129
  "./where-clause-types": {
126
130
  "types": "./src/where-clause-types.ts",
127
131
  "default": "./src/where-clause-types.ts"
@@ -3,6 +3,7 @@ import type { KumikoEventTypeMap } from "./event-type-map";
3
3
  import type {
4
4
  AccessRule,
5
5
  AgentHandlerHints,
6
+ EscapeHatchDeclaration,
6
7
  HandlerContext,
7
8
  QueryEvent,
8
9
  RateLimitOption,
@@ -34,11 +35,12 @@ export type WriteHandlerDefinition<
34
35
  > = {
35
36
  readonly name: TName;
36
37
  readonly schema: TSchema;
37
- readonly access?: AccessRule;
38
+ readonly access: AccessRule;
38
39
  readonly description?: string;
39
40
  readonly agent?: AgentHandlerHints;
40
41
  readonly unsafeSkipTransitionGuard?: boolean;
41
42
  readonly rateLimit?: RateLimitOption;
43
+ readonly escapeHatch?: EscapeHatchDeclaration;
42
44
  readonly handler: (
43
45
  event: WriteEvent<z.infer<TSchema>>,
44
46
  context: HandlerContext<TMap>,
@@ -61,11 +63,12 @@ export type WriteHandlerInput<
61
63
  > = {
62
64
  readonly name: TName;
63
65
  readonly schema: TSchema;
64
- readonly access?: AccessRule;
66
+ readonly access: AccessRule;
65
67
  readonly description?: string;
66
68
  readonly agent?: AgentHandlerHints;
67
69
  readonly unsafeSkipTransitionGuard?: boolean;
68
70
  readonly rateLimit?: RateLimitOption;
71
+ readonly escapeHatch?: EscapeHatchDeclaration;
69
72
  } & (
70
73
  | {
71
74
  readonly handler: (
@@ -90,7 +93,7 @@ export type QueryHandlerDefinition<
90
93
  > = {
91
94
  readonly name: TName;
92
95
  readonly schema: TSchema;
93
- readonly access?: AccessRule;
96
+ readonly access: AccessRule;
94
97
  readonly description?: string;
95
98
  readonly agent?: AgentHandlerHints;
96
99
  readonly rateLimit?: RateLimitOption;
@@ -120,7 +123,7 @@ export type StreamHandlerDefinition<
120
123
  > = {
121
124
  readonly name: TName;
122
125
  readonly schema: TSchema;
123
- readonly access?: AccessRule;
126
+ readonly access: AccessRule;
124
127
  readonly rateLimit?: RateLimitOption;
125
128
  readonly handler: (
126
129
  query: QueryEvent<z.infer<TSchema>>,
@@ -2,7 +2,7 @@ import type { EntityDefinition } from "./fields";
2
2
  import type { AccessRule, AgentHandlerHints, QueryHandlerDef, WriteHandlerDef } from "./handlers";
3
3
 
4
4
  export type EntityHandlerOptions = {
5
- readonly access?: AccessRule;
5
+ readonly access: AccessRule;
6
6
  readonly description?: string;
7
7
  readonly agent?: AgentHandlerHints;
8
8
  /** Reads and writes across every tenant instead of the caller's own — for a
@@ -26,9 +26,14 @@ export type EntityQueryHandlerOptions = EntityHandlerOptions;
26
26
 
27
27
  export type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
28
28
 
29
+ // `access` stays optional here — registerEntityCrud resolves it per-verb from `verbAccess`, falling back to this default.
30
+ export type EntityCrudHandlerDefaults = Omit<EntityHandlerOptions, "access"> & {
31
+ readonly access?: AccessRule;
32
+ };
33
+
29
34
  export type RegisterEntityCrudOptions = {
30
- readonly write?: EntityHandlerOptions;
31
- readonly read?: EntityQueryHandlerOptions;
35
+ readonly write?: EntityCrudHandlerDefaults;
36
+ readonly read?: EntityCrudHandlerDefaults;
32
37
  readonly verbs?: Partial<Record<EntityCrudVerb, boolean>>;
33
38
  /** Per-verb access override — falls back to `write.access`/`read.access` when unset for a verb. */
34
39
  readonly verbAccess?: Partial<Record<EntityCrudVerb, AccessRule>>;
@@ -5,6 +5,7 @@
5
5
  // (crypto/DB deps).
6
6
 
7
7
  import type { EntityRelations } from "./relations";
8
+ import type { EntityTenancy } from "./tenancy-brand";
8
9
 
9
10
  // PG type repertoire the read-model tables need. Deliberately narrow — no
10
11
  // vendor-specific types (TSVECTOR, HSTORE, etc.). An app-author who needs
@@ -80,6 +81,9 @@ export type EntityTableMeta = {
80
81
  // deriveEntityTableMeta so the registry can reject r.storeTable stores
81
82
  // whose direct writes would skip the executor's encryption (#820).
82
83
  readonly piiSubjectFields?: readonly string[];
84
+ // Only present for "global" tables — absent means "tenant" so existing
85
+ // schema snapshots don't drift. See EntityDefinition.tenancy.
86
+ readonly tenancy?: "global";
83
87
  };
84
88
 
85
89
  export type BuildEntityTableMetaOptions = {
@@ -93,4 +97,5 @@ export type UnmanagedTableInput = {
93
97
  readonly columns: readonly ColumnMeta[];
94
98
  readonly indexes?: readonly IndexMeta[];
95
99
  readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
100
+ readonly tenancy?: EntityTenancy;
96
101
  };
package/src/feature.ts CHANGED
@@ -35,6 +35,7 @@ import type {
35
35
  ClaimKeyType,
36
36
  DeclarativeEventMigration,
37
37
  EntityRef,
38
+ EscapeHatchDeclaration,
38
39
  EventDef,
39
40
  EventMigrationDef,
40
41
  EventPiiStance,
@@ -454,11 +455,12 @@ export type FeatureRegistrar<TFeature extends string = string> = {
454
455
  name: string,
455
456
  schema: TSchema,
456
457
  handler: WriteHandlerFn<z.infer<TSchema>>,
457
- options?: {
458
- access?: AccessRule;
458
+ options: {
459
+ access: AccessRule;
459
460
  rateLimit?: RateLimitOption;
460
461
  description?: string;
461
462
  agent?: AgentHandlerHints;
463
+ escapeHatch?: EscapeHatchDeclaration;
462
464
  },
463
465
  ): HandlerRef;
464
466
 
@@ -469,8 +471,8 @@ export type FeatureRegistrar<TFeature extends string = string> = {
469
471
  name: string,
470
472
  schema: TSchema,
471
473
  handler: QueryHandlerFn<z.infer<TSchema>>,
472
- options?: {
473
- access?: AccessRule;
474
+ options: {
475
+ access: AccessRule;
474
476
  rateLimit?: RateLimitOption;
475
477
  outputSchema?: ZodType;
476
478
  description?: string;
@@ -485,7 +487,7 @@ export type FeatureRegistrar<TFeature extends string = string> = {
485
487
  name: string,
486
488
  schema: TSchema,
487
489
  handler: StreamHandlerFn<z.infer<TSchema>>,
488
- options?: { access?: AccessRule; rateLimit?: RateLimitOption },
490
+ options: { access: AccessRule; rateLimit?: RateLimitOption },
489
491
  ): HandlerRef;
490
492
 
491
493
  relation(entity: NameOrRef, relationName: string, definition: RelationDefinition): void;
package/src/fields.ts CHANGED
@@ -8,6 +8,7 @@ import type { VariantSpec } from "./derivatives-types";
8
8
  // array are auto-normalized to { [role]: "all" } at registry build.
9
9
  // Long-term: string[] disappears.
10
10
  import type { OwnershipMap } from "./ownership";
11
+ import type { EntityTenancy } from "./tenancy-brand";
11
12
 
12
13
  export type FieldAccess = {
13
14
  readonly read?: OwnershipMap | readonly string[];
@@ -914,10 +915,16 @@ export type ParentRefDef = {
914
915
  readonly allowedTypes?: readonly string[];
915
916
  };
916
917
 
917
- export type EntityDefinition<F extends FieldsMap = FieldsMap> = {
918
+ export type EntityDefinition<
919
+ F extends FieldsMap = FieldsMap,
920
+ T extends EntityTenancy = EntityTenancy,
921
+ > = {
918
922
  readonly table?: string;
919
923
  readonly fields: F;
920
924
  readonly softDelete?: boolean;
925
+ /** "tenant" (default): auto-filtered by every db accessor. "global": shared across
926
+ * tenants, reachable only via `db.global(table)`. Not named `scope` — config keys use that. */
927
+ readonly tenancy?: T;
921
928
  /** This aggregate's event stream lives on SYSTEM_TENANT_ID rather than the
922
929
  * creator's tenant. Opt-in per entity (NOT inherited from r.systemScope()):
923
930
  * only for genuinely tenant-independent aggregates like `user`. The first
package/src/handlers.ts CHANGED
@@ -16,11 +16,35 @@ import type { TzContext } from "./tz-context";
16
16
 
17
17
  // --- Access ---
18
18
 
19
+ export type OpenToAllDeclaration = { readonly reason: string };
20
+
21
+ export type OpenToAllAccessRule = {
22
+ // `true` is the deprecated pre-#2855 form, kept until the call-site migration (fw#2854).
23
+ readonly openToAll: OpenToAllDeclaration | true;
24
+ // Write handler intentionally accepts personal data from any authenticated caller (boot-validator gate).
25
+ readonly publicIntake?: true;
26
+ };
27
+
19
28
  // AccessRule is DEFAULT-DENY: a handler without an access rule is not reachable.
20
29
  // To grant access, set one of:
21
- // - { roles: ["Admin", ...] } — role-based allowlist (empty array denies everyone)
22
- // - { openToAll: true } — any authenticated user may call (still requires a valid JWT)
23
- export type AccessRule = { readonly roles: readonly string[] } | { readonly openToAll: true };
30
+ // - { roles: ["Admin", ...] } — role-based allowlist (empty array denies everyone)
31
+ // - { openToAll: { reason: "..." } } — any authenticated user may call (still requires a valid JWT)
32
+ // - { openToAll: true } deprecated pre-#2855 form, still accepted
33
+ export type AccessRule = { readonly roles: readonly string[] } | OpenToAllAccessRule;
34
+
35
+ export type EscapeHatchDeclaration = { readonly reason: string };
36
+
37
+ // AccessRule can arrive from untyped sources (pattern-library JSON, Designer)
38
+ // where `openToAll` doesn't actually match the declared union — narrow via
39
+ // `unknown` instead of trusting the static type, deny on anything malformed.
40
+ export function isOpenToAllGranted(rule: AccessRule): boolean {
41
+ if (!("openToAll" in rule)) return false;
42
+ const openToAll: unknown = rule.openToAll;
43
+ if (openToAll === true) return true;
44
+ if (typeof openToAll !== "object" || openToAll === null) return false;
45
+ if (!("reason" in openToAll) || typeof openToAll.reason !== "string") return false;
46
+ return openToAll.reason.trim().length > 0;
47
+ }
24
48
 
25
49
  // --- Pipeline User ---
26
50
 
@@ -996,11 +1020,12 @@ export type WriteHandlerDef = {
996
1020
  readonly name: string;
997
1021
  readonly schema: ZodType;
998
1022
  readonly handler: WriteHandlerFn;
999
- readonly access?: AccessRule;
1023
+ readonly access: AccessRule;
1000
1024
  readonly description?: string;
1001
1025
  readonly agent?: AgentHandlerHints;
1002
1026
  readonly unsafeSkipTransitionGuard?: boolean;
1003
1027
  readonly rateLimit?: RateLimitOption;
1028
+ readonly escapeHatch?: EscapeHatchDeclaration;
1004
1029
  // Set when the author wrote a `perform: stepsPipeline(...)` block. Boot-
1005
1030
  // validators (projection-allowlist) and Designer/AI tooling read this
1006
1031
  // to inspect the step list. Absent on free-form handlers.
@@ -1025,7 +1050,7 @@ export type QueryHandlerDef = {
1025
1050
  * read-gate. The boot-validator requires it on a `parentRef` entity's
1026
1051
  * list/detail handler — see boot-validator/parent-ref.ts. */
1027
1052
  readonly [ENTITY_CONVENTION_QUERY_BRAND]?: true;
1028
- readonly access?: AccessRule;
1053
+ readonly access: AccessRule;
1029
1054
  readonly description?: string;
1030
1055
  readonly agent?: AgentHandlerHints;
1031
1056
  readonly rateLimit?: RateLimitOption;
@@ -1044,6 +1069,6 @@ export type StreamHandlerDef = {
1044
1069
  readonly name: string;
1045
1070
  readonly schema: ZodType;
1046
1071
  readonly handler: StreamHandlerFn;
1047
- readonly access?: AccessRule;
1072
+ readonly access: AccessRule;
1048
1073
  readonly rateLimit?: RateLimitOption;
1049
1074
  };
package/src/screen.ts CHANGED
@@ -1035,7 +1035,9 @@ export type EntityEditScreenDefinition = {
1035
1035
  * The object form additionally names the success-payload field the
1036
1036
  * navigation id comes from (`ActionFormRedirect.idFrom`) — needed when
1037
1037
  * the edited record is a child and the target screen is the parent's
1038
- * detail screen. */
1038
+ * detail screen. Update falls back to the loaded record when the
1039
+ * success payload doesn't carry that field flatly; create falls back
1040
+ * to the submitted form values. */
1039
1041
  readonly redirect?: string | ActionFormRedirect;
1040
1042
  /** Parent list screen (short id) for breadcrumb + nav highlighting when
1041
1043
  * this screen has no nav entry of its own. Same field/semantics as on
@@ -0,0 +1,9 @@
1
+ // Makes `db.global()` on a "tenant" table a compile error, mirroring executor-brand.ts's
2
+ // approach for the executor-only write path.
3
+ export type EntityTenancy = "global" | "tenant";
4
+
5
+ declare const TENANCY: unique symbol;
6
+
7
+ export interface TenancyBrand<T extends EntityTenancy> {
8
+ readonly [TENANCY]: T;
9
+ }
@@ -1,8 +1,9 @@
1
1
  import type { DbRunner } from "./db-connection";
2
2
  import type { EntityTableMeta } from "./entity-table-meta-types";
3
- import type { NotExecutorOnly } from "./executor-brand";
3
+ import type { ExecutorOnly, NotExecutorOnly } from "./executor-brand";
4
4
  import type { TenantId } from "./identifiers";
5
5
  import type { SchemaTable } from "./schema-table-types";
6
+ import type { TenancyBrand } from "./tenancy-brand";
6
7
  import type { SelectOptions, WhereObject } from "./where-clause-types";
7
8
 
8
9
  // Method-form writes reject the executor-only brand exactly like the free-function
@@ -14,6 +15,28 @@ import type { SelectOptions, WhereObject } from "./where-clause-types";
14
15
  // the plain `SchemaTable` param.
15
16
  type WritableTable = (SchemaTable | EntityTableMeta) & NotExecutorOnly;
16
17
 
18
+ // db.global(table)'s surface: reads are unrestricted; writes require the managed
19
+ // EntityTable's executor-only brand to be absent (mirrors WritableTable above).
20
+ type GlobalReads = {
21
+ selectMany<T = Record<string, unknown>>(
22
+ where?: WhereObject,
23
+ options?: SelectOptions,
24
+ ): Promise<readonly T[]>;
25
+ fetchOne<T = Record<string, unknown>>(where: WhereObject): Promise<T | undefined>;
26
+ };
27
+ type GlobalWrites = {
28
+ insertOne<T = Record<string, unknown>>(values: Record<string, unknown>): Promise<T | undefined>;
29
+ updateMany<T = Record<string, unknown>>(
30
+ set: Record<string, unknown>,
31
+ where: WhereObject,
32
+ ): Promise<readonly T[]>;
33
+ deleteMany(where: WhereObject): Promise<void>;
34
+ };
35
+ // Checks `extends ExecutorOnly`, not `NotExecutorOnly` — the latter's optional-never
36
+ // property is a TS "weak type" that would silently collapse every TTable to GlobalReads.
37
+ export type GlobalTableDb<TTable> = GlobalReads &
38
+ (TTable extends ExecutorOnly ? unknown : GlobalWrites);
39
+
17
40
  /**
18
41
  * TenantDb scope modes:
19
42
  *
@@ -34,8 +57,18 @@ export type TenantDb = {
34
57
  * Underlying DbRunner. Framework-internal use (event-store, migrations) —
35
58
  * bypasses tenant-filter. Feature code uses the typed helpers above so the
36
59
  * automatic scoping stays intact.
60
+ * @deprecated Use `ctx.systemDb.unsafeRaw(reason)` or `db.global(table)`
61
+ * instead — both make the cross-tenant intent an explicit, named
62
+ * declaration instead of a silent unfiltered escape hatch. Removal fw#2860.
37
63
  */
38
64
  readonly raw: DbRunner;
65
+ /**
66
+ * Reach a "global" table with the tenant filter lifted — reads always work; writes
67
+ * reject unless the write handler declared `escapeHatch: { reason }`. "tenant"-tenancy is a compile error here.
68
+ */
69
+ global<TTable extends (SchemaTable | EntityTableMeta) & TenancyBrand<"global">>(
70
+ table: TTable,
71
+ ): GlobalTableDb<TTable>;
39
72
  selectMany<T = Record<string, unknown>>(
40
73
  table: SchemaTable,
41
74
  where?: WhereObject,
@@ -68,6 +101,11 @@ export type UncheckedSystemDb = {
68
101
  assertTenantMatch(tenantId: TenantId): TenantDb;
69
102
  assertRowsTenant<T>(rows: readonly T[], tenantField: keyof T): readonly T[];
70
103
  acknowledgeCrossTenant(reason: string): TenantDb;
104
+ /**
105
+ * Raw unfiltered DbRunner, gated behind a mandatory non-empty `reason` for auditability.
106
+ * Throws on an empty (or whitespace-only) reason.
107
+ */
108
+ unsafeRaw(reason: string): DbRunner;
71
109
  // Same self-check pair as above, but hands back the caller's
72
110
  // ctx.dbOutsideTransaction TenantDb instead of the in-tx one — for
73
111
  // durability writes that must survive a rollback of the handler's own