@cosmicdrift/kumiko-framework 0.363.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 (189) 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 +7 -0
  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/routes.js +46 -17
  80. package/dist/api/server.d.ts +2 -1
  81. package/dist/api/server.js +18 -6
  82. package/dist/api/session-cookies.d.ts +1 -0
  83. package/dist/api/session-cookies.js +20 -5
  84. package/dist/api/sse-route.js +16 -1
  85. package/dist/bun-db/index.d.ts +1 -1
  86. package/dist/bun-db/index.js +1 -1
  87. package/dist/bun-db/query.d.ts +8 -3
  88. package/dist/bun-db/query.js +27 -7
  89. package/dist/db/collect-table-metas.js +11 -7
  90. package/dist/db/eagerload.js +2 -2
  91. package/dist/db/event-store-executor-context.d.ts +5 -6
  92. package/dist/db/event-store-executor-context.js +24 -22
  93. package/dist/db/event-store-executor-read.js +31 -16
  94. package/dist/db/event-store-executor-write.js +18 -20
  95. package/dist/db/index.d.ts +1 -1
  96. package/dist/db/index.js +1 -1
  97. package/dist/db/lock-guard.d.ts +4 -0
  98. package/dist/db/lock-guard.js +14 -0
  99. package/dist/db/migrate-generator.js +1 -1
  100. package/dist/db/migrate-runner.js +9 -8
  101. package/dist/db/queries/event-consumer.d.ts +6 -6
  102. package/dist/db/queries/event-store.d.ts +1 -1
  103. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  104. package/dist/db/tenant-db-runner.d.ts +4 -0
  105. package/dist/db/tenant-db-runner.js +17 -0
  106. package/dist/db/tenant-db.d.ts +2 -0
  107. package/dist/db/tenant-db.js +30 -4
  108. package/dist/derivatives/derivatives-context.js +1 -1
  109. package/dist/engine/boot-validator/access-declarations.js +2 -2
  110. package/dist/engine/boot-validator/api-ext.js +3 -3
  111. package/dist/engine/boot-validator/boot-check.js +1 -1
  112. package/dist/engine/boot-validator/config-deps.js +13 -13
  113. package/dist/engine/boot-validator/detail-screens.js +1 -1
  114. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  115. package/dist/engine/boot-validator/entity-handler.js +84 -35
  116. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  117. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  118. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  119. package/dist/engine/boot-validator/nav.js +1 -1
  120. package/dist/engine/boot-validator/pii-retention.js +8 -8
  121. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  122. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  123. package/dist/engine/boot-validator/screens.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens.js +153 -95
  125. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  126. package/dist/engine/define-handler.d.ts +2 -2
  127. package/dist/engine/define-handler.js +8 -1
  128. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  129. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  130. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  131. package/dist/engine/feature-ast/render.js +10 -2
  132. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  133. package/dist/engine/feature-entity-handlers.js +12 -3
  134. package/dist/engine/handler-field-references.d.ts +21 -2
  135. package/dist/engine/handler-field-references.js +55 -17
  136. package/dist/engine/index.d.ts +2 -2
  137. package/dist/engine/schema-builder.js +2 -1
  138. package/dist/engine/soft-delete-cleanup.js +10 -3
  139. package/dist/engine/steps/retry.js +2 -1
  140. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  141. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  142. package/dist/engine/template-variables.d.ts +3 -0
  143. package/dist/engine/template-variables.js +3 -0
  144. package/dist/engine/types/index.d.ts +1 -1
  145. package/dist/env/_zod-introspect.d.ts +1 -0
  146. package/dist/errors/classes.js +1 -0
  147. package/dist/errors/kumiko-error.d.ts +2 -0
  148. package/dist/errors/kumiko-error.js +2 -0
  149. package/dist/errors/write-error-info.js +2 -0
  150. package/dist/event-store/tenant-db-append.js +2 -2
  151. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  152. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  153. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  154. package/dist/files/bind-pending-file-refs.js +3 -3
  155. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  156. package/dist/files/bind-referenced-files.write.js +22 -13
  157. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  158. package/dist/files/create-file-ref-from-bytes.js +67 -4
  159. package/dist/files/create-file-ref.d.ts +22 -0
  160. package/dist/files/create-file-ref.js +53 -25
  161. package/dist/files/file-handle.d.ts +1 -1
  162. package/dist/files/file-handle.js +8 -0
  163. package/dist/files/file-routes.js +1 -1
  164. package/dist/files/index.d.ts +2 -2
  165. package/dist/files/index.js +1 -1
  166. package/dist/files/load-save-file-refs.d.ts +10 -0
  167. package/dist/files/load-save-file-refs.js +47 -0
  168. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  169. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  170. package/dist/jobs/index.d.ts +1 -1
  171. package/dist/jobs/job-runner.d.ts +4 -1
  172. package/dist/jobs/job-runner.js +8 -4
  173. package/dist/observability/escape-hatch-report.d.ts +1 -1
  174. package/dist/observability/escape-hatch-report.js +17 -5
  175. package/dist/pipeline/cascade-handler.js +5 -2
  176. package/dist/pipeline/dispatch-batch.js +79 -31
  177. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  178. package/dist/pipeline/dispatch-shared.js +99 -22
  179. package/dist/pipeline/dispatch-write.js +34 -22
  180. package/dist/pipeline/index.d.ts +1 -0
  181. package/dist/pipeline/index.js +1 -0
  182. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  183. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  184. package/dist/rate-limit/bucket.d.ts +3 -3
  185. package/dist/rate-limit/bucket.js +14 -4
  186. package/dist/redis/cache-sync-bus.js +9 -3
  187. package/dist/search/meilisearch-adapter.js +36 -9
  188. package/package.json +7 -5
  189. package/src/changes.json +207 -8
