@cosmicdrift/kumiko-framework 0.397.0 → 0.399.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.
@@ -0,0 +1,4962 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from the API entry points. Do not edit. -->
2
+
3
+ # Referenced types
4
+
5
+ Types that appear in the signatures of the entry points but are not exported by any of them. Use them to read option shapes; import only what an entry point exports.
6
+
7
+ ## packages/dev-server/src/setup-test-stack-from-features.ts
8
+
9
+ ```ts
10
+ type SetupTestStackFromFeaturesOptions = Omit<TestStackOptions, "features"> & {
11
+ includeBundled?: boolean;
12
+ authOptions?: ComposeFeaturesOptions["authOptions"];
13
+ bundledLists?: ComposeFeaturesOptions["bundledLists"];
14
+ presets?: TestStackPreset[];
15
+ };
16
+ type TestStackPreset = "config" | "template-resolver";
17
+ ```
18
+
19
+ ## packages/framework/src/api/auth-middleware.ts
20
+
21
+ ```ts
22
+ type AuthSessionCheckResult =
23
+ | AuthSessionStatus
24
+ | {
25
+ status: "live";
26
+ roles: string[];
27
+ }
28
+ ;
29
+ type AuthSessionChecker = (sid: string, expectedUserId: string) => Promise<AuthSessionCheckResult>;
30
+ type AuthSessionStatus = "live" | "revoked" | "expired" | "missing" | "blocked";
31
+ type TokenVerifier = (rawToken: string) => Promise<SessionUser | null>;
32
+ ```
33
+
34
+ ## packages/framework/src/api/auth-routes-types.ts
35
+
36
+ ```ts
37
+ type AccountUnlockConfig = {
38
+ requestHandler: string;
39
+ confirmHandler: string;
40
+ };
41
+ type AuthRoutesConfig = {
42
+ membershipQuery: string;
43
+ userQuery?: string;
44
+ loginHandler?: string;
45
+ loginErrorStatusMap?: Readonly<Record<string, number>>;
46
+ loginRateLimit?: LoginRateLimiter | null;
47
+ mfaVerifyHandler?: string;
48
+ switchTenantMfaGateHandler?: string;
49
+ mfaVerifyErrorStatusMap?: Readonly<Record<string, number>>;
50
+ mfaVerifyRateLimit?: LoginRateLimiter | null;
51
+ mfaPreauthEnableStartHandler?: string;
52
+ mfaPreauthEnableStartErrorStatusMap?: Readonly<Record<string, number>>;
53
+ mfaPreauthEnableStartRateLimit?: LoginRateLimiter | null;
54
+ mfaPreauthConfirmHandler?: string;
55
+ mfaPreauthConfirmErrorStatusMap?: Readonly<Record<string, number>>;
56
+ mfaPreauthConfirmRateLimit?: LoginRateLimiter | null;
57
+ sessionCreator?: SessionCreator;
58
+ sessionRevoker?: SessionRevoker;
59
+ sessionChecker?: SessionChecker;
60
+ tokenVerifier?: TokenVerifier;
61
+ patRateLimiter?: LoginRateLimiter;
62
+ resolveTenantLifecycleStatus?: TenantLifecycleStatusResolver;
63
+ passwordReset?: PasswordResetConfig;
64
+ emailVerification?: EmailVerificationConfig;
65
+ accountUnlock?: AccountUnlockConfig;
66
+ signup?: SignupConfig;
67
+ invite?: InviteConfig;
68
+ cookieSameSite?: "lax" | "strict";
69
+ cookieDomain?: string;
70
+ secureCookies?: boolean;
71
+ retiredCookieDomains?: string[];
72
+ allowedOrigins?: string[];
73
+ publicOrigin?: string;
74
+ unsafeSkipOriginCheck?: boolean;
75
+ trustedProxyHops?: number;
76
+ postAuthLanding?: PostAuthLandingResolver;
77
+ resolver?: ClientIpResolver;
78
+ };
79
+ type EmailVerificationConfig = {
80
+ requestHandler: string;
81
+ confirmHandler: string;
82
+ };
83
+ type InviteConfig = {
84
+ acceptHandler: string;
85
+ acceptWithLoginHandler: string;
86
+ signupCompleteHandler: string;
87
+ infoHandler?: string;
88
+ };
89
+ type LoginRateLimiter = {
90
+ check(key: string): Promise<boolean>;
91
+ reset(key: string): Promise<void>;
92
+ };
93
+ type PasswordResetConfig = {
94
+ requestHandler: string;
95
+ confirmHandler: string;
96
+ };
97
+ type SessionChecker = AuthSessionChecker;
98
+ type SessionCreator = (user: SessionUser, meta: SessionMetadata) => Promise<string>;
99
+ type SessionMetadata = {
100
+ ip: string;
101
+ userAgent: string;
102
+ };
103
+ type SessionRevoker = (sid: string) => Promise<void>;
104
+ type SignupConfig = {
105
+ requestHandler: string;
106
+ confirmHandler: string;
107
+ };
108
+ ```
109
+
110
+ ## packages/framework/src/api/client-ip.ts
111
+
112
+ ```ts
113
+ type ClientIpHeaderSource = {
114
+ header: (name: string) => string | undefined;
115
+ socketAddress?: string;
116
+ };
117
+ type ClientIpResolver = {
118
+ resolve: (source: ClientIpHeaderSource) => string;
119
+ };
120
+ ```
121
+
122
+ ## packages/framework/src/api/extra-route.ts
123
+
124
+ ```ts
125
+ type AnonymousExtraRoute = {
126
+ method: HttpRouteMethod;
127
+ path: string;
128
+ entry: "anonymous";
129
+ handler: (c: Context<any, any>, deps: AnonymousExtraRouteDeps) => Response | Promise<Response>;
130
+ };
131
+ type AnonymousExtraRouteDeps = {
132
+ app: Hono<any, any>;
133
+ registry: Registry;
134
+ systemQuery: (type: string, payload: unknown, tenantId: TenantId) => Promise<unknown>;
135
+ write: (type: string, payload: unknown) => Promise<WriteResult>; // Runs as the session the /api chain resolved for this request — anonymous when no token is sent, the authenticated user otherwise.
136
+ clientIp: string; // Resolved by the server's single trustedProxyHops-aware resolver — never read X-Forwarded-For yourself.
137
+ };
138
+ type ExtraRouteDefinition = AnonymousExtraRoute | UserExtraRoute | SignatureExtraRoute<unknown>;
139
+ type RoleDispatchArgs = SystemDispatchArgs & {
140
+ roles: string[];
141
+ };
142
+ type SignatureExtraRoute<TVerified> = {
143
+ method: HttpRouteMethod;
144
+ path: string;
145
+ entry: "signature";
146
+ verify: (request: SignatureExtraRouteVerifyRequest, deps: SignatureExtraRouteVerifyDeps) => Promise<TVerified>;
147
+ handler: (c: Context<any, any>, verified: TVerified, deps: SignatureExtraRouteDeps) => Response | Promise<Response>;
148
+ };
149
+ type SignatureExtraRouteDeps = {
150
+ app: Hono<any, any>;
151
+ registry: Registry;
152
+ secrets?: SecretsContext;
153
+ systemQuery: (type: string, payload: unknown, tenantId: TenantId) => Promise<unknown>;
154
+ dispatchSystemWrite: (args: SystemDispatchArgs) => Promise<WriteResult>; // Privilege scope: SystemAdmin of the target tenant, WITHOUT the route's access check — only reachable because verify() has already proven the caller's authenticity (signature, HMAC state, etc.).
155
+ dispatchSystemQuery: (args: SystemDispatchArgs) => Promise<unknown>;
156
+ dispatchWriteAs: (args: RoleDispatchArgs) => Promise<WriteResult>; // Same trust as dispatchSystemWrite, but runs as the tenant's system user with the "system" role plus exactly `roles` (no SystemAdmin), so the write passes app-role access checks.
157
+ };
158
+ type SignatureExtraRouteVerifyDeps = {
159
+ registry: Registry;
160
+ secrets?: SecretsContext;
161
+ };
162
+ type SignatureExtraRouteVerifyRequest = {
163
+ rawBody: string;
164
+ headers: Readonly<Record<string, string>>; // Lowercase header names — Hono/undici normalize incoming headers to lowercase, verify() must not have to re-normalize per provider.
165
+ params: Readonly<Record<string, string>>;
166
+ query: Readonly<Record<string, string>>;
167
+ };
168
+ type SystemDispatchArgs = {
169
+ handlerQn: string;
170
+ payload: unknown;
171
+ tenantId: TenantId;
172
+ };
173
+ type UserExtraRoute = {
174
+ method: HttpRouteMethod;
175
+ path: string;
176
+ entry: "user";
177
+ handler: (c: Context<any, any>, deps: UserExtraRouteDeps) => Response | Promise<Response>;
178
+ };
179
+ type UserExtraRouteDeps = {
180
+ app: Hono<any, any>;
181
+ registry: Registry;
182
+ user: SessionUser;
183
+ query: (type: string, payload: unknown) => Promise<unknown>;
184
+ write: (type: string, payload: unknown) => Promise<WriteResult>;
185
+ };
186
+ ```
187
+
188
+ ## packages/framework/src/api/jwt.ts
189
+
190
+ ```ts
191
+ type JwtHelper = {
192
+ sign(user: SessionUser): Promise<string>;
193
+ verify(token: string): Promise<JwtPayload>;
194
+ ttlSeconds: number;
195
+ };
196
+ type JwtPayload = {
197
+ sub: string;
198
+ tenantId: TenantId;
199
+ roles: string[];
200
+ timezone?: string;
201
+ locale?: string;
202
+ claims?: Record<string, unknown>;
203
+ jti?: string;
204
+ exp?: number;
205
+ };
206
+ ```
207
+
208
+ ## packages/framework/src/api/post-auth-landing.ts
209
+
210
+ ```ts
211
+ type PostAuthLandingArgs = {
212
+ flow: PostAuthLandingFlow;
213
+ roles: string[];
214
+ tenantId: string;
215
+ tenantKey?: string;
216
+ handover?: {
217
+ entityType: string;
218
+ id: string;
219
+ };
220
+ };
221
+ type PostAuthLandingFlow = "login" | "signup" | "invite";
222
+ type PostAuthLandingResolver = (args: PostAuthLandingArgs) => string | undefined;
223
+ ```
224
+
225
+ ## packages/framework/src/api/sse-broker.ts
226
+
227
+ ```ts
228
+ type AccessInvalidationCredential = {
229
+ sid?: string;
230
+ patTokenId?: string;
231
+ };
232
+ type AccessInvalidationScope =
233
+ | {
234
+ kind: "user";
235
+ }
236
+ | {
237
+ kind: "all-except-session";
238
+ keptSessionId: string;
239
+ }
240
+ | {
241
+ kind: "sessions";
242
+ sessionIds: string[];
243
+ }
244
+ | {
245
+ kind: "pat-tokens";
246
+ tokenIds: string[];
247
+ }
248
+ ;
249
+ type SseBroker = {
250
+ addClient(channel: string, send: (event: SseEvent) => void, close: () => void): string;
251
+ removeClient(channel: string, clientId: string): void;
252
+ pushToChannel(channel: string, event: SseEvent): void | Promise<void>;
253
+ getClientCount(channel: string): number;
254
+ getTotalClientCount(): number;
255
+ subscribeAccessInvalidation(userId: string, onInvalidate: () => void, credential?: AccessInvalidationCredential): () => void;
256
+ publishAccessInvalidation(userId: string, scope?: AccessInvalidationScope): void | Promise<void>;
257
+ };
258
+ type SseEvent = {
259
+ type: string;
260
+ data: Record<string, unknown>;
261
+ };
262
+ ```
263
+
264
+ ## packages/framework/src/db/apply-entity-event.ts
265
+
266
+ ```ts
267
+ type ApplyResult =
268
+ | {
269
+ kind: "applied";
270
+ verb: AutoVerb;
271
+ row: DbRow | null;
272
+ }
273
+ | {
274
+ kind: "skipped";
275
+ }
276
+ ;
277
+ type AutoVerb = "created" | "updated" | "deleted" | "restored" | "forgotten";
278
+ type Table = TableColumns;
279
+ ```
280
+
281
+ ## packages/framework/src/db/connection.ts
282
+
283
+ ```ts
284
+ type BunSqlOptions = Bun.SQL.PostgresOrMySQLOptions;
285
+ type CreateDbConnectionOptions = DbConnectionOptions & {
286
+ url: string;
287
+ rowLevelSecurity?: RowLevelSecurityMode;
288
+ };
289
+ type DbConnectionHandle = {
290
+ db: DbConnection;
291
+ client: PgClient;
292
+ listenClient: PgListenClient;
293
+ close: () => Promise<void>;
294
+ };
295
+ type DbConnectionOptions = {
296
+ maxConnections?: number;
297
+ idleTimeoutSeconds?: number;
298
+ connectTimeoutSeconds?: number;
299
+ closeTimeoutSeconds?: number;
300
+ ssl?: BunSqlOptions["tls"];
301
+ statementTimeoutMs?: number;
302
+ idleInTransactionSessionTimeoutMs?: number;
303
+ lockTimeoutMs?: number;
304
+ };
305
+ ```
306
+
307
+ ## packages/framework/src/db/cursor.ts
308
+
309
+ ```ts
310
+ type DecodedKeysetCursor = {
311
+ id: string;
312
+ sortValue: string | null | undefined;
313
+ };
314
+ ```
315
+
316
+ ## packages/framework/src/db/dialect.ts
317
+
318
+ ```ts
319
+ type ColumnBuilder<TValue = unknown> = {
320
+ __column: true;
321
+ finalise: () => ColumnFinal;
322
+ notNull(): ColumnBuilder<TValue>;
323
+ primaryKey(): ColumnBuilder<TValue>;
324
+ default(value: TValue | SqlExpression | unknown[] | number | string | boolean | null): ColumnBuilder<TValue>;
325
+ defaultRandom(): ColumnBuilder<TValue>;
326
+ defaultNow(): ColumnBuilder<TValue>;
327
+ generatedAlwaysAsIdentity(): ColumnBuilder<TValue>;
328
+ unique(name?: string): ColumnBuilder<TValue>;
329
+ $type<T>(): ColumnBuilder<T>;
330
+ $onUpdate(fn: () => unknown): ColumnBuilder<TValue>;
331
+ };
332
+ type ColumnFinal = {
333
+ sqlName: string;
334
+ pgType: PgType;
335
+ notNull: boolean;
336
+ primaryKey: boolean;
337
+ unique: boolean;
338
+ identity: boolean;
339
+ defaultSql?: string;
340
+ bigintJsMode?: "number" | "bigint";
341
+ };
342
+ type ColumnMap = Record<string, ColumnBuilder<unknown>>;
343
+ type IndexBuilder = {
344
+ __index: true;
345
+ on(...cols: ColumnHandle[]): IndexBuilderWithCols;
346
+ };
347
+ type IndexBuilderWithCols = {
348
+ __index: true;
349
+ name: string;
350
+ unique: boolean;
351
+ columns: string[];
352
+ where(expr: SqlExpression): IndexBuilderWithCols;
353
+ whereSql?: string;
354
+ };
355
+ type IndexOrPk = IndexBuilderWithCols | PrimaryKeyDescriptor;
356
+ type PrimaryKeyDescriptor = {
357
+ __pk: true;
358
+ columns: string[];
359
+ name?: string;
360
+ };
361
+ type SqlExpression = {
362
+ kind: "sql-expr";
363
+ text: string;
364
+ params: unknown[];
365
+ [SQL_EXPR_BRAND]: true;
366
+ };
367
+ type TableColumns<_T = unknown> = SchemaTable;
368
+ ```
369
+
370
+ ## packages/framework/src/db/eagerload.ts
371
+
372
+ ```ts
373
+ type EagerLoadEntityResolver = (entityName: string) => EntityDefinition | undefined;
374
+ type EagerLoadViewer = {
375
+ user: SessionUser;
376
+ parentVisibility: ParentVisibilityOption;
377
+ };
378
+ ```
379
+
380
+ ## packages/framework/src/db/event-store-executor-context.ts
381
+
382
+ ```ts
383
+ type EntityLifecycleVerb = "created" | "updated" | "deleted" | "restored" | "forgotten";
384
+ type Table = TableColumns;
385
+ ```
386
+
387
+ ## packages/framework/src/db/event-store-executor.ts
388
+
389
+ ```ts
390
+ type EventStoreExecutorOptions = {
391
+ searchAdapter?: SearchAdapter;
392
+ entityName: string;
393
+ entityCache?: EntityCache;
394
+ encryption?: EnvelopeCipher; // Override the boot-injected cipher for fields marked `encrypted: true`.
395
+ kms?: LocalKeyKmsAdapter; // Override the boot-injected subject KMS for pii-annotated fields.
396
+ };
397
+ ```
398
+
399
+ ## packages/framework/src/db/feature-table-sources.ts
400
+
401
+ ```ts
402
+ type FeatureTableSource = {
403
+ table: unknown;
404
+ origin: string;
405
+ };
406
+ ```
407
+
408
+ ## packages/framework/src/db/list-aggregate-labels.ts
409
+
410
+ ```ts
411
+ type AggregateRequest = Parameters<EventStoreExecutor["aggregate"]>[0];
412
+ type LabelDeps = {
413
+ entity: EntityDefinition;
414
+ resolveEntity: EagerLoadEntityResolver;
415
+ viewer: EagerLoadViewer;
416
+ };
417
+ ```
418
+
419
+ ## packages/framework/src/db/located-timestamp.ts
420
+
421
+ ```ts
422
+ type LocatedTimestampRead = { // Shape of a single rehydrated locatedTimestamp field.
423
+ at: string;
424
+ tz: string;
425
+ utc: string;
426
+ };
427
+ ```
428
+
429
+ ## packages/framework/src/db/migrate-change-render.ts
430
+
431
+ ```ts
432
+ type ColumnChange = {
433
+ name: string;
434
+ nullabilityChanged?: {
435
+ from: boolean;
436
+ to: boolean;
437
+ };
438
+ defaultChanged?: {
439
+ from: string | undefined;
440
+ to: string | undefined;
441
+ };
442
+ typeChanged?: {
443
+ from: string;
444
+ to: string;
445
+ };
446
+ };
447
+ type IndexChange = {
448
+ name: string;
449
+ columnsChanged?: {
450
+ from: string[];
451
+ to: string[];
452
+ };
453
+ uniqueChanged?: {
454
+ from: boolean;
455
+ to: boolean;
456
+ };
457
+ whereSqlChanged?: {
458
+ from: string | undefined;
459
+ to: string | undefined;
460
+ };
461
+ needsManualWhereChanged?: {
462
+ from: boolean;
463
+ to: boolean;
464
+ };
465
+ };
466
+ ```
467
+
468
+ ## packages/framework/src/db/migrate-generator.ts
469
+
470
+ ```ts
471
+ type GenerateMigrationInput = {
472
+ metas: EntityTableMeta[];
473
+ prevSnapshot: Snapshot | null;
474
+ name: string;
475
+ sequenceNumber: number;
476
+ priorMigrationsSql?: string[]; // Raw SQL of all earlier migrations, in order — lets the generator decide whether a retired framework table still needs its executable drop.
477
+ rowLevelSecurity?: RowLevelSecurityMode;
478
+ };
479
+ type GenerateMigrationOutput = {
480
+ filename: string;
481
+ sqlContent: string;
482
+ snapshot: Snapshot;
483
+ diff: SchemaDiff;
484
+ retiredDrops: RetiredFrameworkTable[];
485
+ rowLevelSecurityChanges: RowLevelSecurityChange[];
486
+ };
487
+ type SchemaDiff = {
488
+ newTables: EntityTableMeta[];
489
+ droppedTables: string[];
490
+ changedTables: TableDiff[];
491
+ };
492
+ type Snapshot = {
493
+ version: typeof SNAPSHOT_VERSION;
494
+ generatedAt?: string;
495
+ tables: EntityTableMeta[];
496
+ rowLevelSecurity?: Readonly<Record<string, TableRowLevelSecurity>>;
497
+ };
498
+ type TableDiff = {
499
+ tableName: string;
500
+ newColumns: ColumnMeta[];
501
+ droppedColumns: string[];
502
+ changedColumns: ColumnChange[];
503
+ newIndexes: IndexMeta[];
504
+ droppedIndexes: string[];
505
+ changedIndexes: IndexChange[];
506
+ primaryKeyChanged?: PrimaryKeyChange;
507
+ nextMeta: EntityTableMeta;
508
+ };
509
+ ```
510
+
511
+ ## packages/framework/src/db/migrate-primary-key.ts
512
+
513
+ ```ts
514
+ type PrimaryKey = {
515
+ columns: string[];
516
+ constraintName: string;
517
+ };
518
+ type PrimaryKeyChange = {
519
+ from: PrimaryKey;
520
+ to: PrimaryKey;
521
+ };
522
+ ```
523
+
524
+ ## packages/framework/src/db/migrate-rls-diff.ts
525
+
526
+ ```ts
527
+ type RowLevelSecurityChange =
528
+ | {
529
+ kind: "enable";
530
+ tableName: string;
531
+ next: TableRowLevelSecurity;
532
+ previous?: TableRowLevelSecurity;
533
+ }
534
+ | {
535
+ kind: "disable";
536
+ tableName: string;
537
+ previous: TableRowLevelSecurity;
538
+ }
539
+ ;
540
+ ```
541
+
542
+ ## packages/framework/src/db/migrate-runner.ts
543
+
544
+ ```ts
545
+ type ApplyResult = {
546
+ applied: string[];
547
+ skipped: string[];
548
+ };
549
+ ```
550
+
551
+ ## packages/framework/src/db/pg-error.ts
552
+
553
+ ```ts
554
+ type PgErrorInfo = {
555
+ code: string | undefined;
556
+ constraint_name: string | undefined;
557
+ };
558
+ ```
559
+
560
+ ## packages/framework/src/db/queries/backfill-pii.ts
561
+
562
+ ```ts
563
+ type PiiBackfillBatchOptions = PiiBackfillOptions & {
564
+ afterEventId?: bigint | string;
565
+ eventIds?: string[];
566
+ scanCache?: PiiBackfillScanCache;
567
+ };
568
+ type PiiBackfillBatchResult = PiiBackfillResult & {
569
+ lastEventId: string | null;
570
+ scannedAll: boolean;
571
+ firstFailedEventId: string | null;
572
+ touchedAggregateTypes: string[];
573
+ touchedEventTypes: string[];
574
+ };
575
+ type PiiBackfillFailure = {
576
+ eventId: string;
577
+ reason: string;
578
+ };
579
+ type PiiBackfillOptions = {
580
+ batchSize?: number;
581
+ dryRun?: boolean;
582
+ resolveOwnerFromProjection?: boolean;
583
+ eraseUnresolvableSubjects?: boolean;
584
+ rekeyTenantKeyedOwnerFields?: boolean;
585
+ };
586
+ type PiiBackfillResult = {
587
+ scannedEvents: number;
588
+ updatedEvents: number;
589
+ encryptedFields: number;
590
+ erasedFields: number;
591
+ rekeyedFields: number;
592
+ ownerFromProjection: number;
593
+ erasedUnresolvable: number;
594
+ deletedSnapshots: number;
595
+ failures: PiiBackfillFailure[];
596
+ };
597
+ type PiiBackfillScanCache = {
598
+ forgottenAggregates?: ReadonlySet<string>;
599
+ destroyedTenants?: ReadonlySet<string>;
600
+ };
601
+ ```
602
+
603
+ ## packages/framework/src/db/queries/event-store-orphan-rows.ts
604
+
605
+ ```ts
606
+ type DeleteRowsWithoutReplayableStreamArgs = {
607
+ table: unknown;
608
+ ids: string[];
609
+ tenantId?: TenantId;
610
+ };
611
+ ```
612
+
613
+ ## packages/framework/src/db/queries/postgres-array-literal.ts
614
+
615
+ ```ts
616
+ type PostgresArrayElement = string | number | bigint | null;
617
+ ```
618
+
619
+ ## packages/framework/src/db/query.ts
620
+
621
+ ```ts
622
+ type AnyDb = DbRunner | unknown;
623
+ type AnyRow = any;
624
+ type DeleteManyBatchedOptions = {
625
+ limit: number;
626
+ maxBatches?: number;
627
+ idColumn?: string; // Row id field for the LIMIT subquery.
628
+ };
629
+ type DeleteManyBatchedResult = {
630
+ deleted: number;
631
+ batches: number;
632
+ };
633
+ type IncrementCounterOptions = {
634
+ conflictKeys?: string[];
635
+ set?: Record<string, unknown>; // Extra SET clauses on conflict (e.g. lastUpdatedAt: sql\`NOW()\`).
636
+ };
637
+ type InnerJoinRow<TLeft, TRight> = {
638
+ left: TLeft;
639
+ right: TRight;
640
+ };
641
+ type InnerJoinSpec = {
642
+ left: TableLike;
643
+ right: TableLike;
644
+ on: [ JoinColumnPair, ...JoinColumnPair[] ];
645
+ leftWhere?: WhereObject;
646
+ rightWhere?: WhereObject;
647
+ leftFields?: string[]; // Fields to return per side; default: all columns of `left`, none of `right`.
648
+ rightFields?: string[];
649
+ limit?: number;
650
+ };
651
+ type InsertOnConflictDoNothingOptions = {
652
+ conflictKeys?: string[];
653
+ };
654
+ type JoinColumnPair = {
655
+ left: string;
656
+ right: string;
657
+ };
658
+ type RawClient = {
659
+ unsafe: <TRow = unknown>(sql: string, params?: unknown[]) => Promise<TRow[]>;
660
+ begin?: <T>(fn: (tx: DbTx) => Promise<T>) => Promise<T>;
661
+ savepoint?: <T>(fn: (sp: unknown) => Promise<T>) => Promise<T>;
662
+ release?: unknown;
663
+ reserve?: () => Promise<ReservedClient>;
664
+ };
665
+ type ReservedClient = {
666
+ unsafe: RawClient["unsafe"];
667
+ release: () => unknown;
668
+ };
669
+ type TableLike = EntityTableMeta | any;
670
+ type UpsertOnConflictOptions = {
671
+ conflictKeys: string[];
672
+ update?: Record<string, unknown>; // Columns to set on conflict.
673
+ };
674
+ ```
675
+
676
+ ## packages/framework/src/db/render-ddl.ts
677
+
678
+ ```ts
679
+ type RenderDdlOptions = {
680
+ rowLevelSecurity?: RowLevelSecurityMode;
681
+ };
682
+ ```
683
+
684
+ ## packages/framework/src/db/render-rls-ddl.ts
685
+
686
+ ```ts
687
+ type RowLevelSecurityPolicy = {
688
+ name: string;
689
+ command: (typeof POLICY_COMMANDS)[number];
690
+ kind: (typeof POLICY_KINDS)[number];
691
+ rolesSql: string;
692
+ using?: string;
693
+ withCheck?: string;
694
+ };
695
+ type TableRowLevelSecurity = {
696
+ policies: RowLevelSecurityPolicy[];
697
+ };
698
+ ```
699
+
700
+ ## packages/framework/src/db/retired-framework-tables.ts
701
+
702
+ ```ts
703
+ type RetiredFrameworkTable = {
704
+ tableName: string;
705
+ sinceVersion: string;
706
+ explanation: string;
707
+ };
708
+ ```
709
+
710
+ ## packages/framework/src/db/row-level-security-boot-check.ts
711
+
712
+ ```ts
713
+ type RowLevelSecurityCheckScope = "database" | "declared tables";
714
+ ```
715
+
716
+ ## packages/framework/src/db/table-builder.ts
717
+
718
+ ```ts
719
+ type BaseColumnsType<E extends EntityDefinition> = {
720
+ id: E extends { idType: "serial"; } ? Col<number> : Col<string>;
721
+ tenantId: Col<string>;
722
+ version: Col<number>;
723
+ insertedAt: Col<Temporal.Instant>;
724
+ modifiedAt: NullCol<Temporal.Instant>;
725
+ insertedById: NullCol<string>;
726
+ modifiedById: NullCol<string>;
727
+ };
728
+ type BlindIndexColumnForField<K extends string, F extends FieldDefinition> = F extends { type: "text"; lookupable: true; } ? { [P in `${K}Bidx`]: NullCol<string>; } : unknown;
729
+ type BuildEntityTableOptions = {
730
+ featureName?: string;
731
+ relations?: EntityRelations;
732
+ };
733
+ type Col<T> = ColumnHandle & {
734
+ __notNull: true;
735
+ __value?: T;
736
+ };
737
+ type ColumnValue<C> = C extends { __notNull: true; __value?: infer T; } ? T : C extends { __notNull: false; __value?: infer T; } ? T | null : unknown;
738
+ type ColumnsForEntity<F extends FieldsMap> = UnionToIntersection<{ [K in keyof F & string]: ColumnsForField<K, F[K]>; }[keyof F & string]>;
739
+ type ColumnsForField<K extends string, F extends FieldDefinition> = ValueColumnsForField<K, F> & BlindIndexColumnForField<K, F>;
740
+ type CompoundRowField<K extends string, V, F> = F extends { required: true; } ? { [P in K]: V; } : { [P in K]?: V; };
741
+ type EntityRow<E extends EntityDefinition = EntityDefinition> = string extends keyof E["fields"] ? Record<string, unknown> : Flatten<RowOfColumns<BaseColumnsType<E>> & SoftDeleteRow<E> & RowForFields<E["fields"]>>; // The row `executor.list()` / `executor.detail()` return for entity `E`.
742
+ type EntityTableTenancy<E extends EntityDefinition> = [ NonNullable<E["tenancy"]> ] extends [ never ] ? "tenant" : [ NonNullable<E["tenancy"]> ] extends [ "global" ] ? "global" : "tenant";
743
+ type Flatten<T> = { [K in keyof T]: T[K]; } & {
744
+ };
745
+ type NullCol<T> = ColumnHandle & {
746
+ __notNull: false;
747
+ __value?: T;
748
+ };
749
+ type RowForField<K extends string, F extends FieldDefinition> = F extends { type: "money"; } ? CompoundRowField<K, MoneyRead, F> : F extends { type: "locatedTimestamp"; } ? CompoundRowField<K, LocatedTimestampRead, F> : [ ValueColumnsForField<K, F> ] extends [ never ] ? { [P in K]: unknown; } : RowOfColumns<ValueColumnsForField<K, F>>;
750
+ type RowForFields<F extends FieldsMap> = UnionToIntersection<{ [K in keyof F & string]: RowForField<K, F[K]>; }[keyof F & string]>;
751
+ type RowOfColumns<C> = { [K in keyof C]: ColumnValue<C[K]>; };
752
+ type SoftDeleteColumnsType = {
753
+ isDeleted: Col<boolean>;
754
+ deletedAt: NullCol<Temporal.Instant>;
755
+ deletedById: NullCol<string>;
756
+ };
757
+ type SoftDeleteRow<E extends EntityDefinition> = E["softDelete"] extends true ? RowOfColumns<SoftDeleteColumnsType> : [ E["softDelete"] ] extends [ false | undefined ] ? unknown : Partial<RowOfColumns<SoftDeleteColumnsType>>;
758
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
759
+ type ValueColumnsForField<K extends string, F extends FieldDefinition> = F extends { type: "text" | "longText" | "select" | "tz"; } ? F extends { required: true; } ? { [P in K]: Col<string>; } : { [P in K]: NullCol<string>; } : F extends { type: "boolean"; } ? F extends { default: boolean; } | { required: true; } ? { [P in K]: Col<boolean>; } : { [P in K]: NullCol<boolean>; } : F extends { type: "multiSelect"; } ? { [P in K]: Col<string[]>; } : F extends { type: "number"; } ? F extends { required: true; } ? { [P in K]: Col<number>; } : { [P in K]…;
760
+ ```
761
+
762
+ ## packages/framework/src/db/tenant-db.ts
763
+
764
+ ```ts
765
+ type CreateTenantDbOptions = ({ mode?: "tenant"; reason?: undefined; } | { mode: "system"; reason: string; }) & {
766
+ db: DbRunner;
767
+ tenantId: TenantId;
768
+ tracer?: Tracer;
769
+ meter?: Meter;
770
+ signal?: AbortSignal;
771
+ grants?: TenantDbGrants;
772
+ };
773
+ type PersonalDataGate = (tableName: string, keys: string[], entity?: EntityDefinition) => void;
774
+ type TenantDbGrants = {
775
+ globalWrites?: EscapeHatchDeclaration;
776
+ unsafeRaw?: EscapeHatchDeclaration;
777
+ report?: EscapeHatchReporter;
778
+ memberReadOnly?: boolean;
779
+ personalDataGate?: PersonalDataGate;
780
+ projectionRegistry?: Registry;
781
+ writesDeniedReason?: string;
782
+ };
783
+ ```
784
+
785
+ ## packages/framework/src/derivatives/derivatives-context.ts
786
+
787
+ ```ts
788
+ type OverlayResolverArgs = {
789
+ entityId: string;
790
+ tenantId: TenantId;
791
+ fieldName: string;
792
+ dataToken: string;
793
+ };
794
+ type OverlayResolverPlugin = {
795
+ resolve: (args: OverlayResolverArgs) => string | Promise<string>;
796
+ };
797
+ ```
798
+
799
+ ## packages/framework/src/engine/boot-validator/card-meta-overflow.ts
800
+
801
+ ```ts
802
+ type CardMetaOverflow = {
803
+ feature: string;
804
+ screen: string;
805
+ location?: string;
806
+ count: number;
807
+ fields: string[];
808
+ };
809
+ ```
810
+
811
+ ## packages/framework/src/engine/build-app-schema.ts
812
+
813
+ ```ts
814
+ type BuildAppSchemaOptions = {
815
+ authoringWarnings?: boolean; // Dev-server authoring hints (Settings-Hub placement).
816
+ searchAdapterMissing?: boolean; // Forwarded onto every FeatureSchema.searchAdapterMissing.
817
+ };
818
+ ```
819
+
820
+ ## packages/framework/src/engine/build-config-feature-schema.ts
821
+
822
+ ```ts
823
+ type ConfigFeatureSchema = {
824
+ screens: ScreenDefinition[];
825
+ navs: NavDefinition[];
826
+ workspace?: WorkspaceSchema;
827
+ translations?: TranslationKeys;
828
+ };
829
+ ```
830
+
831
+ ## packages/framework/src/engine/config-helpers.ts
832
+
833
+ ```ts
834
+ type ConfigKeyOptions<T extends ConfigKeyType> = {
835
+ write?: string[];
836
+ read?: string[];
837
+ default?: ConfigValue<T>;
838
+ encrypted?: boolean;
839
+ piiEncrypted?: T extends "text" ? boolean : never;
840
+ options?: string[];
841
+ optionsQuery?: T extends "select" ? string : never;
842
+ optionsQueryPayload?: T extends "select" ? OptionsQueryPayload : never;
843
+ bounds?: T extends "number" ? ConfigBounds : never;
844
+ pattern?: T extends "text" ? { regex: string; flags?: string; } : never;
845
+ computed?: ConfigComputedFn<T>;
846
+ validate?: ConfigValidateFn<T>;
847
+ allowPerRequest?: T extends "text" ? never : boolean;
848
+ required?: boolean;
849
+ env?: string;
850
+ inheritedToTenant?: boolean;
851
+ backing?: ConfigBacking;
852
+ mask?: ConfigMask;
853
+ group?: string;
854
+ };
855
+ ```
856
+
857
+ ## packages/framework/src/engine/define-feature.ts
858
+
859
+ ```ts
860
+ type DefineFeatureSettings = {
861
+ dedupeOptions?: Readonly<Record<string, unknown>>;
862
+ };
863
+ ```
864
+
865
+ ## packages/framework/src/engine/define-roles.ts
866
+
867
+ ```ts
868
+ type RoleMap<T extends string[]> = { [K in T[number]]: K; };
869
+ ```
870
+
871
+ ## packages/framework/src/engine/define-workflow.ts
872
+
873
+ ```ts
874
+ type WorkflowInput<TPayload = unknown, TData = unknown, TAwaits extends Record<string, string> = Record<string, string>> = { // Input shape for defineWorkflow() — the user-facing API.
875
+ name: string;
876
+ trigger: WorkflowTrigger;
877
+ awaits?: TAwaits;
878
+ steps: PipelineDef<TPayload, TData, { [K in keyof TAwaits]: AwaitedEventType; }>;
879
+ idempotencyKey?: string | ((event: WriteEvent<TPayload>) => string);
880
+ onError?: PipelineDef<unknown>;
881
+ };
882
+ type WorkflowTrigger = // Trigger configuration for a workflow.
883
+ | {
884
+ kind: "event";
885
+ eventType: string;
886
+ filter?: (event: WriteEvent) => boolean;
887
+ }
888
+ | {
889
+ kind: "cron";
890
+ schedule: string;
891
+ }
892
+ | {
893
+ kind: "webhook";
894
+ path: string;
895
+ }
896
+ ;
897
+ ```
898
+
899
+ ## packages/framework/src/engine/event-helpers.ts
900
+
901
+ ```ts
902
+ type EmitCtx = {
903
+ events: Pick<HandlerContext["events"], "unsafeAppend">;
904
+ };
905
+ ```
906
+
907
+ ## packages/framework/src/engine/extension-names.ts
908
+
909
+ ```ts
910
+ interface KumikoExtensionOptionsMap {
911
+ [EXT_TENANT_DATA]: TenantDataExtensionHooks;
912
+ [EXT_USER_DATA]: UserDataExtensionOptions;
913
+ [EXT_FILE_PROVIDER]: FileProviderPlugin;
914
+ [EXT_DERIVATIVE_RENDERER]: DerivativeRendererPlugin;
915
+ [EXT_DERIVATIVE_OVERLAY_RESOLVER]: OverlayResolverPlugin;
916
+ [EXT_PRINCIPAL_STATUS]: PrincipalStatusPlugin;
917
+ [EXT_TENANT_LIFECYCLE_STATUS]: TenantLifecycleStatusPlugin;
918
+ [EXT_ASSIGNABLE_ROLE]: {
919
+ assignableFrom?: AssignableFromRole;
920
+ };
921
+ [EXT_STORAGE_PROVIDER]: TenantResourceExtensionHooks;
922
+ [EXT_SEARCH_ADAPTER]: TenantResourceExtensionHooks;
923
+ [EXT_EXTERNAL_RESOURCE]: TenantResourceExtensionHooks;
924
+ [EXT_INFRA_RESOURCE]: TenantResourceExtensionHooks;
925
+ [TENANT_TIER_RESOLVER_EXT]: TierResolverPlugin;
926
+ }
927
+ ```
928
+
929
+ ## packages/framework/src/engine/extensions/tenant-data.ts
930
+
931
+ ```ts
932
+ interface TenantDataExtensionHooks {
933
+ destroy: TenantDataDestroyHook;
934
+ }
935
+ ```
936
+
937
+ ## packages/framework/src/engine/extensions/tenant-resource.ts
938
+
939
+ ```ts
940
+ interface TenantResourceExtensionHooks {
941
+ destroyTenant: TenantResourceDestroyHook;
942
+ }
943
+ ```
944
+
945
+ ## packages/framework/src/engine/extensions/user-data.ts
946
+
947
+ ```ts
948
+ type UserDataDeleteHookResult = // Forget hook: deletes or anonymizes entity rows belonging to a user.
949
+ | void
950
+ | {
951
+ status: "ok";
952
+ }
953
+ | {
954
+ status: "incomplete";
955
+ reason: string;
956
+ }
957
+ ;
958
+ type UserDataExtensionOptions = ({ export: UserDataExportHook; delete?: UserDataDeleteHook; } | { export?: UserDataExportHook; delete: UserDataDeleteHook; }) & {
959
+ order?: number;
960
+ };
961
+ type UserId = string;
962
+ ```
963
+
964
+ ## packages/framework/src/engine/feature-changelog.ts
965
+
966
+ ```ts
967
+ type ChangelogType = (typeof CHANGELOG_TYPES)[number];
968
+ type FeatureChangelog = {
969
+ feature: string;
970
+ entries: ChangelogEntry[];
971
+ };
972
+ ```
973
+
974
+ ## packages/framework/src/engine/feature-manifest.ts
975
+
976
+ ```ts
977
+ type BuildManifestOptions = {
978
+ source: string; // Description of the manifest's origin, included unchanged in the JSON.
979
+ featureNames?: ReadonlySet<string>; // Emit only these features (e.g. the 16 enterprise features from a Registry that also mounted their bundled requirements).
980
+ tier?: string; // Tags each feature and the top-level manifest (e.g. "enterprise").
981
+ };
982
+ ```
983
+
984
+ ## packages/framework/src/engine/handler-field-references.ts
985
+
986
+ ```ts
987
+ type HandlerFieldReference = {
988
+ entity: string;
989
+ by: string | undefined;
990
+ multiple: boolean;
991
+ };
992
+ ```
993
+
994
+ ## packages/framework/src/engine/paged-list.ts
995
+
996
+ ```ts
997
+ type PagedListRequest<TSortField extends string> = {
998
+ limit: number;
999
+ offset?: number | undefined;
1000
+ search?: string | undefined;
1001
+ sort?: TSortField | undefined;
1002
+ sortDirection?: "asc" | "desc" | undefined;
1003
+ totalCount?: boolean | undefined;
1004
+ };
1005
+ type PagedListRowValue = string | number | boolean | null;
1006
+ ```
1007
+
1008
+ ## packages/framework/src/engine/qualified-name.ts
1009
+
1010
+ ```ts
1011
+ type ParsedQn = {
1012
+ scope: string;
1013
+ type: string;
1014
+ name: string;
1015
+ };
1016
+ type QnType = string;
1017
+ ```
1018
+
1019
+ ## packages/framework/src/engine/role-assignment.ts
1020
+
1021
+ ```ts
1022
+ type AssignableFromRole = (typeof ASSIGNABLE_FROM_ROLE_LIST)[number];
1023
+ ```
1024
+
1025
+ ## packages/framework/src/engine/run-pipeline.ts
1026
+
1027
+ ```ts
1028
+ type StepListOutcome =
1029
+ | {
1030
+ kind: "return";
1031
+ result: WriteResult<unknown>;
1032
+ }
1033
+ | {
1034
+ kind: "suspended";
1035
+ stepIndex: number;
1036
+ }
1037
+ | {
1038
+ kind: "exhausted";
1039
+ }
1040
+ ;
1041
+ ```
1042
+
1043
+ ## packages/framework/src/engine/screen-helpers.ts
1044
+
1045
+ ```ts
1046
+ type FieldsOrGroupsSection = { // Structural reader contract for every section that can carry `fields` and/or `groups`: the `EditFieldsSection` union members and `EditWriteFormSection` all assign without narrowing.
1047
+ fields?: EditFieldSpec[];
1048
+ groups?: { title: string; fields: EditFieldSpec[]; }[];
1049
+ };
1050
+ type SelectOptionRules = Pick<SelectFieldDef, "options" | "conditionalOptions">;
1051
+ type UnavailableSelectOption = {
1052
+ field: string;
1053
+ value: string;
1054
+ allowed: string[];
1055
+ };
1056
+ ```
1057
+
1058
+ ## packages/framework/src/errors/classes.ts
1059
+
1060
+ ```ts
1061
+ type RateLimitDetails = {
1062
+ bucket: string;
1063
+ limit: number;
1064
+ windowSeconds: number;
1065
+ remaining: number;
1066
+ retryAfterSeconds: number;
1067
+ resetAt: string;
1068
+ };
1069
+ type UnconfiguredDetails = {
1070
+ feature: string;
1071
+ key: string;
1072
+ hint?: string;
1073
+ };
1074
+ type UniqueViolationDetails = {
1075
+ entityName: string;
1076
+ constraintName?: string;
1077
+ };
1078
+ type UnprocessableOpts = Pick<ErrorOpts, "i18nKey" | "i18nParams" | "cause" | "message"> & {
1079
+ details?: Readonly<Record<string, unknown>> & { reason?: never; };
1080
+ };
1081
+ type ValidationDetails = {
1082
+ fields: FieldIssue[];
1083
+ };
1084
+ type VersionConflictDetails = {
1085
+ entityId: number | string;
1086
+ expectedVersion: number;
1087
+ currentVersion: number;
1088
+ };
1089
+ ```
1090
+
1091
+ ## packages/framework/src/errors/kumiko-error.ts
1092
+
1093
+ ```ts
1094
+ type ErrorCtorInput = {
1095
+ message: string;
1096
+ i18nKey: string;
1097
+ i18nParams?: Readonly<Record<string, unknown>>;
1098
+ details?: unknown;
1099
+ cause?: Error;
1100
+ retryAfterSeconds?: number;
1101
+ };
1102
+ type ErrorOpts = {
1103
+ message?: string;
1104
+ i18nKey?: string;
1105
+ i18nParams?: Readonly<Record<string, unknown>>;
1106
+ details?: unknown;
1107
+ cause?: Error;
1108
+ };
1109
+ ```
1110
+
1111
+ ## packages/framework/src/event-store/archive.ts
1112
+
1113
+ ```ts
1114
+ type ArchiveStreamArgs = {
1115
+ tenantId: TenantId;
1116
+ aggregateId: string;
1117
+ aggregateType: string;
1118
+ archivedBy: string;
1119
+ reason?: string;
1120
+ };
1121
+ ```
1122
+
1123
+ ## packages/framework/src/event-store/event-store.ts
1124
+
1125
+ ```ts
1126
+ type EventToAppend = {
1127
+ aggregateId: string;
1128
+ aggregateType: string;
1129
+ tenantId: TenantId;
1130
+ expectedVersion: number;
1131
+ type: string;
1132
+ eventVersion?: number;
1133
+ payload: Record<string, unknown>;
1134
+ metadata: EventMetadata;
1135
+ };
1136
+ ```
1137
+
1138
+ ## packages/framework/src/event-store/provenance-append.ts
1139
+
1140
+ ```ts
1141
+ type ProvenanceEventInput = {
1142
+ aggregateId: string;
1143
+ aggregateType: string;
1144
+ tenantId: TenantId;
1145
+ expectedVersion: number | "current";
1146
+ type: string;
1147
+ payload: Record<string, unknown>;
1148
+ metadata: EventMetadata;
1149
+ };
1150
+ ```
1151
+
1152
+ ## packages/framework/src/event-store/tenant-db-append.ts
1153
+
1154
+ ```ts
1155
+ type AppendEventInTenantDbOptions = {
1156
+ registry?: Registry;
1157
+ };
1158
+ ```
1159
+
1160
+ ## packages/framework/src/event-store/transfer.ts
1161
+
1162
+ ```ts
1163
+ type TransferAggregateStreamsArgs = {
1164
+ sourceTenantId: TenantId;
1165
+ destinationTenantId: TenantId;
1166
+ aggregateType: string;
1167
+ aggregateIds: string[];
1168
+ transferredBy: string;
1169
+ };
1170
+ ```
1171
+
1172
+ ## packages/framework/src/event-store/upcaster.ts
1173
+
1174
+ ```ts
1175
+ type EventUpcasters = ReadonlyMap<string, { currentVersion: number; chain: ReadonlyMap<number, EventUpcastFn>; }>;
1176
+ ```
1177
+
1178
+ ## packages/framework/src/files/provider-resolver.ts
1179
+
1180
+ ```ts
1181
+ type FileProviderContext = { // Narrow surface type for provider plugins.
1182
+ config?: ConfigAccessor;
1183
+ registry?: Registry;
1184
+ secrets: SecretsContext | undefined;
1185
+ auditUserId?: string | undefined;
1186
+ };
1187
+ type FileProviderPlugin = { // File-Storage-Plugin contract.
1188
+ build: (ctx: FileProviderContext, tenantId: string) => Promise<FileStorageProvider>;
1189
+ };
1190
+ ```
1191
+
1192
+ ## packages/framework/src/jobs/job-runner-types.ts
1193
+
1194
+ ```ts
1195
+ type JobDroppedMeta = {
1196
+ tenantId: string;
1197
+ };
1198
+ type JobLogEntry = {
1199
+ level: "info" | "warn" | "error";
1200
+ message: string;
1201
+ timestamp: Temporal.Instant;
1202
+ };
1203
+ type JobMeta = {
1204
+ triggeredById?: string | undefined;
1205
+ payload?: string | undefined;
1206
+ attempt?: number | undefined;
1207
+ priority?: number | undefined;
1208
+ jobId?: string | undefined;
1209
+ tenantVisibleRun?: { tenantId: string; subject: string | null; } | undefined;
1210
+ };
1211
+ type JobOutcomeMeta = {
1212
+ tenantId: string;
1213
+ finalAttempt: boolean;
1214
+ tenantVisible?: { subject: string | null; messageKey: string | null; } | undefined;
1215
+ tenantVisibleRun?: { subject: string | null; } | undefined;
1216
+ };
1217
+ type JobQueuedMeta = {
1218
+ tenantId: string;
1219
+ subject: string | null;
1220
+ jobCreatedAtMs: number;
1221
+ };
1222
+ type JobRunner = {
1223
+ start(): Promise<void>;
1224
+ stop(): Promise<void>;
1225
+ dispatch(jobName: string, payload?: Record<string, unknown>, meta?: JobMeta): Promise<string>;
1226
+ handleEvent(eventName: string, payload: Record<string, unknown>, user?: SessionUser, triggerEvent?: JobTriggerEvent): Promise<void>;
1227
+ handleCatchUpEvent(jobName: string, eventName: string, payload: Record<string, unknown>, user: SessionUser, triggerEvent: JobTriggerEvent): Promise<void>;
1228
+ attachDispatcher(ref: DispatchWriteRef): void;
1229
+ countPendingJobs(): Promise<number>;
1230
+ };
1231
+ type JobRunnerOptions = {
1232
+ registry: Registry;
1233
+ context: AppContext;
1234
+ redisUrl: string;
1235
+ consumerLane?: JobRunIn | undefined;
1236
+ queueNamePrefix?: string | undefined;
1237
+ bootRedisTimeoutMs?: number | undefined;
1238
+ schedulerSeenRefreshMs?: number | undefined;
1239
+ jobRetention?: { completedAgeSec?: number | undefined; failedAgeSec?: number | undefined; } | undefined;
1240
+ getActiveTenantIds?: () => Promise<TenantId[]>;
1241
+ onJobStart?: (jobName: string, jobId: string, meta: JobMeta) => void;
1242
+ onJobComplete?: (jobName: string, jobId: string, duration: number, logs: JobLogEntry[], outcome?: JobOutcomeMeta) => void;
1243
+ onJobFailed?: (jobName: string, jobId: string, error: string, logs: JobLogEntry[], outcome?: JobOutcomeMeta) => void;
1244
+ onJobDeferred?: (jobName: string, jobId: string, meta: JobMeta) => void;
1245
+ onJobQueued?: (jobName: string, jobId: string, meta: JobQueuedMeta) => void;
1246
+ onJobDropped?: (jobName: string, jobId: string, meta: JobDroppedMeta) => void;
1247
+ };
1248
+ ```
1249
+
1250
+ ## packages/framework/src/lifecycle/lifecycle.ts
1251
+
1252
+ ```ts
1253
+ interface Lifecycle {
1254
+ state(): LifecycleState;
1255
+ uptimeSec(): number;
1256
+ markReady(): void;
1257
+ onStateChange(cb: StateChangeListener): () => void;
1258
+ registerShutdownHook(name: string, fn: ShutdownHookFn): void;
1259
+ hookNames(): string[];
1260
+ drain(opts?: { signal?: string; timeoutMs?: number; }): Promise<void>;
1261
+ }
1262
+ type LifecycleState = "starting" | "ready" | "draining" | "stopped";
1263
+ type ShutdownHookFn = (signal: string) => Promise<void>;
1264
+ type StateChangeListener = (from: LifecycleState, to: LifecycleState) => void;
1265
+ ```
1266
+
1267
+ ## packages/framework/src/observability/recording-meter.ts
1268
+
1269
+ ```ts
1270
+ type MetricEvent =
1271
+ | {
1272
+ type: "counter.inc";
1273
+ name: string;
1274
+ value: number;
1275
+ labels: MetricLabels | undefined;
1276
+ }
1277
+ | {
1278
+ type: "histogram.observe";
1279
+ name: string;
1280
+ value: number;
1281
+ labels: MetricLabels | undefined;
1282
+ }
1283
+ | {
1284
+ type: "gauge.set" | "gauge.inc" | "gauge.dec";
1285
+ name: string;
1286
+ value: number;
1287
+ labels: MetricLabels | undefined;
1288
+ }
1289
+ ;
1290
+ ```
1291
+
1292
+ ## packages/framework/src/observability/recording-tracer.ts
1293
+
1294
+ ```ts
1295
+ type RecordedSpan = {
1296
+ traceId: string;
1297
+ spanId: string;
1298
+ parentSpanId: string | undefined;
1299
+ name: string;
1300
+ kind: SpanKind;
1301
+ startTime: number;
1302
+ endTime: number | undefined;
1303
+ attributes: Record<string, SpanAttributeValue>;
1304
+ status: SpanStatus;
1305
+ statusMessage: string | undefined;
1306
+ exception: { name: string; message: string; } | undefined;
1307
+ };
1308
+ ```
1309
+
1310
+ ## packages/framework/src/pipeline/dispatch-shared.ts
1311
+
1312
+ ```ts
1313
+ type BatchCommand = {
1314
+ type: string;
1315
+ payload: unknown;
1316
+ };
1317
+ type BatchResult =
1318
+ | {
1319
+ isSuccess: true;
1320
+ results: WriteResult[];
1321
+ }
1322
+ | {
1323
+ isSuccess: false;
1324
+ error: WriteErrorInfo;
1325
+ failedIndex: number;
1326
+ results: WriteResult[];
1327
+ }
1328
+ ;
1329
+ ```
1330
+
1331
+ ## packages/framework/src/pipeline/dispatch-stream.ts
1332
+
1333
+ ```ts
1334
+ type StreamOptions = {
1335
+ tokenExpiresAtSec?: number | undefined; // JWT exp (epoch seconds) the stream was opened with; the stream ends when it passes.
1336
+ };
1337
+ ```
1338
+
1339
+ ## packages/framework/src/pipeline/dispatcher-utils.ts
1340
+
1341
+ ```ts
1342
+ type HandlerType = string | HandlerRef;
1343
+ ```
1344
+
1345
+ ## packages/framework/src/pipeline/dispatcher.ts
1346
+
1347
+ ```ts
1348
+ type Dispatcher = {
1349
+ write(type: HandlerType, payload: unknown, user: SessionUser, requestId?: string): Promise<WriteResult>;
1350
+ query(type: HandlerType, payload: unknown, user: SessionUser): Promise<unknown>;
1351
+ stream(type: HandlerType, payload: unknown, user: SessionUser, options?: StreamOptions): AsyncGenerator<unknown>;
1352
+ command(type: HandlerType, payload: unknown, user: SessionUser): Promise<void>;
1353
+ batch(commands: BatchCommand[], user: SessionUser, requestId?: string): Promise<BatchResult>;
1354
+ resolveAuthClaims(user: SessionUser): Promise<Record<string, unknown>>;
1355
+ resolveActiveMembership(userId: string, tenantId: TenantId): Promise<ActiveMembershipResult>;
1356
+ createMemberReader(tenantId: TenantId): MemberReader;
1357
+ };
1358
+ ```
1359
+
1360
+ ## packages/framework/src/pipeline/event-dispatcher.ts
1361
+
1362
+ ```ts
1363
+ type DispatcherPassResult = {
1364
+ processed: number;
1365
+ failed: number;
1366
+ byConsumer: Record<string, { processed: number; failed: number; }>;
1367
+ };
1368
+ type EventConsumer = {
1369
+ name: string;
1370
+ handler: EventConsumerHandler;
1371
+ batchHandler?: EventConsumerBatchHandler;
1372
+ errorPolicy?: EventConsumerErrorPolicy;
1373
+ featureName?: string;
1374
+ delivery?: "shared" | "per-instance";
1375
+ startFrom?: "beginning" | "now";
1376
+ };
1377
+ type EventConsumerBatchHandler = (events: StoredEvent[], ctx: AppContext) => Promise<void>;
1378
+ type EventConsumerErrorPolicy = {
1379
+ skipApplyErrors?: boolean;
1380
+ maxAttempts?: number;
1381
+ isTransientError?: (error: unknown) => boolean;
1382
+ };
1383
+ type EventConsumerHandler = (event: StoredEvent, ctx: AppContext) => Promise<void>;
1384
+ type EventDispatcher = {
1385
+ start(): Promise<void>;
1386
+ stop(): Promise<void>;
1387
+ runOnce(): Promise<DispatcherPassResult>;
1388
+ ensureRegistered(): Promise<void>;
1389
+ drain(): Promise<void>;
1390
+ withBackgroundPassesPaused<T>(fn: () => Promise<T>): Promise<T>;
1391
+ consumers: EventConsumer[];
1392
+ };
1393
+ ```
1394
+
1395
+ ## packages/framework/src/pipeline/lifecycle-pipeline.ts
1396
+
1397
+ ```ts
1398
+ type SystemHookDef<TFn> = {
1399
+ name: string;
1400
+ priority: number;
1401
+ fn: TFn;
1402
+ phase?: HookPhase;
1403
+ };
1404
+ type SystemHooks = {
1405
+ preSave?: SystemHookDef<PreSaveHookFn>[];
1406
+ postSave?: SystemHookDef<PostSaveHookFn>[];
1407
+ postSaveBatch?: SystemHookDef<PostSaveBatchHookFn>[];
1408
+ preDelete?: SystemHookDef<PreDeleteHookFn>[];
1409
+ postDelete?: SystemHookDef<PostDeleteHookFn>[];
1410
+ postDeleteBatch?: SystemHookDef<PostDeleteBatchHookFn>[];
1411
+ };
1412
+ ```
1413
+
1414
+ ## packages/framework/src/secrets/dek-cache.ts
1415
+
1416
+ ```ts
1417
+ type DekCache = {
1418
+ unwrapDek(encryptedDek: Buffer, kekVersion: number, provider: MasterKeyProvider, scope?: KeyScope): Promise<Buffer>;
1419
+ clear(): void;
1420
+ size(): number;
1421
+ };
1422
+ ```
1423
+
1424
+ ## packages/framework/src/secrets/envelope-cipher.ts
1425
+
1426
+ ```ts
1427
+ type EnvelopeCipherOptions = {
1428
+ dekCache?: DekCache;
1429
+ };
1430
+ ```
1431
+
1432
+ ## packages/framework/src/seeding/entity-seed.ts
1433
+
1434
+ ```ts
1435
+ type EventStoreSeedExisting<TId extends string | number = string> = {
1436
+ id: TId;
1437
+ version: number;
1438
+ };
1439
+ type RunEventStoreSeedOptions<TId extends string | number = string, TExisting extends EventStoreSeedExisting<TId> = EventStoreSeedExisting<TId>> = {
1440
+ existing: TExisting | null | undefined;
1441
+ ifExists?: SeedIfExists;
1442
+ create: () => Promise<{ id: TId; }>;
1443
+ update: (existing: TExisting) => Promise<{ id: TId; }>;
1444
+ };
1445
+ ```
1446
+
1447
+ ## packages/framework/src/ui-types/card-column-roles.ts
1448
+
1449
+ ```ts
1450
+ type CardColumnRoles<C extends CardRoleColumn> = {
1451
+ image: C | undefined;
1452
+ title: C | undefined;
1453
+ status: C | undefined;
1454
+ meta: C[];
1455
+ };
1456
+ type CardRoleColumn = {
1457
+ field: string;
1458
+ highlighted?: boolean;
1459
+ type?: string;
1460
+ renderer?: unknown;
1461
+ hideOnNarrow?: boolean;
1462
+ fileDisplay?: {
1463
+ kind: string;
1464
+ };
1465
+ };
1466
+ ```
1467
+
1468
+ ## packages/framework/src/ui-types/list-row-meta.ts
1469
+
1470
+ ```ts
1471
+ type ListRowMetaColumnType = "text" | "number" | "timestamp";
1472
+ type ListRowMetaReference = {
1473
+ refFeature: string;
1474
+ refEntity: string;
1475
+ refLabelField: string;
1476
+ };
1477
+ type ReferenceLookupSource = {
1478
+ queryQn: string;
1479
+ labelKey: string;
1480
+ };
1481
+ type SystemReferenceLabel = {
1482
+ id: string;
1483
+ aliases?: string[]; // Further literals that mean the same system identity (e.g. the "system" createdBy literal).
1484
+ labelKey: string;
1485
+ };
1486
+ ```
1487
+
1488
+ ## packages/renderer/src/app/client-actions.tsx
1489
+
1490
+ ```ts
1491
+ type ClientActionHandler = (payload: Readonly<Record<string, unknown>>) => Promise<ClientActionOutcome>;
1492
+ type ClientActionOutcome =
1493
+ | {
1494
+ ok: true;
1495
+ }
1496
+ | {
1497
+ ok: false;
1498
+ message: string;
1499
+ }
1500
+ ;
1501
+ ```
1502
+
1503
+ ## packages/renderer/src/app/column-renderers.tsx
1504
+
1505
+ ```ts
1506
+ type ColumnRendererComponent = ComponentType<ColumnRendererProps>;
1507
+ type ColumnRendererProps = {
1508
+ value: unknown;
1509
+ row: Readonly<Record<string, unknown>>;
1510
+ column: {
1511
+ field: string;
1512
+ };
1513
+ onChange?: (value: unknown) => void; // Set only when the caller (DataTable's `onCellChange`) wired an editable path for this cell — undefined means read-only.
1514
+ };
1515
+ ```
1516
+
1517
+ ## packages/renderer/src/app/content-editors.tsx
1518
+
1519
+ ```ts
1520
+ type ContentEditorComponent = ComponentType<ContentEditorProps>;
1521
+ type ContentEditorProps = {
1522
+ id: string; // DOM id every registered editor must render onto its own focusable root element.
1523
+ value: string;
1524
+ onChange: (value: string) => void;
1525
+ variables: string[]; // Variable names insertable as chips (from the collection's variableSchema).
1526
+ readOnly: boolean;
1527
+ };
1528
+ ```
1529
+
1530
+ ## packages/renderer/src/app/extension-sections.tsx
1531
+
1532
+ ```ts
1533
+ type ExtensionSectionComponent = ComponentType<ExtensionSectionProps>;
1534
+ type ExtensionSectionProps = {
1535
+ entityName: string;
1536
+ entityId: string | null;
1537
+ initialValues?: Readonly<Record<string, unknown>>; // Previously saved extension values for the entity (passed through from the loaded detail row).
1538
+ screenId?: string; // Set only in the list-header slot (`entityId` is null): the list's `screen.id`, so a header control can access the per-screen URL filter state (`useListUrlState`) — for example, a tag filter that narrows the list to a set of IDs.
1539
+ filterParams?: Readonly<Record<string, unknown>>; // Set only in a Dashboard-`custom` panel: the currently selected value of the screen filter (see DashboardFilterDefinition), merged as with any other panel query.
1540
+ values?: Readonly<Record<string, unknown>>; // Current form values of the host RenderEdit (controlled mode) — same snapshot reference as `RenderEditChangeState.values`.
1541
+ patch?: (partial: Readonly<Record<string, unknown>>) => void; // Sets host RenderEdit form values from outside (e.g. a VIN-decode roundtrip that fills other fields) — same function as `RenderEditControls.patch`, merges only the given keys.
1542
+ validate?: () => boolean; // Validates the host form without writing — same function as `RenderEditControls.validate`; field errors land in `snapshot.errors` on the field instead of a collective message.
1543
+ markSaved?: (fields?: string[]) => void; // Marks host form values as saved after the section wrote them itself, so a following navigation does not ask to discard.
1544
+ hasUnsavedChanges?: boolean; // Whether the host form has unsaved changes.
1545
+ wizardStep?: {
1546
+ index: number;
1547
+ isLast: boolean;
1548
+ }; // Current wizard step, only set for `layout.mode === "wizard"` forms.
1549
+ reportStepComplete?: (complete: boolean) => void; // Only set for extension steps of a `layout.mode: "wizard"` entityEdit editing an existing record (update mode); undefined everywhere else.
1550
+ };
1551
+ ```
1552
+
1553
+ ## packages/renderer/src/app/nav.tsx
1554
+
1555
+ ```ts
1556
+ type NavigateOptions = {
1557
+ searchParams?: Readonly<Record<string, string>>; // Search params of the target route, set in the same history entry as the path.
1558
+ };
1559
+ type ObjectTarget = {
1560
+ workspaceId?: string;
1561
+ entity: string;
1562
+ id: string;
1563
+ };
1564
+ type ScreenTarget = {
1565
+ workspaceId?: string;
1566
+ screenId: string;
1567
+ entityId?: string;
1568
+ };
1569
+ ```
1570
+
1571
+ ## packages/renderer/src/components/list-empty-state.tsx
1572
+
1573
+ ```ts
1574
+ type ListEmptyStateContent = {
1575
+ title: string;
1576
+ description?: string;
1577
+ action?: RenderEditAction;
1578
+ };
1579
+ ```
1580
+
1581
+ ## packages/renderer/src/components/render-edit-types.ts
1582
+
1583
+ ```ts
1584
+ type RenderEditAction = {
1585
+ id: string;
1586
+ label: string;
1587
+ onPress: () => void | Promise<void>;
1588
+ style?: "primary" | "secondary" | "danger";
1589
+ confirm?: string;
1590
+ confirmLabel?: string;
1591
+ confirmRequired?: boolean; // Overrides the default "danger implies a confirm dialog" rule.
1592
+ icon?: IconKey; // Resolved icon (author `RowAction.icon` or the id-derived default) — drives both the icon-left-of-text render and the icon-only collapse rule (see `shouldRenderActionsIconOnly`).
1593
+ };
1594
+ ```
1595
+
1596
+ ## packages/renderer/src/components/render-list-toolbar-action.tsx
1597
+
1598
+ ```ts
1599
+ type ToolbarActionButton = {
1600
+ id: string;
1601
+ label: string;
1602
+ style?: "primary" | "secondary" | "danger";
1603
+ confirm?: string;
1604
+ confirmLabel?: string;
1605
+ confirmRequired?: boolean; // Overrides the default "danger implies a confirm dialog" rule.
1606
+ onTrigger: () => Promise<void> | void;
1607
+ icon?: IconKey; // Id-derived default icon (ACTION_ICON_BY_ID in kumiko-screen.tsx) — ToolbarAction has no author-declared icon field, unlike RowAction.
1608
+ };
1609
+ ```
1610
+
1611
+ ## packages/renderer/src/context/draft-storage-context.tsx
1612
+
1613
+ ```ts
1614
+ type DraftStorage = {
1615
+ getDraftId: (screenId: string) => string | null;
1616
+ setDraftId: (screenId: string, draftId: string) => void;
1617
+ clearDraftId: (screenId: string) => void;
1618
+ };
1619
+ ```
1620
+
1621
+ ## packages/renderer/src/data-table-aggregate-types.ts
1622
+
1623
+ ```ts
1624
+ type DataTableListNotice = "truncated" | "searchTruncated"; // `truncated`: more groups than the server answers.
1625
+ type DataTableMeasure = { // One server-computed measure; a money measure holds one minor-unit value per currency.
1626
+ field: string;
1627
+ kind: ListAggregateMeasureKind;
1628
+ value: ListAggregateValue;
1629
+ };
1630
+ type DataTableRangeBound = "min" | "max";
1631
+ type DataTableRangeFacet = { // "From/to" inputs on a number field.
1632
+ field: string;
1633
+ label: string;
1634
+ min: string;
1635
+ max: string;
1636
+ unit?: string; // Shown after the inputs, e.g. the money field's currency.
1637
+ };
1638
+ type DataTableServerGroup = {
1639
+ key: string; // `relatedListGroupKey` of the group value; stable across reloads.
1640
+ value: AggregateKey; // Raw group value, rendered through the group column when `label` is absent.
1641
+ label?: string;
1642
+ count: number; // Rows of the whole group on the server, not the loaded ones.
1643
+ measures: DataTableMeasure[];
1644
+ collapsed: boolean;
1645
+ rowCount: number; // How many of the table's `rows` belong to this group; rows arrive in group order.
1646
+ hasMore: boolean;
1647
+ loading: boolean;
1648
+ };
1649
+ type DataTableServerGroups = { // Server-side grouping: groups are the load unit, each loads its own rows.
1650
+ field: string;
1651
+ groups: DataTableServerGroup[];
1652
+ totalCount: number;
1653
+ onToggle: (key: string) => void;
1654
+ onLoadMore: (key: string) => void;
1655
+ onHeaderVisible: (key: string) => void;
1656
+ };
1657
+ type DataTableTotals = {
1658
+ measures: DataTableMeasure[];
1659
+ };
1660
+ ```
1661
+
1662
+ ## packages/renderer/src/data-table-bulk-types.ts
1663
+
1664
+ ```ts
1665
+ type BulkRowOutcome =
1666
+ | {
1667
+ kind: "succeeded";
1668
+ }
1669
+ | {
1670
+ kind: "failed";
1671
+ message: string;
1672
+ }
1673
+ ;
1674
+ type DataTableBulk = {
1675
+ actions: DataTableBulkAction[];
1676
+ reload: () => Promise<void>; // Reloads the list once after a run.
1677
+ };
1678
+ type DataTableBulkAction = {
1679
+ id: string;
1680
+ label: string; // Translated label of the underlying row action.
1681
+ style?: "primary" | "secondary" | "danger";
1682
+ icon?: IconKey;
1683
+ confirmText?: (count: number) => string; // Present: one confirm dialog for the whole selection, with this text for `count` rows.
1684
+ confirmLabel?: string;
1685
+ appliesTo: (row: ListRowViewModel) => boolean; // False: the row action's `visible` hides it for this row; the run skips the row.
1686
+ writeRow: (row: ListRowViewModel) => Promise<BulkRowOutcome>; // Exactly one server write for one row.
1687
+ };
1688
+ ```
1689
+
1690
+ ## packages/renderer/src/data-table-facet-types.ts
1691
+
1692
+ ```ts
1693
+ type DataTableDateRangeFacet = {
1694
+ field: string; // The facet's namespace (its date field) — a key, not a filter value.
1695
+ label: string; // Translated label ("When").
1696
+ from: string;
1697
+ to: string;
1698
+ };
1699
+ type DataTableFacet = {
1700
+ field: string; // Entity-Feldname (= URL-State-Key + payload.filters[].field).
1701
+ label: string; // Translated Button-Label ("Status", "Active").
1702
+ options: { value: string; label: string; }[]; // Selectable values with display labels.
1703
+ chips?: DataTableFacetChip[]; // Present: render the facet as a row of single-choice toggle chips instead of a dropdown.
1704
+ showCounts?: boolean; // Chips only: show each chip's `count` next to its label.
1705
+ hideEmpty?: boolean; // Chips only: hide chips whose `count` is 0 (unless pressed).
1706
+ onSearchChange?: (query: string) => void; // Present: the options come from a server-side search (reference facets), so the dropdown shows a search field and reports the typed term instead of filtering `options` itself.
1707
+ searching?: boolean; // The search is loading; the option list may still show the previous term's rows.
1708
+ };
1709
+ type DataTableFacetChip = {
1710
+ id: string;
1711
+ label: string; // Translated chip label.
1712
+ values: string[]; // Selection a click sets; `[]` clears the facet.
1713
+ count?: number; // Matching row count, undefined while unknown.
1714
+ };
1715
+ ```
1716
+
1717
+ ## packages/renderer/src/data-table-view-types.ts
1718
+
1719
+ ```ts
1720
+ type DataTableColumnPin = ListColumnPin;
1721
+ type DataTableColumnPins = Readonly<Record<string, DataTableColumnPin>>;
1722
+ type DataTableColumnWidths = Readonly<Record<string, number>>;
1723
+ type DataTableDensity = ListTableDensity;
1724
+ type DataTableExport = { // What the export button sends to POST /api/export/xlsx (xlsx is the only format).
1725
+ screen: string; // Screen QN `<feature>:screen:<id>`.
1726
+ screenShortId: string;
1727
+ payload: Readonly<Record<string, unknown>>; // The list payload without paging keys; the server reads the whole filtered list.
1728
+ columns: string[]; // Visible column ids in display order.
1729
+ };
1730
+ type DataTableGroupByChoice = {
1731
+ value: string; // Active group field, "" = not grouped.
1732
+ fields: string[];
1733
+ onChange: (next: string) => void;
1734
+ };
1735
+ type DataTableResetAll = {
1736
+ active: boolean; // True while something deviates from the screen default.
1737
+ onReset: () => void;
1738
+ };
1739
+ type DataTableTableBlock = { // The DataTable props a screen's `table` block feeds.
1740
+ view?: DataTableViewOptions; // View menu (column picker, density); absent = no menu.
1741
+ resetAll?: DataTableResetAll; // Replaces the facet reset with one that also covers sort, search and page size.
1742
+ exportList?: DataTableExport; // Present: the toolbar has an export button (web only).
1743
+ serverGroups?: DataTableServerGroups; // Present: the rows are grouped on the server; replaces pager and infinite scroll.
1744
+ totals?: DataTableTotals; // Present: a footer row with the server-computed totals of the filtered list.
1745
+ notice?: DataTableListNotice; // Present: the toolbar says the answer is incomplete and asks to narrow the list.
1746
+ rangeFacets?: DataTableRangeFacet[];
1747
+ onRangeChange?: (field: string, bound: DataTableRangeBound, value: string) => void;
1748
+ };
1749
+ type DataTableViewColumn = { // One column in the view menu.
1750
+ field: string;
1751
+ visible: boolean;
1752
+ };
1753
+ type DataTableViewOptions = {
1754
+ columns?: DataTableViewColumn[]; // Present: the view menu has a column section (visibility + order).
1755
+ onColumnsChange?: (next: DataTableViewColumn[]) => void;
1756
+ density: DataTableDensity;
1757
+ onDensityChange?: (next: DataTableDensity) => void; // Present: the view menu has a density section.
1758
+ pins?: DataTableColumnPins; // Present: pinning is on and the view menu has a pin section.
1759
+ onPinsChange?: (next: DataTableColumnPins) => void;
1760
+ widths?: DataTableColumnWidths; // Present: resizing is on.
1761
+ minWidths?: DataTableColumnWidths; // Present together with `widths`; every column has an entry (px).
1762
+ onWidthChange?: (field: string, width: number, others?: DataTableColumnWidths) => void; // `others` holds the displayed widths of the columns that had no entry yet, so they keep their size once the layout turns fixed.
1763
+ keyboardNavigation?: boolean;
1764
+ groupBy?: DataTableGroupByChoice; // Present: the view menu has a "group by" section.
1765
+ viewIsDefault: boolean; // False once columns, order or density differ from the screen default.
1766
+ onViewReset: () => void;
1767
+ };
1768
+ ```
1769
+
1770
+ ## packages/renderer/src/headless/contracts/locale.ts
1771
+
1772
+ ```ts
1773
+ type LocaleResolver = {
1774
+ translate(key: string, params?: Readonly<Record<string, unknown>>): string;
1775
+ locale(): string;
1776
+ timeZone(): string;
1777
+ subscribe(listener: () => void): () => void;
1778
+ setLocale?(locale: string): void;
1779
+ };
1780
+ ```
1781
+
1782
+ ## packages/renderer/src/headless/dispatcher/types.ts
1783
+
1784
+ ```ts
1785
+ type BatchResult =
1786
+ | {
1787
+ isSuccess: true;
1788
+ results: WriteResult[];
1789
+ }
1790
+ | {
1791
+ isSuccess: false;
1792
+ error: DispatcherError;
1793
+ failedIndex: number;
1794
+ results: WriteResult[];
1795
+ }
1796
+ ;
1797
+ type Command = {
1798
+ type: string;
1799
+ payload: unknown;
1800
+ };
1801
+ type Dispatcher = {
1802
+ write<TData = unknown>(type: string, payload: unknown, opts?: WriteOpts): Promise<WriteResult<TData>>;
1803
+ query<TData = unknown>(type: string, payload: unknown, opts?: QueryOpts): Promise<QueryResult<TData>>;
1804
+ batch(commands: Command[], opts?: WriteOpts): Promise<BatchResult>;
1805
+ stream<TChunk = unknown>(type: string, payload: unknown, opts?: StreamOpts): AsyncGenerator<TChunk, void, undefined>;
1806
+ statusStore: Store<DispatcherStatus>;
1807
+ pendingWrites(): PendingWrite[];
1808
+ pendingFiles(): PendingFile[];
1809
+ };
1810
+ type DispatcherError = {
1811
+ code: string;
1812
+ httpStatus: number;
1813
+ i18nKey: string;
1814
+ i18nParams?: Readonly<Record<string, unknown>>;
1815
+ message: string;
1816
+ details?: { fields?: FieldIssue[]; } & Record<string, unknown>;
1817
+ docsUrl?: string;
1818
+ requestId?: string;
1819
+ };
1820
+ type DispatcherStatus = "online" | "offline" | "syncing";
1821
+ type PendingFile = {
1822
+ id: string;
1823
+ fileName: string;
1824
+ sizeBytes: number;
1825
+ queuedAt: string;
1826
+ attempts: number;
1827
+ progress?: number;
1828
+ lastError?: DispatcherError;
1829
+ };
1830
+ type PendingWrite = {
1831
+ id: string;
1832
+ type: string;
1833
+ payload: unknown;
1834
+ queuedAt: string;
1835
+ attempts: number;
1836
+ lastError?: DispatcherError;
1837
+ };
1838
+ type QueryOpts = {
1839
+ signal?: AbortSignal;
1840
+ };
1841
+ type QueryResult<TData = unknown> =
1842
+ | {
1843
+ isSuccess: true;
1844
+ data: TData;
1845
+ }
1846
+ | {
1847
+ isSuccess: false;
1848
+ error: DispatcherError;
1849
+ }
1850
+ ;
1851
+ type StreamOpts = {
1852
+ signal?: AbortSignal;
1853
+ };
1854
+ type WriteOpts = {
1855
+ requestId?: string;
1856
+ signal?: AbortSignal;
1857
+ };
1858
+ type WriteResult<TData = unknown> =
1859
+ | {
1860
+ isSuccess: true;
1861
+ data: TData;
1862
+ }
1863
+ | {
1864
+ isSuccess: false;
1865
+ error: DispatcherError;
1866
+ }
1867
+ ;
1868
+ ```
1869
+
1870
+ ## packages/renderer/src/headless/form/types.ts
1871
+
1872
+ ```ts
1873
+ type FieldConditionPredicate<TValues extends FormValues, TCtx> = (values: TValues, ctx: TCtx) => boolean;
1874
+ type FieldConditionValue<TValues extends FormValues, TCtx> = boolean | FieldConditionPredicate<TValues, TCtx>;
1875
+ type FieldConditions<TValues extends FormValues, TCtx = unknown> = {
1876
+ visible?: FieldConditionValue<TValues, TCtx>;
1877
+ readonly?: FieldConditionValue<TValues, TCtx>;
1878
+ required?: FieldConditionValue<TValues, TCtx>;
1879
+ };
1880
+ type FieldState = {
1881
+ visible: boolean;
1882
+ readonly: boolean;
1883
+ required: boolean;
1884
+ };
1885
+ type FormController<TValues extends FormValues> = {
1886
+ getSnapshot(): FormSnapshot<TValues>;
1887
+ subscribe(listener: () => void): () => void;
1888
+ setField<K extends keyof TValues>(key: K, value: TValues[K]): void;
1889
+ setValues(partial: Partial<TValues>): void;
1890
+ clearErrors(path?: string): void;
1891
+ setErrors(errors: Readonly<Record<string, FieldIssue[]>>): void;
1892
+ validate(scope?: FormScopeKey<TValues>[]): boolean;
1893
+ reset(): void;
1894
+ rebase(): void;
1895
+ rebaseFields(fields: string[]): void;
1896
+ seedField(field: string, value: unknown): void;
1897
+ setCtx(ctx: unknown): void;
1898
+ submit<TData = unknown>(): Promise<SubmitResult<TData>>;
1899
+ };
1900
+ type FormControllerOptions<TValues extends FormValues, TCtx = unknown> = {
1901
+ initial: TValues;
1902
+ schema?: ZodType;
1903
+ fields?: Readonly<Record<string, FieldConditions<TValues, TCtx>>>;
1904
+ ctx?: TCtx;
1905
+ submit?: SubmitConfig<TValues>;
1906
+ };
1907
+ type FormScopeKey<TValues extends FormValues> = (keyof TValues & string) | `${keyof TValues & string}.${string}`;
1908
+ type FormSnapshot<TValues extends FormValues> = {
1909
+ values: TValues;
1910
+ initial: TValues;
1911
+ changes: Partial<TValues>;
1912
+ isDirty: boolean;
1913
+ isUnchanged: boolean;
1914
+ errors: Readonly<Record<string, FieldIssue[]>>;
1915
+ fields: Readonly<Record<string, FieldState>>;
1916
+ };
1917
+ type FormValues = Record<string, unknown>;
1918
+ type SubmitConfig<TValues extends FormValues = FormValues> = {
1919
+ dispatcher?: Dispatcher;
1920
+ type: string;
1921
+ payloadMode?: SubmitPayloadMode;
1922
+ stripEmptySeeds?: boolean; // Only applies to payloadMode "values".
1923
+ omitFields?: string[]; // Top-level payload keys removed right before dispatch, for fields that are shown or validated but must not reach the write handler.
1924
+ buildPayload?: (snapshot: FormSnapshot<TValues>) => unknown;
1925
+ serverFieldPathPrefix?: string;
1926
+ validateScope?: string[]; // Restricts submit()'s internal validate() call to these field names — same contract as validate()'s `scope` param.
1927
+ };
1928
+ type SubmitPayloadMode = "values" | "changes";
1929
+ type SubmitResult<TData = unknown> =
1930
+ | ({ validationBlocked: false; isNoOp?: boolean; } & WriteResult<TData>)
1931
+ | {
1932
+ validationBlocked: true;
1933
+ isSuccess: false;
1934
+ }
1935
+ ;
1936
+ ```
1937
+
1938
+ ## packages/renderer/src/headless/nav/types.ts
1939
+
1940
+ ```ts
1941
+ type NavRegistrySlice = {
1942
+ topLevel: NavDefinition[];
1943
+ byParent: (parentQualifiedName: string) => NavDefinition[];
1944
+ };
1945
+ ```
1946
+
1947
+ ## packages/renderer/src/headless/store/types.ts
1948
+
1949
+ ```ts
1950
+ type Store<T> = {
1951
+ getSnapshot(): T;
1952
+ subscribe(listener: () => void): () => void;
1953
+ };
1954
+ ```
1955
+
1956
+ ## packages/renderer/src/headless/view-model/types.ts
1957
+
1958
+ ```ts
1959
+ type CellLink = { // Where a cell's text links to.
1960
+ href: string;
1961
+ onPress: () => void;
1962
+ };
1963
+ type CellLinkItem = { // One entry of a multi-reference cell: its label, linked when the target is readable.
1964
+ id: string;
1965
+ label: string;
1966
+ link?: CellLink;
1967
+ };
1968
+ type EditFieldViewModel = {
1969
+ field: string;
1970
+ label: string;
1971
+ type: string;
1972
+ value: unknown;
1973
+ visible: boolean;
1974
+ readOnly: boolean;
1975
+ required: boolean;
1976
+ span?: number;
1977
+ renderer?: FieldRenderer;
1978
+ options?: string[]; // Set for `type: "select"` and `type: "multiSelect"` — the allowed values.
1979
+ optionLabels?: Readonly<Record<string, string>>; // Set for `type: "select"` and `type: "multiSelect"` — translated labels per option, keyed by raw value.
1980
+ selectOptionsQuery?: {
1981
+ query: string;
1982
+ payload: OptionsQueryPayload;
1983
+ }; // Set for `type: "select"` when SelectFieldDef.optionsQuery is set: the renderer loads the options from this query (`{ rows: { value, label, description?, group?
1984
+ selectOptionsAvailabilityQuery?: {
1985
+ query: string;
1986
+ payload: OptionsQueryPayload;
1987
+ }; // Set for `type: "select"` / `"multiSelect"` when optionsAvailabilityQuery is set: the renderer loads `{ rows: { value, disabled?, hint?
1988
+ display?: "checkboxes" | "dropdown" | "radio"; // Set for `type: "multiSelect"` when MultiSelectFieldDef.display is "checkboxes" — the renderer shows a checkbox grid with a select-all toggle instead of the default combobox dropdown.
1989
+ columns?: 1 | 2 | 3 | 4; // Set for `type: "multiSelect"` when MultiSelectFieldDef.columns is defined — column count for the checkbox grid at the widest breakpoint.
1990
+ maxRows?: number; // Set for `type: "multiSelect"` when MultiSelectFieldDef.maxRows is defined — grid rows visible before the checkbox grid becomes scrollable.
1991
+ multiline?: boolean | { rows?: number; }; // For `type: "text"`, set when TextFieldDef.multiline is true — the renderer then renders a textarea instead of a single-line input.
1992
+ format?: "email" | "url" | "phone" | "password" | "month" | "iban"; // From TextFieldDef.format — `password` makes the renderer mask the input.
1993
+ autoComplete?: string; // From TextFieldDef.autoComplete — HTML autocomplete token for the text input.
1994
+ writeOnly?: true; // From TextFieldDef.writeOnly — value is `true` (set) / `null` (empty), never plaintext.
1995
+ sensitive?: true; // From the field's `sensitive: true` — the value must not leave the form (drafts, URL prefill).
1996
+ wallClock?: boolean; // Only set for `type: "timestamp"` when TimestampFieldDef.locatedBy exists — wall-clock time without offset.
1997
+ min?: string; // Only for `type: "date" | "timestamp"` — date bounds (ISO) and format/locale override from the FieldDefinition.
1998
+ max?: string;
1999
+ dateLocale?: string;
2000
+ grouping?: boolean; // Only for `type: "number"` — mirrors `NumberFieldDef.grouping` (default `true`).
2001
+ refEntity?: string; // Only set for `type: "reference"`.
2002
+ refFeature?: string; // Only set for `type: "reference"` — name of the feature the referenced entity lives in.
2003
+ refLabelField?: string; // Only set for `type: "reference"` — which field of the referenced entity appears as the display label in the dropdown.
2004
+ refOptionsQuery?: string; // Only for `type: "reference"` — query handler QN the picker sources its options from, replacing the referenced entity's list handler.
2005
+ refMultiple?: boolean; // Only for `type: "reference"` — multi mode: the value is a UUID array, the renderer mounts a multi-combobox with tags.
2006
+ accept?: string[]; // Only for `type: "file" | "image"` — allowed extensions/MIME (e.g. ["jpg","png"]) + max size ("5mb").
2007
+ maxSize?: string;
2008
+ maxCount?: number; // Only for `type: "files" | "images"` — upper bound on list length.
2009
+ entityType?: string; // Only for `type: "file" | "image"` — entity name + field name for the upload POST (`/api/files`), so the endpoint can check maxSize/accept against the right field def.
2010
+ uploadActionForm?: string; // Only for file fields of an actionForm — the screen QN the upload POST names as `actionForm` instead of `entityType`; the server resolves the field through that screen.
2011
+ fieldName?: string;
2012
+ fileDisplay?: "list" | "dropzone"; // Only for `type: "files" | "images"` — how the field is shown.
2013
+ entityId?: string; // Record being edited (update mode only).
2014
+ imageVariant?: string; // Only for `type: "image"` and `"images"` — the variant name the preview requests (`/api/files/:id/variant/:name`) instead of the original.
2015
+ capture?: "environment" | "user"; // Only for `type: "image"` — forwarded to the file input's `capture` attribute so a phone opens the camera instead of the file picker.
2016
+ icon?: FieldIconKey; // Closed FieldIconKey from EditFieldSpec.icon (FIELD_ICONS registry).
2017
+ placeholder?: string; // From EditFieldSpec.placeholder, already translated — hint shown while the input is empty.
2018
+ prefix?: FieldAffix; // From EditFieldSpec.prefix / suffix.
2019
+ suffix?: FieldAffix;
2020
+ fillAction?: FieldFillAction; // From EditFieldSpec.fillAction, `label` already translated.
2021
+ embeddedListCells?: EmbeddedListCellViewModel[]; // Only set for an embedded-LIST field (`multiple: true`) — the per-cell metadata the renderer needs to draw each column/cell.
2022
+ embeddedListMinItems?: number; // Minimum row count for the embedded list.
2023
+ embeddedListMaxItems?: number; // Maximum row count for the embedded list.
2024
+ embeddedListDerived?: Readonly<Record<string, { op: "multiply" | "sum" | "subtract" | "percent"; from: string[]; }>>; // Sub-field name → how to compute its cell from other sub-fields of the same row.
2025
+ embeddedListTotals?: (string | { label: string; from: string[]; })[]; // Footer totals: a sub-field name, or a labelled sum of several.
2026
+ embeddedListFixedRows?: boolean; // No add, remove, duplicate or move; cells stay editable.
2027
+ embeddedListCurrency?: string; // Currency for the whole embedded list's money cells and totals row — currency lives on the head aggregate (EntityDefinition.defaultCurrency), not per row.
2028
+ currency?: string; // Only set for a plain (non-list, non-embedded) `type: "money"` field — same source as embeddedListCurrency (EntityDefinition.defaultCurrency, falls back to "EUR").
2029
+ unit?: string | { field: string; }; // Only set for `type: "number"` when NumberFieldDef.unit is set — a static suffix string, or a sibling-field reference the renderer resolves against the live row values.
2030
+ };
2031
+ type EmbeddedListCellViewModel = {
2032
+ field: string;
2033
+ label: string;
2034
+ type: "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
2035
+ required: boolean;
2036
+ options?: string[]; // Only for `type: "select"`.
2037
+ optionLabels?: Readonly<Record<string, string>>;
2038
+ display?: "radio" | "dropdown"; // Only for `type: "select"`; `"radio"` renders segmented buttons instead of a dropdown.
2039
+ readOnly?: boolean; // Cell is shown but not editable (`EditFieldSpec.embeddedList.readOnlyCells`).
2040
+ refEntity?: string; // Only for `type: "reference"`.
2041
+ refFeature?: string;
2042
+ refLabelField?: string;
2043
+ refOptionsQuery?: string;
2044
+ scale?: number; // Only for `type: "decimal"`.
2045
+ };
2046
+ type ListColumnFileDisplay = // How a list cell shows a FileRef value (id or id array).
2047
+ | {
2048
+ kind: "image";
2049
+ variant?: string;
2050
+ }
2051
+ | {
2052
+ kind: "file";
2053
+ }
2054
+ ;
2055
+ type ListColumnViewModel = {
2056
+ field: string;
2057
+ label: string;
2058
+ type: string;
2059
+ renderer?: FieldRenderer | RuntimeRenderer;
2060
+ linkFor?: RuntimeCellLink;
2061
+ linkItemsFor?: RuntimeCellLinkItems;
2062
+ sortable: boolean;
2063
+ optionLabels?: Readonly<Record<string, string>>; // Only for `type: "select"` or `type: "multiSelect"` — translated option labels keyed by raw value.
2064
+ optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>; // Only for `type: "select"` — declared tone per raw option value.
2065
+ refEntity?: string; // Only for `type: "reference"` — referenced entity name for the bulk lookup in the renderer (`<refFeature>:query:<refEntity>:list`).
2066
+ refFeature?: string; // Only for `type: "reference"` — name of the feature the referenced entity lives in.
2067
+ refLabelField?: string; // Only for `type: "reference"` — which field of the referenced entity appears as the display value in the cell (default "id").
2068
+ highlighted?: boolean; // Set by the caller at runtime (not schema-derived) to emphasize one column — e.g. the currently selected base-year column in a multi-year statement grid.
2069
+ hideOnNarrow?: boolean; // Mirrors `ListColumnSpec.hideOnNarrow`: card layout skips the column.
2070
+ grouping?: boolean; // Only for `type: "number"` — mirrors `NumberFieldDef.grouping` (default `true`).
2071
+ fileDisplay?: ListColumnFileDisplay; // Set for file/image columns: from the entity field type, or from a `{ format: "image" | "file" }` renderer.
2072
+ };
2073
+ type ListRowViewModel = {
2074
+ id: string;
2075
+ values: Readonly<Record<string, unknown>>;
2076
+ };
2077
+ type RuntimeCellLink = (value: unknown, row?: Readonly<Record<string, unknown>>) => CellLink | undefined; // Runtime-only like RuntimeRenderer; undefined means the cell stays plain text.
2078
+ type RuntimeCellLinkItems = (value: unknown, row?: Readonly<Record<string, unknown>>) => CellLinkItem[] | undefined; // Runtime-only like RuntimeCellLink; undefined means the cell uses its single renderer.
2079
+ type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
2080
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
2081
+ ```
2082
+
2083
+ ## packages/renderer/src/hooks/use-list-url-state.ts
2084
+
2085
+ ```ts
2086
+ type ListSort = DataTableSort;
2087
+ ```
2088
+
2089
+ ## packages/renderer/src/metric-types.ts
2090
+
2091
+ ```ts
2092
+ type MetricBandProps = { // Band above the tabs of a record detail: optional subtitle line plus the metrics as a definition list.
2093
+ lead?: ReactNode; // Free-form block rendered unstyled as the first full-width row of the band.
2094
+ subtitle?: ReactNode;
2095
+ children: ReactNode;
2096
+ tiles?: boolean; // The children are KPI tiles, laid out as a tile grid instead of a definition list.
2097
+ testId?: string;
2098
+ };
2099
+ type MetricProps = { // Compact label/value tile (record-detail metrics band).
2100
+ label: string;
2101
+ value: string;
2102
+ testId?: string;
2103
+ onPress?: () => void; // Click handler — set when the metric declares a `navigate` target (see `MetricSpec`).
2104
+ tile?: boolean; // Draw as a KPI tile (the dashboard StatCard) instead of a band entry.
2105
+ tone?: DashboardStatTone;
2106
+ icon?: NavIconKey;
2107
+ };
2108
+ ```
2109
+
2110
+ ## packages/renderer/src/primitives.tsx
2111
+
2112
+ ```ts
2113
+ type ActionMenuItemSpec = { // One item in an `ActionOverflowMenu` (header/row actions beyond the primary one collapse into this menu instead of a wall of buttons).
2114
+ id: string;
2115
+ label: string;
2116
+ icon?: IconKey; // Icon name, resolved by the implementation (same names as row actions).
2117
+ disabled?: boolean;
2118
+ variant?: "default" | "danger";
2119
+ testId?: string;
2120
+ onSelect: () => void;
2121
+ };
2122
+ type ActionOverflowMenuProps = {
2123
+ items: ActionMenuItemSpec[];
2124
+ label: string; // aria-label for the three-dots trigger.
2125
+ testId?: string;
2126
+ };
2127
+ type ButtonBaseProps = {
2128
+ type?: "button" | "submit";
2129
+ onClick?: () => void | Promise<void>;
2130
+ disabled?: boolean;
2131
+ loading?: boolean; // Render a spinner instead of the children.
2132
+ variant?: "primary" | "secondary" | "danger" | "link" | "danger-ghost" | "ghost"; // Semantic class — default="primary".
2133
+ size?: "sm" | "md" | "icon"; // Size—default="md".
2134
+ ariaLabel?: string; // Accessible label—required for icon-only buttons (when children contain only an icon or character), otherwise the button has no accessible name.
2135
+ title?: string; // Tooltip — rendered as the `title` attribute.
2136
+ pressed?: boolean; // Toggle state for toggle buttons — web sets `aria-pressed`; leave unset for plain buttons.
2137
+ expanded?: boolean; // Disclosure state for buttons that open a menu or panel — web sets `aria-expanded`.
2138
+ width?: "full" | "auto"; // Width—default="auto" (content width).
2139
+ testId?: string;
2140
+ className?: string; // Layout extras, merged via cn().
2141
+ ref?: Ref<HTMLButtonElement>; // DOM ref on the button — escape hatch for cases that need more than a click (e.g. binding a drop-target handler).
2142
+ icon?: NavIconKey; // Icon before the label.
2143
+ iconEnd?: NavIconKey; // Icon after the label (e.g. "Continue →").
2144
+ dataAttributes?: DataAttributes;
2145
+ };
2146
+ type CardOptions = { // Card-Options — Verhalten/Optik, ebenfalls additiv erweiterbar.
2147
+ padded?: boolean; // Body-Padding (px-6 py-6).
2148
+ radius?: "lg" | "xl"; // Eckenradius.
2149
+ footerBordered?: boolean; // Separates the footer with `border-t` and `bg-muted/30`.
2150
+ footerInset?: boolean; // Footer horizontal padding.
2151
+ fillHeight?: boolean; // Joins the ancestor fillHeight flex chain — a tabs-mode relatedList table sizing to the panel instead of the page — instead of normal document-flow height.
2152
+ framed?: boolean; // false drops border, background, shadow and radius but keeps padding and header slots — a panel inside a host surface such as a tab panel.
2153
+ screenBody?: boolean; // The card is the whole screen body (secretMint reveal and done phase): no frame, the shared screen insets and width of form screens, children stacked with a gap.
2154
+ };
2155
+ type CardSlots = { // Card slots are named content regions, all optional.
2156
+ header?: ReactNode;
2157
+ title?: ReactNode;
2158
+ subtitle?: ReactNode;
2159
+ headerContent?: ReactNode; // Free-form block content in its own full-width row below the title/subtitle/`headerActions` row, within the same header block — for a host-supplied header (projectionDetail `slots.header`).
2160
+ headerActions?: ReactNode; // Right-aligned header slot, for example a range switcher, filter, or badge.
2161
+ footer?: ReactNode; // Footer content, separated by a top border; aligned to the right by default.
2162
+ };
2163
+ type ConfigCascadeViewProps = { // Cascade view for a config-edit field: one origin line ("Set for this tenant", "Uses the default", …), a reset action for an own value, and a disclosure listing every level.
2164
+ cascade: ConfigCascade;
2165
+ screenScope: ConfigScope;
2166
+ onReset?: (key: string, scope: ConfigScope) => void;
2167
+ qualifiedKey?: string;
2168
+ required?: boolean; // The key declares `required: true` — a missing value reads "Not set.
2169
+ renderValue?: (value: string | number | boolean) => ReactNode; // Display form of a level value (e.g. a select's label instead of its raw value).
2170
+ };
2171
+ type ConfigSourceBadgeProps = { // Source-badge for one cascade step (User / Tenant / System / …).
2172
+ source: ConfigValueSource;
2173
+ screenScope?: ConfigScope; // Scope of the hosting screen.
2174
+ };
2175
+ type CopyButtonProps = { // Copies `text` to the clipboard; the label flips to `copiedLabel` once the platform confirmed the write.
2176
+ text: string;
2177
+ label: string;
2178
+ copiedLabel: string;
2179
+ variant?: "primary" | "secondary";
2180
+ className?: string; // Layout extras, merged via cn().
2181
+ testId?: string;
2182
+ };
2183
+ type DataAttributes = Readonly<Record<`data-${string}`, string>>; // Escape hatch for `data-*` test hooks (e.g. an E2E selector) that don't warrant a dedicated typed prop.
2184
+ type DataTableRowAction = {
2185
+ id: string; // Stable ID for aria-labels and data-testids.
2186
+ label: string; // Translated Label.
2187
+ style?: "primary" | "secondary" | "danger"; // Visual style — "danger" renders the red variant in the default primitive.
2188
+ confirm?: string; // Translated confirmation prompt (dialog description).
2189
+ confirmLabel?: string; // Translated confirm-button label in the dialog.
2190
+ confirmRequired?: boolean; // Overrides the default "danger implies a confirm dialog" rule.
2191
+ onTrigger: (row: ListRowViewModel) => Promise<void> | void; // Called with the clicked row's `ListRowViewModel`.
2192
+ isVisible?: (row: ListRowViewModel) => boolean; // Controls per-row visibility (e.g. show "Start" only when `status==="scheduled"`).
2193
+ rowClick?: boolean; // This action is what a click on the row does.
2194
+ icon?: IconKey; // Resolved icon (author `RowAction.icon` or the id-derived default) — drives both the icon-left-of-text render and the icon-only collapse rule (see `shouldRenderActionsIconOnly`).
2195
+ display?: RowActionDisplay; // Author-chosen inline form (`RowAction.display`).
2196
+ };
2197
+ type DataTableRowActionMode = "adaptive" | "inline";
2198
+ type DataTableSort = {
2199
+ field: string;
2200
+ dir: DataTableSortDir;
2201
+ };
2202
+ type DataTableSortDir = "asc" | "desc";
2203
+ type DataTableTableBlockProps = Pick<DataTableProps, keyof DataTableTableBlock>;
2204
+ type DialogProps = { // Modal dialog for confirmations or compact sub-forms.
2205
+ open: boolean;
2206
+ onOpenChange: (open: boolean) => void;
2207
+ title: string;
2208
+ description?: string; // Optional description, typically a sentence explaining the consequence ("This action cannot be undone.").
2209
+ confirmLabel?: string; // Confirm-button label.
2210
+ cancelLabel?: string; // Cancel-Button-Label.
2211
+ variant?: "default" | "danger"; // `default` = Confirm primary, `danger` = Confirm danger.
2212
+ initialFocus?: "confirm" | "cancel"; // Which button takes focus on open.
2213
+ onConfirm: () => void | Promise<void>; // Called when the user confirms.
2214
+ children?: ReactNode; // Optional additional content between the description and buttons, such as an input when the dialog also collects data.
2215
+ testId?: string;
2216
+ };
2217
+ type DrawerProps = { // Side panel for hosting an existing self-contained form/widget (own submit/cancel buttons) without leaving the underlying screen — e.g. a toolbar action's `kind: "drawer"` mounts an actionForm here instead of navigating to a full page.
2218
+ open: boolean;
2219
+ onOpenChange: (open: boolean) => void;
2220
+ title?: string;
2221
+ subtitle?: string; // Muted line under the title.
2222
+ children: ReactNode;
2223
+ testId?: string;
2224
+ };
2225
+ type EmbeddedListCellType = "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp"; // Cell type for one column of an embedded-list field.
2226
+ type EmbeddedListColumn = { // One column of an embedded-list table (one entry per key of the source EmbeddedFieldDef's `schema`).
2227
+ field: string;
2228
+ label: string;
2229
+ type: EmbeddedListCellType;
2230
+ required: boolean;
2231
+ derived: boolean; // Read-only, value supplied by the caller (already computed from sibling cells) — not directly user-editable.
2232
+ readOnly?: boolean; // Shown but not editable by the user; unlike `derived` it is not computed.
2233
+ options?: string[]; // Only for `type: "select"`.
2234
+ optionLabels?: Readonly<Record<string, string>>;
2235
+ display?: "radio" | "dropdown"; // Only for `type: "select"`; `"radio"` renders segmented buttons, the default is a dropdown.
2236
+ referenceOptions?: { value: string; label: string; }[]; // Only for `type: "reference"` — pre-fetched by the caller, ONE query per column shared across every row (not one query per cell).
2237
+ referenceLoading?: boolean;
2238
+ };
2239
+ type EmbeddedListInputProps = { // Row-array + totals table for a `createEmbeddedListField` (invoice- positions-style) field — structurally too different from `Input`'s value/onChange union to fit there, so it's its own primitive, same as `DataTable`.
2240
+ id: string;
2241
+ columns: EmbeddedListColumn[];
2242
+ rows: Readonly<Record<string, unknown>>[];
2243
+ totals?: EmbeddedListTotal[];
2244
+ currency?: string; // Currency for money cells and the totals row — one value for the whole list (currency lives on the head aggregate, not the row).
2245
+ disabled?: boolean; // Whole-list read-only (e.g. a released invoice) — hides row-mutation affordances and disables every cell.
2246
+ fixedRows?: boolean; // Rows are fixed: no add, remove, duplicate or move; cells stay editable.
2247
+ minItems?: number;
2248
+ maxItems?: number;
2249
+ listIssues?: FieldIssue[]; // Shown under the whole list (e.g. under the totals row).
2250
+ rowIssues?: Readonly<Record<number, FieldIssue[]>>; // Keyed by row index.
2251
+ cellIssues?: Readonly<Record<string, FieldIssue[]>>; // Keyed `${rowIndex}.${field}`.
2252
+ onCellChange: (rowIndex: number, field: string, value: unknown) => void;
2253
+ onAddRow: () => void;
2254
+ onRemoveRow: (rowIndex: number) => void;
2255
+ onDuplicateRow: (rowIndex: number) => void;
2256
+ onMoveRow: (fromIndex: number, toIndex: number) => void;
2257
+ onPasteCells?: (rowIndex: number, columnIndex: number, grid: (string[])[]) => void; // Tab/newline-delimited clipboard paste starting at (rowIndex, columnIndex) — parsed to a 2D string grid by the Web impl (paste events are a browser-only concept); undefined caller = no paste handling wired up.
2258
+ addLabel: string;
2259
+ actionsLabel: string; // Accessible name of the (otherwise headerless) row-actions column.
2260
+ removeLabel: string;
2261
+ duplicateLabel: string;
2262
+ moveUpLabel: string;
2263
+ moveDownLabel: string;
2264
+ emptyLabel: string;
2265
+ emptyCtaLabel: string;
2266
+ testId?: string;
2267
+ };
2268
+ type EmbeddedListTotal = { // One entry of an embedded-list totals row (e.g. an invoice's total amount, summed across all rows by the caller).
2269
+ field: string;
2270
+ label: string;
2271
+ value: number;
2272
+ money?: boolean; // Set for labelled multi-column totals, which have no single column to read the money type from.
2273
+ };
2274
+ type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "toggle"; // Width class of a field cell inside a `flow` grid: sized by what the field holds.
2275
+ type FileValueListProps = { // Read-only list of stored files (FileRef ids): name, download link and, for PDFs, an inline preview.
2276
+ fileIds: string[];
2277
+ previewOpenByDefault?: boolean; // Starts the inline PDF preview of the newest file open; uploads append, so that is the last id.
2278
+ testId?: string;
2279
+ };
2280
+ type FillContainerProps = { // Chromeless flex-fill layout host — no title, no card frame, no padding, just a container that sizes to its content and, once the ancestor chain is height-constrained, shrinks so one scrolling child can scroll internally instead of the page growing.
2281
+ children: ReactNode;
2282
+ testId?: string;
2283
+ grow?: boolean; // Claims the remaining height of the flex chain (`flex-1`) instead of sizing to its content, so a footer inside the child sits at the bottom edge even with few rows.
2284
+ };
2285
+ type FormSectionNavItem = { // One entry of a form's "on this page" navigation: `id` matches the `SectionProps.id` of the section it scrolls to.
2286
+ id: string;
2287
+ title: string;
2288
+ };
2289
+ type InputFillAction = { // Button beside a single-line text/number input that fills it with a suggestion.
2290
+ label: string;
2291
+ onPress: () => void;
2292
+ busy?: boolean; // A suggestion is being fetched: the button shows a spinner and is inert.
2293
+ disabled?: boolean; // The surrounding form is locked as a whole.
2294
+ testId?: string;
2295
+ };
2296
+ type JsonViewProps = { // Structured JSON display (audit payload/metadata, job logs, unsupported jsonb/embedded field fallback).
2297
+ value: unknown;
2298
+ indent?: number;
2299
+ testId?: string;
2300
+ };
2301
+ type LightboxBaseProps = {
2302
+ open: boolean;
2303
+ onOpenChange: (open: boolean) => void;
2304
+ testId?: string;
2305
+ actions?: ReactNode; // Controls for the shown image, e.g. a download button.
2306
+ showPosition?: boolean; // `false` hides the `{current} / {total}` counter of a set, for images that carry their own.
2307
+ };
2308
+ type LightboxImage = {
2309
+ src: string;
2310
+ alt: string;
2311
+ };
2312
+ type LightboxProps = LightboxBaseProps & ({ src: string; alt: string; } | { images: LightboxImage[]; index: number; onIndexChange: (index: number) => void; }); // Image lightbox — full-size preview on click.
2313
+ type LinkProps = { // Navigation link.
2314
+ href: string;
2315
+ variant?: "default" | "button" | "muted";
2316
+ target?: "_blank" | "_self" | "_parent" | "_top"; // `_blank` makes the web impl set `rel="noopener noreferrer"` unless `rel` is set explicitly.
2317
+ rel?: string;
2318
+ onPress?: () => void; // Plain left-click handler (SPA navigation); `href` stays set for modifier/middle clicks, which the browser handles itself.
2319
+ className?: string; // Layout extras (self-center, text-xs), merged via cn().
2320
+ children: ReactNode;
2321
+ testId?: string;
2322
+ dataAttributes?: DataAttributes;
2323
+ };
2324
+ type ModalProps = { // Bare content shell for hosting an existing self-contained form/widget (own submit/cancel buttons) inside a modal overlay — unlike `Dialog`, it renders no footer buttons of its own.
2325
+ open: boolean;
2326
+ onOpenChange: (open: boolean) => void;
2327
+ title: string; // Screen-reader-only accessible title (required by the underlying dialog primitive) — not shown visually since the hosted content is expected to render its own heading.
2328
+ children: ReactNode;
2329
+ testId?: string;
2330
+ };
2331
+ type PageHeaderProps = { // Page title area rendered by the app shell's header instead of inside the screen: `title` replaces the last breadcrumb, `status` sits right after it, `actions` at the right edge.
2332
+ title?: string;
2333
+ recordTitle?: string; // Name of the loaded record; shown as an extra breadcrumb before the last one.
2334
+ status?: ReactNode;
2335
+ actions?: ReactNode;
2336
+ overflowItems?: ActionMenuItemSpec[]; // Land in the shell header's overflow menu at phone width.
2337
+ hideBreadcrumb?: boolean; // Hide the shell header's breadcrumb while mounted, for a screen that stands in for the one the route names (a locked screen's fallback).
2338
+ breadcrumbScreenId?: string; // Label the breadcrumb with this screen (id as in the route) instead of the one the route names, for a locked screen's fallback.
2339
+ };
2340
+ type PickedFile = { // Platform-neutral view of a picked file; the DOM `File` satisfies it.
2341
+ name: string;
2342
+ size: number;
2343
+ type: string;
2344
+ webkitRelativePath: string;
2345
+ arrayBuffer: () => Promise<ArrayBuffer>;
2346
+ };
2347
+ type ProgressProps = {
2348
+ value: number;
2349
+ tone?: ProgressTone;
2350
+ ariaLabel?: string;
2351
+ testId?: string;
2352
+ };
2353
+ type ProgressTone = "default" | "success" | "warn" | "danger"; // Determinate progress bar (e.g. wizard step progress).
2354
+ type PromoPanelAction =
2355
+ | {
2356
+ label: string;
2357
+ href: string;
2358
+ testId?: string;
2359
+ }
2360
+ | {
2361
+ label: string;
2362
+ onClick: () => void;
2363
+ testId?: string;
2364
+ }
2365
+ ;
2366
+ type PromoPanelProps = { // Offer surface for upsells (e.g. "create an account"): its own tinted panel with eyebrow, headline, benefit content and one call to action, so an offer never reads like an info or error banner.
2367
+ title: string;
2368
+ eyebrow?: string;
2369
+ children?: ReactNode;
2370
+ action?: PromoPanelAction;
2371
+ testId?: string;
2372
+ };
2373
+ type SecretRevealProps = { // One-time secret reveal — mint-form confirm phase.
2374
+ values: SecretRevealValue[];
2375
+ copyLabel: string;
2376
+ copiedLabel: string;
2377
+ testId?: string;
2378
+ };
2379
+ type SecretRevealValue = { // One revealed value on a `secretMint` screen's confirm phase.
2380
+ label: string;
2381
+ value: string;
2382
+ copyable: boolean;
2383
+ multiline: boolean;
2384
+ qr?: boolean; // Render `value` as a scannable QR code instead of monospaced text — for an otpauth://-style enrollment URI.
2385
+ };
2386
+ type ShareButtonProps = ({ target: "whatsapp"; phone?: string; } | { target: "system"; title?: string; url?: string; }) & { // Hands `text` to another app.
2387
+ text: string;
2388
+ label: string;
2389
+ variant?: "primary" | "secondary";
2390
+ className?: string; // Layout extras, merged via cn().
2391
+ testId?: string;
2392
+ };
2393
+ type StatusBadgeOptionTone = StatusTone | "accent"; // Tones a select option can request: the status tones plus the brand `accent`.
2394
+ type StatusBadgeProps = { // Status pill.
2395
+ value: string;
2396
+ tone?: StatusBadgeOptionTone;
2397
+ testId?: string;
2398
+ };
2399
+ type StatusTone = "ok" | "warn" | "bad" | "critical" | "muted";
2400
+ type StepBarProps = { // Wizard step overview — numbered chips, one per step label, with the active step highlighted.
2401
+ steps: string[];
2402
+ currentIndex: number;
2403
+ compactLabel: string;
2404
+ onStepSelect?: (index: number) => void;
2405
+ doneSteps?: boolean[]; // Explicit done state per step index.
2406
+ selectableSteps?: "done" | "all"; // "done" (default): only done steps are jump targets.
2407
+ narrowLayout?: "label" | "steps"; // "label" (default) swaps the step row for `compactLabel` on narrow viewports; "steps" keeps the row there too — for short wizards whose labels fit a phone and whose done steps must stay tappable.
2408
+ orientation?: "horizontal" | "vertical"; // "vertical" renders a step rail (from `lg`, with `compactLabel` as the single line below it) instead of the chip row.
2409
+ heading?: string; // Vertical only: rail heading ("Step 4 of 6") and optional description.
2410
+ description?: string;
2411
+ subtitles?: (string | undefined)[]; // Vertical only: line under each step label, same order as `steps`.
2412
+ upNext?: {
2413
+ heading: string;
2414
+ title: string;
2415
+ subtitle?: string;
2416
+ }; // Vertical only: box under the rail announcing the next step.
2417
+ testId?: string;
2418
+ compactTestId?: string;
2419
+ };
2420
+ type StickyActionBarProps = { // Thumb-reach action row for custom screens: pinned to the bottom edge on narrow viewports (above the device's safe-area inset), inline otherwise.
2421
+ children: ReactNode;
2422
+ back?: {
2423
+ onBack: () => void;
2424
+ label: string;
2425
+ };
2426
+ testId?: string;
2427
+ };
2428
+ type TabsProps = { // Tab strip for a projectionDetail `layout.mode: "tabs"`.
2429
+ items: { id: string; label: string; count?: number; }[];
2430
+ activeId: string;
2431
+ onSelect: (id: string) => void;
2432
+ testId?: string;
2433
+ };
2434
+ type TrailingInputAction = {
2435
+ icon: "clear" | "undo";
2436
+ label: string;
2437
+ onPress: () => void;
2438
+ testId?: string;
2439
+ };
2440
+ type WizardStepGroupProps = { // Wraps a single wizard step so it stays mounted but inert while off- screen — `hidden` visually hides it, and implementations that have a native constraint-validation concept (web: `<fieldset disabled>`, spec- guaranteed to also bar descendants from participating) must bar it too, or a `type="submit"` step-advance button triggers the browser's full-form check against fields on unvisited, unfocusable steps.
2441
+ hidden: boolean;
2442
+ inset?: boolean; // Pads the step content (tab panels sit full-bleed under the tab strip).
2443
+ children: ReactNode;
2444
+ };
2445
+ ```
2446
+
2447
+ ## packages/renderer/src/web/app/client-plugin.tsx
2448
+
2449
+ ```ts
2450
+ type CollectionNavProviders = { // What a `navProvidersFromCollections` factory returns: the providers plus the entities whose live events should re-fire them.
2451
+ providers: Readonly<Record<string, TreeChildrenSubscribe>>;
2452
+ entities?: Readonly<Record<string, string[]>>;
2453
+ };
2454
+ ```
2455
+
2456
+ ## packages/renderer/src/web/app/create-app.tsx
2457
+
2458
+ ```ts
2459
+ type BoundaryErrorContext = {
2460
+ errorId: string;
2461
+ screen?: string; // Qualified name of the screen whose boundary caught the error; absent for the root boundary.
2462
+ };
2463
+ type BoundaryErrorHandler = (error: Error, info: ErrorInfo, context: BoundaryErrorContext) => void;
2464
+ ```
2465
+
2466
+ ## packages/renderer/src/web/app/resolvers-context.tsx
2467
+
2468
+ ```ts
2469
+ type ResolverComponent = ComponentType<{ target: TargetRef; onClose: () => void; }>;
2470
+ ```
2471
+
2472
+ ## packages/renderer/src/web/layout/default-app-shell.tsx
2473
+
2474
+ ```ts
2475
+ type DefaultAppShellUser = {
2476
+ id: string;
2477
+ roles: string[];
2478
+ };
2479
+ ```
2480
+
2481
+ ## packages/renderer/src/web/layout/sidebar-panel.tsx
2482
+
2483
+ ```ts
2484
+ type SidebarPanelProps = {
2485
+ children: ReactNode;
2486
+ defaultWidth?: number; // Initial width in px.
2487
+ minWidth?: number;
2488
+ maxWidth?: number;
2489
+ storageKey?: string; // localStorage key for the dragged width.
2490
+ className?: string;
2491
+ tone?: "nav" | "surface"; // "nav" (default) matches the shell's own sidebar chrome.
2492
+ };
2493
+ ```
2494
+
2495
+ ## packages/renderer/src/web/lib/file-upload-form-data.ts
2496
+
2497
+ ```ts
2498
+ type FileUploadFormDataInput = {
2499
+ file: Blob;
2500
+ entityType?: string | undefined;
2501
+ entityId?: string | undefined;
2502
+ fieldName?: string | undefined;
2503
+ actionForm?: string | undefined; // Screen QN of an actionForm: the upload is pending for that form's field, not for an entity.
2504
+ };
2505
+ ```
2506
+
2507
+ ## packages/renderer/src/web/primitives/form.tsx
2508
+
2509
+ ```ts
2510
+ type FormScreenShellWidth = FormWidth;
2511
+ ```
2512
+
2513
+ ## packages/renderer/src/web/primitives/layout.tsx
2514
+
2515
+ ```ts
2516
+ type PageSectionProps = {
2517
+ className?: string;
2518
+ children?: ReactNode;
2519
+ testId?: string;
2520
+ maxWidth?: ScreenWidth;
2521
+ };
2522
+ type ScreenWidth = FormWidth;
2523
+ type StackGap = keyof typeof STACK_GAP;
2524
+ type StackProps = {
2525
+ gap?: StackGap;
2526
+ className?: string;
2527
+ children?: ReactNode;
2528
+ testId?: string;
2529
+ };
2530
+ ```
2531
+
2532
+ ## packages/renderer/src/web/primitives/toast.tsx
2533
+
2534
+ ```ts
2535
+ type ToastApi = {
2536
+ toast: (opts: ToastOptions) => void;
2537
+ };
2538
+ ```
2539
+
2540
+ ## packages/renderer/src/web/sse/live-events.ts
2541
+
2542
+ ```ts
2543
+ type LiveEventTimers = {
2544
+ set: (callback: () => void, delayMs: number) => () => void; // Schedules the callback and returns a function that cancels it.
2545
+ };
2546
+ ```
2547
+
2548
+ ## packages/renderer/src/web/widgets/ai-text-field.tsx
2549
+
2550
+ ```ts
2551
+ type AiTextAction = "correct" | "translate" | "rewrite";
2552
+ interface AiTextFieldBaseProps {
2553
+ label: string;
2554
+ id: string;
2555
+ name: string;
2556
+ value: string;
2557
+ onChange: (v: string) => void;
2558
+ required?: boolean;
2559
+ disabled?: boolean;
2560
+ placeholder?: string;
2561
+ testId?: string;
2562
+ hideLabel?: boolean;
2563
+ completion?: boolean; // Ghost-text completion on/off.
2564
+ completionDebounceMs?: number; // Debounce for ghost-text requests, ms.
2565
+ actions?: AiTextAction[]; // Toolbar actions to expose.
2566
+ translateLanguages?: string[]; // Target languages the translate action offers.
2567
+ }
2568
+ ```
2569
+
2570
+ ## packages/renderer/src/web/widgets/chart-data.ts
2571
+
2572
+ ```ts
2573
+ type ChartMarker = {
2574
+ atMs: number;
2575
+ label: string;
2576
+ color?: string; // Colors the pin and, on StackedAreaChart, draws a dashed guide line in the plot.
2577
+ legendLabel?: string; // Translated by the caller.
2578
+ };
2579
+ ```
2580
+
2581
+ ## packages/renderer/src/web/widgets/form-fields.tsx
2582
+
2583
+ ```ts
2584
+ interface FieldBase {
2585
+ label: string;
2586
+ id: string;
2587
+ name: string;
2588
+ required?: boolean;
2589
+ disabled?: boolean;
2590
+ hideLabel?: boolean;
2591
+ testId?: string;
2592
+ }
2593
+ ```
2594
+
2595
+ ## packages/renderer/src/web/widgets/public-shell.tsx
2596
+
2597
+ ```ts
2598
+ type PublicShellCommonProps = {
2599
+ brand: ReactNode;
2600
+ headerActions?: ReactNode;
2601
+ footer?: ReactNode;
2602
+ children: ReactNode;
2603
+ testId?: string;
2604
+ };
2605
+ ```
2606
+
2607
+ ## packages/renderer/src/web/widgets/result-panel.tsx
2608
+
2609
+ ```ts
2610
+ interface ResultTableFooterRow {
2611
+ label: string;
2612
+ value: ReactNode;
2613
+ emphasize?: boolean;
2614
+ }
2615
+ ```
2616
+
2617
+ ## packages/testing/src/bundled-injection.ts
2618
+
2619
+ ```ts
2620
+ type InboxMessage = {
2621
+ to: string;
2622
+ subject: string;
2623
+ html: string;
2624
+ text?: string;
2625
+ from?: string;
2626
+ fromName?: string;
2627
+ replyTo?: string;
2628
+ headers?: Readonly<Record<string, string>>;
2629
+ };
2630
+ ```
2631
+
2632
+ ## packages/testing/src/e2e/seed-route.ts
2633
+
2634
+ ```ts
2635
+ type E2eExtraSeeder = (ctx: E2eSeederContext, tenantId: TenantId, body: unknown) => Promise<unknown>;
2636
+ type E2eSeedRoutesOptions = {
2637
+ extraRoles?: string[];
2638
+ extraSeeders?: Readonly<Record<string, E2eExtraSeeder>>;
2639
+ mailOutbox?: {
2640
+ sent: InboxMessage[];
2641
+ };
2642
+ };
2643
+ type E2eSeederContext = {
2644
+ write: (handlerQn: string, payload: unknown) => Promise<unknown>;
2645
+ query: (handlerQn: string, payload: unknown) => Promise<unknown>;
2646
+ writeAs: (roles: string[], handlerQn: string, payload: unknown) => Promise<unknown>;
2647
+ };
2648
+ ```
2649
+
2650
+ ## packages/testing/src/helpers/access-assertions.ts
2651
+
2652
+ ```ts
2653
+ type CheckedHandlerCounts = {
2654
+ checkedWriteHandlers: number;
2655
+ checkedQueryHandlers: number;
2656
+ };
2657
+ ```
2658
+
2659
+ ## packages/testing/src/helpers/closed-connection-error.ts
2660
+
2661
+ ```ts
2662
+ type AdminClient = {
2663
+ unsafe(sql: string, params?: unknown[]): Promise<unknown>;
2664
+ };
2665
+ ```
2666
+
2667
+ ## packages/testing/src/helpers/event-store-writes.ts
2668
+
2669
+ ```ts
2670
+ type ExecutorArgs = Parameters<typeof createEventStoreExecutor>;
2671
+ ```
2672
+
2673
+ ## packages/testing/src/helpers/manual-clock.ts
2674
+
2675
+ ```ts
2676
+ type ClockStep = {
2677
+ hours?: number;
2678
+ minutes?: number;
2679
+ seconds?: number;
2680
+ milliseconds?: number;
2681
+ };
2682
+ ```
2683
+
2684
+ ## packages/testing/src/helpers/seed.ts
2685
+
2686
+ ```ts
2687
+ type SeedTable = EntityTable | EntityTableMeta;
2688
+ ```
2689
+
2690
+ ## packages/testing/src/helpers/sse-frames.ts
2691
+
2692
+ ```ts
2693
+ type ReadSseFramesOptions = {
2694
+ until: (frame: SseFrame, all: SseFrame[]) => boolean;
2695
+ skipPings?: boolean;
2696
+ timeoutMs?: number;
2697
+ };
2698
+ ```
2699
+
2700
+ ## packages/testing/src/seed-tenant.ts
2701
+
2702
+ ```ts
2703
+ type InProcessSeededTenant = SeededTenant & {
2704
+ unsubscribeSse: () => void;
2705
+ };
2706
+ type PersistedTenant = {
2707
+ id: TenantId;
2708
+ key: string;
2709
+ name: string;
2710
+ admin: SeededCredentials;
2711
+ members: SeededCredentials[];
2712
+ };
2713
+ type SavedRow = {
2714
+ id: string;
2715
+ data: {
2716
+ version: number;
2717
+ };
2718
+ };
2719
+ type SeedWriter = (handlerQn: string, payload: unknown, tenantId: TenantId) => Promise<SavedRow>;
2720
+ ```
2721
+
2722
+ ## packages/testing/src/seed-types.ts
2723
+
2724
+ ```ts
2725
+ type BoundApi = {
2726
+ writeOk: <T = Record<string, unknown>>(type: string, payload: unknown, requestId?: string) => Promise<T>;
2727
+ writeErr: (type: string, payload: unknown) => Promise<WriteErrorInfo>;
2728
+ queryOk: <T = unknown>(type: string, payload: unknown) => Promise<T>;
2729
+ queryErr: (type: string, payload: unknown) => Promise<WriteErrorInfo>;
2730
+ };
2731
+ type SeedAdminIdentity = {
2732
+ displayName?: string;
2733
+ email?: string;
2734
+ };
2735
+ type SeedPart = (ctx: { tenant: SeededTenant; }) => Promise<void>;
2736
+ type SeedTenantOptions = {
2737
+ name?: string;
2738
+ users?: number;
2739
+ with?: SeedPart[];
2740
+ persist?: boolean;
2741
+ admin?: SeedAdminIdentity;
2742
+ };
2743
+ type SeededCredentials = {
2744
+ id: string;
2745
+ email: string;
2746
+ password: string;
2747
+ };
2748
+ type SeededTenant = {
2749
+ id: TenantId;
2750
+ key: string;
2751
+ name: string;
2752
+ admin: SeededUser;
2753
+ members: SeededUser[];
2754
+ addUser: (roles?: string[], identity?: SeedAdminIdentity) => Promise<SeededUser>;
2755
+ api: BoundApi;
2756
+ apiAs: (user: SeededUser) => BoundApi;
2757
+ };
2758
+ type SeededUser = SeededCredentials & {
2759
+ session: SessionUser;
2760
+ mfaTotpSecret?: string;
2761
+ };
2762
+ ```
2763
+
2764
+ ## packages/testing/src/stack/push-entity-projection-tables.ts
2765
+
2766
+ ```ts
2767
+ type DbHolder = {
2768
+ db: DbConnection;
2769
+ unscopedDb?: DbConnection;
2770
+ };
2771
+ ```
2772
+
2773
+ ## packages/testing/src/stack/redis.ts
2774
+
2775
+ ```ts
2776
+ type CreateTestRedisOptions = {
2777
+ borrowKeyPrefix?: string; // Reuse another stack's keyPrefix on a fresh connection.
2778
+ };
2779
+ ```
2780
+
2781
+ ## packages/testing/src/stack/request-helper.ts
2782
+
2783
+ ```ts
2784
+ type BatchCommand = {
2785
+ type: string;
2786
+ payload: unknown;
2787
+ };
2788
+ type RequestHelperOptions = {
2789
+ sessionCreator?: SessionCreator;
2790
+ };
2791
+ ```
2792
+
2793
+ ## packages/types/src/aggregate-types.ts
2794
+
2795
+ ```ts
2796
+ type AggregateDimension =
2797
+ | {
2798
+ field: string;
2799
+ }
2800
+ | {
2801
+ field: string;
2802
+ bucket: AggregateTimeBucket;
2803
+ timeZone: string;
2804
+ }
2805
+ ;
2806
+ type AggregateKey = string | number | boolean | null;
2807
+ type AggregateMeasure =
2808
+ | {
2809
+ fn: "count";
2810
+ }
2811
+ | {
2812
+ fn: "countDistinct" | "sum" | "avg";
2813
+ field: string;
2814
+ }
2815
+ ;
2816
+ type AggregateRow = {
2817
+ keys: AggregateKey[];
2818
+ value: number | null;
2819
+ };
2820
+ type AggregateSpec = {
2821
+ measure: AggregateMeasure;
2822
+ groupBy?: AggregateDimension[];
2823
+ orderByValue?: "asc" | "desc";
2824
+ orderByKeys?: "asc" | "desc"; // Direction of the group-key ordering (default asc).
2825
+ limit?: number;
2826
+ };
2827
+ type AggregateTimeBucket = "hour" | "day";
2828
+ ```
2829
+
2830
+ ## packages/types/src/cache-sync-types.ts
2831
+
2832
+ ```ts
2833
+ type CacheSyncBus = {
2834
+ publish(topic: string, message: unknown, options?: CacheSyncPublishOptions): void;
2835
+ subscribe(topic: string, listener: (message: unknown) => void): () => void;
2836
+ onResync(listener: () => void): () => void;
2837
+ };
2838
+ type CacheSyncPublishOptions = {
2839
+ remoteOnly?: boolean;
2840
+ };
2841
+ type ClosableCacheSyncBus = CacheSyncBus & {
2842
+ close(): Promise<void>;
2843
+ };
2844
+ ```
2845
+
2846
+ ## packages/types/src/concurrency-mode.ts
2847
+
2848
+ ```ts
2849
+ type ConcurrencyMode = "parallel" | "skip" | "replace" | "sequential" | "debounce";
2850
+ ```
2851
+
2852
+ ## packages/types/src/config.ts
2853
+
2854
+ ```ts
2855
+ type ConfigBacking = "config" | "secrets";
2856
+ type ConfigBounds = {
2857
+ min?: number;
2858
+ max?: number;
2859
+ };
2860
+ type ConfigComputedContext = {
2861
+ tenantId: TenantId;
2862
+ userId: string;
2863
+ db: DbConnection | TenantDb;
2864
+ };
2865
+ type ConfigComputedFn<T extends ConfigKeyType = ConfigKeyType> = (ctx: ConfigComputedContext) => Promise<ConfigValue<T>>;
2866
+ type ConfigKeyAccess = {
2867
+ read: string[];
2868
+ write: string[];
2869
+ };
2870
+ type ConfigMask = {
2871
+ title: string;
2872
+ description?: string; // i18n key for help text shown under the field label.
2873
+ icon?: NavIconKey;
2874
+ order?: number;
2875
+ };
2876
+ type ConfigSeedDef = {
2877
+ key: string;
2878
+ value: string | number | boolean;
2879
+ scope?: ConfigScope;
2880
+ tenantId?: string;
2881
+ userId?: string;
2882
+ };
2883
+ type ConfigStoredRow = {
2884
+ id: string;
2885
+ key: string;
2886
+ value: string | null;
2887
+ tenantId: string;
2888
+ userId: string | null;
2889
+ };
2890
+ type ConfigValidateContext = {
2891
+ key: string;
2892
+ scope: ConfigScope;
2893
+ tenantId: TenantId;
2894
+ userId: string | null;
2895
+ db: TenantDb;
2896
+ readCascade: (qualifiedKey: string) => Promise<ConfigCascade>;
2897
+ };
2898
+ type CreateSeedOptions = {
2899
+ value: string | number | boolean;
2900
+ };
2901
+ type CreateTenantSeedOptions = {
2902
+ tenantId?: string;
2903
+ };
2904
+ type ExtensionSelectorDef = {
2905
+ extensionName: string;
2906
+ qualifiedKey: string;
2907
+ panels?: ExtensionSelectorPanel[];
2908
+ };
2909
+ type ExtensionSelectorPanel = DashboardCustomPanel | Omit<DashboardScreenPanel, "label">;
2910
+ type JobBackoff =
2911
+ | JobBackoffStrategy
2912
+ | {
2913
+ type: JobBackoffStrategy;
2914
+ delayMs?: number | undefined;
2915
+ }
2916
+ ;
2917
+ type JobBackoffStrategy = "fixed" | "exponential";
2918
+ type JobRunIn = Exclude<RunIn, "both">;
2919
+ type MaxPerTenantOptions = {
2920
+ limit: number;
2921
+ onLimit: "skip" | "defer";
2922
+ deferMs?: number | undefined;
2923
+ };
2924
+ type NotificationDataFn = (result: SaveContext) => Record<string, unknown>;
2925
+ type NotificationRecipientFn = (result: SaveContext) => string | string[] | { tenant: string; } | null;
2926
+ type NotificationTemplateFn = (data: Record<string, unknown>) => Record<string, unknown>;
2927
+ type PluralForms = {
2928
+ zero?: string;
2929
+ one?: string;
2930
+ two?: string;
2931
+ few?: string;
2932
+ many?: string;
2933
+ other: string;
2934
+ };
2935
+ type RegistrarExtensionDef = {
2936
+ onRegister?: (entityName: string, options?: Record<string, unknown>) => void;
2937
+ extendSchema?: (entityName: string) => Record<string, EntityFieldDefinition>;
2938
+ hooks?: RegistrarExtensionHooks;
2939
+ extendSearch?: (entityName: string) => Record<string, unknown>;
2940
+ uiExtension?: UiExtensionDef;
2941
+ };
2942
+ type RegistrarExtensionHooks = {
2943
+ preSave?: PreSaveHookFn;
2944
+ postSave?: PostSaveHookFn;
2945
+ preDelete?: PreDeleteHookFn;
2946
+ postDelete?: PostDeleteHookFn;
2947
+ };
2948
+ type RegistrarExtensionRegistration = {
2949
+ extensionName: string;
2950
+ entityName: string;
2951
+ options?: Record<string, unknown> | undefined;
2952
+ featureName?: string;
2953
+ };
2954
+ type RunIn = "api" | "worker" | "both";
2955
+ type TemplateImagesVariableDef = TemplateVariableCommon & {
2956
+ type: "images";
2957
+ };
2958
+ type TemplateVariableCommon = {
2959
+ label?: string;
2960
+ required?: boolean;
2961
+ };
2962
+ type TranslationsDef = {
2963
+ keys: TranslationKeys;
2964
+ };
2965
+ type UiExtensionDef = {
2966
+ editSection?: string;
2967
+ listColumns?: string;
2968
+ filters?: string;
2969
+ };
2970
+ ```
2971
+
2972
+ ## packages/types/src/cursor-types.ts
2973
+
2974
+ ```ts
2975
+ type CursorResult<T> = {
2976
+ rows: T[];
2977
+ nextCursor: string | null;
2978
+ total?: number; // Optional total row count — only present when the caller sets `totalCount: true` on the query.
2979
+ searchTruncated?: true; // Set when the search backend returned as many candidates as it can in one response, so matches beyond that may be missing from `rows`.
2980
+ };
2981
+ ```
2982
+
2983
+ ## packages/types/src/db-connection.ts
2984
+
2985
+ ```ts
2986
+ type PgClient = SQL;
2987
+ type PgListenClient = {
2988
+ listen(channel: string, onnotify: (payload: string) => void, onlisten?: () => void): Promise<PgListenSubscription>;
2989
+ };
2990
+ type PgListenSubscription = {
2991
+ unlisten(): Promise<void>;
2992
+ };
2993
+ type RawDbClient = {
2994
+ unsafe: (sql: string, params?: unknown[]) => PromiseLike<unknown[]>;
2995
+ begin: <T>(fn: (tx: DbTx) => Promise<T>) => Promise<T>;
2996
+ end?: (options?: { timeout?: number; }) => Promise<void>;
2997
+ };
2998
+ ```
2999
+
3000
+ ## packages/types/src/define-handler.ts
3001
+
3002
+ ```ts
3003
+ type StreamHandlerDefinition<TName extends string = string, TSchema extends ZodType = ZodType, TChunk = unknown, TMap extends object = KumikoEventTypeMap> = {
3004
+ name: TName;
3005
+ schema: TSchema;
3006
+ access: AccessRule;
3007
+ rateLimit?: RateLimitDeclaration;
3008
+ escapeHatch?: EscapeHatchDeclaration;
3009
+ handler: (query: QueryEvent<z.infer<TSchema>>, context: HandlerContext<TMap>) => AsyncGenerator<TChunk>;
3010
+ };
3011
+ ```
3012
+
3013
+ ## packages/types/src/derivatives-types.ts
3014
+
3015
+ ```ts
3016
+ type BlurRegion = {
3017
+ x: number;
3018
+ y: number;
3019
+ width: number;
3020
+ height: number;
3021
+ };
3022
+ type DerivativeRendererPlugin = {
3023
+ render: (input: Uint8Array, spec: VariantSpec, sourceMimeType: string) => Promise<Uint8Array>;
3024
+ };
3025
+ type DerivativesContext = {
3026
+ variant: (fileRefId: string, spec: VariantSpec, name: string) => Promise<VariantResult>;
3027
+ };
3028
+ type OverlayGravity = "north-west" | "north-east" | "south-west" | "south-east" | "center";
3029
+ type OverlayLayer = ({ kind: "qr"; dataToken: string; } & OverlayPlacement) | ({ kind: "image"; imageBase64: string; } & OverlayPlacement);
3030
+ type OverlayPlacement = {
3031
+ widthPct: number;
3032
+ gravity: OverlayGravity;
3033
+ marginPct?: number;
3034
+ };
3035
+ type ResolvedOverlayLayer = ({ kind: "qr"; data: string; } & OverlayPlacement) | Extract<OverlayLayer, { kind: "image"; }>;
3036
+ type VariantFit = "cover" | "inside" | "contain";
3037
+ type VariantFormat = "webp" | "avif" | "jpeg";
3038
+ type VariantResult = {
3039
+ storageKey: string;
3040
+ mimeType: string;
3041
+ rendered: boolean;
3042
+ };
3043
+ type VariantSpec = {
3044
+ fit?: VariantFit;
3045
+ size?: {
3046
+ width: number;
3047
+ height: number;
3048
+ };
3049
+ maxEdge?: number;
3050
+ format?: VariantFormat;
3051
+ quality?: number;
3052
+ blur?: number;
3053
+ blurRegions?: BlurRegion[];
3054
+ overlays?: OverlayLayer[];
3055
+ resolvedOverlays?: ResolvedOverlayLayer[];
3056
+ };
3057
+ ```
3058
+
3059
+ ## packages/types/src/entity-cache.ts
3060
+
3061
+ ```ts
3062
+ type EntityCache = {
3063
+ get(tenantId: TenantId, entityName: string, id: EntityId): Promise<Record<string, unknown> | null>; // Get a single cached entity.
3064
+ mget(tenantId: TenantId, entityName: string, ids: EntityId[]): Promise<Map<EntityId, Record<string, unknown>>>; // Get multiple cached entities.
3065
+ set(tenantId: TenantId, entityName: string, id: EntityId, data: Record<string, unknown>): Promise<void>; // Cache a single entity.
3066
+ mset(tenantId: TenantId, entityName: string, entries: ReadonlyArray<{ id: EntityId; data: Record<string, unknown>; }>): Promise<void>; // Cache multiple entities at once.
3067
+ del(tenantId: TenantId, entityName: string, id: EntityId): Promise<void>; // Invalidate a single cached entity.
3068
+ };
3069
+ ```
3070
+
3071
+ ## packages/types/src/entity-handlers.ts
3072
+
3073
+ ```ts
3074
+ type EntityCrudHandlerDefaults = Omit<EntityHandlerOptions, "access"> & {
3075
+ access?: AccessRule;
3076
+ };
3077
+ type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
3078
+ type EntityHandlerOptions = {
3079
+ access: AccessRule;
3080
+ description?: string;
3081
+ agent?: AgentHandlerHints;
3082
+ escapeHatch?: EscapeHatchDeclaration; // Lifts row filtering for this ONE handler across every tenant instead of the caller's own — for a SystemAdmin-only operator handler over an otherwise tenant-scoped entity.
3083
+ };
3084
+ type EntityQueryHandlerOptions = EntityHandlerOptions;
3085
+ type EntityWriteHandlerOptions = EntityHandlerOptions & { // Options for the generic create/update handlers.
3086
+ excludeFields?: string[]; // Fields this handler never writes.
3087
+ };
3088
+ type RegisterEntityCrudOptions = {
3089
+ write?: EntityCrudHandlerDefaults;
3090
+ read?: EntityCrudHandlerDefaults;
3091
+ verbs?: Partial<Record<EntityCrudVerb, boolean>>;
3092
+ verbAccess?: Partial<Record<EntityCrudVerb, AccessRule>>; // Per-verb access override — falls back to `write.access`/`read.access` when unset for a verb.
3093
+ descriptions?: Partial<Record<EntityCrudVerb, string>>; // Per-verb `description` — the author writes one sentence per verb; the AI-agent manifest exposes only the verbs described here (fail-closed, same rule as hand-written handlers).
3094
+ agents?: Partial<Record<EntityCrudVerb, AgentHandlerHints>>; // Per-verb agent hints; win over `write.agent`/`read.agent`, so one irreversible verb (e.g. a hard delete) can be `risk: "high"` without raising create/update.
3095
+ excludeFields?: Partial<Record<"create" | "update", string[]>>; // Per-verb `excludeFields` for the generic create/update handlers — see EntityWriteHandlerOptions.
3096
+ registerEntity?: boolean; // Default true.
3097
+ };
3098
+ ```
3099
+
3100
+ ## packages/types/src/entity-table-meta-types.ts
3101
+
3102
+ ```ts
3103
+ type BuildEntityTableMetaOptions = {
3104
+ featureName?: string;
3105
+ relations?: EntityRelations;
3106
+ source?: EntityTableMeta["source"];
3107
+ };
3108
+ type CompositePrimaryKeyMeta = {
3109
+ name: string;
3110
+ columns: string[];
3111
+ };
3112
+ type IndexMeta = {
3113
+ name: string;
3114
+ columns: string[];
3115
+ unique?: boolean;
3116
+ whereSql?: string;
3117
+ needsManualWhere?: boolean;
3118
+ };
3119
+ type PgType = "uuid" | "text" | "boolean" | "integer" | "double precision" | "bigint" | "serial" | "bigserial" | "jsonb" | "timestamptz" | "timestamptz(3)" | "date" | `numeric(${number},${number})`;
3120
+ type UnmanagedTableInput = {
3121
+ tableName: string;
3122
+ columns: ColumnMeta[];
3123
+ indexes?: IndexMeta[];
3124
+ compositePrimaryKey?: CompositePrimaryKeyMeta;
3125
+ tenancy?: EntityTenancy;
3126
+ };
3127
+ ```
3128
+
3129
+ ## packages/types/src/envelope-cipher-types.ts
3130
+
3131
+ ```ts
3132
+ type EnvelopeCipher = {
3133
+ encrypt(plaintext: string, scope?: KeyScope): Promise<string>;
3134
+ decrypt(stored: string, scope?: KeyScope): Promise<string>;
3135
+ };
3136
+ ```
3137
+
3138
+ ## packages/types/src/event-store-executor-types.ts
3139
+
3140
+ ```ts
3141
+ type ListAggregateRequest = {
3142
+ search?: string | undefined;
3143
+ filter?: ListFilterOption | undefined;
3144
+ filters?: ListFilterOption[] | undefined;
3145
+ aggregate: ListAggregatePayload;
3146
+ };
3147
+ type ListFilterOption = {
3148
+ field: string;
3149
+ op: "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
3150
+ value: unknown;
3151
+ };
3152
+ type ListRuntimeOptions = {
3153
+ searchAdapter?: SearchAdapter;
3154
+ includeDeleted?: boolean;
3155
+ referenceSearch?: {
3156
+ fields: ReadonlyArray<{ fieldName: string; targetEntityName: string; labelField: string; }>;
3157
+ resolveEntity: (entityName: string) => EntityDefinition | undefined;
3158
+ };
3159
+ referenceSort?: {
3160
+ fields: ReadonlyArray<{ fieldName: string; targetEntityName: string; labelField: string; }>;
3161
+ resolveEntity: (entityName: string) => EntityDefinition | undefined;
3162
+ };
3163
+ parentVisibility?: ParentVisibilityOption;
3164
+ };
3165
+ type LockGuardOption = {
3166
+ unsafeSkipLockGuard?: boolean;
3167
+ writePurpose?: WritePurpose;
3168
+ };
3169
+ type ParentVisibilityGateOption = {
3170
+ parentVisibility?: ParentVisibilityOption;
3171
+ };
3172
+ type ParentVisibilityOption = {
3173
+ entities: ReadonlyMap<string, EntityDefinition>;
3174
+ };
3175
+ type PreSaveRunner = (changes: Record<string, unknown>, previous: Readonly<Record<string, unknown>>, isNew: boolean) => Promise<Record<string, unknown>>;
3176
+ type StreamTenantOption = {
3177
+ streamTenantId?: TenantId | undefined;
3178
+ };
3179
+ ```
3180
+
3181
+ ## packages/types/src/event-store-types.ts
3182
+
3183
+ ```ts
3184
+ type WriteOrigin = {
3185
+ rootHandler: string;
3186
+ anonymousRoot: boolean;
3187
+ publicIntake: boolean;
3188
+ viaJob?: string;
3189
+ };
3190
+ ```
3191
+
3192
+ ## packages/types/src/executor-brand.ts
3193
+
3194
+ ```ts
3195
+ interface ExecutorManagedWriteCapability {
3196
+ [EXECUTOR_MANAGED_WRITE]: true;
3197
+ }
3198
+ interface Writable {
3199
+ [WRITABLE]: true;
3200
+ }
3201
+ ```
3202
+
3203
+ ## packages/types/src/feature-registrar.ts
3204
+
3205
+ ```ts
3206
+ type EntityObjectDefinition = EntityDefinition & {
3207
+ name: string;
3208
+ backingTable?: unknown;
3209
+ };
3210
+ type HookDefinition =
3211
+ | {
3212
+ type: "validation";
3213
+ target: RefOrRefs;
3214
+ handler: ValidationHookFn;
3215
+ escapeHatch?: never;
3216
+ phase?: never;
3217
+ }
3218
+ | ({ type: "preSave"; target: RefOrRefs; handler: PreSaveHookFn; phase?: never; } & HookEscapeHatch)
3219
+ | ({ type: "postSave"; target: HookTarget; handler: PostSaveHookFn; } & HookEscapeHatch & HookPhaseOption)
3220
+ | ({ type: "preDelete"; target: HookTarget; handler: PreDeleteHookFn; phase?: never; } & HookEscapeHatch)
3221
+ | ({ type: "postDelete"; target: HookTarget; handler: PostDeleteHookFn; } & HookEscapeHatch & HookPhaseOption)
3222
+ | ({ type: "postQuery"; target: HookTarget; handler: PostQueryHookFn; phase?: never; } & HookEscapeHatch)
3223
+ ;
3224
+ type HookEscapeHatch = {
3225
+ escapeHatch?: EscapeHatchDeclaration;
3226
+ };
3227
+ type HookPhaseOption = {
3228
+ phase?: HookPhase;
3229
+ };
3230
+ type HookTarget =
3231
+ | RefOrRefs
3232
+ | {
3233
+ allOf: NameOrRef;
3234
+ }
3235
+ ;
3236
+ type RefOrRefs = NameOrRef | NameOrRef[];
3237
+ type RequiresApi = ((options: { features: string[]; }) => void) & { // `r.requires({ features })` declares the features this one depends on; the `.projection` extension declares read-side projection tables that this feature's pipeline steps are allowed to write via `r.step.unsafeProjectionUpsert` (hard-required for any unsafeProjection-* step usage, see Q10).
3238
+ projection: (tableName: string) => void;
3239
+ step: (stepKind: string) => void;
3240
+ };
3241
+ ```
3242
+
3243
+ ## packages/types/src/feature.ts
3244
+
3245
+ ```ts
3246
+ type BootCheckContext = {
3247
+ features: FeatureDefinition[];
3248
+ };
3249
+ type BootCheckFn = (ctx: BootCheckContext) => void;
3250
+ type FeatureMetricDef = {
3251
+ shortName: string;
3252
+ type: FeatureMetricType;
3253
+ description?: string;
3254
+ labels?: string[];
3255
+ buckets?: number[];
3256
+ unit?: string;
3257
+ tenantLabel?: boolean;
3258
+ };
3259
+ type FeatureMetricType = "counter" | "histogram" | "gauge";
3260
+ type MetricOptions = Omit<FeatureMetricDef, "shortName">;
3261
+ type SecretOptions = Omit<SecretKeyDefinition, "shortName" | "qualifiedName">;
3262
+ type StackBootRequirement = {
3263
+ stack: string;
3264
+ options: BootOptionName[];
3265
+ severity?: "error" | "warn"; // `"warn"`: a missing option logs a boot warning instead of aborting.
3266
+ };
3267
+ type StoreTableDef = StoreTableEntry & { // Registry-aggregated store-table — the per-feature `StoreTableEntry` plus the owning feature name.
3268
+ featureName: string;
3269
+ };
3270
+ type StoreTableEntry = { // Per-feature store-table registration.
3271
+ name: string;
3272
+ meta: EntityTableMeta;
3273
+ reason: string;
3274
+ piiEncryptedOnWrite?: true;
3275
+ };
3276
+ type StoreTableOptions = { // Options accepted by `r.storeTable()`.
3277
+ reason: string; // Why this table needs to bypass the event-sourcing system.
3278
+ piiEncryptedOnWrite?: true; // Direct-write stores skip the executor, so the executor's PII encryption never runs for them — a feature whose meta carries piiSubjectFields must encrypt those fields itself before every insert/update and declare that here, or boot fails.
3279
+ };
3280
+ type UiHintOption = {
3281
+ key: string;
3282
+ label: string;
3283
+ type: "boolean";
3284
+ default: boolean;
3285
+ };
3286
+ type UiHints = {
3287
+ displayLabel?: string;
3288
+ category?: string;
3289
+ recommended?: boolean;
3290
+ configurableOptions?: UiHintOption[];
3291
+ };
3292
+ ```
3293
+
3294
+ ## packages/types/src/fields-derived.ts
3295
+
3296
+ ```ts
3297
+ type DerivedFieldDef = {
3298
+ valueType: DerivedValueType;
3299
+ derive: (row: Readonly<Record<string, unknown>>, ctx: DeriveContext) => unknown; // Pure function of the stored row + clock.
3300
+ };
3301
+ type DerivedFieldsMap = Readonly<Record<string, DerivedFieldDef>>;
3302
+ type DerivedValueType = "text" | "number" | "decimal" | "boolean" | "date" | "timestamp"; // Display type a derived value formats as — drives the column's renderer choice in the view-model, parallel to FieldDefinition["type"].
3303
+ ```
3304
+
3305
+ ## packages/types/src/fields.ts
3306
+
3307
+ ```ts
3308
+ type BigIntFieldDef = ResolvedPiiFlags & { // 64-bit integer field for audit counters, byte sizes, event IDs, and other values that can exceed 2^31 (~2.1 billion).
3309
+ type: "bigInt";
3310
+ description?: string;
3311
+ required?: boolean;
3312
+ sortable?: boolean;
3313
+ sortNulls?: SortNulls;
3314
+ filterable?: boolean;
3315
+ sensitive?: boolean;
3316
+ default?: number;
3317
+ access?: FieldAccess;
3318
+ };
3319
+ type BooleanFieldDef = {
3320
+ type: "boolean";
3321
+ description?: string;
3322
+ required?: boolean;
3323
+ sortable?: boolean;
3324
+ sortNulls?: SortNulls;
3325
+ filterable?: boolean;
3326
+ sensitive?: boolean;
3327
+ default?: boolean;
3328
+ access?: FieldAccess;
3329
+ };
3330
+ type DateFieldDef = ResolvedPiiFlags & {
3331
+ type: "date";
3332
+ description?: string;
3333
+ required?: boolean;
3334
+ sortable?: boolean;
3335
+ sortNulls?: SortNulls;
3336
+ filterable?: boolean;
3337
+ sensitive?: boolean;
3338
+ access?: FieldAccess;
3339
+ min?: string; // Permitted date bounds as ISO `yyyy-mm-dd` (for example, a birth date cannot be in the future when `max` is today).
3340
+ max?: string;
3341
+ locale?: string; // Format/locale override for display and input parsing (for example, "de-DE").
3342
+ };
3343
+ type DecimalFieldDef = ResolvedPiiFlags & { // Exact decimal — Postgres `numeric(precision, scale)`.
3344
+ type: "decimal";
3345
+ description?: string;
3346
+ precision: number;
3347
+ scale: number;
3348
+ required?: boolean;
3349
+ sortable?: boolean;
3350
+ sortNulls?: SortNulls;
3351
+ filterable?: boolean;
3352
+ sensitive?: boolean;
3353
+ default?: number;
3354
+ access?: FieldAccess;
3355
+ };
3356
+ type EmbeddedFieldDef = ResolvedPiiFlags & {
3357
+ type: "embedded";
3358
+ description?: string;
3359
+ required?: boolean;
3360
+ sensitive?: boolean;
3361
+ schema: Readonly<Record<string, EmbeddedSubFieldDef>>;
3362
+ access?: FieldAccess;
3363
+ multiple?: boolean; // A list of like-shaped objects instead of exactly one: the value is an array and every element is validated against `schema`.
3364
+ minItems?: number; // Minimum row count for an embedded list.
3365
+ maxItems?: number; // Maximum row count for an embedded list.
3366
+ derived?: Readonly<Record<string, EmbeddedDerivedCellDef>>; // Renderer metadata: sub-field name → how to compute its cell from other sub-fields of the same row, rounded to that sub-field's declared precision.
3367
+ totals?: EmbeddedTotalDef[]; // Renderer metadata: footer totals.
3368
+ totalsMatch?: Readonly<Record<string, string>>; // Maps an embedded-list subfield name to a sibling top-level money field name — the sum of that subfield across all rows must equal the sibling field's amount.
3369
+ };
3370
+ type EmbeddedSubFieldBase = {
3371
+ required?: boolean;
3372
+ searchable?: boolean;
3373
+ access?: FieldAccess;
3374
+ };
3375
+ type EmbeddedSubFieldDef = (EmbeddedSubFieldBase & { type: "text" | "number" | "boolean" | "date" | "money" | "timestamp"; }) | (EmbeddedSubFieldBase & { type: "decimal"; scale: number; }) | (EmbeddedSubFieldBase & { type: "select"; options: string[]; display?: "radio" | "dropdown"; }) | (EmbeddedSubFieldBase & { type: "reference"; entity: string; labelField?: string; optionsQuery?: string; }); // `money` is a signed integer amount in minor units (cents); which currency it means is defined by the owning aggregate (a head-level field or tenant config), not per row.
3376
+ type EmbeddedTotalDef =
3377
+ | string
3378
+ | {
3379
+ label: string;
3380
+ from: string[];
3381
+ }
3382
+ ;
3383
+ type EntityEventMigration = {
3384
+ fromVersion: number;
3385
+ toVersion: number;
3386
+ transform: (fields: Readonly<Record<string, unknown>>, ctx: EventUpcastCtx) => Record<string, unknown> | Promise<Record<string, unknown>>;
3387
+ };
3388
+ type EntityFieldDefinition = Exclude<FieldDefinition, TextFieldDef | LongTextFieldDef> | EntityTextFieldDef | EntityLongTextFieldDef;
3389
+ type EntityIndexDef = { // Composite index on an entity.
3390
+ columns: [ string, ...string[] ];
3391
+ unique?: boolean;
3392
+ name?: string;
3393
+ where?: unknown; // Optional SQL fragment for a partial index — `CREATE [UNIQUE] INDEX ...
3394
+ };
3395
+ type EntityLongTextFieldDef = LongTextFieldDef & ResolvedPiiStance;
3396
+ type EntityPiiAbsentSubject = "tenant" | "self";
3397
+ type EntityTextFieldDef = TextFieldDef & ResolvedPiiStance;
3398
+ type FieldAccess = {
3399
+ read?: OwnershipMap | string[];
3400
+ write?: OwnershipMap | string[];
3401
+ };
3402
+ type FieldsMap = Readonly<Record<string, EntityFieldDefinition>>;
3403
+ type FileFieldDef = ResolvedPiiFlags & {
3404
+ type: "file";
3405
+ description?: string;
3406
+ required?: boolean;
3407
+ maxSize?: string;
3408
+ anonymousUpload?: boolean; // Anonymous visitors may upload into this field as a pending upload (no entityId); the app binds the file itself.
3409
+ accept?: string[];
3410
+ access?: FieldAccess;
3411
+ };
3412
+ type FilesFieldDef = ResolvedPiiFlags & {
3413
+ type: "files";
3414
+ description?: string;
3415
+ maxSize?: string;
3416
+ anonymousUpload?: boolean; // Anonymous visitors may upload into this field as a pending upload (no entityId); the app binds the file itself.
3417
+ accept?: string[];
3418
+ maxCount?: number;
3419
+ display?: "list" | "dropzone"; // `"list"` (default) shows the picked files as a list with a file picker; `"dropzone"` shows a drag-and-drop zone with one status row per file, a remove action per file and the field's `maxCount`/`accept`/`maxSize` as hints.
3420
+ access?: FieldAccess;
3421
+ };
3422
+ type Findability = "exact" | "fuzzy" | "none" | "secret";
3423
+ type FormFieldDefinition = Exclude<FieldDefinition, MoneyFieldDef> | (MoneyFieldDef & { currency: MoneyCurrencySource; }); // FieldDefinition as an entity-less inline form screen (actionForm, secretMint) may declare it: a `money` field there has no entity to borrow `defaultCurrency` from, so it must name its own currency source, or the form seeds a bare `0` that the handler's zod schema then rejects .
3424
+ type ImageFieldDef = ResolvedPiiFlags & {
3425
+ type: "image";
3426
+ description?: string;
3427
+ required?: boolean;
3428
+ maxSize?: string;
3429
+ anonymousUpload?: boolean; // Anonymous visitors may upload into this field as a pending upload (no entityId); the app binds the file itself.
3430
+ accept?: string[];
3431
+ variants?: Readonly<Record<string, VariantSpec>>; // Named derived versions of the uploaded image.
3432
+ capture?: "environment" | "user"; // Forwarded to the file input's `capture` attribute so a phone opens the camera instead of the file picker.
3433
+ access?: FieldAccess;
3434
+ };
3435
+ type ImagesFieldDef = ResolvedPiiFlags & {
3436
+ type: "images";
3437
+ description?: string;
3438
+ maxSize?: string;
3439
+ anonymousUpload?: boolean; // Anonymous visitors may upload into this field as a pending upload (no entityId); the app binds the file itself.
3440
+ accept?: string[];
3441
+ maxCount?: number;
3442
+ display?: "list" | "dropzone"; // `"list"` (default) shows the picked files as a list with a file picker; `"dropzone"` shows a drag-and-drop zone with one status row per file, a remove action per file and the field's `maxCount`/`accept`/`maxSize` as hints.
3443
+ variants?: Readonly<Record<string, VariantSpec>>;
3444
+ capture?: "environment" | "user"; // Forwarded to the file input's `capture` attribute so a phone opens the camera instead of the file picker.
3445
+ access?: FieldAccess;
3446
+ };
3447
+ type JsonbEmptyValue = "object" | "array";
3448
+ type LocatedTimestampFieldDef = ResolvedPiiFlags & {
3449
+ type: "locatedTimestamp";
3450
+ description?: string;
3451
+ required?: boolean;
3452
+ sortable?: boolean;
3453
+ sortNulls?: SortNulls;
3454
+ filterable?: boolean;
3455
+ sensitive?: boolean;
3456
+ access?: FieldAccess;
3457
+ min?: string; // Permitted bounds as ISO datetime values (wall-clock time).
3458
+ max?: string;
3459
+ locale?: string; // Format/locale override for display and input parsing.
3460
+ };
3461
+ type LongTextFieldDef = ResolvedPiiFlags & { // Long-form text content — source code, markdown, blog posts, email templates, and anything that can be megabytes large.
3462
+ type: "longText";
3463
+ description?: string;
3464
+ maxLength?: number; // Optional soft cap.
3465
+ required?: boolean;
3466
+ encrypted?: boolean;
3467
+ sensitive?: boolean;
3468
+ readAsInstruction?: boolean; // A later run reads this field's value as an instruction — prompt, rule, template.
3469
+ default?: string;
3470
+ access?: FieldAccess;
3471
+ multiline?: boolean | { rows?: number; };
3472
+ };
3473
+ type LongTextFindability = "none" | "secret";
3474
+ type MoneyCurrencySource = // Where a money field's currency comes from.
3475
+ | {
3476
+ kind: "tenant";
3477
+ }
3478
+ | {
3479
+ kind: "literal";
3480
+ code: string;
3481
+ }
3482
+ ;
3483
+ type MoneyFieldDef = {
3484
+ type: "money";
3485
+ description?: string;
3486
+ required?: boolean;
3487
+ sortable?: boolean;
3488
+ sortNulls?: SortNulls;
3489
+ filterable?: boolean;
3490
+ sensitive?: boolean;
3491
+ access?: FieldAccess;
3492
+ currency?: MoneyCurrencySource; // Declares where this field's currency comes from for a currently-empty value in an entityEdit form.
3493
+ };
3494
+ type MultiSelectFieldDef<TOptions extends string[] = string[]> = ResolvedPiiFlags & {
3495
+ type: "multiSelect";
3496
+ description?: string;
3497
+ options: TOptions;
3498
+ required?: boolean;
3499
+ filterable?: boolean;
3500
+ sensitive?: boolean;
3501
+ default?: TOptions[number][]; // Default selection.
3502
+ access?: FieldAccess;
3503
+ display?: "checkboxes" | "dropdown"; // Renderer metadata: `"checkboxes"` renders every option as a visible checkbox with a select-all toggle.
3504
+ columns?: 1 | 2 | 3 | 4; // Renderer metadata: column count for the `"checkboxes"` grid at the widest breakpoint; narrow viewports always collapse to one column.
3505
+ maxRows?: number; // Renderer metadata: how many grid rows stay visible before the `"checkboxes"` grid becomes scrollable.
3506
+ optionsAvailabilityQuery?: string; // Query returning `{ rows: { value, disabled?, hint?
3507
+ };
3508
+ type NoPersonalAnnotation = {
3509
+ personal?: never;
3510
+ find?: never;
3511
+ reason?: never;
3512
+ anonymize?: never;
3513
+ };
3514
+ type NumberFieldDef = ResolvedPiiFlags & { // Storage: `integer` flag decides the Postgres column type, not just Zod validation.
3515
+ type: "number";
3516
+ description?: string;
3517
+ required?: boolean;
3518
+ sortable?: boolean;
3519
+ sortNulls?: SortNulls;
3520
+ filterable?: boolean;
3521
+ sensitive?: boolean;
3522
+ default?: number;
3523
+ access?: FieldAccess;
3524
+ min?: number;
3525
+ max?: number; // Upper bound at the write boundary (Zod `.max`).
3526
+ integer?: boolean; // `true` → `integer` column + `.int()` Zod validation.
3527
+ unit?: string | { field: string; }; // Display-only suffix: a static string, or a sibling field name whose value supplies it.
3528
+ grouping?: boolean; // Thousands-grouping in the rendered display (Intl `useGrouping`).
3529
+ };
3530
+ type OptionsQueryPayloadValue = // One `optionsQueryPayload` entry: a literal, or `{ field }` = the current value of a sibling field of the same form (config keys: another key of the owner feature).
3531
+ | string
3532
+ | number
3533
+ | boolean
3534
+ | {
3535
+ field: string;
3536
+ }
3537
+ ;
3538
+ type ParentRefDef = {
3539
+ entityTypeField: string; // Field holding the name of the host entity this row hangs off.
3540
+ entityIdField: string; // Field holding the host row's id.
3541
+ allowedTypes?: string[]; // Narrow which registered entities may act as host.
3542
+ };
3543
+ type PersonalAnnotations =
3544
+ | {
3545
+ personal: PersonalSubject;
3546
+ find: Findability;
3547
+ anonymize?: () => unknown | Promise<unknown>;
3548
+ }
3549
+ | {
3550
+ personal: "ref";
3551
+ }
3552
+ | {
3553
+ personal: false;
3554
+ reason: string;
3555
+ anonymize?: () => unknown | Promise<unknown>;
3556
+ }
3557
+ ;
3558
+ type PersonalAnnotationsLongText =
3559
+ | {
3560
+ personal: PersonalSubject;
3561
+ find: LongTextFindability;
3562
+ anonymize?: () => unknown | Promise<unknown>;
3563
+ }
3564
+ | {
3565
+ personal: "ref";
3566
+ }
3567
+ | {
3568
+ personal: false;
3569
+ reason: string;
3570
+ anonymize?: () => unknown | Promise<unknown>;
3571
+ }
3572
+ ;
3573
+ type PersonalAnnotationsNoFind =
3574
+ | {
3575
+ personal: PersonalSubject;
3576
+ anonymize?: () => unknown | Promise<unknown>;
3577
+ }
3578
+ | {
3579
+ personal: "ref";
3580
+ }
3581
+ | {
3582
+ personal: false;
3583
+ reason: string;
3584
+ anonymize?: () => unknown | Promise<unknown>;
3585
+ }
3586
+ | NoPersonalAnnotation
3587
+ ;
3588
+ type PersonalSubject =
3589
+ | "self"
3590
+ | {
3591
+ of: string;
3592
+ whenAbsent?: EntityPiiAbsentSubject;
3593
+ }
3594
+ | "tenant"
3595
+ ;
3596
+ type ReferenceFieldDef = ResolvedPiiFlags & {
3597
+ type: "reference";
3598
+ description?: string;
3599
+ entity: string;
3600
+ required?: boolean;
3601
+ filterable?: boolean;
3602
+ sensitive?: boolean;
3603
+ access?: FieldAccess;
3604
+ labelField?: string; // Field of the referenced entity displayed as the Select dropdown label.
3605
+ multiple?: boolean; // Multi-reference: the value is an array of UUIDs rather than a single UUID.
3606
+ searchable?: true; // Include a row in this entity's result set on a text-search hit against the referenced entity's `labelField`, instead of only matching the UUID column itself.
3607
+ sortable?: true; // Order a list by the referenced entity's `labelField` instead of by the raw UUID column.
3608
+ sortNulls?: SortNulls;
3609
+ optionsQuery?: string; // Fill the picker from this query handler instead of the referenced entity's own list handler — for targets whose identity is composed from joined rows, where no single column of the referenced entity is a readable label.
3610
+ handover?: "stay"; // tenant-handover opt-out: rows of THIS entity that point at a handed-over row through this field stay in the source tenant instead of travelling with it (or failing the claim with `entity_not_transferable`).
3611
+ };
3612
+ type ResolvedPiiStance =
3613
+ | {
3614
+ pii: true;
3615
+ }
3616
+ | {
3617
+ userOwned: NonNullable<ResolvedPiiFlags["userOwned"]>;
3618
+ }
3619
+ | {
3620
+ tenantOwned: true;
3621
+ }
3622
+ | {
3623
+ recordOwned: true;
3624
+ }
3625
+ | {
3626
+ allowPlaintext: string;
3627
+ }
3628
+ | {
3629
+ subjectRef: true;
3630
+ }
3631
+ ;
3632
+ type SelectConditionalOptions<TOption extends string = string> = { // Options of a select field that are only available while `when` holds for the row values.
3633
+ options: TOption[];
3634
+ when: Exclude<FieldCondition, boolean>;
3635
+ };
3636
+ type SelectFieldDef<TOptions extends string[] = string[]> = ResolvedPiiFlags & {
3637
+ type: "select";
3638
+ description?: string;
3639
+ options: TOptions;
3640
+ required?: boolean;
3641
+ sortable?: boolean;
3642
+ sortNulls?: SortNulls;
3643
+ filterable?: boolean;
3644
+ sensitive?: boolean;
3645
+ default?: TOptions[number];
3646
+ access?: FieldAccess;
3647
+ display?: "radio" | "dropdown"; // Renderer metadata: `"radio"` always renders the option set as a visible radio group, `"dropdown"` always renders the combobox.
3648
+ optionTones?: { [K in TOptions[number]]?: SelectOptionTone; }; // Status tone per option, shown as a toned badge with dot in lists and detail bands.
3649
+ optionsQuery?: string; // Loads the options from this query (QN), labels shown verbatim: `{ rows: { value, label, description?, group?, default?
3650
+ optionsQueryPayload?: OptionsQueryPayload; // Payload sent with every `optionsQuery` call.
3651
+ optionsAvailabilityQuery?: string; // Marks static options as unavailable per tenant (e.g. by tier).
3652
+ conditionalOptions?: SelectConditionalOptions<TOptions[number]>[]; // Options that are only available while `when` holds for the row values; options in no rule are always available.
3653
+ };
3654
+ type SortNulls = "first" | "last"; // Where rows without a value sort in a list, in both directions.
3655
+ type TimestampFieldDef = ResolvedPiiFlags & {
3656
+ type: "timestamp";
3657
+ description?: string;
3658
+ required?: boolean;
3659
+ sortable?: boolean;
3660
+ sortNulls?: SortNulls;
3661
+ filterable?: boolean;
3662
+ sensitive?: boolean;
3663
+ access?: FieldAccess;
3664
+ locatedBy?: string; // Marker: this timestamp field represents wall-clock time at a location.
3665
+ min?: string; // Permitted bounds as ISO datetime values.
3666
+ max?: string;
3667
+ locale?: string; // Format/locale override for display and input parsing.
3668
+ };
3669
+ type TransitionMap = Readonly<Record<string, string[]>>;
3670
+ type TzFieldDef = ResolvedPiiFlags & {
3671
+ type: "tz";
3672
+ description?: string;
3673
+ required?: boolean;
3674
+ sensitive?: boolean;
3675
+ access?: FieldAccess;
3676
+ };
3677
+ ```
3678
+
3679
+ ## packages/types/src/file-handle-types.ts
3680
+
3681
+ ```ts
3682
+ type FileContext = {
3683
+ ref(key: string): FileHandle;
3684
+ refFromFileRef(row: { storageKey: string; }): FileHandle;
3685
+ listFromFileRef(row: { storageKey: string; }): Promise<string[]>;
3686
+ list(prefix: string): Promise<string[]>;
3687
+ writeUploadBytes(key: string, data: Uint8Array, mimeType: string): Promise<void>;
3688
+ forgetUploadBytes(key: string): Promise<void>;
3689
+ };
3690
+ type FileHandle = {
3691
+ key: string;
3692
+ read(): Promise<Uint8Array>;
3693
+ write(data: Uint8Array, mimeType?: string): Promise<void>;
3694
+ delete(): Promise<void>;
3695
+ exists(): Promise<boolean>;
3696
+ derive(suffix: string): FileHandle;
3697
+ };
3698
+ ```
3699
+
3700
+ ## packages/types/src/file-provider-resolver-types.ts
3701
+
3702
+ ```ts
3703
+ type FileProviderResolver = ((tenantId: TenantId) => Promise<FileStorageProvider>) & {
3704
+ uploadLedgerFor?: (tenantId: TenantId) => FileUploadLedger;
3705
+ };
3706
+ type FileUploadLedger = {
3707
+ record(storageKey: string): Promise<void>;
3708
+ forget(storageKey: string): Promise<void>;
3709
+ };
3710
+ ```
3711
+
3712
+ ## packages/types/src/file-storage-provider-types.ts
3713
+
3714
+ ```ts
3715
+ type FileStorageProvider = {
3716
+ write(key: string, data: Uint8Array, mimeType?: string): Promise<void>;
3717
+ writeStream(key: string, source: AsyncIterable<Uint8Array>, options?: WriteStreamOptions): Promise<void>;
3718
+ read(key: string): Promise<Uint8Array>;
3719
+ readStream(key: string): AsyncIterable<Uint8Array>;
3720
+ delete(key: string): Promise<void>;
3721
+ exists(key: string): Promise<boolean>;
3722
+ list(prefix: string): Promise<string[]>;
3723
+ getSignedUrl?(key: string, expiresInSeconds: number, options?: SignedUrlOptions): Promise<string>;
3724
+ };
3725
+ type SignedUrlOptions = {
3726
+ contentDisposition?: string;
3727
+ };
3728
+ type WriteStreamOptions = {
3729
+ mimeType?: string;
3730
+ contentLength?: number;
3731
+ };
3732
+ ```
3733
+
3734
+ ## packages/types/src/geo-tz.ts
3735
+
3736
+ ```ts
3737
+ type GeoAddress = {
3738
+ street?: string;
3739
+ city?: string;
3740
+ region?: string;
3741
+ postalCode?: string;
3742
+ country?: string;
3743
+ };
3744
+ type GeoCoordinates = {
3745
+ latitude: number;
3746
+ longitude: number;
3747
+ };
3748
+ type GeoTzProvider = {
3749
+ fromCoordinates: (coords: GeoCoordinates) => string | Promise<string>; // Geo-coordinates → IANA zone (offline lat/lng → tz).
3750
+ fromAddress?: (address: GeoAddress) => string | Promise<string>; // Optional: postal address → IANA zone (geocoding API provider).
3751
+ };
3752
+ ```
3753
+
3754
+ ## packages/types/src/handler-context-facets.ts
3755
+
3756
+ ```ts
3757
+ type PhaseWithoutTransaction = "events" | "notify" | "dispatch" | "lifecycle";
3758
+ ```
3759
+
3760
+ ## packages/types/src/handler-context-services.ts
3761
+
3762
+ ```ts
3763
+ type ContextServices = { // Infrastructure the app wired at boot, as the handler or job sees it.
3764
+ redis?: Redis;
3765
+ cacheSync?: CacheSyncBus;
3766
+ jobRunner?: JobRunnerRef;
3767
+ searchAdapter?: SearchAdapter;
3768
+ entityCache?: EntityCache;
3769
+ secrets?: SecretsContext;
3770
+ kms?: KmsAdapter;
3771
+ files?: FileContext;
3772
+ derivatives?: DerivativesContext;
3773
+ configResolver?: ConfigResolver;
3774
+ configEncryption?: EnvelopeCipher;
3775
+ rateLimit?: RateLimitResolver;
3776
+ effectiveFeatures?: (tenantId: TenantId) => ReadonlySet<string>;
3777
+ masterKeyProvider?: MasterKeyProvider;
3778
+ _fileProviderResolver?: FileProviderResolver;
3779
+ _notifyFactory?: NotifyFactory;
3780
+ _notifyInTransactionFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
3781
+ _notifyAfterCommitFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
3782
+ _escapeHatchAuditSink?: EscapeHatchAuditSink;
3783
+ extensions: Readonly<Record<string, unknown>>;
3784
+ };
3785
+ type HandlerLifecycle = {
3786
+ runPreSave?: (handlerName: string, changes: Record<string, unknown>, previous: Readonly<Record<string, unknown>>, isNew: boolean) => Promise<Record<string, unknown>>; // Runs the registered `preSave` hooks (and system preSave hooks) for `handlerName` and returns the hook-transformed `changes`.
3787
+ afterCommit?: (hook: () => Promise<void>) => void; // Queues a side-effect to run after the outermost write transaction commits (same sink as ctx.dispatch.write).
3788
+ afterRollback?: (hook: () => Promise<void>) => void; // Runs when the handler transaction (or enclosing savepoint) does not commit, also after a failed COMMIT of unknown outcome, so write it idempotent and outcome-safe.
3789
+ signal?: AbortSignal; // Cancellation.
3790
+ };
3791
+ type HandlerTenants = {
3792
+ config?: (tenantId: TenantId) => ConfigAccessor; // Config accessor resolved for ANOTHER tenant, identity = that tenant's system user.
3793
+ writeTarget?: (tenantId: TenantId) => TenantWriteTarget; // Write target for ANOTHER tenant: a tenant-mode db bound to it plus the `streamTenantId` to hand the executor, so the event actor stays the operator.
3794
+ };
3795
+ type JobLifecycle = Pick<HandlerLifecycle, "signal">;
3796
+ ```
3797
+
3798
+ ## packages/types/src/handlers.ts
3799
+
3800
+ ```ts
3801
+ type ActiveMembership = {
3802
+ tenantId: TenantId;
3803
+ roles: string[];
3804
+ tenantName?: string;
3805
+ tenantKey?: string;
3806
+ };
3807
+ type ActiveMembershipRejection = "not_a_member" | "principal_blocked" | "tenant_teardown";
3808
+ type ActiveMembershipResult =
3809
+ | {
3810
+ kind: "active";
3811
+ membership: ActiveMembership;
3812
+ }
3813
+ | {
3814
+ kind: "rejected";
3815
+ reason: ActiveMembershipRejection;
3816
+ }
3817
+ ;
3818
+ type AgentExposure = {
3819
+ expose: boolean;
3820
+ risk: AgentRisk;
3821
+ };
3822
+ type AgentScreenHints = Pick<AgentHandlerHints, "expose">; // Screens only read `expose`; `risk` ranks handler tool calls, so it is not offered here.
3823
+ type AggregateStreamHandle = {
3824
+ events: StoredEvent[];
3825
+ version: number;
3826
+ appendOne: (args: { type: string; payload: unknown; }) => Promise<void>;
3827
+ hasEvent: (type: string) => boolean;
3828
+ };
3829
+ type AppendEventArgs = {
3830
+ aggregateId: string;
3831
+ aggregateType: string;
3832
+ type: string;
3833
+ payload: unknown;
3834
+ headers?: Readonly<Record<string, string | number | boolean>>;
3835
+ };
3836
+ type AppendEventFn<TMap extends object = KumikoEventTypeMap> = <K extends keyof TMap>(args: TypedAppendEventArgs<TMap, K>) => Promise<void>;
3837
+ type AuthClaimsFn = (user: SessionUser, ctx: AuthClaimsContext) => Promise<Record<string, unknown>>;
3838
+ type AuthClaimsHookDef = {
3839
+ featureName: string;
3840
+ fn: AuthClaimsFn;
3841
+ declaredKeys?: ReadonlySet<string>;
3842
+ };
3843
+ type BeforeTransactionPhase<TPayload = unknown, TPrepared = unknown> = (event: WriteEvent<TPayload>, ctx: BeforeTransactionContext) => Promise<BeforeTransactionResult<TPrepared>>;
3844
+ type BeforeTransactionWhenNested = "skip"; // `skip`: run as a nested write without the `beforeTransaction` phase.
3845
+ type CamelToKebab<S extends string> = CamelToKebabImpl<S, "start", "">; // Compile-time mirror of `engine/qualified-name.ts:toKebab` for camelCase → kebab-case.
3846
+ type CamelToKebabImpl<S extends string, Prev extends "start" | "upper" | "post-letter", Acc extends string> = S extends `${infer C}${infer Rest}` ? CharKind<C> extends "upper" ? Prev extends "start" ? CamelToKebabImpl<Rest, "upper", `${Acc}${Lowercase<C>}`> : Prev extends "post-letter" ? CamelToKebabImpl<Rest, "upper", `${Acc}-${Lowercase<C>}`> : Rest extends `${infer Next}${string}` ? CharKind<Next> extends "lower" ? CamelToKebabImpl<Rest, "upper", `${Acc}-${Lowercase<C>}`> : CamelToKebabImpl<Rest, "upper", `${Acc}${Lowercase<C>}`> : `${Acc}${Lowercase<C>}` : CharKind<C> extends "l…;
3847
+ type CharKind<C extends string> = C extends Lowercase<C> ? (C extends Uppercase<C> ? "non-letter" : "lower") : "upper"; // Three-way classification used by `CamelToKebab`: - "lower" — a lowercase letter (a-z and Unicode lowercase) - "upper" — an uppercase letter (A-Z and Unicode uppercase) - "non-letter" — digit, dot, dash, etc.
3848
+ type ClaimKeyDefinition = {
3849
+ shortName: string;
3850
+ qualifiedName: string;
3851
+ type: ClaimKeyType;
3852
+ };
3853
+ type ClaimKeyHandle<T extends ClaimKeyType = ClaimKeyType> = {
3854
+ name: string;
3855
+ type: T;
3856
+ };
3857
+ type ClaimKeyJsType<T extends ClaimKeyType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : T extends "string[]" ? string[] : T extends "object" ? Readonly<Record<string, unknown>> : never;
3858
+ type DeclarativeEventMigration = {
3859
+ rename?: Readonly<Record<string, string>>;
3860
+ default?: Readonly<Record<string, unknown>>;
3861
+ map?: Readonly<Record<string, (value: unknown) => unknown>>;
3862
+ };
3863
+ type DispatchWriteRef = {
3864
+ write: (user: SessionUser, qn: string, payload: unknown, inheritedOrigin?: WriteOrigin) => Promise<WriteResult>;
3865
+ queryAs: (user: SessionUser, qn: string, payload: unknown, inheritedOrigin?: WriteOrigin) => Promise<unknown>;
3866
+ createMemberReader: (tenantId: TenantId) => MemberReader;
3867
+ };
3868
+ type EntityRef = {
3869
+ name: string;
3870
+ table: string;
3871
+ };
3872
+ type EscapeHatchGrant = (typeof ESCAPE_HATCH_GRANTS)[number];
3873
+ type EscapeHatchReporter = (kind: EscapeHatchKind, reason: string, target?: EscapeHatchTarget) => void;
3874
+ type EscapeHatchTarget = {
3875
+ id: string;
3876
+ tenantId: TenantId;
3877
+ };
3878
+ type EventMigrationDef = {
3879
+ eventName: string;
3880
+ fromVersion: number;
3881
+ toVersion: number;
3882
+ transform: EventUpcastFn;
3883
+ };
3884
+ type EventPiiAbsentSubject = "plaintext" | "tenant" | "self";
3885
+ type EventPiiFields = Readonly<Record<string, EventPiiSubject>>;
3886
+ type EventPiiStance = EventPiiFields | "none";
3887
+ type EventPiiSubject =
3888
+ | {
3889
+ personal: {
3890
+ of: string;
3891
+ whenAbsent?: EventPiiAbsentSubject;
3892
+ };
3893
+ }
3894
+ | {
3895
+ personal: "tenant";
3896
+ }
3897
+ | {
3898
+ personal: "self";
3899
+ }
3900
+ | {
3901
+ subjectField: string;
3902
+ }
3903
+ ;
3904
+ type EventUpcastCtx = {
3905
+ db: DbRunner;
3906
+ tenantId: TenantId;
3907
+ };
3908
+ type EventUpcastFn = (payload: unknown, ctx: EventUpcastCtx) => unknown | Promise<unknown>;
3909
+ type FetchForWritingArgs = {
3910
+ aggregateId: string;
3911
+ aggregateType: string;
3912
+ expectedVersion?: number;
3913
+ };
3914
+ type HandlerRef = {
3915
+ name: string;
3916
+ };
3917
+ type JobRunnerRef = {
3918
+ handleEvent(eventName: string, payload: Record<string, unknown>, user?: SessionUser, triggerEvent?: JobTriggerEvent): Promise<void>;
3919
+ handleCatchUpEvent?(jobName: string, eventName: string, payload: Record<string, unknown>, user: SessionUser, triggerEvent: JobTriggerEvent): Promise<void>;
3920
+ dispatch?(jobName: string, payload: Record<string, unknown>, meta?: { priority?: number; jobId?: string; }): Promise<unknown>;
3921
+ };
3922
+ type JobTriggerEvent = {
3923
+ id: string;
3924
+ headers: NonNullable<EventMetadata["headers"]>;
3925
+ };
3926
+ type MemberReader = (userId: string, qn: string, payload: unknown) => Promise<unknown>;
3927
+ type NameOrRef =
3928
+ | string
3929
+ | {
3930
+ name: string;
3931
+ }
3932
+ ;
3933
+ type NotifyDeferred = {
3934
+ kind: "deferred";
3935
+ };
3936
+ type OpenToAllAccessRule = {
3937
+ openToAll: OpenToAllDeclaration;
3938
+ };
3939
+ type OpenToAllDeclaration = {
3940
+ reason: string;
3941
+ personalData?: OpenToAllPersonalData;
3942
+ };
3943
+ type OpenToAllPersonalData = "tenant-members";
3944
+ type PayloadBucketNormalization = "email";
3945
+ type PayloadRateLimitOption = {
3946
+ per: RateLimitPayloadBucket;
3947
+ limit: number;
3948
+ windowSeconds: number;
3949
+ cost?: number;
3950
+ normalize?: PayloadBucketNormalization;
3951
+ };
3952
+ type PreTransactionReservation = (event: WriteEvent, ctx: ReservationContext) => Promise<ReservationRelease | ReservationHandle | undefined>; // Runs in the dispatcher after every pre-handler gate (feature, rate limit, access, schema, validation, field roles) passed but before the handler transaction opens, so it must commit its own short write instead of holding a second connection while the transaction is open.
3953
+ type PreparedOrSkipped<TPrepared, TWhenNested> = [ TWhenNested ] extends [ BeforeTransactionWhenNested ] ? TPrepared | undefined : TPrepared; // The `prepared` type a handler sees: with `beforeTransactionWhenNested: "skip"` a nested run leaves it undefined.
3954
+ type PreparedWriteEvent<TPayload = unknown, TPrepared = unknown> = WriteEvent<TPayload> & ([ TPrepared ] extends [ undefined ] ? { prepared?: undefined; } : { prepared: TPrepared; }); // What a handler sees when its `beforeTransaction` phase ran: the phase's `data` as `prepared`; absent for handlers without the phase.
3955
+ type QualifiedEventName<TFeature extends string, TInner extends string> = `${CamelToKebab<TFeature>}:event:${CamelToKebab<TInner>}`; // Builds the qualified event-name from feature + inner-name in the same shape the runtime emits via `qn(toKebab(feature), "event", toKebab(inner))`.
3956
+ type QueryHandlerFn<TPayload = unknown, TResult = unknown> = (query: QueryEvent<TPayload>, context: HandlerContext) => Promise<TResult>;
3957
+ type RateLimitDeclaration = RateLimitOption | RateLimitOptions | RateLimitDisabled;
3958
+ type RateLimitDisabled = {
3959
+ disabled: true;
3960
+ reason: string;
3961
+ };
3962
+ type RateLimitOption = {
3963
+ per: RateLimitPer;
3964
+ limit: number;
3965
+ windowSeconds: number;
3966
+ cost?: number;
3967
+ };
3968
+ type RateLimitOptions = [ RateLimitOption, ...RateLimitOption[] ];
3969
+ type RateLimitPayloadBucket = {
3970
+ payloadField: string;
3971
+ };
3972
+ type ReservationConfirmContext = { // The dispatcher's own open handler transaction: a reservation must settle on it, not on HandlerContext, whose db is fail-closed for r.systemScope() handlers.
3973
+ tx: DbTx;
3974
+ tenantId: TenantId;
3975
+ };
3976
+ type ReservationHandle = { // A reservation that also settles inside the handler transaction.
3977
+ release: ReservationRelease;
3978
+ confirmInTransaction: (confirm: ReservationConfirmContext) => Promise<void>;
3979
+ };
3980
+ type ReservationRelease = () => Promise<void>; // Undoes a reservation made before the handler transaction.
3981
+ type SessionUserOrigin = "member-resolution";
3982
+ type SharedContextFields = {
3983
+ redis?: Redis;
3984
+ cacheSync?: CacheSyncBus;
3985
+ jobRunner?: JobRunnerRef;
3986
+ configResolver?: ConfigResolver;
3987
+ config?: ConfigAccessor;
3988
+ configFor?: (tenantId: TenantId) => ConfigAccessor; // Becomes `ctx.tenants.config`.
3989
+ forTenant?: (tenantId: TenantId) => TenantWriteTarget; // Becomes `ctx.tenants.writeTarget`.
3990
+ configAccessorFactory?: ConfigAccessorFactory; // Mints the per-request `ctx.config`; set by the boot (`addConfigAccessorFactory`) from the effective `configResolver`, overriding an app-supplied value.
3991
+ configEncryption?: EnvelopeCipher;
3992
+ rateLimit?: RateLimitResolver;
3993
+ _rateLimitPayloadDigest?: (value: string) => string;
3994
+ searchAdapter?: SearchAdapter;
3995
+ files?: FileContext;
3996
+ derivatives?: DerivativesContext;
3997
+ _fileProviderResolver?: FileProviderResolver;
3998
+ entityCache?: EntityCache;
3999
+ notify?: NotifyFn;
4000
+ _notifyFactory?: NotifyFactory;
4001
+ _notifyInTransactionFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
4002
+ _notifyAfterCommitFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
4003
+ _escapeHatchAuditSink?: EscapeHatchAuditSink;
4004
+ secrets?: SecretsContext;
4005
+ masterKeyProvider?: MasterKeyProvider;
4006
+ kms?: KmsAdapter;
4007
+ tracer?: Tracer;
4008
+ meter?: Meter;
4009
+ signal?: AbortSignal;
4010
+ effectiveFeatures?: (tenantId: TenantId) => ReadonlySet<string>;
4011
+ scheduleAfterCommit?: (hook: () => Promise<void>) => void; // Becomes `ctx.lifecycle.afterCommit`.
4012
+ scheduleAfterRollback?: (hook: () => Promise<void>) => void; // Becomes `ctx.lifecycle.afterRollback`.
4013
+ };
4014
+ type StreamHandlerFn<TPayload = unknown, TChunk = unknown> = (query: QueryEvent<TPayload>, context: HandlerContext) => AsyncGenerator<TChunk>;
4015
+ type TryAppendEventFn = (args: AppendEventArgs) => Promise<TryAppendEventResult>;
4016
+ type TryAppendEventResult =
4017
+ | {
4018
+ ok: true;
4019
+ event: StoredEvent;
4020
+ }
4021
+ | {
4022
+ ok: false;
4023
+ conflict: VersionConflict;
4024
+ }
4025
+ ;
4026
+ type TypedAppendEventArgs<TMap extends object, K extends keyof TMap> = {
4027
+ aggregateId: string;
4028
+ aggregateType: string;
4029
+ type: K;
4030
+ payload: TMap[K];
4031
+ headers?: Readonly<Record<string, string | number | boolean>>;
4032
+ };
4033
+ type UnsafeAppendEventFn = (args: AppendEventArgs) => Promise<void>;
4034
+ type WriteHandlerTransactionMode = "perCommand"; // `perCommand`: the handler runs without a transaction of its own.
4035
+ ```
4036
+
4037
+ ## packages/types/src/hook-phase.ts
4038
+
4039
+ ```ts
4040
+ type HookPhase = "inTransaction" | "afterCommit";
4041
+ ```
4042
+
4043
+ ## packages/types/src/hooks.ts
4044
+
4045
+ ```ts
4046
+ type EntityHookMap = {
4047
+ postSave?: Readonly<Record<string, PhasedHook<PostSaveHookFn>[]>>;
4048
+ preDelete?: Readonly<Record<string, PhasedHook<PreDeleteHookFn>[]>>;
4049
+ postDelete?: Readonly<Record<string, PhasedHook<PostDeleteHookFn>[]>>;
4050
+ postQuery?: Readonly<Record<string, OwnedFn<PostQueryHookFn>[]>>;
4051
+ };
4052
+ type HookMap = {
4053
+ validation?: Readonly<Record<string, ValidationHookFn>>;
4054
+ preSave?: Readonly<Record<string, OwnedFn<PreSaveHookFn>[]>>;
4055
+ postSave?: Readonly<Record<string, PhasedHook<PostSaveHookFn>[]>>;
4056
+ preDelete?: Readonly<Record<string, PhasedHook<PreDeleteHookFn>[]>>;
4057
+ postDelete?: Readonly<Record<string, PhasedHook<PostDeleteHookFn>[]>>;
4058
+ postQuery?: Readonly<Record<string, OwnedFn<PostQueryHookFn>[]>>;
4059
+ };
4060
+ type HookOwner = {
4061
+ featureName?: string;
4062
+ };
4063
+ type OwnedFn<TFn> = HookOwner & {
4064
+ fn: TFn;
4065
+ };
4066
+ type PhasedHook<TFn> = HookOwner & {
4067
+ fn: TFn;
4068
+ phase: HookPhase;
4069
+ };
4070
+ type PostDeleteBatchHookFn = (payloads: DeleteContext[], context: HandlerContext) => Promise<void>;
4071
+ type PostQueryHookFn = (result: { entityName: string | undefined; rows: ReadonlyArray<Record<string, unknown>>; }, context: HandlerContext) => Promise<{ rows: ReadonlyArray<Record<string, unknown>>; }>;
4072
+ type PostSaveBatchHookFn = (results: SaveContext[], context: HandlerContext) => Promise<void>;
4073
+ type SearchPayloadContributorFn = (args: { entityName: string; entityId: EntityId; state: Record<string, unknown>; }) => Record<string, unknown> | Promise<Record<string, unknown>>;
4074
+ ```
4075
+
4076
+ ## packages/types/src/http-route.ts
4077
+
4078
+ ```ts
4079
+ type HttpRouteDefinition = {
4080
+ method: HttpRouteMethod; // HTTP method — mounted in Hono via app.{get,post,...}(path).
4081
+ path: string; // URL path (Hono pattern, e.g. "/feed.xml" or "/og/:tenantId.png").
4082
+ anonymous: boolean; // true = public, no session required.
4083
+ handler: HttpRouteHandler; // Hono handler.
4084
+ rateLimit?: HttpRouteRateLimit; // Enforced before the route's guards through the server's rate-limit resolver (fail-open like the global L1 limiter: a limiter outage lets the request through, counted and logged).
4085
+ };
4086
+ type HttpRouteHandler = (c: Context<any, any>, deps: HttpRouteHandlerDeps) => Response | Promise<Response>;
4087
+ type HttpRouteHandlerDeps = { // Dependencies provided to the handler by the framework.
4088
+ app: Hono<any, any>; // The Hono app — the handler can use app.fetch(...) to call internal routes (e.g. /api/query with the full auth/anonymous chain).
4089
+ systemQuery: (type: string, payload: unknown, tenantId: TenantId) => Promise<unknown>; // Run a query handler in-process, forcing a SPECIFIC tenant — WITHOUT going through the public /api/query HTTP layer (no header parsing, no anonymousAccess tenant resolution).
4090
+ clientIp: string; // Caller IP resolved via the server's configured, trustedProxyHops-aware resolver (same one requestIdMiddleware uses for `/api/*`) — a route handler needing the caller's IP (rate limiting, audit) should read this instead of parsing X-Forwarded-For itself, which trusts whichever hop count the deployment actually has in front of it.
4091
+ };
4092
+ type HttpRouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"; // Subset of HTTP methods actively supported.
4093
+ type HttpRouteRateLimit = Omit<RateLimitOption, "per"> & { // Same shape as a handler's `rateLimit`, restricted to the IP-keyed buckets: a route has no dispatcher identity to key on.
4094
+ per: "ip" | "ip+handler";
4095
+ };
4096
+ ```
4097
+
4098
+ ## packages/types/src/kms-adapter-types.ts
4099
+
4100
+ ```ts
4101
+ type KmsAdapter = LocalKeyKmsAdapter | RemoteCryptoKmsAdapter;
4102
+ interface KmsAdapterBase {
4103
+ createKey(subject: SubjectId, ctx: KmsContext): Promise<void>; // Creates a fresh subject key.
4104
+ eraseKey(subject: SubjectId, ctx: KmsContext): Promise<void>; // Erases the key material immediately; the tombstone row stays for the audit trail.
4105
+ health(): Promise<KmsHealth>; // Probe for boot + readiness.
4106
+ }
4107
+ interface KmsContext {
4108
+ tenantId?: TenantId;
4109
+ requestId: string;
4110
+ userId?: string;
4111
+ eraseReason?: string;
4112
+ }
4113
+ interface KmsHealth {
4114
+ ok: boolean;
4115
+ latencyMs: number;
4116
+ details?: Record<string, unknown>;
4117
+ }
4118
+ interface LocalKeyKmsAdapter extends KmsAdapterBase {
4119
+ capabilities: {
4120
+ mode: "local-key";
4121
+ };
4122
+ getKey(subject: SubjectId, ctx: KmsContext): Promise<SubjectDek>; // Throws KeyErasedError after eraseKey (callers render "[[erased]]"), KeyNotFoundError when the subject never had a key (typically a bug).
4123
+ }
4124
+ interface RemoteCryptoKmsAdapter extends KmsAdapterBase {
4125
+ capabilities: {
4126
+ mode: "remote-crypto";
4127
+ };
4128
+ encrypt(subject: SubjectId, plaintext: Uint8Array, ctx: KmsContext): Promise<Uint8Array>;
4129
+ decrypt(subject: SubjectId, ciphertext: Uint8Array, ctx: KmsContext): Promise<Uint8Array>; // Same error contract as LocalKeyKmsAdapter.getKey.
4130
+ }
4131
+ type SubjectDek = Buffer;
4132
+ type SubjectId =
4133
+ | {
4134
+ kind: "user";
4135
+ userId: string;
4136
+ }
4137
+ | {
4138
+ kind: "tenant";
4139
+ tenantId: TenantId;
4140
+ }
4141
+ | {
4142
+ kind: "record";
4143
+ entity: string;
4144
+ id: string;
4145
+ }
4146
+ ;
4147
+ ```
4148
+
4149
+ ## packages/types/src/list-aggregate-types.ts
4150
+
4151
+ ```ts
4152
+ type ListAggregateCurrencyAmount = {
4153
+ currency: string;
4154
+ value: number;
4155
+ };
4156
+ type ListAggregateGroup = {
4157
+ key: AggregateKey;
4158
+ label?: string | null;
4159
+ count: number;
4160
+ measures: ListAggregateValue[];
4161
+ };
4162
+ type ListAggregateMeasureKind = (typeof LIST_AGGREGATE_MEASURE_KINDS)[number];
4163
+ type ListAggregatePayload = {
4164
+ groupBy?: string | undefined;
4165
+ measures: { field: string; kind: ListAggregateMeasureKind; }[];
4166
+ };
4167
+ type ListAggregateResult = {
4168
+ groups: ListAggregateGroup[];
4169
+ truncated?: true;
4170
+ searchTruncated?: true;
4171
+ };
4172
+ type ListAggregateValue = number | string | null | ListAggregateCurrencyAmount[];
4173
+ ```
4174
+
4175
+ ## packages/types/src/logger.ts
4176
+
4177
+ ```ts
4178
+ type Logger = {
4179
+ info(msg: string, data?: Record<string, unknown>): void;
4180
+ warn(msg: string, data?: Record<string, unknown>): void;
4181
+ error(msg: string, data?: Record<string, unknown>): void;
4182
+ debug(msg: string, data?: Record<string, unknown>): void;
4183
+ child(context: Record<string, unknown>): Logger;
4184
+ };
4185
+ ```
4186
+
4187
+ ## packages/types/src/multi-stream-apply-context-types.ts
4188
+
4189
+ ```ts
4190
+ type MultiStreamApplyContext<TMap extends object = KumikoEventTypeMap> = {
4191
+ appendEvent: AppendEventFn<TMap>;
4192
+ unsafeAppendEvent: UnsafeAppendEventFn;
4193
+ loadAggregate: (aggregateId: string, options?: { asOf?: Temporal.Instant; }) => Promise<StoredEvent[]>;
4194
+ registry: Registry;
4195
+ files?: FileContext;
4196
+ derivatives?: DerivativesContext;
4197
+ secrets?: SecretsContext;
4198
+ jobs?: {
4199
+ dispatch(jobName: string, payload: Record<string, unknown>, options?: { jobId?: string; }): Promise<void>;
4200
+ };
4201
+ };
4202
+ ```
4203
+
4204
+ ## packages/types/src/nav.ts
4205
+
4206
+ ```ts
4207
+ type ContentEditorFormat = "plain" | "document"; // Client editor format for r.contentCollection() — not template body storage formats.
4208
+ ```
4209
+
4210
+ ## packages/types/src/observability/metric.ts
4211
+
4212
+ ```ts
4213
+ interface Counter {
4214
+ inc(value?: number, labels?: MetricLabels): void;
4215
+ }
4216
+ interface Gauge {
4217
+ set(value: number, labels?: MetricLabels): void;
4218
+ inc(value?: number, labels?: MetricLabels): void;
4219
+ dec(value?: number, labels?: MetricLabels): void;
4220
+ }
4221
+ interface Histogram {
4222
+ observe(value: number, labels?: MetricLabels): void;
4223
+ }
4224
+ interface Meter {
4225
+ registerMetric(def: MetricDefinition): void;
4226
+ counter(name: string): Counter;
4227
+ histogram(name: string): Histogram;
4228
+ gauge(name: string): Gauge;
4229
+ definitions(): ReadonlyMap<string, MetricDefinition>;
4230
+ }
4231
+ type MetricDefinition = {
4232
+ name: string;
4233
+ type: MetricType;
4234
+ description?: string;
4235
+ labels?: string[];
4236
+ buckets?: number[];
4237
+ unit?: string;
4238
+ tenantLabel?: boolean;
4239
+ };
4240
+ type MetricLabels = Record<string, string | number | boolean>;
4241
+ type MetricType = "counter" | "histogram" | "gauge";
4242
+ interface MetricsHandle {
4243
+ inc(name: string, labels?: MetricLabels, value?: number): void;
4244
+ observe(name: string, value: number, labels?: MetricLabels): void;
4245
+ set(name: string, value: number, labels?: MetricLabels): void;
4246
+ }
4247
+ ```
4248
+
4249
+ ## packages/types/src/observability/provider.ts
4250
+
4251
+ ```ts
4252
+ type ObservabilityOptions = {
4253
+ sampling?: SamplingConfig;
4254
+ sensitiveFilter?: Partial<SensitiveFilterConfig>;
4255
+ };
4256
+ interface ObservabilityProvider {
4257
+ name: string;
4258
+ tracer: Tracer;
4259
+ meter: Meter;
4260
+ shutdown(): Promise<void>;
4261
+ }
4262
+ type SamplingConfig = {
4263
+ tracing?: number;
4264
+ alwaysOnError?: boolean;
4265
+ alwaysOnSlow?: {
4266
+ thresholdMs: number;
4267
+ };
4268
+ };
4269
+ type SensitiveFilterConfig = {
4270
+ redactedHeaders: string[];
4271
+ redactedQueryParams: string[];
4272
+ redactedAttributeKeyPatterns: RegExp[];
4273
+ };
4274
+ ```
4275
+
4276
+ ## packages/types/src/observability/span.ts
4277
+
4278
+ ```ts
4279
+ type SerializedTraceContext = {
4280
+ traceId: string;
4281
+ spanId: string;
4282
+ baggage?: Readonly<Record<string, string>>;
4283
+ };
4284
+ interface Span {
4285
+ traceId: string;
4286
+ spanId: string;
4287
+ parentSpanId: string | undefined;
4288
+ name: string;
4289
+ setAttribute(key: string, value: SpanAttributeValue): void;
4290
+ setAttributes(attrs: SpanAttributes): void;
4291
+ setStatus(status: SpanStatus, message?: string): void;
4292
+ recordException(error: Error): void;
4293
+ end(endTime?: number): void;
4294
+ ended: boolean;
4295
+ }
4296
+ type SpanAttributeValue = string | number | boolean;
4297
+ type SpanAttributes = Record<string, SpanAttributeValue>;
4298
+ type SpanKind = "internal" | "server" | "client" | "producer" | "consumer";
4299
+ type SpanStatus = "unset" | "ok" | "error";
4300
+ type StartSpanOptions = {
4301
+ parent?: Span | SerializedTraceContext;
4302
+ attributes?: SpanAttributes;
4303
+ kind?: SpanKind;
4304
+ startTime?: number;
4305
+ };
4306
+ interface Tracer {
4307
+ startSpan(name: string, options?: StartSpanOptions): Span;
4308
+ withSpan<T>(name: string, optionsOrFn: StartSpanOptions | ((span: Span) => Promise<T>), fn?: (span: Span) => Promise<T>): Promise<T>;
4309
+ getActiveSpan(): Span | undefined;
4310
+ }
4311
+ ```
4312
+
4313
+ ## packages/types/src/ownership.ts
4314
+
4315
+ ```ts
4316
+ type FromRule = {
4317
+ kind: "from";
4318
+ refKind: FromRuleKind;
4319
+ refPath: string;
4320
+ column: string;
4321
+ };
4322
+ type FromRuleKind = "user" | "claim";
4323
+ type OwnershipRef = string;
4324
+ type OwnershipRule = "all" | FromRule | WhereRule;
4325
+ type SqlFragment = {
4326
+ sqlText: string;
4327
+ params: unknown[];
4328
+ };
4329
+ type WhereRule<TTable = unknown> = {
4330
+ kind: "where";
4331
+ where: (user: SessionUser, ctx: WhereRuleContext<TTable>) => SqlFragment;
4332
+ };
4333
+ type WhereRuleContext<TTable = unknown> = {
4334
+ table: TTable;
4335
+ tableName: string;
4336
+ paramStart: number;
4337
+ };
4338
+ ```
4339
+
4340
+ ## packages/types/src/projection.ts
4341
+
4342
+ ```ts
4343
+ type EntityProjectionExtension = {
4344
+ sources?: string[];
4345
+ rowIdOf?: ProjectionRowIdOf;
4346
+ apply: Readonly<Record<string, SingleStreamApplyFn>>;
4347
+ };
4348
+ type MspErrorPolicy = {
4349
+ skipApplyErrors?: boolean;
4350
+ };
4351
+ type ProjectionRowIdOf = (source: Pick<StoredEvent, "tenantId" | "aggregateId">) => string;
4352
+ type ProjectionTable = SchemaTable;
4353
+ type SingleStreamApplyFn<TPayload = Record<string, unknown>> = (event: StoredEvent<TPayload>, tx: ExecutorManagedRunner, table: ProjectionTable) => Promise<void>;
4354
+ ```
4355
+
4356
+ ## packages/types/src/rate-limit-types.ts
4357
+
4358
+ ```ts
4359
+ type RateLimitBucketRequest = {
4360
+ bucket: string;
4361
+ config: RateLimitConfig;
4362
+ };
4363
+ type RateLimitConfig = {
4364
+ limit: number;
4365
+ windowSeconds: number;
4366
+ cost?: number;
4367
+ };
4368
+ type RateLimitDecision = {
4369
+ allowed: boolean;
4370
+ limit: number;
4371
+ remaining: number;
4372
+ retryAfterSeconds: number;
4373
+ windowSeconds: number;
4374
+ resetAt: Temporal.Instant;
4375
+ };
4376
+ type RateLimitResolver = {
4377
+ check(bucket: string, config: RateLimitConfig): Promise<RateLimitDecision>;
4378
+ enforce(bucket: string, config: RateLimitConfig): Promise<RateLimitDecision>;
4379
+ enforceAll(requests: RateLimitBucketRequest[]): Promise<RateLimitDecision[]>;
4380
+ peek(bucket: string, config: Omit<RateLimitConfig, "cost">): Promise<RateLimitDecision>;
4381
+ };
4382
+ ```
4383
+
4384
+ ## packages/types/src/relations.ts
4385
+
4386
+ ```ts
4387
+ type BelongsToRelation = {
4388
+ type: "belongsTo";
4389
+ target: string;
4390
+ foreignKey: string;
4391
+ };
4392
+ type EntityRelations = Readonly<Record<string, RelationDefinition>>;
4393
+ type HasManyRelation = {
4394
+ type: "hasMany";
4395
+ target: string;
4396
+ foreignKey: string;
4397
+ onDelete?: OnDeleteStrategy;
4398
+ nestedWrite?: boolean;
4399
+ };
4400
+ type ManyToManyRelation = {
4401
+ type: "manyToMany";
4402
+ target: string;
4403
+ through: {
4404
+ table: string;
4405
+ sourceKey: string;
4406
+ targetKey: string;
4407
+ };
4408
+ onDelete?: OnDeleteStrategy;
4409
+ };
4410
+ type OnDeleteStrategy = "cascade" | "restrict" | "setNull" | "nothing";
4411
+ ```
4412
+
4413
+ ## packages/types/src/schema-table-types.ts
4414
+
4415
+ ```ts
4416
+ type ColumnHandle = {
4417
+ name: string;
4418
+ pgType: PgType;
4419
+ getSQLType: () => string;
4420
+ };
4421
+ ```
4422
+
4423
+ ## packages/types/src/screen-action-form.ts
4424
+
4425
+ ```ts
4426
+ type FormFieldDerivation = // A form field computed from other fields of the same form.
4427
+ | {
4428
+ op: "sum" | "subtract" | "multiply" | "percent";
4429
+ from: string[];
4430
+ }
4431
+ | {
4432
+ op: "listSum";
4433
+ list: string;
4434
+ cell: string;
4435
+ whenEmpty?: FormFieldDerivationWithoutList;
4436
+ }
4437
+ ;
4438
+ type FormFieldDerivationWithoutList = Extract<FormFieldDerivation, { from: unknown; }>;
4439
+ type SecretReveal = {
4440
+ fields: SecretRevealField[];
4441
+ title?: string; // i18n key.
4442
+ warning?: string; // i18n key for the "this is the only time you see it" warning.
4443
+ confirmLabel?: string; // i18n key for the acknowledge button.
4444
+ acknowledge?: string; // i18n key for a checkbox on the reveal card ("I have saved my recovery codes").
4445
+ };
4446
+ type SecretRevealField = { // Author-declared field shown once after a mint-form submit — API token, recovery codes, similar one-time secrets.
4447
+ field: string; // Flat field name in the handler's success payload.
4448
+ label: string; // i18n key for this value's label.
4449
+ display?: "code" | "list" | "qr"; // "code" (default) renders one monospaced value; "list" renders a string-array payload one value per line (recovery codes); "qr" renders the value as a scannable QR code (otpauth://-style enrollment URI) — platforms without a QR-capable SecretReveal primitive fall back to the monospaced text display.
4450
+ copyable?: boolean; // Default true — offer a copy-to-clipboard control next to the value.
4451
+ };
4452
+ ```
4453
+
4454
+ ## packages/types/src/screen-custom-config.ts
4455
+
4456
+ ```ts
4457
+ type CustomScreenRoute = {
4458
+ path: string;
4459
+ component: PlatformComponent;
4460
+ };
4461
+ type SecretsEditSection = {
4462
+ title?: string;
4463
+ fields: string[];
4464
+ };
4465
+ ```
4466
+
4467
+ ## packages/types/src/screen-dashboard.ts
4468
+
4469
+ ```ts
4470
+ type DashboardChartKind = "timeseries" | "stacked-bars" | "segment-bars" | "stacked-area";
4471
+ type DashboardChartRangeOption = { // One option of a stacked-area range switch.
4472
+ value: string;
4473
+ label: string;
4474
+ months?: number;
4475
+ };
4476
+ type DashboardChartRanges = {
4477
+ options: DashboardChartRangeOption[];
4478
+ default: string;
4479
+ };
4480
+ type DashboardChartTone = "positive" | "negative" | "active" | "neutral";
4481
+ type DashboardPanelGate = {
4482
+ visibleWhen?: DashboardPanelVisibility;
4483
+ };
4484
+ type DashboardRecordBinding = {
4485
+ param: string; // URL search param carrying the record id.
4486
+ payloadKey?: string; // Key of the id in each panel query payload.
4487
+ query?: string; // Optional query for the record row (QN).
4488
+ titleField?: string; // Field of the record row shown as the page title.
4489
+ };
4490
+ type DashboardRelativeTimeFormat = { // The stat value is an epoch ms timestamp, shown relative to now and kept current.
4491
+ kind: "relativeTime";
4492
+ };
4493
+ type DashboardStatTone = (typeof DASHBOARD_STAT_TONES)[number];
4494
+ type DashboardValueFormat = DashboardCurrencyFormat | DashboardRelativeTimeFormat; // Stat panels take both formats; chart panels only the currency one.
4495
+ ```
4496
+
4497
+ ## packages/types/src/screen-entity-edit.ts
4498
+
4499
+ ```ts
4500
+ type EditFieldGroup = {
4501
+ title: string;
4502
+ fields: EditFieldSpec[];
4503
+ columns?: number; // Default 2.
4504
+ };
4505
+ type EditFieldPrefill =
4506
+ | {
4507
+ kind: "today";
4508
+ }
4509
+ | {
4510
+ kind: "currentUser";
4511
+ property: "displayName" | "email" | "id";
4512
+ }
4513
+ ;
4514
+ type EditFieldsSectionBase = {
4515
+ id?: string; // Kebab-case tab id, used as the `?tab=` value.
4516
+ kind?: "fields";
4517
+ title?: string; // Optional.
4518
+ description?: string; // Optional help text under the block heading (i18n key or raw string, like `title`) — makes a form usable without training instead of falling back to custom JSX for help text.
4519
+ columns?: number;
4520
+ icon?: IconKey; // Rendered left of the section title, `text-muted-foreground` — closed IconKey vocabulary into the ICONS registry (renderer/web), analogous to EditFieldSpec.icon.
4521
+ countField?: string; // Record field rendered as a count badge in the tab label when the enclosing `EditLayout.mode` is "tabs" (e.g. an open-items counter).
4522
+ subtitle?: string; // Short line (i18n key) under this step's title in the wizard step rail and in the "up next" box.
4523
+ actions?: RowAction[]; // Rendered top-right in the section's Card title row — same navigate/drawer/writeHandler dispatch as `ProjectionDetailScreenDefinition.actions`, evaluated against the same record.
4524
+ };
4525
+ type EntityEditActionPlacement = "inline" | "menu";
4526
+ type EntityWriteExcludedFields = {
4527
+ create?: string[];
4528
+ update?: string[];
4529
+ };
4530
+ ```
4531
+
4532
+ ## packages/types/src/screen-entity-list.ts
4533
+
4534
+ ```ts
4535
+ type EntityListFacetExtraOption = { // An extra choice on a facet next to the field's own options: a named bundle of values ("Running" = several statuses) or "no filter" (`values: []`, e.g. "All").
4536
+ id: string;
4537
+ label: string; // i18n key.
4538
+ values: string[]; // Option values this choice selects; `[]` selects nothing, i.e. no filter.
4539
+ position?: "start" | "end"; // Where the chip sits relative to the field's own options.
4540
+ };
4541
+ type ListColumnDisplay = "bar" | "badge" | "datetime";
4542
+ type ListPaginationMode = "pages" | "infinite" | false;
4543
+ type ListSortDir = "asc" | "desc";
4544
+ type RowActionNavigateBase = {
4545
+ kind: "navigate";
4546
+ id: string;
4547
+ label: string;
4548
+ entityId?: string; // Field name whose value is embedded as entityId in the URL path (`/<workspace>/<screen>/<entityId>`).
4549
+ params?: RowFieldExtractor; // Declarative URL search params extracted from the clicked row.
4550
+ tab?: string; // Section id of the tab to activate on the target projectionDetail (layout.mode "tabs"); merged into the navigate search params next to returnTo.
4551
+ visible?: FieldCondition; // Conditional visibility per row.
4552
+ style?: "primary" | "secondary" | "danger"; // Visual style only — unlike the writeHandler variants, "danger" does not force a confirm dialog here: the target form is the confirmation.
4553
+ rowClick?: boolean; // When true, a click on the whole row (not just the action menu) triggers this navigate action.
4554
+ icon?: IconKey; // Overrides the id-based default icon (see ACTION_ICON_BY_ID in kumiko-renderer) — closed IconKey vocabulary into the ICONS registry (renderer/web), analogous to EditFieldSpec.icon.
4555
+ display?: RowActionDisplay; // Inline render form in the row-action column — see `RowActionDisplay`.
4556
+ };
4557
+ type ScreenFilter = {
4558
+ field: string;
4559
+ op: ScreenFilterOp;
4560
+ value: unknown;
4561
+ };
4562
+ type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
4563
+ ```
4564
+
4565
+ ## packages/types/src/screen-expandable-row.ts
4566
+
4567
+ ```ts
4568
+ type EntityListExpandableRow = Omit<EditRelatedListSection, "id" | "countField" | "table" | "bulkActions"> & { // Expandable area of an `entityList` row: a relatedList of the row's record.
4569
+ table?: {
4570
+ grouping?: ExpandableRowGrouping;
4571
+ };
4572
+ };
4573
+ type ExpandableRowGrouping = Omit<ListTableGrouping, "field" | "userChoice" | "fields"> & { // Server-side grouping of an expandable row's sub-list: a fixed `field`, no "Group by" choice.
4574
+ field: string;
4575
+ };
4576
+ ```
4577
+
4578
+ ## packages/types/src/screen-list-table.ts
4579
+
4580
+ ```ts
4581
+ type ListColumnPin = (typeof LIST_COLUMN_PINS)[number];
4582
+ type ListTableDensity = (typeof LIST_TABLE_DENSITIES)[number];
4583
+ type ListTableExportOptions = {
4584
+ xlsx: true;
4585
+ maxRows?: number; // Export is refused above this many rows.
4586
+ };
4587
+ type ListTableGrouping = {
4588
+ field?: string; // Start grouping.
4589
+ collapsedWhen?: string | number | boolean | null; // Groups whose key equals this start collapsed and load no rows until opened.
4590
+ label?: string; // i18n key of the group header.
4591
+ labels?: Readonly<Record<string, string>>; // Header i18n key per group key (as string), overriding `label`.
4592
+ userChoice?: boolean; // Shows "Group by" in the view menu (needs `fields` or `field`) and remembers the choice per session.
4593
+ fields?: string[]; // Fields offered under "Group by"; `field` is always offered.
4594
+ };
4595
+ type ListTableTotal = {
4596
+ field: string;
4597
+ kind: ListAggregateMeasureKind; // sum and avg on number, decimal, money and bigInt; min and max also on date and timestamp.
4598
+ };
4599
+ ```
4600
+
4601
+ ## packages/types/src/screen-projection.ts
4602
+
4603
+ ```ts
4604
+ type MetricSpec = {
4605
+ field: string;
4606
+ label: string;
4607
+ navigate?: MetricNavigate;
4608
+ tone?: DashboardStatTone; // Tile color as on dashboard stat panels; not applied while the value is 0.
4609
+ icon?: NavIconKey; // Icon chip of the tile; only drawn with `metricsStyle: "tiles"`.
4610
+ };
4611
+ type RecordHeaderSpec = {
4612
+ title: string;
4613
+ subtitle?: string | RecordHeaderSubtitlePart[]; // Field name, or several parts (field name or `{ field, navigate }`) joined by a separator; parts with an empty value are dropped.
4614
+ status?: string;
4615
+ statusFormat?: FormatSpec; // Formats the badge text (typically `enumOption` with a `keyPrefix`); tones still resolve on the raw value.
4616
+ subtitleHref?: string; // Record field holding an absolute http(s) URL.
4617
+ statusToneField?: string; // Record field whose value selects the `statusTones` entry, for a badge text that carries more than the state (e.g. "Terminated on 31.03.2027").
4618
+ statusTones?: {
4619
+ [statusValue: string]: SelectOptionTone;
4620
+ }; // Badge tone per `statusToneField` value (default: the `status` value); unlisted values fall back to the value heuristic.
4621
+ };
4622
+ ```
4623
+
4624
+ ## packages/types/src/screen.ts
4625
+
4626
+ ```ts
4627
+ type FormatSpec = { [K in keyof FieldFormatRegistry]: { format: K; } & FieldFormatRegistry[K]; }[keyof FieldFormatRegistry];
4628
+ type NumberFormatOptions = {
4629
+ locale?: string;
4630
+ grouping?: boolean;
4631
+ minimumFractionDigits?: number; // Intl `minimumFractionDigits` / `maximumFractionDigits` (integers 0..20, min <= max).
4632
+ maximumFractionDigits?: number;
4633
+ };
4634
+ type ScreenNavSugar = {
4635
+ label: string;
4636
+ icon?: NavIconKey;
4637
+ parent?: string;
4638
+ order?: number;
4639
+ };
4640
+ ```
4641
+
4642
+ ## packages/types/src/search-adapter.ts
4643
+
4644
+ ```ts
4645
+ type SearchAdapter = {
4646
+ configure(tenantId: TenantId, config: SearchAdapterConfig): Promise<void>;
4647
+ setDefaultConfig?(config: SearchAdapterConfig): void;
4648
+ index(tenantId: TenantId, doc: SearchDocument): Promise<void>;
4649
+ search(tenantId: TenantId, query: string, options?: SearchOptions): Promise<SearchResult[]>;
4650
+ remove(tenantId: TenantId, entityType: string, entityId: EntityId): Promise<void>;
4651
+ indexBatch?(tenantId: TenantId, docs: SearchDocument[]): Promise<void>;
4652
+ removeBatch?(tenantId: TenantId, items: { entityType: string; entityId: EntityId; }[]): Promise<void>;
4653
+ dropAllIndexes?(): Promise<number>;
4654
+ };
4655
+ type SearchAdapterConfig = {
4656
+ searchableFields: string[];
4657
+ rankingFields?: string[];
4658
+ };
4659
+ type SearchDocument = {
4660
+ entityType: string;
4661
+ entityId: EntityId;
4662
+ weight: number;
4663
+ fields: Record<string, unknown>;
4664
+ };
4665
+ type SearchOptions = {
4666
+ limit?: number;
4667
+ filterType?: string;
4668
+ };
4669
+ type SearchResult = {
4670
+ entityType: string;
4671
+ entityId: EntityId;
4672
+ };
4673
+ ```
4674
+
4675
+ ## packages/types/src/secrets-types.ts
4676
+
4677
+ ```ts
4678
+ type ContainsSecret<T, Depth extends unknown[] = [ ]> = [ T ] extends [ never ] ? false : unknown extends T ? false : Depth["length"] extends MaxSecretDepth ? false : T extends Secret<unknown> ? true : T extends Primitive ? false : T extends SafeLeaf ? false : T extends (infer U)[] ? ContainsSecret<U, [ ...Depth, unknown ]> : T extends object ? true extends { [K in keyof T]-?: ContainsSecret<T[K], [ ...Depth, unknown ]>; }[keyof T] ? true : false : false;
4679
+ type KeyScope = {
4680
+ tenantId?: TenantId;
4681
+ };
4682
+ interface MasterKeyProvider {
4683
+ wrapDek(dek: Buffer, scope?: KeyScope): Promise<{ encryptedDek: Buffer; kekVersion: number; }>;
4684
+ unwrapDek(encryptedDek: Buffer, kekVersion: number, scope?: KeyScope): Promise<Buffer>;
4685
+ currentVersion(): number;
4686
+ isAvailable(): Promise<boolean>;
4687
+ }
4688
+ type MaxSecretDepth = 8;
4689
+ type Primitive = string | number | boolean | bigint | symbol | null | undefined;
4690
+ type SafeLeaf = Date | RegExp | Temporal.Instant | Temporal.ZonedDateTime | Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainTime | Temporal.PlainYearMonth | Temporal.PlainMonthDay | Temporal.Duration | Map<unknown, unknown> | Set<unknown>;
4691
+ type Secret<T = string> = {
4692
+ [SecretBrand]: true;
4693
+ reveal: () => T;
4694
+ };
4695
+ type SecretAuditContext = {
4696
+ userId: string;
4697
+ handlerName: string;
4698
+ };
4699
+ type SecretKeyRef =
4700
+ | string
4701
+ | {
4702
+ name: string;
4703
+ }
4704
+ ;
4705
+ interface SecretsContext {
4706
+ withRunner(runner: DbRunner): SecretsContext;
4707
+ get(tenantId: TenantId, key: SecretKeyRef, auditCtx?: SecretAuditContext): Promise<Secret<string> | undefined>;
4708
+ has(tenantId: TenantId, key: SecretKeyRef): Promise<boolean>;
4709
+ set(tenantId: TenantId, key: SecretKeyRef, value: string, opts?: { redact?: (plaintext: string) => string; hint?: string; updatedBy?: string; }): Promise<void>;
4710
+ delete(tenantId: TenantId, key: SecretKeyRef, opts?: { deletedBy?: string; }): Promise<boolean>;
4711
+ }
4712
+ ```
4713
+
4714
+ ## packages/types/src/step.ts
4715
+
4716
+ ```ts
4717
+ type EventMatch = {
4718
+ version: 1;
4719
+ expr: EventMatchExpr;
4720
+ };
4721
+ type EventMatchExpr =
4722
+ | {
4723
+ kind: "and";
4724
+ nodes: EventMatchExpr[];
4725
+ }
4726
+ | {
4727
+ kind: "or";
4728
+ nodes: EventMatchExpr[];
4729
+ }
4730
+ | {
4731
+ kind: "atom";
4732
+ path: string[];
4733
+ op: EventMatchOp;
4734
+ }
4735
+ ;
4736
+ type EventMatchOp =
4737
+ | {
4738
+ kind: "eq";
4739
+ value: EventMatchValue;
4740
+ }
4741
+ | {
4742
+ kind: "ne";
4743
+ value: EventMatchValue;
4744
+ }
4745
+ | {
4746
+ kind: "in";
4747
+ values: EventMatchValue[];
4748
+ }
4749
+ | {
4750
+ kind: "gt";
4751
+ value: number | string;
4752
+ }
4753
+ | {
4754
+ kind: "gte";
4755
+ value: number | string;
4756
+ }
4757
+ | {
4758
+ kind: "lt";
4759
+ value: number | string;
4760
+ }
4761
+ | {
4762
+ kind: "lte";
4763
+ value: number | string;
4764
+ }
4765
+ ;
4766
+ type EventMatchValue = string | number | boolean | null;
4767
+ type PipelineBuildCtx<TPayload = unknown, TAwaits extends Record<string, AwaitedEventType> = Record<string, AwaitedEventType>> = { // Argument bundle passed to the closure inside `stepsPipeline(closure)`.
4768
+ event: WriteEvent<TPayload>;
4769
+ r: StepBuilder;
4770
+ awaits: TAwaits;
4771
+ };
4772
+ type PipelineDef<TPayload = unknown, _TData = unknown, TAwaits extends Record<string, AwaitedEventType> = Record<string, AwaitedEventType>> = { // What `stepsPipeline(closure)` returns.
4773
+ __kind: "pipeline";
4774
+ awaits?: Readonly<Record<string, string>>;
4775
+ build: (ctx: PipelineBuildCtx<TPayload, TAwaits>) => StepInstance[];
4776
+ };
4777
+ type StepBuilder = { // Step-builder namespace handed to the pipeline closure.
4778
+ step: StepNamespace;
4779
+ };
4780
+ type StepDef<TArgs = unknown, TResult = unknown> = {
4781
+ kind: StepKind;
4782
+ defaultFailureStrategy: StepFailureStrategy;
4783
+ resultKey?: (args: TArgs) => string | undefined;
4784
+ subPaths?: string[];
4785
+ tier?: 1 | 2 | 3;
4786
+ run: (args: TArgs, ctx: PipelineCtx) => Promise<TResult> | TResult;
4787
+ };
4788
+ type StepFailureStrategy = "throw"; // Per-step error strategy.
4789
+ type StepKind = string; // The kind discriminator for a step instance — matches the step's registration name in the step-registry (e.g. "return", "compute", "aggregate.create").
4790
+ type StepNamespace = { // The collection of step factory functions.
4791
+ return: <TData>(resolver: StepResolver<WriteResult<TData>>) => StepInstance;
4792
+ compute: <TResult>(name: string, fn: (ctx: PipelineCtx) => TResult) => StepInstance;
4793
+ unsafeProjectionUpsert: (args: { table: unknown; on: string[]; row: StepResolver<Record<string, unknown>>; }) => StepInstance;
4794
+ unsafeProjectionDelete: (args: { table: unknown; where: StepResolver<WhereObject>; }) => StepInstance;
4795
+ read: {
4796
+ findOne: (name: string, opts: { table: unknown; where: StepResolver<WhereObject | undefined>; unsafeAllTenants?: { reason: string; }; }) => StepInstance;
4797
+ findMany: (name: string, opts: { table: unknown; where?: StepResolver<WhereObject | undefined>; limit?: number; unsafeAllTenants?: { reason: string; }; }) => StepInstance;
4798
+ };
4799
+ aggregate: {
4800
+ create: (name: string, opts: { executor: EventStoreExecutor; data: StepResolver<Record<string, unknown>>; }) => StepInstance;
4801
+ update: (name: string, opts: { executor: EventStoreExecutor; id: StepResolver<EntityId>; changes: StepResolver<Record<string, unknown>>; version?: StepResolver<number | undefined>; skipOptimisticLock?: boolean; }) => StepInstance;
4802
+ appendEvent: (args: { aggregateId: StepResolver<string>; aggregateType: string; type: string; payload: StepResolver<unknown>; headers?: StepResolver<Readonly<Record<string, string | number | boolean>>>; }) => StepInstance;
4803
+ };
4804
+ branch: (args: { if: StepResolver<boolean>; onTrue: StepInstance[]; onFalse?: StepInstance[]; }) => StepInstance;
4805
+ forEach: <TItem = unknown>(args: { over: StepResolver<TItem[]>; as: string; do: StepInstance[]; concurrency?: 1; }) => StepInstance;
4806
+ webhook: {
4807
+ send: (args: { url: StepResolver<string>; method?: "POST" | "PUT" | "PATCH"; headers?: StepResolver<Readonly<Record<string, string>>>; body?: StepResolver<unknown>; auth?: { kind: "bearer"; secret: string; } | { kind: "header"; name: string; sec…;
4808
+ };
4809
+ mail: {
4810
+ send: (args: { to: StepResolver<string | string[]>; subject: StepResolver<string>; body: StepResolver<string>; from?: StepResolver<string>; mode: "deferred"; }) => StepInstance;
4811
+ };
4812
+ callFeature: (name: string, opts: { handler: string; payload: StepResolver<unknown>; as?: SessionUser; }) => StepInstance;
4813
+ wait: (args: { for: StepResolver<string>; }) => StepInstance;
4814
+ waitForEvent: (args: { event: AwaitedEventType; match?: StepResolver<EventMatch>; timeout: StepResolver<string>; }) => StepInstance;
4815
+ retry: (args: { times: number; backoff: "exponential" | "linear"; do: StepInstance[]; }) => StepInstance;
4816
+ };
4817
+ ```
4818
+
4819
+ ## packages/types/src/temporal.ts
4820
+
4821
+ ```ts
4822
+ type Instant = globalThis.Temporal.Instant;
4823
+ ```
4824
+
4825
+ ## packages/types/src/tenancy-brand.ts
4826
+
4827
+ ```ts
4828
+ type EntityTenancy = "global" | "tenant";
4829
+ interface TenancyBrand<T extends EntityTenancy> {
4830
+ [TENANCY]: T;
4831
+ }
4832
+ ```
4833
+
4834
+ ## packages/types/src/tenant-db-types.ts
4835
+
4836
+ ```ts
4837
+ type CrossTenantReads = Pick<TenantDb, "selectMany" | "fetchOne" | "count">;
4838
+ type GlobalReads = {
4839
+ selectMany<T = Record<string, unknown>>(where?: WhereObject, options?: SelectOptions): Promise<T[]>;
4840
+ fetchOne<T = Record<string, unknown>>(where: WhereObject): Promise<T | undefined>;
4841
+ };
4842
+ type GlobalTableDb<TTable> = GlobalReads & (TTable extends Writable ? GlobalWrites : unknown);
4843
+ type GlobalWrites = {
4844
+ insertOne<T = Record<string, unknown>>(values: Record<string, unknown>): Promise<T | undefined>;
4845
+ updateMany<T = Record<string, unknown>>(set: Record<string, unknown>, where: WhereObject): Promise<T[]>;
4846
+ deleteMany(where: WhereObject): Promise<void>;
4847
+ };
4848
+ type TenantDbMode = "tenant" | "system"; // TenantDb scope modes: - "tenant" (default): SELECT/UPDATE/DELETE filtered by tenantId + reference data (tenantId=SYSTEM_TENANT_ID).
4849
+ type TenantDbNonWrites = {
4850
+ tenantId: TenantId;
4851
+ mode: TenantDbMode;
4852
+ unsafeRaw(): DbRunner; // Unfiltered DbRunner escape hatch for handlers/hooks that declare `escapeHatch: { reason }`.
4853
+ global<TTable extends (SchemaTable | EntityTableMeta) & TenancyBrand<"global">>(table: TTable): GlobalTableDb<TTable>; // Reach a "global" table with the tenant filter lifted — reads always work; writes reject unless the write handler declared `escapeHatch: { reason }`.
4854
+ selectMany<T = Record<string, unknown>>(table: SchemaTable | EntityTableMeta, where?: WhereObject, options?: SelectOptions): Promise<T[]>;
4855
+ fetchOne<T = Record<string, unknown>>(table: SchemaTable | EntityTableMeta, where: WhereObject): Promise<T | undefined>;
4856
+ count(table: SchemaTable | EntityTableMeta, where?: WhereObject): Promise<number>;
4857
+ advisoryXactLock(namespace: number, key: string): Promise<void>; // Transaction-scoped advisory lock keyed on (namespace, this handle's tenantId + key); released at commit or rollback.
4858
+ databaseNow(): Promise<Date>; // Postgres `now()`; inside a transaction this is the transaction start.
4859
+ aggregate(table: SchemaTable | EntityTableMeta, spec: AggregateSpec, where?: WhereObject): Promise<AggregateRow[]>;
4860
+ upsert<T = Record<string, unknown>>(table: WritableTable, values: Record<string, unknown>, options: TenantDbUpsertOptions): Promise<T | undefined>;
4861
+ };
4862
+ type TenantDbUpsertOptions = {
4863
+ conflictTarget: string[];
4864
+ update?: Record<string, unknown>;
4865
+ };
4866
+ type TenantDbWrites<TTable> = {
4867
+ insertOne<T = Record<string, unknown>>(table: TTable, values: Record<string, unknown>): Promise<T | undefined>;
4868
+ updateMany<T = Record<string, unknown>>(table: TTable, set: Record<string, unknown>, where: WhereObject): Promise<T[]>;
4869
+ deleteMany(table: TTable, where: WhereObject): Promise<number>;
4870
+ };
4871
+ ```
4872
+
4873
+ ## packages/types/src/tree-node.ts
4874
+
4875
+ ```ts
4876
+ type Subscribe<T> = (emit: (value: T) => void, emitError?: (error: Error) => void) => () => void;
4877
+ type TreeActionBase = {
4878
+ icon: string;
4879
+ label: string;
4880
+ };
4881
+ type TreeActionsHandle<TFeature extends string, TActions extends Record<string, TreeActionDef>> = {
4882
+ id: TFeature;
4883
+ treeActions: TActions;
4884
+ };
4885
+ type TreeNodeState = "filled" | "stub" | "empty" | "loading" | "error";
4886
+ ```
4887
+
4888
+ ## packages/types/src/tz-context.ts
4889
+
4890
+ ```ts
4891
+ type Clock = { // Source of "now" for ctx.tz; tests inject a fixed or advancing one.
4892
+ now: () => Temporal.Instant;
4893
+ };
4894
+ type LocatedTimestampJson = {
4895
+ at: string; // Wall-clock ISO without offset, e.g. "2026-04-03T10:00:00"
4896
+ tz: string; // IANA zone, e.g. "Europe/Lisbon"
4897
+ };
4898
+ type TzContext = {
4899
+ tenant: string; // Default TZ of the tenant (from tenant.timezone, default "UTC").
4900
+ user: string; // Display TZ of the current user (profile override, fallback tenant).
4901
+ now(): Temporal.Instant; // Current moment as UTC instant.
4902
+ nowIn(tz: string): Temporal.ZonedDateTime; // Current moment as ZonedDateTime in the requested zone.
4903
+ today(tz: string): Temporal.PlainDate; // Today's calendar date in the requested zone.
4904
+ todayRange(tz: string): { start: Temporal.Instant; end: Temporal.Instant; }; // Day boundaries (00:00 to 24:00 next day) as UTC instants — for DB range queries.
4905
+ parse(wallClock: string, tz: string): Temporal.ZonedDateTime; // Wall-clock string + IANA zone → ZonedDateTime.
4906
+ toInstant(zdt: Temporal.ZonedDateTime): Temporal.Instant; // ZonedDateTime → UTC instant.
4907
+ toLocatedJson(zdt: Temporal.ZonedDateTime): LocatedTimestampJson; // ZonedDateTime → JSON pair { at, tz } (API boundary).
4908
+ fromLocatedJson(obj: LocatedTimestampJson): Temporal.ZonedDateTime; // JSON pair { at, tz } → ZonedDateTime (wall-clock + IANA).
4909
+ fromCoordinates(coords: GeoCoordinates): Promise<string>; // Geo coordinates → IANA zone via the configured GeoTzProvider.
4910
+ fromAddress(address: GeoAddress): Promise<string>; // Postal address → IANA zone via GeoTzProvider.
4911
+ };
4912
+ ```
4913
+
4914
+ ## packages/types/src/websocket-route.ts
4915
+
4916
+ ```ts
4917
+ type WebSocketConnection = {
4918
+ send: (data: WebSocketMessageData) => void;
4919
+ close: (code?: number, reason?: string) => void;
4920
+ signal: AbortSignal; // Aborts when the socket closes.
4921
+ };
4922
+ type WebSocketMessageData = string | Uint8Array;
4923
+ type WebSocketRouteConnectDeps = {
4924
+ user: SessionUser;
4925
+ query: (type: string, payload: unknown) => Promise<unknown>;
4926
+ write: (type: string, payload: unknown) => Promise<WriteResult>;
4927
+ clientIp: string; // Resolved via the server's trustedProxyHops-aware resolver, like HttpRouteHandlerDeps.clientIp.
4928
+ };
4929
+ type WebSocketRouteDefinition = {
4930
+ path: string; // Must start with "/api/ws/"; `:param` segments allowed, no "*".
4931
+ maxMessageBytes?: number; // Per-message cap; oversized messages close the socket with 1009.
4932
+ maxConnectionsPerUser?: number; // Concurrent sockets per user and tenant on this route (per server process).
4933
+ connect: (c: Context<any, any>, deps: WebSocketRouteConnectDeps) => WebSocketSessionHandlers | Response | Promise<WebSocketSessionHandlers | Response>; // Runs after auth + origin checks, before the upgrade.
4934
+ };
4935
+ type WebSocketSessionHandlers = {
4936
+ onOpen?: (connection: WebSocketConnection) => void | Promise<void>; // May still be awaiting when onClose runs; see `WebSocketConnection.signal`.
4937
+ onMessage?: (data: WebSocketMessageData, connection: WebSocketConnection) => void | Promise<void>;
4938
+ onClose?: (code: number, reason: string, connection: WebSocketConnection) => void | Promise<void>; // Runs immediately on close, not queued behind a slow onMessage; pending messages never start after it.
4939
+ };
4940
+ ```
4941
+
4942
+ ## packages/types/src/where-clause-types.ts
4943
+
4944
+ ```ts
4945
+ type OrderByClause = {
4946
+ col: string;
4947
+ direction?: "asc" | "desc";
4948
+ };
4949
+ type WhereOperator = {
4950
+ gt?: unknown;
4951
+ gte?: unknown;
4952
+ lt?: unknown;
4953
+ lte?: unknown;
4954
+ ne?: unknown;
4955
+ in?: unknown[];
4956
+ notIn?: unknown[]; // Not for jsonb columns; an empty list matches every row.
4957
+ like?: string;
4958
+ jsonText?: JsonTextMatch | JsonTextMatch[]; // jsonb columns only; cannot be combined with other operators in the same object.
4959
+ };
4960
+ type WherePrimitive = string | number | boolean | bigint | Date | null | undefined;
4961
+ type WhereValue = WherePrimitive | WherePrimitive[] | WhereOperator;
4962
+ ```