@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
package/dist/errors/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
1
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
2
|
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
3
3
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
4
4
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
5
5
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
6
6
|
export { buildErrorLog, serializeError } from "./serialize.js";
|
|
7
|
+
export { subjectErasedConflict } from "./subject-erased.js";
|
|
7
8
|
export { toKumikoError } from "./to-kumiko-error.js";
|
|
8
9
|
export { buildInvalidTransitionDetails } from "./transition-details.js";
|
|
9
10
|
export { failNotFound, failTransition, failUnprocessable, reraiseAsKumikoError, toWriteErrorInfo, writeFailure, } from "./write-error-info.js";
|
package/dist/errors/reasons.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const FrameworkReasons: {
|
|
|
12
12
|
readonly publicIntakeRequired: "public_intake_required";
|
|
13
13
|
readonly irreversibleOperationRequiresHighRisk: "irreversible_operation_requires_high_risk";
|
|
14
14
|
readonly instructionFieldWriteRequiresHighRisk: "instruction_field_write_requires_high_risk";
|
|
15
|
+
readonly subjectErased: "subject_erased";
|
|
15
16
|
};
|
|
16
17
|
export type FrameworkReason = (typeof FrameworkReasons)[keyof typeof FrameworkReasons];
|
|
17
18
|
export declare const AgentReasons: {
|
package/dist/errors/reasons.js
CHANGED
|
@@ -61,6 +61,9 @@ export const FrameworkReasons = {
|
|
|
61
61
|
// delegation via ctx.write/writeAs from a lower-risk handler does not
|
|
62
62
|
// inherit the gate.
|
|
63
63
|
instructionFieldWriteRequiresHighRisk: "instruction_field_write_requires_high_risk",
|
|
64
|
+
// ConflictError: a write touched a personal-data field of a data subject whose
|
|
65
|
+
// key was erased (crypto-shredding); the subject can no longer receive data.
|
|
66
|
+
subjectErased: "subject_erased",
|
|
64
67
|
};
|
|
65
68
|
// Reason codes the AI-agent runtime surfaces via `details.reason`. Kept as a
|
|
66
69
|
// sister catalog to FrameworkReasons instead of extending it — see the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ConflictError } from "./classes.js";
|
|
2
|
+
import { FrameworkReasons } from "./reasons.js";
|
|
3
|
+
export function subjectErasedConflict(cause) {
|
|
4
|
+
return new ConflictError({
|
|
5
|
+
message: "the data subject was erased — its personal data can no longer be written",
|
|
6
|
+
details: { reason: FrameworkReasons.subjectErased },
|
|
7
|
+
...(cause instanceof Error && { cause }),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Allowlist: samples/*/migration/, scripts/migrations/, die Definition
|
|
9
9
|
// selbst, das Guard-Script selbst.
|
|
10
10
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
11
|
-
import { claimXactId } from "../db/queries/event-store.js";
|
|
11
|
+
import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
|
|
12
12
|
import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
|
|
13
13
|
import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
|
|
14
14
|
function mapEventUniqueViolation(e, event) {
|
|
@@ -26,13 +26,15 @@ export async function appendRaw(runner, event) {
|
|
|
26
26
|
try {
|
|
27
27
|
// See db/queries/event-store.ts's claimXactId — gap-finality needs a
|
|
28
28
|
// real xact id assigned before this holder's insert.
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
await runAtomicEventWrite(runner, async (tx) => {
|
|
30
|
+
await claimXactId(tx);
|
|
31
|
+
if (event.expectedVersion === 0) {
|
|
32
|
+
await insertRawFirst(tx, event, newVersion, eventVersion);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await insertRawSubsequent(tx, event, newVersion, eventVersion);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
36
38
|
}
|
|
37
39
|
catch (e) {
|
|
38
40
|
if (isUniqueViolation(e)) {
|
|
@@ -90,8 +92,10 @@ export async function appendRawBatch(runner, events) {
|
|
|
90
92
|
return `($${baseIdx + 1}::uuid, $${baseIdx + 2}, $${baseIdx + 3}::uuid, $${baseIdx + 4}, $${baseIdx + 5}, $${baseIdx + 6}, $${baseIdx + 7}::jsonb, $${baseIdx + 8}::jsonb, $${baseIdx + 9}::timestamptz, $${baseIdx + 10})`;
|
|
91
93
|
});
|
|
92
94
|
try {
|
|
93
|
-
await
|
|
94
|
-
|
|
95
|
+
await runAtomicEventWrite(runner, async (tx) => {
|
|
96
|
+
await claimXactId(tx);
|
|
97
|
+
await insertRawEventBatch(tx, valuesClauses.join(", "), params);
|
|
98
|
+
});
|
|
95
99
|
}
|
|
96
100
|
catch (e) {
|
|
97
101
|
if (isUniqueViolation(e)) {
|
|
@@ -24,6 +24,7 @@ export declare function loadAggregateAsOf(db: DbRunner, aggregateId: string, ten
|
|
|
24
24
|
readonly includeArchived?: boolean;
|
|
25
25
|
}): Promise<readonly StoredEvent[]>;
|
|
26
26
|
export declare function getStreamVersion(db: DbRunner, aggregateId: string, tenantId: TenantId): Promise<number>;
|
|
27
|
+
export declare function getStreamVersions(db: DbRunner, aggregateIds: readonly string[], tenantId: TenantId): Promise<Map<string, number>>;
|
|
27
28
|
/** MAX(version) for one aggregate — no tenant filter. SECURITY: existence-oracle,
|
|
28
29
|
* a caller can probe whether an aggregateId has any events regardless of tenant
|
|
29
30
|
* membership. Only call from seed/system-internal paths (idempotency checks
|
|
@@ -4,7 +4,7 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
5
|
import { encryptEventPayloadPii } from "../crypto/event-pii.js";
|
|
6
6
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
7
|
-
import { claimXactId, insertSubsequentEventRow, notifyPgChannel, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, } from "../db/queries/event-store.js";
|
|
7
|
+
import { claimXactId, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
8
8
|
import { insertOne, selectMany } from "../db/query.js";
|
|
9
9
|
import { isStreamArchived } from "./archive.js";
|
|
10
10
|
import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
|
|
@@ -50,14 +50,16 @@ export async function append(db, event) {
|
|
|
50
50
|
const newVersion = toStore.expectedVersion + 1;
|
|
51
51
|
const eventVersion = toStore.eventVersion ?? 1;
|
|
52
52
|
try {
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
const row = await runAtomicEventWrite(db, async (runner) => {
|
|
54
|
+
await claimXactId(runner);
|
|
55
|
+
const inserted = toStore.expectedVersion === 0
|
|
56
|
+
? await insertFirstEvent(runner, toStore, newVersion, eventVersion, seedCreatedAt)
|
|
57
|
+
: await insertSubsequentEvent(runner, toStore, newVersion, eventVersion, seedCreatedAt);
|
|
58
|
+
// NOTIFY after the INSERT and inside the same transaction: PG delivers it on
|
|
59
|
+
// commit, so the dispatcher cannot wake before the row is visible.
|
|
60
|
+
await notifyPgChannel(runner, EVENTS_PUBSUB_CHANNEL);
|
|
61
|
+
return inserted;
|
|
62
|
+
});
|
|
61
63
|
return buildStoredEvent(toStore, newVersion, eventVersion, row);
|
|
62
64
|
}
|
|
63
65
|
catch (e) {
|
|
@@ -221,6 +223,9 @@ export async function loadAggregateAsOf(db, aggregateId, tenantId, asOf, options
|
|
|
221
223
|
export async function getStreamVersion(db, aggregateId, tenantId) {
|
|
222
224
|
return selectStreamMaxVersion(db, aggregateId, tenantId);
|
|
223
225
|
}
|
|
226
|
+
export async function getStreamVersions(db, aggregateIds, tenantId) {
|
|
227
|
+
return selectStreamMaxVersions(db, aggregateIds, tenantId);
|
|
228
|
+
}
|
|
224
229
|
/** MAX(version) for one aggregate — no tenant filter. SECURITY: existence-oracle,
|
|
225
230
|
* a caller can probe whether an aggregateId has any events regardless of tenant
|
|
226
231
|
* membership. Only call from seed/system-internal paths (idempotency checks
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sql now comes from native dialect
|
|
2
2
|
import { bigserial, index, instant, integer, jsonb, table as pgTable, sql, text, uniqueIndex, uuid, } from "../db/dialect.js";
|
|
3
3
|
import { tableExists } from "../db/index.js";
|
|
4
|
-
import { ensureIdempotencyKeyIndex } from "../db/queries/event-store.js";
|
|
4
|
+
import { ensureIdempotencyKeyIndex, ensureMembershipPayloadTenantIndex, } from "../db/queries/event-store.js";
|
|
5
5
|
import { unsafePushTables } from "../stack/index.js";
|
|
6
6
|
import { createArchivedStreamsTable } from "./archive.js";
|
|
7
7
|
import { createSnapshotsTable } from "./snapshot.js";
|
|
@@ -61,6 +61,7 @@ export async function createEventsTable(db) {
|
|
|
61
61
|
// that predate the idempotency-key index get healed the same way
|
|
62
62
|
// ensureSnapshotVersionColumn heals kumiko_snapshots.
|
|
63
63
|
await ensureIdempotencyKeyIndex(db);
|
|
64
|
+
await ensureMembershipPayloadTenantIndex(db);
|
|
64
65
|
await createArchivedStreamsTable(db);
|
|
65
66
|
await createSnapshotsTable(db);
|
|
66
67
|
}
|
|
@@ -2,7 +2,7 @@ export { APP_INSTANCE_STREAM_TYPE, APP_STARTED_EVENT_TYPE } from "../crypto/syst
|
|
|
2
2
|
export { backfillEventPiiEncryption, backfillEventPiiEncryptionBatch, type PiiBackfillBatchOptions, type PiiBackfillBatchResult, type PiiBackfillFailure, type PiiBackfillOptions, type PiiBackfillResult, type PiiBackfillScanCache, } from "../db/queries/backfill-pii.js";
|
|
3
3
|
export { type ArchiveStreamArgs, archivedStreamsTable, archiveStream, createArchivedStreamsTable, isStreamArchived, restoreStream, } from "./archive.js";
|
|
4
4
|
export { ArchivedStreamError, IdempotentAppendConflictError, VersionConflictError, } from "./errors.js";
|
|
5
|
-
export { append, EVENTS_PUBSUB_CHANNEL, type EventMetadata, type EventToAppend, getEventsHighWaterMark, getStreamVersion, getUnscopedAggregateStreamMaxVersion, getUnscopedStreamMaxVersionForSeed, LOAD_ALL_EVENTS_ROW_LIMIT, loadAggregate, loadAggregateAsOf, loadAllEventsByType, loadEventsAfterVersion, type StoredEvent, streamAllEventsByType, } from "./event-store.js";
|
|
5
|
+
export { append, EVENTS_PUBSUB_CHANNEL, type EventMetadata, type EventToAppend, getEventsHighWaterMark, getStreamVersion, getStreamVersions, getUnscopedAggregateStreamMaxVersion, getUnscopedStreamMaxVersionForSeed, LOAD_ALL_EVENTS_ROW_LIMIT, loadAggregate, loadAggregateAsOf, loadAllEventsByType, loadEventsAfterVersion, type StoredEvent, streamAllEventsByType, } from "./event-store.js";
|
|
6
6
|
export { createEventsTable, eventsTable } from "./events-schema.js";
|
|
7
7
|
export { appendProvenanceEvent, type ProvenanceEventInput } from "./provenance-append.js";
|
|
8
8
|
export { createRebuildDeadLetterTable, listRebuildDeadLetters, type RebuildDeadLetterRow, rebuildDeadLetterTable, recordRebuildDeadLetters, type SkippedApply, } from "./rebuild-dead-letter.js";
|
|
@@ -2,7 +2,7 @@ export { APP_INSTANCE_STREAM_TYPE, APP_STARTED_EVENT_TYPE } from "../crypto/syst
|
|
|
2
2
|
export { backfillEventPiiEncryption, backfillEventPiiEncryptionBatch, } from "../db/queries/backfill-pii.js";
|
|
3
3
|
export { archivedStreamsTable, archiveStream, createArchivedStreamsTable, isStreamArchived, restoreStream, } from "./archive.js";
|
|
4
4
|
export { ArchivedStreamError, IdempotentAppendConflictError, VersionConflictError, } from "./errors.js";
|
|
5
|
-
export { append, EVENTS_PUBSUB_CHANNEL, getEventsHighWaterMark, getStreamVersion, getUnscopedAggregateStreamMaxVersion, getUnscopedStreamMaxVersionForSeed, LOAD_ALL_EVENTS_ROW_LIMIT, loadAggregate, loadAggregateAsOf, loadAllEventsByType, loadEventsAfterVersion, streamAllEventsByType, } from "./event-store.js";
|
|
5
|
+
export { append, EVENTS_PUBSUB_CHANNEL, getEventsHighWaterMark, getStreamVersion, getStreamVersions, getUnscopedAggregateStreamMaxVersion, getUnscopedStreamMaxVersionForSeed, LOAD_ALL_EVENTS_ROW_LIMIT, loadAggregate, loadAggregateAsOf, loadAllEventsByType, loadEventsAfterVersion, streamAllEventsByType, } from "./event-store.js";
|
|
6
6
|
export { createEventsTable, eventsTable } from "./events-schema.js";
|
|
7
7
|
export { appendProvenanceEvent } from "./provenance-append.js";
|
|
8
8
|
export { createRebuildDeadLetterTable, listRebuildDeadLetters, rebuildDeadLetterTable, recordRebuildDeadLetters, } from "./rebuild-dead-letter.js";
|
|
@@ -2,6 +2,6 @@ import type { FileContext, FileHandle } from "@cosmicdrift/kumiko-types/file-han
|
|
|
2
2
|
import type { FileStorageProvider } from "./types.js";
|
|
3
3
|
export type { FileContext, FileHandle };
|
|
4
4
|
export declare function createFileHandle(key: string, getProvider: () => Promise<FileStorageProvider>): FileHandle;
|
|
5
|
-
export declare function createFileContext(resolve: () => Promise<FileStorageProvider
|
|
5
|
+
export declare function createFileContext(resolve: () => Promise<FileStorageProvider>, tenantId?: string): FileContext;
|
|
6
6
|
export declare function deriveKey(key: string, suffix: string): string;
|
|
7
7
|
export declare function storageKeyStemPrefix(key: string): string;
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// the bytes, and big files stay in the storage layer where they belong.
|
|
8
8
|
//
|
|
9
9
|
// `derive(suffix)` is the primitive for thumbnail/variant keys: it inserts a
|
|
10
|
-
// suffix before the file extension — `foo/bar.jpg` + `"medium"` →
|
|
11
|
-
// `foo/bar.medium
|
|
10
|
+
// suffix before the file extension — `foo/bar.jpg` + `"medium-<16 hex>"` →
|
|
11
|
+
// `foo/bar.medium-<16 hex>.jpg`. The suffix must be a `variantSuffix(...)` so
|
|
12
|
+
// the erasure sweep recognizes it. Stable, reversible, no extra lookup tables.
|
|
13
|
+
import { isDerivativeSuffix } from "../derivatives/variant-key.js";
|
|
12
14
|
// `getProvider` is a lazily-resolved, memoized accessor — the provider is
|
|
13
15
|
// resolved (config + s3.secretAccessKey secret read) only when a handle method
|
|
14
16
|
// actually does I/O, never on every request that merely builds a ctx.
|
|
@@ -19,7 +21,15 @@ export function createFileHandle(key, getProvider) {
|
|
|
19
21
|
write: async (data, mimeType) => (await getProvider()).write(key, data, mimeType),
|
|
20
22
|
delete: async () => (await getProvider()).delete(key),
|
|
21
23
|
exists: async () => (await getProvider()).exists(key),
|
|
22
|
-
derive: (suffix) =>
|
|
24
|
+
derive: (suffix) => {
|
|
25
|
+
// A suffix outside the derivative grammar would write a key that the
|
|
26
|
+
// forget/tenant-destroy sweep (isDerivativeKeyOf) can never recognize,
|
|
27
|
+
// so the binary would survive erasure.
|
|
28
|
+
if (!isDerivativeSuffix(suffix)) {
|
|
29
|
+
throw new Error(`FileHandle.derive: suffix "${suffix}" must be <name>-<16 hex chars>; build it with variantSuffix(name, spec)`);
|
|
30
|
+
}
|
|
31
|
+
return createFileHandle(deriveKey(key, suffix), getProvider);
|
|
32
|
+
},
|
|
23
33
|
};
|
|
24
34
|
}
|
|
25
35
|
// `resolve` is a tenant-bound thunk (the caller binds the tenant). The result
|
|
@@ -27,15 +37,29 @@ export function createFileHandle(key, getProvider) {
|
|
|
27
37
|
// produces — never a process-global cache, since each FileContext is bound to a
|
|
28
38
|
// single request/event tenant; sharing would leak one tenant's provider (and
|
|
29
39
|
// its bucket/credentials) to another.
|
|
30
|
-
|
|
40
|
+
//
|
|
41
|
+
// With `tenantId`, `ref` and `list` only accept keys/prefixes inside `${tenantId}/` —
|
|
42
|
+
// the key space is the isolation boundary in a store shared by several tenants, and
|
|
43
|
+
// a request-derived key or prefix (or "") would otherwise reach every tenant's keys.
|
|
44
|
+
export function createFileContext(resolve, tenantId) {
|
|
31
45
|
let cached;
|
|
32
46
|
const getProvider = () => {
|
|
33
47
|
cached ??= resolve();
|
|
34
48
|
return cached;
|
|
35
49
|
};
|
|
36
50
|
return {
|
|
37
|
-
ref: (key) =>
|
|
38
|
-
|
|
51
|
+
ref: (key) => {
|
|
52
|
+
if (tenantId !== undefined && !key.startsWith(`${tenantId}/`)) {
|
|
53
|
+
throw new Error(`FileContext.ref: key must start with "${tenantId}/"`);
|
|
54
|
+
}
|
|
55
|
+
return createFileHandle(key, getProvider);
|
|
56
|
+
},
|
|
57
|
+
list: async (prefix) => {
|
|
58
|
+
if (tenantId !== undefined && !prefix.startsWith(`${tenantId}/`)) {
|
|
59
|
+
throw new Error(`FileContext.list: prefix must start with "${tenantId}/"`);
|
|
60
|
+
}
|
|
61
|
+
return (await getProvider()).list(prefix);
|
|
62
|
+
},
|
|
39
63
|
};
|
|
40
64
|
}
|
|
41
65
|
// Splits a key into the part before the last dot of its last path segment
|
|
@@ -13,7 +13,7 @@ import { buildContentDispositionHeader } from "./content-disposition.js";
|
|
|
13
13
|
import { createFileContext } from "./file-handle.js";
|
|
14
14
|
import { fileRefEntity } from "./file-ref-entity.js";
|
|
15
15
|
import { fileRefsTable } from "./file-ref-table.js";
|
|
16
|
-
import { resolveContentType } from "./resolve-content-type.js";
|
|
16
|
+
import { neutralFileNameForMimeType, resolveContentType } from "./resolve-content-type.js";
|
|
17
17
|
import { buildStorageKey, parseMaxSize, resolveServedContentType, resolveUploadMimeType, validateFile, validateFileContent, } from "./types.js";
|
|
18
18
|
const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
|
|
19
19
|
// 15 minutes — long enough for a download to start, short enough that a
|
|
@@ -97,6 +97,7 @@ export function createFileRoutes(options) {
|
|
|
97
97
|
const api = new Hono();
|
|
98
98
|
const projectionGrants = options.registry ? { projectionRegistry: options.registry } : undefined;
|
|
99
99
|
const tenantDbFor = (tenantId) => createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, projectionGrants);
|
|
100
|
+
const fileNameIsPersonal = piiSubjectFields.includes("fileName");
|
|
100
101
|
function kmsContextFor() {
|
|
101
102
|
return { requestId: requestContext.get()?.requestId ?? "file-routes" };
|
|
102
103
|
}
|
|
@@ -311,7 +312,7 @@ export function createFileRoutes(options) {
|
|
|
311
312
|
}
|
|
312
313
|
// Built per request: createFileContext caches the resolved provider, so
|
|
313
314
|
// one shared across requests would serve tenant A's store to tenant B.
|
|
314
|
-
const files = createFileContext(() => options.resolveProvider(user.tenantId));
|
|
315
|
+
const files = createFileContext(() => options.resolveProvider(user.tenantId), user.tenantId);
|
|
315
316
|
const derivatives = createDerivativesContext({
|
|
316
317
|
files,
|
|
317
318
|
registry,
|
|
@@ -397,10 +398,15 @@ export function createFileRoutes(options) {
|
|
|
397
398
|
const expiresInSeconds = SIGNED_URL_DEFAULT_EXPIRY_SECONDS;
|
|
398
399
|
const url = await storageProvider.getSignedUrl(fileRef.storageKey, expiresInSeconds, {
|
|
399
400
|
// Hint the provider to set Content-Disposition so the browser prompts
|
|
400
|
-
// with
|
|
401
|
+
// with a readable filename instead of the UUID-based storage key.
|
|
401
402
|
// Sanitised via buildContentDispositionHeader — the same attacker-
|
|
402
403
|
// controlled fileName reaches the provider's presigned response.
|
|
403
|
-
|
|
404
|
+
// A personal fileName never goes into the URL: the signed query string
|
|
405
|
+
// lands in storage access logs and proxies and outlives a crypto-shred.
|
|
406
|
+
// The streaming route keeps the real name in a response header only.
|
|
407
|
+
contentDisposition: buildContentDispositionHeader(fileNameIsPersonal
|
|
408
|
+
? neutralFileNameForMimeType(fileRef.mimeType)
|
|
409
|
+
: await resolveFileName(fileRef)),
|
|
404
410
|
});
|
|
405
411
|
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000).toISOString();
|
|
406
412
|
return c.json({ url, expiresAt });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** A name derived from the MIME type alone, for places a personal file name must not appear. */
|
|
2
|
+
export declare function neutralFileNameForMimeType(mimeType: string): string;
|
|
1
3
|
/**
|
|
2
4
|
* Content type for a file whose sender may not have declared a useful one
|
|
3
5
|
* (mail attachments commonly arrive as application/octet-stream). A concrete
|
|
@@ -26,6 +26,14 @@ const EXTENSION_FALLBACK = {
|
|
|
26
26
|
csv: "text/csv",
|
|
27
27
|
txt: "text/plain",
|
|
28
28
|
};
|
|
29
|
+
const EXTENSION_BY_MIME_TYPE = new Map(Object.entries(EXTENSION_FALLBACK)
|
|
30
|
+
.reverse()
|
|
31
|
+
.map(([extension, mime]) => [mime, extension]));
|
|
32
|
+
/** A name derived from the MIME type alone, for places a personal file name must not appear. */
|
|
33
|
+
export function neutralFileNameForMimeType(mimeType) {
|
|
34
|
+
const extension = EXTENSION_BY_MIME_TYPE.get(normalizeMimeType(mimeType));
|
|
35
|
+
return extension === undefined ? "download" : `download.${extension}`;
|
|
36
|
+
}
|
|
29
37
|
function hasPrefix(bytes, prefix) {
|
|
30
38
|
return prefix.every((byte, i) => bytes[i] === byte);
|
|
31
39
|
}
|
|
@@ -10,6 +10,8 @@ export declare const PROJECTION_DETAIL_ENTITY = "__projection-detail__";
|
|
|
10
10
|
* drawn from the host record's display entity). */
|
|
11
11
|
export declare const WRITE_FORM_SECTION_ENTITY = "__write-form-section__";
|
|
12
12
|
export declare function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string;
|
|
13
|
+
export declare function handlerTitleKey(handlerQn: string): string;
|
|
14
|
+
export declare function handlerFieldLabelKey(handlerQn: string, fieldName: string): string;
|
|
13
15
|
export declare function booleanFacetOptionKeys(featureName: string, entityName: string, fieldName: string): readonly string[];
|
|
14
16
|
export declare function selectFacetOptionKey(featureName: string, entityName: string, fieldName: string, value: string): string;
|
|
15
17
|
export declare function screenTitleKey(screenId: string): string;
|
|
@@ -13,6 +13,12 @@ export const WRITE_FORM_SECTION_ENTITY = "__write-form-section__";
|
|
|
13
13
|
export function fieldLabelKey(featureName, entityName, fieldName) {
|
|
14
14
|
return `${featureName}:entity:${entityName}:field:${fieldName}`;
|
|
15
15
|
}
|
|
16
|
+
export function handlerTitleKey(handlerQn) {
|
|
17
|
+
return `${handlerQn}:title`;
|
|
18
|
+
}
|
|
19
|
+
export function handlerFieldLabelKey(handlerQn, fieldName) {
|
|
20
|
+
return `${handlerQn}:field:${fieldName}`;
|
|
21
|
+
}
|
|
16
22
|
export function booleanFacetOptionKeys(featureName, entityName, fieldName) {
|
|
17
23
|
const base = `${featureName}:entity:${entityName}:field:${fieldName}:option`;
|
|
18
24
|
return [`${base}:true`, `${base}:false`];
|
|
@@ -116,6 +122,8 @@ function pushDashboardPanelKeys(out, panel) {
|
|
|
116
122
|
function pushDashboardChartRangeKeys(out, panel) {
|
|
117
123
|
for (const option of panel.ranges?.options ?? [])
|
|
118
124
|
pushKey(out, option.label);
|
|
125
|
+
for (const kind of Object.values(panel.markerKinds ?? {}))
|
|
126
|
+
pushKey(out, kind.label);
|
|
119
127
|
}
|
|
120
128
|
function pushDashboardFilterKeys(out, filter) {
|
|
121
129
|
pushKey(out, filter.label);
|
package/dist/jobs/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { JobLogEntry, JobMeta, JobOutcomeMeta, JobRunner, JobRunnerOptions, JobSubjectValue, } from "./job-runner.js";
|
|
1
|
+
export type { JobLogEntry, JobMeta, JobOutcomeMeta, JobQueuedMeta, JobRunner, JobRunnerOptions, JobSubjectValue, } from "./job-runner.js";
|
|
2
2
|
export { createJobRunner, serializeJobSubject } from "./job-runner.js";
|
|
@@ -19,6 +19,14 @@ export type JobMeta = {
|
|
|
19
19
|
payload?: string | undefined;
|
|
20
20
|
attempt?: number | undefined;
|
|
21
21
|
priority?: number | undefined;
|
|
22
|
+
tenantVisibleRun?: {
|
|
23
|
+
readonly tenantId: string;
|
|
24
|
+
readonly subject: string | null;
|
|
25
|
+
} | undefined;
|
|
26
|
+
};
|
|
27
|
+
export type JobQueuedMeta = {
|
|
28
|
+
readonly tenantId: string;
|
|
29
|
+
readonly subject: string | null;
|
|
22
30
|
};
|
|
23
31
|
export type JobOutcomeMeta = {
|
|
24
32
|
readonly tenantId: string;
|
|
@@ -27,6 +35,9 @@ export type JobOutcomeMeta = {
|
|
|
27
35
|
readonly subject: string | null;
|
|
28
36
|
readonly messageKey: string | null;
|
|
29
37
|
} | undefined;
|
|
38
|
+
readonly tenantVisibleRun?: {
|
|
39
|
+
readonly subject: string | null;
|
|
40
|
+
} | undefined;
|
|
30
41
|
};
|
|
31
42
|
export type JobSubjectValue = string | number | boolean | null;
|
|
32
43
|
export declare function serializeJobSubject(subject: Readonly<Record<string, JobSubjectValue>>): string;
|
|
@@ -53,5 +64,7 @@ export type JobRunnerOptions = {
|
|
|
53
64
|
onJobStart?: (jobName: string, jobId: string, meta: JobMeta) => void;
|
|
54
65
|
onJobComplete?: (jobName: string, jobId: string, duration: number, logs: JobLogEntry[], outcome?: JobOutcomeMeta) => void;
|
|
55
66
|
onJobFailed?: (jobName: string, jobId: string, error: string, logs: JobLogEntry[], outcome?: JobOutcomeMeta) => void;
|
|
67
|
+
onJobQueued?: (jobName: string, jobId: string, meta: JobQueuedMeta) => void;
|
|
68
|
+
onJobDropped?: (jobName: string, jobId: string) => void;
|
|
56
69
|
};
|
|
57
70
|
export declare function createJobRunner(options: JobRunnerOptions): JobRunner;
|