@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.
Files changed (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. 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
+ }
@@ -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 { type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
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 declare function tableNameOf(table: Table | EntityTableMeta): string;
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;
@@ -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 { bindTenantDbRunner, tenantDbRunner } from "./tenant-db-runner.js";
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 function tableNameOf(table) {
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["tenantId"] !== undefined;
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 = missingEscapeHatch(table) ??
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 = missingEscapeHatch(table) ??
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 = missingEscapeHatch(table) ?? executorManagedWriteDenied(table);
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 type-brand — GlobalWrites is only present in the type when TTable is not ExecutorOnly; the object above always carries the methods.
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.ref(row.storageKey);
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 !== null && typeof value === "object") {
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;