@cosmicdrift/kumiko-types 0.197.1 → 0.199.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.1",
3
+ "version": "0.199.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-
package/src/feature.ts CHANGED
@@ -932,6 +932,10 @@ export type Registry = {
932
932
  ): readonly SearchPayloadContributorFn[];
933
933
  getHandlerEntity(qualifiedHandler: string): string | undefined;
934
934
  isHandlerSystemScoped(qualifiedHandler: string): boolean;
935
+ // Job counterpart to isHandlerSystemScoped — a job's owning feature is
936
+ // tracked separately (jobFeatureMap) since jobs aren't write/query/stream
937
+ // handlers and don't populate handlerFeatureMap.
938
+ isJobSystemScoped(qualifiedJobName: string): boolean;
935
939
  getHandlerFeature(qualifiedHandler: string): string | undefined;
936
940
  // True iff at least one registered handler declares a `rateLimit`
937
941
  // option. Pre-computed at registry-build so the boot path can skip
package/src/handlers.ts CHANGED
@@ -177,6 +177,7 @@ export type WriteResult<TData = unknown> =
177
177
  // Forward import: Registry is in feature.ts (circular type import — fine in TS)
178
178
  import type { Registry } from "./feature";
179
179
  import type { TenantId } from "./identifiers";
180
+ import type { UncheckedSystemDb } from "./tenant-db-types";
180
181
 
181
182
  // Minimal interface for job event triggers (framework-owned, concrete type in jobs/)
182
183
  export type JobRunnerRef = {
@@ -187,6 +188,17 @@ export type JobRunnerRef = {
187
188
  ): Promise<void>;
188
189
  };
189
190
 
191
+ // Minimal interface a JobRunner attaches via attachDispatcher() to reach the
192
+ // write pipeline — framework-owned, concrete type wraps the dispatcher's
193
+ // executeWrite/executeQuery. Unlike HandlerContext.write (identity implicit
194
+ // via closure), both take an explicit user: a JobRunner is a boot-time
195
+ // singleton, not bound to one caller's identity. Only write+queryAs — no
196
+ // .query/.stream/.command/.batch/.resolveAuthClaims, that stays Dispatcher-only.
197
+ export type DispatchWriteRef = {
198
+ readonly write: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
199
+ readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
200
+ };
201
+
190
202
  // Priority levels for notifications
191
203
  export type NotifyPriority = "critical" | "normal" | "low";
192
204
 
@@ -347,6 +359,9 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
347
359
  // outside the DB. `undefined` when the pipeline has no outside-tx source
348
360
  // for this dispatch (see dispatch-shared.ts) — callers must check before use.
349
361
  readonly dbOutsideTransaction: TenantDb | undefined;
362
+ // Only present for r.systemScope() handlers, bound to the same `db` as above.
363
+ // Non-system handlers never receive this — reach for `db` instead.
364
+ readonly systemDb?: UncheckedSystemDb;
350
365
  readonly registry: Registry;
351
366
  /** Aktiver SessionUser des Handler-Aufrufs — Convenience-Alias zu
352
367
  * `event.user`. Existiert weil Handler intuitiv `ctx.user.tenantId`
@@ -544,13 +559,43 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
544
559
  readonly resolveAuthClaims: (user: SessionUser) => Promise<Record<string, unknown>>;
545
560
  };
546
561
 
547
- // Job execution: db + registry + systemUser + logging guaranteed
562
+ // Job execution: db + registry + systemUser + logging guaranteed, plus a
563
+ // write path into the same dispatcher pipeline write-handlers use.
564
+ //
565
+ // write/queryAs throw until JobRunner.attachDispatcher() has run — a call
566
+ // this early is a framework boot bug, not a job-author mistake.
567
+ //
568
+ // Identity is implicit: write runs as the job's own systemUser, tenant-
569
+ // scoped via the job's _tenantId (falls back to SYSTEM_TENANT_ID for jobs
570
+ // with no tenant context, e.g. plain cron jobs). A job that writes without
571
+ // fanning out per-tenant (`_perTenant:` trigger) silently writes into the
572
+ // system tenant instead of each tenant it may have meant to reach — no
573
+ // guard against this, only this warning.
548
574
  export type JobContext = SharedContextFields & {
549
575
  readonly db: DbConnection;
550
576
  readonly registry: Registry;
551
577
  readonly systemUser: SessionUser;
552
578
  readonly log: Logger;
553
579
  readonly triggeredBy: { readonly id: string; readonly tenantId: TenantId } | null;
580
+ // Only present for jobs whose owning feature declares r.systemScope(),
581
+ // mirroring HandlerContext.systemDb (dispatch-shared.ts buildHandlerContext).
582
+ // assertTenantMatch()/acknowledgeCrossTenant() return a TenantDb, not the
583
+ // raw DbConnection above — job code that needs a DbRunner for a helper
584
+ // like reindexEntity() reaches through `.raw` on that TenantDb. `.raw`
585
+ // bypasses tenant filtering entirely, so it's only safe to hand to a
586
+ // helper that filters by tenantId itself (as reindexEntity does) — never
587
+ // pass it to code that trusts the connection to already be scoped.
588
+ readonly systemDb?: UncheckedSystemDb;
589
+ readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
590
+ readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
591
+ // Multi-trigger jobs (`on: [...]`) use this to tell which trigger fired —
592
+ // undefined for cron/manual jobs. Mirrors AppContext.triggerName.
593
+ readonly triggerName?: string;
594
+ // Fallback tenant/user when systemUser doesn't carry it (tenant-less cron
595
+ // jobs). Mirrors AppContext._tenantId/_userId — see the tenant-scoping
596
+ // footgun above.
597
+ readonly _tenantId?: TenantId;
598
+ readonly _userId?: string | undefined;
554
599
  };
555
600
 
556
601
  // --- 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
  };
@@ -56,3 +56,16 @@ export type TenantDb = {
56
56
  ): Promise<readonly T[]>;
57
57
  deleteMany(table: WritableTable, where: WhereObject): Promise<void>;
58
58
  };
59
+
60
+ // Symbol.for (global registry) so the brand identity matches even if kumiko-types
61
+ // resolves to two independent copies (workspace symlink vs. published npm) — a plain
62
+ // `Symbol()` per copy would make each resolution's `UncheckedSystemDb` structurally
63
+ // incompatible with the other, per the kumiko.secret precedent in secrets-types.ts.
64
+ export const SYSTEM_SCOPE_CHECK_BRAND: unique symbol = Symbol.for("kumiko.system-scope-check");
65
+
66
+ export type UncheckedSystemDb = {
67
+ readonly [SYSTEM_SCOPE_CHECK_BRAND]: true;
68
+ assertTenantMatch(tenantId: TenantId): TenantDb;
69
+ assertRowsTenant<T>(rows: readonly T[], tenantField: keyof T): readonly T[];
70
+ acknowledgeCrossTenant(reason: string): TenantDb;
71
+ };