@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -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 +323 -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 +314 -0
- package/agents/examples/custom-handlers.md +397 -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 +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -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 +214 -0
- package/agents/examples/ledger-invoicing.md +235 -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 +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -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 +259 -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 +410 -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 +74 -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 +14 -5
- package/dist/api/auth-routes.js +5 -3
- 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/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- 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/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- 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 +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -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 +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- 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 +9 -9
- 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/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -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 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- 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 +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- 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 +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- 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/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
+
// table() spreads column handles onto the table object as enumerable props, so
|
|
4
|
+
// an entity field named `source`/`columns`/`tableName`/… shadows the matching
|
|
5
|
+
// meta key. The symbol-keyed slots are the only collision-safe source; plain
|
|
6
|
+
// metas (deriveEntityTableMeta / defineUnmanagedTable) carry no handle spread
|
|
7
|
+
// and are unshadowable by their structural shape.
|
|
8
|
+
export function isEntityTableMeta(value) {
|
|
9
|
+
return (typeof value === "object" &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
"tableName" in value &&
|
|
12
|
+
typeof value.tableName === "string" &&
|
|
13
|
+
"columns" in value &&
|
|
14
|
+
Array.isArray(value.columns) &&
|
|
15
|
+
"indexes" in value &&
|
|
16
|
+
Array.isArray(value.indexes) &&
|
|
17
|
+
"source" in value &&
|
|
18
|
+
(value.source === "managed" || value.source === "unmanaged"));
|
|
19
|
+
}
|
|
20
|
+
export function asEntityTableMeta(table) {
|
|
21
|
+
if (typeof table !== "object" || table === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (KUMIKO_META_SYMBOL in table && isEntityTableMeta(table[KUMIKO_META_SYMBOL])) {
|
|
24
|
+
return table[KUMIKO_META_SYMBOL];
|
|
25
|
+
}
|
|
26
|
+
return isEntityTableMeta(table) ? table : undefined;
|
|
27
|
+
}
|
|
28
|
+
export function requireEntityTableMeta(table, tableName) {
|
|
29
|
+
const meta = asEntityTableMeta(table);
|
|
30
|
+
if (!meta) {
|
|
31
|
+
throw new Error(`${tableName}: table carries no EntityTableMeta — built via table()?`);
|
|
32
|
+
}
|
|
33
|
+
return meta;
|
|
34
|
+
}
|
|
35
|
+
/** Physical table name from the unshadowable name symbol; undefined when absent. */
|
|
36
|
+
export function schemaTableName(table) {
|
|
37
|
+
if (typeof table !== "object" || table === null || !(KUMIKO_NAME_SYMBOL in table)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const name = table[KUMIKO_NAME_SYMBOL];
|
|
41
|
+
return typeof name === "string" ? name : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Name symbol first, then the canonical meta; "<unknown>" when neither resolves. */
|
|
44
|
+
export function tableNameOf(table) {
|
|
45
|
+
return schemaTableName(table) ?? asEntityTableMeta(table)?.tableName ?? "<unknown>";
|
|
46
|
+
}
|
|
47
|
+
/** Column-handle map stamped by table(); undefined for tables without one. */
|
|
48
|
+
export function tableColumnsOf(table) {
|
|
49
|
+
if (typeof table !== "object" || table === null || !(KUMIKO_COLUMNS_SYMBOL in table)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const columns = table[KUMIKO_COLUMNS_SYMBOL];
|
|
53
|
+
return isPlainObject(columns) ? columns : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** SQL name carried by the enumerable column handle `table[field]`; undefined when the field has none. */
|
|
56
|
+
export function columnHandleName(table, field) {
|
|
57
|
+
if (!isPlainObject(table))
|
|
58
|
+
return undefined;
|
|
59
|
+
const handle = table[field];
|
|
60
|
+
if (!isPlainObject(handle))
|
|
61
|
+
return undefined;
|
|
62
|
+
const name = handle["name"];
|
|
63
|
+
return typeof name === "string" ? name : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** True when `table[field]` is defined, i.e. the table exposes a column handle (or plain value) for the field. */
|
|
66
|
+
export function hasFieldProperty(table, field) {
|
|
67
|
+
return isPlainObject(table) && table[field] !== undefined;
|
|
68
|
+
}
|
|
69
|
+
/** SQL column name of a JS field via the column-handle map; undefined when not mapped. */
|
|
70
|
+
export function columnSqlNameOf(table, field) {
|
|
71
|
+
const handle = tableColumnsOf(table)?.[field];
|
|
72
|
+
if (!isPlainObject(handle))
|
|
73
|
+
return undefined;
|
|
74
|
+
const name = handle["name"];
|
|
75
|
+
return typeof name === "string" ? name : undefined;
|
|
76
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { TenantDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
2
|
+
import { InternalError } from "../errors/index.js";
|
|
2
3
|
import type { DbRunner } from "./connection.js";
|
|
3
4
|
export declare function bindTenantDbRunner(tenantDb: TenantDb, runner: DbRunner): void;
|
|
4
5
|
export declare function tenantDbRunner(tenantDb: TenantDb): DbRunner;
|
|
6
|
+
export declare function denyTenantDbWrites(tenantDb: TenantDb, reason: string): void;
|
|
7
|
+
export declare function tenantDbWritesDeniedError(operation: string, reason: string): InternalError;
|
|
8
|
+
export declare function tenantDbWriteRunner(tenantDb: TenantDb, operation: string): DbRunner;
|
|
@@ -15,3 +15,20 @@ export function tenantDbRunner(tenantDb) {
|
|
|
15
15
|
}
|
|
16
16
|
return runner;
|
|
17
17
|
}
|
|
18
|
+
const writesDeniedReasons = new WeakMap();
|
|
19
|
+
export function denyTenantDbWrites(tenantDb, reason) {
|
|
20
|
+
writesDeniedReasons.set(tenantDb, reason);
|
|
21
|
+
}
|
|
22
|
+
export function tenantDbWritesDeniedError(operation, reason) {
|
|
23
|
+
return new InternalError({
|
|
24
|
+
message: `${operation}: writes are not available ${reason}; run the write through ctx.write so it gets its own transaction`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// For framework code that writes through the raw runner (executor verbs, event appends, projection steps):
|
|
28
|
+
// the TenantDb's own write methods are denied via TenantDbGrants, this closes the runner-based door.
|
|
29
|
+
export function tenantDbWriteRunner(tenantDb, operation) {
|
|
30
|
+
const reason = writesDeniedReasons.get(tenantDb);
|
|
31
|
+
if (reason !== undefined)
|
|
32
|
+
throw tenantDbWritesDeniedError(operation, reason);
|
|
33
|
+
return tenantDbRunner(tenantDb);
|
|
34
|
+
}
|
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityTableMeta } from "@cosmicdrift/kumiko-types/entity-table-meta-types";
|
|
2
2
|
import type { EscapeHatchDeclaration, EscapeHatchReporter, EscapeHatchTarget } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
|
-
import {
|
|
3
|
+
import type { SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
4
4
|
import { type TenantDb, type TenantDbMode, type UncheckedSystemDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
5
5
|
import { asRawClient } from "../db/query.js";
|
|
6
6
|
import type { EntityDefinition } from "../engine/types/fields.js";
|
|
@@ -8,6 +8,7 @@ import { type TenantId } from "../engine/types/identifiers.js";
|
|
|
8
8
|
import type { Registry } from "../engine/types/index.js";
|
|
9
9
|
import { type Meter, type Tracer } from "../observability/index.js";
|
|
10
10
|
import type { DbRunner } from "./connection.js";
|
|
11
|
+
import { tableNameOf } from "./table-introspection.js";
|
|
11
12
|
type Table = SchemaTable;
|
|
12
13
|
export { SYSTEM_SCOPE_CHECK_BRAND, type TenantDb, type TenantDbMode, type UncheckedSystemDb, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
13
14
|
export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
|
|
@@ -15,8 +16,9 @@ export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSys
|
|
|
15
16
|
export declare function withSystemDbUnsafeRawGrant(systemDb: UncheckedSystemDb, grant: EscapeHatchDeclaration | undefined, deniedCallerLabel: string): UncheckedSystemDb;
|
|
16
17
|
export declare function createUncheckedSystemDb(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
|
|
17
18
|
export declare function createSystemDbView(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
|
|
19
|
+
export declare function scopeTenantDbToTenant(db: TenantDb, tenantId: TenantId): TenantDb;
|
|
18
20
|
export declare function castTenantRows<T>(rows: readonly Record<string, unknown>[]): readonly T[];
|
|
19
|
-
export
|
|
21
|
+
export { tableNameOf };
|
|
20
22
|
export declare function hasTenantColumn(table: Table | EntityTableMeta): boolean;
|
|
21
23
|
export type TenantDbGrants = {
|
|
22
24
|
readonly globalWrites?: EscapeHatchDeclaration;
|
|
@@ -25,6 +27,7 @@ export type TenantDbGrants = {
|
|
|
25
27
|
readonly memberReadOnly?: boolean;
|
|
26
28
|
readonly personalDataGate?: PersonalDataGate;
|
|
27
29
|
readonly projectionRegistry?: Registry;
|
|
30
|
+
readonly writesDeniedReason?: string;
|
|
28
31
|
};
|
|
29
32
|
export type PersonalDataGate = (tableName: string, keys: readonly string[], entity?: EntityDefinition) => void;
|
|
30
33
|
export declare function withUnsafeRawGrant(tenantDb: TenantDb, grant: EscapeHatchDeclaration | undefined): TenantDb;
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { KUMIKO_NAME_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
1
|
import { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
3
2
|
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";
|
|
4
3
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
5
4
|
import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
6
5
|
import { emitDbQuery, fallbackEscapeHatchReporter, registerStandardMetrics, } from "../observability/index.js";
|
|
7
6
|
import { bindProjectionRegistry, projectionRegistryOf } from "./projection-registry-binding.js";
|
|
8
|
-
import {
|
|
7
|
+
import { hasFieldProperty, tableNameOf } from "./table-introspection.js";
|
|
8
|
+
import { bindTenantDbRunner, denyTenantDbWrites, tenantDbRunner, tenantDbWritesDeniedError, } from "./tenant-db-runner.js";
|
|
9
9
|
export { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
10
10
|
const declaredUnsafeRawRunners = new WeakMap();
|
|
11
11
|
// The CRUD executor writes through tenantDbRunner, not insertOne, so it asks the
|
|
@@ -53,7 +53,6 @@ function boundRunner(runner, gate, projectionRegistry) {
|
|
|
53
53
|
}
|
|
54
54
|
return value.bind(target);
|
|
55
55
|
},
|
|
56
|
-
// @cast-boundary proxy-erasure — Proxy<object> re-tags as the wrapped DbRunner shape.
|
|
57
56
|
});
|
|
58
57
|
if (gate)
|
|
59
58
|
runnerPersonalDataGates.set(proxy, gate);
|
|
@@ -221,16 +220,17 @@ export function createUncheckedSystemDb(db, dbOutsideTransaction, report = fallb
|
|
|
221
220
|
export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEscapeHatchReporter(db.tenantId)) {
|
|
222
221
|
return buildUncheckedSystemDb(db, dbOutsideTransaction, report, { kind: "source-tenant-db" });
|
|
223
222
|
}
|
|
223
|
+
// Narrows a (possibly "system"-mode) TenantDb to a "tenant"-mode handle for one tenant on the
|
|
224
|
+
// same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
|
|
225
|
+
// validators) without handing them a cross-tenant view.
|
|
226
|
+
export function scopeTenantDbToTenant(db, tenantId) {
|
|
227
|
+
return createTenantDb(tenantDbRunner(db), tenantId, "tenant");
|
|
228
|
+
}
|
|
224
229
|
// @cast-boundary tenant-db-row
|
|
225
230
|
export function castTenantRows(rows) {
|
|
226
231
|
return rows;
|
|
227
232
|
}
|
|
228
|
-
export
|
|
229
|
-
const sym = table[KUMIKO_NAME_SYMBOL];
|
|
230
|
-
if (typeof sym === "string")
|
|
231
|
-
return sym;
|
|
232
|
-
return asEntityTableMeta(table)?.tableName ?? "<unknown>";
|
|
233
|
-
}
|
|
233
|
+
export { tableNameOf };
|
|
234
234
|
// A grant is only real when its reason is non-empty — `{ reason: "" }` must not silently unlock it.
|
|
235
235
|
function hasGrant(decl) {
|
|
236
236
|
return decl !== undefined && decl.reason.trim().length > 0;
|
|
@@ -247,7 +247,7 @@ export function hasTenantColumn(table) {
|
|
|
247
247
|
const meta = asEntityTableMeta(table);
|
|
248
248
|
if (meta)
|
|
249
249
|
return meta.columns.some((c) => c.name === "tenant_id");
|
|
250
|
-
return table
|
|
250
|
+
return hasFieldProperty(table, "tenantId");
|
|
251
251
|
}
|
|
252
252
|
const unsafeRawRebinders = new WeakMap();
|
|
253
253
|
// Rebinds tenantDb's unsafeRaw grant (e.g. a hook's own escapeHatch); inputs not built by
|
|
@@ -396,6 +396,10 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
396
396
|
message: `db.global(${tableNameOf(table)}): writes on executor-managed entity tables must go through the entity executor`,
|
|
397
397
|
});
|
|
398
398
|
}
|
|
399
|
+
function writesDenied(operation) {
|
|
400
|
+
const reason = grants?.writesDeniedReason;
|
|
401
|
+
return reason === undefined ? undefined : tenantDbWritesDeniedError(operation, reason);
|
|
402
|
+
}
|
|
399
403
|
function globalWriteReason() {
|
|
400
404
|
return grants?.globalWrites?.reason ?? "";
|
|
401
405
|
}
|
|
@@ -443,7 +447,8 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
443
447
|
return withDbSpan("select", table, async () => bunFetchOne(db, table, where));
|
|
444
448
|
},
|
|
445
449
|
insertOne(values) {
|
|
446
|
-
const denied =
|
|
450
|
+
const denied = writesDenied("ctx.db.global().insertOne") ??
|
|
451
|
+
missingEscapeHatch(table) ??
|
|
447
452
|
executorManagedWriteDenied(table) ??
|
|
448
453
|
foreignTenantOnGlobalWrite(table, values["tenantId"]) ??
|
|
449
454
|
personalDataDenied(table, Object.keys(values));
|
|
@@ -453,7 +458,8 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
453
458
|
return withDbSpan("insert", table, async () => bunInsertOne(db, table, values));
|
|
454
459
|
},
|
|
455
460
|
updateMany(set, where) {
|
|
456
|
-
const denied =
|
|
461
|
+
const denied = writesDenied("ctx.db.global().updateMany") ??
|
|
462
|
+
missingEscapeHatch(table) ??
|
|
457
463
|
executorManagedWriteDenied(table) ??
|
|
458
464
|
foreignTenantOnGlobalWrite(table, set["tenantId"]) ??
|
|
459
465
|
personalDataDenied(table, Object.keys(set));
|
|
@@ -466,7 +472,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
466
472
|
return withDbSpan("update", table, async () => bunUpdateMany(db, table, set, where));
|
|
467
473
|
},
|
|
468
474
|
deleteMany(where) {
|
|
469
|
-
const denied =
|
|
475
|
+
const denied = writesDenied("ctx.db.global().deleteMany") ??
|
|
476
|
+
missingEscapeHatch(table) ??
|
|
477
|
+
executorManagedWriteDenied(table);
|
|
470
478
|
if (denied)
|
|
471
479
|
return Promise.reject(denied);
|
|
472
480
|
if (!where || Object.keys(where).length === 0) {
|
|
@@ -475,7 +483,7 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
475
483
|
report("global-write", globalWriteReason());
|
|
476
484
|
return withDbSpan("delete", table, async () => bunDeleteMany(db, table, where));
|
|
477
485
|
},
|
|
478
|
-
// @cast-boundary
|
|
486
|
+
// @cast-boundary phantom-brand — GlobalWrites is only present in the type when TTable is not ExecutorOnly; the object above always carries the methods.
|
|
479
487
|
};
|
|
480
488
|
}
|
|
481
489
|
function grantedUnsafeRawRunner(declaredStepReason) {
|
|
@@ -515,6 +523,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
515
523
|
return withDbSpan("select", table, async () => bunAggregateWhere(db, table, spec, filter));
|
|
516
524
|
},
|
|
517
525
|
insertOne(table, values) {
|
|
526
|
+
const writeDenied = writesDenied("ctx.db.insertOne");
|
|
527
|
+
if (writeDenied)
|
|
528
|
+
return Promise.reject(writeDenied);
|
|
518
529
|
if (mode === "tenant" &&
|
|
519
530
|
hasTenantColumn(table) &&
|
|
520
531
|
asEntityTableMeta(table)?.tenancy === "global") {
|
|
@@ -530,6 +541,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
530
541
|
return withDbSpan("insert", table, async () => bunInsertOne(db, table, data));
|
|
531
542
|
},
|
|
532
543
|
updateMany(table, set, where) {
|
|
544
|
+
const writeDenied = writesDenied("ctx.db.updateMany");
|
|
545
|
+
if (writeDenied)
|
|
546
|
+
return Promise.reject(writeDenied);
|
|
533
547
|
if (!where || Object.keys(where).length === 0) {
|
|
534
548
|
return Promise.reject(new Error("TenantDb.updateMany without where would mass-update all tenant rows. Pass at least one where condition."));
|
|
535
549
|
}
|
|
@@ -543,6 +557,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
543
557
|
return withDbSpan("update", table, async () => bunUpdateMany(db, table, set, filter));
|
|
544
558
|
},
|
|
545
559
|
deleteMany(table, where) {
|
|
560
|
+
const writeDenied = writesDenied("ctx.db.deleteMany");
|
|
561
|
+
if (writeDenied)
|
|
562
|
+
return Promise.reject(writeDenied);
|
|
546
563
|
if (!where || Object.keys(where).length === 0) {
|
|
547
564
|
return Promise.reject(new Error("TenantDb.deleteMany without where would mass-delete all tenant rows. Pass at least one where condition."));
|
|
548
565
|
}
|
|
@@ -579,6 +596,9 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
579
596
|
return runInNewTransaction(db, (tx) => fn(createTenantDb(tx, tenantId, mode, tracer, meter, signal, txGrants)));
|
|
580
597
|
});
|
|
581
598
|
bindTenantDbRunner(tenantDb, db);
|
|
599
|
+
if (grants?.writesDeniedReason !== undefined) {
|
|
600
|
+
denyTenantDbWrites(tenantDb, grants.writesDeniedReason);
|
|
601
|
+
}
|
|
582
602
|
if (personalDataGate)
|
|
583
603
|
personalDataGates.set(tenantDb, personalDataGate);
|
|
584
604
|
if (projectionRegistry)
|
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
// Deterministic JSON: object keys sorted (recursively), `undefined` values
|
|
3
4
|
// dropped, arrays keep their order. Two specs that are semantically equal
|
|
4
5
|
// (same keys, different insertion order; explicit `undefined` vs. omitted)
|
|
@@ -7,7 +8,7 @@ export function canonicalJson(value) {
|
|
|
7
8
|
if (Array.isArray(value)) {
|
|
8
9
|
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
9
10
|
}
|
|
10
|
-
if (value
|
|
11
|
+
if (isPlainObject(value)) {
|
|
11
12
|
const entries = Object.entries(value)
|
|
12
13
|
.filter(([, v]) => v !== undefined)
|
|
13
14
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
@@ -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;
|