@cosmicdrift/kumiko-types 0.197.0 → 0.198.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.197.0",
3
+ "version": "0.198.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>",
@@ -162,6 +162,10 @@
162
162
  "types": "./src/nav.ts",
163
163
  "default": "./src/nav.ts"
164
164
  },
165
+ "./nav-icon": {
166
+ "types": "./src/nav-icon.ts",
167
+ "default": "./src/nav-icon.ts"
168
+ },
165
169
  "./ownership": {
166
170
  "types": "./src/ownership.ts",
167
171
  "default": "./src/ownership.ts"
package/src/config.ts CHANGED
@@ -3,7 +3,7 @@ import type { ConcurrencyMode } from "./concurrency-mode";
3
3
  import type { ConfigScope } from "./config-scope";
4
4
  import type { DbConnection } from "./db-connection";
5
5
  import type { FieldDefinition } from "./fields";
6
- import type { AppContext } from "./handlers";
6
+ import type { JobContext } from "./handlers";
7
7
  import type {
8
8
  PostDeleteHookFn,
9
9
  PostSaveHookFn,
@@ -12,6 +12,7 @@ import type {
12
12
  PreSaveHookFn,
13
13
  } from "./hooks";
14
14
  import type { TenantId } from "./identifiers";
15
+ import type { NavIconKey } from "./nav-icon";
15
16
  import type { TenantDb } from "./tenant-db-types";
16
17
 
17
18
  // --- Config ---
@@ -154,12 +155,12 @@ export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
154
155
  readonly group?: string;
155
156
  };
156
157
 
157
- // Label-Träger für den Settings-Hub. `title` ist ein i18n-Key (kein Literal
158
- // Guard), `icon` ein Icon-Registry-Key für den Nav-Eintrag, `order` die
159
- // Sortier-Gewichtung innerhalb seiner Audience-Gruppe.
158
+ // Label carrier for the settings hub. `title` is an i18n key (no literal
159
+ // guarded), `icon` a NavIconKey for the nav entry, `order` the sort weight
160
+ // within its audience group.
160
161
  export type ConfigMask = {
161
162
  readonly title: string;
162
- readonly icon?: string;
163
+ readonly icon?: NavIconKey;
163
164
  readonly order?: number;
164
165
  };
165
166
 
@@ -335,7 +336,7 @@ export type JobRunIn = Exclude<RunIn, "both">;
335
336
 
336
337
  // --- Jobs ---
337
338
 
338
- export type JobHandlerFn = (payload: Record<string, unknown>, context: AppContext) => Promise<void>;
339
+ export type JobHandlerFn = (payload: Record<string, unknown>, context: JobContext) => Promise<void>;
339
340
 
340
341
  export type JobTrigger =
341
342
  // `on` akzeptiert ein einzelnes Handler-Ref ODER eine Liste. Multi-
@@ -35,10 +35,12 @@ export type VariantSpec = {
35
35
  //
36
36
  // The renderer must produce `spec.format` when set, and the source's own
37
37
  // format otherwise. It never reports back what it produced: the caller
38
- // derives the output mimeType from the spec alone (see outputMimeType in
39
- // derivatives-context.ts), because a cache hit has no renderer run to read
40
- // a mimeType off, and a spec-derived value is the only one both branches
41
- // can share.
38
+ // derives the output mimeType from the spec plus the source FileRef's
39
+ // mimeType (see outputMimeType in derivatives-context.ts), because a cache
40
+ // hit has no renderer run to read a mimeType off. When `spec.format` is
41
+ // unset, the source mimeType is client-controlled, so outputMimeType
42
+ // allowlists it against known-safe raster types instead of passing it
43
+ // through verbatim (#2021).
42
44
  export type DerivativeRendererPlugin = {
43
45
  readonly render: (
44
46
  input: Uint8Array,
package/src/handlers.ts CHANGED
@@ -187,6 +187,17 @@ export type JobRunnerRef = {
187
187
  ): Promise<void>;
188
188
  };
189
189
 
190
+ // Minimal interface a JobRunner attaches via attachDispatcher() to reach the
191
+ // write pipeline — framework-owned, concrete type wraps the dispatcher's
192
+ // executeWrite/executeQuery. Unlike HandlerContext.write (identity implicit
193
+ // via closure), both take an explicit user: a JobRunner is a boot-time
194
+ // singleton, not bound to one caller's identity. Only write+queryAs — no
195
+ // .query/.stream/.command/.batch/.resolveAuthClaims, that stays Dispatcher-only.
196
+ export type DispatchWriteRef = {
197
+ readonly write: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
198
+ readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
199
+ };
200
+
190
201
  // Priority levels for notifications
191
202
  export type NotifyPriority = "critical" | "normal" | "low";
192
203
 
@@ -544,13 +555,34 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
544
555
  readonly resolveAuthClaims: (user: SessionUser) => Promise<Record<string, unknown>>;
545
556
  };
546
557
 
547
- // Job execution: db + registry + systemUser + logging guaranteed
558
+ // Job execution: db + registry + systemUser + logging guaranteed, plus a
559
+ // write path into the same dispatcher pipeline write-handlers use.
560
+ //
561
+ // write/queryAs throw until JobRunner.attachDispatcher() has run — a call
562
+ // this early is a framework boot bug, not a job-author mistake.
563
+ //
564
+ // Identity is implicit: write runs as the job's own systemUser, tenant-
565
+ // scoped via the job's _tenantId (falls back to SYSTEM_TENANT_ID for jobs
566
+ // with no tenant context, e.g. plain cron jobs). A job that writes without
567
+ // fanning out per-tenant (`_perTenant:` trigger) silently writes into the
568
+ // system tenant instead of each tenant it may have meant to reach — no
569
+ // guard against this, only this warning.
548
570
  export type JobContext = SharedContextFields & {
549
571
  readonly db: DbConnection;
550
572
  readonly registry: Registry;
551
573
  readonly systemUser: SessionUser;
552
574
  readonly log: Logger;
553
575
  readonly triggeredBy: { readonly id: string; readonly tenantId: TenantId } | null;
576
+ readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
577
+ readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
578
+ // Multi-trigger jobs (`on: [...]`) use this to tell which trigger fired —
579
+ // undefined for cron/manual jobs. Mirrors AppContext.triggerName.
580
+ readonly triggerName?: string;
581
+ // Fallback tenant/user when systemUser doesn't carry it (tenant-less cron
582
+ // jobs). Mirrors AppContext._tenantId/_userId — see the tenant-scoping
583
+ // footgun above.
584
+ readonly _tenantId?: TenantId;
585
+ readonly _userId?: string | undefined;
554
586
  };
555
587
 
556
588
  // --- Handler Functions ---
@@ -15,6 +15,14 @@ export type TenantId = string;
15
15
  // the system boundary.
16
16
  const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
17
17
 
18
+ // Shared shape check for any UUID-formatted identifier crossing an
19
+ // untrusted boundary (fileRefId, entity ids, ...) — same loose,
20
+ // version-agnostic regex as TENANT_ID_REGEX (v4/v7/nil all match), kept as
21
+ // one predicate so call sites don't grow their own copy of the shape.
22
+ export function isUuid(value: unknown): value is string {
23
+ return typeof value === "string" && TENANT_ID_REGEX.test(value);
24
+ }
25
+
18
26
  // Validates a candidate string against the tenantId format and returns it
19
27
  // as a TenantId, or `null` when it doesn't match. Use at every system
20
28
  // boundary that admits untrusted input (HTTP headers, cookies, query
@@ -22,9 +30,7 @@ const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-
22
30
  // of the rejection shape — middleware returns 400, batch jobs may filter
23
31
  // + log, and unit tests don't need a try/catch.
24
32
  export function parseTenantId(value: unknown): TenantId | null {
25
- if (typeof value !== "string") return null;
26
- if (!TENANT_ID_REGEX.test(value)) return null;
27
- return value;
33
+ return isUuid(value) ? value : null;
28
34
  }
29
35
 
30
36
  // "System-scope" tenant marker: handlers carry this tenantId when the event
@@ -0,0 +1,56 @@
1
+ // Closed vocabulary of nav icon keys. NavDefinition.icon, ScreenNavSugar.icon
2
+ // and ConfigMask.icon are all typed against this union, so an unregistered
3
+ // key is a compile error at the r.nav()/r.screen()/config-mask call site
4
+ // instead of a silent missing-icon at runtime.
5
+ //
6
+ // The renderer-web NAV_ICONS map (packages/renderer-web/src/layout/
7
+ // nav-tree.tsx) is checked against this same union via `satisfies`, so the
8
+ // two can't drift — add a key here only together with its lucide-react entry
9
+ // there, and vice versa.
10
+ export type NavIconKey =
11
+ | "dashboard"
12
+ | "layout-grid"
13
+ | "book-open"
14
+ | "clipboard-list"
15
+ | "package"
16
+ | "gauge"
17
+ | "list"
18
+ | "table"
19
+ | "layers"
20
+ | "building"
21
+ | "calculator"
22
+ | "wallet"
23
+ | "coins"
24
+ | "credit-card"
25
+ | "piggy-bank"
26
+ | "receipt"
27
+ | "chart"
28
+ | "bar-chart"
29
+ | "trending"
30
+ | "sparkles"
31
+ | "wand"
32
+ | "calendar"
33
+ | "file"
34
+ | "folder"
35
+ | "folder-open"
36
+ | "home"
37
+ | "bell"
38
+ | "shield"
39
+ | "settings"
40
+ | "users"
41
+ | "user"
42
+ | "search"
43
+ | "tag"
44
+ | "key"
45
+ | "link"
46
+ | "palette"
47
+ | "share"
48
+ | "server"
49
+ | "mail"
50
+ | "lock"
51
+ | "hash"
52
+ | "download"
53
+ | "upload"
54
+ | "rocket"
55
+ | "plus"
56
+ | "languages";
package/src/nav.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { AccessRule } from "./handlers";
2
+ import type { NavIconKey } from "./nav-icon";
2
3
  import type { TargetRef } from "./target-ref";
3
4
  import type { TreeAction } from "./tree-node";
4
5
 
6
+ export type { NavIconKey } from "./nav-icon";
7
+
5
8
  // Nav entry declaration. Every feature that wants to appear in the app's
6
9
  // navigation tree registers one or more entries via r.nav(). The engine
7
10
  // keeps the list flat — ui-core's resolveNavigation assembles the parent/
@@ -22,10 +25,10 @@ export type NavDefinition = {
22
25
  // i18n translation key. Resolved at render time by the renderer's
23
26
  // useTranslation hook; engine keeps it opaque.
24
27
  readonly label: string;
25
- // Icon key whatever the icon registry of the active renderer understands.
26
- // Engine doesn't validate; unknown icons surface as a missing icon on screen,
27
- // not a boot failure.
28
- readonly icon?: string;
28
+ // Icon key from the closed NavIconKey vocabulary (see nav-icon.ts). Every
29
+ // renderer's icon map is checked against the same union, so a typo'd key
30
+ // is a compile error here rather than a missing icon at runtime.
31
+ readonly icon?: NavIconKey;
29
32
  // Qualified name of a parent nav entry ("<feature>:nav:<id>"). Omit for
30
33
  // top-level entries. Boot-validator rejects cycles + dangling refs.
31
34
  readonly parent?: string;
@@ -113,7 +116,7 @@ export type ContentCollectionDefinition = {
113
116
  readonly variableSchema?: Readonly<Record<string, string>>;
114
117
  readonly nav: {
115
118
  readonly label: string;
116
- readonly icon?: string;
119
+ readonly icon?: NavIconKey;
117
120
  readonly parent?: string;
118
121
  readonly order?: number;
119
122
  // Visibility of the nav node. Leave unset — it then follows the
package/src/screen.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { FieldDefinition } from "./fields";
2
2
  import type { AccessRule } from "./handlers";
3
+ import type { NavIconKey } from "./nav-icon";
3
4
 
4
5
  // Screen definitions describe how a feature surfaces data to the user.
5
6
  // Pure data — the engine stores these verbatim and ui-core / the renderer
@@ -704,6 +705,16 @@ export type CustomScreenDefinition = {
704
705
  /** Parent list screen for breadcrumb when this detail is not in nav. */
705
706
  readonly listScreenId?: string;
706
707
  readonly access?: AccessRule;
708
+ /** This screen is registered without a self-owned `r.nav()` entry by
709
+ * design — an app opts in by navving it explicitly (e.g. a settings-area
710
+ * self-service screen). `createKumikoApp`'s boot diagnostic
711
+ * (kumiko-framework#2025) skips dormant screens instead of flagging
712
+ * every consumer that doesn't mount the client plugin as a false
713
+ * positive (kumiko-framework#2034). Only set this on screens the
714
+ * feature itself never navs — a screen the feature DOES nav still needs
715
+ * its client plugin mounted by every consumer, and should keep
716
+ * triggering the diagnostic if it's missing. */
717
+ readonly dormant?: boolean;
707
718
  };
708
719
 
709
720
  // --- configEdit ---
@@ -790,7 +801,7 @@ export type ScreenSlots = {
790
801
  // declare a standalone `r.nav()` entry instead.
791
802
  export type ScreenNavSugar = {
792
803
  readonly label: string;
793
- readonly icon?: string;
804
+ readonly icon?: NavIconKey;
794
805
  readonly parent?: string;
795
806
  readonly order?: number;
796
807
  };