@cosmicdrift/kumiko-framework 0.158.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 (109) 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__/server-jwt-ttl.test.ts +58 -0
  7. package/src/api/api-constants.ts +4 -0
  8. package/src/api/auth-middleware.ts +48 -59
  9. package/src/api/auth-routes.ts +51 -17
  10. package/src/api/index.ts +3 -3
  11. package/src/api/jwt.ts +148 -7
  12. package/src/api/pii-leak-guard.ts +5 -2
  13. package/src/api/server.ts +19 -5
  14. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  15. package/src/bun-db/query.ts +34 -2
  16. package/src/consumer-cli.ts +87 -0
  17. package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
  18. package/src/crypto/blind-index.ts +8 -4
  19. package/src/crypto/event-pii.ts +1 -0
  20. package/src/crypto/pii-field-encryption.ts +49 -15
  21. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  22. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +305 -0
  23. package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
  24. package/src/db/blind-index-cleanup.ts +3 -1
  25. package/src/db/connection.ts +3 -11
  26. package/src/db/encryption.ts +2 -3
  27. package/src/db/entity-table-meta-types.ts +92 -0
  28. package/src/db/entity-table-meta.ts +16 -90
  29. package/src/db/queries/backfill-pii.ts +1 -0
  30. package/src/db/queries/event-consumer.ts +35 -2
  31. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  32. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
  33. package/src/engine/__tests__/define-roles.test.ts +21 -0
  34. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  35. package/src/engine/__tests__/store-table.test.ts +12 -0
  36. package/src/engine/boot-validator/action-wiring.ts +1 -1
  37. package/src/engine/boot-validator/boot-check.ts +21 -0
  38. package/src/engine/boot-validator/entity-list-screens.ts +1 -1
  39. package/src/engine/boot-validator/gdpr-storage.ts +0 -112
  40. package/src/engine/boot-validator/index.ts +3 -9
  41. package/src/engine/boot-validator/screens.ts +1 -1
  42. package/src/engine/define-feature.ts +1 -0
  43. package/src/engine/define-handler.ts +10 -91
  44. package/src/engine/entity-handlers.ts +15 -27
  45. package/src/engine/feature-builder-state.ts +3 -0
  46. package/src/engine/feature-config-events-jobs.ts +1 -1
  47. package/src/engine/feature-entity-handlers.ts +1 -1
  48. package/src/engine/feature-ui-extensions.ts +5 -1
  49. package/src/engine/field-helpers.ts +31 -0
  50. package/src/engine/handler-helpers.ts +26 -0
  51. package/src/engine/hook-helpers.ts +14 -0
  52. package/src/engine/index.ts +2 -2
  53. package/src/engine/ownership.ts +22 -76
  54. package/src/engine/registry-validate.ts +1 -1
  55. package/src/engine/screen-helpers.ts +54 -0
  56. package/src/engine/tier-resolver-extension.ts +3 -2
  57. package/src/engine/types/define-handler.ts +94 -0
  58. package/src/engine/types/entity-handlers.ts +30 -0
  59. package/src/engine/types/event-type-map.ts +1 -37
  60. package/src/engine/types/feature.ts +45 -0
  61. package/src/engine/types/fields.ts +19 -31
  62. package/src/engine/types/handlers.ts +7 -26
  63. package/src/engine/types/hooks.ts +1 -15
  64. package/src/engine/types/http-route.ts +1 -72
  65. package/src/engine/types/identifiers.ts +1 -47
  66. package/src/engine/types/index.ts +34 -9
  67. package/src/engine/types/ownership.ts +83 -0
  68. package/src/engine/types/relations.ts +1 -51
  69. package/src/engine/types/screen.ts +0 -46
  70. package/src/engine/types/target-ref.ts +1 -21
  71. package/src/engine/types/tree-node.ts +1 -129
  72. package/src/entrypoint/index.ts +2 -2
  73. package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
  74. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
  75. package/src/event-store/event-store.ts +28 -32
  76. package/src/event-store/events-schema.ts +1 -10
  77. package/src/event-store/index.ts +3 -2
  78. package/src/event-store/types.ts +22 -0
  79. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  80. package/src/files/file-handle.ts +2 -19
  81. package/src/i18n/required-surface-keys.ts +1 -1
  82. package/src/logging/types.ts +1 -7
  83. package/src/observability/types/index.ts +1 -29
  84. package/src/observability/types/metric.ts +1 -56
  85. package/src/observability/types/provider.ts +1 -32
  86. package/src/observability/types/span.ts +1 -58
  87. package/src/pipeline/__tests__/dispatcher.test.ts +38 -1
  88. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  89. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
  90. package/src/pipeline/dispatch-shared.ts +12 -2
  91. package/src/pipeline/entity-cache.ts +2 -33
  92. package/src/pipeline/event-consumer-state.ts +28 -3
  93. package/src/pipeline/event-dispatcher-admin.ts +4 -0
  94. package/src/pipeline/event-dispatcher-delivery.ts +29 -3
  95. package/src/pipeline/event-dispatcher.ts +27 -1
  96. package/src/pipeline/system-hooks.ts +7 -0
  97. package/src/search/types.ts +1 -39
  98. package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
  99. package/src/secrets/__tests__/envelope.test.ts +1 -1
  100. package/src/secrets/envelope-cipher.ts +13 -39
  101. package/src/stack/__tests__/event-collector.test.ts +42 -0
  102. package/src/testing/__tests__/late-bound.test.ts +25 -0
  103. package/src/testing/__tests__/wait-for.test.ts +53 -0
  104. package/src/testing/boot-validator-fixture.ts +1 -1
  105. package/src/testing/file-provider-contract.ts +84 -0
  106. package/src/testing/handler-context.ts +1 -1
  107. package/src/testing/index.ts +1 -0
  108. package/src/time/geo-tz.ts +1 -32
  109. package/src/ui-types/index.ts +7 -7
