@cosmicdrift/kumiko-types 0.372.0 → 0.374.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.
@@ -513,7 +513,8 @@ export type EntityRef = {
513
513
  export type HandlerRef = {
514
514
  readonly name: string;
515
515
  };
516
- export type RateLimitPer = "user" | "tenant" | "ip" | "user+handler" | "tenant+handler" | "ip+handler";
516
+ export declare const RATE_LIMIT_PERS: readonly ["user", "tenant", "ip", "user+handler", "tenant+handler", "ip+handler"];
517
+ export type RateLimitPer = (typeof RATE_LIMIT_PERS)[number];
517
518
  export type RateLimitOption = {
518
519
  readonly per: RateLimitPer;
519
520
  readonly limit: number;
@@ -531,11 +532,14 @@ export type PayloadRateLimitOption = {
531
532
  readonly cost?: number;
532
533
  readonly normalize?: PayloadBucketNormalization;
533
534
  };
535
+ export declare function isRateLimitOption(value: unknown): value is RateLimitOption;
536
+ export type RateLimitOptions = readonly [RateLimitOption, ...RateLimitOption[]];
534
537
  export type RateLimitDisabled = {
535
538
  readonly disabled: true;
536
539
  readonly reason: string;
537
540
  };
538
- export type RateLimitDeclaration = RateLimitOption | RateLimitDisabled;
541
+ export type RateLimitDeclaration = RateLimitOption | RateLimitOptions | RateLimitDisabled;
542
+ export declare function declaredRateLimitOptions(declaration: RateLimitDeclaration | undefined): readonly RateLimitOption[];
539
543
  export declare function isRateLimitDisabled(v: RateLimitDeclaration | undefined): v is RateLimitDisabled;
540
544
  export type AgentRisk = "low" | "mid" | "high";
541
545
  /** Per-handler hints for the AI-agent manifest. `expose` overrides the
package/dist/handlers.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isOneOf, propertyOf } from "./type-guards.js";
1
2
  export const ESCAPE_HATCH_GRANTS = ["systemIdentity", "globalWrites", "unsafeRaw"];
2
3
  export function isEscapeHatchGrant(value) {
3
4
  return ESCAPE_HATCH_GRANTS.some((grant) => grant === value);
@@ -98,8 +99,41 @@ export function normalizeEventPiiSubject(spec) {
98
99
  return { kind: "self" };
99
100
  return { kind: "user", ownerField: spec.personal.of, whenAbsent: spec.personal.whenAbsent };
100
101
  }
102
+ // --- Handler Definitions (stored in feature/registry) ---
103
+ // Per-handler rate limit. Bucket key derived from `per`:
104
+ // "user" → userId
105
+ // "tenant" → tenantId
106
+ // "ip" → request IP
107
+ // "user+handler" → userId + handlerName
108
+ // "tenant+handler" → tenantId + handlerName
109
+ // "ip+handler" → IP + handlerName (anonymous endpoints)
110
+ // `cost` is the tokens this handler-call deducts. Default 1 — bump for
111
+ // expensive operations (bulk export, bulk import).
112
+ export const RATE_LIMIT_PERS = [
113
+ "user",
114
+ "tenant",
115
+ "ip",
116
+ "user+handler",
117
+ "tenant+handler",
118
+ "ip+handler",
119
+ ];
120
+ export function isRateLimitOption(value) {
121
+ const cost = propertyOf(value, "cost");
122
+ return (isOneOf(RATE_LIMIT_PERS, propertyOf(value, "per")) &&
123
+ typeof propertyOf(value, "limit") === "number" &&
124
+ typeof propertyOf(value, "windowSeconds") === "number" &&
125
+ (cost === undefined || typeof cost === "number"));
126
+ }
127
+ export function declaredRateLimitOptions(declaration) {
128
+ if (declaration === undefined || isRateLimitDisabled(declaration))
129
+ return [];
130
+ return isRateLimitOptionList(declaration) ? declaration : [declaration];
131
+ }
132
+ function isRateLimitOptionList(declaration) {
133
+ return Array.isArray(declaration);
134
+ }
101
135
  export function isRateLimitDisabled(v) {
102
- return v !== undefined && "disabled" in v && v.disabled === true;
136
+ return v !== undefined && !Array.isArray(v) && "disabled" in v && v.disabled === true;
103
137
  }
104
138
  // Marker set by defineEntityQueryHandler on both of its verbs. A string, not
105
139
  // a Symbol(), for the same reason as PAGED_QUERY_HANDLER_BRAND: a symlinked
@@ -13,8 +13,13 @@ export type RateLimitConfig = {
13
13
  readonly windowSeconds: number;
14
14
  readonly cost?: number;
15
15
  };
16
+ export type RateLimitBucketRequest = {
17
+ readonly bucket: string;
18
+ readonly config: RateLimitConfig;
19
+ };
16
20
  export type RateLimitResolver = {
17
21
  check(bucket: string, config: RateLimitConfig): Promise<RateLimitDecision>;
18
22
  enforce(bucket: string, config: RateLimitConfig): Promise<RateLimitDecision>;
23
+ enforceAll(requests: readonly RateLimitBucketRequest[]): Promise<readonly RateLimitDecision[]>;
19
24
  peek(bucket: string, config: Omit<RateLimitConfig, "cost">): Promise<RateLimitDecision>;
20
25
  };
package/dist/screen.d.ts CHANGED
@@ -167,7 +167,7 @@ export type RowActionDisplay = "button" | "link" | "icon" | "responsive";
167
167
  * the rest in a kebab menu — except actions with an explicit `display`, which
168
168
  * stay inline. `"inline"`: every action inline, no kebab. */
169
169
  export type RowActionMode = "adaptive" | "inline";
170
- export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer;
170
+ export type RowAction = RowActionWriteHandler | RowActionNavigate | RowActionDrawer | RowActionClientAction;
171
171
  export type RowActionWriteHandler = {
172
172
  /** Default für RowActions ohne explizit gesetzten `kind` —
173
173
  * Backwards-kompatible Form. Kann auch explizit gesetzt werden. */
@@ -293,6 +293,37 @@ export type RowActionDrawer = {
293
293
  /** Line under the drawer heading (i18n key or a bare `{name}` template),
294
294
  * resolved like `title`. */
295
295
  readonly subtitle?: string;
296
+ /** Clicking the row opens this drawer with that row's `params`. At most one
297
+ * `rowClick` action (navigate or drawer) per list; rejected on
298
+ * projectionDetail header actions, like navigate's. */
299
+ readonly rowClick?: boolean;
300
+ };
301
+ export type RowActionClientAction = {
302
+ readonly kind: "clientAction";
303
+ readonly id: string;
304
+ readonly label: string;
305
+ /** Registered client handler name, `<feature>:<kebab>`, e.g.
306
+ * "auth-email-password:request-password-reset". Resolved when the action is
307
+ * triggered; a name no client feature registered shows an error toast. */
308
+ readonly action: string;
309
+ /** Payload handed to the handler, picked from the row (or the shown record).
310
+ * Default `{ id: row.id }`, like a writeHandler. */
311
+ readonly payload?: RowFieldExtractor;
312
+ /** i18n key for the confirm dialog description. */
313
+ readonly confirm?: string;
314
+ /** i18n key for the confirm button, default `label`. */
315
+ readonly confirmLabel?: string;
316
+ /** Conditional visibility per row. */
317
+ readonly visible?: FieldCondition;
318
+ /** Visual style. "danger" forces a confirm dialog, like a writeHandler. */
319
+ readonly style?: "primary" | "secondary" | "danger";
320
+ /** Overrides the id-based default icon, see `RowActionWriteHandler.icon`. */
321
+ readonly icon?: IconKey;
322
+ /** Inline render form in the row-action column — see `RowActionDisplay`. */
323
+ readonly display?: RowActionDisplay;
324
+ /** i18n key of the toast shown after the handler reported `{ ok: true }`
325
+ * (default `kumiko.clientAction.success`). */
326
+ readonly successMessage?: string;
296
327
  };
297
328
  export type ToolbarAction = {
298
329
  readonly kind: "navigate";
@@ -692,18 +723,30 @@ export type DashboardDateParam = {
692
723
  readonly kind: "date";
693
724
  readonly atMs: number;
694
725
  };
726
+ /** Epoch ms, rendered relative to now ("5 minutes ago", "in 30 seconds") in the
727
+ * user locale; the text keeps ticking while it is on screen. */
728
+ export type DashboardRelativeTimeParam = {
729
+ readonly kind: "relativeTime";
730
+ readonly atMs: number;
731
+ };
695
732
  /** Nested DashboardI18nText params resolve first (e.g. a duration "{years} {months}"
696
733
  * with one plural key each); numbers stay raw so plural rules can read `count`. */
697
- export type DashboardTextParam = string | number | DashboardI18nText | DashboardMoneyParam | DashboardDateParam;
734
+ export type DashboardTextParam = string | number | DashboardI18nText | DashboardMoneyParam | DashboardDateParam | DashboardRelativeTimeParam;
698
735
  /** Plain string is displayed as-is; DashboardI18nText is translated by the renderer. */
699
736
  export type DashboardText = string | DashboardI18nText;
700
737
  /** Chart/stat values are minor units, formatted with the user locale. */
701
- export type DashboardValueFormat = {
738
+ export type DashboardCurrencyFormat = {
702
739
  readonly kind: "currency";
703
740
  readonly currency: string;
704
741
  /** Defaults to the currency's own decimals. */
705
742
  readonly fractionDigits?: number;
706
743
  };
744
+ /** The stat value is an epoch ms timestamp, shown relative to now and kept current. */
745
+ export type DashboardRelativeTimeFormat = {
746
+ readonly kind: "relativeTime";
747
+ };
748
+ /** Stat panels take both formats; chart panels only the currency one. */
749
+ export type DashboardValueFormat = DashboardCurrencyFormat | DashboardRelativeTimeFormat;
707
750
  export type DashboardPanelQueryOptions = {
708
751
  /** Static, author-set query parameters merged on top of the screen's
709
752
  * dynamic filterParams — lets a panel pin a value (e.g. a status facet)
@@ -737,7 +780,8 @@ export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate
737
780
  readonly valueField: string;
738
781
  readonly subField?: string;
739
782
  readonly toneField?: string;
740
- /** Formats a numeric value as currency (minor units); ignored for string values. The sparkline is not formatted. */
783
+ /** Formats a numeric value as currency (minor units) or, with `relativeTime`, as
784
+ * a ticking "5 minutes ago" from epoch ms; ignored for string values. The sparkline is not formatted. */
741
785
  readonly valueFormat?: DashboardValueFormat;
742
786
  /** Fixed tone when `toneField` is unset or yields none; not applied while the value is 0. */
743
787
  readonly tone?: DashboardStatTone;
@@ -802,7 +846,9 @@ export type DashboardChartPanel = DashboardPanelQueryOptions & DashboardPanelEmp
802
846
  readonly seriesColors?: Readonly<Record<string, string>>;
803
847
  /** Formats values as currency (minor units): y ticks, legend totals and tooltips on
804
848
  * stacked-bars/segment-bars/stacked-area, only the y ticks on timeseries. */
805
- readonly valueFormat?: DashboardValueFormat;
849
+ readonly valueFormat?: DashboardCurrencyFormat;
850
+ /** i18n key of a unit label. Only for chart "timeseries": shown once above the y ticks instead of on every tick. */
851
+ readonly unit?: string;
806
852
  /** Only for chart "stacked-area": fixed width per bucket, plot scrolls horizontally. */
807
853
  readonly scrollable?: boolean;
808
854
  /** Only for chart "stacked-area": marker `kind` -> look. Markers without a
@@ -849,6 +895,7 @@ export type DashboardFeedPanel = DashboardPanelQueryOptions & DashboardPanelEmpt
849
895
  readonly label: string;
850
896
  readonly query: string;
851
897
  };
898
+ export type DashboardProgressTone = "default" | "danger";
852
899
  export type DashboardProgressListPanel = DashboardPanelQueryOptions & DashboardPanelEmptyState & DashboardPanelGate & {
853
900
  readonly kind: "progress-list";
854
901
  readonly id: string;
@@ -860,6 +907,32 @@ export type DashboardCustomPanel = {
860
907
  readonly id: string;
861
908
  readonly component: PlatformComponent;
862
909
  };
910
+ export type DashboardTextBlock = {
911
+ readonly kind: "heading";
912
+ readonly text: string;
913
+ } | {
914
+ readonly kind: "paragraph";
915
+ readonly text: string;
916
+ /** Default "muted", like body copy inside panels. */
917
+ readonly emphasis?: "muted" | "strong";
918
+ } | {
919
+ readonly kind: "list";
920
+ readonly items: readonly string[];
921
+ } | {
922
+ readonly kind: "code";
923
+ readonly code: string;
924
+ } | {
925
+ readonly kind: "hint";
926
+ readonly text: string;
927
+ };
928
+ export type DashboardTextPanel = DashboardPanelGate & {
929
+ readonly kind: "text";
930
+ readonly id: string;
931
+ /** Panel title (i18n key). */
932
+ readonly label?: string;
933
+ readonly span?: DashboardPanelSpan;
934
+ readonly blocks: readonly DashboardTextBlock[];
935
+ };
863
936
  export type DashboardPanelVisibility = {
864
937
  readonly query: string;
865
938
  readonly field: string;
@@ -872,13 +945,17 @@ export type DashboardScreenPanel = {
872
945
  readonly screen: string;
873
946
  readonly label?: string;
874
947
  readonly visibleWhen?: DashboardPanelVisibility;
948
+ /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` embedded in this
949
+ * panel's place when `visibleWhen` is unmet or its query fails (nothing while it loads).
950
+ * Needs `visibleWhen`; the target must not have a `visibleWhen` of its own. */
951
+ readonly fallback?: string;
875
952
  /** Render the embedded form without its own card frame and padding, aligned to
876
953
  * the dashboard's page grid like a standalone settings screen. Default false
877
954
  * (framed card); set for panels whose body is a settings-list form, where a
878
955
  * card inside the page would double the inset. */
879
956
  readonly chromeless?: boolean;
880
957
  };
881
- export type DashboardPanelDefinition = DashboardStatPanel | DashboardStatGroupPanel | DashboardChartPanel | DashboardListPanel | DashboardFeedPanel | DashboardProgressListPanel | DashboardCustomPanel | DashboardScreenPanel;
958
+ export type DashboardPanelDefinition = DashboardStatPanel | DashboardStatGroupPanel | DashboardChartPanel | DashboardListPanel | DashboardFeedPanel | DashboardProgressListPanel | DashboardTextPanel | DashboardCustomPanel | DashboardScreenPanel;
882
959
  export type DashboardFilterDefinition = {
883
960
  readonly id: string;
884
961
  readonly label: string;
@@ -906,6 +983,30 @@ export type DashboardScopeDefinition = {
906
983
  readonly badge: string;
907
984
  readonly notice?: string;
908
985
  };
986
+ /** Minimum polling interval of `DashboardScreenDefinition.refresh`; shorter values are rejected
987
+ * at boot (`seconds`) or raised at runtime (`field`). */
988
+ export declare const DASHBOARD_MIN_REFRESH_SECONDS = 5;
989
+ export type DashboardRecordBinding = {
990
+ /** URL search param carrying the record id. Without it the dashboard shows a
991
+ * hint instead of its panels and runs no query. */
992
+ readonly param: string;
993
+ /** Key of the id in each panel query payload. Default: `param`. */
994
+ readonly payloadKey?: string;
995
+ /** Optional query for the record row (QN). It receives `{ [payloadKey]: id }`
996
+ * and returns a flat row; `actions` take that row as their input and
997
+ * `refresh.field` and `titleField` read from it. */
998
+ readonly query?: string;
999
+ /** Field of the record row shown as the page title. Needs `query`. */
1000
+ readonly titleField?: string;
1001
+ };
1002
+ /** Polling of the record and all panel queries, never faster than
1003
+ * DASHBOARD_MIN_REFRESH_SECONDS. `field` reads the seconds from the record row
1004
+ * (needs `record.query`), so a monitor can be polled at its own check interval. */
1005
+ export type DashboardRefresh = {
1006
+ readonly seconds: number;
1007
+ } | {
1008
+ readonly field: string;
1009
+ };
909
1010
  export type DashboardScreenDefinition = {
910
1011
  readonly id: string;
911
1012
  readonly type: "dashboard";
@@ -921,6 +1022,14 @@ export type DashboardScreenDefinition = {
921
1022
  /** Same-feature short id or cross-feature QN `<feature>:screen:<id>` shown when `visibleWhen` is unmet (default: standard notice). */
922
1023
  readonly fallback?: string;
923
1024
  readonly panels: readonly DashboardPanelDefinition[];
1025
+ /** Binds the dashboard to one record via a URL search param, see DashboardRecordBinding. */
1026
+ readonly record?: DashboardRecordBinding;
1027
+ /** Header actions on the bound record (writeHandler, navigate, clientAction).
1028
+ * The row is the `record.query` row, or `{ id, [payloadKey]: id }` without a
1029
+ * query. After a success the record and all panels refetch. Needs `record`;
1030
+ * no drawer, redirect or rowClick. */
1031
+ readonly actions?: readonly RowAction[];
1032
+ readonly refresh?: DashboardRefresh;
924
1033
  readonly filter?: DashboardFilterDefinition;
925
1034
  /** Segmented control in the toolbar; the value lives in URL search param
926
1035
  * `id` and is merged into EVERY panel query (also ignoreScreenFilter ones). */
@@ -953,6 +1062,21 @@ export type EditFieldSpec = string | {
953
1062
  * `multiline` routes to a textarea (no icon slot), and other input
954
1063
  * kinds (select, combobox, date, …) silently ignore the key. */
955
1064
  readonly icon?: FieldIconKey;
1065
+ /** Input placeholder (i18n key, falls back to the literal). Only for
1066
+ * single-line text, number and textarea inputs; other kinds ignore it. */
1067
+ readonly placeholder?: string;
1068
+ /** Static text shown before the input (i18n key, falls back to the
1069
+ * literal), or `{ field }` for the current form value of another field
1070
+ * (a hidden field carrying e.g. a base domain). Only for single-line
1071
+ * text and number inputs; other kinds ignore it. */
1072
+ readonly prefix?: FieldAffix;
1073
+ /** Same as `prefix`, shown after the input. */
1074
+ readonly suffix?: FieldAffix;
1075
+ /** Button beside the input that runs a query and writes one scalar of
1076
+ * its result into this field. Also offered on a readOnly field, where
1077
+ * the value can only come from the suggestion. Only for single-line
1078
+ * text and number inputs; other kinds ignore it. */
1079
+ readonly fillAction?: FieldFillAction;
956
1080
  /** Marks this field as a reference lookup instead of a plain value —
957
1081
  * for projectionDetail fields, which have no `EntityDefinition` to
958
1082
  * carry a real `reference` field type. Same target convention as
@@ -971,6 +1095,20 @@ export type EditFieldSpec = string | {
971
1095
  * field `default`, it is not applied server-side. */
972
1096
  readonly prefill?: EditFieldPrefill;
973
1097
  };
1098
+ export type FieldAffix = string | {
1099
+ readonly field: string;
1100
+ };
1101
+ export type FieldFillAction = {
1102
+ /** i18n key (falls back to the literal) of the button label. */
1103
+ readonly label: string;
1104
+ /** Qualified query name, run with the current form values picked by `params`. */
1105
+ readonly query: string;
1106
+ /** Key in the query result written into the field; the value must be a
1107
+ * string or a number, anything else counts as a failed fill. */
1108
+ readonly field: string;
1109
+ /** Form fields sent as the query payload (none: an empty payload). */
1110
+ readonly params?: RowFieldExtractor;
1111
+ };
974
1112
  export type EmbeddedListEditSpec = {
975
1113
  /** Sub-fields shown as read-only cells; every other cell stays editable. */
976
1114
  readonly readOnlyCells?: readonly string[];
@@ -1469,6 +1607,20 @@ export type ActionFormScreenDefinition = {
1469
1607
  * the handler creates a child record but the target screen is the
1470
1608
  * parent's detail screen (fw#2670). */
1471
1609
  readonly redirect?: string | ActionFormRedirect;
1610
+ /** Links shown in the success box after a successful submit while the
1611
+ * screen stays put. Targets resolve like `redirect` (the id comes from the
1612
+ * handler's success payload); a link without a string id is hidden.
1613
+ * Mutually exclusive with `redirect`. */
1614
+ readonly successLinks?: readonly ActionFormSuccessLink[];
1615
+ /** Reset the form to its initial values after a successful submit (prefill
1616
+ * kept, typed values cleared). Only applies while the screen stays put,
1617
+ * i.e. without `redirect`. */
1618
+ readonly resetOnSuccess?: boolean;
1619
+ /** Load the initial values from a query. The record id comes from the route
1620
+ * (or the drawer row id) and is sent as `{ [idParam]: id }`; the result,
1621
+ * picked to `fields`, becomes the initial values. Turns URL prefill off:
1622
+ * search params never reach the form. */
1623
+ readonly load?: ActionFormLoad;
1472
1624
  /** Target of the Cancel button. Default: `redirect` (historical
1473
1625
  * behavior — Cancel and the submit-redirect then land in the same
1474
1626
  * place). `false` = no Cancel button; correct for single-action
@@ -1519,6 +1671,21 @@ export type ActionFormRedirect = {
1519
1671
  * Boot rejects other targets and unknown section ids. */
1520
1672
  readonly tab?: string;
1521
1673
  };
1674
+ export type ActionFormLoad = {
1675
+ /** Query QN that returns the record. */
1676
+ readonly query: string;
1677
+ /** Payload key carrying the record id (default "id"). */
1678
+ readonly idParam?: string;
1679
+ };
1680
+ /** A redirect target rendered as a link in the success box. */
1681
+ export type ActionFormSuccessLink = Omit<ActionFormRedirect, "idFrom"> & {
1682
+ /** Success-payload field carrying the navigation id. Required for a record
1683
+ * target (`entityEdit`, `projectionDetail`); omit it for a list, dashboard
1684
+ * or form target, which opens without an id. */
1685
+ readonly idFrom?: string;
1686
+ /** Link text (i18n key). */
1687
+ readonly label: string;
1688
+ };
1522
1689
  /** Author-declared field shown once after a mint-form submit — API token,
1523
1690
  * recovery codes, similar one-time secrets. */
1524
1691
  export type SecretRevealField = {
package/dist/screen.js CHANGED
@@ -8,6 +8,9 @@ export function metricField(metric) {
8
8
  // `visibleWhen` only on a top-level stat; the boot validator rejects it on a
9
9
  // stat-group child (gate the whole group instead).
10
10
  export const DASHBOARD_STAT_TONES = ["default", "positive", "ok", "warn", "negative"];
11
+ /** Minimum polling interval of `DashboardScreenDefinition.refresh`; shorter values are rejected
12
+ * at boot (`seconds`) or raised at runtime (`field`). */
13
+ export const DASHBOARD_MIN_REFRESH_SECONDS = 5;
11
14
  /** Group key of a field value; `collapsedWhen` and row values both go through it. */
12
15
  export function relatedListGroupKey(value) {
13
16
  return value === undefined || value === null ? "" : String(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-types",
3
- "version": "0.372.0",
3
+ "version": "0.374.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>",