@@ -1,6 +1,6 @@
1
1
  import { qualifyEntityName } from "../qualified-name.js";
2
2
  import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
3
- import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, } from "./screens.js";
3
+ import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, validateDrawerPrefillKeys, } from "./screens.js";
4
4
  function isNavigateKind(action) {
5
5
  return action.kind === undefined || action.kind === "navigate";
6
6
  }
@@ -59,13 +59,14 @@ function navigateActionsOf(screen) {
59
59
  return [];
60
60
  }
61
61
  }
62
- function toNavigateParamsSource(action, params, sourceScreenEntity) {
62
+ function toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel) {
63
63
  return {
64
64
  ...(action.screen !== undefined && { screen: action.screen }),
65
65
  ...(action.entity !== undefined && { entity: action.entity }),
66
66
  ...(action.entityId !== undefined && { entityId: action.entityId }),
67
67
  params,
68
68
  sourceScreenEntity,
69
+ sourceLabel,
69
70
  };
70
71
  }
71
72
  // Every declarative navigate that writes params into the target's URL —
@@ -73,17 +74,19 @@ function toNavigateParamsSource(action, params, sourceScreenEntity) {
73
74
  // plus projectionDetail metrics (runMetricNavigate), which reuse that shape —
74
75
  // plus a relatedList toolbarAction with no `params` of its own, which still
75
76
  // writes its implicit parent-id param at runtime (see `implicitParams`).
76
- function navigateParamsSources(screen) {
77
+ function navigateParamsSources(screen, sourceLabel) {
77
78
  return navigateActionsOf(screen).flatMap(({ action, sourceScreenEntity, implicitParams }) => {
78
79
  if (!isNavigateKind(action))
79
80
  return [];
80
81
  const params = action.params ?? implicitParams;
81
- return params === undefined ? [] : [toNavigateParamsSource(action, params, sourceScreenEntity)];
82
+ return params === undefined
83
+ ? []
84
+ : [toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel)];
82
85
  });
83
86
  }
84
- // Per target screen QN, the union of field names any navigate `params` may
85
- // prefill from the URL. A form screen absent from the map accepts no URL prefill.
86
- export function collectUrlPrefillFieldsByScreenQn(features) {
87
+ // Per target screen QN, every navigate `params` source that writes into the
88
+ // target's URL prefill. A form screen absent from the map accepts no URL prefill.
89
+ function collectUrlPrefillSourcesByScreenQn(features) {
87
90
  const screensByShortId = collectScreensByShortId(features);
88
91
  const detailForByEntity = new Map();
89
92
  for (const feature of features) {
@@ -95,8 +98,9 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
95
98
  }
96
99
  const result = new Map();
97
100
  for (const feature of features) {
98
- for (const screen of Object.values(feature.screens)) {
99
- for (const source of navigateParamsSources(screen)) {
101
+ for (const [screenId, screen] of Object.entries(feature.screens)) {
102
+ const sourceLabel = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) navigate action`;
103
+ for (const source of navigateParamsSources(screen, sourceLabel)) {
100
104
  const target = resolveTarget(source, screensByShortId, detailForByEntity);
101
105
  if (target === undefined)
102
106
  continue;
@@ -105,15 +109,21 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
105
109
  continue;
106
110
  }
107
111
  const qn = qualifyEntityName(target.featureName, "screen", target.shortId);
108
- const fields = result.get(qn) ?? new Set();
109
- for (const key of rowFieldExtractorKeys(source.params))
110
- fields.add(key);
111
- result.set(qn, fields);
112
+ result.set(qn, [...(result.get(qn) ?? []), source]);
112
113
  }
113
114
  }
114
115
  }
115
116
  return result;
116
117
  }
118
+ // Per target screen QN, the union of field names any navigate `params` may
119
+ // prefill from the URL.
120
+ export function collectUrlPrefillFieldsByScreenQn(features) {
121
+ const result = new Map();
122
+ for (const [qn, sources] of collectUrlPrefillSourcesByScreenQn(features)) {
123
+ result.set(qn, new Set(sources.flatMap((source) => rowFieldExtractorKeys(source.params))));
124
+ }
125
+ return result;
126
+ }
117
127
  // Runtime resolution: a bare screen id matches the first feature registering
118
128
  // it (create-app's router); an entity target opens its detailFor screen with an id.
119
129
  function resolveTarget(source, screensByShortId, detailForByEntity) {
@@ -124,37 +134,46 @@ function resolveTarget(source, screensByShortId, detailForByEntity) {
124
134
  const match = screensByShortId.get(source.screen)?.[0];
125
135
  return match === undefined ? undefined : { ...match, shortId: source.screen };
126
136
  }
137
+ function assertHiddenRequiredFieldsSeeded(featureName, screenId, screen, seeded) {
138
+ for (const section of screen.layout.sections) {
139
+ if (!isFieldsEditSection(section))
140
+ continue;
141
+ for (const rawSpec of sectionFieldSpecs(section)) {
142
+ const spec = normalizeEditField(rawSpec);
143
+ if (spec.visible !== false)
144
+ continue;
145
+ const fieldDef = screen.fields[spec.field];
146
+ if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
147
+ continue;
148
+ }
149
+ if (seeded.has(spec.field))
150
+ continue;
151
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
152
+ `hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
153
+ `params — the field could never get a value and the form could never submit. Add ` +
154
+ `"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
155
+ `visible, or drop required.`);
156
+ }
157
+ }
158
+ }
127
159
  // submitPrefilled lets an unchanged form submit, so a required field the layout
128
- // hides must be seeded by a navigate param — nothing else can ever fill it.
160
+ // hides must be seeded by a navigate param — nothing else can ever fill it. The
161
+ // param keys of every source (incl. metric/subtitle navigates and the implicit
162
+ // relatedList parent id) must also be fields the renderer would actually prefill.
129
163
  export function validateSubmitPrefilledHiddenFields(features) {
130
- const prefillByScreenQn = collectUrlPrefillFieldsByScreenQn(features);
164
+ const sourcesByScreenQn = collectUrlPrefillSourcesByScreenQn(features);
131
165
  for (const feature of features) {
132
166
  for (const [screenId, screen] of Object.entries(feature.screens)) {
133
167
  if (screen.type !== "actionForm" && screen.type !== "secretMint")
134
168
  continue;
135
169
  if (screen.submitPrefilled !== true)
136
170
  continue;
137
- const seeded = prefillByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
138
- for (const section of screen.layout.sections) {
139
- if (!isFieldsEditSection(section))
140
- continue;
141
- for (const rawSpec of sectionFieldSpecs(section)) {
142
- const spec = normalizeEditField(rawSpec);
143
- if (spec.visible !== false)
144
- continue;
145
- const fieldDef = screen.fields[spec.field];
146
- if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
147
- continue;
148
- }
149
- if (seeded?.has(spec.field) === true)
150
- continue;
151
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
152
- `hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
153
- `params — the field could never get a value and the form could never submit. Add ` +
154
- `"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
155
- `visible, or drop required.`);
156
- }
171
+ const sources = sourcesByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
172
+ for (const source of sources ?? []) {
173
+ validateDrawerPrefillKeys(source.sourceLabel, "navigate-target", screenId, screen, source.params);
157
174
  }
175
+ const seeded = new Set((sources ?? []).flatMap((source) => rowFieldExtractorKeys(source.params)));
176
+ assertHiddenRequiredFieldsSeeded(feature.name, screenId, screen, seeded);
158
177
  }
159
178
  }
160
179
  }
@@ -4,9 +4,9 @@ import type { ContainsSecret } from "../secrets/types.js";
4
4
  import type { QueryHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput } from "./types/define-handler.js";
5
5
  import type { KumikoEventTypeMap } from "./types/index.js";
6
6
  export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "./types/define-handler.js";
7
- export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap>(def: WriteHandlerInput<TName, TSchema, TData, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
7
+ export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap, TPrepared = undefined>(def: WriteHandlerInput<TName, TSchema, TData, TMap, TPrepared>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
8
8
  secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
9
- ] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap>;
9
+ ] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap, TPrepared>;
10
10
  export declare function defineQueryHandler<const TName extends string, TSchema extends ZodType, TResult = unknown, TMap extends object = KumikoEventTypeMap>(def: QueryHandlerDefinition<TName, TSchema, TResult, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TResult> ? [
11
11
  secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
12
12
  ] : []): QueryHandlerDefinition<TName, TSchema, TResult, TMap>;
@@ -1,6 +1,12 @@
1
1
  import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
2
  import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
3
3
  import { runPipeline } from "./run-pipeline.js";
4
+ function pickPreTransactionPhases(def) {
5
+ return {
6
+ ...(def.reserveBeforeTransaction && { reserveBeforeTransaction: def.reserveBeforeTransaction }),
7
+ ...(def.beforeTransaction && { beforeTransaction: def.beforeTransaction }),
8
+ };
9
+ }
4
10
  export function defineWriteHandler(def,
5
11
  // R6: a phantom rest-param. When the inferred response `TData` carries a
6
12
  // Secret<> anywhere, ContainsSecret<TData> is `true` and this resolves to a
@@ -51,8 +57,9 @@ export function defineWriteHandler(def,
51
57
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
52
58
  ...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
53
59
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
54
- ...(def.reserveBeforeTransaction && { reserveBeforeTransaction: def.reserveBeforeTransaction }),
60
+ ...pickPreTransactionPhases(def),
55
61
  ...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
62
+ ...(def.transaction && { transaction: def.transaction }),
56
63
  };
57
64
  if ("perform" in def && def.perform !== undefined) {
58
65
  const performDef = def.perform;
@@ -36,6 +36,8 @@ const WRITE_HANDLER_KEY_KINDS = {
36
36
  escapeHatch: "modeled",
37
37
  tenantlessAnonymous: "opaque",
38
38
  reserveBeforeTransaction: "opaque",
39
+ beforeTransaction: "opaque",
40
+ transaction: "opaque",
39
41
  perform: "opaque",
40
42
  };
41
43
  const QUERY_HANDLER_KEY_KINDS = {
@@ -202,6 +202,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
202
202
  limit: z.ZodNumber;
203
203
  windowSeconds: z.ZodNumber;
204
204
  cost: z.ZodOptional<z.ZodNumber>;
205
+ normalize: z.ZodOptional<z.ZodLiteral<"email">>;
205
206
  }, z.core.$strict>>, z.ZodObject<{
206
207
  __raw: z.ZodString;
207
208
  }, z.core.$strict>]>>;
@@ -724,6 +725,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
724
725
  limit: z.ZodNumber;
725
726
  windowSeconds: z.ZodNumber;
726
727
  cost: z.ZodOptional<z.ZodNumber>;
728
+ normalize: z.ZodOptional<z.ZodLiteral<"email">>;
727
729
  }, z.core.$strict>>, z.ZodObject<{
728
730
  __raw: z.ZodString;
729
731
  }, z.core.$strict>]>>;
@@ -163,6 +163,7 @@ const additionalRateLimitsSchema = z.array(z
163
163
  limit: z.number(),
164
164
  windowSeconds: z.number(),
165
165
  cost: z.number().optional(),
166
+ normalize: z.literal("email").optional(),
166
167
  })
167
168
  .strict());
168
169
  const agentHandlerHintsSchema = z
@@ -240,18 +240,26 @@ function renderMetric(p) {
240
240
  const merged = { name: p.shortName, ...p.options };
241
241
  return `r.metric(${renderValue(merged)});`;
242
242
  }
243
+ // r.secret / r.secretNamespace throw on an empty writeRoles list; the designer's
244
+ // string-list input emits [] when cleared, which means "rely on handler access alone".
245
+ function dropEmptyWriteRoles(options) {
246
+ if (options.writeRoles?.length !== 0)
247
+ return options;
248
+ const { writeRoles: _emptyWriteRoles, ...rest } = options;
249
+ return rest;
250
+ }
243
251
  function renderSecret(p) {
244
252
  if (isRawRefSentinel(p.options)) {
245
253
  return `r.secret(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
246
254
  }
247
- const merged = { name: p.shortName, ...p.options };
255
+ const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
248
256
  return `r.secret(${renderValue(merged)});`;
249
257
  }
250
258
  function renderSecretNamespace(p) {
251
259
  if (isRawRefSentinel(p.options)) {
252
260
  return `r.secretNamespace(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
253
261
  }
254
- const merged = { name: p.shortName, ...p.options };
262
+ const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
255
263
  return `r.secretNamespace(${renderValue(merged)});`;
256
264
  }
257
265
  function renderClaimKey(p) {
@@ -3,7 +3,7 @@ import type { ZodType } from "zod";
3
3
  import { type QueryHandlerDefinition, type StreamHandlerDefinition, type WriteHandlerDefinition } from "./define-handler.js";
4
4
  import type { RegisterEntityCrudOptions } from "./entity-handlers.js";
5
5
  import type { FeatureBuilderState } from "./feature-builder-state.js";
6
- import type { AccessRule, AgentHandlerHints, EntityDefinition, EntityRef, EscapeHatchDeclaration, HandlerRef, NameOrRef, PayloadRateLimitOption, QueryHandlerFn, RateLimitDeclaration, RelationDefinition, StreamHandlerFn, WriteHandlerFn } from "./types/index.js";
6
+ import type { AccessRule, AgentHandlerHints, EntityDefinition, EntityRef, EscapeHatchDeclaration, HandlerRef, KumikoEventTypeMap, NameOrRef, PayloadRateLimitOption, QueryHandlerFn, RateLimitDeclaration, RelationDefinition, StreamHandlerFn, WriteHandlerFn } from "./types/index.js";
7
7
  export declare function buildEntityHandlerMethods<TName extends string>(state: FeatureBuilderState, name: TName): {
8
8
  entity(nameOrDefinition: string | ({
9
9
  readonly name: string;
@@ -14,7 +14,7 @@ export declare function buildEntityHandlerMethods<TName extends string>(state: F
14
14
  readonly entity: NameOrRef;
15
15
  readonly name: string;
16
16
  } & RelationDefinition), relationName?: string, definition?: RelationDefinition): void;
17
- writeHandler<TName_1 extends string, TSchema extends ZodType>(nameOrDef: string | WriteHandlerDefinition<TName_1, TSchema>, schema?: TSchema, handler?: WriteHandlerFn<z.infer<TSchema>>, options?: {
17
+ writeHandler<TName_1 extends string, TSchema extends ZodType, TPrepared = undefined>(nameOrDef: string | WriteHandlerDefinition<TName_1, TSchema, unknown, KumikoEventTypeMap, TPrepared>, schema?: TSchema, handler?: WriteHandlerFn<z.infer<TSchema>>, options?: {
18
18
  access: AccessRule;
19
19
  rateLimit?: RateLimitDeclaration;
20
20
  additionalRateLimits?: readonly PayloadRateLimitOption[];
@@ -54,6 +54,17 @@ function agentSlots(source) {
54
54
  ...(source.agent && { agent: source.agent }),
55
55
  };
56
56
  }
57
+ function transactionPhaseSlots(def) {
58
+ return {
59
+ ...(def.reserveBeforeTransaction && {
60
+ reserveBeforeTransaction: def.reserveBeforeTransaction,
61
+ }),
62
+ ...(def.beforeTransaction && {
63
+ beforeTransaction: def.beforeTransaction, // @cast-boundary engine-bridge
64
+ }),
65
+ ...(def.transaction && { transaction: def.transaction }),
66
+ };
67
+ }
57
68
  function requireInlineHandlerArgs(kind, schema, handler, options) {
58
69
  if (!schema || !handler || !options?.access) {
59
70
  throw new Error(`${kind} inline form requires schema + handler + options.access`);
@@ -108,9 +119,7 @@ export function buildEntityHandlerMethods(state, name) {
108
119
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
109
120
  ...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
110
121
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
111
- ...(def.reserveBeforeTransaction && {
112
- reserveBeforeTransaction: def.reserveBeforeTransaction,
113
- }),
122
+ ...transactionPhaseSlots(def),
114
123
  ...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
115
124
  // Forward the pipeline-build closure so boot-validators and
116
125
  // Designer/AI tooling can inspect the step list. Absent on
@@ -1,3 +1,22 @@
1
- import { type ZodType } from "zod";
1
+ import * as z from "zod";
2
2
  export declare const HANDLER_FIELD_REFERENCES_META_KEY = "references";
3
- export declare function handlerFieldReference(schema: ZodType, field: string): string | undefined;
3
+ export type HandlerFieldReferenceMeta = string | {
4
+ readonly entity: string;
5
+ readonly by?: string;
6
+ };
7
+ export type HandlerFieldReference = {
8
+ readonly entity: string;
9
+ readonly by: string | undefined;
10
+ readonly multiple: boolean;
11
+ };
12
+ export type HandlerFieldReferenceReading = {
13
+ readonly kind: "none";
14
+ } | {
15
+ readonly kind: "invalid";
16
+ readonly raw: unknown;
17
+ } | {
18
+ readonly kind: "ok";
19
+ readonly reference: HandlerFieldReference;
20
+ };
21
+ export declare function readHandlerFieldReference(schema: z.ZodType, field: string): HandlerFieldReferenceReading;
22
+ export declare function handlerFieldReference(schema: z.ZodType, field: string): HandlerFieldReference | undefined;
@@ -1,31 +1,69 @@
1
- import { ZodObject } from "zod";
1
+ import * as z from "zod";
2
2
  import { zodDef, zodMeta, zodShapeField } from "../env/_zod-introspect.js";
3
3
  export const HANDLER_FIELD_REFERENCES_META_KEY = "references";
4
4
  const MAX_WRAPPER_DEPTH = 8;
5
- function referencesFromMeta(field) {
5
+ const referenceMetaSchema = z.union([
6
+ z.string().min(1),
7
+ z.object({ entity: z.string().min(1), by: z.string().min(1).optional() }),
8
+ ]);
9
+ function rawReferenceMeta(field) {
6
10
  const meta = zodMeta(field);
7
11
  if (typeof meta !== "object" || meta === null)
8
12
  return undefined;
9
- const value = Reflect.get(meta, HANDLER_FIELD_REFERENCES_META_KEY);
10
- return typeof value === "string" ? value : undefined;
13
+ return Reflect.get(meta, HANDLER_FIELD_REFERENCES_META_KEY);
11
14
  }
12
- // `.optional()` / `.nullable()` / `.default()` after `.meta()` hide the meta
13
- // from the outer type, so every wrapper layer is probed on the way in.
14
- export function handlerFieldReference(schema, field) {
15
+ function unwrapObjectSchema(schema) {
15
16
  let current = schema;
16
17
  for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
17
- if (current instanceof ZodObject)
18
- break;
18
+ if (current instanceof z.ZodObject)
19
+ return current;
19
20
  current = zodDef(current)?.innerType;
20
21
  }
21
- if (!(current instanceof ZodObject))
22
- return undefined;
23
- let fieldSchema = zodShapeField(current, field);
24
- for (let depth = 0; depth < MAX_WRAPPER_DEPTH && fieldSchema !== undefined; depth++) {
25
- const reference = referencesFromMeta(fieldSchema);
26
- if (reference !== undefined)
27
- return reference;
28
- fieldSchema = zodDef(fieldSchema)?.innerType;
22
+ return undefined;
23
+ }
24
+ // `.optional()` / `.nullable()` / `.default()` after `.meta()` hide the meta from the outer
25
+ // type, so every wrapper layer is probed on the way in; array elements are probed too.
26
+ function findRawReferenceMeta(field) {
27
+ let current = field;
28
+ for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
29
+ const raw = rawReferenceMeta(current);
30
+ if (raw !== undefined)
31
+ return raw;
32
+ current = current instanceof z.ZodArray ? zodDef(current)?.element : zodDef(current)?.innerType;
29
33
  }
30
34
  return undefined;
31
35
  }
36
+ function isArrayField(field) {
37
+ let current = field;
38
+ for (let depth = 0; depth < MAX_WRAPPER_DEPTH && current !== undefined; depth++) {
39
+ if (current instanceof z.ZodArray)
40
+ return true;
41
+ current = zodDef(current)?.innerType;
42
+ }
43
+ return false;
44
+ }
45
+ export function readHandlerFieldReference(schema, field) {
46
+ const objectSchema = unwrapObjectSchema(schema);
47
+ const fieldSchema = objectSchema && zodShapeField(objectSchema, field);
48
+ if (fieldSchema === undefined)
49
+ return { kind: "none" };
50
+ const raw = findRawReferenceMeta(fieldSchema);
51
+ if (raw === undefined)
52
+ return { kind: "none" };
53
+ const parsed = referenceMetaSchema.safeParse(raw);
54
+ if (!parsed.success)
55
+ return { kind: "invalid", raw };
56
+ const meta = parsed.data;
57
+ return {
58
+ kind: "ok",
59
+ reference: {
60
+ entity: typeof meta === "string" ? meta : meta.entity,
61
+ by: typeof meta === "string" ? undefined : meta.by,
62
+ multiple: isArrayField(fieldSchema),
63
+ },
64
+ };
65
+ }
66
+ export function handlerFieldReference(schema, field) {
67
+ const reading = readHandlerFieldReference(schema, field);
68
+ return reading.kind === "ok" ? reading.reference : undefined;
69
+ }
@@ -43,7 +43,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
43
43
  export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
44
44
  export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, type ManifestSystemEvent, serializeManifest, } from "./feature-manifest.js";
45
45
  export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
46
- export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, } from "./handler-field-references.js";
46
+ export { HANDLER_FIELD_REFERENCES_META_KEY, type HandlerFieldReference, type HandlerFieldReferenceMeta, handlerFieldReference, } from "./handler-field-references.js";
47
47
  export { resolveName, withResponseData } from "./handler-helpers.js";
48
48
  export { i18nKey } from "./i18n-key.js";
49
49
  export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.js";
@@ -81,7 +81,7 @@ export { assertNotificationVariables, templateVariable, templateVariablesToZod,
81
81
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
82
82
  export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.js";
83
83
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
84
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
84
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
85
85
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
86
86
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
87
87
  export { runValidation } from "./validation.js";
@@ -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
@@ -80,13 +80,20 @@ export const softDeleteCleanupJob = async (_payload, ctx) => {
80
80
  if (isSystemScopedSoftDeleteTable(proj.table, entity))
81
81
  continue;
82
82
  const where = { isDeleted: true, deletedAt: { lt: cutoff }, tenantId };
83
- await purgeExpiredRows(db, proj.source, proj.table, where, ctx);
83
+ await purgeExpiredRows(db, proj.source, proj.table, where, cutoff, ctx);
84
84
  }
85
85
  };
86
- async function purgeExpiredRows(db, source, table, where, ctx) {
86
+ async function purgeExpiredRows(db, source, table, where, cutoff, ctx) {
87
87
  if (source === FILE_REF_ENTITY_NAME) {
88
88
  // The row is the only record of the storageKey, so its bytes go first.
89
- 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
+ });
90
97
  }
91
98
  else {
92
99
  await deleteMany(db, table, where);
@@ -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) {
@@ -13,7 +13,7 @@
13
13
  import { extractTableName } from "../../db/index.js";
14
14
  import { executeRawQuery } from "../../db/queries/raw-sql.js";
15
15
  import { columnSqlNameOf } from "../../db/table-introspection.js";
16
- import { tenantDbRunner } from "../../db/tenant-db-runner.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) {
@@ -54,7 +54,7 @@ defineStep({
54
54
  }
55
55
  const sqlText = `INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
56
56
  `ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
57
- await executeRawQuery(tenantDbRunner(ctx.db), sqlText, params);
57
+ await executeRawQuery(tenantDbWriteRunner(ctx.db, "unsafe projection upsert"), sqlText, params);
58
58
  },
59
59
  });
60
60
  export function buildUnsafeProjectionUpsertStep(args) {
@@ -11,6 +11,9 @@ export declare const templateVariable: {
11
11
  readonly link: (opts?: CommonOptions & {
12
12
  readonly example?: string;
13
13
  }) => TemplateScalarVariableDef;
14
+ readonly richText: (opts?: CommonOptions & {
15
+ readonly example?: string;
16
+ }) => TemplateScalarVariableDef;
14
17
  readonly money: (opts?: CommonOptions & {
15
18
  readonly example?: number | {
16
19
  readonly amount: number;
@@ -10,6 +10,7 @@ export const templateVariable = {
10
10
  type: "link",
11
11
  ...opts,
12
12
  }),
13
+ richText: (opts = {}) => ({ type: "richText", ...opts }),
13
14
  money: (opts = {}) => ({ type: "money", ...opts }),
14
15
  date: (opts = {}) => ({
15
16
  type: "date",
@@ -39,6 +40,8 @@ function scalarToZod(def) {
39
40
  switch (def.type) {
40
41
  case "text":
41
42
  return z.string();
43
+ case "richText":
44
+ return z.string();
42
45
  case "link":
43
46
  return linkSchema;
44
47
  case "money":
@@ -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, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, 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";
@@ -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;