@cosmicdrift/kumiko-framework 0.353.0 → 0.354.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/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +16 -10
- package/dist/api/request-context.d.ts +1 -0
- package/dist/api/request-context.js +5 -0
- package/dist/api/routes.js +4 -2
- package/dist/api/server.js +28 -3
- package/dist/api/sse-broker.d.ts +7 -3
- package/dist/api/sse-route.js +2 -6
- package/dist/api/token-expiry-timer.d.ts +1 -0
- package/dist/api/token-expiry-timer.js +10 -0
- package/dist/bun-db/query.js +29 -2
- package/dist/crypto/event-pii.js +4 -2
- package/dist/crypto/kms-wiring.js +31 -5
- package/dist/db/eagerload.d.ts +8 -3
- package/dist/db/eagerload.js +85 -81
- package/dist/db/event-store-executor-context.d.ts +12 -0
- package/dist/db/event-store-executor-context.js +59 -18
- package/dist/db/event-store-executor-read.js +81 -31
- package/dist/db/event-store-executor-write.js +54 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/queries/backfill-pii.d.ts +1 -0
- package/dist/db/queries/backfill-pii.js +11 -6
- package/dist/db/queries/event-consumer.d.ts +2 -3
- package/dist/db/queries/event-consumer.js +54 -43
- package/dist/db/queries/event-store.d.ts +5 -0
- package/dist/db/queries/event-store.js +51 -3
- package/dist/db/queries/pii-backfill-state.d.ts +8 -1
- package/dist/db/queries/pii-backfill-state.js +16 -5
- package/dist/db/queries/shadow-swap.js +32 -3
- package/dist/db/render-ddl.js +6 -6
- package/dist/db/table-builder.js +6 -7
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +47 -7
- package/dist/derivatives/variant-key.d.ts +1 -0
- package/dist/derivatives/variant-key.js +3 -0
- package/dist/engine/boot-validator/access-declarations.js +5 -0
- package/dist/engine/boot-validator/config-deps.js +3 -0
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +40 -0
- package/dist/engine/boot-validator/handler-field-references.d.ts +2 -0
- package/dist/engine/boot-validator/handler-field-references.js +25 -0
- package/dist/engine/boot-validator/index.js +6 -1
- package/dist/engine/boot-validator/nav.js +3 -1
- package/dist/engine/boot-validator/ownership.d.ts +2 -1
- package/dist/engine/boot-validator/ownership.js +32 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -1
- package/dist/engine/boot-validator/query-output-columns.js +9 -9
- package/dist/engine/boot-validator/screens.js +98 -7
- package/dist/engine/boot-validator/transfer-graph.d.ts +2 -1
- package/dist/engine/boot-validator/transfer-graph.js +8 -63
- package/dist/engine/boot-validator/url-prefill-fields.d.ts +1 -0
- package/dist/engine/boot-validator/url-prefill-fields.js +35 -0
- package/dist/engine/build-app-schema.js +3 -0
- package/dist/engine/changeset-changes.d.ts +1 -0
- package/dist/engine/changeset-changes.js +49 -33
- package/dist/engine/define-feature.js +11 -0
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/entity-handlers.js +55 -34
- package/dist/engine/escape-hatch-reason.d.ts +2 -0
- package/dist/engine/escape-hatch-reason.js +8 -0
- package/dist/engine/extension-names.d.ts +8 -9
- package/dist/engine/extension-names.js +8 -9
- package/dist/engine/extensions/tenant-data.d.ts +3 -1
- package/dist/engine/extensions/tenant-resource.d.ts +9 -1
- package/dist/engine/factories.d.ts +1 -1
- package/dist/engine/feature-changelog.d.ts +2 -0
- package/dist/engine/feature-changelog.js +6 -0
- package/dist/engine/feature-config-events-jobs.js +41 -4
- package/dist/engine/handler-field-references.d.ts +3 -0
- package/dist/engine/handler-field-references.js +31 -0
- package/dist/engine/i18n-key.js +2 -3
- package/dist/engine/index.d.ts +8 -2
- package/dist/engine/index.js +6 -1
- package/dist/engine/ownership.js +3 -4
- package/dist/engine/project-app-schema-for-roles.js +3 -1
- package/dist/engine/registry-ingest.d.ts +1 -0
- package/dist/engine/registry-ingest.js +8 -3
- package/dist/engine/run-pipeline.js +12 -1
- package/dist/engine/schema-builder.js +35 -2
- package/dist/engine/screen-helpers.d.ts +13 -0
- package/dist/engine/screen-helpers.js +31 -0
- package/dist/engine/soft-delete-cleanup.js +3 -2
- package/dist/engine/steps/_read-source.d.ts +4 -0
- package/dist/engine/steps/_read-source.js +22 -0
- package/dist/engine/steps/describe-workflow-step-error.js +3 -1
- package/dist/engine/steps/read-find-many.js +2 -1
- package/dist/engine/steps/read-find-one.js +2 -1
- package/dist/engine/steps/workflow-step-error.d.ts +5 -0
- package/dist/engine/steps/workflow-step-error.js +13 -0
- package/dist/engine/transfer-adjacency.d.ts +24 -0
- package/dist/engine/transfer-adjacency.js +107 -0
- package/dist/entrypoint/index.d.ts +2 -0
- package/dist/entrypoint/index.js +2 -0
- package/dist/env/index.js +45 -23
- package/dist/errors/classes.d.ts +8 -0
- package/dist/errors/classes.js +13 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +2 -1
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/errors/subject-erased.d.ts +2 -0
- package/dist/errors/subject-erased.js +9 -0
- package/dist/event-store/admin-api.js +14 -10
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +14 -9
- package/dist/event-store/events-schema.js +2 -1
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +30 -6
- package/dist/files/file-routes.js +10 -4
- package/dist/files/resolve-content-type.d.ts +2 -0
- package/dist/files/resolve-content-type.js +8 -0
- package/dist/i18n/required-surface-keys.d.ts +2 -0
- package/dist/i18n/required-surface-keys.js +8 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +13 -0
- package/dist/jobs/job-runner.js +128 -22
- package/dist/migrations/pii-event-backfill.js +46 -25
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/standard-metrics.d.ts +4 -0
- package/dist/observability/standard-metrics.js +12 -0
- package/dist/pipeline/dispatch-shared.d.ts +2 -0
- package/dist/pipeline/dispatch-shared.js +74 -9
- package/dist/pipeline/dispatch-stream.d.ts +5 -1
- package/dist/pipeline/dispatch-stream.js +19 -12
- package/dist/pipeline/dispatcher.d.ts +3 -1
- package/dist/pipeline/dispatcher.js +10 -15
- package/dist/pipeline/event-consumer-state.js +0 -26
- package/dist/pipeline/event-dispatcher-admin.js +17 -11
- package/dist/pipeline/event-dispatcher-delivery.d.ts +1 -1
- package/dist/pipeline/event-dispatcher-delivery.js +3 -2
- package/dist/pipeline/event-dispatcher.js +48 -7
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/member-read-only-transaction.js +25 -5
- package/dist/pipeline/msp-rebuild.js +3 -3
- package/dist/pipeline/pending-gap-ranges.d.ts +1 -0
- package/dist/pipeline/pending-gap-ranges.js +22 -0
- package/dist/pipeline/system-hooks.js +4 -4
- package/dist/pipeline/system-identity-switch.d.ts +1 -1
- package/dist/pipeline/system-identity-switch.js +5 -4
- package/dist/pipeline/tenant-timezone-cache.js +5 -3
- package/dist/redis/cache-sync-topics.d.ts +1 -0
- package/dist/redis/cache-sync-topics.js +1 -0
- package/dist/redis/client.d.ts +1 -0
- package/dist/redis/client.js +7 -0
- package/dist/redis/index.d.ts +1 -1
- package/dist/redis/index.js +1 -1
- package/dist/redis/pubsub-signal.d.ts +1 -0
- package/dist/redis/pubsub-signal.js +3 -0
- package/dist/search/meilisearch-adapter.js +34 -4
- package/dist/stack/test-stack.d.ts +4 -1
- package/dist/stack/test-stack.js +9 -2
- package/dist/testing/e2e-generator.js +7 -2
- package/dist/ui-types/index.d.ts +3 -3
- package/dist/ui-types/index.js +2 -2
- package/dist/upgrade-cli.js +7 -2
- package/package.json +12 -4
- package/src/changes.json +209 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +2 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { extractPgError } from "../db/pg-error.js";
|
|
2
2
|
import { asRawClient, runInSavepoint, transaction } from "../db/query.js";
|
|
3
|
-
import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
3
|
+
import { AccessDeniedError, InternalError, MemberReadTimeoutError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
4
4
|
import { resolveDbSource } from "./dispatch-shared.js";
|
|
5
5
|
const PG_READ_ONLY_SQLSTATE = "25006";
|
|
6
|
+
const PG_QUERY_CANCELED_SQLSTATE = "57014";
|
|
6
7
|
function isReadOnlyTransactionViolation(e) {
|
|
7
8
|
return extractPgError(e)?.code === PG_READ_ONLY_SQLSTATE;
|
|
8
9
|
}
|
|
@@ -12,12 +13,27 @@ class SavepointDiscard extends Error {
|
|
|
12
13
|
this.name = "SavepointDiscard";
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
function isStatementTimeout(e) {
|
|
17
|
+
return extractPgError(e)?.code === PG_QUERY_CANCELED_SQLSTATE;
|
|
18
|
+
}
|
|
19
|
+
// SET cannot take bind parameters, so the value is interpolated — only after it is proven a plain integer.
|
|
20
|
+
function memberReadSessionSettingsSql(timeoutMs, bindIdleTimeout) {
|
|
21
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
22
|
+
throw new InternalError({
|
|
23
|
+
message: `member read timeout must be a positive integer, got ${timeoutMs}`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const idleTimeout = bindIdleTimeout
|
|
27
|
+
? `; SET LOCAL idle_in_transaction_session_timeout = ${timeoutMs}`
|
|
28
|
+
: "";
|
|
29
|
+
return `SET TRANSACTION READ ONLY; SET LOCAL statement_timeout = ${timeoutMs}${idleTimeout}`;
|
|
30
|
+
}
|
|
31
|
+
async function runInDiscardedReadOnlySavepoint(tx, timeoutMs, bindIdleTimeout, fn) {
|
|
16
32
|
const settled = {};
|
|
17
33
|
try {
|
|
18
34
|
await runInSavepoint(tx, async (sp) => {
|
|
19
35
|
// kumiko-lint-ignore raw-sql transaction characteristic, no query helper
|
|
20
|
-
await asRawClient(sp).unsafe(
|
|
36
|
+
await asRawClient(sp).unsafe(memberReadSessionSettingsSql(timeoutMs, bindIdleTimeout));
|
|
21
37
|
// @cast-boundary driver savepoint handle — structurally a DbTx, same shape runInSavepoint's caller relies on
|
|
22
38
|
settled.outcome = { value: await fn(sp) };
|
|
23
39
|
// Always discarded: RELEASE SAVEPOINT would leave the outer tx read-only too.
|
|
@@ -36,9 +52,10 @@ async function runInDiscardedReadOnlySavepoint(tx, fn) {
|
|
|
36
52
|
return settled.outcome.value;
|
|
37
53
|
}
|
|
38
54
|
export async function runInMemberReadOnlyTransaction(ctx, tx, fn) {
|
|
55
|
+
const timeoutMs = ctx.memberReadTimeoutMs;
|
|
39
56
|
try {
|
|
40
57
|
if (tx)
|
|
41
|
-
return await runInDiscardedReadOnlySavepoint(tx, fn);
|
|
58
|
+
return await runInDiscardedReadOnlySavepoint(tx, timeoutMs, false, fn);
|
|
42
59
|
const pool = resolveDbSource(ctx, undefined);
|
|
43
60
|
if (!pool) {
|
|
44
61
|
throw new InternalError({
|
|
@@ -48,13 +65,16 @@ export async function runInMemberReadOnlyTransaction(ctx, tx, fn) {
|
|
|
48
65
|
// Savepoint even on the pool path: Postgres rejects a READ WRITE reset (SQLSTATE 25001) inside a subtransaction.
|
|
49
66
|
return await transaction(pool, (outer) =>
|
|
50
67
|
// @cast-boundary driver begin() handle — structurally a DbTx, same shape runInSavepoint's caller relies on
|
|
51
|
-
runInDiscardedReadOnlySavepoint(outer, fn));
|
|
68
|
+
runInDiscardedReadOnlySavepoint(outer, timeoutMs, true, fn));
|
|
52
69
|
}
|
|
53
70
|
catch (e) {
|
|
54
71
|
if (e instanceof AccessDeniedError)
|
|
55
72
|
throw e;
|
|
56
73
|
if (isReadOnlyTransactionViolation(e))
|
|
57
74
|
throw memberResolutionReadOnlyDenied(e);
|
|
75
|
+
if (isStatementTimeout(e)) {
|
|
76
|
+
throw new MemberReadTimeoutError({ timeoutMs, ...(e instanceof Error && { cause: e }) });
|
|
77
|
+
}
|
|
58
78
|
throw e;
|
|
59
79
|
}
|
|
60
80
|
}
|
|
@@ -8,7 +8,7 @@ import { createRebuildDeadLetterTable, recordRebuildDeadLetters, } from "../even
|
|
|
8
8
|
import { upcastStoredEvents } from "../event-store/upcaster.js";
|
|
9
9
|
import { emitProjectionRebuild } from "../observability/standard-metrics.js";
|
|
10
10
|
import { SHARED_INSTANCE_SENTINEL } from "./event-consumer-state.js";
|
|
11
|
-
import { capPendingGapsBelowCursor, subtractSortedIdsFromRanges } from "./pending-gap-ranges.js";
|
|
11
|
+
import { capPendingGapsBelowCursor, mergeContiguousGapRanges, subtractSortedIdsFromRanges, } from "./pending-gap-ranges.js";
|
|
12
12
|
function createRebuildCtx(registry, db, tenantId) {
|
|
13
13
|
// Both surfaces throw — rebuild MUST NOT emit. We share one impl.
|
|
14
14
|
const refuseAppend = async (args) => {
|
|
@@ -128,9 +128,9 @@ export async function rebuildMultiStreamProjection(mspName, deps) {
|
|
|
128
128
|
const survivingGaps = subtractSortedIdsFromRanges(seededGaps, replayedIds);
|
|
129
129
|
const cappedGaps = capPendingGapsBelowCursor(survivingGaps, lastProcessedEventId);
|
|
130
130
|
const postReplayXmax = cappedGaps.length > 0 ? await selectSnapshotXmax(tx) : null;
|
|
131
|
-
const pendingGaps = postReplayXmax === null
|
|
131
|
+
const pendingGaps = mergeContiguousGapRanges(postReplayXmax === null
|
|
132
132
|
? cappedGaps
|
|
133
|
-
: cappedGaps.map((gap) => ({ ...gap, xmax: postReplayXmax }));
|
|
133
|
+
: cappedGaps.map((gap) => ({ ...gap, xmax: postReplayXmax })));
|
|
134
134
|
await updateConsumerRebuildCursor(tx, mspName, SHARED_INSTANCE_SENTINEL, lastProcessedEventId, pendingGaps);
|
|
135
135
|
await swapShadowIntoLive(tx, meta.tableName);
|
|
136
136
|
});
|
|
@@ -10,3 +10,4 @@ export declare function partitionBurntGaps(gaps: readonly PendingGapEntry[], fet
|
|
|
10
10
|
export declare function splitRangeExcludingIds(range: PendingGapEntry, excludeIds: readonly bigint[]): PendingGapEntry[];
|
|
11
11
|
export declare function subtractSortedIdsFromRanges(ranges: readonly PendingGapEntry[], sortedIds: readonly bigint[]): PendingGapEntry[];
|
|
12
12
|
export declare function capPendingGapsBelowCursor(gaps: readonly PendingGapEntry[], cursor: bigint): PendingGapEntry[];
|
|
13
|
+
export declare function mergeContiguousGapRanges(gaps: readonly PendingGapEntry[]): PendingGapEntry[];
|
|
@@ -97,3 +97,25 @@ export function capPendingGapsBelowCursor(gaps, cursor) {
|
|
|
97
97
|
}
|
|
98
98
|
return capped;
|
|
99
99
|
}
|
|
100
|
+
// Merges ranges that touch exactly (end + 1 === next start). Keeps the row small
|
|
101
|
+
// when many adjacent id holes appear one pass after another. The merged xmax is
|
|
102
|
+
// the larger one: finality may only be claimed once the later-recorded bound has
|
|
103
|
+
// passed, so merging can delay it but never make it premature.
|
|
104
|
+
export function mergeContiguousGapRanges(gaps) {
|
|
105
|
+
const sorted = [...gaps].sort((a, b) => {
|
|
106
|
+
const diff = BigInt(a.from) - BigInt(b.from);
|
|
107
|
+
return diff < 0n ? -1 : diff > 0n ? 1 : 0;
|
|
108
|
+
});
|
|
109
|
+
const merged = [];
|
|
110
|
+
for (const gap of sorted) {
|
|
111
|
+
const last = merged.at(-1);
|
|
112
|
+
if (last && BigInt(last.to) + 1n === BigInt(gap.from)) {
|
|
113
|
+
const xmax = BigInt(gap.xmax) > BigInt(last.xmax) ? gap.xmax : last.xmax;
|
|
114
|
+
merged[merged.length - 1] = { from: last.from, to: gap.to, xmax };
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
merged.push(gap);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return merged;
|
|
121
|
+
}
|
|
@@ -400,7 +400,7 @@ export function createSseBroadcastEventConsumer(sseBroker) {
|
|
|
400
400
|
// exist yet has no audience to replay it to.
|
|
401
401
|
startFrom: "now",
|
|
402
402
|
handler: async (event) => {
|
|
403
|
-
sseBroker.pushToChannel(tenantChannel(event.tenantId), {
|
|
403
|
+
await sseBroker.pushToChannel(tenantChannel(event.tenantId), {
|
|
404
404
|
type: event.type,
|
|
405
405
|
data: {
|
|
406
406
|
id: event.aggregateId,
|
|
@@ -571,7 +571,7 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
|
|
|
571
571
|
// every user behind one bad row).
|
|
572
572
|
if (typeof userId !== "string" || userId.length === 0)
|
|
573
573
|
return;
|
|
574
|
-
sseBroker.publishAccessInvalidation(userId, readSessionRevokedScope(event.payload));
|
|
574
|
+
await sseBroker.publishAccessInvalidation(userId, readSessionRevokedScope(event.payload));
|
|
575
575
|
}
|
|
576
576
|
if (event.type === PAT_REVOKED_EVENT_TYPE) {
|
|
577
577
|
const userId = event.payload["userId"];
|
|
@@ -579,7 +579,7 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
|
|
|
579
579
|
// session-revoked above.
|
|
580
580
|
if (typeof userId !== "string" || userId.length === 0)
|
|
581
581
|
return;
|
|
582
|
-
sseBroker.publishAccessInvalidation(userId, readPatRevokedScope(event.payload));
|
|
582
|
+
await sseBroker.publishAccessInvalidation(userId, readPatRevokedScope(event.payload));
|
|
583
583
|
}
|
|
584
584
|
if (event.type === TENANT_MEMBERSHIP_UPDATED_EVENT_TYPE ||
|
|
585
585
|
event.type === TENANT_MEMBERSHIP_DELETED_EVENT_TYPE) {
|
|
@@ -588,7 +588,7 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
|
|
|
588
588
|
// reasoning as above.
|
|
589
589
|
if (userId === undefined)
|
|
590
590
|
return;
|
|
591
|
-
sseBroker.publishAccessInvalidation(userId);
|
|
591
|
+
await sseBroker.publishAccessInvalidation(userId);
|
|
592
592
|
}
|
|
593
593
|
},
|
|
594
594
|
};
|
|
@@ -24,7 +24,7 @@ type MemberReaderAudit = IdentitySwitchAudit & {
|
|
|
24
24
|
};
|
|
25
25
|
export declare function createGatedIdentitySwitch(callerLabel: string, caller: SessionUser | undefined, hasGrant: boolean, ungated: IdentitySwitch, audit?: IdentitySwitchAudit): IdentitySwitch;
|
|
26
26
|
export declare function createGatedMemberReader(callerLabel: string, allowSystemIdentity: boolean, ungated: MemberReader, audit?: MemberReaderAudit): MemberReader;
|
|
27
|
-
export declare function createGatedProjectionReader(callerLabel: string, hasGrant: boolean, ungated: ProjectionReader, audit?: IdentitySwitchAudit): ProjectionReader;
|
|
27
|
+
export declare function createGatedProjectionReader(callerLabel: string, hasGrant: boolean, ungated: ProjectionReader, audit?: IdentitySwitchAudit, projectionHasTenantColumn?: (qualifiedName: string) => boolean): ProjectionReader;
|
|
28
28
|
export declare function withHookEscapeHatchGrant<TContext extends object>(context: TContext, callerLabel: string, escapeHatch: EscapeHatchDeclaration | undefined): TContext;
|
|
29
29
|
export declare function bindHookEscapeHatchGrant(fn: LifecycleHookFn, label: string, escapeHatch: EscapeHatchDeclaration | undefined): LifecycleHookFn;
|
|
30
30
|
export {};
|
|
@@ -104,11 +104,12 @@ export function createGatedMemberReader(callerLabel, allowSystemIdentity, ungate
|
|
|
104
104
|
}
|
|
105
105
|
// Same idea, for ctx.queryProjection — a single function rather than a pair.
|
|
106
106
|
const ungatedProjectionReaderByGated = new WeakMap();
|
|
107
|
-
// ctx.queryProjection's gate: only { unsafeAllTenants: true }
|
|
108
|
-
// a plain call always passes through, tenant-scoped
|
|
109
|
-
|
|
107
|
+
// ctx.queryProjection's gate: only { unsafeAllTenants: true } on a projection with a
|
|
108
|
+
// tenant column is grant-checked — a plain call always passes through, tenant-scoped
|
|
109
|
+
// by the projection's own table, and the flag is a no-op on a tenant-less one.
|
|
110
|
+
export function createGatedProjectionReader(callerLabel, hasGrant, ungated, audit, projectionHasTenantColumn = () => true) {
|
|
110
111
|
const gated = async (qualifiedName, options) => {
|
|
111
|
-
if (options?.unsafeAllTenants === true) {
|
|
112
|
+
if (options?.unsafeAllTenants === true && projectionHasTenantColumn(qualifiedName)) {
|
|
112
113
|
if (!hasGrant)
|
|
113
114
|
throw unsafeAllTenantsDenied(callerLabel);
|
|
114
115
|
if (audit?.reason !== undefined) {
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// SELECT (sometimes two, cascade + fallback) per request, often inside an
|
|
5
5
|
// open write transaction. A tenant-keyed TTL cache removes the steady-state
|
|
6
6
|
// cost; dispatch-write.ts invalidates entries synchronously on
|
|
7
|
-
// config:write:set/reset for this key,
|
|
8
|
-
//
|
|
9
|
-
|
|
7
|
+
// config:write:set/reset for this key, but only in the process that handled
|
|
8
|
+
// the write. The TTL below therefore bounds how long OTHER replicas (and
|
|
9
|
+
// writes that bypass that path: migrations, seeds, direct DB edits) keep
|
|
10
|
+
// serving the old value, hence the short default.
|
|
11
|
+
const DEFAULT_TTL_MS = 30 * 1000;
|
|
10
12
|
const DEFAULT_MAX_ENTRIES = 1000;
|
|
11
13
|
export function createTenantTimezoneCache(opts = {}) {
|
|
12
14
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
@@ -2,6 +2,7 @@ import type { TenantId } from "../engine/types/identifiers.js";
|
|
|
2
2
|
export declare const CACHE_SYNC_TOPICS: {
|
|
3
3
|
readonly tenantConfig: "tenant-config";
|
|
4
4
|
readonly tierAssignment: "tier-assignment";
|
|
5
|
+
readonly featureToggle: "feature-toggle";
|
|
5
6
|
};
|
|
6
7
|
export type TenantConfigSyncMessage = {
|
|
7
8
|
readonly tenantId: TenantId;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const CACHE_SYNC_TOPICS = {
|
|
2
2
|
tenantConfig: "tenant-config",
|
|
3
3
|
tierAssignment: "tier-assignment",
|
|
4
|
+
featureToggle: "feature-toggle",
|
|
4
5
|
};
|
|
5
6
|
function hasOptionalStringKey(value) {
|
|
6
7
|
return !("key" in value) || value.key === undefined || typeof value.key === "string";
|
package/dist/redis/client.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export type RedisClientOptions = {
|
|
|
6
6
|
readonly extra?: Readonly<Omit<RedisOptions, "connectTimeout" | "commandTimeout" | "maxRetriesPerRequest">>;
|
|
7
7
|
};
|
|
8
8
|
export declare function createRedisClient(url: string, options?: RedisClientOptions): Redis;
|
|
9
|
+
export declare function redisChannelPrefixFromEnv(env?: Readonly<Record<string, string | undefined>>): string;
|
|
9
10
|
export declare function redisClientOptionsFromEnv(env?: Readonly<Record<string, string | undefined>>): RedisClientOptions;
|
package/dist/redis/client.js
CHANGED
|
@@ -26,6 +26,13 @@ export function createRedisClient(url, options = {}) {
|
|
|
26
26
|
maxRetriesPerRequest,
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
// Redis Pub/Sub ignores the DB index and ioredis keyPrefix, so apps or environments sharing one
|
|
30
|
+
// Redis instance would see each other's SSE and toggle-sync traffic. An optional prefix
|
|
31
|
+
// namespaces those channels; unset keeps the unprefixed channel names.
|
|
32
|
+
export function redisChannelPrefixFromEnv(env = process.env) {
|
|
33
|
+
const raw = env["KUMIKO_REDIS_CHANNEL_PREFIX"]?.trim();
|
|
34
|
+
return raw ? `${raw}:` : "";
|
|
35
|
+
}
|
|
29
36
|
// Env-var reader — mirrors dbConnectionOptionsFromEnv's contract. Strict:
|
|
30
37
|
// malformed values throw at boot rather than silently falling back to
|
|
31
38
|
// defaults.
|
package/dist/redis/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export { createCacheSyncBusOverSignal, createDefaultCacheSyncBus, createLocalCac
|
|
|
3
3
|
export type { TenantConfigSyncMessage, TierAssignmentSyncMessage } from "./cache-sync-topics.js";
|
|
4
4
|
export { CACHE_SYNC_TOPICS, isTenantConfigSyncMessage, isTierAssignmentSyncMessage, } from "./cache-sync-topics.js";
|
|
5
5
|
export type { RedisClientOptions } from "./client.js";
|
|
6
|
-
export { createRedisClient, redisClientOptionsFromEnv } from "./client.js";
|
|
6
|
+
export { createRedisClient, redisChannelPrefixFromEnv, redisClientOptionsFromEnv, } from "./client.js";
|
|
7
7
|
export type { PubSubSignal, PubSubSignalOptions } from "./pubsub-signal.js";
|
|
8
8
|
export { createRedisPubSubSignal } from "./pubsub-signal.js";
|
package/dist/redis/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createCacheSyncBusOverSignal, createDefaultCacheSyncBus, createLocalCacheSyncBus, createRedisCacheSyncBus, resolveCacheSyncBus, } from "./cache-sync-bus.js";
|
|
2
2
|
export { CACHE_SYNC_TOPICS, isTenantConfigSyncMessage, isTierAssignmentSyncMessage, } from "./cache-sync-topics.js";
|
|
3
|
-
export { createRedisClient, redisClientOptionsFromEnv } from "./client.js";
|
|
3
|
+
export { createRedisClient, redisChannelPrefixFromEnv, redisClientOptionsFromEnv, } from "./client.js";
|
|
4
4
|
export { createRedisPubSubSignal } from "./pubsub-signal.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RedisClientOptions } from "./client.js";
|
|
2
2
|
export type PubSubSignal = {
|
|
3
3
|
publish(channel: string, payload: unknown): void;
|
|
4
|
+
publishConfirmed(channel: string, payload: unknown): Promise<void>;
|
|
4
5
|
onMessage(listener: (channel: string, payload: unknown) => void): void;
|
|
5
6
|
onReconnect(listener: () => void): void;
|
|
6
7
|
close(): Promise<void>;
|
|
@@ -92,6 +92,9 @@ export function createRedisPubSubSignal(opts) {
|
|
|
92
92
|
console.error(`[kumiko:${opts.label}] publish failed:`, err);
|
|
93
93
|
});
|
|
94
94
|
},
|
|
95
|
+
async publishConfirmed(channel, payload) {
|
|
96
|
+
await publisher.publish(channel, JSON.stringify(payload));
|
|
97
|
+
},
|
|
95
98
|
onMessage(listener) {
|
|
96
99
|
listeners.add(listener);
|
|
97
100
|
},
|
|
@@ -20,6 +20,14 @@ async function awaitSucceededTask(enqueued, toleratedErrorCodes = []) {
|
|
|
20
20
|
// would fail forever.
|
|
21
21
|
const REMOVE_TOLERATED_ERROR_CODES = [ErrorStatusCode.INDEX_NOT_FOUND];
|
|
22
22
|
const INDEX_LIST_PAGE_SIZE = 100;
|
|
23
|
+
// What an unconfigured Meilisearch index reports as searchableAttributes.
|
|
24
|
+
const MEILI_ALL_ATTRIBUTES = "*";
|
|
25
|
+
function isIndexNotFound(err) {
|
|
26
|
+
const cause = typeof err === "object" && err !== null ? err.cause : null;
|
|
27
|
+
return (typeof cause === "object" &&
|
|
28
|
+
cause !== null &&
|
|
29
|
+
cause.code === ErrorStatusCode.INDEX_NOT_FOUND);
|
|
30
|
+
}
|
|
23
31
|
// Exported for unit tests (index-name / primary-key shape) without a live Meili.
|
|
24
32
|
export function meilisearchTenantIndex(prefix, tenantId) {
|
|
25
33
|
return `${prefix}t${tenantId}`;
|
|
@@ -58,6 +66,24 @@ export function createMeilisearchAdapter(options) {
|
|
|
58
66
|
// found". A failed configure is never memoized — remove the map entry (iff
|
|
59
67
|
// it's still the same in-flight promise) and rethrow, so a transient
|
|
60
68
|
// Meili outage doesn't permanently poison the tenant for the process.
|
|
69
|
+
// The configured-tenants map is process memory only: after a restart a stored explicit
|
|
70
|
+
// configure() must not be overwritten by the registry default, so the default is applied only
|
|
71
|
+
// to an index that is missing or still carries Meilisearch's factory settings.
|
|
72
|
+
async function indexNeedsDefaultConfig(tenantId) {
|
|
73
|
+
try {
|
|
74
|
+
const settings = await client.index(meilisearchTenantIndex(prefix, tenantId)).getSettings();
|
|
75
|
+
const searchable = settings.searchableAttributes ?? [MEILI_ALL_ATTRIBUTES];
|
|
76
|
+
const filterable = settings.filterableAttributes ?? [];
|
|
77
|
+
return (searchable.length === 1 &&
|
|
78
|
+
searchable[0] === MEILI_ALL_ATTRIBUTES &&
|
|
79
|
+
!filterable.includes("_type"));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (isIndexNotFound(err))
|
|
83
|
+
return true;
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
61
87
|
async function ensureConfigured(tenantId) {
|
|
62
88
|
// skip: no registry-derived default — callers configure tenants explicitly.
|
|
63
89
|
if (!defaultConfig)
|
|
@@ -68,10 +94,14 @@ export function createMeilisearchAdapter(options) {
|
|
|
68
94
|
// skip: configured (or in flight) already for this tenant.
|
|
69
95
|
return;
|
|
70
96
|
}
|
|
71
|
-
|
|
97
|
+
const config = defaultConfig;
|
|
98
|
+
await trackConfigure(tenantId, async () => {
|
|
99
|
+
if (await indexNeedsDefaultConfig(tenantId))
|
|
100
|
+
await applyConfig(tenantId, config);
|
|
101
|
+
});
|
|
72
102
|
}
|
|
73
|
-
async function trackConfigure(tenantId,
|
|
74
|
-
const pending =
|
|
103
|
+
async function trackConfigure(tenantId, run) {
|
|
104
|
+
const pending = run();
|
|
75
105
|
configuredTenants.set(tenantId, pending);
|
|
76
106
|
try {
|
|
77
107
|
await pending;
|
|
@@ -88,7 +118,7 @@ export function createMeilisearchAdapter(options) {
|
|
|
88
118
|
defaultConfig = config;
|
|
89
119
|
},
|
|
90
120
|
async configure(tenantId, config) {
|
|
91
|
-
await trackConfigure(tenantId, config);
|
|
121
|
+
await trackConfigure(tenantId, () => applyConfig(tenantId, config));
|
|
92
122
|
},
|
|
93
123
|
async index(tenantId, doc) {
|
|
94
124
|
await ensureConfigured(tenantId);
|
|
@@ -146,7 +146,7 @@ export type TestStackOptions = {
|
|
|
146
146
|
runLogger?: (deps: {
|
|
147
147
|
registry: Registry;
|
|
148
148
|
db: import("../db/index.js").DbConnection;
|
|
149
|
-
}) => Pick<JobRunnerOptions, "onJobStart" | "onJobComplete" | "onJobFailed"> | undefined;
|
|
149
|
+
}) => Pick<JobRunnerOptions, "onJobStart" | "onJobComplete" | "onJobFailed" | "onJobQueued" | "onJobDropped"> | undefined;
|
|
150
150
|
};
|
|
151
151
|
/** Override the event dispatcher's polling-timer interval. Default 50ms.
|
|
152
152
|
* Tests that assert LISTEN/NOTIFY wake-up latency need this pushed far
|
|
@@ -157,6 +157,9 @@ export type TestStackOptions = {
|
|
|
157
157
|
* them themselves. Default true — false pushes only projection/MSP/
|
|
158
158
|
* storeTable sources, same as before fw#3102. */
|
|
159
159
|
entityTables?: boolean;
|
|
160
|
+
/** Time source of the stack's rate-limit resolver, so tests can freeze the
|
|
161
|
+
* clock instead of racing the window. Default: system time. */
|
|
162
|
+
rateLimitNowMs?: () => number;
|
|
160
163
|
/** Forwarded to buildServer's top-level `ServerOptions.trustedProxyHops`
|
|
161
164
|
* — see there. Default 0. */
|
|
162
165
|
trustedProxyHops?: number;
|
package/dist/stack/test-stack.js
CHANGED
|
@@ -189,6 +189,8 @@ export async function setupTestStack(options) {
|
|
|
189
189
|
getActiveTenantIds: options.jobs.getActiveTenantIds,
|
|
190
190
|
}),
|
|
191
191
|
...(runLogger?.onJobStart !== undefined && { onJobStart: runLogger.onJobStart }),
|
|
192
|
+
...(runLogger?.onJobQueued !== undefined && { onJobQueued: runLogger.onJobQueued }),
|
|
193
|
+
...(runLogger?.onJobDropped !== undefined && { onJobDropped: runLogger.onJobDropped }),
|
|
192
194
|
// Tracker fires in finally: drainJobs wakes on it, so the log row must
|
|
193
195
|
// be written first, and a throwing logger must not swallow the signal.
|
|
194
196
|
onJobComplete: async (jobName, jobId, ...rest) => {
|
|
@@ -268,8 +270,13 @@ export async function setupTestStack(options) {
|
|
|
268
270
|
// `rateLimit` key, so reading it back needs the same relaxed boundary
|
|
269
271
|
// extraContext itself is typed with.
|
|
270
272
|
const explicitRateLimit = appContext.rateLimit;
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
+
const needsOwnRateLimitResolver = explicitRateLimit === undefined &&
|
|
274
|
+
(appContext.redis === undefined || options.rateLimitNowMs !== undefined);
|
|
275
|
+
const rateLimitFallback = needsOwnRateLimitResolver
|
|
276
|
+
? createRateLimitResolver({
|
|
277
|
+
redis: testRedis.redis,
|
|
278
|
+
...(options.rateLimitNowMs ? { nowMs: options.rateLimitNowMs } : {}),
|
|
279
|
+
})
|
|
273
280
|
: undefined;
|
|
274
281
|
const server = buildServer({
|
|
275
282
|
registry,
|
|
@@ -325,8 +325,13 @@ function fieldToFixture(name, field) {
|
|
|
325
325
|
return `e2e ${name} (long-form content)`;
|
|
326
326
|
case "boolean":
|
|
327
327
|
return true;
|
|
328
|
-
case "select":
|
|
329
|
-
|
|
328
|
+
case "select": {
|
|
329
|
+
const conditional = new Set(field.conditionalOptions?.flatMap((rule) => rule.options));
|
|
330
|
+
return (field.default ??
|
|
331
|
+
field.options.find((option) => !conditional.has(option)) ??
|
|
332
|
+
field.options[0] ??
|
|
333
|
+
"");
|
|
334
|
+
}
|
|
330
335
|
case "multiSelect": {
|
|
331
336
|
const first = field.options[0];
|
|
332
337
|
return first ? [first] : [];
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type { DerivedCellRoundingTarget } from "../engine/embedded-derived.js";
|
|
|
3
3
|
export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
|
|
4
4
|
export type { ParsedRefTarget } from "../engine/parse-ref-target.js";
|
|
5
5
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
6
|
-
export type { FieldsOrGroupsSection } from "../engine/screen-helpers.js";
|
|
7
|
-
export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
6
|
+
export type { FieldsOrGroupsSection, UnavailableSelectOption } from "../engine/screen-helpers.js";
|
|
7
|
+
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
8
8
|
export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.js";
|
|
9
9
|
export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
|
|
10
10
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
@@ -14,7 +14,7 @@ export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "
|
|
|
14
14
|
export type { TargetRef } from "../engine/types/target-ref.js";
|
|
15
15
|
export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
|
|
16
16
|
export type { WorkspaceDefinition } from "../engine/types/workspace.js";
|
|
17
|
-
export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
17
|
+
export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
18
18
|
export type { AppSchema, FeatureSchema, QualifiedContentCollection, WorkspaceSchema, } from "./app-schema.js";
|
|
19
19
|
export { type Formality, formalLocaleTag } from "./formality.js";
|
|
20
20
|
export type { ListRowMetaColumnType, ListRowMetaReference, ReferenceLookupSource, SystemReferenceLabel, } from "./list-row-meta.js";
|
package/dist/ui-types/index.js
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
export { NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-types/fields";
|
|
23
23
|
export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
|
|
24
24
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
25
|
-
export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
25
|
+
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
26
26
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
27
27
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
|
|
28
|
-
export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
28
|
+
export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
29
29
|
export { formalLocaleTag } from "./formality.js";
|
|
30
30
|
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
|
|
31
31
|
export { resolveTranslationValue, translationValueOtherText, } from "./plural.js";
|
package/dist/upgrade-cli.js
CHANGED
|
@@ -257,6 +257,9 @@ async function runCodemodScript(scriptPath, targetDir, repoRoot, dryRun) {
|
|
|
257
257
|
function hasCodemod(e) {
|
|
258
258
|
return typeof e.codemod === "string" && e.codemod.length > 0;
|
|
259
259
|
}
|
|
260
|
+
function needsManualMigration(e) {
|
|
261
|
+
return !hasCodemod(e) || e.manualAfterCodemod === true;
|
|
262
|
+
}
|
|
260
263
|
const MANUAL_ID_HASH_LENGTH = 8;
|
|
261
264
|
const MAX_RESOLVE_REASON_LENGTH = 500;
|
|
262
265
|
// The hash tells apart entries that share a version (studio has several at 0.341.0).
|
|
@@ -438,9 +441,11 @@ markerVersion, previous) {
|
|
|
438
441
|
const codemodEntries = breaking
|
|
439
442
|
.filter(hasCodemod)
|
|
440
443
|
.sort((a, b) => compareVersions(a.version, b.version));
|
|
441
|
-
const manualEntries = breaking.filter(
|
|
444
|
+
const manualEntries = breaking.filter(needsManualMigration);
|
|
442
445
|
for (const e of manualEntries) {
|
|
443
|
-
out.log(
|
|
446
|
+
out.log(hasCodemod(e)
|
|
447
|
+
? ` ⚠ ${e.version} · ${e.title} — codemod covers only part, manual migration still required`
|
|
448
|
+
: ` ⚠ ${e.version} · ${e.title} — no codemod, manual migration required`);
|
|
444
449
|
}
|
|
445
450
|
if (codemodEntries.length === 0) {
|
|
446
451
|
out.log(breaking.length > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.354.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -187,6 +187,10 @@
|
|
|
187
187
|
"types": "./dist/testing/real-providers.d.ts",
|
|
188
188
|
"default": "./dist/testing/real-providers.js"
|
|
189
189
|
},
|
|
190
|
+
"./internal/request-context": {
|
|
191
|
+
"types": "./dist/api/request-context.d.ts",
|
|
192
|
+
"default": "./dist/api/request-context.js"
|
|
193
|
+
},
|
|
190
194
|
"./testing/e2e-generator": {
|
|
191
195
|
"types": "./dist/testing/e2e-generator.d.ts",
|
|
192
196
|
"default": "./dist/testing/e2e-generator.js"
|
|
@@ -206,8 +210,8 @@
|
|
|
206
210
|
"./package.json": "./package.json"
|
|
207
211
|
},
|
|
208
212
|
"dependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
210
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
213
|
+
"@cosmicdrift/kumiko-http": "0.354.1",
|
|
214
|
+
"@cosmicdrift/kumiko-types": "0.354.1",
|
|
211
215
|
"bullmq": "^5.76.7",
|
|
212
216
|
"bun-types": "^1.3.13",
|
|
213
217
|
"hono": "^4.13.1",
|
|
@@ -223,7 +227,7 @@
|
|
|
223
227
|
"zod": "^4.4.3"
|
|
224
228
|
},
|
|
225
229
|
"devDependencies": {
|
|
226
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
230
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.354.1",
|
|
227
231
|
"bun-types": "^1.3.13",
|
|
228
232
|
"pino-pretty": "^13.1.3"
|
|
229
233
|
},
|
|
@@ -387,6 +391,10 @@
|
|
|
387
391
|
"types": "./dist/testing/real-providers.d.ts",
|
|
388
392
|
"default": "./dist/testing/real-providers.js"
|
|
389
393
|
},
|
|
394
|
+
"./internal/request-context": {
|
|
395
|
+
"types": "./dist/api/request-context.d.ts",
|
|
396
|
+
"default": "./dist/api/request-context.js"
|
|
397
|
+
},
|
|
390
398
|
"./testing/e2e-generator": {
|
|
391
399
|
"types": "./dist/testing/e2e-generator.d.ts",
|
|
392
400
|
"default": "./dist/testing/e2e-generator.js"
|