@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.
- package/package.json +7 -2
- package/src/__tests__/consumer-cli.integration.test.ts +110 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +16 -1
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/jwt.test.ts +150 -1
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/api-constants.ts +4 -0
- package/src/api/auth-middleware.ts +48 -59
- package/src/api/auth-routes.ts +51 -17
- package/src/api/index.ts +3 -3
- package/src/api/jwt.ts +148 -7
- package/src/api/pii-leak-guard.ts +5 -2
- package/src/api/server.ts +19 -5
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/query.ts +34 -2
- package/src/consumer-cli.ts +87 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
- package/src/crypto/blind-index.ts +8 -4
- package/src/crypto/event-pii.ts +1 -0
- package/src/crypto/pii-field-encryption.ts +49 -15
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +305 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
- package/src/db/blind-index-cleanup.ts +3 -1
- package/src/db/connection.ts +3 -11
- package/src/db/encryption.ts +2 -3
- package/src/db/entity-table-meta-types.ts +92 -0
- package/src/db/entity-table-meta.ts +16 -90
- package/src/db/queries/backfill-pii.ts +1 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/store-table.test.ts +12 -0
- package/src/engine/boot-validator/action-wiring.ts +1 -1
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/entity-list-screens.ts +1 -1
- package/src/engine/boot-validator/gdpr-storage.ts +0 -112
- package/src/engine/boot-validator/index.ts +3 -9
- package/src/engine/boot-validator/screens.ts +1 -1
- package/src/engine/define-feature.ts +1 -0
- package/src/engine/define-handler.ts +10 -91
- package/src/engine/entity-handlers.ts +15 -27
- package/src/engine/feature-builder-state.ts +3 -0
- package/src/engine/feature-config-events-jobs.ts +1 -1
- package/src/engine/feature-entity-handlers.ts +1 -1
- package/src/engine/feature-ui-extensions.ts +5 -1
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +14 -0
- package/src/engine/index.ts +2 -2
- package/src/engine/ownership.ts +22 -76
- package/src/engine/registry-validate.ts +1 -1
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/define-handler.ts +94 -0
- package/src/engine/types/entity-handlers.ts +30 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +45 -0
- package/src/engine/types/fields.ts +19 -31
- package/src/engine/types/handlers.ts +7 -26
- package/src/engine/types/hooks.ts +1 -15
- package/src/engine/types/http-route.ts +1 -72
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +34 -9
- package/src/engine/types/ownership.ts +83 -0
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +0 -46
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -129
- package/src/entrypoint/index.ts +2 -2
- package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
- package/src/event-store/event-store.ts +28 -32
- package/src/event-store/events-schema.ts +1 -10
- package/src/event-store/index.ts +3 -2
- package/src/event-store/types.ts +22 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/file-handle.ts +2 -19
- package/src/i18n/required-surface-keys.ts +1 -1
- package/src/logging/types.ts +1 -7
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -58
- package/src/pipeline/__tests__/dispatcher.test.ts +38 -1
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
- package/src/pipeline/dispatch-shared.ts +12 -2
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +28 -3
- package/src/pipeline/event-dispatcher-admin.ts +4 -0
- package/src/pipeline/event-dispatcher-delivery.ts +29 -3
- package/src/pipeline/event-dispatcher.ts +27 -1
- package/src/pipeline/system-hooks.ts +7 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/envelope-cipher.ts +13 -39
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/testing/__tests__/late-bound.test.ts +25 -0
- package/src/testing/__tests__/wait-for.test.ts +53 -0
- package/src/testing/boot-validator-fixture.ts +1 -1
- package/src/testing/file-provider-contract.ts +84 -0
- package/src/testing/handler-context.ts +1 -1
- package/src/testing/index.ts +1 -0
- package/src/time/geo-tz.ts +1 -32
- 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
|
-
|
|
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
|
|
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 "
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
330
|
-
"
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|