@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
@@ -288,7 +288,8 @@ export function fieldToZod(field, currencies, opts = {}) {
288
288
  // Multi file: array of fileRef UUIDs. Same story as the singular
289
289
  // variant — the element type has to match the UUID column on
290
290
  // fileRefsTable.id.
291
- return z.array(z.uuid());
291
+ const ids = z.array(z.uuid());
292
+ return field.maxCount === undefined ? ids : ids.max(field.maxCount);
292
293
  }
293
294
  case "reference":
294
295
  // Tier 2.7e-3: Validiert UUID-shape. Existenz-Check der Reference
@@ -358,9 +359,7 @@ function applyTotalsMatchRefinements(entity, schema) {
358
359
  continue;
359
360
  }
360
361
  const sumMinor = rawRows.reduce((total, row) => {
361
- const value = typeof row === "object" && row !== null
362
- ? row[subFieldName]
363
- : undefined;
362
+ const value = typeof row === "object" && row !== null ? Reflect.get(row, subFieldName) : undefined;
364
363
  return total + (typeof value === "number" ? value : 0);
365
364
  }, 0);
366
365
  if (sumMinor !== siblingMinor) {
@@ -11,6 +11,7 @@
11
11
  // table growth; irreversible event-log purging is data-retention's job
12
12
  // (pruneEvents), a separate, consumer-lag-guarded path.
13
13
  import { asEntityTableMeta, deleteMany } from "../db/query.js";
14
+ import { hasFieldProperty } from "../db/table-introspection.js";
14
15
  import { purgeExpiredFileRefsWithBytes } from "../files/purge-expired-file-ref-bytes.js";
15
16
  import { FILE_REF_ENTITY_NAME } from "../files/types.js";
16
17
  import { SYSTEM_USER_ID } from "./system-user.js";
@@ -34,7 +35,7 @@ export const softDeleteGraceDaysConfig = {
34
35
  bounds: { min: 1 },
35
36
  };
36
37
  function hasTenantIdColumn(table) {
37
- return table["tenantId"] !== undefined;
38
+ return hasFieldProperty(table, "tenantId");
38
39
  }
39
40
  // The entity declaration decides, not the column: a `tenancy: "global"` table
40
41
  // (e.g. user) still carries a tenantId column (SYSTEM_TENANT_ID), yet no
@@ -79,13 +80,20 @@ export const softDeleteCleanupJob = async (_payload, ctx) => {
79
80
  if (isSystemScopedSoftDeleteTable(proj.table, entity))
80
81
  continue;
81
82
  const where = { isDeleted: true, deletedAt: { lt: cutoff }, tenantId };
82
- await purgeExpiredRows(db, proj.source, proj.table, where, ctx);
83
+ await purgeExpiredRows(db, proj.source, proj.table, where, cutoff, ctx);
83
84
  }
84
85
  };
