@cosmicdrift/kumiko-types 0.363.0 → 0.365.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.
@@ -1,5 +1,8 @@
1
+ export type CacheSyncPublishOptions = {
2
+ readonly remoteOnly?: boolean;
3
+ };
1
4
  export type CacheSyncBus = {
2
- publish(topic: string, message: unknown): void;
5
+ publish(topic: string, message: unknown, options?: CacheSyncPublishOptions): void;
3
6
  subscribe(topic: string, listener: (message: unknown) => void): () => void;
4
7
  onResync(listener: () => void): () => void;
5
8
  };
package/dist/config.d.ts CHANGED
@@ -30,7 +30,7 @@ export type ConfigValidateContext = {
30
30
  readonly scope: ConfigScope;
31
31
  readonly tenantId: TenantId;
32
32
  readonly userId: string | null;
33
- readonly db: DbConnection | TenantDb;
33
+ readonly db: TenantDb;
34
34
  };
35
35
  export type ConfigValidateFn<T extends ConfigKeyType = ConfigKeyType> = (value: ConfigValue<T>, ctx: ConfigValidateContext) => Promise<void>;
36
36
  export type ConfigBacking = "config" | "secrets";
@@ -193,6 +193,9 @@ export type TemplateScalarVariableDef = (TemplateVariableCommon & {
193
193
  }) | (TemplateVariableCommon & {
194
194
  readonly type: "link";
195
195
  readonly example?: string;
196
+ }) | (TemplateVariableCommon & {
197
+ readonly type: "richText";
198
+ readonly example?: string;
196
199
  }) | (TemplateVariableCommon & {
197
200
  readonly type: "money";
198
201
  readonly example?: number | {
@@ -11,4 +11,9 @@ export type DbTx = postgres.TransactionSql<Record<string, unknown>> | RawDbClien
11
11
  export type DbRunner = DbConnection | DbTx;
12
12
  export type DbRow = Record<string, unknown>;
13
13
  export type PgClient = ReturnType<typeof postgres>;
14
- export type PgListenClient = ReturnType<typeof postgres>;
14
+ export type PgListenSubscription = {
15
+ unlisten(): Promise<void>;
16
+ };
17
+ export type PgListenClient = {
18
+ listen(channel: string, onnotify: (payload: string) => void, onlisten?: () => void): Promise<PgListenSubscription>;
19
+ };
@@ -1,9 +1,9 @@
1
1
  import type * as z from "zod";
2
2
  import type { ZodType } from "zod";
3
3
  import type { KumikoEventTypeMap } from "./event-type-map.js";
4
- import type { AccessRule, AgentHandlerHints, EscapeHatchDeclaration, HandlerContext, PayloadRateLimitOption, PreTransactionReservation, QueryEvent, RateLimitDeclaration, WriteEvent, WriteResult } from "./handlers.js";
4
+ import type { AccessRule, AgentHandlerHints, BeforeTransactionPhase, EscapeHatchDeclaration, HandlerContext, PayloadRateLimitOption, PreparedWriteEvent, PreTransactionReservation, QueryEvent, RateLimitDeclaration, WriteHandlerTransactionMode, WriteResult } from "./handlers.js";
5
5
  import type { PipelineDef } from "./step.js";
6
- export type WriteHandlerDefinition<TName extends string = string, TSchema extends ZodType = ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap> = {
6
+ export type WriteHandlerDefinition<TName extends string = string, TSchema extends ZodType = ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap, TPrepared = undefined> = {
7
7
  readonly name: TName;
8
8
  readonly schema: TSchema;
9
9
  readonly access: AccessRule;
@@ -16,11 +16,13 @@ export type WriteHandlerDefinition<TName extends string = string, TSchema extend
16
16
  readonly additionalRateLimits?: readonly PayloadRateLimitOption[];
17
17
  readonly escapeHatch?: EscapeHatchDeclaration;
18
18
  readonly reserveBeforeTransaction?: PreTransactionReservation;
19
+ readonly beforeTransaction?: BeforeTransactionPhase<z.infer<TSchema>, TPrepared>;
19
20
  readonly tenantlessAnonymous?: boolean;
20
- readonly handler: (event: WriteEvent<z.infer<TSchema>>, context: HandlerContext<TMap>) => Promise<WriteResult<TData>>;
21
+ readonly transaction?: WriteHandlerTransactionMode;
22
+ readonly handler: (event: PreparedWriteEvent<z.infer<TSchema>, TPrepared>, context: HandlerContext<TMap>) => Promise<WriteResult<TData>>;
21
23
  readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
22
24
  };
23
- export type WriteHandlerInput<TName extends string = string, TSchema extends ZodType = ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap> = {
25
+ export type WriteHandlerInput<TName extends string = string, TSchema extends ZodType = ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap, TPrepared = undefined> = {
24
26
  readonly name: TName;
25
27
  readonly schema: TSchema;
26
28
  readonly access: AccessRule;
@@ -31,9 +33,11 @@ export type WriteHandlerInput<TName extends string = string, TSchema extends Zod
31
33
  readonly additionalRateLimits?: readonly PayloadRateLimitOption[];
32
34
  readonly escapeHatch?: EscapeHatchDeclaration;
33
35
  readonly reserveBeforeTransaction?: PreTransactionReservation;
36
+ readonly beforeTransaction?: BeforeTransactionPhase<z.infer<TSchema>, TPrepared>;
34
37
  readonly tenantlessAnonymous?: boolean;
38
+ readonly transaction?: WriteHandlerTransactionMode;
35
39
  } & ({
36
- readonly handler: (event: WriteEvent<z.infer<TSchema>>, context: HandlerContext<TMap>) => Promise<WriteResult<TData>>;
40
+ readonly handler: (event: PreparedWriteEvent<z.infer<TSchema>, TPrepared>, context: HandlerContext<TMap>) => Promise<WriteResult<TData>>;
37
41
  readonly perform?: never;
38
42
  } | {
39
43
  readonly perform: PipelineDef<z.infer<TSchema>, TData>;
package/dist/feature.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigSeedDef
4
4
  import type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition } from "./define-handler.js";
5
5
  import type { RegisterEntityCrudOptions } from "./entity-handlers.js";
6
6
  import type { EntityTableMeta } from "./entity-table-meta-types.js";
7
+ import type { KumikoEventTypeMap } from "./event-type-map.js";
7
8
  import type { ExtensionOptionsArgs, ExtensionOptionsFor } from "./extension-options-map.js";
8
9
  import type { EntityDefinition } from "./fields.js";
9
10
  import type { AccessRule, AgentHandlerHints, AuthClaimsFn, AuthClaimsHookDef, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyType, DeclarativeEventMigration, EntityRef, EscapeHatchDeclaration, EventDef, EventMigrationDef, EventPiiStance, EventUpcastFn, HandlerRef, NameOrRef, PayloadRateLimitOption, QualifiedEventName, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, StreamHandlerDef, StreamHandlerFn, WriteHandlerDef, WriteHandlerFn } from "./handlers.js";
@@ -234,7 +235,7 @@ export type FeatureRegistrar<TFeature extends string = string> = {
234
235
  readonly name: string;
235
236
  } & EntityDefinition): EntityRef;
236
237
  crud(entityName: string, entity: EntityDefinition, options?: RegisterEntityCrudOptions): void;
237
- writeHandler<TName extends string, TSchema extends ZodType>(def: WriteHandlerDefinition<TName, TSchema>): HandlerRef;
238
+ writeHandler<TName extends string, TSchema extends ZodType, TPrepared = undefined>(def: WriteHandlerDefinition<TName, TSchema, unknown, KumikoEventTypeMap, TPrepared>): HandlerRef;
238
239
  writeHandler<TSchema extends ZodType>(name: string, schema: TSchema, handler: WriteHandlerFn<z.infer<TSchema>>, options: {
239
240
  access: AccessRule;
240
241
  rateLimit?: RateLimitDeclaration;
package/dist/fields.d.ts CHANGED
@@ -750,7 +750,8 @@ export type EntityDefinition<F extends FieldsMap = FieldsMap, T extends EntityTe
750
750
  readonly systemStream?: boolean;
751
751
  /** Schema version stamped on every lifecycle event (default 1, integer 1..999). Bump it together with a step in `eventMigrations`. */
752
752
  readonly eventVersion?: number;
753
- /** One step per version, fromVersion → fromVersion+1. Transforms run on row-shaped field records; the record may be partial, touch only keys that are present. */
753
+ /** One step per version, fromVersion → fromVersion+1. Transforms run on row-shaped field records; the record may be partial, touch only keys that are present.
754
+ * `pii` and `encrypted` fields arrive as stored ciphertext bound to their field name (AAD): pass them through under the same key, never rename or derive from them, or the value can no longer be decrypted. */
754
755
  readonly eventMigrations?: readonly EntityEventMigration[];
755
756
  readonly searchWeight?: number;
756
757
  readonly defaultCurrency?: string;
@@ -8,5 +8,11 @@ export type FileHandle = {
8
8
  };
9
9
  export type FileContext = {
10
10
  ref(key: string): FileHandle;
11
+ refFromFileRef(row: {
12
+ readonly storageKey: string;
13
+ }): FileHandle;
14
+ listFromFileRef(row: {
15
+ readonly storageKey: string;
16
+ }): Promise<readonly string[]>;
11
17
  list(prefix: string): Promise<readonly string[]>;
12
18
  };
@@ -101,6 +101,12 @@ export type WriteEvent<TPayload = unknown> = {
101
101
  readonly payload: TPayload;
102
102
  readonly user: SessionUser;
103
103
  };
104
+ /** What a handler sees when its `beforeTransaction` phase ran: the phase's `data` as `prepared`; absent for handlers without the phase. */
105
+ export type PreparedWriteEvent<TPayload = unknown, TPrepared = unknown> = WriteEvent<TPayload> & ([TPrepared] extends [undefined] ? {
106
+ readonly prepared?: undefined;
107
+ } : {
108
+ readonly prepared: TPrepared;
109
+ });
104
110
  export type QueryEvent<TPayload = unknown> = {
105
111
  readonly type: string;
106
112
  readonly payload: TPayload;
@@ -232,6 +238,7 @@ type SharedContextFields = {
232
238
  readonly notify?: NotifyFn;
233
239
  readonly _notifyFactory?: NotifyFactory;
234
240
  readonly _notifyInTransactionFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
241
+ readonly _notifyAfterCommitFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
235
242
  readonly _escapeHatchAuditSink?: EscapeHatchAuditSink;
236
243
  readonly secrets?: import("./secrets-types.js").SecretsContext;
237
244
  readonly masterKeyProvider?: import("./secrets-types.js").MasterKeyProvider;
@@ -299,6 +306,11 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
299
306
  } | null;
300
307
  readonly _userId?: string | undefined;
301
308
  readonly _handlerType?: string | undefined;
309
+ /** Keyed digest for handler-owned rate-limit keys, so Redis never holds a
310
+ * plaintext or plainly hashed identifier. Scoped to the running handler:
311
+ * the same input yields a different digest per handler and never matches a
312
+ * dispatcher payload-bucket digest. Absent when the app wires no digest key. */
313
+ readonly rateLimitDigest?: (value: string) => string;
302
314
  readonly includeDeleted?: boolean;
303
315
  readonly runPreSave?: (handlerName: string, changes: Record<string, unknown>, previous: Readonly<Record<string, unknown>>, isNew: boolean) => Promise<Record<string, unknown>>;
304
316
  readonly query: (qn: string, payload: unknown) => Promise<unknown>;
@@ -344,6 +356,7 @@ export type JobContext = SharedContextFields & {
344
356
  readonly registry: Registry;
345
357
  readonly systemUser: SessionUser;
346
358
  readonly log: Logger;
359
+ readonly metricsFor: (featureName: string) => MetricsHandle;
347
360
  readonly triggeredBy: {
348
361
  readonly id: string;
349
362
  readonly tenantId: TenantId;
@@ -507,11 +520,13 @@ export type RateLimitOption = {
507
520
  export type RateLimitPayloadBucket = {
508
521
  readonly payloadField: string;
509
522
  };
523
+ export type PayloadBucketNormalization = "email";
510
524
  export type PayloadRateLimitOption = {
511
525
  readonly per: RateLimitPayloadBucket;
512
526
  readonly limit: number;
513
527
  readonly windowSeconds: number;
514
528
  readonly cost?: number;
529
+ readonly normalize?: PayloadBucketNormalization;
515
530
  };
516
531
  export type RateLimitDisabled = {
517
532
  readonly disabled: true;
@@ -552,6 +567,15 @@ export type ReservationHandle = {
552
567
  * The returned release (or handle.release) runs when the transaction does not commit (failure
553
568
  * result, throw, rollback, failed commit). Throwing a KumikoError rejects the write. */
554
569
  export type PreTransactionReservation = (event: WriteEvent, ctx: HandlerContext) => Promise<ReservationRelease | ReservationHandle | undefined>;
570
+ /** Outcome of a write handler's pre-transaction phase. `release` runs when the handler transaction does not commit (failure result, throw, rollback, failed commit with unknown outcome), so it must itself tolerate a commit that actually happened. */
571
+ export type BeforeTransactionResult<TPrepared = unknown> = {
572
+ readonly isSuccess: true;
573
+ readonly data: TPrepared;
574
+ readonly release?: ReservationRelease;
575
+ } | WriteFailure;
576
+ export type BeforeTransactionPhase<TPayload = unknown, TPrepared = unknown> = (event: WriteEvent<TPayload>, ctx: HandlerContext) => Promise<BeforeTransactionResult<TPrepared>>;
577
+ /** `perCommand`: the handler runs without a transaction of its own. */
578
+ export type WriteHandlerTransactionMode = "perCommand";
555
579
  export type WriteHandlerDef = {
556
580
  readonly name: string;
557
581
  readonly schema: ZodType;
@@ -571,6 +595,18 @@ export type WriteHandlerDef = {
571
595
  /** Reserves before the handler transaction opens. Reached through a nested ctx.write the
572
596
  * reservation cannot run, so the dispatcher rejects it unless the top-level batch reserved it. */
573
597
  readonly reserveBeforeTransaction?: PreTransactionReservation;
598
+ /** Runs after all pre-handler gates (and after reserveBeforeTransaction) and before the
599
+ * transaction opens: no transaction and no connection held, for external I/O and reads only.
600
+ * Its `data` reaches the handler as `event.prepared`; a failure result or throw rejects the
601
+ * write before anything is written. A handler with this phase cannot run as a nested ctx.write.
602
+ * `ctx.forTenant` is available but read-only here: its db denies writes for the own and a
603
+ * foreign tenant alike (a foreign tenant stays gated by the SystemAdmin check). */
604
+ readonly beforeTransaction?: BeforeTransactionPhase;
605
+ /** `"perCommand"`: the handler runs without a transaction of its own. Every ctx.write/writeAs it
606
+ * issues runs as its own top-level command with its own transaction and afterCommit hooks;
607
+ * ctx.db writes and event appends throw. It cannot run as a nested write or inside a
608
+ * multi-command batch, and cannot be combined with beforeTransaction/reserveBeforeTransaction. */
609
+ readonly transaction?: WriteHandlerTransactionMode;
574
610
  /** Lets an anonymous `POST /api/write` for this handler proceed under
575
611
  * SYSTEM_TENANT_ID when the host resolves no tenant. Boot requires
576
612
  * `access.roles: ["anonymous"]` and a real `rateLimit`. */
package/dist/hooks.d.ts CHANGED
@@ -23,6 +23,7 @@ export type DeleteContext = {
23
23
  readonly data: Readonly<Record<string, unknown>>;
24
24
  readonly entityName?: string | undefined;
25
25
  readonly event?: StoredEvent | undefined;
26
+ readonly lockGuardBypassed?: boolean | undefined;
26
27
  };
27
28
  export type LifecycleResult = SaveContext | DeleteContext;
28
29
  export type PreSaveHookFn = (changes: Record<string, unknown>, context: AppContext & {
package/dist/screen.d.ts CHANGED
@@ -554,8 +554,10 @@ export type ProjectionListScreenDefinition = {
554
554
  * boot-validator only checks structure (`op:"in"` ⇒ array value). */
555
555
  readonly filter?: ScreenFilter;
556
556
  /** User-toggleable facet dropdowns — see `ListFacetSpec` doc. `field`
557
- * must be a declared column; the bound query handler must accept
558
- * `filters` in its Zod schema — the boot-validator checks both. A
557
+ * must be a declared column, except for `type: "reference"`: it filters
558
+ * by id through the bound query's `filters` and needs a registered
559
+ * `entity` instead. The bound query handler must accept `filters` in its
560
+ * Zod schema — the boot-validator checks both. A
559
561
  * "dateRange" facet instead needs its two `params` keys in that schema;
560
562
  * it sends no `filters` entry. */
561
563
  readonly facets?: readonly ListFacetSpec[];
@@ -721,6 +723,8 @@ export type DashboardPanelEmptyState = {
721
723
  /** Second line under `emptyLabel`. */
722
724
  readonly emptyHint?: string;
723
725
  };
726
+ export declare const DASHBOARD_STAT_TONES: readonly ["default", "positive", "ok", "warn", "negative"];
727
+ export type DashboardStatTone = (typeof DASHBOARD_STAT_TONES)[number];
724
728
  export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate & {
725
729
  readonly kind: "stat";
726
730
  /** Stable id: kebab-case, unique within the panel set. */
@@ -734,7 +738,7 @@ export type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate
734
738
  /** Formats a numeric value as currency (minor units); ignored for string values. The sparkline is not formatted. */
735
739
  readonly valueFormat?: DashboardValueFormat;
736
740
  /** Fixed tone when `toneField` is unset or yields none; not applied while the value is 0. */
737
- readonly tone?: "default" | "positive" | "ok" | "warn" | "negative";
741
+ readonly tone?: DashboardStatTone;
738
742
  /** Record field holding `{ atMs, value | null }[]` (MetricResult.points),
739
743
  * drawn as a sparkline next to the value. */
740
744
  readonly sparklineField?: string;
@@ -1144,7 +1148,9 @@ export type EditRelatedListSection = {
1144
1148
  readonly searchable?: boolean;
1145
1149
  /** User-toggleable facet dropdowns — same explicit-label `ListFacetSpec` as
1146
1150
  * `projectionList.facets` (a relatedList has no entity to derive filterable
1147
- * fields from). `field` must be a declared column and the bound query
1151
+ * fields from). `field` must be a declared column, except for
1152
+ * `type: "reference"`: it filters by id through the bound query's
1153
+ * `filters` and needs a registered `entity` instead. The bound query
1148
1154
  * handler must accept `filters` in its Zod schema — the boot-validator
1149
1155
  * checks both. Selected values ride along as `payload.filters`. */
1150
1156
  readonly facets?: readonly ListFacetSpec[];
package/dist/screen.js CHANGED
@@ -1,6 +1,13 @@
1
1
  export function metricField(metric) {
2
2
  return typeof metric === "string" ? metric : metric.field;
3
3
  }
4
+ // Query result contract: flat record; `valueField` points at the
5
+ // value (string is display-ready, number is locale-formatted, DashboardI18nText
6
+ // is translated), `subField` optionally at a sub line (string | DashboardI18nText), `toneField` optionally at
7
+ // "default" | "positive" | "ok" | "warn" | "negative".
8
+ // `visibleWhen` only on a top-level stat; the boot validator rejects it on a
9
+ // stat-group child (gate the whole group instead).
10
+ export const DASHBOARD_STAT_TONES = ["default", "positive", "ok", "warn", "negative"];
4
11
  /** Group key of a field value; `collapsedWhen` and row values both go through it. */
5
12
  export function relatedListGroupKey(value) {
6
13
  return value === undefined || value === null ? "" : String(value);
@@ -1,4 +1,5 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
+ import type { DbRunner } from "./db-connection.js";
2
3
  import type { TenantId } from "./identifiers.js";
3
4
  declare const SecretBrand: unique symbol;
4
5
  export type Secret<T = string> = {
@@ -21,6 +22,7 @@ export type SecretKeyRef = string | {
21
22
  readonly name: string;
22
23
  };
23
24
  export interface SecretsContext {
25
+ withRunner(runner: DbRunner): SecretsContext;
24
26
  get(tenantId: TenantId, key: SecretKeyRef, auditCtx?: SecretAuditContext): Promise<Secret<string> | undefined>;
25
27
  has(tenantId: TenantId, key: SecretKeyRef): Promise<boolean>;
26
28
  set(tenantId: TenantId, key: SecretKeyRef, value: string, opts?: {
@@ -5,6 +5,7 @@ export type WriteErrorInfo = {
5
5
  readonly i18nParams?: Readonly<Record<string, unknown>>;
6
6
  readonly message: string;
7
7
  readonly details?: unknown;
8
+ readonly retryAfterSeconds?: number;
8
9
  };
9
10
  export type WriteFailure = {
10
11
  readonly isSuccess: false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-types",
3
- "version": "0.363.0",
3
+ "version": "0.365.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/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "improvement",
5
+ "title": "DASHBOARD_STAT_TONES is the single source for dashboard stat tones",
6
+ "detail": "screen.ts exports DASHBOARD_STAT_TONES and DashboardStatTone; DashboardStatPanel.tone derives from it, and the web renderer builds its runtime tone check and StatTone type from the same constant, so a new tone can no longer be silently dropped from toneField values."
7
+ },
2
8
  {
3
9
  "version": "0.362.0",
4
10
  "type": "improvement",