@@ -0,0 +1,94 @@
1
+ import type { ZodType, z } from "zod";
2
+ import type { KumikoEventTypeMap } from "./event-type-map";
3
+ import type {
4
+ AccessRule,
5
+ HandlerContext,
6
+ QueryEvent,
7
+ RateLimitOption,
8
+ WriteEvent,
9
+ WriteResult,
10
+ } from "./handlers";
11
+ import type { PipelineDef } from "./step";
12
+
13
+ // --- Write Handler Definition ---
14
+ //
15
+ // TMap propagates the strict event-type-map through the handler's
16
+ // HandlerContext. CRITICAL: TMap is declared as a generic parameter on the
17
+ // FUNCTION (defineWriteHandler), not just on the type. Generic-functions
18
+ // substitute TMap at the USE-site (the caller's compile context, where
19
+ // the augmentation is visible); generic-type-aliases substitute at the
20
+ // definition-site (framework's compile, where the augmentation isn't
21
+ // visible) and collapse `keyof TMap` to `never`. See the spike-findings
22
+ // memory for the empirical proof.
23
+ //
24
+ // Two authoring forms — `handler` (free-form) or `perform: stepsPipeline(...)`
25
+ // (step-pipeline). A `perform` is compiled to a handler-function at
26
+ // definition time; the dispatcher only ever sees `handler`.
27
+
28
+ export type WriteHandlerDefinition<
29
+ TName extends string = string,
30
+ TSchema extends ZodType = ZodType,
31
+ TData = unknown,
32
+ TMap extends object = KumikoEventTypeMap,
33
+ > = {
34
+ readonly name: TName;
35
+ readonly schema: TSchema;
36
+ readonly access?: AccessRule;
37
+ readonly unsafeSkipTransitionGuard?: boolean;
38
+ readonly rateLimit?: RateLimitOption;
39
+ readonly handler: (
40
+ event: WriteEvent<z.infer<TSchema>>,
41
+ context: HandlerContext<TMap>,
42
+ ) => Promise<WriteResult<TData>>;
43
+ // Preserved when the author wrote a `perform` block — the original
44
+ // PipelineDef. Designer/AI/AST tools read this when present; the
45
+ // dispatcher ignores it and just calls `handler`. Absent on free-form
46
+ // handlers.
47
+ readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
48
+ };
49
+
50
+ // Author-facing input — accepts either the free-form `handler` or the
51
+ // pipeline-form `perform`. defineWriteHandler narrows them to the
52
+ // canonical WriteHandlerDefinition shape.
53
+ export type WriteHandlerInput<
54
+ TName extends string = string,
55
+ TSchema extends ZodType = ZodType,
56
+ TData = unknown,
57
+ TMap extends object = KumikoEventTypeMap,
58
+ > = {
59
+ readonly name: TName;
60
+ readonly schema: TSchema;
61
+ readonly access?: AccessRule;
62
+ readonly unsafeSkipTransitionGuard?: boolean;
63
+ readonly rateLimit?: RateLimitOption;
64
+ } & (
65
+ | {
66
+ readonly handler: (
67
+ event: WriteEvent<z.infer<TSchema>>,
68
+ context: HandlerContext<TMap>,
69
+ ) => Promise<WriteResult<TData>>;
70
+ readonly perform?: never;
71
+ }
72
+ | {
73
+ readonly perform: PipelineDef<z.infer<TSchema>, TData>;
74
+ readonly handler?: never;
75
+ }
76
+ );
77
+
78
+ // --- Query Handler Definition ---
79
+
80
+ export type QueryHandlerDefinition<
81
+ TName extends string = string,
82
+ TSchema extends ZodType = ZodType,
83
+ TResult = unknown,
84
+ TMap extends object = KumikoEventTypeMap,
85
+ > = {
86
+ readonly name: TName;
87
+ readonly schema: TSchema;
88
+ readonly access?: AccessRule;
89
+ readonly rateLimit?: RateLimitOption;
90
+ readonly handler: (
91
+ query: QueryEvent<z.infer<TSchema>>,
92
+ context: HandlerContext<TMap>,
93
+ ) => Promise<TResult>;
94
+ };
@@ -0,0 +1,30 @@
1
+ import type { EntityDefinition } from "./fields";
2
+ import type { AccessRule, QueryHandlerDef, WriteHandlerDef } from "./handlers";
3
+
4
+ export type EntityHandlerOptions = { readonly access?: AccessRule };
5
+
6
+ export type EntityQueryHandlerOptions = EntityHandlerOptions & {
7
+ /** Reads across every tenant instead of the caller's own — for a
8
+ * SystemAdmin-only operator inspector over an otherwise tenant-scoped
9
+ * entity. Scope this to the ONE handler that needs it rather than making
10
+ * the whole feature r.systemScope(), which would drop tenant isolation
11
+ * from every other handler the feature registers too. */
12
+ readonly crossTenant?: boolean;
13
+ };
14
+
15
+ export type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
16
+
17
+ export type RegisterEntityCrudOptions = {
18
+ readonly write?: EntityHandlerOptions;
19
+ readonly read?: EntityQueryHandlerOptions;
20
+ readonly verbs?: Partial<Record<EntityCrudVerb, boolean>>;
21
+ /** Default true. Set false when the entity was already registered (e.g. before r.relation). */
22
+ readonly registerEntity?: boolean;
23
+ };
24
+
25
+ /** Minimal registrar surface — keeps entity-handlers free of define-feature imports. */
26
+ export type EntityCrudRegistrar = {
27
+ entity(name: string, definition: EntityDefinition): unknown;
28
+ writeHandler(def: WriteHandlerDef): unknown;
29
+ queryHandler(def: QueryHandlerDef): unknown;
30
+ };
@@ -1,37 +1 @@
1
- // Cross-Feature Compile-Time-Type-Map.
2
- //
3
- // Zweck: ctx.appendEvent / ctx.queryProjection / dispatcher.write gegen ein
4
- // statisch bekanntes Schema-Bild prüfen, statt erst zur Boot- oder Runtime
5
- // (zod-validate) zu scheitern. Designer/AI-Layer profitiert dadurch sofort:
6
- // Autocomplete kennt alle Event-Typen aller geladenen Features, payload-
7
- // Shape-Mismatches werden im Editor angezeigt, nicht erst beim Boot.
8
- //
9
- // Befüllung erfolgt per Feature über `declare module "@cosmicdrift/kumiko-framework/engine"`
10
- // — entweder hand-geschrieben (für stabile Frameworks-Internals) oder vom
11
- // Codegen-Skript erzeugt (für apps/bundled-features). Empty defaults sind
12
- // kein Bug: ein Feature ohne Augmentation ist runtime-pluggable und nutzt
13
- // die Fallback-Overload mit `unknown` payload.
14
- //
15
- // Pattern für hand-geschriebene Augmentation am File-Top:
16
- //
17
- // declare module "@cosmicdrift/kumiko-framework/engine" {
18
- // interface KumikoEventTypeMap {
19
- // "users:user.created": z.infer<typeof userCreatedSchema>;
20
- // }
21
- // }
22
-
23
- // MUST be `interface` (not `type`): only interfaces support TS declaration-
24
- // merging. Apps/features extend these via `declare module "@cosmicdrift/kumiko-framework/engine"`
25
- // blocks. A `type X = {}` alias would silently break that augmentation channel.
26
-
27
- // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker — augmented per feature
28
- export interface KumikoEventTypeMap {}
29
-
30
- // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
31
- export interface KumikoEntityTypeMap {}
32
-
33
- // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
34
- export interface KumikoHandlerPayloadMap {}
35
-
36
- // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
37
- export interface KumikoHandlerResultMap {}
1
+ export * from "@cosmicdrift/kumiko-types/event-type-map";
@@ -204,6 +204,13 @@ export type UiHints = {
204
204
 
205
205
  // --- Feature Definition (output of defineFeature) ---
206
206
 
207
+ // Ctx passed to r.bootCheck(fn) — the full mounted-feature set, so a check
208
+ // can inspect any other feature's shape (entities, PII fields, etc.).
209
+ export type BootCheckContext = {
210
+ readonly features: readonly FeatureDefinition[];
211
+ };
212
+ export type BootCheckFn = (ctx: BootCheckContext) => void;
213
+
207
214
  export type FeatureDefinition = {
208
215
  readonly name: string;
209
216
  // Docs-lead paragraph declared via r.describe(). Flows through the
@@ -276,6 +283,13 @@ export type FeatureDefinition = {
276
283
  * UND das Provider-Feature muss in requires/optionalRequires sein.
277
284
  */
278
285
  readonly usedApis: ReadonlySet<string>;
286
+ /**
287
+ * Boot-time mount-invariant checks declared via `r.bootCheck(fn)`. Run
288
+ * once per feature after all other boot-validators, each with a ctx
289
+ * exposing the full mounted-feature set — throw to fail the boot with a
290
+ * feature-authored message (framework wraps it with `[Feature <name>]`).
291
+ */
292
+ readonly bootChecks: readonly BootCheckFn[];
279
293
  readonly referenceData: readonly ReferenceDataDef[];
280
294
  readonly notifications: Readonly<Record<string, NotificationDefinition>>;
281
295
  readonly events: Readonly<Record<string, EventDef>>;
@@ -617,6 +631,37 @@ export type FeatureRegistrar<TFeature extends string = string> = {
617
631
  */
618
632
  usesApi(apiName: string): void;
619
633
 
634
+ /**
635
+ * Declares a boot-time mount-invariant for this feature. `fn` runs once
636
+ * at boot with a ctx exposing every mounted feature — throw to fail the
637
+ * boot with a clear message. Multiple calls per feature are allowed.
638
+ *
639
+ * Use for cross-feature invariants that `r.requires` can't express
640
+ * because they're conditional (only fail when the feature has a
641
+ * specific shape), e.g. "if this feature has PII entities, some
642
+ * user-data-hook feature must be mounted" (the prompt-store trap,
643
+ * kumiko-enterprise#229 — a UserData feature was written but never
644
+ * mounted, and nothing caught it at boot):
645
+ *
646
+ * ```ts
647
+ * defineFeature("prompt-store", (r) => {
648
+ * const promptFields = { text: { type: "text", pii: true } };
649
+ * r.entity("prompt", { fields: promptFields });
650
+ * r.bootCheck(({ features }) => {
651
+ * // Conditional on this feature's own shape (has a pii field) —
652
+ * // r.requires("user-data-hook") can't express that, it would fail
653
+ * // even for a prompt-store variant with no PII fields at all.
654
+ * const hasPiiField = Object.values(promptFields).some((f) => f.pii);
655
+ * const hasUserDataHook = features.some((f) => f.name === "user-data-hook");
656
+ * if (hasPiiField && !hasUserDataHook) {
657
+ * throw new Error("prompt-store has PII fields but no user-data-hook feature is mounted");
658
+ * }
659
+ * });
660
+ * });
661
+ * ```
662
+ */
663
+ bootCheck(fn: BootCheckFn): void;
664
+
620
665
  // Declare a metric. Short name (without kumiko_<feature>_ prefix) — Framework
621
666
  // qualifies it on boot. Validation (snake_case + typ-suffix) runs at boot.
622
667
  // Usage at runtime: ctx.metrics.inc("created_total", { status: "new" }).
@@ -1,11 +1,11 @@
1
1
  // --- Field Types ---
2
2
 
3
- // OwnershipMap is declared in engine/ownership.ts — field-access maps to
3
+ // OwnershipMap is declared in ./ownership.ts — field-access maps to
4
4
  // per-role ownership rules. A legacy `readonly string[]` form is still
5
5
  // accepted at the type layer during migration: features that pass an
6
6
  // array are auto-normalized to { [role]: "all" } at registry build.
7
7
  // Long-term: string[] disappears.
8
- import type { OwnershipMap } from "../ownership";
8
+ import type { OwnershipMap } from "./ownership";
9
9
 
10
10
  export type FieldAccess = {
11
11
  readonly read?: OwnershipMap | readonly string[];
@@ -312,25 +312,23 @@ export type ReferenceFieldDef = {
312
312
 
313
313
  // --- Currency ---
314
314
 
315
- export const DEFAULT_CURRENCIES = [
316
- "EUR",
317
- "USD",
318
- "GBP",
319
- "CHF",
320
- "JPY",
321
- "SEK",
322
- "NOK",
323
- "DKK",
324
- "PLN",
325
- "CZK",
326
- "CAD",
327
- "AUD",
328
- "NZD",
329
- "CNY",
330
- "INR",
331
- ] as const;
332
-
333
- export type DefaultCurrency = (typeof DEFAULT_CURRENCIES)[number];
315
+ // Keep in sync with DEFAULT_CURRENCIES in ../field-helpers.ts.
316
+ export type DefaultCurrency =
317
+ | "EUR"
318
+ | "USD"
319
+ | "GBP"
320
+ | "CHF"
321
+ | "JPY"
322
+ | "SEK"
323
+ | "NOK"
324
+ | "DKK"
325
+ | "PLN"
326
+ | "CZK"
327
+ | "CAD"
328
+ | "AUD"
329
+ | "NZD"
330
+ | "CNY"
331
+ | "INR";
334
332
 
335
333
  // --- Embedded Object ---
336
334
 
@@ -531,16 +529,6 @@ export type FieldDefinition =
531
529
  // `maxSize` and `accept`, which is what upload validation cares about.
532
530
  export type AnyFileFieldDef = FileFieldDef | ImageFieldDef | FilesFieldDef | ImagesFieldDef;
533
531
 
534
- export function isFileField(field: FieldDefinition | undefined): field is AnyFileFieldDef {
535
- if (!field) return false;
536
- return (
537
- field.type === "file" ||
538
- field.type === "image" ||
539
- field.type === "files" ||
540
- field.type === "images"
541
- );
542
- }
543
-
544
532
  // --- Derived (computed) fields ---
545
533
  //
546
534
  // A derived field is read-time only: its value is computed from the stored row
@@ -166,27 +166,6 @@ export type WriteResult<TData = unknown> =
166
166
  | { readonly isSuccess: true; readonly data: TData }
167
167
  | WriteFailure;
168
168
 
169
- /**
170
- * Override the success-side `data` of a WriteResult while forwarding the
171
- * failure half untouched. Useful for handlers that delegate to the
172
- * event-store executor (which returns a SaveContext / DeleteContext
173
- * envelope) but want to keep their own response shape — caller contract
174
- * stays flat instead of leaking the executor's internals.
175
- *
176
- * ```ts
177
- * const result = await executor.delete({ id }, user, db);
178
- * return withResponseData(result, { userId, tenantId });
179
- * ```
180
- *
181
- * On failure the same WriteFailure instance is returned — the error
182
- * object round-trips without any wrapping, so the dispatcher / HTTP layer
183
- * still read the original error code + httpStatus + i18nKey.
184
- */
185
- export function withResponseData<T>(result: WriteResult<unknown>, data: T): WriteResult<T> {
186
- if (!result.isSuccess) return result;
187
- return { isSuccess: true, data };
188
- }
189
-
190
169
  // --- Context Types ---
191
170
 
192
171
  // Forward import: Registry is in feature.ts (circular type import — fine in TS)
@@ -375,7 +354,13 @@ export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedCon
375
354
  // is on, add extra columns to the export"). The dispatcher gate already
376
355
  // blocks calls to handlers of disabled features — this is the fine-grained
377
356
  // opt-in counterpart, not a substitute for the gate.
378
- readonly hasFeature: (featureName: string) => boolean;
357
+ //
358
+ // Async: falls back to the live trial-gate (tenant.inserted_at-derived,
359
+ // can't live in the boot-cached sync resolver) whenever the synchronous
360
+ // feature-set says a feature is off — otherwise trial tenants checking a
361
+ // companion feature (not their own handler's owning feature) would see a
362
+ // stale `false`.
363
+ readonly hasFeature: (featureName: string) => Promise<boolean>;
379
364
 
380
365
  // Append a domain event to a specific aggregate stream in the current tx.
381
366
  // Marten-aligned: every event belongs to exactly one aggregate. The runtime
@@ -774,10 +759,6 @@ export type EventMigrationDef = {
774
759
  // Anything that carries a name — accepted by hooks, relations, jobs, etc.
775
760
  export type NameOrRef = string | { readonly name: string };
776
761
 
777
- export function resolveName(ref: NameOrRef): string {
778
- return typeof ref === "string" ? ref : ref.name;
779
- }
780
-
781
762
  export type EntityRef = {
782
763
  readonly name: string;
783
764
  readonly table: string;
@@ -1,4 +1,5 @@
1
1
  import type { StoredEvent } from "../../event-store/event-store";
2
+ import type { HookPhases } from "../hook-helpers";
2
3
  import type { AppContext } from "./handlers";
3
4
  import type { EntityId } from "./identifiers";
4
5
 
@@ -101,21 +102,6 @@ export type LifecycleHookFn =
101
102
  | PreQueryHookFn
102
103
  | PostQueryHookFn;
103
104
 
104
- // --- Hook Phases ---
105
- //
106
- // inTransaction: Hook runs inside the DB transaction. Failures roll back
107
- // the entire write. Use for: DB-based side-effects (counter updates,
108
- // dependent entity writes).
109
- //
110
- // afterCommit (default): Hook runs after the transaction commits. Failures
111
- // are logged but don't affect the write. Use for: external systems
112
- // (SSE broadcast, search index, email, webhooks).
113
-
114
- export const HookPhases = {
115
- inTransaction: "inTransaction",
116
- afterCommit: "afterCommit",
117
- } as const;
118
-
119
105
  export type HookPhase = (typeof HookPhases)[keyof typeof HookPhases];
120
106
 
121
107
  // Owner-tag shared across every hook structure. The lifecycle pipeline uses
@@ -1,72 +1 @@
1
- // HTTP-Route-Definition feature-deklarierte HTTP-Endpoints außerhalb
2
- // der /api/write|query|batch-Pipeline. Use-Case: RSS/Atom-Feeds, OpenAPI-
3
- // Specs, OG-Image-Generators, Webhook-Receiver — alles wo der Feature-
4
- // Author das Wire-Format selbst kontrolliert.
5
- //
6
- // Pattern symmetrisch zu r.queryHandler / r.writeHandler: Definition als
7
- // Teil des Features (nicht des App-Bootstrapping). Phase-3 Multi-Tenant
8
- // wird trivial weil tenant-context via host-resolution greift.
9
- //
10
- // Escape-hatch bleibt: runProdApp.extraRoutes für hand-rolled Routes die
11
- // nichts mit einem Feature zu tun haben (z.B. plattform-spezifische
12
- // Static-Serving-Logic).
13
-
14
- import type { Context } from "hono";
15
-
16
- /** Subset von HTTP-Methoden den wir aktiv unterstützen. Hono spricht
17
- * alle, aber das hier sind die einzigen die ein Feature-Author
18
- * realistisch deklariert. */
19
- export type HttpRouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
20
-
21
- /** Dependencies die der Handler vom Framework bekommt. App-Author kann
22
- * die App selbst aufrufen (`deps.app.fetch(...)` für intern-call) oder
23
- * direkt per dispatcher Daten ziehen. Db/Redis sind die rohen Connections
24
- * — wer Tenant-Scope braucht muss durch dispatcher.query gehen.
25
- *
26
- * Hono-typing: `Context<any, any>` weil das Hono-Type-Param-Setup nur
27
- * intern relevant ist. Concrete Hono-app wird im Boot-Path zugewiesen. */
28
- export type HttpRouteHandlerDeps = {
29
- /** Die Hono-app — Handler kann via app.fetch(...) interne Routes
30
- * ansprechen (z.B. /api/query mit der vollen Auth-/Anonymous-Chain). */
31
- // biome-ignore lint/suspicious/noExplicitAny: Hono's generic-Param ist im Framework-Boundary unsichtbar
32
- readonly app: import("hono").Hono<any, any>;
33
- /** Run a query handler in-process, forcing a SPECIFIC tenant — WITHOUT
34
- * going through the public /api/query HTTP layer (no header parsing, no
35
- * anonymousAccess tenant resolution). The synthesized caller carries
36
- * anonymous-level access ONLY (same role a real anonymous request would
37
- * have, no more) — the primitive forces the tenant, not the privilege
38
- * level, so it stays safe to call from any `anonymous: true` route
39
- * without risking a field-level disclosure a real anonymous caller
40
- * couldn't already get. Use this whenever the route needs a tenant
41
- * other than the one the request resolves to (e.g. always
42
- * SYSTEM_TENANT_ID regardless of the visited host) — spoofing that via
43
- * an internal X-Tenant header on `app.fetch(...)` is indistinguishable
44
- * from an external client and gets rejected by resolverTrust:
45
- * "authoritative" anonymousAccess configs (see auth-middleware.ts). */
46
- readonly systemQuery: (
47
- type: string,
48
- payload: unknown,
49
- tenantId: import("./identifiers").TenantId,
50
- ) => Promise<unknown>;
51
- };
52
-
53
- export type HttpRouteHandler = (
54
- // biome-ignore lint/suspicious/noExplicitAny: Hono Context-Generics sind im Framework-Boundary unsichtbar
55
- c: Context<any, any>,
56
- deps: HttpRouteHandlerDeps,
57
- ) => Response | Promise<Response>;
58
-
59
- export type HttpRouteDefinition = {
60
- /** HTTP-Methode — bei Hono-Mount via app.{get,post,...}(path). */
61
- readonly method: HttpRouteMethod;
62
- /** URL-Pfad (Hono-Pattern, z.B. "/feed.xml" oder "/og/:tenantId.png"). */
63
- readonly path: string;
64
- /** Wenn true, bypasses die /api/*-Auth-Middleware. Default false —
65
- * Routes liegen außerhalb /api/* und sehen die Auth-Middleware
66
- * ohnehin nicht; das Flag ist semantisch (= "diese Route ist
67
- * bewusst öffentlich") für Boot-Validator + Doku. */
68
- readonly anonymous?: boolean;
69
- /** Hono-Handler. Bekommt Hono-Context + Framework-Deps; returnt
70
- * Response (sync oder async). */
71
- readonly handler: HttpRouteHandler;
72
- };
1
+ export * from "@cosmicdrift/kumiko-types/http-route";
@@ -1,47 +1 @@
1
- // Domain-identifier type aliases. Used everywhere a tenantId/userId/aggregateId
2
- // travels through the framework. One declaration per concept so future
3
- // representation changes (branded types, UUID validation, opaque wrappers)
4
- // land in a single place.
5
-
6
- // Tenant identifier — UUID string today. May become branded/opaque later
7
- // without touching call sites.
8
- export type TenantId = string;
9
-
10
- // Lowercase UUID (any RFC-4122 variant). Strict enough to keep client-
11
- // supplied junk (e.g. SQL fragments, path-traversal probes) out of the
12
- // pipeline; loose enough that v4 / v7 / nil all match. Any caller that
13
- // already holds a TenantId from a trusted source (JWT payload, server
14
- // config) skips this — the helper is for **untrusted input** crossing
15
- // the system boundary.
16
- const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
17
-
18
- // Validates a candidate string against the tenantId format and returns it
19
- // as a TenantId, or `null` when it doesn't match. Use at every system
20
- // boundary that admits untrusted input (HTTP headers, cookies, query
21
- // params). Returning null instead of throwing keeps the caller in charge
22
- // of the rejection shape — middleware returns 400, batch jobs may filter
23
- // + log, and unit tests don't need a try/catch.
24
- export function parseTenantId(value: unknown): TenantId | null {
25
- if (typeof value !== "string") return null;
26
- if (!TENANT_ID_REGEX.test(value)) return null;
27
- return value;
28
- }
29
-
30
- // "System-scope" tenant marker: handlers carry this tenantId when the event
31
- // doesn't belong to any particular tenant (reference data, cross-tenant
32
- // jobs, global config). The concrete UUID is a valid v4 (not all-zeroes —
33
- // Postgres' UUID type rejects invalid variants), chosen to be easy to
34
- // eyeball in logs. Central constant so call sites don't re-type the string
35
- // and the isSystemTenant() check stays in sync.
36
- export const SYSTEM_TENANT_ID: TenantId = "00000000-0000-4000-8000-000000000000";
37
-
38
- export function isSystemTenant(tenantId: TenantId | null | undefined): boolean {
39
- return !tenantId || tenantId === SYSTEM_TENANT_ID;
40
- }
41
-
42
- // Primary-key identifier for any entity row. Two shapes coexist because of
43
- // the entity-def `idType` switch: classic CRUD entities keep `serial` (number),
44
- // while tenant + ES aggregates run on `uuid` (string). Call sites that pass
45
- // the id through to the DB layer stay agnostic; only code that formats ids
46
- // for URLs, logs, or cache keys needs `String(id)` — JS coerces both safely.
47
- export type EntityId = number | string;
1
+ export * from "@cosmicdrift/kumiko-types/identifiers";
@@ -9,6 +9,15 @@ export type {
9
9
  LifecycleHookType,
10
10
  OnDeleteStrategy,
11
11
  } from "../constants";
12
+ export { DEFAULT_CURRENCIES, isFileField } from "../field-helpers";
13
+ export { resolveName, withResponseData } from "../handler-helpers";
14
+ export { HookPhases } from "../hook-helpers";
15
+ export {
16
+ isExtensionEditSection,
17
+ isFormatSpec,
18
+ normalizeEditField,
19
+ normalizeListColumn,
20
+ } from "../screen-helpers";
12
21
  export type {
13
22
  ConfigAccessor,
14
23
  ConfigAccessorFactory,
@@ -54,6 +63,18 @@ export type {
54
63
  TranslationsDef,
55
64
  UiExtensionDef,
56
65
  } from "./config";
66
+ export type {
67
+ QueryHandlerDefinition,
68
+ WriteHandlerDefinition,
69
+ WriteHandlerInput,
70
+ } from "./define-handler";
71
+ export type {
72
+ EntityCrudRegistrar,
73
+ EntityCrudVerb,
74
+ EntityHandlerOptions,
75
+ EntityQueryHandlerOptions,
76
+ RegisterEntityCrudOptions,
77
+ } from "./entity-handlers";
57
78
  // Cross-Feature Compile-Time-Type-Map — features extend per declare-module.
58
79
  export type {
59
80
  KumikoEntityTypeMap,
@@ -62,6 +83,8 @@ export type {
62
83
  KumikoHandlerResultMap,
63
84
  } from "./event-type-map";
64
85
  export type {
86
+ BootCheckContext,
87
+ BootCheckFn,
65
88
  FeatureDefinition,
66
89
  FeatureMetricDef,
67
90
  FeatureMetricType,
@@ -115,7 +138,6 @@ export type {
115
138
  TransitionMap,
116
139
  TzFieldDef,
117
140
  } from "./fields";
118
- export { DEFAULT_CURRENCIES, isFileField } from "./fields";
119
141
  export type {
120
142
  AccessRule,
121
143
  AggregateStreamHandle,
@@ -162,7 +184,6 @@ export type {
162
184
  WriteHandlerFn,
163
185
  WriteResult,
164
186
  } from "./handlers";
165
- export { resolveName, withResponseData } from "./handlers";
166
187
  export type {
167
188
  DeleteContext,
168
189
  EntityHookMap,
@@ -186,7 +207,6 @@ export type {
186
207
  ValidationError,
187
208
  ValidationHookFn,
188
209
  } from "./hooks";
189
- export { HookPhases } from "./hooks";
190
210
  export type {
191
211
  HttpRouteDefinition,
192
212
  HttpRouteHandler,
@@ -197,6 +217,17 @@ export type {
197
217
  export type { EntityId, TenantId } from "./identifiers";
198
218
  export { isSystemTenant, parseTenantId, SYSTEM_TENANT_ID } from "./identifiers";
199
219
  export type { NavDefinition } from "./nav";
220
+ export type {
221
+ FromRule,
222
+ FromRuleKind,
223
+ OwnershipClause,
224
+ OwnershipMap,
225
+ OwnershipRef,
226
+ OwnershipRule,
227
+ SqlFragment,
228
+ WhereRule,
229
+ WhereRuleContext,
230
+ } from "./ownership";
200
231
  export type {
201
232
  EntityProjectionExtension,
202
233
  MspErrorMode,
@@ -253,12 +284,6 @@ export type {
253
284
  ScreenSlots,
254
285
  ToolbarAction,
255
286
  } from "./screen";
256
- export {
257
- isExtensionEditSection,
258
- isFormatSpec,
259
- normalizeEditField,
260
- normalizeListColumn,
261
- } from "./screen";
262
287
  export type { TargetRef } from "./target-ref";
263
288
  export type {
264
289
  Subscribe,