85
- async function purgeExpiredRows(db, source, table, where, ctx) {
86
+ async function purgeExpiredRows(db, source, table, where, cutoff, ctx) {
86
87
  if (source === FILE_REF_ENTITY_NAME) {
87
88
  // The row is the only record of the storageKey, so its bytes go first.
88
- await purgeExpiredFileRefsWithBytes({ db, table, where, files: ctx.files, log: ctx.log });
89
+ await purgeExpiredFileRefsWithBytes({
90
+ db,
91
+ table,
92
+ where,
93
+ expiredBefore: cutoff,
94
+ files: ctx.files,
95
+ log: ctx.log,
96
+ });
89
97
  }
90
98
  else {
91
99
  await deleteMany(db, table, where);
@@ -18,9 +18,7 @@ defineStep({
18
18
  // dispatcher's catch maps it back to a typed error response
19
19
  // (e.g. validation_failed stays validation_failed, not a
20
20
  // generic internal_error).
21
- const err = new Error(`callFeature("${args.handler}") returned failure`);
22
- err.cause = result.error;
23
- throw err;
21
+ throw new Error(`callFeature("${args.handler}") returned failure`, { cause: result.error });
24
22
  }
25
23
  return result.data;
26
24
  },
@@ -11,6 +11,7 @@ import { defineStep } from "../define-step.js";
11
11
  import { runStepList } from "../run-pipeline.js";
12
12
  import { SUSPEND_SENTINEL, WORKFLOW_AGGREGATE_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, } from "./_step-dispatch-constants.js";
13
13
  import { describeWorkflowStepError } from "./describe-workflow-step-error.js";
14
+ import { unwrapWorkflowStepError } from "./workflow-step-error.js";
14
15
  const log = createFallbackLogger("workflow-retry");
15
16
  defineStep({
16
17
  kind: "workflow.retry",
@@ -38,7 +39,7 @@ defineStep({
38
39
  workflowName: ctx.workflow.workflowName,
39
40
  stepIndex: ctx.workflow.stepIndex,
40
41
  attempt,
41
- error: String(error),
42
+ error: String(unwrapWorkflowStepError(error)),
42
43
  });
43
44
  const backoffMs = calculateBackoff(attempt, args.backoff);
44
45
  const wakeAt = Temporal.Now.instant().add({ milliseconds: backoffMs }).toString();
@@ -16,7 +16,7 @@
16
16
  // must commit in the same TX as the aggregate-mutation that triggered
17
17
  // it (stronger consistency than an async projection). Reviewer judges.
18
18
  import { deleteMany } from "../../db/query.js";
19
- import { tenantDbRunner } from "../../db/tenant-db-runner.js";
19
+ import { tenantDbWriteRunner } from "../../db/tenant-db-runner.js";
20
20
  import { defineStep } from "../define-step.js";
21
21
  import { resolveRequired } from "./_resolver-utils.js";
22
22
  defineStep({
@@ -27,7 +27,7 @@ defineStep({
27
27
  // The `unsafe*`-step IS the sanctioned direct projection write — it exists
28
28
  // to bypass the executor deliberately (boot-validated allowlist + reviewer
29
29
  // gate). The brand-strip cast makes that explicit at the one blessed seam.
30
- await deleteMany(tenantDbRunner(ctx.db), args.table, where);
30
+ await deleteMany(tenantDbWriteRunner(ctx.db, "unsafe projection delete"), args.table, where);
31
31
  },
32
32
  });
33
33
  export function buildUnsafeProjectionDeleteStep(args) {
@@ -10,25 +10,14 @@
10
10
  // the owning feature. Aggregate-tables (registered via r.entity) are
11
11
  // rejected by boot-validation — domain mutation MUST go through
12
12
  // r.step.aggregate.*.
13
- import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
14
13
  import { extractTableName } from "../../db/index.js";
15
14
  import { executeRawQuery } from "../../db/queries/raw-sql.js";
16
- import { tenantDbRunner } from "../../db/tenant-db-runner.js";
15
+ import { columnSqlNameOf } from "../../db/table-introspection.js";
16
+ import { tenantDbWriteRunner } from "../../db/tenant-db-runner.js";
17
17
  import { defineStep } from "../define-step.js";
18
18
  import { resolveRequired } from "./_resolver-utils.js";
19
19
  function resolveColumnName(table, field) {
20
- if (typeof table !== "object" || table === null)
21
- return field;
22
- const cols = table[KUMIKO_COLUMNS_SYMBOL];
23
- if (typeof cols !== "object" || cols === null)
24
- return field;
25
- const col = cols[field];
26
- if (typeof col === "object" && col !== null) {
27
- const nameVal = col["name"];
28
- if (typeof nameVal === "string")
29
- return nameVal;
30
- }
31
- return field;
20
+ return columnSqlNameOf(table, field) ?? field;
32
21
  }
33
22
  function quoteIdent(name) {
34
23
  return `"${name.replace(/"/g, '""')}"`;
@@ -65,7 +54,7 @@ defineStep({
65
54
  }
66
55
  const sqlText = `INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
67
56
  `ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
68
- await executeRawQuery(tenantDbRunner(ctx.db), sqlText, params);
57
+ await executeRawQuery(tenantDbWriteRunner(ctx.db, "unsafe projection upsert"), sqlText, params);
69
58
  },
70
59
  });
71
60
  export function buildUnsafeProjectionUpsertStep(args) {
@@ -0,0 +1,32 @@
1
+ import * as z from "zod";
2
+ import type { Registry, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariables } from "./types/index.js";
3
+ type CommonOptions = {
4
+ readonly label?: string;
5
+ readonly required?: boolean;
6
+ };
7
+ export declare const templateVariable: {
8
+ readonly text: (opts?: CommonOptions & {
9
+ readonly example?: string;
10
+ }) => TemplateScalarVariableDef;
11
+ readonly link: (opts?: CommonOptions & {
12
+ readonly example?: string;
13
+ }) => TemplateScalarVariableDef;
14
+ readonly richText: (opts?: CommonOptions & {
15
+ readonly example?: string;
16
+ }) => TemplateScalarVariableDef;
17
+ readonly money: (opts?: CommonOptions & {
18
+ readonly example?: number | {
19
+ readonly amount: number;
20
+ readonly currency: string;
21
+ };
22
+ }) => TemplateScalarVariableDef;
23
+ readonly date: (opts?: CommonOptions & {
24
+ readonly example?: string;
25
+ }) => TemplateScalarVariableDef;
26
+ readonly list: (item: Readonly<Record<string, TemplateScalarVariableDef>>, opts?: CommonOptions & {
27
+ readonly example?: readonly Readonly<Record<string, unknown>>[];
28
+ }) => TemplateListVariableDef;
29
+ };
30
+ export declare function templateVariablesToZod(variables: TemplateVariables): z.ZodObject;
31
+ export declare function assertNotificationVariables(registry: Pick<Registry, "getAllNotifications">, notificationType: string, data: Readonly<Record<string, unknown>> | undefined): void;
32
+ export {};
@@ -0,0 +1,80 @@
1
+ import * as z from "zod";
2
+ import { InternalError } from "../errors/index.js";
3
+ // Builders keep declarations terse and the `type` discriminant out of app code.
4
+ export const templateVariable = {
5
+ text: (opts = {}) => ({
6
+ type: "text",
7
+ ...opts,
8
+ }),
9
+ link: (opts = {}) => ({
10
+ type: "link",
11
+ ...opts,
12
+ }),
13
+ richText: (opts = {}) => ({ type: "richText", ...opts }),
14
+ money: (opts = {}) => ({ type: "money", ...opts }),
15
+ date: (opts = {}) => ({
16
+ type: "date",
17
+ ...opts,
18
+ }),
19
+ list: (item, opts = {}) => ({ type: "list", item, ...opts }),
20
+ };
21
+ const LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
22
+ function isSafeLinkUrl(value) {
23
+ try {
24
+ return LINK_PROTOCOLS.has(new URL(value).protocol);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ const linkSchema = z.string().refine(isSafeLinkUrl, "must be an http, https or mailto URL");
31
+ const moneySchema = z.union([
32
+ z.number().int(),
33
+ z.object({
34
+ amount: z.number().int(),
35
+ currency: z.string().regex(/^[A-Z]{3}$/),
36
+ }),
37
+ ]);
38
+ const dateSchema = z.union([z.iso.date(), z.iso.datetime({ offset: true })]);
39
+ function scalarToZod(def) {
40
+ switch (def.type) {
41
+ case "text":
42
+ return z.string();
43
+ case "richText":
44
+ return z.string();
45
+ case "link":
46
+ return linkSchema;
47
+ case "money":
48
+ return moneySchema;
49
+ case "date":
50
+ return dateSchema;
51
+ }
52
+ }
53
+ function variableToZod(def) {
54
+ if (def.type !== "list")
55
+ return scalarToZod(def);
56
+ return z.array(z.object(Object.fromEntries(Object.entries(def.item).map(([name, item]) => [name, withOptionality(item)]))));
57
+ }
58
+ function withOptionality(def) {
59
+ const schema = variableToZod(def);
60
+ return def.required === true ? schema : schema.optional();
61
+ }
62
+ export function templateVariablesToZod(variables) {
63
+ return z.object(Object.fromEntries(Object.entries(variables).map(([name, def]) => [name, withOptionality(def)])));
64
+ }
65
+ // Validates only: the caller keeps passing the original data on, because
66
+ // parsing would strip keys the delivery adds itself (unsubscribeUrl, fromName).
67
+ export function assertNotificationVariables(registry, notificationType, data) {
68
+ const definition = registry.getAllNotifications().get(notificationType);
69
+ if (definition?.template !== undefined && definition.variables !== undefined) {
70
+ const parsed = templateVariablesToZod(definition.variables).safeParse(data ?? {});
71
+ if (!parsed.success) {
72
+ const issues = parsed.error.issues
73
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
74
+ .join("; ");
75
+ throw new InternalError({
76
+ message: `notification "${notificationType}" has invalid variables: ${issues}`,
77
+ });
78
+ }
79
+ }
80
+ }
@@ -19,6 +19,7 @@
19
19
  //
20
20
  // **Apps ohne tier-engine:** wenn keine plugin registriert ist, framework
21
21
  // macht nichts — `effectiveFeatures` bleibt undefined, alle features sind on.
22
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
22
23
  /**
23
24
  * Extension-name unter dem ein tier-resolver-plugin im registry registriert
24
25
  * werden muss. Konstante damit `r.useExtension(TENANT_TIER_RESOLVER_EXT, ...)`
@@ -39,9 +40,7 @@ export const TENANT_TIER_RESOLVER_EXT = "tenantTierResolver";
39
40
  * plugin shape — the only thing every implementation must provide is the
40
41
  * `build` factory. */
41
42
  export function isTierResolverPlugin(v) {
42
- return (typeof v === "object" &&
43
- v !== null &&
44
- typeof v["build"] === "function");
43
+ return isPlainObject(v) && typeof v["build"] === "function";
45
44
  }
46
45
  export function findTierResolverUsage(features) {
47
46
  for (const feature of features) {
@@ -1,5 +1,5 @@
1
1
  export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
2
- export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
2
+ export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
3
3
  export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "@cosmicdrift/kumiko-types/define-handler";
4
4
  export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
5
5
  export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
@@ -7,7 +7,7 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
7
7
  export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
8
8
  export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
9
9
  export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
10
- export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
10
+ export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
11
11
  export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
12
12
  export type { DeleteContext, EntityHookMap, HookMap, HookOwner, HookPhase, LifecycleHookFn, LifecycleResult, OwnedFn, PhasedHook, PostDeleteBatchHookFn, PostDeleteHookFn, PostQueryHookFn, PostSaveBatchHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SaveContext, SearchPayloadContributorFn, ValidationError, ValidationHookFn, } from "@cosmicdrift/kumiko-types/hooks";
13
13
  export type { HttpRouteDefinition, HttpRouteHandler, HttpRouteHandlerDeps, HttpRouteMethod, } from "@cosmicdrift/kumiko-types/http-route";
@@ -22,7 +22,8 @@
22
22
  // boot-time is surfaced cleanly; a quietly-different step-list is not
23
23
  // caught by this validator. A future lint-rule will enforce the contract
24
24
  // statically; today it lives in this comment + the StepBuilder doc.
25
- import { KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
25
+ import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
26
+ import { schemaTableName } from "../db/table-introspection.js";
26
27
  import { getStep } from "./define-step.js";
27
28
  import { buildPipelineSteps } from "./pipeline.js";
28
29
  // Listed step-kinds whose `args.table` must be in the owning feature's
@@ -57,21 +58,19 @@ function* walkAllSteps(steps) {
57
58
  function resolveTableNameFromStep(table) {
58
59
  if (typeof table === "object" && table !== null) {
59
60
  // Canonical meta under the unshadowable symbol — preferred path.
60
- const meta = table[KUMIKO_META_SYMBOL];
61
- if (meta !== null && typeof meta === "object") {
62
- const metaName = meta["tableName"];
63
- if (typeof metaName === "string")
64
- return metaName;
61
+ const meta = Reflect.get(table, KUMIKO_META_SYMBOL);
62
+ if (typeof meta === "object" &&
63
+ meta !== null &&
64
+ "tableName" in meta &&
65
+ typeof meta.tableName === "string") {
66
+ return meta.tableName;
65
67
  }
66
68
  // Plain meta (deriveEntityTableMeta / defineUnmanagedTable — no handle-spread).
67
- if ("source" in table &&
68
- "tableName" in table &&
69
- typeof table["tableName"] === "string") {
70
- return table["tableName"];
69
+ if ("source" in table && "tableName" in table && typeof table.tableName === "string") {
70
+ return table.tableName;
71
71
  }
72
- // drizzle pgTable
73
- const name = table[KUMIKO_NAME_SYMBOL];
74
- if (typeof name === "string")
72
+ const name = schemaTableName(table);
73
+ if (name !== undefined)
75
74
  return name;
76
75
  }
77
76
  throw new Error(`validate-projection-allowlist: cannot resolve table name from ${String(table)}`);
@@ -8,8 +8,8 @@
8
8
  // parser) that catches the two shapes that always mean "this rule is
9
9
  // broken": a bare column reference inside a subquery that shadows an outer
10
10
  // column, and a literal self-comparison.
11
- import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
12
11
  import { toSnakeCase } from "../db/table-builder.js";
12
+ import { tableColumnsOf } from "../db/table-introspection.js";
13
13
  const IDENT_SRC = `"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*`;
14
14
  const QUALIFIED_SRC = `(?:${IDENT_SRC})(?:\\.(?:${IDENT_SRC}))?`;
15
15
  const TOKEN_RE = new RegExp(IDENT_SRC, "g");
@@ -23,14 +23,15 @@ const QUALIFIED_PARSE_RE = new RegExp(`^(${IDENT_SRC})(?:\\.(${IDENT_SRC}))?$`);
23
23
  export function tableColumnSqlNames(table) {
24
24
  if (table === null || typeof table !== "object")
25
25
  return new Set();
26
- const cols = table[KUMIKO_COLUMNS_SYMBOL];
27
- if (cols && typeof cols === "object") {
26
+ const cols = tableColumnsOf(table);
27
+ if (cols) {
28
28
  const names = new Set();
29
29
  for (const col of Object.values(cols)) {
30
- if (col && typeof col === "object") {
31
- const nameVal = col["name"];
32
- if (typeof nameVal === "string")
33
- names.add(nameVal.toLowerCase());
30
+ if (typeof col === "object" &&
31
+ col !== null &&
32
+ "name" in col &&
33
+ typeof col.name === "string") {
34
+ names.add(col.name.toLowerCase());
34
35
  }
35
36
  }
36
37
  return names;
@@ -2,6 +2,7 @@ import type * as z from "zod";
2
2
  export type ZodDef = {
3
3
  readonly innerType?: z.ZodType;
4
4
  readonly in?: z.ZodType;
5
+ readonly element?: z.ZodType;
5
6
  readonly defaultValue?: unknown;
6
7
  };
7
8
  /** Read Zod v4 `.meta()` value. Undefined when no meta is set. */
@@ -223,6 +223,7 @@ export class RateLimitError extends KumikoError {
223
223
  message: `rate limited: ${details.bucket} (limit ${details.limit} per ${details.windowSeconds}s)`,
224
224
  i18nKey: opts?.i18nKey ?? "errors.rate_limited",
225
225
  details,
226
+ retryAfterSeconds: details.retryAfterSeconds,
226
227
  ...(opts?.cause && { cause: opts.cause }),
227
228
  });
228
229
  this.details = details;
@@ -11,6 +11,7 @@ export type ErrorCtorInput = {
11
11
  readonly i18nParams?: Readonly<Record<string, unknown>>;
12
12
  readonly details?: unknown;
13
13
  readonly cause?: Error;
14
+ readonly retryAfterSeconds?: number;
14
15
  };
15
16
  export declare abstract class KumikoError extends Error {
16
17
  abstract readonly code: string;
@@ -18,6 +19,7 @@ export declare abstract class KumikoError extends Error {
18
19
  readonly i18nKey: string;
19
20
  readonly i18nParams: Readonly<Record<string, unknown>> | undefined;
20
21
  readonly details: unknown;
22
+ readonly retryAfterSeconds: number | undefined;
21
23
  constructor(input: ErrorCtorInput);
22
24
  }
23
25
  export declare function isKumikoError(e: unknown): e is KumikoError;
@@ -6,12 +6,14 @@ export class KumikoError extends Error {
6
6
  i18nKey;
7
7
  i18nParams;
8
8
  details;
9
+ retryAfterSeconds;
9
10
  constructor(input) {
10
11
  super(input.message, input.cause ? { cause: input.cause } : undefined);
11
12
  this.name = new.target.name;
12
13
  this.i18nKey = input.i18nKey;
13
14
  this.i18nParams = input.i18nParams;
14
15
  this.details = input.details;
16
+ this.retryAfterSeconds = input.retryAfterSeconds;
15
17
  }
16
18
  }
17
19
  export function isKumikoError(e) {
@@ -7,6 +7,7 @@ export declare const FrameworkReasons: {
7
7
  readonly systemIdentitySwitchDenied: "system_identity_switch_denied";
8
8
  readonly identitySwitchDenied: "identity_switch_denied";
9
9
  readonly tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin";
10
+ readonly notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast";
10
11
  readonly unsafeAllTenantsDenied: "unsafe_all_tenants_denied";
11
12
  readonly memberResolutionDenied: "member_resolution_denied";
12
13
  readonly memberResolutionReadOnly: "member_resolution_read_only";
@@ -42,6 +42,8 @@ export const FrameworkReasons = {
42
42
  identitySwitchDenied: "identity_switch_denied",
43
43
  // AccessDeniedError: a payload carried tenantIdOverride but the caller is no SystemAdmin.
44
44
  tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin",
45
+ // AccessDeniedError: ctx.notify was called with `to: { tenant }` naming a tenant other than the caller's.
46
+ notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast",
45
47
  // AccessDeniedError: a handler/hook called ctx.queryProjection(..., { unsafeAllTenants: true })
46
48
  // without an r.systemScope() feature or a declared escapeHatch.
47
49
  unsafeAllTenantsDenied: "unsafe_all_tenants_denied",
@@ -69,6 +69,7 @@ export function toWriteErrorInfo(err) {
69
69
  message: err.message,
70
70
  ...(err.i18nParams && { i18nParams: err.i18nParams }),
71
71
  ...(effectiveDetails !== undefined && { details: effectiveDetails }),
72
+ ...(err.retryAfterSeconds !== undefined && { retryAfterSeconds: err.retryAfterSeconds }),
72
73
  };
73
74
  if (err.cause instanceof Error)
74
75
  causeByWriteErrorInfo.set(info, err.cause);
@@ -91,6 +92,7 @@ class ReraisedError extends KumikoError {
91
92
  i18nKey: info.i18nKey,
92
93
  ...(info.i18nParams && { i18nParams: info.i18nParams }),
93
94
  ...(info.details !== undefined && { details: info.details }),
95
+ ...(info.retryAfterSeconds !== undefined && { retryAfterSeconds: info.retryAfterSeconds }),
94
96
  ...(cause && { cause }),
95
97
  });
96
98
  this.code = info.code;
@@ -1,11 +1,11 @@
1
- import { tenantDbRunner } from "../db/tenant-db-runner.js";
1
+ import { tenantDbRunner, tenantDbWriteRunner } from "../db/tenant-db-runner.js";
2
2
  import { runProjectionsForEvent } from "../pipeline/projections-runner.js";
3
3
  import { append, getStreamVersion } from "./event-store.js";
4
4
  // Appends on the connection or transaction the TenantDb is bound to. The tenant is always the
5
5
  // TenantDb's own, so a caller cannot write into another tenant's stream; a TenantDb the framework
6
6
  // did not build (or a fail-closed systemScope guard) is rejected by tenantDbRunner.
7
7
  export async function appendEventInTenantDb(tenantDb, event, options = {}) {
8
- const runner = tenantDbRunner(tenantDb);
8
+ const runner = tenantDbWriteRunner(tenantDb, "appendEventInTenantDb");
9
9
  const stored = await append(runner, { ...event, tenantId: tenantDb.tenantId });
10
10
  if (options.registry)
11
11
  await runProjectionsForEvent(stored, options.registry, runner);
@@ -1,8 +1,7 @@
1
- import type { TenantDb } from "../db/tenant-db.js";
2
- import { type Registry, type SaveContext, type SessionUser } from "../engine/types/index.js";
1
+ import { type Registry, type SaveContext } from "../engine/types/index.js";
2
+ import type { FileRefRows } from "./load-save-file-refs.js";
3
3
  export declare function assertFileRefsBelongToTenant(args: {
4
4
  readonly registry: Registry;
5
5
  readonly result: SaveContext;
6
- readonly user: SessionUser;
7
- readonly db: TenantDb;
8
- }): Promise<void>;
6
+ readonly fileRefRows: FileRefRows;
7
+ }): void;
@@ -1,16 +1,17 @@
1
- import { isFileField, isUuid, } from "../engine/types/index.js";
1
+ import { isFileField } from "../engine/types/index.js";
2
2
  import { ValidationError } from "../errors/index.js";
3
3
  import { fileRefIdsOf } from "./bind-pending-file-refs.js";
4
- import { getFileRefExecutor } from "./create-file-ref.js";
5
4
  import { FILE_REF_ENTITY_NAME } from "./file-ref-entity.js";
6
5
  // A file field only stores an id, and the file routes authorize by tenant at
7
6
  // read time: without this check a writer could name another tenant's fileRef
8
7
  // and make the record point at it. Only newly written values are checked (all
9
8
  // of a create, `changes` of an update), so existing data is left alone. The
10
9
  // error is the same for "missing" and "belongs to another tenant" so the id
11
- // cannot be probed across tenants.
12
- export async function assertFileRefsBelongToTenant(args) {
13
- const { registry, result, user, db } = args;
10
+ // cannot be probed across tenants. `fileRefRows` holds only the writer's own
11
+ // tenant's live rows, so a missing entry is exactly those two cases. Values
12
+ // that are not UUIDs are skipped by fileRefIdsOf and never reach this check.
13
+ export function assertFileRefsBelongToTenant(args) {
14
+ const { registry, result, fileRefRows } = args;
14
15
  // skip: saves of unregistered aggregates (custom executors) declare no file fields
15
16
  if (result.entityName === undefined)
16
17
  return;
@@ -21,12 +22,11 @@ export async function assertFileRefsBelongToTenant(args) {
21
22
  // skip: without the files feature the file_refs table is not part of the schema
22
23
  if (registry.getEntity(FILE_REF_ENTITY_NAME) === undefined)
23
24
  return;
24
- const executor = getFileRefExecutor();
25
25
  for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
26
26
  if (!isFileField(fieldDef))
27
27
  continue;
28
28
  for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
29
- if (await isFileRefOfTenant(executor, fileRefId, user, db))
29
+ if (fileRefRows.has(fileRefId))
30
30
  continue;
31
31
  throw new ValidationError({
32
32
  fields: [
@@ -40,11 +40,3 @@ export async function assertFileRefsBelongToTenant(args) {
40
40
  }
41
41
  }
42
42
  }
43
- async function isFileRefOfTenant(executor, fileRefId, user, db) {
44
- if (!isUuid(fileRefId))
45
- return false;
46
- // detail() widens its tenant filter to (self, SYSTEM); only the writer's own
47
- // tenant counts here, same as GET /files/:id/meta.
48
- const row = await executor.detail({ id: fileRefId }, user, db);
49
- return row !== null && row["tenantId"] === user.tenantId;
50
- }
@@ -1,9 +1,11 @@
1
1
  import type { TenantDb } from "../db/tenant-db.js";
2
2
  import { type Registry, type SaveContext, type SessionUser } from "../engine/types/index.js";
3
+ import type { FileRefRows } from "./load-save-file-refs.js";
3
4
  export declare function fileRefIdsOf(value: unknown): readonly string[];
4
5
  export declare function bindPendingFileRefs(args: {
5
6
  readonly registry: Registry;
6
7
  readonly result: SaveContext;
7
8
  readonly user: SessionUser;
8
9
  readonly db: TenantDb;
10
+ readonly fileRefRows: FileRefRows;
9
11
  }): Promise<void>;
@@ -23,7 +23,7 @@ function isPendingUploadFor(row, entityName, fieldName, user) {
23
23
  // someone else's upload id in a save payload must not pull that file into a
24
24
  // record the caller controls.
25
25
  export async function bindPendingFileRefs(args) {
26
- const { registry, result, user, db } = args;
26
+ const { registry, result, user, db, fileRefRows } = args;
27
27
  // skip: saves of unnamed aggregates declare no file fields
28
28
  if (result.entityName === undefined)
29
29
  return;
@@ -42,8 +42,8 @@ export async function bindPendingFileRefs(args) {
42
42
  // changes, not data: an update's row carries every stored file id, including
43
43
  // ones bound earlier; only the ids this save names can be pending uploads.
44
44
  for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
45
- const row = await executor.detail({ id: fileRefId }, user, db);
46
- if (row === null || !isPendingUploadFor(row, entityName, fieldName, user))
45
+ const row = fileRefRows.get(fileRefId);
46
+ if (row === undefined || !isPendingUploadFor(row, entityName, fieldName, user))
47
47
  continue;
48
48
  const version = typeof row["version"] === "number" ? row["version"] : undefined;
49
49
  const bound = await executor.update({ id: fileRefId, version, changes: { entityId: String(result.id) } }, user, db);
@@ -13,4 +13,4 @@ export declare const bindReferencedFilesWrite: import("@cosmicdrift/kumiko-types
13
13
  bound: number;
14
14
  skipped: number;
15
15
  nextAfterId: string | null;
16
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
16
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;