@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.
Files changed (281) hide show
  1. package/AGENTS.md +338 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +446 -0
  4. package/agents/examples/anonymous-access.md +288 -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 +319 -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 +315 -0
  20. package/agents/examples/custom-handlers.md +444 -0
  21. package/agents/examples/dashboard-stats.md +229 -0
  22. package/agents/examples/delivery-notifications.md +432 -0
  23. package/agents/examples/designer-demo.md +86 -0
  24. package/agents/examples/e2e-generator.md +211 -0
  25. package/agents/examples/embedded-entity-form.md +598 -0
  26. package/agents/examples/embedded.md +649 -0
  27. package/agents/examples/encrypted-tenant-config.md +414 -0
  28. package/agents/examples/error-contract.md +535 -0
  29. package/agents/examples/event-sourcing.md +1287 -0
  30. package/agents/examples/feature-toggles.md +274 -0
  31. package/agents/examples/field-access.md +271 -0
  32. package/agents/examples/files-post-processing.md +433 -0
  33. package/agents/examples/i18n.md +166 -0
  34. package/agents/examples/idempotency.md +179 -0
  35. package/agents/examples/inbound-mail-triage.md +315 -0
  36. package/agents/examples/lane-routing.md +246 -0
  37. package/agents/examples/ledger-banking.md +215 -0
  38. package/agents/examples/ledger-invoicing.md +236 -0
  39. package/agents/examples/legal-pages.md +393 -0
  40. package/agents/examples/lifecycle-hooks.md +368 -0
  41. package/agents/examples/managed-config.md +428 -0
  42. package/agents/examples/managed-pages.md +353 -0
  43. package/agents/examples/notes-history-basic.md +346 -0
  44. package/agents/examples/ownership.md +404 -0
  45. package/agents/examples/pdf-audit-export.md +306 -0
  46. package/agents/examples/pipeline-basics.md +792 -0
  47. package/agents/examples/public-share-token.md +351 -0
  48. package/agents/examples/rate-limiting.md +211 -0
  49. package/agents/examples/realtime-sse.md +187 -0
  50. package/agents/examples/record-detail-layout.md +297 -0
  51. package/agents/examples/reference-data.md +151 -0
  52. package/agents/examples/relations.md +280 -0
  53. package/agents/examples/screens-nav.md +353 -0
  54. package/agents/examples/search.md +233 -0
  55. package/agents/examples/secrets-demo.md +417 -0
  56. package/agents/examples/security-baseline.md +187 -0
  57. package/agents/examples/seed-migration.md +99 -0
  58. package/agents/examples/session-revocation.md +250 -0
  59. package/agents/examples/state-machine.md +920 -0
  60. package/agents/examples/tags-basic.md +401 -0
  61. package/agents/examples/tenant-isolation.md +173 -0
  62. package/agents/examples/tenant-settings.md +275 -0
  63. package/agents/examples/tier-admin.md +347 -0
  64. package/agents/examples/timezones.md +366 -0
  65. package/agents/examples/user-data-rights.md +418 -0
  66. package/agents/examples/user-profile.md +226 -0
  67. package/agents/examples/webhook-step.md +597 -0
  68. package/agents/examples/wizard-form.md +379 -0
  69. package/agents/examples/workflow-engine.md +1731 -0
  70. package/agents/examples/writeform-section.md +300 -0
  71. package/agents/examples.md +75 -0
  72. package/agents/features.md +768 -0
  73. package/agents/widgets.md +181 -0
  74. package/dist/api/auth-middleware.d.ts +2 -0
  75. package/dist/api/auth-middleware.js +7 -0
  76. package/dist/api/auth-routes.d.ts +0 -1
  77. package/dist/api/auth-routes.js +49 -46
  78. package/dist/api/extra-route.d.ts +3 -0
  79. package/dist/api/index.d.ts +2 -2
  80. package/dist/api/index.js +1 -1
  81. package/dist/api/routes.js +46 -17
  82. package/dist/api/server.d.ts +4 -3
  83. package/dist/api/server.js +39 -10
  84. package/dist/api/session-cookies.d.ts +1 -0
  85. package/dist/api/session-cookies.js +20 -5
  86. package/dist/api/sse-route.js +16 -1
  87. package/dist/bun-db/connection.js +3 -16
  88. package/dist/bun-db/index.d.ts +1 -1
  89. package/dist/bun-db/index.js +1 -1
  90. package/dist/bun-db/query.d.ts +15 -3
  91. package/dist/bun-db/query.js +76 -24
  92. package/dist/crypto/owner-transition-rekey.d.ts +1 -0
  93. package/dist/crypto/owner-transition-rekey.js +7 -3
  94. package/dist/crypto/pii-field-encryption.d.ts +1 -0
  95. package/dist/crypto/pii-field-encryption.js +9 -2
  96. package/dist/crypto/subject-resolver.d.ts +1 -0
  97. package/dist/crypto/subject-resolver.js +7 -0
  98. package/dist/db/api.d.ts +4 -3
  99. package/dist/db/apply-entity-event.js +26 -2
  100. package/dist/db/blind-index-cleanup.js +2 -1
  101. package/dist/db/bun-provider.js +9 -18
  102. package/dist/db/collect-table-metas.js +11 -7
  103. package/dist/db/dialect.d.ts +1 -1
  104. package/dist/db/dialect.js +2 -0
  105. package/dist/db/eagerload.js +4 -3
  106. package/dist/db/event-store-executor-context.d.ts +6 -6
  107. package/dist/db/event-store-executor-context.js +25 -22
  108. package/dist/db/event-store-executor-read.js +31 -16
  109. package/dist/db/event-store-executor-write.d.ts +1 -1
  110. package/dist/db/event-store-executor-write.js +84 -63
  111. package/dist/db/index.d.ts +2 -1
  112. package/dist/db/index.js +2 -1
  113. package/dist/db/lock-guard.d.ts +4 -0
  114. package/dist/db/lock-guard.js +14 -0
  115. package/dist/db/migrate-generator.js +1 -1
  116. package/dist/db/migrate-runner.d.ts +1 -0
  117. package/dist/db/migrate-runner.js +64 -38
  118. package/dist/db/queries/backfill-pii.js +63 -25
  119. package/dist/db/queries/event-consumer.d.ts +6 -6
  120. package/dist/db/queries/event-consumer.js +2 -1
  121. package/dist/db/queries/event-store-transfer.js +15 -4
  122. package/dist/db/queries/event-store.d.ts +1 -1
  123. package/dist/db/queries/event-store.js +11 -10
  124. package/dist/db/queries/pii-backfill-state.js +3 -2
  125. package/dist/db/queries/postgres-array-literal.d.ts +2 -0
  126. package/dist/db/queries/postgres-array-literal.js +6 -0
  127. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  128. package/dist/db/queries/projection-rebuild.js +3 -2
  129. package/dist/db/queries/shadow-swap.d.ts +1 -1
  130. package/dist/db/queries/shadow-swap.js +12 -8
  131. package/dist/db/query.d.ts +1 -1
  132. package/dist/db/query.js +1 -1
  133. package/dist/db/retired-framework-tables.js +22 -4
  134. package/dist/db/table-builder.js +6 -2
  135. package/dist/db/tenant-db-runner.d.ts +4 -0
  136. package/dist/db/tenant-db-runner.js +17 -0
  137. package/dist/db/tenant-db.d.ts +4 -0
  138. package/dist/db/tenant-db.js +50 -5
  139. package/dist/derivatives/delete-stored-file.d.ts +4 -0
  140. package/dist/derivatives/delete-stored-file.js +9 -0
  141. package/dist/derivatives/derivatives-context.js +1 -1
  142. package/dist/derivatives/index.d.ts +1 -1
  143. package/dist/derivatives/index.js +1 -1
  144. package/dist/engine/boot-validator/access-declarations.js +2 -2
  145. package/dist/engine/boot-validator/api-ext.js +3 -3
  146. package/dist/engine/boot-validator/boot-check.js +1 -1
  147. package/dist/engine/boot-validator/config-deps.js +13 -13
  148. package/dist/engine/boot-validator/detail-screens.js +1 -1
  149. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  150. package/dist/engine/boot-validator/entity-handler.js +87 -35
  151. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  152. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  153. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  154. package/dist/engine/boot-validator/nav.js +1 -1
  155. package/dist/engine/boot-validator/pii-retention.js +8 -8
  156. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  157. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  158. package/dist/engine/boot-validator/query-refs.js +14 -2
  159. package/dist/engine/boot-validator/screens.d.ts +2 -1
  160. package/dist/engine/boot-validator/screens.js +153 -95
  161. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  162. package/dist/engine/dedupe-features.js +31 -4
  163. package/dist/engine/define-handler.d.ts +2 -2
  164. package/dist/engine/define-handler.js +8 -1
  165. package/dist/engine/entity-handlers.js +14 -7
  166. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  167. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  168. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  169. package/dist/engine/feature-ast/render.js +10 -2
  170. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  171. package/dist/engine/feature-entity-handlers.js +12 -3
  172. package/dist/engine/handler-field-references.d.ts +21 -2
  173. package/dist/engine/handler-field-references.js +55 -17
  174. package/dist/engine/index.d.ts +3 -3
  175. package/dist/engine/index.js +1 -1
  176. package/dist/engine/schema-builder.js +2 -1
  177. package/dist/engine/screen-helpers.d.ts +1 -0
  178. package/dist/engine/screen-helpers.js +2 -2
  179. package/dist/engine/soft-delete-cleanup.js +10 -3
  180. package/dist/engine/steps/_read-source.js +5 -2
  181. package/dist/engine/steps/retry.js +2 -1
  182. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  183. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  184. package/dist/engine/template-variables.d.ts +3 -0
  185. package/dist/engine/template-variables.js +3 -0
  186. package/dist/engine/tier-resolver-extension.d.ts +1 -0
  187. package/dist/engine/tier-resolver-extension.js +6 -0
  188. package/dist/engine/types/index.d.ts +1 -1
  189. package/dist/entrypoint/index.d.ts +2 -2
  190. package/dist/env/_zod-introspect.d.ts +1 -0
  191. package/dist/errors/classes.js +1 -0
  192. package/dist/errors/docs-url.js +6 -6
  193. package/dist/errors/index.d.ts +1 -1
  194. package/dist/errors/index.js +1 -1
  195. package/dist/errors/kumiko-error.d.ts +2 -0
  196. package/dist/errors/kumiko-error.js +2 -0
  197. package/dist/errors/subject-erased.d.ts +1 -0
  198. package/dist/errors/subject-erased.js +6 -0
  199. package/dist/errors/write-error-info.js +2 -0
  200. package/dist/event-store/admin-api.js +3 -2
  201. package/dist/event-store/errors.d.ts +9 -0
  202. package/dist/event-store/errors.js +20 -0
  203. package/dist/event-store/event-store.d.ts +6 -0
  204. package/dist/event-store/event-store.js +28 -6
  205. package/dist/event-store/index.d.ts +1 -1
  206. package/dist/event-store/index.js +1 -1
  207. package/dist/event-store/tenant-db-append.js +2 -2
  208. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  209. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  210. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  211. package/dist/files/bind-pending-file-refs.js +3 -3
  212. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  213. package/dist/files/bind-referenced-files.write.js +22 -13
  214. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  215. package/dist/files/create-file-ref-from-bytes.js +67 -4
  216. package/dist/files/create-file-ref.d.ts +24 -1
  217. package/dist/files/create-file-ref.js +67 -25
  218. package/dist/files/dispose-removed-file-refs.js +21 -6
  219. package/dist/files/feature.d.ts +1 -0
  220. package/dist/files/feature.js +2 -1
  221. package/dist/files/file-handle.d.ts +1 -1
  222. package/dist/files/file-handle.js +12 -0
  223. package/dist/files/file-routes.js +10 -6
  224. package/dist/files/index.d.ts +3 -3
  225. package/dist/files/index.js +2 -2
  226. package/dist/files/load-save-file-refs.d.ts +10 -0
  227. package/dist/files/load-save-file-refs.js +47 -0
  228. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  229. package/dist/files/purge-expired-file-ref-bytes.js +46 -21
  230. package/dist/files/resolve-content-type.js +40 -4
  231. package/dist/files/storage-tracking.js +5 -4
  232. package/dist/files/types.js +5 -8
  233. package/dist/files/zip-entry-names.d.ts +1 -0
  234. package/dist/files/zip-entry-names.js +46 -0
  235. package/dist/jobs/index.d.ts +1 -1
  236. package/dist/jobs/job-runner.d.ts +5 -1
  237. package/dist/jobs/job-runner.js +124 -37
  238. package/dist/observability/escape-hatch-report.d.ts +2 -1
  239. package/dist/observability/escape-hatch-report.js +48 -7
  240. package/dist/observability/index.d.ts +1 -1
  241. package/dist/observability/index.js +1 -1
  242. package/dist/observability/metrics-handle.d.ts +1 -0
  243. package/dist/observability/metrics-handle.js +7 -0
  244. package/dist/observability/standard-metrics.js +4 -4
  245. package/dist/pipeline/cascade-handler.js +5 -2
  246. package/dist/pipeline/dispatch-batch.js +79 -31
  247. package/dist/pipeline/dispatch-query.d.ts +2 -1
  248. package/dist/pipeline/dispatch-query.js +6 -6
  249. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  250. package/dist/pipeline/dispatch-shared.js +136 -48
  251. package/dist/pipeline/dispatch-stream.js +10 -4
  252. package/dist/pipeline/dispatch-write.js +34 -22
  253. package/dist/pipeline/dispatcher.js +3 -1
  254. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  255. package/dist/pipeline/event-dispatcher.js +11 -9
  256. package/dist/pipeline/event-retention.d.ts +1 -0
  257. package/dist/pipeline/event-retention.js +7 -1
  258. package/dist/pipeline/index.d.ts +1 -0
  259. package/dist/pipeline/index.js +1 -0
  260. package/dist/pipeline/notify-scope.js +3 -1
  261. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  262. package/dist/pipeline/pre-transaction-reservations.js +29 -9
  263. package/dist/rate-limit/bucket.d.ts +3 -3
  264. package/dist/rate-limit/bucket.js +14 -4
  265. package/dist/redis/cache-sync-bus.js +9 -3
  266. package/dist/schema-cli.js +12 -1
  267. package/dist/search/meilisearch-adapter.js +36 -9
  268. package/dist/stack/db.d.ts +2 -2
  269. package/dist/stack/test-stack.js +2 -1
  270. package/dist/testing/index.d.ts +1 -0
  271. package/dist/testing/index.js +1 -0
  272. package/dist/testing/multipart-helper.d.ts +6 -0
  273. package/dist/testing/multipart-helper.js +1 -1
  274. package/dist/testing/zip-fixture.d.ts +1 -0
  275. package/dist/testing/zip-fixture.js +33 -0
  276. package/dist/ui-types/card-column-roles.js +4 -2
  277. package/dist/ui-types/index.d.ts +1 -1
  278. package/dist/ui-types/index.js +1 -1
  279. package/dist/upgrade-cli.js +46 -13
  280. package/package.json +7 -5
  281. package/src/changes.json +512 -8
@@ -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 = Array.isArray(requested) ? requested : [requested];
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 = missingEscapeHatch(table) ??
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 = missingEscapeHatch(table) ??
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 = missingEscapeHatch(table) ?? executorManagedWriteDenied(table);
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.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,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;