@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +319 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +215 -0
- package/agents/examples/ledger-invoicing.md +236 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +250 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +418 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +75 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
package/dist/db/tenant-db.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { asEntityTableMeta, asRawClient, aggregateWhere as bunAggregateWhere, countWhere as bunCountWhere, deleteMany as bunDeleteMany, fetchOne as bunFetchOne, insertOne as bunInsertOne, selectMany as bunSelectMany, updateMany as bunUpdateMany, runInNewTransaction, } from "../db/query.js";
|
|
3
4
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
4
5
|
import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
5
6
|
import { emitDbQuery, fallbackEscapeHatchReporter, registerStandardMetrics, } from "../observability/index.js";
|
|
6
7
|
import { bindProjectionRegistry, projectionRegistryOf } from "./projection-registry-binding.js";
|
|
7
8
|
import { hasFieldProperty, tableNameOf } from "./table-introspection.js";
|
|
8
|
-
import { bindTenantDbRunner, tenantDbRunner } from "./tenant-db-runner.js";
|
|
9
|
+
import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, } from "./tenant-db-runner.js";
|
|
9
10
|
export { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
10
11
|
const declaredUnsafeRawRunners = new WeakMap();
|
|
11
12
|
// The CRUD executor writes through tenantDbRunner, not insertOne, so it asks the
|
|
@@ -220,6 +221,12 @@ export function createUncheckedSystemDb(db, dbOutsideTransaction, report = fallb
|
|
|
220
221
|
export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEscapeHatchReporter(db.tenantId)) {
|
|
221
222
|
return buildUncheckedSystemDb(db, dbOutsideTransaction, report, { kind: "source-tenant-db" });
|
|
222
223
|
}
|
|
224
|
+
// Narrows a (possibly "system"-mode) TenantDb to a "tenant"-mode handle for one tenant on the
|
|
225
|
+
// same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
|
|
226
|
+
// validators) without handing them a cross-tenant view.
|
|
227
|
+
export function scopeTenantDbToTenant(db, tenantId) {
|
|
228
|
+
return createTenantDb(tenantDbRunner(db), tenantId, "tenant");
|
|
229
|
+
}
|
|
223
230
|
// @cast-boundary tenant-db-row
|
|
224
231
|
export function castTenantRows(rows) {
|
|
225
232
|
return rows;
|
|
@@ -229,6 +236,19 @@ export { tableNameOf };
|
|
|
229
236
|
function hasGrant(decl) {
|
|
230
237
|
return decl !== undefined && decl.reason.trim().length > 0;
|
|
231
238
|
}
|
|
239
|
+
// Tenant ids a where.tenantId filter asks for; undefined for operator forms
|
|
240
|
+
// that cannot narrow to a tenant list (ne, like, ...).
|
|
241
|
+
export function requestedTenantIds(requested) {
|
|
242
|
+
if (Array.isArray(requested))
|
|
243
|
+
return requested;
|
|
244
|
+
if (!isPlainObject(requested))
|
|
245
|
+
return [requested];
|
|
246
|
+
if (Array.isArray(requested["in"]))
|
|
247
|
+
return requested["in"];
|
|
248
|
+
if ("eq" in requested)
|
|
249
|
+
return [requested["eq"]];
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
232
252
|
function isForeignTenantId(tenantIdValue) {
|
|
233
253
|
return tenantIdValue !== undefined && tenantIdValue !== SYSTEM_TENANT_ID;
|
|
234
254
|
}
|
|
@@ -354,7 +374,7 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
354
374
|
const allowed = [tenantId, SYSTEM_TENANT_ID];
|
|
355
375
|
const requested = where?.["tenantId"];
|
|
356
376
|
if (requested !== undefined) {
|
|
357
|
-
const requestedList =
|
|
377
|
+
const requestedList = requestedTenantIds(requested) ?? [];
|
|
358
378
|
const narrowed = requestedList.filter((t) => typeof t === "string" && allowed.includes(t));
|
|
359
379
|
return { ...where, tenantId: narrowed.length > 0 ? narrowed : allowed };
|
|
360
380
|
}
|
|
@@ -390,6 +410,10 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
390
410
|
message: `db.global(${tableNameOf(table)}): writes on executor-managed entity tables must go through the entity executor`,
|
|
391
411
|
});
|
|
392
412
|
}
|
|
413
|
+
function writesDenied(operation) {
|
|
414
|
+
const reason = grants?.writesDeniedReason;
|
|
415
|
+
return reason === undefined ? undefined : tenantDbWritesDeniedError(operation, reason);
|
|
416
|
+
}
|
|
393
417
|
function globalWriteReason() {
|
|
394
418
|
return grants?.globalWrites?.reason ?? "";
|
|
395
419
|
}
|
|
@@ -437,7 +461,8 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
437
461
|
return withDbSpan("select", table, async () => bunFetchOne(db, table, where));
|
|
438
462
|
},
|
|
439
463
|
insertOne(values) {
|
|
440
|
-
const denied =
|
|
464
|
+
const denied = writesDenied("ctx.db.global().insertOne") ??
|
|
465
|
+
missingEscapeHatch(table) ??
|
|
441
466
|
executorManagedWriteDenied(table) ??
|
|
442
467
|
foreignTenantOnGlobalWrite(table, values["tenantId"]) ??
|
|
443
468
|
personalDataDenied(table, Object.keys(values));
|
|
@@ -447,7 +472,8 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
447
472
|
return withDbSpan("insert", table, async () => bunInsertOne(db, table, values));
|
|
448
473
|
},
|
|
449
474
|
updateMany(set, where) {
|
|
450
|
-
const denied =
|
|
475
|
+
const denied = writesDenied("ctx.db.global().updateMany") ??
|
|
476
|
+
missingEscapeHatch(table) ??
|
|
451
477
|
executorManagedWriteDenied(table) ??
|
|
452
478
|
foreignTenantOnGlobalWrite(table, set["tenantId"]) ??
|
|
453
479
|
personalDataDenied(table, Object.keys(set));
|
|
@@ -460,7 +486,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
460
486
|
return withDbSpan("update", table, async () => bunUpdateMany(db, table, set, where));
|
|
461
487
|
},
|
|
462
488
|
deleteMany(where) {
|
|
463
|
-
const denied =
|
|
489
|
+
const denied = writesDenied("ctx.db.global().deleteMany") ??
|
|
490
|
+
missingEscapeHatch(table) ??
|
|
491
|
+
executorManagedWriteDenied(table);
|
|
464
492
|
if (denied)
|
|
465
493
|
return Promise.reject(denied);
|
|
466
494
|
if (!where || Object.keys(where).length === 0) {
|
|
@@ -509,6 +537,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
509
537
|
return withDbSpan("select", table, async () => bunAggregateWhere(db, table, spec, filter));
|
|
510
538
|
},
|
|
511
539
|
insertOne(table, values) {
|
|
540
|
+
const writeDenied = writesDenied("ctx.db.insertOne");
|
|
541
|
+
if (writeDenied)
|
|
542
|
+
return Promise.reject(writeDenied);
|
|
512
543
|
if (mode === "tenant" &&
|
|
513
544
|
hasTenantColumn(table) &&
|
|
514
545
|
asEntityTableMeta(table)?.tenancy === "global") {
|
|
@@ -524,6 +555,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
524
555
|
return withDbSpan("insert", table, async () => bunInsertOne(db, table, data));
|
|
525
556
|
},
|
|
526
557
|
updateMany(table, set, where) {
|
|
558
|
+
const writeDenied = writesDenied("ctx.db.updateMany");
|
|
559
|
+
if (writeDenied)
|
|
560
|
+
return Promise.reject(writeDenied);
|
|
527
561
|
if (!where || Object.keys(where).length === 0) {
|
|
528
562
|
return Promise.reject(new Error("TenantDb.updateMany without where would mass-update all tenant rows. Pass at least one where condition."));
|
|
529
563
|
}
|
|
@@ -537,6 +571,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
537
571
|
return withDbSpan("update", table, async () => bunUpdateMany(db, table, set, filter));
|
|
538
572
|
},
|
|
539
573
|
deleteMany(table, where) {
|
|
574
|
+
const writeDenied = writesDenied("ctx.db.deleteMany");
|
|
575
|
+
if (writeDenied)
|
|
576
|
+
return Promise.reject(writeDenied);
|
|
540
577
|
if (!where || Object.keys(where).length === 0) {
|
|
541
578
|
return Promise.reject(new Error("TenantDb.deleteMany without where would mass-delete all tenant rows. Pass at least one where condition."));
|
|
542
579
|
}
|
|
@@ -573,10 +610,18 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
573
610
|
return runInNewTransaction(db, (tx) => fn(createTenantDb(tx, tenantId, mode, tracer, meter, signal, txGrants)));
|
|
574
611
|
});
|
|
575
612
|
bindTenantDbRunner(tenantDb, db);
|
|
613
|
+
if (grants?.writesDeniedReason !== undefined) {
|
|
614
|
+
denyTenantDbWrites(tenantDb, grants.writesDeniedReason);
|
|
615
|
+
}
|
|
576
616
|
if (personalDataGate)
|
|
577
617
|
personalDataGates.set(tenantDb, personalDataGate);
|
|
578
618
|
if (projectionRegistry)
|
|
579
619
|
bindProjectionRegistry(tenantDb, projectionRegistry);
|
|
580
620
|
return tenantDb;
|
|
581
621
|
}
|
|
622
|
+
// Tenant-mode TenantDb whose writes reach the registry's custom projections. The positional
|
|
623
|
+
// undefineds of createTenantDb would hide a moved grants parameter from the type checker.
|
|
624
|
+
export function createProjectingTenantDb(db, tenantId, projectionRegistry) {
|
|
625
|
+
return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, projectionRegistry ? { projectionRegistry } : undefined);
|
|
626
|
+
}
|
|
582
627
|
export { asRawClient };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { FileContext } from "@cosmicdrift/kumiko-types/file-handle-types";
|
|
1
2
|
export type StoredFileStore = {
|
|
2
3
|
list(prefix: string): Promise<readonly string[]>;
|
|
3
4
|
delete(key: string): Promise<void>;
|
|
4
5
|
};
|
|
5
6
|
export declare function listDerivativeKeys(originalKey: string, store: Pick<StoredFileStore, "list">, errorContext: string): Promise<readonly string[]>;
|
|
6
7
|
export declare function deleteStoredFileAndDerivatives(originalKey: string, store: StoredFileStore, errorContext: string): Promise<readonly string[]>;
|
|
8
|
+
export declare function deleteFileRefBytes(files: FileContext, row: {
|
|
9
|
+
readonly storageKey: string;
|
|
10
|
+
}, errorContext: string): Promise<readonly string[]>;
|
|
@@ -68,3 +68,12 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
|
|
|
68
68
|
}
|
|
69
69
|
return failedKeys;
|
|
70
70
|
}
|
|
71
|
+
// deleteStoredFileAndDerivatives through ctx.files for a key read from a tenant-filtered fileRef
|
|
72
|
+
// row. After a tenant handover the key keeps the previous tenant's prefix, which ref() and list()
|
|
73
|
+
// reject; every key the helper deletes is the original or one of its derivatives.
|
|
74
|
+
export function deleteFileRefBytes(files, row, errorContext) {
|
|
75
|
+
return deleteStoredFileAndDerivatives(row.storageKey, {
|
|
76
|
+
list: () => files.listFromFileRef(row),
|
|
77
|
+
delete: (key) => files.refFromFileRef({ storageKey: key }).delete(),
|
|
78
|
+
}, errorContext);
|
|
79
|
+
}
|
|
@@ -181,7 +181,7 @@ export function createDerivativesContext(deps) {
|
|
|
181
181
|
// Resolves qr dataTokens to concrete values BEFORE the spec is hashed
|
|
182
182
|
// — see resolveOverlaySpec.
|
|
183
183
|
const resolvedSpec = await resolveOverlaySpec(spec, row, deps.registry, deps.tenantId);
|
|
184
|
-
const src = deps.files.
|
|
184
|
+
const src = deps.files.refFromFileRef(row);
|
|
185
185
|
// ponytail: derived key keeps the source extension regardless of the
|
|
186
186
|
// spec's format — widen deriveKey if a storage backend ever routes on
|
|
187
187
|
// extension.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { StoredFileStore } from "./delete-stored-file.js";
|
|
2
|
-
export { deleteStoredFileAndDerivatives, listDerivativeKeys } from "./delete-stored-file.js";
|
|
2
|
+
export { deleteFileRefBytes, deleteStoredFileAndDerivatives, listDerivativeKeys, } from "./delete-stored-file.js";
|
|
3
3
|
export type { DerivativesContextDeps } from "./derivatives-context.js";
|
|
4
4
|
export { createDerivativesContext, resolveRenderer } from "./derivatives-context.js";
|
|
5
5
|
export { resolveFieldVariant } from "./field-variants.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { deleteStoredFileAndDerivatives, listDerivativeKeys } from "./delete-stored-file.js";
|
|
1
|
+
export { deleteFileRefBytes, deleteStoredFileAndDerivatives, listDerivativeKeys, } from "./delete-stored-file.js";
|
|
2
2
|
export { createDerivativesContext, resolveRenderer } from "./derivatives-context.js";
|
|
3
3
|
export { resolveFieldVariant } from "./field-variants.js";
|
|
4
4
|
export { canonicalJson, derivativeListPrefix, isDerivativeKeyOf, parseDerivativeKey, specHash, VARIANT_NAME_PATTERN, variantSuffix, } from "./variant-key.js";
|
|
@@ -83,7 +83,7 @@ function validatePersonalDataSupportedForKind(feature, kind, handlerName, access
|
|
|
83
83
|
? 'only access.personalData: "public-intake" applies to query handlers, which write ' +
|
|
84
84
|
"personal data through nested writes of an anonymous request."
|
|
85
85
|
: "it only applies to write handlers, whose input is checked for personal-data fields.";
|
|
86
|
-
throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares ${property} — ${onlyAllowed}
|
|
86
|
+
throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares ${property} — ${onlyAllowed} Remove the personalData declaration from this handler.`);
|
|
87
87
|
}
|
|
88
88
|
function validatePersonalDataValue(feature, kind, handlerName, access) {
|
|
89
89
|
const declared = declaredPersonalData(access);
|
|
@@ -107,7 +107,7 @@ function validatePersonalDataValue(feature, kind, handlerName, access) {
|
|
|
107
107
|
throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares ` +
|
|
108
108
|
'access.personalData: "public-intake" but its roles do not include ' +
|
|
109
109
|
`"${ANONYMOUS_ROLE}" — personalData: "public-intake" only applies to handlers ` +
|
|
110
|
-
"that allow anonymous callers.");
|
|
110
|
+
"that allow anonymous callers. Add the anonymous role to access.roles or remove personalData.");
|
|
111
111
|
}
|
|
112
112
|
function handlerInputKeys(feature, handlerName, handler) {
|
|
113
113
|
try {
|
|
@@ -31,7 +31,7 @@ export function validateApiExposureMatching(feature, allExposedApis, featureMap)
|
|
|
31
31
|
// Should always be true if allExposedApis was built from `features`,
|
|
32
32
|
// aber defensiv für unklare Constructor-Pfade.
|
|
33
33
|
if (!featureMap.has(providerFeature)) {
|
|
34
|
-
throw new Error(`[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map
|
|
34
|
+
throw new Error(`[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map. Register the provider feature in the app's feature list.`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -40,7 +40,7 @@ export function validateExtensionUsages(feature, extensionProviders) {
|
|
|
40
40
|
for (const usage of feature.extensionUsages) {
|
|
41
41
|
const providerFeature = extensionProviders.get(usage.extensionName);
|
|
42
42
|
if (!providerFeature) {
|
|
43
|
-
throw new Error(`Feature "${feature.name}" uses extension "${usage.extensionName}" on entity "${usage.entityName}" but no feature defines that extension
|
|
43
|
+
throw new Error(`Feature "${feature.name}" uses extension "${usage.extensionName}" on entity "${usage.entityName}" but no feature defines that extension. Register the feature that defines it or fix the extension name.`);
|
|
44
44
|
}
|
|
45
45
|
// self-extension is legitimate: requires(self) would be circular.
|
|
46
46
|
if (providerFeature === feature.name) {
|
|
@@ -48,7 +48,7 @@ export function validateExtensionUsages(feature, extensionProviders) {
|
|
|
48
48
|
}
|
|
49
49
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
50
50
|
if (!allDeps.includes(providerFeature)) {
|
|
51
|
-
throw new Error(`Feature "${feature.name}" uses extension "${usage.extensionName}" but missing requires("${providerFeature}")
|
|
51
|
+
throw new Error(`Feature "${feature.name}" uses extension "${usage.extensionName}" but missing requires("${providerFeature}"). Add r.requires("${providerFeature}").`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -13,7 +13,7 @@ export function validateFeatureBootChecks(features) {
|
|
|
13
13
|
}
|
|
14
14
|
catch (err) {
|
|
15
15
|
const message = err instanceof Error ? err.message : String(err);
|
|
16
|
-
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}
|
|
16
|
+
throw new Error(`[Feature ${feature.name}] r.bootCheck failed: ${message}. Fix the condition this check reports.`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -38,15 +38,15 @@ export function validateConfigKeyBounds(feature) {
|
|
|
38
38
|
}
|
|
39
39
|
const { min, max } = bounds;
|
|
40
40
|
if (min !== undefined && max !== undefined && min > max) {
|
|
41
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has bounds.min (${min}) > bounds.max (${max})
|
|
41
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has bounds.min (${min}) > bounds.max (${max}). Set bounds.min to a value at or below bounds.max.`);
|
|
42
42
|
}
|
|
43
43
|
if (keyDef.default !== undefined) {
|
|
44
44
|
const defaultNum = keyDef.default; // @cast-boundary engine-payload
|
|
45
45
|
if (min !== undefined && defaultNum < min) {
|
|
46
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" default (${defaultNum}) is below bounds.min (${min})
|
|
46
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" default (${defaultNum}) is below bounds.min (${min}). Raise the default or lower bounds.min.`);
|
|
47
47
|
}
|
|
48
48
|
if (max !== undefined && defaultNum > max) {
|
|
49
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" default (${defaultNum}) is above bounds.max (${max})
|
|
49
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" default (${defaultNum}) is above bounds.max (${max}). Lower the default or raise bounds.max.`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -61,7 +61,7 @@ export function validateConfigKeyComputed(feature) {
|
|
|
61
61
|
// cascade doesn't know which one to prefer on write. Rejecting at boot
|
|
62
62
|
// is cheaper than surprising behaviour at runtime.
|
|
63
63
|
if (isEncryptedAtRest(keyDef)) {
|
|
64
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has both encrypted/backing="secrets" and a computed resolver — these are mutually exclusive paradigms
|
|
64
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has both encrypted/backing="secrets" and a computed resolver — these are mutually exclusive paradigms. Use either encrypted/backing="secrets" or a computed resolver on one key.`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -92,7 +92,7 @@ export function validateConfigKeyAllowPerRequest(feature) {
|
|
|
92
92
|
// anyway, but declaring allowPerRequest on a text key is a
|
|
93
93
|
// misconfiguration that should fail loudly at boot.
|
|
94
94
|
if (keyDef.type === "text") {
|
|
95
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has allowPerRequest=true but type="text" — text keys are permanently ineligible for per-request overrides (XSS/injection risk)
|
|
95
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has allowPerRequest=true but type="text" — text keys are permanently ineligible for per-request overrides (XSS/injection risk). Remove allowPerRequest or change the key type.`);
|
|
96
96
|
}
|
|
97
97
|
// encrypted + per-request would expose a cipher-text interpretation
|
|
98
98
|
// to query-strings. The secret-value shouldn't be transported this
|
|
@@ -107,16 +107,16 @@ export function validateConfigKeyOptionsQuery(feature) {
|
|
|
107
107
|
for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
|
|
108
108
|
const prefix = `[Feature ${feature.name}] Config key "${keyName}"`;
|
|
109
109
|
if ("conditionalOptions" in keyDef) {
|
|
110
|
-
throw new Error(`${prefix} declares conditionalOptions — conditionalOptions is only supported on entity select fields (enforced by the entity write path)
|
|
110
|
+
throw new Error(`${prefix} declares conditionalOptions — conditionalOptions is only supported on entity select fields (enforced by the entity write path). Remove conditionalOptions from the config key.`);
|
|
111
111
|
}
|
|
112
112
|
if (keyDef.optionsQuery === undefined) {
|
|
113
113
|
if (keyDef.optionsQueryPayload !== undefined) {
|
|
114
|
-
throw new Error(`${prefix} has optionsQueryPayload without optionsQuery
|
|
114
|
+
throw new Error(`${prefix} has optionsQueryPayload without optionsQuery. Set optionsQuery or remove optionsQueryPayload.`);
|
|
115
115
|
}
|
|
116
116
|
continue;
|
|
117
117
|
}
|
|
118
118
|
if (keyDef.optionsQuery.length === 0) {
|
|
119
|
-
throw new Error(`${prefix} has an empty optionsQuery
|
|
119
|
+
throw new Error(`${prefix} has an empty optionsQuery. Set it to a query name or remove it.`);
|
|
120
120
|
}
|
|
121
121
|
if (keyDef.type !== "select") {
|
|
122
122
|
throw new Error(`${prefix} has optionsQuery but type="${keyDef.type}" — optionsQuery is only valid on select keys`);
|
|
@@ -134,7 +134,7 @@ function validateOptionsQueryKeyRefs(feature, keyName, keyDef) {
|
|
|
134
134
|
const prefix = `[Feature ${feature.name}] Config key "${keyName}"`;
|
|
135
135
|
for (const ref of optionsQueryFieldRefs(keyDef.optionsQueryPayload)) {
|
|
136
136
|
if (ref === keyName) {
|
|
137
|
-
throw new Error(`${prefix} optionsQueryPayload references itself ({ field: "${ref}" })
|
|
137
|
+
throw new Error(`${prefix} optionsQueryPayload references itself ({ field: "${ref}" }). Reference a different config key.`);
|
|
138
138
|
}
|
|
139
139
|
if (feature.configKeys[ref] === undefined) {
|
|
140
140
|
throw new Error(`${prefix} optionsQueryPayload references unknown config key "${ref}" of feature "${feature.name}"`);
|
|
@@ -166,12 +166,12 @@ export function validateConfigKeyPiiEncrypted(feature) {
|
|
|
166
166
|
if (!keyDef.piiEncrypted)
|
|
167
167
|
continue;
|
|
168
168
|
if (keyDef.type !== "text") {
|
|
169
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but type="${keyDef.type}" — piiEncrypted only applies to text keys
|
|
169
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but type="${keyDef.type}" — piiEncrypted only applies to text keys. Use type="text" or remove piiEncrypted.`);
|
|
170
170
|
}
|
|
171
171
|
// system scope has no subject (no tenant, no user) for the subject-KMS
|
|
172
172
|
// to encrypt under.
|
|
173
173
|
if (keyDef.scope === "system") {
|
|
174
|
-
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but scope="system" — system-scope has no subject (tenant/user) for the subject-KMS to encrypt under
|
|
174
|
+
throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has piiEncrypted=true but scope="system" — system-scope has no subject (tenant/user) for the subject-KMS to encrypt under. Use scope="tenant" or "user", or remove piiEncrypted.`);
|
|
175
175
|
}
|
|
176
176
|
// Mutually exclusive with the shared-cipher path: a config value is
|
|
177
177
|
// either subject-keyed (piiEncrypted) or master-key-keyed (encrypted/
|
|
@@ -190,7 +190,7 @@ export function validateConfigReads(features, allConfigKeys) {
|
|
|
190
190
|
for (const feature of features) {
|
|
191
191
|
for (const key of feature.configReads) {
|
|
192
192
|
if (!allConfigKeys.has(key)) {
|
|
193
|
-
throw new Error(`Feature "${feature.name}" reads config "${key}" but no feature defines that key
|
|
193
|
+
throw new Error(`Feature "${feature.name}" reads config "${key}" but no feature defines that key. Declare the key via r.config(...) in a feature, or fix the key name.`);
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -201,7 +201,7 @@ export function validateCircularDeps(featureName, featureMap) {
|
|
|
201
201
|
const stack = new Set();
|
|
202
202
|
function visit(name, path) {
|
|
203
203
|
if (stack.has(name)) {
|
|
204
|
-
throw new Error(`Circular dependency: ${[...path, name].join(" → ")}
|
|
204
|
+
throw new Error(`Circular dependency: ${[...path, name].join(" → ")}. Break the cycle by removing one requires() link.`);
|
|
205
205
|
}
|
|
206
206
|
// skip: node already visited in DFS traversal
|
|
207
207
|
if (visited.has(name))
|
|
@@ -22,7 +22,7 @@ export function collectDetailForScreens(features, featureMap) {
|
|
|
22
22
|
screenQnByEntity.set(detailFor, qualified);
|
|
23
23
|
if (findEntityFeature(detailFor, featureMap) === undefined) {
|
|
24
24
|
throw new Error(`[detailFor] Screen "${qualified}" declares detailFor: "${detailFor}", ` +
|
|
25
|
-
`but no feature registers an entity with that name.`);
|
|
25
|
+
`but no feature registers an entity with that name. Set detailFor to a registered entity name or remove it.`);
|
|
26
26
|
}
|
|
27
27
|
result.set(detailFor, { featureName: feature.name, screen });
|
|
28
28
|
}
|
|
@@ -7,6 +7,7 @@ export declare const PII_USER_REFERENCE_NAME_HINTS: ReadonlySet<string>;
|
|
|
7
7
|
export declare function validateExtensionPreSaveWiring(features: readonly FeatureDefinition[]): void;
|
|
8
8
|
export declare function validateHandlerAccess(feature: FeatureDefinition): void;
|
|
9
9
|
export declare function validateAdditionalRateLimits(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
10
|
+
export declare function validatePerCommandTransaction(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
10
11
|
export declare function validateTenantlessAnonymous(featureName: string, handlerName: string, handler: FeatureDefinition["writeHandlers"][string]): void;
|
|
11
12
|
export declare function validateRateLimitDisabledReason(featureName: string, kind: "write" | "query" | "stream", handlerName: string, rateLimit: FeatureDefinition["writeHandlers"][string]["rateLimit"]): void;
|
|
12
13
|
export declare function validateNoAllRoleInHandlerAccess(featureName: string, kind: "write" | "query" | "stream", handlerName: string, access: NonNullable<FeatureDefinition["writeHandlers"][string]["access"]>): void;
|