@cosmicdrift/kumiko-framework 0.367.0 → 0.369.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/AGENTS.md +1 -7
- package/README.md +1 -1
- package/agents/examples/access-control.md +2 -2
- package/agents/examples/anonymous-access-multitenant.md +2 -2
- package/agents/examples/anonymous-access.md +3 -3
- package/agents/examples/apex-landing.md +1 -1
- package/agents/examples/apex-surface-auth.md +6 -7
- package/agents/examples/api-worker-split.md +7 -8
- package/agents/examples/auth-claims.md +2 -1
- package/agents/examples/auth-foundation-providers.md +3 -3
- package/agents/examples/auth-mfa.md +3 -4
- package/agents/examples/basic-entity.md +3 -4
- package/agents/examples/chat-channels.md +4 -4
- package/agents/examples/compliance-profiles-demo.md +2 -3
- package/agents/examples/cross-feature-events.md +4 -4
- package/agents/examples/crypto-shredding-hr.md +14 -9
- package/agents/examples/currencies-global.md +3 -4
- package/agents/examples/currencies-per-tenant.md +3 -4
- package/agents/examples/custom-fields-basic.md +2 -3
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/dashboard-stats.md +2 -3
- package/agents/examples/delivery-notifications.md +5 -6
- package/agents/examples/e2e-generator.md +1 -1
- package/agents/examples/embedded-entity-form.md +2 -3
- package/agents/examples/embedded.md +2 -3
- package/agents/examples/encrypted-tenant-config.md +5 -6
- package/agents/examples/error-contract.md +3 -4
- package/agents/examples/event-sourcing.md +19 -23
- package/agents/examples/feature-toggles.md +4 -4
- package/agents/examples/field-access.md +3 -4
- package/agents/examples/files-post-processing.md +12 -13
- package/agents/examples/golden-path.md +208 -0
- package/agents/examples/idempotency.md +2 -2
- package/agents/examples/inbound-mail-triage.md +7 -7
- package/agents/examples/lane-routing.md +3 -3
- package/agents/examples/ledger-banking.md +2 -3
- package/agents/examples/ledger-invoicing.md +2 -3
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +3 -3
- package/agents/examples/managed-config.md +3 -4
- package/agents/examples/managed-pages.md +2 -2
- package/agents/examples/notes-history-basic.md +4 -5
- package/agents/examples/ownership.md +3 -5
- package/agents/examples/pdf-audit-export.md +2 -3
- package/agents/examples/pipeline-basics.md +2 -2
- package/agents/examples/public-share-token.md +3 -3
- package/agents/examples/rate-limiting.md +2 -1
- package/agents/examples/realtime-sse.md +2 -3
- package/agents/examples/record-detail-layout.md +1 -1
- package/agents/examples/reference-data.md +3 -3
- package/agents/examples/relations.md +2 -2
- package/agents/examples/screens-nav.md +1 -1
- package/agents/examples/search.md +2 -2
- package/agents/examples/secrets-demo.md +8 -8
- package/agents/examples/security-baseline.md +2 -2
- package/agents/examples/session-revocation.md +7 -8
- package/agents/examples/state-machine.md +181 -165
- package/agents/examples/tags-basic.md +2 -3
- package/agents/examples/tenant-isolation.md +2 -3
- package/agents/examples/tenant-settings.md +2 -2
- package/agents/examples/tier-admin.md +3 -4
- package/agents/examples/timezones.md +3 -3
- package/agents/examples/user-data-rights.md +12 -8
- package/agents/examples/user-profile.md +3 -3
- package/agents/examples/webhook-step.md +2 -3
- package/agents/examples/wizard-form.md +4 -5
- package/agents/examples/workflow-engine.md +7 -3
- package/agents/examples/writeform-section.md +1 -1
- package/agents/examples.md +1 -0
- package/agents/features.md +4 -4
- package/dist/consumer-cli.js +2 -3
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/owner-transition-rekey.js +2 -0
- package/dist/crypto/pg-kms-adapter.js +1 -1
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +30 -15
- package/dist/db/bun-db-deprecated.d.ts +10 -0
- package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
- package/dist/db/config-seed.js +1 -1
- package/dist/db/connection.d.ts +23 -5
- package/dist/db/connection.js +10 -5
- package/dist/db/dialect.js +1 -1
- package/dist/db/entity-field-encryption.d.ts +5 -0
- package/dist/db/entity-field-encryption.js +29 -0
- package/dist/db/entity-table-meta.js +6 -5
- package/dist/db/event-store-executor-read.js +26 -5
- package/dist/db/index.d.ts +4 -4
- package/dist/db/index.js +1 -1
- package/dist/db/migrate-runner.js +1 -1
- package/dist/db/queries/backfill-pii.js +16 -9
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/event-store.d.ts +2 -0
- package/dist/db/queries/event-store.js +26 -0
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/query.d.ts +105 -1
- package/dist/db/query.js +1450 -3
- package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
- package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
- package/dist/db/tenant-db.d.ts +11 -0
- package/dist/db/tenant-db.js +51 -12
- package/dist/derivatives/derivatives-context.d.ts +1 -1
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/gdpr-storage.js +3 -3
- package/dist/engine/boot-validator/pii-retention.js +3 -3
- package/dist/engine/boot-validator/record-owned.js +3 -3
- package/dist/engine/boot-validator/screens.js +37 -1
- package/dist/engine/bridge-stub.d.ts +4 -0
- package/dist/engine/bridge-stub.js +62 -0
- package/dist/engine/build-config-feature-schema.js +197 -65
- package/dist/engine/event-helpers.d.ts +2 -1
- package/dist/engine/event-helpers.js +1 -0
- package/dist/engine/extension-selector-plugins.d.ts +3 -0
- package/dist/engine/extension-selector-plugins.js +14 -0
- package/dist/engine/feature-config-events-jobs.d.ts +2 -0
- package/dist/engine/feature-config-events-jobs.js +3 -2
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/es-ops/operations-schema.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/archive.js +1 -1
- package/dist/event-store/errors.d.ts +2 -1
- package/dist/event-store/errors.js +6 -2
- package/dist/event-store/event-store.js +17 -9
- package/dist/event-store/events-schema.js +1 -1
- package/dist/event-store/rebuild-dead-letter.js +3 -3
- package/dist/event-store/snapshot.js +1 -1
- package/dist/event-store/upcaster-dead-letter.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +1 -1
- package/dist/files/file-routes.js +1 -1
- package/dist/i18n/settings-hub-keys.js +1 -2
- package/dist/internal/stack-support.d.ts +12 -0
- package/dist/internal/stack-support.js +12 -0
- package/dist/internal/testing.d.ts +4 -0
- package/dist/internal/testing.js +5 -0
- package/dist/jobs/job-runner.js +23 -11
- package/dist/migrations/pending-rebuilds.js +1 -1
- package/dist/migrations/pii-event-backfill.js +2 -2
- package/dist/pipeline/dispatch-shared.js +43 -23
- package/dist/pipeline/dispatch-write.js +6 -2
- package/dist/pipeline/dispatcher-utils.js +6 -2
- package/dist/pipeline/event-consumer-state.js +1 -1
- package/dist/pipeline/projection-rebuild.d.ts +1 -0
- package/dist/pipeline/projection-rebuild.js +1 -1
- package/dist/pipeline/projection-state.js +1 -1
- package/dist/pipeline/system-hooks.js +1 -1
- package/dist/schema-cli.js +3 -3
- package/dist/search/purge-subject.js +20 -10
- package/dist/seeding/index.d.ts +1 -0
- package/dist/seeding/index.js +1 -0
- package/dist/stack/index.d.ts +10 -9
- package/dist/stack/index.js +11 -19
- package/dist/testing/e2e-generator.d.ts +5 -49
- package/dist/testing/e2e-generator.js +5 -382
- package/dist/testing/handler-context.d.ts +5 -4
- package/dist/testing/handler-context.js +5 -69
- package/dist/testing/index.d.ts +10 -26
- package/dist/testing/index.js +10 -34
- package/dist/testing/real-providers.d.ts +5 -3
- package/dist/testing/real-providers.js +5 -21
- package/dist/upgrade-cli.js +6 -4
- package/package.json +37 -9
- package/src/changes.json +64 -0
- package/src/scripts/codemod/README.md +43 -0
- package/src/scripts/codemod/codemod-support.ts +276 -0
- package/src/scripts/codemod/db-factory.ts +810 -0
- package/src/scripts/codemod/feature-factory.ts +429 -0
- package/src/scripts/codemod/s3-imports.ts +274 -0
- package/src/scripts/codemod/tenant-db-options.ts +454 -0
- package/src/scripts/codemod/testing-move-targets.ts +161 -0
- package/src/scripts/codemod/testing-move.ts +520 -0
- package/dist/bun-db/connection.d.ts +0 -17
- package/dist/bun-db/connection.js +0 -40
- package/dist/bun-db/index.d.ts +0 -4
- package/dist/bun-db/query.d.ts +0 -106
- package/dist/bun-db/query.js +0 -1457
- package/dist/db/api.d.ts +0 -19
- package/dist/db/api.js +0 -23
- package/dist/db/bun-provider.d.ts +0 -2
- package/dist/db/bun-provider.js +0 -28
- package/dist/db/postgres-provider.d.ts +0 -2
- package/dist/db/postgres-provider.js +0 -21
- package/dist/db/query-api.d.ts +0 -2
- package/dist/db/query-api.js +0 -6
- package/dist/stack/db.d.ts +0 -22
- package/dist/stack/db.js +0 -67
- package/dist/stack/drain-event-consumers.d.ts +0 -4
- package/dist/stack/drain-event-consumers.js +0 -51
- package/dist/stack/drain-jobs.d.ts +0 -22
- package/dist/stack/drain-jobs.js +0 -141
- package/dist/stack/event-collector.d.ts +0 -9
- package/dist/stack/event-collector.js +0 -12
- package/dist/stack/push-entity-projection-tables.d.ts +0 -24
- package/dist/stack/push-entity-projection-tables.js +0 -74
- package/dist/stack/redis.d.ts +0 -17
- package/dist/stack/redis.js +0 -52
- package/dist/stack/request-helper.d.ts +0 -35
- package/dist/stack/request-helper.js +0 -144
- package/dist/stack/test-stack.d.ts +0 -192
- package/dist/stack/test-stack.js +0 -429
- package/dist/testing/access-assertions.d.ts +0 -2
- package/dist/testing/access-assertions.js +0 -13
- package/dist/testing/assertions.d.ts +0 -10
- package/dist/testing/assertions.js +0 -22
- package/dist/testing/boot-validator-fixture.d.ts +0 -3
- package/dist/testing/boot-validator-fixture.js +0 -101
- package/dist/testing/closed-connection-error.d.ts +0 -7
- package/dist/testing/closed-connection-error.js +0 -46
- package/dist/testing/db-cleanup.d.ts +0 -9
- package/dist/testing/db-cleanup.js +0 -31
- package/dist/testing/expect-error.d.ts +0 -2
- package/dist/testing/expect-error.js +0 -22
- package/dist/testing/file-provider-contract.d.ts +0 -2
- package/dist/testing/file-provider-contract.js +0 -102
- package/dist/testing/http-cookies.d.ts +0 -7
- package/dist/testing/http-cookies.js +0 -47
- package/dist/testing/late-bound.d.ts +0 -9
- package/dist/testing/late-bound.js +0 -31
- package/dist/testing/manual-clock.d.ts +0 -14
- package/dist/testing/manual-clock.js +0 -12
- package/dist/testing/multipart-helper.d.ts +0 -22
- package/dist/testing/multipart-helper.js +0 -77
- package/dist/testing/mutable-master-key-provider.d.ts +0 -7
- package/dist/testing/mutable-master-key-provider.js +0 -35
- package/dist/testing/observability-recorder.d.ts +0 -9
- package/dist/testing/observability-recorder.js +0 -29
- package/dist/testing/seed.d.ts +0 -9
- package/dist/testing/seed.js +0 -21
- package/dist/testing/shared-entities.d.ts +0 -286
- package/dist/testing/shared-entities.js +0 -56
- package/dist/testing/utils.d.ts +0 -1
- package/dist/testing/utils.js +0 -1
- package/dist/testing/wait-for.d.ts +0 -20
- package/dist/testing/wait-for.js +0 -47
- package/dist/testing/without-ambient-temporal.d.ts +0 -5
- package/dist/testing/without-ambient-temporal.js +0 -17
- package/dist/testing/zip-fixture.d.ts +0 -1
- package/dist/testing/zip-fixture.js +0 -33
- /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
- /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { EntityDefinition } from "../engine/types/index.js";
|
|
2
|
+
import type { DbConnection } from "./connection.js";
|
|
3
|
+
import type { ColumnMeta, EntityTableMeta, IndexMeta } from "./entity-table-meta.js";
|
|
4
4
|
/**
|
|
5
5
|
* Bypass: creates an entity-table directly without going through the
|
|
6
6
|
* full registry. Reserved for framework-internal meta-tables and
|
|
7
7
|
* test setup — apps declare data via `r.entity(...)`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function unsafeCreateEntityTable(db: DbConnection, entity:
|
|
10
|
-
export declare function unsafeEnsureEntityTable(db: DbConnection, entity:
|
|
9
|
+
export declare function unsafeCreateEntityTable(db: DbConnection, entity: EntityDefinition, entityName?: string): Promise<void>;
|
|
10
|
+
export declare function unsafeEnsureEntityTable(db: DbConnection, entity: EntityDefinition, entityName?: string): Promise<boolean>;
|
|
11
11
|
export declare function tableToMeta(table: unknown): EntityTableMeta;
|
|
12
12
|
/** Applies ALTER TABLE ADD COLUMN for exactly the given columns. */
|
|
13
13
|
export declare function addMissingColumns(db: DbConnection, tableName: string, columns: readonly ColumnMeta[]): Promise<void>;
|
|
@@ -22,13 +22,3 @@ export declare function addMissingIndexes(db: DbConnection, tableName: string, i
|
|
|
22
22
|
* entities go through `kumiko schema apply` (committed SQL files).
|
|
23
23
|
*/
|
|
24
24
|
export declare function unsafePushTables(db: DbConnection, tables: Record<string, unknown>, prevTables?: Record<string, unknown>): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* Wipes event store + framework-state + the given feature read-models in
|
|
27
|
-
* one TRUNCATE, then re-registers the event-consumer state rows. Used in
|
|
28
|
-
* test beforeEach-hooks to return the stack to a clean slate without
|
|
29
|
-
* rebuilding it.
|
|
30
|
-
*/
|
|
31
|
-
export declare function resetEventStore(stack: {
|
|
32
|
-
db: unknown;
|
|
33
|
-
eventDispatcher?: EventDispatcher;
|
|
34
|
-
}, extraTables?: readonly (unknown | string)[]): Promise<void>;
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { pgTypeToSqlType } from "
|
|
2
|
-
import { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { asEntityTableMeta, schemaTableName } from "../db/table-introspection.js";
|
|
8
|
-
function tableNameOf(table) {
|
|
9
|
-
if (typeof table !== "object" || table === null) {
|
|
10
|
-
throw new Error("table-helpers: table is not a SchemaTable object");
|
|
11
|
-
}
|
|
12
|
-
const symbolName = schemaTableName(table);
|
|
13
|
-
if (symbolName !== undefined)
|
|
14
|
-
return symbolName;
|
|
15
|
-
if ("tableName" in table && typeof table.tableName === "string")
|
|
16
|
-
return table.tableName;
|
|
17
|
-
throw new Error("table-helpers: table has no name");
|
|
18
|
-
}
|
|
1
|
+
import { pgTypeToSqlType } from "./dialect.js";
|
|
2
|
+
import { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement } from "./queries/ddl.js";
|
|
3
|
+
import { renderTableDdl } from "./render-ddl.js";
|
|
4
|
+
import { tableExists } from "./schema-inspection.js";
|
|
5
|
+
import { buildEntityTable, toTableName } from "./table-builder.js";
|
|
6
|
+
import { asEntityTableMeta } from "./table-introspection.js";
|
|
19
7
|
/**
|
|
20
8
|
* Bypass: creates an entity-table directly without going through the
|
|
21
9
|
* full registry. Reserved for framework-internal meta-tables and
|
|
@@ -96,31 +84,3 @@ export async function unsafePushTables(db, tables, prevTables) {
|
|
|
96
84
|
function renderColumnType(col) {
|
|
97
85
|
return pgTypeToSqlType(col.pgType);
|
|
98
86
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Wipes event store + framework-state + the given feature read-models in
|
|
101
|
-
* one TRUNCATE, then re-registers the event-consumer state rows. Used in
|
|
102
|
-
* test beforeEach-hooks to return the stack to a clean slate without
|
|
103
|
-
* rebuilding it.
|
|
104
|
-
*/
|
|
105
|
-
export async function resetEventStore(stack, extraTables = []) {
|
|
106
|
-
const frameworkTables = [
|
|
107
|
-
"kumiko_events",
|
|
108
|
-
"kumiko_event_consumers",
|
|
109
|
-
"kumiko_archived_streams",
|
|
110
|
-
"kumiko_snapshots",
|
|
111
|
-
"kumiko_projections",
|
|
112
|
-
];
|
|
113
|
-
const extraNames = extraTables.map((t) => (typeof t === "string" ? t : tableNameOf(t)));
|
|
114
|
-
const truncateAndReregister = async () => {
|
|
115
|
-
await truncateTablesRestartIdentity(stack.db, [...frameworkTables, ...extraNames]);
|
|
116
|
-
await stack.eventDispatcher?.ensureRegistered();
|
|
117
|
-
};
|
|
118
|
-
// The dispatcher's own timer/LISTEN must not start a pass between the
|
|
119
|
-
// TRUNCATE and the consumer re-registration, so they are paused for the whole reset.
|
|
120
|
-
if (stack.eventDispatcher) {
|
|
121
|
-
await stack.eventDispatcher.withBackgroundPassesPaused(truncateAndReregister);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
await truncateAndReregister();
|
|
125
|
-
}
|
|
126
|
-
}
|
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -37,6 +37,17 @@ export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, rea
|
|
|
37
37
|
}): TenantDb;
|
|
38
38
|
export declare function reportConventionCrossTenantUse(acknowledged: TenantDb, target?: EscapeHatchTarget): void;
|
|
39
39
|
export declare function runInOwnTransaction<T>(tenantDb: TenantDb, fn: (txDb: TenantDb) => Promise<T>): Promise<T>;
|
|
40
|
+
export type CreateTenantDbOptions = {
|
|
41
|
+
readonly db: DbRunner;
|
|
42
|
+
readonly tenantId: TenantId;
|
|
43
|
+
readonly mode?: TenantDbMode;
|
|
44
|
+
readonly tracer?: Tracer;
|
|
45
|
+
readonly meter?: Meter;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
readonly grants?: TenantDbGrants;
|
|
48
|
+
};
|
|
49
|
+
export declare function createTenantDb(options: CreateTenantDbOptions): TenantDb;
|
|
50
|
+
/** @deprecated Positional form, use createTenantDb({ db, tenantId, ... }). Run `bun kumiko-upgrade --apply` to rewrite call sites. Removed in the first release after 2026-10-29. */
|
|
40
51
|
export declare function createTenantDb(db: DbRunner, tenantId: TenantId, mode?: TenantDbMode, tracer?: Tracer, meter?: Meter, signal?: AbortSignal, grants?: TenantDbGrants): TenantDb;
|
|
41
52
|
export declare function createProjectingTenantDb(db: DbRunner, tenantId: TenantId, projectionRegistry: Registry | undefined): TenantDb;
|
|
42
53
|
export { asRawClient };
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -225,7 +225,7 @@ export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEs
|
|
|
225
225
|
// same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
|
|
226
226
|
// validators) without handing them a cross-tenant view.
|
|
227
227
|
export function scopeTenantDbToTenant(db, tenantId) {
|
|
228
|
-
return createTenantDb(tenantDbRunner(db), tenantId, "tenant");
|
|
228
|
+
return createTenantDb({ db: tenantDbRunner(db), tenantId, mode: "tenant" });
|
|
229
229
|
}
|
|
230
230
|
// @cast-boundary tenant-db-row
|
|
231
231
|
export function castTenantRows(rows) {
|
|
@@ -311,7 +311,26 @@ export async function runInOwnTransaction(tenantDb, fn) {
|
|
|
311
311
|
}
|
|
312
312
|
return rebind(fn);
|
|
313
313
|
}
|
|
314
|
-
|
|
314
|
+
// Runners (postgres-js Sql/tx handles) are functions and RawDbClient mocks carry no own
|
|
315
|
+
// `tenantId`/`db`, so a runner is never mistaken for the options object.
|
|
316
|
+
function isCreateTenantDbOptions(value) {
|
|
317
|
+
return (typeof value === "object" &&
|
|
318
|
+
value !== null &&
|
|
319
|
+
Object.hasOwn(value, "tenantId") &&
|
|
320
|
+
Object.hasOwn(value, "db"));
|
|
321
|
+
}
|
|
322
|
+
export function createTenantDb(dbOrOptions, tenantId, mode, tracer, meter, signal, grants) {
|
|
323
|
+
if (isCreateTenantDbOptions(dbOrOptions))
|
|
324
|
+
return buildTenantDb(dbOrOptions);
|
|
325
|
+
return buildTenantDb({ db: dbOrOptions, tenantId, mode, tracer, meter, signal, grants });
|
|
326
|
+
}
|
|
327
|
+
// Untyped JS callers or an `as` cast can still hand in undefined; a scoped handle
|
|
328
|
+
// without a tenant would bind `tenant_id = NULL` instead of failing loudly.
|
|
329
|
+
function buildTenantDb(options) {
|
|
330
|
+
const { db, tenantId, mode = "tenant", tracer, meter, signal, grants } = options;
|
|
331
|
+
if (tenantId === undefined) {
|
|
332
|
+
throw new InternalError({ message: "createTenantDb: tenantId is required" });
|
|
333
|
+
}
|
|
315
334
|
if (meter)
|
|
316
335
|
registerStandardMetrics(meter);
|
|
317
336
|
const report = grants?.report ?? fallbackEscapeHatchReporter(tenantId);
|
|
@@ -585,16 +604,32 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
585
604
|
},
|
|
586
605
|
};
|
|
587
606
|
declaredUnsafeRawRunners.set(tenantDb, grantedUnsafeRawRunner);
|
|
588
|
-
unsafeRawRebinders.set(tenantDb, (grant) => createTenantDb(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
607
|
+
unsafeRawRebinders.set(tenantDb, (grant) => createTenantDb({
|
|
608
|
+
db,
|
|
609
|
+
tenantId,
|
|
610
|
+
mode,
|
|
611
|
+
tracer,
|
|
612
|
+
meter,
|
|
613
|
+
signal,
|
|
614
|
+
grants: {
|
|
615
|
+
...rebindGrants,
|
|
616
|
+
unsafeRaw: grant,
|
|
617
|
+
// db.global() writes are write-handler-only: a rebind (a hook's own grant) must not inherit them.
|
|
618
|
+
globalWrites: undefined,
|
|
619
|
+
},
|
|
593
620
|
}));
|
|
594
621
|
crossTenantRebinders.set(tenantDb, (reason, deferReport) => {
|
|
595
622
|
if (!deferReport)
|
|
596
623
|
report("acknowledge-cross-tenant", reason);
|
|
597
|
-
const acknowledged = createTenantDb(
|
|
624
|
+
const acknowledged = createTenantDb({
|
|
625
|
+
db,
|
|
626
|
+
tenantId,
|
|
627
|
+
mode: "system",
|
|
628
|
+
tracer,
|
|
629
|
+
meter,
|
|
630
|
+
signal,
|
|
631
|
+
grants: rebindGrants,
|
|
632
|
+
});
|
|
598
633
|
crossTenantUseReporters.set(acknowledged, (target) => report("acknowledge-cross-tenant", reason, target));
|
|
599
634
|
return acknowledged;
|
|
600
635
|
});
|
|
@@ -607,7 +642,7 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
607
642
|
}
|
|
608
643
|
// Carries a runner-bound gate (grants.personalDataGate unset, resolved via the fallback above) forward explicitly, since the fresh tx-handle isn't itself registered in runnerPersonalDataGates.
|
|
609
644
|
const txGrants = personalDataGate ? { ...rebindGrants, personalDataGate } : rebindGrants;
|
|
610
|
-
return runInNewTransaction(db, (tx) => fn(createTenantDb(tx, tenantId, mode, tracer, meter, signal, txGrants)));
|
|
645
|
+
return runInNewTransaction(db, (tx) => fn(createTenantDb({ db: tx, tenantId, mode, tracer, meter, signal, grants: txGrants })));
|
|
611
646
|
});
|
|
612
647
|
bindTenantDbRunner(tenantDb, db);
|
|
613
648
|
if (grants?.writesDeniedReason !== undefined) {
|
|
@@ -619,9 +654,13 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
619
654
|
bindProjectionRegistry(tenantDb, projectionRegistry);
|
|
620
655
|
return tenantDb;
|
|
621
656
|
}
|
|
622
|
-
// Tenant-mode TenantDb whose writes reach the registry's custom projections.
|
|
623
|
-
// undefineds of createTenantDb would hide a moved grants parameter from the type checker.
|
|
657
|
+
// Tenant-mode TenantDb whose writes reach the registry's custom projections.
|
|
624
658
|
export function createProjectingTenantDb(db, tenantId, projectionRegistry) {
|
|
625
|
-
return createTenantDb(
|
|
659
|
+
return createTenantDb({
|
|
660
|
+
db,
|
|
661
|
+
tenantId,
|
|
662
|
+
mode: "tenant",
|
|
663
|
+
grants: projectionRegistry ? { projectionRegistry } : undefined,
|
|
664
|
+
});
|
|
626
665
|
}
|
|
627
666
|
export { asRawClient };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DerivativeRendererPlugin, DerivativesContext } from "@cosmicdrift/kumiko-types/derivatives-types";
|
|
2
|
-
import { type AnyDb } from "../
|
|
2
|
+
import { type AnyDb } from "../db/query.js";
|
|
3
3
|
import type { Registry, TenantId } from "../engine/types/index.js";
|
|
4
4
|
import type { FileContext } from "../files/file-handle.js";
|
|
5
5
|
export type DerivativesContextDeps = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchOne } from "../
|
|
1
|
+
import { fetchOne } from "../db/query.js";
|
|
2
2
|
import { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_RENDERER, } from "../engine/extension-names.js";
|
|
3
3
|
import { InternalError, NotFoundError } from "../errors/index.js";
|
|
4
4
|
import { fileRefsTable } from "../files/file-ref-table.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Providers whose bytes do not survive a process restart. Only "inmemory"
|
|
2
2
|
// today; extend if another ephemeral bundled provider lands.
|
|
3
3
|
const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
|
|
4
|
-
// Env vars
|
|
4
|
+
// Env vars createFileProviderS3Feature({ fromEnv: true }) reads (see file-provider-s3/lib/env-helper).
|
|
5
5
|
// Missing any → createS3ProviderFromEnv throws on the first file op.
|
|
6
6
|
const S3_ENV_REQUIRED = ["S3_BUCKET", "S3_REGION", "S3_ACCESS_KEY", "S3_SECRET_KEY"];
|
|
7
7
|
// Cross-feature GDPR storage guard (V1). Catches the failure class we shipped
|
|
@@ -31,7 +31,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
31
31
|
const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
|
|
32
32
|
if (persistent.length === 0) {
|
|
33
33
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
34
|
-
console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount
|
|
34
|
+
console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
|
|
35
35
|
// skip: ephemeral store already warned; the s3-env env-var check below is moot
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
@@ -42,7 +42,7 @@ export function validateGdprStoragePersistence(features) {
|
|
|
42
42
|
const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
|
|
43
43
|
if (missing.length > 0) {
|
|
44
44
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
45
|
-
console.warn(`[kumiko:boot]
|
|
45
|
+
console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -204,11 +204,11 @@ export function validatePiiAndRetention(feature) {
|
|
|
204
204
|
throw new Error(`[Feature ${feature.name}] personal.of "${ownerName}" on entity "${entityName}" must be a reference or text (userId) field, got type "${ownerField.type}"`);
|
|
205
205
|
}
|
|
206
206
|
const { whenAbsent } = annot.userOwned;
|
|
207
|
-
if (whenAbsent !== undefined && whenAbsent !== "tenant") {
|
|
208
|
-
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant".`);
|
|
207
|
+
if (whenAbsent !== undefined && whenAbsent !== "tenant" && whenAbsent !== "self") {
|
|
208
|
+
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" or "self" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant" (tenant key) or "self" (the row's own record key).`);
|
|
209
209
|
}
|
|
210
210
|
if (ownerField.required !== true && whenAbsent === undefined) {
|
|
211
|
-
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
|
|
211
|
+
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, whenAbsent: "self" to encrypt them under the row's own record key (shreddable per record), or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
|
|
212
212
|
}
|
|
213
213
|
// Soft-Warning wenn das reference-target nicht offensichtlich user
|
|
214
214
|
// ist — custom subject-entities (HR-Mitarbeiter, Patient) sind
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A recordOwned field is encrypted under `record:<entity>:<id>` (kms-adapter-types.ts)
|
|
1
|
+
// A recordOwned field (or one with whenAbsent: "self") is encrypted under `record:<entity>:<id>` (kms-adapter-types.ts)
|
|
2
2
|
// and forgetSubject's subjectIdSchema (crypto/kms-adapter.ts) requires that id
|
|
3
3
|
// to be a UUID — an entity with idType: "serial" would encrypt the field but
|
|
4
4
|
// could never satisfy a forget-subject request for it.
|
|
@@ -8,9 +8,9 @@ export function validateRecordOwnedSubjects(feature) {
|
|
|
8
8
|
continue;
|
|
9
9
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
10
10
|
const annot = field; // @cast-boundary schema-walk
|
|
11
|
-
if (annot.recordOwned !== true)
|
|
11
|
+
if (annot.recordOwned !== true && annot.userOwned?.whenAbsent !== "self")
|
|
12
12
|
continue;
|
|
13
|
-
throw new Error(`[Feature ${feature.name}] entity "${entityName}" field "${fieldName}" is recordOwned but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field would be encrypted but could never be shredded. Set idType: "uuid" on entity "${entityName}", or give field "${fieldName}" a different personal stance (personal: "self" / "tenant" / { of: "<field>" }).`);
|
|
13
|
+
throw new Error(`[Feature ${feature.name}] entity "${entityName}" field "${fieldName}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field would be encrypted but could never be shredded. Set idType: "uuid" on entity "${entityName}", or give field "${fieldName}" a different personal stance (personal: "self" / "tenant" / { of: "<field>" }).`);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -7,6 +7,7 @@ import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
|
7
7
|
import { NO_WIDGET_FIELD_TYPES, optionsQueryFieldRefs, SELECT_OPTION_TONES, } from "@cosmicdrift/kumiko-types/fields";
|
|
8
8
|
import { NAV_ICON_KEYS } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
9
9
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
10
|
+
import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
|
|
10
11
|
import { rowMetaFieldNames } from "../../db/table-builder.js";
|
|
11
12
|
import { REFERENCE_LOOKUP_SOURCES } from "../../ui-types/list-row-meta.js";
|
|
12
13
|
import { isEncryptedAtRest } from "../config-helpers.js";
|
|
@@ -230,7 +231,7 @@ function validateWizardLayout(featureName, screenId, screenType, layout, feature
|
|
|
230
231
|
if (!featureMap.has("form-draft")) {
|
|
231
232
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) sets draft: true but the ` +
|
|
232
233
|
`bundled feature "form-draft" is not mounted — every resume would silently lose its ` +
|
|
233
|
-
`values. Add
|
|
234
|
+
`values. Add createFormDraftFeature() from @cosmicdrift/kumiko-bundled-features to the app's ` +
|
|
234
235
|
`feature list.`);
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -550,6 +551,25 @@ function validateOptionsQueryFieldRefs(where, fieldName, payload, siblingFieldNa
|
|
|
550
551
|
function isWriteOnlyField(fdef) {
|
|
551
552
|
return fdef?.writeOnly === true; // @cast-boundary schema-walk
|
|
552
553
|
}
|
|
554
|
+
// A secret on a configEdit form keeps its value out of the client like an encrypted
|
|
555
|
+
// config key does, and the secrets store only exists per tenant.
|
|
556
|
+
function validateConfigEditSecretField(featureName, screen, fieldName, secretQn, featureMap) {
|
|
557
|
+
const where = `[Feature ${featureName}] Screen "${screen.id}" (configEdit) field "${fieldName}"`;
|
|
558
|
+
if (screen.configKeys[fieldName] !== undefined) {
|
|
559
|
+
throw new Error(`${where} is mapped in both configKeys and secretKeys — keep one.`);
|
|
560
|
+
}
|
|
561
|
+
if (screen.scope !== "tenant") {
|
|
562
|
+
throw new Error(`${where} maps secret "${secretQn}" on a ${screen.scope}-scope screen — secrets are tenant-scoped.`);
|
|
563
|
+
}
|
|
564
|
+
const fdef = screen.fields[fieldName];
|
|
565
|
+
if (fdef?.type !== "text" || !isWriteOnlyField(fdef)) {
|
|
566
|
+
throw new Error(`${where} maps secret "${secretQn}" but is no writeOnly text field — a stored secret never goes back to the client.`);
|
|
567
|
+
}
|
|
568
|
+
const declared = [...featureMap.values()].some((feature) => Object.values(feature.secretKeys ?? {}).some((def) => def.qualifiedName === secretQn));
|
|
569
|
+
if (!declared) {
|
|
570
|
+
throw new Error(`${where} → secret "${secretQn}" is not declared via r.secret() in any feature.`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
553
573
|
// Inline forms have no stored value to hide or keep: writeOnly there would
|
|
554
574
|
// render a "set / keep" affordance with nothing behind it.
|
|
555
575
|
function rejectWriteOnlyFormField(featureName, screenId, context, fieldName, fdef) {
|
|
@@ -1476,9 +1496,19 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1476
1496
|
}
|
|
1477
1497
|
}
|
|
1478
1498
|
validateWizardLayout(feature.name, screenId, "configEdit", screen.layout, featureMap);
|
|
1499
|
+
for (const fname of Object.keys(screen.secretKeys ?? {})) {
|
|
1500
|
+
if (!fieldNames.has(fname)) {
|
|
1501
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) secretKeys names unknown field "${fname}".`);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1479
1504
|
// configKeys: jeder fieldName muss einen Mapping-Eintrag haben,
|
|
1480
1505
|
// jeder qualifizierte Key muss in der Registry existieren.
|
|
1481
1506
|
for (const fname of fieldNames) {
|
|
1507
|
+
const secretQn = screen.secretKeys?.[fname];
|
|
1508
|
+
if (secretQn !== undefined) {
|
|
1509
|
+
validateConfigEditSecretField(feature.name, screen, fname, secretQn, featureMap);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1482
1512
|
const qualified = screen.configKeys[fname];
|
|
1483
1513
|
if (qualified === undefined) {
|
|
1484
1514
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" has ` +
|
|
@@ -1641,6 +1671,12 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1641
1671
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references unknown ` +
|
|
1642
1672
|
`field "${filterField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
|
|
1643
1673
|
}
|
|
1674
|
+
if (!isListFilterAllowed(entityDef, filterField, screen.filter)) {
|
|
1675
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
|
|
1676
|
+
`"${filterField}" which is encrypted, personal or secret, so the list handler rejects ` +
|
|
1677
|
+
`this filter at runtime. Only op "eq" works on a personal field with find: "exact" or ` +
|
|
1678
|
+
`"fuzzy"; otherwise drop the filter or filter on another field.`);
|
|
1679
|
+
}
|
|
1644
1680
|
const fieldDef = entityDef.fields[filterField];
|
|
1645
1681
|
if (fieldDef !== undefined && !isFieldFilterable(fieldDef)) {
|
|
1646
1682
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HandlerContext, SessionUser } from "./types/index.js";
|
|
2
|
+
export declare function bridgeStub(opts?: {
|
|
3
|
+
readonly user?: SessionUser;
|
|
4
|
+
}): Pick<HandlerContext, "query" | "queryAs" | "write" | "writeAs" | "appendEvent" | "unsafeAppendEvent" | "tryAppendEvent" | "fetchForWriting" | "loadAggregate" | "archiveStream" | "restoreStream" | "isStreamArchived" | "snapshotAggregate" | "loadAggregateWithSnapshot" | "queryProjection" | "resolveAuthClaims" | "resolveActiveMembership" | "queryAsMember" | "hasFeature" | "metrics" | "metricsFor" | "tracer" | "tz" | "locale" | "user">;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DEFAULT_LOCALE } from "../i18n/request-locale.js";
|
|
2
|
+
import { createNoopMetricsHandle, getFallbackTracer } from "../observability/index.js";
|
|
3
|
+
import { createTzContext } from "../time/index.js";
|
|
4
|
+
import { createAnonymousUser } from "./system-user.js";
|
|
5
|
+
// Test/service helper: cross-feature bridge methods that throw on use.
|
|
6
|
+
//
|
|
7
|
+
// Production code always receives a full HandlerContext from the Dispatcher's
|
|
8
|
+
// buildHandlerContext (with real query/write closures). Some internal services
|
|
9
|
+
// and tests construct a mini-context manually (typically just `{ db, registry }`)
|
|
10
|
+
// to invoke a single handler. Those call sites don't use ctx.query/write —
|
|
11
|
+
// the stubs make the TypeScript shape match while still failing loudly if
|
|
12
|
+
// anything downstream accidentally reaches for them.
|
|
13
|
+
//
|
|
14
|
+
// Use: `{ db, registry, ...bridgeStub() }`
|
|
15
|
+
const notAvailable = (what) => async () => {
|
|
16
|
+
throw new Error(`ctx.${what} not available in this context — use the dispatcher, not a stubbed handler context`);
|
|
17
|
+
};
|
|
18
|
+
// Noop observability — hand back the shared fallback tracer so ctx.tracer has
|
|
19
|
+
// a valid Tracer shape. No allocations per call.
|
|
20
|
+
const noopTracer = getFallbackTracer();
|
|
21
|
+
export function bridgeStub(opts) {
|
|
22
|
+
// ctx.user is a convenience alias for event.user (see HandlerContext
|
|
23
|
+
// docs). Caller code expects the field; bridgeStub hands back a stub with
|
|
24
|
+
// anonymous default values when no user is passed explicitly. Test code
|
|
25
|
+
// that cares about identity passes its own SessionUser here and gets it
|
|
26
|
+
// back on ctx.
|
|
27
|
+
const stubUser = opts?.user ??
|
|
28
|
+
createAnonymousUser("00000000-0000-0000-0000-000000000000"); // @cast-boundary engine-bridge
|
|
29
|
+
return {
|
|
30
|
+
user: stubUser,
|
|
31
|
+
query: notAvailable("query"), // @cast-boundary engine-bridge
|
|
32
|
+
queryAs: notAvailable("queryAs"),
|
|
33
|
+
write: notAvailable("write"),
|
|
34
|
+
writeAs: notAvailable("writeAs"),
|
|
35
|
+
appendEvent: notAvailable("appendEvent"),
|
|
36
|
+
unsafeAppendEvent: notAvailable("unsafeAppendEvent"),
|
|
37
|
+
tryAppendEvent: notAvailable("tryAppendEvent"),
|
|
38
|
+
fetchForWriting: notAvailable("fetchForWriting"),
|
|
39
|
+
loadAggregate: notAvailable("loadAggregate"),
|
|
40
|
+
archiveStream: notAvailable("archiveStream"),
|
|
41
|
+
restoreStream: notAvailable("restoreStream"),
|
|
42
|
+
isStreamArchived: notAvailable("isStreamArchived"),
|
|
43
|
+
snapshotAggregate: notAvailable("snapshotAggregate"),
|
|
44
|
+
loadAggregateWithSnapshot: notAvailable("loadAggregateWithSnapshot"),
|
|
45
|
+
queryProjection: notAvailable("queryProjection"),
|
|
46
|
+
resolveAuthClaims: notAvailable("resolveAuthClaims"),
|
|
47
|
+
resolveActiveMembership: notAvailable("resolveActiveMembership"),
|
|
48
|
+
queryAsMember: notAvailable("queryAsMember"),
|
|
49
|
+
// Stub defaults to always-enabled — matches the dispatcher's behaviour
|
|
50
|
+
// when no effectiveFeatures resolver is wired (tests without toggles).
|
|
51
|
+
hasFeature: async () => true,
|
|
52
|
+
metrics: createNoopMetricsHandle(),
|
|
53
|
+
metricsFor: () => createNoopMetricsHandle(),
|
|
54
|
+
tracer: noopTracer,
|
|
55
|
+
// Real TzContext, not notAvailable — test code uses ctx.tz routinely,
|
|
56
|
+
// it isn't a "bridge" concept. Defaults to UTC.
|
|
57
|
+
tz: createTzContext(),
|
|
58
|
+
// Same reasoning as tz above — ctx.locale is always-present, not a
|
|
59
|
+
// bridge method. Defaults to DEFAULT_LOCALE.
|
|
60
|
+
locale: DEFAULT_LOCALE,
|
|
61
|
+
};
|
|
62
|
+
}
|