@cosmicdrift/kumiko-framework 0.159.1 → 0.161.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 +3 -3
- package/src/api/__tests__/api.test.ts +65 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +1 -0
- 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__/dispatcher-live.integration.test.ts +74 -0
- 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/api-constants.ts +1 -0
- package/src/api/auth-middleware.ts +17 -44
- package/src/api/auth-routes.ts +6 -2
- package/src/api/index.ts +1 -0
- package/src/api/routes.ts +57 -0
- package/src/api/server.ts +5 -4
- package/src/bun-db/query.ts +12 -25
- package/src/crypto/kms-adapter.ts +2 -118
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +91 -0
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +8 -19
- package/src/db/entity-table-meta-types.ts +2 -92
- package/src/db/event-store-executor.ts +4 -96
- package/src/db/table-builder.ts +2 -19
- package/src/db/tenant-db.ts +6 -55
- package/src/engine/__tests__/boot-validator.test.ts +46 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +139 -10
- package/src/engine/__tests__/engine.test.ts +28 -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__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/boot-validator/entity-handler.ts +10 -1
- package/src/engine/define-feature.ts +1 -0
- package/src/engine/define-handler.ts +1 -0
- 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 +3 -0
- package/src/engine/feature-entity-handlers.ts +35 -1
- package/src/engine/index.ts +3 -0
- 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/types/config.ts +2 -497
- package/src/engine/types/define-handler.ts +2 -94
- package/src/engine/types/entity-handlers.ts +2 -30
- package/src/engine/types/feature.ts +2 -1021
- package/src/engine/types/fields.ts +2 -685
- package/src/engine/types/handlers.ts +2 -820
- package/src/engine/types/hooks.ts +2 -170
- package/src/engine/types/index.ts +44 -36
- package/src/engine/types/nav.ts +2 -67
- package/src/engine/types/ownership.ts +2 -83
- package/src/engine/types/projection.ts +2 -165
- package/src/engine/types/screen.ts +2 -747
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/workspace.ts +2 -42
- package/src/errors/write-error-info.ts +6 -22
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +2 -21
- package/src/event-store/snapshot.ts +11 -35
- package/src/event-store/types.ts +2 -22
- package/src/files/provider-resolver.ts +3 -5
- package/src/files/types.ts +5 -54
- package/src/jobs/__tests__/jobs.integration.test.ts +102 -1
- package/src/pipeline/__tests__/dispatcher.test.ts +96 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +208 -0
- package/src/pipeline/dispatch-shared.ts +39 -1
- 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/multi-stream-apply-context.ts +4 -42
- package/src/rate-limit/resolver.ts +10 -30
- package/src/secrets/envelope-cipher.ts +4 -6
- package/src/secrets/types.ts +2 -177
- package/src/stack/request-helper.ts +19 -2
- package/src/stack/test-stack.ts +33 -14
- package/src/time/tz-context.ts +9 -56
|
@@ -8,6 +8,7 @@ import { runBatch, unwrapSingle } from "./dispatch-batch";
|
|
|
8
8
|
import { executeQuery } from "./dispatch-query";
|
|
9
9
|
import type { BatchCommand, BatchResult, DispatchContext } from "./dispatch-shared";
|
|
10
10
|
import { resolveAuthClaimsFn } from "./dispatch-shared";
|
|
11
|
+
import { executeStream } from "./dispatch-stream";
|
|
11
12
|
import { type HandlerType, resolveType } from "./dispatcher-utils";
|
|
12
13
|
import type { IdempotencyGuard } from "./idempotency";
|
|
13
14
|
import type { LifecycleHooks } from "./lifecycle-pipeline";
|
|
@@ -52,6 +53,10 @@ export type Dispatcher = {
|
|
|
52
53
|
requestId?: string,
|
|
53
54
|
): Promise<WriteResult>;
|
|
54
55
|
query(type: HandlerType, payload: unknown, user: SessionUser): Promise<unknown>;
|
|
56
|
+
// AsyncGenerator, not Promise — gates (feature/rate-limit/access/
|
|
57
|
+
// validation) fire on the consumer's first `.next()` pull, not on this
|
|
58
|
+
// call, since they live inside the underlying async function*.
|
|
59
|
+
stream(type: HandlerType, payload: unknown, user: SessionUser): AsyncGenerator<unknown>;
|
|
55
60
|
command(type: HandlerType, payload: unknown, user: SessionUser): Promise<void>;
|
|
56
61
|
// Atomic multi-command write: all commands run in a single DB transaction.
|
|
57
62
|
// On any failure, the transaction rolls back and afterCommit hooks do NOT fire.
|
|
@@ -114,6 +119,8 @@ export function createDispatcher(
|
|
|
114
119
|
|
|
115
120
|
query: (typeOrRef, payload, user) => executeQuery(ctx, resolveType(typeOrRef), payload, user),
|
|
116
121
|
|
|
122
|
+
stream: (typeOrRef, payload, user) => executeStream(ctx, resolveType(typeOrRef), payload, user),
|
|
123
|
+
|
|
117
124
|
async command(typeOrRef, payload, user) {
|
|
118
125
|
const type = resolveType(typeOrRef);
|
|
119
126
|
const batchResult = await runBatch(ctx, [{ type, payload }], user);
|
|
@@ -1,50 +1,12 @@
|
|
|
1
|
+
import type { MultiStreamApplyContext } from "@cosmicdrift/kumiko-types/multi-stream-apply-context-types";
|
|
1
2
|
import type { DbRunner } from "../db/connection";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
AppendEventFn,
|
|
5
|
-
KumikoEventTypeMap,
|
|
6
|
-
Registry,
|
|
7
|
-
TenantId,
|
|
8
|
-
UnsafeAppendEventFn,
|
|
9
|
-
} from "../engine/types";
|
|
10
|
-
import { loadAggregate, loadAggregateAsOf, type StoredEvent } from "../event-store/event-store";
|
|
3
|
+
import type { AppendEventArgs, AppendEventFn, Registry, TenantId } from "../engine/types";
|
|
4
|
+
import { loadAggregate, loadAggregateAsOf } from "../event-store/event-store";
|
|
11
5
|
import { upcastStoredEvents } from "../event-store/upcaster";
|
|
12
6
|
import type { FileContext } from "../files/file-handle";
|
|
13
7
|
import { appendDomainEventCore } from "./append-event-core";
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
// when it needs to produce follow-up events (saga / process-manager
|
|
17
|
-
// pattern). Keeps the MSP feature-decoupled: applies don't reach into
|
|
18
|
-
// handler-bridge (no query/write/writeAs), they just read the aggregate
|
|
19
|
-
// stream and append new events — Marten's session scope for projections.
|
|
20
|
-
//
|
|
21
|
-
// TMap propagates the strict event-type-map (see HandlerContext). Default
|
|
22
|
-
// matches the global KumikoEventTypeMap; runtime-pluggable callers route
|
|
23
|
-
// through unsafeAppendEvent.
|
|
24
|
-
export type MultiStreamApplyContext<TMap extends object = KumikoEventTypeMap> = {
|
|
25
|
-
// Append a domain event onto an aggregate stream in the CURRENT tx.
|
|
26
|
-
// Schema-validated, archive-guarded, stream-version derived. Metadata
|
|
27
|
-
// inherits from the triggering event (correlationId) + requestContext
|
|
28
|
-
// (causationId is already set to the triggering event.id by the
|
|
29
|
-
// dispatcher wrap). Strict against KumikoEventTypeMap — same contract
|
|
30
|
-
// as HandlerContext.appendEvent (compile-time-validated payload).
|
|
31
|
-
readonly appendEvent: AppendEventFn<TMap>;
|
|
32
|
-
// Escape hatch for runtime-pluggable events without compile-time
|
|
33
|
-
// augmentation. Same runtime semantics; type-surface is `payload: unknown`.
|
|
34
|
-
readonly unsafeAppendEvent: UnsafeAppendEventFn;
|
|
35
|
-
// Read an aggregate stream — useful when a saga needs to inspect the
|
|
36
|
-
// current state of a different aggregate before deciding what to emit.
|
|
37
|
-
readonly loadAggregate: (
|
|
38
|
-
aggregateId: string,
|
|
39
|
-
options?: { readonly asOf?: Temporal.Instant },
|
|
40
|
-
) => Promise<readonly StoredEvent[]>;
|
|
41
|
-
// Binary storage handle factory, mirrors AppContext.files. Present when
|
|
42
|
-
// the app booted with `files.storageProvider`; undefined otherwise.
|
|
43
|
-
// Post-processing MSPs (resize, EXIF-strip, virus-scan) read bytes via
|
|
44
|
-
// `ctx.files.ref(payload.storageKey).read()` and write derivates via
|
|
45
|
-
// `.derive("thumb").write(...)` — binaries never ride through events.
|
|
46
|
-
readonly files?: FileContext;
|
|
47
|
-
};
|
|
9
|
+
export type { MultiStreamApplyContext } from "@cosmicdrift/kumiko-types/multi-stream-apply-context-types";
|
|
48
10
|
|
|
49
11
|
export type MultiStreamApplyContextDeps = {
|
|
50
12
|
readonly registry: Registry;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RateLimitConfig,
|
|
3
|
+
RateLimitDecision,
|
|
4
|
+
RateLimitResolver,
|
|
5
|
+
} from "@cosmicdrift/kumiko-types/rate-limit-types";
|
|
1
6
|
import type Redis from "ioredis";
|
|
2
7
|
import { RateLimitError } from "../errors";
|
|
3
8
|
import { RedisKeys } from "../pipeline/redis-keys";
|
|
@@ -108,36 +113,11 @@ end
|
|
|
108
113
|
return { math.floor(tokens), retryAfterMs }
|
|
109
114
|
`;
|
|
110
115
|
|
|
111
|
-
export type
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
readonly windowSeconds: number;
|
|
117
|
-
readonly resetAt: Temporal.Instant;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export type RateLimitConfig = {
|
|
121
|
-
readonly limit: number;
|
|
122
|
-
readonly windowSeconds: number;
|
|
123
|
-
readonly cost?: number;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export type RateLimitResolver = {
|
|
127
|
-
// Atomic check + deduct. Returns the decision and current bucket state
|
|
128
|
-
// — caller decides whether to throw RateLimitError or proceed.
|
|
129
|
-
check(bucket: string, config: RateLimitConfig): Promise<RateLimitDecision>;
|
|
130
|
-
|
|
131
|
-
// Convenience: throws RateLimitError when blocked. Useful inside the
|
|
132
|
-
// dispatcher / middleware code-paths where the failure shape is fixed.
|
|
133
|
-
enforce(bucket: string, config: RateLimitConfig): Promise<RateLimitDecision>;
|
|
134
|
-
|
|
135
|
-
// Read-only inspection: returns the same shape as check() but never
|
|
136
|
-
// mutates the bucket — no token deduction, no refill-timestamp update.
|
|
137
|
-
// Use for ops/status queries (e.g. "kumiko rl status user:42") that
|
|
138
|
-
// must observe the bucket without disturbing it.
|
|
139
|
-
peek(bucket: string, config: Omit<RateLimitConfig, "cost">): Promise<RateLimitDecision>;
|
|
140
|
-
};
|
|
116
|
+
export type {
|
|
117
|
+
RateLimitConfig,
|
|
118
|
+
RateLimitDecision,
|
|
119
|
+
RateLimitResolver,
|
|
120
|
+
} from "@cosmicdrift/kumiko-types/rate-limit-types";
|
|
141
121
|
|
|
142
122
|
export type RateLimitResolverOptions = {
|
|
143
123
|
readonly redis: Redis;
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// kekVersion inside makes every value rotatable via the MasterKeyProvider
|
|
4
4
|
// keyring.
|
|
5
5
|
|
|
6
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-types/envelope-cipher-types";
|
|
6
7
|
import { InternalError } from "../errors/classes";
|
|
7
8
|
import type { DekCache } from "./dek-cache";
|
|
8
9
|
import { createDekCache, withDekCache } from "./dek-cache";
|
|
9
10
|
import { decryptValue, encryptValue } from "./envelope";
|
|
10
11
|
import { decodeStoredEnvelope, encodeStoredEnvelope, isStoredEnvelope } from "./stored-envelope";
|
|
11
|
-
import type {
|
|
12
|
+
import type { MasterKeyProvider } from "./types";
|
|
13
|
+
|
|
14
|
+
export type { EnvelopeCipher } from "@cosmicdrift/kumiko-types/envelope-cipher-types";
|
|
12
15
|
|
|
13
16
|
export type EnvelopeCipherOptions = {
|
|
14
17
|
// Shared DEK cache — pass the app-wide instance so config/entity reads
|
|
@@ -16,11 +19,6 @@ export type EnvelopeCipherOptions = {
|
|
|
16
19
|
readonly dekCache?: DekCache;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
export type EnvelopeCipher = {
|
|
20
|
-
encrypt(plaintext: string, scope?: KeyScope): Promise<string>;
|
|
21
|
-
decrypt(stored: string, scope?: KeyScope): Promise<string>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
22
|
export function createEnvelopeCipher(
|
|
25
23
|
provider: MasterKeyProvider,
|
|
26
24
|
opts: EnvelopeCipherOptions = {},
|
package/src/secrets/types.ts
CHANGED
|
@@ -1,177 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// encryptedDek, never touch the ciphertext.
|
|
4
|
-
|
|
5
|
-
import type { TenantId } from "../engine";
|
|
6
|
-
|
|
7
|
-
// Plaintext-secret wrapper (branded). Carries the actual string internally
|
|
8
|
-
// but the nominal typing stops it from landing in an HTTP response by
|
|
9
|
-
// accident — a response-serializer guard + the reveal() cost make the leak
|
|
10
|
-
// intentional. Framework code that sees `Secret<string>` knows the caller
|
|
11
|
-
// has already gone through the audited ctx.secrets.get path.
|
|
12
|
-
//
|
|
13
|
-
// The brand is a real (non-registered) Symbol so it exists at runtime for
|
|
14
|
-
// isSecret() without clashing with user-land symbols of the same name.
|
|
15
|
-
const SecretBrand: unique symbol = Symbol("kumiko.secret");
|
|
16
|
-
|
|
17
|
-
export type Secret<T = string> = {
|
|
18
|
-
readonly [SecretBrand]: true;
|
|
19
|
-
readonly reveal: () => T;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Implementation helper — bundled-features uses this to wrap a plaintext after
|
|
23
|
-
// decryption. Kept in the framework so both sides share one canonical brand.
|
|
24
|
-
export function createSecret<T>(value: T): Secret<T> {
|
|
25
|
-
return {
|
|
26
|
-
[SecretBrand]: true as const,
|
|
27
|
-
reveal: () => value,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// True for any object carrying the Secret brand. Used by the response guard
|
|
32
|
-
// to reject leaks before serialization.
|
|
33
|
-
export function isSecret(v: unknown): v is Secret<unknown> {
|
|
34
|
-
return typeof v === "object" && v !== null && SecretBrand in v;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// --- Compile-time response guard (R6) --------------------------------------
|
|
38
|
-
//
|
|
39
|
-
// ContainsSecret<T> is `true` only when a Secret<> is DEFINITELY present
|
|
40
|
-
// somewhere in T. The handler-registration guard (defineWriteHandler/
|
|
41
|
-
// defineQueryHandler) turns a `true` into a compile error — the static twin of
|
|
42
|
-
// assertNoSecretLeak's runtime walk.
|
|
43
|
-
//
|
|
44
|
-
// Biased to `false`: anything it cannot inspect — a bare generic type param (a
|
|
45
|
-
// handler generic over its response), `unknown`/`any`, `never` — resolves to
|
|
46
|
-
// `false` = allowed, with the runtime guard as the backstop. The alternative
|
|
47
|
-
// (default-to-leak) false-flags every legitimate generic-over-response handler.
|
|
48
|
-
//
|
|
49
|
-
// Branch order is load-bearing: never/unknown/any first (uninspectable), then
|
|
50
|
-
// Secret, then primitives (covers branded primitives like TenantId without
|
|
51
|
-
// enumerating them), then the SafeLeaf allowlist (opaque class instances that
|
|
52
|
-
// blind `{ [K in keyof T] }` recursion would mangle — the type-level mirror of
|
|
53
|
-
// leak-guard.ts skipping non-plain objects), then arrays, then a "does any
|
|
54
|
-
// field contain a secret" fold over plain objects.
|
|
55
|
-
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
|
|
56
|
-
|
|
57
|
-
// Opaque built-in leaves a response legitimately carries; never recurse into
|
|
58
|
-
// them. Extend when the bundled-features tsc sweep surfaces a real leaf type.
|
|
59
|
-
// Map/Set (556/2): `keyof Map<K,V>` yields method names, not V, so these
|
|
60
|
-
// already fell through to `false` via the object-mapped-type branch — listed
|
|
61
|
-
// explicitly here so the compile-time treatment matches leak-guard.ts's
|
|
62
|
-
// runtime `instanceof Map`/`instanceof Set` branch (walk entries separately)
|
|
63
|
-
// instead of looking like an oversight.
|
|
64
|
-
type SafeLeaf =
|
|
65
|
-
| Date
|
|
66
|
-
| RegExp
|
|
67
|
-
| Temporal.Instant
|
|
68
|
-
| Temporal.ZonedDateTime
|
|
69
|
-
| Temporal.PlainDate
|
|
70
|
-
| Temporal.PlainDateTime
|
|
71
|
-
| Temporal.PlainTime
|
|
72
|
-
| Temporal.PlainYearMonth
|
|
73
|
-
| Temporal.PlainMonthDay
|
|
74
|
-
| Temporal.Duration
|
|
75
|
-
| Map<unknown, unknown>
|
|
76
|
-
| Set<unknown>;
|
|
77
|
-
|
|
78
|
-
export type ContainsSecret<T> = [T] extends [never]
|
|
79
|
-
? false
|
|
80
|
-
: unknown extends T
|
|
81
|
-
? false
|
|
82
|
-
: T extends Secret<unknown>
|
|
83
|
-
? true
|
|
84
|
-
: T extends Primitive
|
|
85
|
-
? false
|
|
86
|
-
: T extends SafeLeaf
|
|
87
|
-
? false
|
|
88
|
-
: T extends readonly (infer U)[]
|
|
89
|
-
? ContainsSecret<U>
|
|
90
|
-
: T extends object
|
|
91
|
-
? true extends { [K in keyof T]-?: ContainsSecret<T[K]> }[keyof T]
|
|
92
|
-
? true
|
|
93
|
-
: false
|
|
94
|
-
: false;
|
|
95
|
-
|
|
96
|
-
// Per-read audit context. Populated by requireSecretsContext() wrapper so
|
|
97
|
-
// handlers don't need to pass userId/handlerName manually on every call.
|
|
98
|
-
// Undefined for framework-internal reads (rotation job, tests) — the audit
|
|
99
|
-
// table stays a "who touched this credential" log, not a crash-report sink.
|
|
100
|
-
export type SecretAuditContext = {
|
|
101
|
-
readonly userId: string;
|
|
102
|
-
readonly handlerName: string;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Feature code can pass either the raw qualified-name string or a typed
|
|
106
|
-
// handle returned by r.secret. The handle form is safer — renaming the
|
|
107
|
-
// r.secret call updates all references through the import graph.
|
|
108
|
-
export type SecretKeyRef = string | { readonly name: string };
|
|
109
|
-
|
|
110
|
-
// The ctx.secrets contract. Concrete implementation lives in bundled-features
|
|
111
|
-
// (createSecretsContext) where the DB and MasterKeyProvider are known. This
|
|
112
|
-
// lean interface is what the framework's HandlerContext carries so engine
|
|
113
|
-
// code can talk about it without pulling in bundled-features.
|
|
114
|
-
export interface SecretsContext {
|
|
115
|
-
get(
|
|
116
|
-
tenantId: TenantId,
|
|
117
|
-
key: SecretKeyRef,
|
|
118
|
-
auditCtx?: SecretAuditContext,
|
|
119
|
-
): Promise<Secret<string> | undefined>;
|
|
120
|
-
// Metadata-only existence probe: no decryption, no read-audit event.
|
|
121
|
-
// For readiness checks — use get() when the value itself is needed.
|
|
122
|
-
has(tenantId: TenantId, key: SecretKeyRef): Promise<boolean>;
|
|
123
|
-
set(
|
|
124
|
-
tenantId: TenantId,
|
|
125
|
-
key: SecretKeyRef,
|
|
126
|
-
value: string,
|
|
127
|
-
opts?: { redact?: (plaintext: string) => string; hint?: string; updatedBy?: string },
|
|
128
|
-
): Promise<void>;
|
|
129
|
-
delete(tenantId: TenantId, key: SecretKeyRef, opts?: { deletedBy?: string }): Promise<boolean>;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export type Envelope = {
|
|
133
|
-
// AES-256-GCM ciphertext of the plaintext, keyed with a DEK.
|
|
134
|
-
readonly ciphertext: Buffer;
|
|
135
|
-
// GCM nonce (12 bytes). Generated fresh per encryption.
|
|
136
|
-
readonly iv: Buffer;
|
|
137
|
-
// GCM auth tag (16 bytes). Guarantees the ciphertext wasn't tampered.
|
|
138
|
-
readonly authTag: Buffer;
|
|
139
|
-
// DEK wrapped with the current KEK. Decryption needs provider.unwrapDek
|
|
140
|
-
// with the kekVersion to recover the DEK.
|
|
141
|
-
readonly encryptedDek: Buffer;
|
|
142
|
-
// Which KEK version was used to wrap the DEK. On rotation, rows with old
|
|
143
|
-
// versions still decrypt — the provider keeps a keyring of historical KEKs.
|
|
144
|
-
readonly kekVersion: number;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// BYOK hook: callers pass the tenant a value belongs to; a per-tenant-KMS
|
|
148
|
-
// provider keys its wrap/unwrap on it. EnvMasterKeyProvider (app-wide
|
|
149
|
-
// keyring) ignores it — the param exists so the contract doesn't have to
|
|
150
|
-
// break when a tenant-scoped provider ships.
|
|
151
|
-
export type KeyScope = {
|
|
152
|
-
readonly tenantId?: TenantId;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// The contract a KEK backend must fulfil. The framework sees only this
|
|
156
|
-
// interface; concrete implementations live in separate packages
|
|
157
|
-
// (@cosmicdrift/kumiko-secrets-vault, @cosmicdrift/kumiko-secrets-aws-kms, ...). The default is
|
|
158
|
-
// EnvMasterKeyProvider which reads keys from environment variables.
|
|
159
|
-
export interface MasterKeyProvider {
|
|
160
|
-
// Wrap a fresh DEK with the current KEK. Returns the wrapped bytes + the
|
|
161
|
-
// KEK version used — the version ends up in the Envelope so decryption
|
|
162
|
-
// later knows which KEK to ask for.
|
|
163
|
-
wrapDek(dek: Buffer, scope?: KeyScope): Promise<{ encryptedDek: Buffer; kekVersion: number }>;
|
|
164
|
-
|
|
165
|
-
// Unwrap a previously-wrapped DEK. During rotation the provider must
|
|
166
|
-
// accept older kekVersion values (2-version window minimum), otherwise
|
|
167
|
-
// old rows become unreadable.
|
|
168
|
-
unwrapDek(encryptedDek: Buffer, kekVersion: number, scope?: KeyScope): Promise<Buffer>;
|
|
169
|
-
|
|
170
|
-
// Which KEK version new wraps use. Rotation flips this to a new value
|
|
171
|
-
// and older-version reads continue to work until rows are re-wrapped.
|
|
172
|
-
currentVersion(): number;
|
|
173
|
-
|
|
174
|
-
// Health check: can the provider talk to its backend? Used by
|
|
175
|
-
// /health/ready. Cheap probe, no KEK material read.
|
|
176
|
-
isAvailable(): Promise<boolean>;
|
|
177
|
-
}
|
|
1
|
+
// Legacy path — re-exported for callers still importing this module directly.
|
|
2
|
+
export * from "@cosmicdrift/kumiko-types/secrets-types";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Hono } from "hono";
|
|
2
|
+
import type { SessionCreator } from "../api/auth-routes";
|
|
2
3
|
import type { JwtHelper } from "../api/jwt";
|
|
3
4
|
import type { SessionUser } from "../engine/types";
|
|
4
5
|
|
|
@@ -98,9 +99,25 @@ export type RequestHelper = {
|
|
|
98
99
|
) => Promise<Response>;
|
|
99
100
|
};
|
|
100
101
|
|
|
101
|
-
export
|
|
102
|
+
export type RequestHelperOptions = {
|
|
103
|
+
// When sessionChecker is wired (sessions feature), JWTs without jti are
|
|
104
|
+
// rejected as no_sid. Seed/test helpers that only jwt.sign(user) need a
|
|
105
|
+
// live sid — create one via the same sessionCreator login uses (#1372).
|
|
106
|
+
readonly sessionCreator?: SessionCreator;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function createRequestHelper(
|
|
110
|
+
app: Hono,
|
|
111
|
+
jwt: JwtHelper,
|
|
112
|
+
options: RequestHelperOptions = {},
|
|
113
|
+
): RequestHelper {
|
|
102
114
|
async function authHeader(user: SessionUser): Promise<Record<string, string>> {
|
|
103
|
-
|
|
115
|
+
let forJwt = user;
|
|
116
|
+
if (options.sessionCreator && !user.sid) {
|
|
117
|
+
const sid = await options.sessionCreator(user, { ip: "test", userAgent: "request-helper" });
|
|
118
|
+
forJwt = { ...user, sid };
|
|
119
|
+
}
|
|
120
|
+
const token = await jwt.sign(forJwt);
|
|
104
121
|
return { Authorization: `Bearer ${token}` };
|
|
105
122
|
}
|
|
106
123
|
|
package/src/stack/test-stack.ts
CHANGED
|
@@ -124,6 +124,16 @@ export type TestStackOptions = {
|
|
|
124
124
|
sseBroker: import("../api/sse-broker").SseBroker;
|
|
125
125
|
redis: import("ioredis").default;
|
|
126
126
|
}) => import("../api/server").ServerOptions["anonymousAccess"]);
|
|
127
|
+
/** Optional post-factory enricher (e.g. merge auth-foundation tenant
|
|
128
|
+
* providers). Keeps framework free of a bundled-features dependency. */
|
|
129
|
+
enrichAnonymousAccess?: (
|
|
130
|
+
base: import("../api/server").ServerOptions["anonymousAccess"] | undefined,
|
|
131
|
+
deps: {
|
|
132
|
+
registry: Registry;
|
|
133
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
|
|
134
|
+
db: any;
|
|
135
|
+
},
|
|
136
|
+
) => Promise<import("../api/server").ServerOptions["anonymousAccess"] | undefined>;
|
|
127
137
|
/** Opt-in JobRunner wired into ctx.jobRunner and merged into
|
|
128
138
|
* dispatcherOptions so event-triggered jobs enqueue on commit — mirrors
|
|
129
139
|
* the prod entrypoint's `buildJobRunnerWithHook`. Unlike prod (which
|
|
@@ -382,19 +392,24 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
382
392
|
: {}),
|
|
383
393
|
...(options.lifecycle ? { lifecycle: options.lifecycle } : {}),
|
|
384
394
|
...(options.rateLimit ? { rateLimit: options.rateLimit } : {}),
|
|
385
|
-
...(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
...(await (async () => {
|
|
396
|
+
const baseAnon =
|
|
397
|
+
typeof options.anonymousAccess === "function"
|
|
398
|
+
? options.anonymousAccess({
|
|
399
|
+
registry,
|
|
400
|
+
db: testDb.db,
|
|
401
|
+
sseBroker,
|
|
402
|
+
redis: testRedis.redis,
|
|
403
|
+
})
|
|
404
|
+
: options.anonymousAccess;
|
|
405
|
+
const resolvedAnon = options.enrichAnonymousAccess
|
|
406
|
+
? await options.enrichAnonymousAccess(baseAnon, {
|
|
407
|
+
registry,
|
|
408
|
+
db: testDb.db,
|
|
409
|
+
})
|
|
410
|
+
: baseAnon;
|
|
411
|
+
return resolvedAnon ? { anonymousAccess: resolvedAnon } : {};
|
|
412
|
+
})()),
|
|
398
413
|
});
|
|
399
414
|
|
|
400
415
|
const eventDispatcher: EventDispatcher | undefined = server.eventDispatcher;
|
|
@@ -406,7 +421,11 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
406
421
|
// timer loop call start() again (idempotent) after setup.
|
|
407
422
|
if (eventDispatcher) await eventDispatcher.ensureRegistered();
|
|
408
423
|
|
|
409
|
-
const http = createRequestHelper(server.app, server.jwt
|
|
424
|
+
const http = createRequestHelper(server.app, server.jwt, {
|
|
425
|
+
...(options.authConfig?.sessionCreator !== undefined && {
|
|
426
|
+
sessionCreator: options.authConfig.sessionCreator,
|
|
427
|
+
}),
|
|
428
|
+
});
|
|
410
429
|
|
|
411
430
|
return {
|
|
412
431
|
app: server.app,
|
package/src/time/tz-context.ts
CHANGED
|
@@ -13,65 +13,18 @@
|
|
|
13
13
|
// `tenant` + `user` sind die TZ-Defaults für den aktuellen Request. Aktueller
|
|
14
14
|
// Stand: beide default auf "UTC" — sobald tenant.timezone +
|
|
15
15
|
// user.timezone Felder existieren, lese ich sie aus dem Request-Context.
|
|
16
|
+
//
|
|
17
|
+
// Die reinen Type-Contracts (TzContext, TzContextOptions, LocatedTimestampJson)
|
|
18
|
+
// leben in @cosmicdrift/kumiko-types/tz-context — hier nur die Factories.
|
|
16
19
|
|
|
17
|
-
import type {
|
|
20
|
+
import type { TzContext, TzContextOptions } from "@cosmicdrift/kumiko-types/tz-context";
|
|
18
21
|
import { ensureTemporalPolyfill, getTemporal } from "./polyfill";
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/** IANA-Zone, z.B. "Europe/Lisbon" */
|
|
26
|
-
readonly tz: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type TzContext = {
|
|
30
|
-
/** Default-TZ des Mandanten (aus tenant.timezone, default "UTC"). */
|
|
31
|
-
readonly tenant: string;
|
|
32
|
-
/** Anzeige-TZ des aktuellen Users (User-Profil-Override, fallback Tenant). */
|
|
33
|
-
readonly user: string;
|
|
34
|
-
|
|
35
|
-
/** Aktueller Moment als UTC-Instant. */
|
|
36
|
-
now(): Temporal.Instant;
|
|
37
|
-
/** Aktueller Moment als ZonedDateTime in der gewünschten Zone. */
|
|
38
|
-
nowIn(tz: string): Temporal.ZonedDateTime;
|
|
39
|
-
|
|
40
|
-
/** Heutiges Kalender-Datum in der gewünschten Zone. */
|
|
41
|
-
today(tz: string): Temporal.PlainDate;
|
|
42
|
-
/** Tagesgrenzen (00:00 bis 24:00 nächster Tag) als UTC-Instants — für DB-Range-Queries. */
|
|
43
|
-
todayRange(tz: string): { readonly start: Temporal.Instant; readonly end: Temporal.Instant };
|
|
44
|
-
|
|
45
|
-
/** Wall-Clock-String + IANA-Zone → ZonedDateTime. */
|
|
46
|
-
parse(wallClock: string, tz: string): Temporal.ZonedDateTime;
|
|
47
|
-
|
|
48
|
-
/** ZonedDateTime → UTC-Instant. */
|
|
49
|
-
toInstant(zdt: Temporal.ZonedDateTime): Temporal.Instant;
|
|
50
|
-
|
|
51
|
-
/** ZonedDateTime → JSON-Pair { at, tz } (API-Boundary). */
|
|
52
|
-
toLocatedJson(zdt: Temporal.ZonedDateTime): LocatedTimestampJson;
|
|
53
|
-
|
|
54
|
-
/** JSON-Pair { at, tz } → ZonedDateTime (Wall-Clock + IANA). */
|
|
55
|
-
fromLocatedJson(obj: LocatedTimestampJson): Temporal.ZonedDateTime;
|
|
56
|
-
|
|
57
|
-
/** Geo-Koordinaten → IANA-Zone via konfiguriertem GeoTzProvider.
|
|
58
|
-
* Wirft wenn kein Provider konfiguriert ist (v1-Default). */
|
|
59
|
-
fromCoordinates(coords: GeoCoordinates): Promise<string>;
|
|
60
|
-
/** Postadresse → IANA-Zone via GeoTzProvider. Wirft wenn kein Provider
|
|
61
|
-
* konfiguriert ist ODER der Provider kein fromAddress unterstützt (der
|
|
62
|
-
* Offline-lat/lng-Provider tut das nicht). */
|
|
63
|
-
fromAddress(address: GeoAddress): Promise<string>;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type TzContextOptions = {
|
|
67
|
-
/** Tenant-Default-TZ. Default "UTC" wenn nicht gesetzt. */
|
|
68
|
-
readonly tenant?: string;
|
|
69
|
-
/** User-Override. Default = tenant. */
|
|
70
|
-
readonly user?: string;
|
|
71
|
-
/** Optionaler Geo→Zone-Adapter für ctx.tz.fromCoordinates / fromAddress.
|
|
72
|
-
* Ohne Provider werfen diese Methoden. */
|
|
73
|
-
readonly geoTz?: GeoTzProvider;
|
|
74
|
-
};
|
|
23
|
+
export type {
|
|
24
|
+
LocatedTimestampJson,
|
|
25
|
+
TzContext,
|
|
26
|
+
TzContextOptions,
|
|
27
|
+
} from "@cosmicdrift/kumiko-types/tz-context";
|
|
75
28
|
|
|
76
29
|
/**
|
|
77
30
|
* Factory: erzeugt einen TzContext für den aktuellen Request.
|