@cosmicdrift/kumiko-framework 0.158.2 → 0.160.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -2
- package/src/__tests__/consumer-cli.integration.test.ts +110 -0
- package/src/api/__tests__/api.test.ts +65 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +237 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +1 -0
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +150 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +41 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/api-constants.ts +5 -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/routes.ts +57 -0
- 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 +46 -27
- 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/kms-adapter.ts +2 -118
- package/src/crypto/pii-field-encryption.ts +49 -15
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- 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 +396 -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/cursor.ts +1 -18
- package/src/db/dialect.ts +8 -19
- package/src/db/encryption.ts +2 -3
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +16 -90
- package/src/db/event-store-executor.ts +4 -96
- package/src/db/queries/backfill-pii.ts +1 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/table-builder.ts +2 -19
- package/src/db/tenant-db.ts +6 -55
- 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__/boot-validator.test.ts +46 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +139 -10
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +28 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/registry-facade-sweep.test.ts +80 -0
- package/src/engine/__tests__/registry.test.ts +40 -0
- package/src/engine/__tests__/store-table.test.ts +12 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- 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-handler.ts +10 -1
- 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 +2 -0
- package/src/engine/define-handler.ts +11 -91
- package/src/engine/entity-handlers.ts +15 -27
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +11 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +983 -3
- package/src/engine/feature-ast/__tests__/patch.test.ts +168 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +7 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +9 -0
- package/src/engine/feature-ast/extractors/handlers.ts +19 -2
- package/src/engine/feature-ast/extractors/index.ts +1 -0
- package/src/engine/feature-ast/index.ts +2 -0
- package/src/engine/feature-ast/parse.ts +3 -0
- package/src/engine/feature-ast/patch.ts +2 -0
- package/src/engine/feature-ast/patcher.ts +21 -0
- package/src/engine/feature-ast/patterns.ts +16 -0
- package/src/engine/feature-ast/render.ts +15 -0
- package/src/engine/feature-builder-state.ts +6 -0
- package/src/engine/feature-config-events-jobs.ts +1 -1
- package/src/engine/feature-entity-handlers.ts +36 -2
- 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 +5 -2
- package/src/engine/ownership.ts +22 -76
- package/src/engine/pattern-library/__tests__/library.test.ts +9 -0
- package/src/engine/pattern-library/library.ts +2 -0
- package/src/engine/pattern-library/mixed-schemas.ts +37 -0
- package/src/engine/registry-facade.ts +9 -0
- package/src/engine/registry-ingest.ts +10 -0
- package/src/engine/registry-state.ts +3 -0
- 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/config.ts +2 -497
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -976
- package/src/engine/types/fields.ts +2 -697
- package/src/engine/types/handlers.ts +2 -839
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -72
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +66 -33
- package/src/engine/types/nav.ts +2 -67
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -165
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -793
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -129
- package/src/engine/types/workspace.ts +2 -42
- package/src/entrypoint/index.ts +2 -2
- package/src/errors/write-error-info.ts +6 -22
- 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/errors.ts +2 -35
- package/src/event-store/event-store.ts +28 -51
- package/src/event-store/events-schema.ts +1 -10
- package/src/event-store/index.ts +3 -2
- package/src/event-store/snapshot.ts +11 -35
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/provider-resolver.ts +3 -5
- package/src/files/types.ts +5 -54
- package/src/i18n/required-surface-keys.ts +1 -1
- package/src/jobs/__tests__/jobs.integration.test.ts +102 -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 +134 -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/__tests__/lifecycle-pipeline.test.ts +208 -0
- package/src/pipeline/dispatch-shared.ts +51 -3
- package/src/pipeline/dispatch-stream.ts +74 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +7 -0
- 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/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/system-hooks.ts +7 -0
- package/src/rate-limit/resolver.ts +10 -30
- 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 +17 -45
- package/src/secrets/types.ts +2 -177
- 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/time/tz-context.ts +9 -56
- package/src/ui-types/index.ts +7 -7
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Glob } from "bun";
|
|
3
|
+
|
|
4
|
+
// getUnscopedAggregateStreamMaxVersion / getUnscopedAggregateStreamTenant have
|
|
5
|
+
// no tenant filter — a caller can use them to probe whether a foreign tenant's
|
|
6
|
+
// aggregate exists (see event-store.ts SECURITY doc). Restricted to known
|
|
7
|
+
// seed/system-internal callers; extend only for genuine new ones.
|
|
8
|
+
const RESTRICTED_SYMBOLS = [
|
|
9
|
+
"getUnscopedAggregateStreamMaxVersion",
|
|
10
|
+
"getUnscopedAggregateStreamTenant",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const ALLOWED_FILES = new Set([
|
|
14
|
+
"packages/framework/src/event-store/event-store.ts",
|
|
15
|
+
"packages/framework/src/event-store/index.ts",
|
|
16
|
+
"packages/bundled-features/src/tenant/seeding.ts",
|
|
17
|
+
"packages/bundled-features/src/tier-engine/feature.ts",
|
|
18
|
+
"packages/bundled-features/src/auth-email-password/__tests__/email-verification.integration.test.ts",
|
|
19
|
+
"packages/bundled-features/src/auth-email-password/__tests__/password-reset.integration.test.ts",
|
|
20
|
+
"packages/framework/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = `${import.meta.dir}/../../../../..`;
|
|
24
|
+
|
|
25
|
+
describe("unscoped stream primitives — caller allowlist", () => {
|
|
26
|
+
test("only seed/system-internal paths reference the existence-oracle primitives", async () => {
|
|
27
|
+
const glob = new Glob("packages/{framework,bundled-features}/src/**/*.ts");
|
|
28
|
+
const matches = new Set<string>();
|
|
29
|
+
for await (const relPath of glob.scan({ cwd: REPO_ROOT })) {
|
|
30
|
+
const content = await Bun.file(`${REPO_ROOT}/${relPath}`).text();
|
|
31
|
+
if (RESTRICTED_SYMBOLS.some((symbol) => content.includes(symbol))) {
|
|
32
|
+
matches.add(relPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Positive control — proves the scan actually ran and found the known
|
|
37
|
+
// caller, not just that it (silently) found nothing.
|
|
38
|
+
expect(matches.has("packages/bundled-features/src/tenant/seeding.ts")).toBe(true);
|
|
39
|
+
|
|
40
|
+
const offenders = [...matches].filter((relPath) => !ALLOWED_FILES.has(relPath));
|
|
41
|
+
expect(offenders).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,35 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// WriteResult error contract (version_conflict).
|
|
4
|
-
|
|
5
|
-
export class VersionConflictError extends Error {
|
|
6
|
-
public readonly aggregateId: string;
|
|
7
|
-
public readonly expectedVersion: number;
|
|
8
|
-
constructor(aggregateId: string, expectedVersion: number) {
|
|
9
|
-
super(
|
|
10
|
-
`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`,
|
|
11
|
-
);
|
|
12
|
-
this.name = "VersionConflictError";
|
|
13
|
-
this.aggregateId = aggregateId;
|
|
14
|
-
this.expectedVersion = expectedVersion;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
|
|
19
|
-
// are read-only — restoreStream() makes them writable again. The archive
|
|
20
|
-
// state is not carried on the events themselves; it lives on the sparse
|
|
21
|
-
// kumiko_archived_streams table. Handlers that need to branch on archive
|
|
22
|
-
// state should call ctx.isStreamArchived(id) first.
|
|
23
|
-
export class ArchivedStreamError extends Error {
|
|
24
|
-
public readonly tenantId: string;
|
|
25
|
-
public readonly aggregateId: string;
|
|
26
|
-
constructor(tenantId: string, aggregateId: string) {
|
|
27
|
-
super(
|
|
28
|
-
`Aggregate ${aggregateId} on tenant ${tenantId} is archived — appendEvent is blocked. ` +
|
|
29
|
-
`Call restoreStream() to re-open the stream before writing.`,
|
|
30
|
-
);
|
|
31
|
-
this.name = "ArchivedStreamError";
|
|
32
|
-
this.tenantId = tenantId;
|
|
33
|
-
this.aggregateId = aggregateId;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
// Legacy path — re-exported for callers still importing this module directly.
|
|
2
|
+
export * from "@cosmicdrift/kumiko-types/event-store-errors";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
1
2
|
import { encryptEventPayloadPii } from "../crypto/event-pii";
|
|
2
3
|
import type { DbRunner } from "../db";
|
|
3
4
|
import { isUniqueViolation } from "../db/pg-error";
|
|
@@ -16,28 +17,7 @@ import { VersionConflictError } from "./errors";
|
|
|
16
17
|
import { eventsTable } from "./events-schema";
|
|
17
18
|
import { toStoredEvent } from "./row-to-stored-event";
|
|
18
19
|
|
|
19
|
-
export type EventMetadata
|
|
20
|
-
readonly userId: string;
|
|
21
|
-
readonly requestId?: string;
|
|
22
|
-
// End-to-end business-operation id. Root HTTP requests get it from the
|
|
23
|
-
// x-correlation-id header (default: requestId). MSP-applies inherit it
|
|
24
|
-
// from the triggering event. Lets you trace "which user click caused
|
|
25
|
-
// this email 3 streams later?".
|
|
26
|
-
readonly correlationId?: string;
|
|
27
|
-
// Stored event id that triggered this write. Null for root commands;
|
|
28
|
-
// set to event.id when an MSP-apply runs ctx.appendEvent. Together with
|
|
29
|
-
// correlationId forms a causation DAG across aggregate streams.
|
|
30
|
-
readonly causationId?: string;
|
|
31
|
-
// Marten-conform free key/value space for app-specific metadata that
|
|
32
|
-
// doesn't deserve its own EventMetadata field. Examples: A/B-test bucket,
|
|
33
|
-
// feature-flag snapshot, geo-region, client SDK version. Persisted into
|
|
34
|
-
// events.metadata jsonb (no schema change — it's already a free-form
|
|
35
|
-
// jsonb column), survives upcasters untouched, available on every
|
|
36
|
-
// StoredEvent.metadata.headers. Framework does not interpret values; the
|
|
37
|
-
// app reads them when filtering/auditing. Keep values JSON-primitive
|
|
38
|
-
// (string|number|boolean) so JSON serialization stays bulletproof.
|
|
39
|
-
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
40
|
-
};
|
|
20
|
+
export type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
41
21
|
|
|
42
22
|
export type EventToAppend = {
|
|
43
23
|
readonly aggregateId: string;
|
|
@@ -51,25 +31,6 @@ export type EventToAppend = {
|
|
|
51
31
|
readonly metadata: EventMetadata;
|
|
52
32
|
};
|
|
53
33
|
|
|
54
|
-
// Generic über payload-shape. Default = Record<string, unknown> macht
|
|
55
|
-
// alle existierenden Konsumenten backwards-compatible. Konkrete Apply-
|
|
56
|
-
// Handler / Tests können `StoredEvent<MyEventPayload>` annotieren um
|
|
57
|
-
// payload typed zu lesen. Type-Propagation kommt durch r.defineEvent +
|
|
58
|
-
// SingleStreamApplyFn<T> in apply-Maps.
|
|
59
|
-
export type StoredEvent<TPayload = Record<string, unknown>> = {
|
|
60
|
-
readonly id: string;
|
|
61
|
-
readonly aggregateId: string;
|
|
62
|
-
readonly aggregateType: string;
|
|
63
|
-
readonly tenantId: TenantId;
|
|
64
|
-
readonly version: number;
|
|
65
|
-
readonly type: string;
|
|
66
|
-
readonly eventVersion: number;
|
|
67
|
-
readonly payload: TPayload;
|
|
68
|
-
readonly metadata: EventMetadata;
|
|
69
|
-
readonly createdAt: Temporal.Instant;
|
|
70
|
-
readonly createdBy: string;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
34
|
type SelectedEvent = {
|
|
74
35
|
readonly id: bigint;
|
|
75
36
|
readonly aggregateId: string;
|
|
@@ -281,9 +242,13 @@ export async function getStreamVersion(
|
|
|
281
242
|
return selectStreamMaxVersion(db, aggregateId, tenantId);
|
|
282
243
|
}
|
|
283
244
|
|
|
284
|
-
/** MAX(version) for one aggregate — no tenant filter.
|
|
245
|
+
/** MAX(version) for one aggregate — no tenant filter. SECURITY: existence-oracle,
|
|
246
|
+
* a caller can probe whether an aggregateId has any events regardless of tenant
|
|
247
|
+
* membership. Only call from seed/system-internal paths (idempotency checks
|
|
248
|
+
* against a known aggregateId) — never from a handler reachable with
|
|
249
|
+
* caller-controlled input. */
|
|
285
250
|
// @wrapper-known semantic-alias
|
|
286
|
-
export async function
|
|
251
|
+
export async function getUnscopedAggregateStreamMaxVersion(
|
|
287
252
|
db: DbRunner,
|
|
288
253
|
aggregateId: string,
|
|
289
254
|
): Promise<number> {
|
|
@@ -291,10 +256,11 @@ export async function getAggregateStreamMaxVersion(
|
|
|
291
256
|
}
|
|
292
257
|
|
|
293
258
|
/** Stream tenant of an aggregate (the tenant_id its events live under), with no
|
|
294
|
-
* membership/tenant filter.
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
|
|
259
|
+
* membership/tenant filter. SECURITY: existence-oracle, same caveat as
|
|
260
|
+
* getUnscopedAggregateStreamMaxVersion — seed/system-internal use only. Recovers
|
|
261
|
+
* the write target for a systemScope aggregate whose stream tenant isn't one of
|
|
262
|
+
* the subject's memberships. Returns null for unknown streams. */
|
|
263
|
+
export async function getUnscopedAggregateStreamTenant(
|
|
298
264
|
db: DbRunner,
|
|
299
265
|
aggregateId: string,
|
|
300
266
|
aggregateType: string,
|
|
@@ -333,15 +299,19 @@ export async function loadEventsAfterVersion(
|
|
|
333
299
|
|
|
334
300
|
// Load every event for an aggregate_type across all tenants. Ordered by
|
|
335
301
|
// (created_at, id) — chronological replay order for projection rebuilds.
|
|
336
|
-
//
|
|
337
|
-
// CAUTION — buffers ALL matching events in memory. Safe for smaller
|
|
338
|
-
// aggregate-types (≤ 100k events), a memory cliff for large stores.
|
|
339
|
-
// For >100k events use `streamAllEventsByType` (yields batchwise).
|
|
340
302
|
// Mostly called from tests today — production rebuild goes through
|
|
341
303
|
// projection-rebuild's own streaming path.
|
|
304
|
+
//
|
|
305
|
+
// Fails loud past LOAD_ALL_EVENTS_ROW_LIMIT rather than silently buffering
|
|
306
|
+
// an unbounded result set — that's the memory cliff this guard exists to
|
|
307
|
+
// prevent.
|
|
308
|
+
export const LOAD_ALL_EVENTS_ROW_LIMIT = 100_000;
|
|
309
|
+
|
|
310
|
+
/** @deprecated buffers ALL matching events in memory — a memory cliff for large stores. Use `streamAllEventsByType` (yields batchwise) instead. */
|
|
342
311
|
export async function loadAllEventsByType(
|
|
343
312
|
db: DbRunner,
|
|
344
313
|
aggregateType: string,
|
|
314
|
+
rowLimit: number = LOAD_ALL_EVENTS_ROW_LIMIT,
|
|
345
315
|
): Promise<readonly StoredEvent[]> {
|
|
346
316
|
const rows = await selectMany<SelectedEvent>(
|
|
347
317
|
db,
|
|
@@ -352,8 +322,15 @@ export async function loadAllEventsByType(
|
|
|
352
322
|
{ col: "createdAt", direction: "asc" },
|
|
353
323
|
{ col: "id", direction: "asc" },
|
|
354
324
|
],
|
|
325
|
+
limit: rowLimit + 1,
|
|
355
326
|
},
|
|
356
327
|
);
|
|
328
|
+
if (rows.length > rowLimit) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`loadAllEventsByType("${aggregateType}") exceeds ${rowLimit} rows — ` +
|
|
331
|
+
"use streamAllEventsByType instead of buffering the full result set in memory.",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
357
334
|
return rows.map(toStoredEvent);
|
|
358
335
|
}
|
|
359
336
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { unsafePushTables } from "../stack";
|
|
16
16
|
import { createArchivedStreamsTable } from "./archive";
|
|
17
17
|
import { createSnapshotsTable } from "./snapshot";
|
|
18
|
+
import type { EventMetadata } from "./types";
|
|
18
19
|
|
|
19
20
|
// Event-store schema as a Drizzle table. The typed select/insert path handles
|
|
20
21
|
// most operations; append() for subsequent versions uses raw SQL because
|
|
@@ -24,16 +25,6 @@ import { createSnapshotsTable } from "./snapshot";
|
|
|
24
25
|
// (Redis-backed check + cached-response replay). The event-store itself
|
|
25
26
|
// imposes no idempotency index — a single HTTP request may write N events
|
|
26
27
|
// freely, metadata.requestId is purely a trace marker.
|
|
27
|
-
export type EventMetadata = {
|
|
28
|
-
readonly userId: string;
|
|
29
|
-
readonly requestId?: string;
|
|
30
|
-
readonly correlationId?: string;
|
|
31
|
-
readonly causationId?: string;
|
|
32
|
-
// App-specific free key/value (Marten "headers"). Mirror of the canonical
|
|
33
|
-
// type in event-store.ts — kept duplicate because events-schema must stay
|
|
34
|
-
// import-cycle-free vs the event-store module.
|
|
35
|
-
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
36
|
-
};
|
|
37
28
|
|
|
38
29
|
export const eventsTable = pgTable(
|
|
39
30
|
"kumiko_events",
|
package/src/event-store/index.ts
CHANGED
|
@@ -18,10 +18,11 @@ export {
|
|
|
18
18
|
EVENTS_PUBSUB_CHANNEL,
|
|
19
19
|
type EventMetadata,
|
|
20
20
|
type EventToAppend,
|
|
21
|
-
getAggregateStreamMaxVersion,
|
|
22
|
-
getAggregateStreamTenant,
|
|
23
21
|
getEventsHighWaterMark,
|
|
24
22
|
getStreamVersion,
|
|
23
|
+
getUnscopedAggregateStreamMaxVersion,
|
|
24
|
+
getUnscopedAggregateStreamTenant,
|
|
25
|
+
LOAD_ALL_EVENTS_ROW_LIMIT,
|
|
25
26
|
loadAggregate,
|
|
26
27
|
loadAggregateAsOf,
|
|
27
28
|
loadAllEventsByType,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// sql now comes from native dialect
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
LoadAggregateWithSnapshotOptions,
|
|
5
|
+
LoadAggregateWithSnapshotResult,
|
|
6
|
+
SnapshotReducer,
|
|
7
|
+
} from "@cosmicdrift/kumiko-types/snapshot-types";
|
|
3
8
|
import type { DbConnection, DbRunner } from "../db/connection";
|
|
4
9
|
import {
|
|
5
10
|
index,
|
|
@@ -18,7 +23,7 @@ import { tableExists } from "../db/schema-inspection";
|
|
|
18
23
|
import type { TenantId } from "../engine/types";
|
|
19
24
|
import { unsafePushTables } from "../stack";
|
|
20
25
|
import { isStreamArchived } from "./archive";
|
|
21
|
-
import { loadEventsAfterVersion
|
|
26
|
+
import { loadEventsAfterVersion } from "./event-store";
|
|
22
27
|
|
|
23
28
|
// Marten-aligned snapshot store. A snapshot is a point-in-time materialised
|
|
24
29
|
// state of an aggregate at a specific version, cached so rehydrating the
|
|
@@ -157,40 +162,11 @@ export async function loadLatestSnapshot<
|
|
|
157
162
|
};
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
event: StoredEvent,
|
|
166
|
-
) => TState;
|
|
167
|
-
|
|
168
|
-
export type LoadAggregateWithSnapshotResult<TState extends Record<string, unknown>> = {
|
|
169
|
-
readonly state: TState;
|
|
170
|
-
readonly version: number;
|
|
171
|
-
readonly snapshotHit: boolean;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
export type LoadAggregateWithSnapshotOptions = {
|
|
175
|
-
// Opt-in: include archived streams in the rehydrate. Default false — same
|
|
176
|
-
// semantics as loadAggregate / loadAggregateAsOf. Archive check is a
|
|
177
|
-
// single indexed lookup, so the cost stays negligible on the hot path.
|
|
178
|
-
readonly includeArchived?: boolean;
|
|
179
|
-
// Optional upcaster step: every delta event goes through this transform
|
|
180
|
-
// BEFORE the reducer sees it. The dispatcher wires this up with
|
|
181
|
-
// r.eventMigration so feature code always sees current-version payloads.
|
|
182
|
-
// Async to support Marten-style AsyncOnlyEventUpcaster (DB lookups).
|
|
183
|
-
readonly upcastEvent?: (event: StoredEvent) => Promise<StoredEvent>;
|
|
184
|
-
// Auto-snapshot policy: when the fold applied at least this many delta
|
|
185
|
-
// events, persist a fresh snapshot at the folded version (best-effort —
|
|
186
|
-
// a failed save never fails the load). Omit to keep snapshotting manual.
|
|
187
|
-
readonly snapshotEvery?: number;
|
|
188
|
-
// Reducer-shape generation stamped onto saved snapshots (default 1). A
|
|
189
|
-
// stored snapshot with a different generation is ignored — full replay
|
|
190
|
-
// through the upcaster chain — and restamped on the next auto-save. Bump
|
|
191
|
-
// whenever the reducer's state shape changes.
|
|
192
|
-
readonly snapshotVersion?: number;
|
|
193
|
-
};
|
|
165
|
+
export type {
|
|
166
|
+
LoadAggregateWithSnapshotOptions,
|
|
167
|
+
LoadAggregateWithSnapshotResult,
|
|
168
|
+
SnapshotReducer,
|
|
169
|
+
} from "@cosmicdrift/kumiko-types/snapshot-types";
|
|
194
170
|
|
|
195
171
|
// Snapshot-aware rehydrate. Loads the latest snapshot (if any), applies
|
|
196
172
|
// events strictly newer than snapshot.version, and returns the fold.
|
package/src/files/file-handle.ts
CHANGED
|
@@ -10,27 +10,10 @@
|
|
|
10
10
|
// suffix before the file extension — `foo/bar.jpg` + `"medium"` →
|
|
11
11
|
// `foo/bar.medium.jpg`. Stable, reversible, no extra lookup tables.
|
|
12
12
|
|
|
13
|
+
import type { FileContext, FileHandle } from "@cosmicdrift/kumiko-types/file-handle-types";
|
|
13
14
|
import type { FileStorageProvider } from "./types";
|
|
14
15
|
|
|
15
|
-
export type FileHandle
|
|
16
|
-
readonly key: string;
|
|
17
|
-
read(): Promise<Uint8Array>;
|
|
18
|
-
write(data: Uint8Array, mimeType?: string): Promise<void>;
|
|
19
|
-
delete(): Promise<void>;
|
|
20
|
-
exists(): Promise<boolean>;
|
|
21
|
-
// Produce a handle for a derived key (e.g. a thumbnail). Does not touch
|
|
22
|
-
// storage; only computes the key. Writing to the derived handle is the
|
|
23
|
-
// caller's job.
|
|
24
|
-
derive(suffix: string): FileHandle;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// The `ctx.files` service — a factory that materialises a FileHandle for a
|
|
28
|
-
// storage key. One per request/event, bound to a single tenant: the provider
|
|
29
|
-
// is resolved per-tenant through file-foundation, so uploads, ctx.files and the
|
|
30
|
-
// GDPR jobs all hit the same store by construction.
|
|
31
|
-
export type FileContext = {
|
|
32
|
-
ref(key: string): FileHandle;
|
|
33
|
-
};
|
|
16
|
+
export type { FileContext, FileHandle };
|
|
34
17
|
|
|
35
18
|
// `getProvider` is a lazily-resolved, memoized accessor — the provider is
|
|
36
19
|
// resolved (config + s3.secretAccessKey secret read) only when a handle method
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// file-foundation re-exports `createFileProviderForTenant` + the plugin types
|
|
13
13
|
// (moved here from there) so existing imports keep working.
|
|
14
14
|
|
|
15
|
+
import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
15
16
|
import type { DbConnection } from "../db/connection";
|
|
16
17
|
import type { TenantDb } from "../db/tenant-db";
|
|
17
18
|
import { EXT_FILE_PROVIDER, FILE_PROVIDER_CONFIG_KEY } from "../engine/extension-names";
|
|
18
19
|
import { SYSTEM_USER_ID } from "../engine/system-user";
|
|
19
|
-
import type { ConfigAccessor, ConfigAccessorFactory, Registry
|
|
20
|
+
import type { ConfigAccessor, ConfigAccessorFactory, Registry } from "../engine/types";
|
|
20
21
|
import type { SecretsContext } from "../secrets";
|
|
21
22
|
import type { FileStorageProvider } from "./types";
|
|
22
23
|
|
|
@@ -120,10 +121,7 @@ export async function createFileProviderForTenant(
|
|
|
120
121
|
return usage.options.build(ctx, tenantId);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
// (tenantId is the call argument) — the single spine shared by upload routes,
|
|
125
|
-
// ctx.files and the GDPR jobs.
|
|
126
|
-
export type FileProviderResolver = (tenantId: TenantId) => Promise<FileStorageProvider>;
|
|
124
|
+
export type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
127
125
|
|
|
128
126
|
export type FileProviderResolverDeps = {
|
|
129
127
|
readonly registry?: Registry;
|
package/src/files/types.ts
CHANGED
|
@@ -6,60 +6,11 @@ export type FileMetadata = {
|
|
|
6
6
|
readonly size: number;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
readonly contentDisposition?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Options fuer `writeStream`. `mimeType` ist Content-Type-Hint analog zu
|
|
18
|
-
// `write`. `contentLength` ist optional fuer Provider die einen Length-
|
|
19
|
-
// Header brauchen (S3 multipart hat einen TransferManager, kann auch ohne
|
|
20
|
-
// length); local-Provider ignoriert beides.
|
|
21
|
-
export type WriteStreamOptions = {
|
|
22
|
-
readonly mimeType?: string;
|
|
23
|
-
readonly contentLength?: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// Primitive storage contract: key+bytes in, bytes out. Metadata (fileName,
|
|
27
|
-
// mimeType, size) lives on the FileRef row — the provider only needs to
|
|
28
|
-
// shuttle bytes. `mimeType` on write() is a hint for providers that need a
|
|
29
|
-
// Content-Type header (S3/R2/…); local filesystems can ignore it.
|
|
30
|
-
//
|
|
31
|
-
// **Streaming (`writeStream` + `readStream`) ist PFLICHT** — beide
|
|
32
|
-
// Methoden sind required, kein optional-feature. Begruendung:
|
|
33
|
-
// - User-Data-Export (Atom 3c) braucht beide, sonst silent fail bei
|
|
34
|
-
// erstem Job mit fileRefs in Production.
|
|
35
|
-
// - Apps die nur kleine Files (Avatar-Uploads, Profile-Pics) handeln,
|
|
36
|
-
// koennen trivial via `oneShot`-Pattern den Stream-Contract erfuellen
|
|
37
|
-
// (single-chunk yield von write/read-Bytes). 5 Zeilen pro Provider.
|
|
38
|
-
// - Optional-Type wuerde TypeScript-Lying erlauben: Type sagt "kann
|
|
39
|
-
// fehlen", Worker throws zur Runtime → App-Authors sehen den Bug
|
|
40
|
-
// erst in Production. Required + TS-enforced ist ehrlich.
|
|
41
|
-
//
|
|
42
|
-
// `getSignedUrl` BLEIBT optional: object-store backends (S3/R2/GCS)
|
|
43
|
-
// implement it so clients can download directly from the provider after
|
|
44
|
-
// the server has checked access — offloads bandwidth and enables browser-
|
|
45
|
-
// native caching. Filesystem providers leave it undefined; the route then
|
|
46
|
-
// returns 501 and the client falls back to streaming via GET /files/:id.
|
|
47
|
-
// Callers must feature-detect via `typeof provider.getSignedUrl === "function"`.
|
|
48
|
-
// Hier ist Optional korrekt weil die Fallback-Pfad existiert — kein
|
|
49
|
-
// silent-fail, sondern 501 + alternativer download.
|
|
50
|
-
export type FileStorageProvider = {
|
|
51
|
-
write(key: string, data: Uint8Array, mimeType?: string): Promise<void>;
|
|
52
|
-
writeStream(
|
|
53
|
-
key: string,
|
|
54
|
-
source: AsyncIterable<Uint8Array>,
|
|
55
|
-
options?: WriteStreamOptions,
|
|
56
|
-
): Promise<void>;
|
|
57
|
-
read(key: string): Promise<Uint8Array>;
|
|
58
|
-
readStream(key: string): AsyncIterable<Uint8Array>;
|
|
59
|
-
delete(key: string): Promise<void>;
|
|
60
|
-
exists(key: string): Promise<boolean>;
|
|
61
|
-
getSignedUrl?(key: string, expiresInSeconds: number, options?: SignedUrlOptions): Promise<string>;
|
|
62
|
-
};
|
|
9
|
+
export type {
|
|
10
|
+
FileStorageProvider,
|
|
11
|
+
SignedUrlOptions,
|
|
12
|
+
WriteStreamOptions,
|
|
13
|
+
} from "@cosmicdrift/kumiko-types/file-storage-provider-types";
|
|
63
14
|
|
|
64
15
|
export type FileValidationOptions = {
|
|
65
16
|
readonly maxSize?: string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isExtensionEditSection, normalizeListColumn } from "../engine/screen-helpers";
|
|
1
2
|
import type {
|
|
2
3
|
ActionFormScreenDefinition,
|
|
3
4
|
ConfigEditScreenDefinition,
|
|
@@ -15,7 +16,6 @@ import type {
|
|
|
15
16
|
ToolbarAction,
|
|
16
17
|
WorkspaceDefinition,
|
|
17
18
|
} from "../engine/types";
|
|
18
|
-
import { isExtensionEditSection, normalizeListColumn } from "../engine/types/screen";
|
|
19
19
|
|
|
20
20
|
/** Pseudo-entity for actionForm field labels (renderer action-form-shim). */
|
|
21
21
|
export const ACTION_FORM_ENTITY = "__action-form__";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { requestContext } from "../../api/request-context";
|
|
3
4
|
import { createRegistry, defineFeature } from "../../engine";
|
|
4
5
|
import type { AppContext, Registry } from "../../engine/types";
|
|
5
|
-
import { createTestRedis, type TestRedis } from "../../stack";
|
|
6
|
+
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
6
7
|
import { sleep, waitFor } from "../../testing";
|
|
7
8
|
import {
|
|
8
9
|
createJobRunner,
|
|
@@ -103,6 +104,30 @@ const testFeature = defineFeature("test", (r) => {
|
|
|
103
104
|
await sleep(500);
|
|
104
105
|
},
|
|
105
106
|
);
|
|
107
|
+
r.writeHandler(
|
|
108
|
+
"capped-event",
|
|
109
|
+
z.object({ n: z.number() }),
|
|
110
|
+
async (event) => ({ isSuccess: true, data: { n: event.payload.n } }),
|
|
111
|
+
{ access: { openToAll: true } },
|
|
112
|
+
);
|
|
113
|
+
// Event-triggered twin — exercises handleEvent's maxPerTenant guard
|
|
114
|
+
// (dispatch and handleEvent share isOverPerTenantLimit).
|
|
115
|
+
r.job(
|
|
116
|
+
"perTenantLimitedEvent",
|
|
117
|
+
{
|
|
118
|
+
trigger: { on: "test:write:capped-event" },
|
|
119
|
+
concurrency: "parallel",
|
|
120
|
+
maxPerTenant: 2,
|
|
121
|
+
},
|
|
122
|
+
async (payload) => {
|
|
123
|
+
jobLog.push({
|
|
124
|
+
name: "test:job:per-tenant-limited-event",
|
|
125
|
+
payload,
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
});
|
|
128
|
+
await sleep(500);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
106
131
|
|
|
107
132
|
// Job that fails
|
|
108
133
|
r.job("failingJob", { trigger: { manual: true }, retries: 1 }, async () => {
|
|
@@ -130,6 +155,18 @@ const testFeature = defineFeature("test", (r) => {
|
|
|
130
155
|
timestamp: Date.now(),
|
|
131
156
|
});
|
|
132
157
|
});
|
|
158
|
+
// Exercises createJobLogger (info/warn/error/debug/child) — otherwise those
|
|
159
|
+
// one-liners stay uncovered even though every job builds a logger.
|
|
160
|
+
r.job("logProbe", { trigger: { manual: true } }, async (payload, ctx) => {
|
|
161
|
+
expect(ctx.log).toBeDefined();
|
|
162
|
+
const log = ctx.log!;
|
|
163
|
+
log.info("log-probe-info", { n: payload["n"] });
|
|
164
|
+
log.warn("log-probe-warn");
|
|
165
|
+
log.error("log-probe-error", { ok: false });
|
|
166
|
+
log.debug("log-probe-debug");
|
|
167
|
+
log.child({ probe: true }).info("log-probe-child");
|
|
168
|
+
jobLog.push({ name: "test:job:log-probe", payload, timestamp: Date.now() });
|
|
169
|
+
});
|
|
133
170
|
});
|
|
134
171
|
|
|
135
172
|
beforeAll(async () => {
|
|
@@ -423,6 +460,26 @@ describe("concurrency: debounce", () => {
|
|
|
423
460
|
});
|
|
424
461
|
});
|
|
425
462
|
|
|
463
|
+
describe("concurrency: replace", () => {
|
|
464
|
+
test("enqueuer-only: each dispatch removes prior waiting peers", async () => {
|
|
465
|
+
// No consumer → jobs stay waiting. The replace branch walks getWaiting()
|
|
466
|
+
// and removes same-name peers before add — so three dispatches leave one.
|
|
467
|
+
await withRunner(
|
|
468
|
+
async (runner) => {
|
|
469
|
+
const id1 = await runner.dispatch("test:job:replace-job", { n: 1 });
|
|
470
|
+
const id2 = await runner.dispatch("test:job:replace-job", { n: 2 });
|
|
471
|
+
const id3 = await runner.dispatch("test:job:replace-job", { n: 3 });
|
|
472
|
+
expect(id1).toBeDefined();
|
|
473
|
+
expect(id2).toBeDefined();
|
|
474
|
+
expect(id3).toBeDefined();
|
|
475
|
+
expect(id1).not.toBe("skipped");
|
|
476
|
+
expect(id3).not.toBe(id1);
|
|
477
|
+
},
|
|
478
|
+
{ consumerLane: undefined },
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
426
483
|
describe("concurrency: maxPerTenant", () => {
|
|
427
484
|
test("max=2: third dispatch for same tenant returns skipped, other tenant unaffected", async () => {
|
|
428
485
|
clearLog();
|
|
@@ -486,6 +543,18 @@ describe("concurrency: maxPerTenant", () => {
|
|
|
486
543
|
|
|
487
544
|
// --- Correlation propagation ---
|
|
488
545
|
|
|
546
|
+
describe("job logger", () => {
|
|
547
|
+
test("handler can call info/warn/error/debug/child without throw", async () => {
|
|
548
|
+
clearLog();
|
|
549
|
+
await withRunner(async (runner) => {
|
|
550
|
+
await runner.dispatch("test:job:log-probe", { n: 1 });
|
|
551
|
+
await waitFor(() => {
|
|
552
|
+
expect(jobLog.some((e) => e.name === "test:job:log-probe")).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
489
558
|
describe("correlation propagation", () => {
|
|
490
559
|
test("dispatch inside requestContext.run passes correlationId into the job", async () => {
|
|
491
560
|
clearLog();
|
|
@@ -599,6 +668,38 @@ describe("error handling", () => {
|
|
|
599
668
|
});
|
|
600
669
|
});
|
|
601
670
|
});
|
|
671
|
+
|
|
672
|
+
test("perTenant with getActiveTenantIds fans out one run per tenant", async () => {
|
|
673
|
+
clearLog();
|
|
674
|
+
const tenants = [101, 102];
|
|
675
|
+
await withRunner(
|
|
676
|
+
async (runner) => {
|
|
677
|
+
await runner.dispatch("test:job:per-tenant-fanout", { n: 7 });
|
|
678
|
+
await waitFor(() => {
|
|
679
|
+
const runs = jobLog.filter((e) => e.name === "test:job:per-tenant-fanout");
|
|
680
|
+
expect(runs.length).toBe(2);
|
|
681
|
+
expect(runs.every((e) => e.payload["n"] === 7)).toBe(true);
|
|
682
|
+
});
|
|
683
|
+
},
|
|
684
|
+
{ getActiveTenantIds: async () => tenants },
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
describe("handleEvent maxPerTenant", () => {
|
|
690
|
+
test("skips enqueue when tenant is already at the cap", async () => {
|
|
691
|
+
clearLog();
|
|
692
|
+
await withRunner(async (runner) => {
|
|
693
|
+
const user = { ...TestUsers.admin, tenantId: "cap-tenant-evt" };
|
|
694
|
+
await runner.handleEvent("test:write:capped-event", { n: 1 }, user);
|
|
695
|
+
await runner.handleEvent("test:write:capped-event", { n: 2 }, user);
|
|
696
|
+
// Third must be skipped by the maxPerTenant guard.
|
|
697
|
+
await runner.handleEvent("test:write:capped-event", { n: 3 }, user);
|
|
698
|
+
await sleep(200);
|
|
699
|
+
const started = jobLog.filter((e) => e.name === "test:job:per-tenant-limited-event");
|
|
700
|
+
expect(started.length).toBeLessThanOrEqual(2);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
602
703
|
});
|
|
603
704
|
|
|
604
705
|
// --- Registry ---
|
package/src/logging/types.ts
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
info(msg: string, data?: Record<string, unknown>): void;
|
|
3
|
-
warn(msg: string, data?: Record<string, unknown>): void;
|
|
4
|
-
error(msg: string, data?: Record<string, unknown>): void;
|
|
5
|
-
debug(msg: string, data?: Record<string, unknown>): void;
|
|
6
|
-
child(context: Record<string, unknown>): Logger;
|
|
7
|
-
};
|
|
1
|
+
export * from "@cosmicdrift/kumiko-types/logger";
|
|
@@ -1,29 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// each module stays focused; consumers still import from "./types".
|
|
3
|
-
|
|
4
|
-
export type {
|
|
5
|
-
Counter,
|
|
6
|
-
Gauge,
|
|
7
|
-
Histogram,
|
|
8
|
-
Meter,
|
|
9
|
-
MetricDefinition,
|
|
10
|
-
MetricLabels,
|
|
11
|
-
MetricsHandle,
|
|
12
|
-
MetricType,
|
|
13
|
-
} from "./metric";
|
|
14
|
-
export type {
|
|
15
|
-
ObservabilityOptions,
|
|
16
|
-
ObservabilityProvider,
|
|
17
|
-
SamplingConfig,
|
|
18
|
-
SensitiveFilterConfig,
|
|
19
|
-
} from "./provider";
|
|
20
|
-
export type {
|
|
21
|
-
SerializedTraceContext,
|
|
22
|
-
Span,
|
|
23
|
-
SpanAttributes,
|
|
24
|
-
SpanAttributeValue,
|
|
25
|
-
SpanKind,
|
|
26
|
-
SpanStatus,
|
|
27
|
-
StartSpanOptions,
|
|
28
|
-
Tracer,
|
|
29
|
-
} from "./span";
|
|
1
|
+
export * from "@cosmicdrift/kumiko-types/observability";
|