@cosmicdrift/kumiko-framework 0.157.2 → 0.159.1

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.
Files changed (110) hide show
  1. package/package.json +7 -2
  2. package/src/__tests__/consumer-cli.integration.test.ts +110 -0
  3. package/src/api/__tests__/auth-routes-cookie.test.ts +16 -1
  4. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  5. package/src/api/__tests__/jwt.test.ts +150 -1
  6. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +66 -0
  7. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  8. package/src/api/api-constants.ts +4 -0
  9. package/src/api/auth-middleware.ts +48 -59
  10. package/src/api/auth-routes.ts +83 -17
  11. package/src/api/index.ts +8 -4
  12. package/src/api/jwt.ts +148 -7
  13. package/src/api/pii-leak-guard.ts +5 -2
  14. package/src/api/server.ts +19 -5
  15. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  16. package/src/bun-db/query.ts +34 -2
  17. package/src/consumer-cli.ts +87 -0
  18. package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
  19. package/src/crypto/blind-index.ts +8 -4
  20. package/src/crypto/event-pii.ts +1 -0
  21. package/src/crypto/pii-field-encryption.ts +49 -15
  22. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  23. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +305 -0
  24. package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
  25. package/src/db/blind-index-cleanup.ts +3 -1
  26. package/src/db/connection.ts +3 -11
  27. package/src/db/encryption.ts +2 -3
  28. package/src/db/entity-table-meta-types.ts +92 -0
  29. package/src/db/entity-table-meta.ts +16 -90
  30. package/src/db/queries/backfill-pii.ts +1 -0
  31. package/src/db/queries/event-consumer.ts +35 -2
  32. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  33. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
  34. package/src/engine/__tests__/define-roles.test.ts +21 -0
  35. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  36. package/src/engine/__tests__/store-table.test.ts +12 -0
  37. package/src/engine/boot-validator/action-wiring.ts +1 -1
  38. package/src/engine/boot-validator/boot-check.ts +21 -0
  39. package/src/engine/boot-validator/entity-list-screens.ts +1 -1
  40. package/src/engine/boot-validator/gdpr-storage.ts +0 -112
  41. package/src/engine/boot-validator/index.ts +3 -9
  42. package/src/engine/boot-validator/screens.ts +1 -1
  43. package/src/engine/define-feature.ts +1 -0
  44. package/src/engine/define-handler.ts +10 -91
  45. package/src/engine/entity-handlers.ts +15 -27
  46. package/src/engine/feature-builder-state.ts +3 -0
  47. package/src/engine/feature-config-events-jobs.ts +1 -1
  48. package/src/engine/feature-entity-handlers.ts +1 -1
  49. package/src/engine/feature-ui-extensions.ts +5 -1
  50. package/src/engine/field-helpers.ts +31 -0
  51. package/src/engine/handler-helpers.ts +26 -0
  52. package/src/engine/hook-helpers.ts +14 -0
  53. package/src/engine/index.ts +2 -2
  54. package/src/engine/ownership.ts +22 -76
  55. package/src/engine/registry-validate.ts +1 -1
  56. package/src/engine/screen-helpers.ts +54 -0
  57. package/src/engine/tier-resolver-extension.ts +3 -2
  58. package/src/engine/types/define-handler.ts +94 -0
  59. package/src/engine/types/entity-handlers.ts +30 -0
  60. package/src/engine/types/event-type-map.ts +1 -37
  61. package/src/engine/types/feature.ts +45 -0
  62. package/src/engine/types/fields.ts +19 -31
  63. package/src/engine/types/handlers.ts +7 -26
  64. package/src/engine/types/hooks.ts +1 -15
  65. package/src/engine/types/http-route.ts +1 -72
  66. package/src/engine/types/identifiers.ts +1 -47
  67. package/src/engine/types/index.ts +34 -9
  68. package/src/engine/types/ownership.ts +83 -0
  69. package/src/engine/types/relations.ts +1 -51
  70. package/src/engine/types/screen.ts +0 -46
  71. package/src/engine/types/target-ref.ts +1 -21
  72. package/src/engine/types/tree-node.ts +1 -129
  73. package/src/entrypoint/index.ts +2 -2
  74. package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
  75. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
  76. package/src/event-store/event-store.ts +28 -32
  77. package/src/event-store/events-schema.ts +1 -10
  78. package/src/event-store/index.ts +3 -2
  79. package/src/event-store/types.ts +22 -0
  80. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  81. package/src/files/file-handle.ts +2 -19
  82. package/src/i18n/required-surface-keys.ts +1 -1
  83. package/src/logging/types.ts +1 -7
  84. package/src/observability/types/index.ts +1 -29
  85. package/src/observability/types/metric.ts +1 -56
  86. package/src/observability/types/provider.ts +1 -32
  87. package/src/observability/types/span.ts +1 -58
  88. package/src/pipeline/__tests__/dispatcher.test.ts +38 -1
  89. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  90. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
  91. package/src/pipeline/dispatch-shared.ts +12 -2
  92. package/src/pipeline/entity-cache.ts +2 -33
  93. package/src/pipeline/event-consumer-state.ts +28 -3
  94. package/src/pipeline/event-dispatcher-admin.ts +4 -0
  95. package/src/pipeline/event-dispatcher-delivery.ts +29 -3
  96. package/src/pipeline/event-dispatcher.ts +27 -1
  97. package/src/pipeline/system-hooks.ts +7 -0
  98. package/src/search/types.ts +1 -39
  99. package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
  100. package/src/secrets/__tests__/envelope.test.ts +1 -1
  101. package/src/secrets/envelope-cipher.ts +13 -39
  102. package/src/stack/__tests__/event-collector.test.ts +42 -0
  103. package/src/testing/__tests__/late-bound.test.ts +25 -0
  104. package/src/testing/__tests__/wait-for.test.ts +53 -0
  105. package/src/testing/boot-validator-fixture.ts +1 -1
  106. package/src/testing/file-provider-contract.ts +84 -0
  107. package/src/testing/handler-context.ts +1 -1
  108. package/src/testing/index.ts +1 -0
  109. package/src/time/geo-tz.ts +1 -32
  110. package/src/ui-types/index.ts +7 -7
@@ -3,6 +3,7 @@ import { QnTypes, qualifyEntityName } from "../qualified-name";
3
3
  import type { ClaimKeyDefinition, FeatureDefinition } from "../types";
4
4
  import { validateActionWiring, validateFieldWiring } from "./action-wiring";
5
5
  import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext";
6
+ import { validateFeatureBootChecks } from "./boot-check";
6
7
  import {
7
8
  validateCircularDeps,
8
9
  validateConfigKeyAllowPerRequest,
@@ -30,12 +31,7 @@ import {
30
31
  validateTransitions,
31
32
  } from "./entity-handler";
32
33
  import { validateEntityListScreens } from "./entity-list-screens";
33
- import {
34
- validateGdprHookCompleteness,
35
- validateGdprPiiHookCoverage,
36
- validateGdprStoragePersistence,
37
- validateTenantDataHookCoverage,
38
- } from "./gdpr-storage";
34
+ import { validateGdprStoragePersistence } from "./gdpr-storage";
39
35
  import { validateI18nSurfaceKeys } from "./i18n-keys";
40
36
  import {
41
37
  collectKnownRoles,
@@ -198,9 +194,7 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
198
194
  validateEntityListScreens(features);
199
195
  validateExtensionPreSaveWiring(features);
200
196
  validateGdprStoragePersistence(features);
201
- validateGdprHookCompleteness(features);
202
- validateGdprPiiHookCoverage(features);
203
- validateTenantDataHookCoverage(features);
197
+ validateFeatureBootChecks(features);
204
198
 
205
199
  if (hasEncryptedFields) {
206
200
  // Availability check, not env-presence: eagerly building the keyring
@@ -7,6 +7,7 @@
7
7
  import { rowMetaFieldNames } from "../../db/table-builder";
8
8
  import { qualifyEntityName } from "../qualified-name";
9
9
  import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops";
10
+ import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
10
11
  import type { FeatureDefinition } from "../types";
11
12
  import type {
12
13
  DashboardCustomPanel,
@@ -20,7 +21,6 @@ import type {
20
21
  ScreenDefinition,
21
22
  ToolbarAction,
22
23
  } from "../types/screen";
23
- import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../types/screen";
24
24
 
25
25
  // --- Screen validation ---
26
26
  //
@@ -136,6 +136,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
136
136
  extensionSelectors: state.extensionSelectors,
137
137
  exposedApis: state.exposedApis,
138
138
  usedApis: state.usedApis,
139
+ bootChecks: state.bootChecks,
139
140
  referenceData: state.referenceData,
140
141
  events: state.events,
141
142
  eventMigrations: state.eventMigrations,
@@ -1,81 +1,18 @@
1
1
  import type { ZodType, z } from "zod";
2
2
  import type { ContainsSecret } from "../secrets/types";
3
3
  import { runPipeline } from "./run-pipeline";
4
+ import type { HandlerContext, KumikoEventTypeMap, WriteEvent, WriteResult } from "./types";
4
5
  import type {
5
- AccessRule,
6
- HandlerContext,
7
- KumikoEventTypeMap,
8
- QueryEvent,
9
- RateLimitOption,
10
- WriteEvent,
11
- WriteResult,
12
- } from "./types";
13
- import type { PipelineDef } from "./types/step";
6
+ QueryHandlerDefinition,
7
+ WriteHandlerDefinition,
8
+ WriteHandlerInput,
9
+ } from "./types/define-handler";
14
10
 
15
- // --- Write Handler Definition ---
16
- //
17
- // TMap propagates the strict event-type-map through the handler's
18
- // HandlerContext. CRITICAL: TMap is declared as a generic parameter on the
19
- // FUNCTION (defineWriteHandler), not just on the type. Generic-functions
20
- // substitute TMap at the USE-site (the caller's compile context, where
21
- // the augmentation is visible); generic-type-aliases substitute at the
22
- // definition-site (framework's compile, where the augmentation isn't
23
- // visible) and collapse `keyof TMap` to `never`. See the spike-findings
24
- // memory for the empirical proof.
25
- //
26
- // Two authoring forms — `handler` (free-form) or `perform: stepsPipeline(...)`
27
- // (step-pipeline). A `perform` is compiled to a handler-function at
28
- // definition time; the dispatcher only ever sees `handler`.
29
-
30
- export type WriteHandlerDefinition<
31
- TName extends string = string,
32
- TSchema extends ZodType = ZodType,
33
- TData = unknown,
34
- TMap extends object = KumikoEventTypeMap,
35
- > = {
36
- readonly name: TName;
37
- readonly schema: TSchema;
38
- readonly access?: AccessRule;
39
- readonly unsafeSkipTransitionGuard?: boolean;
40
- readonly rateLimit?: RateLimitOption;
41
- readonly handler: (
42
- event: WriteEvent<z.infer<TSchema>>,
43
- context: HandlerContext<TMap>,
44
- ) => Promise<WriteResult<TData>>;
45
- // Preserved when the author wrote a `perform` block — the original
46
- // PipelineDef. Designer/AI/AST tools read this when present; the
47
- // dispatcher ignores it and just calls `handler`. Absent on free-form
48
- // handlers.
49
- readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
50
- };
51
-
52
- // Author-facing input — accepts either the free-form `handler` or the
53
- // pipeline-form `perform`. defineWriteHandler narrows them to the
54
- // canonical WriteHandlerDefinition shape.
55
- export type WriteHandlerInput<
56
- TName extends string = string,
57
- TSchema extends ZodType = ZodType,
58
- TData = unknown,
59
- TMap extends object = KumikoEventTypeMap,
60
- > = {
61
- readonly name: TName;
62
- readonly schema: TSchema;
63
- readonly access?: AccessRule;
64
- readonly unsafeSkipTransitionGuard?: boolean;
65
- readonly rateLimit?: RateLimitOption;
66
- } & (
67
- | {
68
- readonly handler: (
69
- event: WriteEvent<z.infer<TSchema>>,
70
- context: HandlerContext<TMap>,
71
- ) => Promise<WriteResult<TData>>;
72
- readonly perform?: never;
73
- }
74
- | {
75
- readonly perform: PipelineDef<z.infer<TSchema>, TData>;
76
- readonly handler?: never;
77
- }
78
- );
11
+ export type {
12
+ QueryHandlerDefinition,
13
+ WriteHandlerDefinition,
14
+ WriteHandlerInput,
15
+ } from "./types/define-handler";
79
16
 
80
17
  export function defineWriteHandler<
81
18
  const TName extends string,
@@ -160,24 +97,6 @@ export function defineWriteHandler<
160
97
  return { ...base, handler: def.handler };
161
98
  }
162
99
 
163
- // --- Query Handler Definition ---
164
-
165
- export type QueryHandlerDefinition<
166
- TName extends string = string,
167
- TSchema extends ZodType = ZodType,
168
- TResult = unknown,
169
- TMap extends object = KumikoEventTypeMap,
170
- > = {
171
- readonly name: TName;
172
- readonly schema: TSchema;
173
- readonly access?: AccessRule;
174
- readonly rateLimit?: RateLimitOption;
175
- readonly handler: (
176
- query: QueryEvent<z.infer<TSchema>>,
177
- context: HandlerContext<TMap>,
178
- ) => Promise<TResult>;
179
- };
180
-
181
100
  export function defineQueryHandler<
182
101
  const TName extends string,
183
102
  TSchema extends ZodType,
@@ -18,6 +18,21 @@ import type {
18
18
  QueryHandlerDef,
19
19
  WriteHandlerDef,
20
20
  } from "./types";
21
+ import type {
22
+ EntityCrudRegistrar,
23
+ EntityCrudVerb,
24
+ EntityHandlerOptions,
25
+ EntityQueryHandlerOptions,
26
+ RegisterEntityCrudOptions,
27
+ } from "./types/entity-handlers";
28
+
29
+ export type {
30
+ EntityCrudRegistrar,
31
+ EntityCrudVerb,
32
+ EntityHandlerOptions,
33
+ EntityQueryHandlerOptions,
34
+ RegisterEntityCrudOptions,
35
+ } from "./types/entity-handlers";
21
36
 
22
37
  // Convention-based handler factories for event-sourced aggregates.
23
38
  //
@@ -330,16 +345,6 @@ export function defineEntityQueryHandler(
330
345
  // would need a Branded-EntityDefinition with `softDelete: true` literal
331
346
  // — feasible but not yet wired; the runtime guard catches misuse.
332
347
 
333
- type EntityHandlerOptions = { readonly access?: AccessRule };
334
- type EntityQueryHandlerOptions = EntityHandlerOptions & {
335
- /** Reads across every tenant instead of the caller's own — for a
336
- * SystemAdmin-only operator inspector over an otherwise tenant-scoped
337
- * entity. Scope this to the ONE handler that needs it rather than making
338
- * the whole feature r.systemScope(), which would drop tenant isolation
339
- * from every other handler the feature registers too. */
340
- readonly crossTenant?: boolean;
341
- };
342
-
343
348
  // @wrapper-known semantic-alias
344
349
  export function defineEntityCreateHandler(
345
350
  entityName: string,
@@ -446,23 +451,6 @@ export function defineProjectionQueryHandler(
446
451
  };
447
452
  }
448
453
 
449
- type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
450
-
451
- export type RegisterEntityCrudOptions = {
452
- readonly write?: EntityHandlerOptions;
453
- readonly read?: EntityQueryHandlerOptions;
454
- readonly verbs?: Partial<Record<EntityCrudVerb, boolean>>;
455
- /** Default true. Set false when the entity was already registered (e.g. before r.relation). */
456
- readonly registerEntity?: boolean;
457
- };
458
-
459
- /** Minimal registrar surface — keeps entity-handlers free of define-feature imports. */
460
- export type EntityCrudRegistrar = {
461
- entity(name: string, definition: EntityDefinition): unknown;
462
- writeHandler(def: WriteHandlerDef): unknown;
463
- queryHandler(def: QueryHandlerDef): unknown;
464
- };
465
-
466
454
  function defaultCrudVerbs(entity: EntityDefinition): Record<EntityCrudVerb, boolean> {
467
455
  return {
468
456
  create: true,
@@ -2,6 +2,7 @@ import type { ZodType, z } from "zod";
2
2
  import { LifecycleHookTypes } from "./constants";
3
3
  import type {
4
4
  AuthClaimsFn,
5
+ BootCheckFn,
5
6
  ClaimKeyDefinition,
6
7
  ConfigKeyDefinition,
7
8
  ConfigSeedDef,
@@ -79,6 +80,7 @@ export type FeatureBuilderState = {
79
80
  extensionSelectors: ExtensionSelectorDef[];
80
81
  exposedApis: Set<string>;
81
82
  usedApis: Set<string>;
83
+ bootChecks: BootCheckFn[];
82
84
  referenceData: ReferenceDataDef[];
83
85
  handlerEntityMappings: Record<string, string>;
84
86
  metrics: Record<string, FeatureMetricDef>;
@@ -137,6 +139,7 @@ export function createInitialFeatureBuilderState(): FeatureBuilderState {
137
139
  extensionSelectors: [],
138
140
  exposedApis: new Set(),
139
141
  usedApis: new Set(),
142
+ bootChecks: [],
140
143
  referenceData: [],
141
144
  handlerEntityMappings: {},
142
145
  metrics: {},
@@ -1,5 +1,6 @@
1
1
  import { ZodObject, type ZodType, type z } from "zod";
2
2
  import type { FeatureBuilderState } from "./feature-builder-state";
3
+ import { resolveName } from "./handler-helpers";
3
4
  import { splitNamedDefinition, unwrapArrayForm } from "./object-form";
4
5
  import { QnTypes, qn, toKebab } from "./qualified-name";
5
6
  import type {
@@ -26,7 +27,6 @@ import type {
26
27
  SecretOptions,
27
28
  TranslationsDef,
28
29
  } from "./types";
29
- import { resolveName } from "./types/handlers";
30
30
 
31
31
  // Builds config/secrets/claims/events/jobs/notifications registrar methods.
32
32
  export function buildConfigEventsJobsMethods<TName extends string>(
@@ -4,6 +4,7 @@ import type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-ha
4
4
  import type { RegisterEntityCrudOptions } from "./entity-handlers";
5
5
  import { registerEntityCrud } from "./entity-handlers";
6
6
  import type { FeatureBuilderState } from "./feature-builder-state";
7
+ import { resolveName } from "./handler-helpers";
7
8
  import { splitNamedDefinition } from "./object-form";
8
9
  import type {
9
10
  AccessRule,
@@ -16,7 +17,6 @@ import type {
16
17
  RelationDefinition,
17
18
  WriteHandlerFn,
18
19
  } from "./types";
19
- import { resolveName } from "./types/handlers";
20
20
  import type { PipelineDef } from "./types/step";
21
21
 
22
22
  const CRUD_VERBS = new Set(["create", "update", "delete"]);
@@ -1,8 +1,10 @@
1
1
  import type { EntityTableMeta } from "../db/entity-table-meta";
2
2
  import { LifecycleHookTypes } from "./constants";
3
3
  import type { FeatureBuilderState } from "./feature-builder-state";
4
+ import { resolveName } from "./handler-helpers";
4
5
  import { isKebabSegment, toKebab } from "./qualified-name";
5
6
  import type {
7
+ BootCheckFn,
6
8
  EntityProjectionExtension,
7
9
  HookPhase,
8
10
  LifecycleHookFn,
@@ -22,7 +24,6 @@ import type {
22
24
  ValidationHookFn,
23
25
  } from "./types";
24
26
  import { HookPhases } from "./types";
25
- import { resolveName } from "./types/handlers";
26
27
  import type { HttpRouteDefinition } from "./types/http-route";
27
28
  import type { NavDefinition } from "./types/nav";
28
29
  import type { ScreenDefinition } from "./types/screen";
@@ -255,6 +256,9 @@ export function buildUiExtensionsMethods<TName extends string>(
255
256
  usesApi(apiName: string): void {
256
257
  state.usedApis.add(apiName);
257
258
  },
259
+ bootCheck(fn: BootCheckFn): void {
260
+ state.bootChecks.push(fn);
261
+ },
258
262
  projection(definition: ProjectionDefinition): void {
259
263
  // Reject names that would blow up at registry-boot when we qualify them.
260
264
  // Catch it at the registration site so the stack trace points at the
@@ -0,0 +1,31 @@
1
+ import type { AnyFileFieldDef, FieldDefinition } from "./types/fields";
2
+
3
+ // --- Currency ---
4
+
5
+ export const DEFAULT_CURRENCIES = [
6
+ "EUR",
7
+ "USD",
8
+ "GBP",
9
+ "CHF",
10
+ "JPY",
11
+ "SEK",
12
+ "NOK",
13
+ "DKK",
14
+ "PLN",
15
+ "CZK",
16
+ "CAD",
17
+ "AUD",
18
+ "NZD",
19
+ "CNY",
20
+ "INR",
21
+ ] as const;
22
+
23
+ export function isFileField(field: FieldDefinition | undefined): field is AnyFileFieldDef {
24
+ if (!field) return false;
25
+ return (
26
+ field.type === "file" ||
27
+ field.type === "image" ||
28
+ field.type === "files" ||
29
+ field.type === "images"
30
+ );
31
+ }
@@ -0,0 +1,26 @@
1
+ import type { NameOrRef, WriteResult } from "./types/handlers";
2
+
3
+ /**
4
+ * Override the success-side `data` of a WriteResult while forwarding the
5
+ * failure half untouched. Useful for handlers that delegate to the
6
+ * event-store executor (which returns a SaveContext / DeleteContext
7
+ * envelope) but want to keep their own response shape — caller contract
8
+ * stays flat instead of leaking the executor's internals.
9
+ *
10
+ * ```ts
11
+ * const result = await executor.delete({ id }, user, db);
12
+ * return withResponseData(result, { userId, tenantId });
13
+ * ```
14
+ *
15
+ * On failure the same WriteFailure instance is returned — the error
16
+ * object round-trips without any wrapping, so the dispatcher / HTTP layer
17
+ * still read the original error code + httpStatus + i18nKey.
18
+ */
19
+ export function withResponseData<T>(result: WriteResult<unknown>, data: T): WriteResult<T> {
20
+ if (!result.isSuccess) return result;
21
+ return { isSuccess: true, data };
22
+ }
23
+
24
+ export function resolveName(ref: NameOrRef): string {
25
+ return typeof ref === "string" ? ref : ref.name;
26
+ }
@@ -0,0 +1,14 @@
1
+ // --- Hook Phases ---
2
+ //
3
+ // inTransaction: Hook runs inside the DB transaction. Failures roll back
4
+ // the entire write. Use for: DB-based side-effects (counter updates,
5
+ // dependent entity writes).
6
+ //
7
+ // afterCommit (default): Hook runs after the transaction commits. Failures
8
+ // are logged but don't affect the write. Use for: external systems
9
+ // (SSE broadcast, search index, email, webhooks).
10
+
11
+ export const HookPhases = {
12
+ inTransaction: "inTransaction",
13
+ afterCommit: "afterCommit",
14
+ } as const;
@@ -177,6 +177,7 @@ export {
177
177
  checkWriteFieldRoles,
178
178
  filterReadFields,
179
179
  } from "./field-access";
180
+ export { resolveName, withResponseData } from "./handler-helpers";
180
181
  // findForbiddenMembershipRole/isForbiddenMembershipRole/
181
182
  // stripForbiddenMembershipRoles/buildSessionRoles are Public API for host
182
183
  // apps that build their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
@@ -203,6 +204,7 @@ export { runsInLane } from "./run-in";
203
204
  export type { StepListOutcome } from "./run-pipeline";
204
205
  export { runPipeline, runStepList } from "./run-pipeline";
205
206
  export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder";
207
+ export { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./screen-helpers";
206
208
  export type { TransitionGraph } from "./state-machine";
207
209
  export { defineTransitions, guardTransition } from "./state-machine";
208
210
  export {
@@ -385,9 +387,7 @@ export type {
385
387
  WriteResult,
386
388
  } from "./types";
387
389
  export { DEFAULT_CURRENCIES, HookPhases } from "./types";
388
- export { resolveName, withResponseData } from "./types/handlers";
389
390
  export { isSystemTenant, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers";
390
- export { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./types/screen";
391
391
  export type {
392
392
  PipelineBuildCtx,
393
393
  PipelineCtx,
@@ -20,64 +20,28 @@
20
20
  import { toSnakeCase } from "../db/table-builder";
21
21
  import type { SessionUser } from "./types";
22
22
 
23
- // Parameterised SQL fragment produced by buildOwnershipClause + by the
24
- // WhereRule escape-hatch. Caller weaves `sqlText` into a larger statement,
25
- // renumbering placeholders if needed (FragmentBuilder below).
26
- export type SqlFragment = {
27
- readonly sqlText: string;
28
- readonly params: readonly unknown[];
29
- };
30
-
31
- // Reference spec supported by `from()`:
32
- // "user:id" → user.id
33
- // "user:tenantId" → user.tenantId (rarely needed — TenantDb scopes anyway)
34
- // "claim:<featureName>:<key>" → user.claims["<featureName>:<key>"]
35
- //
36
- // The string form is keyed so the framework can look up the referenced
37
- // Registry entry at boot (Claim-QN exists? Column type compatible?). A typed
38
- // object form would force features to import each other's handles — the
39
- // whole point of H.2's unified path is string-based references, no imports.
40
- export type OwnershipRef = string;
41
-
42
- // Resolved during `from()` — the parser eagerly splits the prefix so the
43
- // runtime evaluator avoids string-parsing on every row. `kind` drives the
44
- // evaluator branch; the rest is the resolved metadata.
45
- export type FromRuleKind = "user" | "claim";
46
-
47
- export type FromRule = {
48
- readonly kind: "from";
49
- readonly refKind: FromRuleKind;
50
- // For "user:id" → "id"; for "user:tenantId" → "tenantId".
51
- // For "claim:<featureName>:<key>" → "<featureName>:<key>" (the full QN,
52
- // which is exactly the key under which the JWT stores the value).
53
- readonly refPath: string;
54
- // Row-column to match against. For claim rules defaults to the claim's
55
- // shortName (second segment of the claim QN). For user-rules the column
56
- // is always explicit.
57
- readonly column: string;
58
- };
59
-
60
- // Context passed to a WhereRule escape-hatch. The author returns a SqlFragment
61
- // whose placeholders start at `paramStart` ($N, $N+1, ...); the framework
62
- // concatenates the fragment into the larger query.
63
- export type WhereRuleContext<TTable = unknown> = {
64
- readonly table: TTable;
65
- readonly tableName: string;
66
- readonly paramStart: number;
67
- };
68
-
69
- export type WhereRule<TTable = unknown> = {
70
- readonly kind: "where";
71
- readonly where: (user: SessionUser, ctx: WhereRuleContext<TTable>) => SqlFragment;
72
- };
73
-
74
- // "all" collapses to a primitive so map authors can write `Admin: "all"`
75
- // without importing a helper.
76
- export type OwnershipRule = "all" | FromRule | WhereRule;
77
-
78
- // Per-role map: every key is a role name, value is the rule that role
79
- // satisfies to pass the access check.
80
- export type OwnershipMap = Readonly<Record<string, OwnershipRule>>;
23
+ // Types live in engine/types/ownership.ts (no runtime dependency); re-exported
24
+ // here for backwards compatibility with existing importers of this file.
25
+ export type {
26
+ FromRule,
27
+ FromRuleKind,
28
+ OwnershipClause,
29
+ OwnershipMap,
30
+ OwnershipRef,
31
+ OwnershipRule,
32
+ SqlFragment,
33
+ WhereRule,
34
+ WhereRuleContext,
35
+ } from "./types/ownership";
36
+
37
+ import type {
38
+ FromRule,
39
+ OwnershipClause,
40
+ OwnershipMap,
41
+ OwnershipRef,
42
+ OwnershipRule,
43
+ SqlFragment,
44
+ } from "./types/ownership";
81
45
 
82
46
  // Parse an OwnershipRef into kind + resolved path + default column.
83
47
  // Throws on malformed input so the error surfaces at `from()`-call-site (in
@@ -254,24 +218,6 @@ export function userCanCreateFieldRow(
254
218
  return false;
255
219
  }
256
220
 
257
- // Result of buildOwnershipClause. The discriminant lets the caller handle
258
- // the three outcomes without inspecting SQL internals:
259
- //
260
- // "pass" → user is unrestricted. Run the query as-is.
261
- // "empty" → user has a role mapped but no rule accepts any row (missing
262
- // claim, empty array, role not in map). Skip the DB call entirely
263
- // — returning [] is equivalent and avoids a pointless roundtrip.
264
- // "sql" → apply the parameterised fragment as an AND on the query.
265
- // Caller is responsible for renumbering placeholders when
266
- // concatenating with other fragments (see `shiftParams` below).
267
- //
268
- // "empty" vs. "pass" is the critical distinction for a safe default:
269
- // undefined/pass = allow, empty = deny-by-construction.
270
- export type OwnershipClause =
271
- | { readonly kind: "pass" }
272
- | { readonly kind: "empty" }
273
- | { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
274
-
275
221
  const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
276
222
  const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
277
223
 
@@ -1,4 +1,5 @@
1
1
  import { configureEventPiiCatalog } from "../crypto/event-pii";
2
+ import { resolveName } from "./handler-helpers";
2
3
  import type { RegistryState } from "./registry-state";
3
4
  import { buildImplicitProjection, hasFieldAccessRules, qualify } from "./registry-state";
4
5
  import {
@@ -11,7 +12,6 @@ import {
11
12
  } from "./soft-delete-cleanup";
12
13
  import type { EventPiiFields, EventUpcastFn, FeatureDefinition } from "./types";
13
14
  import { HookPhases } from "./types";
14
- import { resolveName } from "./types/handlers";
15
15
 
16
16
  function allHandlerQns(state: RegistryState): ReadonlySet<string> {
17
17
  return new Set([...state.writeHandlerMap.keys(), ...state.queryHandlerMap.keys()]);
@@ -0,0 +1,54 @@
1
+ import type {
2
+ EditExtensionSection,
3
+ EditFieldSpec,
4
+ EditSectionSpec,
5
+ FieldCondition,
6
+ FormatSpec,
7
+ ListColumnSpec,
8
+ } from "./types/screen";
9
+
10
+ export function isExtensionEditSection(section: EditSectionSpec): section is EditExtensionSection {
11
+ return section.kind === "extension";
12
+ }
13
+
14
+ // Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
15
+ // authors who branch on the three FieldRenderer variants without manual
16
+ // "format" in renderer checks.
17
+ export function isFormatSpec(r: unknown): r is FormatSpec {
18
+ return typeof r === "object" && r !== null && "format" in r && typeof r.format === "string";
19
+ }
20
+
21
+ // Collapse the string-shorthand into the object form. Both the boot-validator
22
+ // and (later) ui-core's view-model builder iterate over fields/columns — the
23
+ // helper keeps that loop from growing two branches everywhere.
24
+ export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
25
+ const col = typeof c === "string" ? { field: c } : c;
26
+ if (
27
+ typeof process !== "undefined" &&
28
+ process.env.NODE_ENV !== "production" &&
29
+ col.renderer !== undefined &&
30
+ typeof col.renderer === "function"
31
+ ) {
32
+ // biome-ignore lint/suspicious/noConsole: dev-only warning
33
+ console.warn(
34
+ `[kumiko] normalizeListColumn: Feld "${col.field}" hat einen Funktions-Renderer — dieser wird von JSON.stringify verworfen. Bitte auf FormatSpec ({ format: "..." }) migrieren.`,
35
+ );
36
+ }
37
+ return col;
38
+ }
39
+
40
+ /** Evaluates a declarative FieldCondition against the current row/form
41
+ * values. THE single implementation — renderer (row-action visibility),
42
+ * headless view-model (visible/readOnly/required) and render-edit
43
+ * (form-condition closures) reuse it; three hand-rolled copies had
44
+ * already drifted in shape. */
45
+ export function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean {
46
+ if (typeof cond === "boolean") return cond;
47
+ const val = values[cond.field];
48
+ if ("eq" in cond) return val === cond.eq;
49
+ return val !== cond.ne;
50
+ }
51
+
52
+ export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
53
+ return typeof f === "string" ? { field: f } : f;
54
+ }
@@ -48,8 +48,9 @@ export type EffectiveFeaturesResolver = ((tenantId: TenantId) => ReadonlySet<str
48
48
  * Signup-Datum des Tenants live (tenant.inserted_at) und returnt true wenn
49
49
  * der Tenant im Trial-Fenster ist UND das Feature zum Trial-Tier gehört.
50
50
  * Async + nicht im Boot-Cache, weil der Trial zeit-abgeleitet ist (ändert
51
- * sich zwischen Requests). Nur die 2 Gate-Aufrufstellen awaiten ihn; der
52
- * synchrone Hot-Path (ctx.hasFeature, Feature-Set) bleibt unberührt.
51
+ * sich zwischen Requests). Konsultiert an 3 Stellen: den 2 Dispatch-Gate-
52
+ * Aufrufstellen (checkFeatureEnabled) UND ctx.hasFeature im Handler-Body
53
+ * (fällt darauf zurück, wenn der synchrone Feature-Set-Check negativ ist).
53
54
  */
54
55
  readonly trialGate?: TrialGate;
55
56
  };