@cosmicdrift/kumiko-framework 0.387.0 → 0.388.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 (240) hide show
  1. package/AGENTS.md +6 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/legal-pages.md +1 -1
  9. package/agents/examples/prod-boot-options.md +16 -0
  10. package/agents/examples/rate-limiting.md +48 -0
  11. package/agents/examples/record-detail-layout.md +692 -0
  12. package/agents/examples/seed-migration.md +212 -0
  13. package/agents/examples/wizard-form.md +941 -0
  14. package/agents/examples/writeform-section.md +548 -0
  15. package/agents/features.md +19 -2
  16. package/agents/widgets.md +1 -0
  17. package/dist/api/client-error-route.js +8 -2
  18. package/dist/api/index.d.ts +3 -1
  19. package/dist/api/index.js +3 -1
  20. package/dist/api/request-context.d.ts +1 -1
  21. package/dist/api/request-context.js +1 -1
  22. package/dist/api/server-boot-checks.d.ts +10 -0
  23. package/dist/api/server-boot-checks.js +157 -0
  24. package/dist/api/server-event-dispatcher.d.ts +26 -0
  25. package/dist/api/server-event-dispatcher.js +208 -0
  26. package/dist/api/server-extra-routes.d.ts +11 -0
  27. package/dist/api/server-extra-routes.js +203 -0
  28. package/dist/api/server-middleware.d.ts +20 -0
  29. package/dist/api/server-middleware.js +211 -0
  30. package/dist/api/server-routes.d.ts +19 -0
  31. package/dist/api/server-routes.js +293 -0
  32. package/dist/api/server-websocket-routes.d.ts +4 -0
  33. package/dist/api/server-websocket-routes.js +108 -0
  34. package/dist/api/server-wiring.d.ts +32 -0
  35. package/dist/api/server-wiring.js +236 -0
  36. package/dist/api/server.d.ts +10 -15
  37. package/dist/api/server.js +48 -1251
  38. package/dist/api/session-cookies.js +4 -4
  39. package/dist/api/sse-route.js +16 -12
  40. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  41. package/dist/crypto/owner-transition-rekey.js +1 -1
  42. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  43. package/dist/crypto/pii-field-encryption.js +3 -1
  44. package/dist/db/aggregate-sql.d.ts +21 -0
  45. package/dist/db/aggregate-sql.js +155 -0
  46. package/dist/db/closed-connection.d.ts +9 -0
  47. package/dist/db/closed-connection.js +95 -0
  48. package/dist/db/dialect.js +1 -1
  49. package/dist/db/distinct-on.d.ts +1 -1
  50. package/dist/db/driver-row-coercion.d.ts +2 -0
  51. package/dist/db/driver-row-coercion.js +186 -0
  52. package/dist/db/driver-value-preparation.d.ts +16 -0
  53. package/dist/db/driver-value-preparation.js +99 -0
  54. package/dist/db/entity-field-encryption.d.ts +1 -0
  55. package/dist/db/entity-field-encryption.js +1 -1
  56. package/dist/db/entity-table-meta.js +103 -110
  57. package/dist/db/event-store-executor-context.d.ts +2 -1
  58. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  59. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  60. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  61. package/dist/db/event-store-executor-read-filter.js +203 -0
  62. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  63. package/dist/db/event-store-executor-read-paging.js +99 -0
  64. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  65. package/dist/db/event-store-executor-read-rows.js +32 -0
  66. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  67. package/dist/db/event-store-executor-read-search.js +221 -0
  68. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  69. package/dist/db/event-store-executor-read-sort.js +130 -0
  70. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  71. package/dist/db/event-store-executor-read-where.js +81 -0
  72. package/dist/db/event-store-executor-read.d.ts +2 -9
  73. package/dist/db/event-store-executor-read.js +48 -701
  74. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  75. package/dist/db/event-store-executor-write-steps.js +100 -0
  76. package/dist/db/event-store-executor-write.js +395 -499
  77. package/dist/db/index.d.ts +6 -2
  78. package/dist/db/index.js +5 -1
  79. package/dist/db/list-aggregate-labels.d.ts +14 -0
  80. package/dist/db/list-aggregate-labels.js +26 -0
  81. package/dist/db/migrate-generator.js +71 -86
  82. package/dist/db/migrate-runner.js +4 -1
  83. package/dist/db/query.d.ts +2 -15
  84. package/dist/db/query.js +8 -761
  85. package/dist/db/reference-data.js +6 -1
  86. package/dist/db/table-builder.js +113 -166
  87. package/dist/db/table-info.d.ts +10 -0
  88. package/dist/db/table-info.js +45 -0
  89. package/dist/db/temporal-value-guards.d.ts +6 -0
  90. package/dist/db/temporal-value-guards.js +15 -0
  91. package/dist/db/tenant-db-runner.d.ts +1 -0
  92. package/dist/db/tenant-db-runner.js +3 -0
  93. package/dist/db/tenant-db.js +39 -9
  94. package/dist/db/where-clause.d.ts +6 -0
  95. package/dist/db/where-clause.js +236 -0
  96. package/dist/derivatives/delete-stored-file.js +2 -1
  97. package/dist/engine/boot-validator/boot-check.js +1 -1
  98. package/dist/engine/boot-validator/detail-screens.js +2 -2
  99. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  100. package/dist/engine/boot-validator/entity-handler.js +17 -0
  101. package/dist/engine/boot-validator/form-derivations.js +35 -1
  102. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  103. package/dist/engine/boot-validator/index.js +2 -0
  104. package/dist/engine/boot-validator/pii-retention.js +233 -155
  105. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  106. package/dist/engine/boot-validator/screens-actions.js +8 -1
  107. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  108. package/dist/engine/boot-validator/screens-forms.js +2 -6
  109. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  110. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  111. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  112. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  113. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  114. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  115. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  116. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  117. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  118. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  119. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  120. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  121. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  122. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  123. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens-lists.js +12 -9
  125. package/dist/engine/boot-validator/screens.d.ts +0 -2
  126. package/dist/engine/boot-validator/screens.js +99 -860
  127. package/dist/engine/build-app-schema.js +56 -96
  128. package/dist/engine/define-feature.js +9 -2
  129. package/dist/engine/define-handler.js +1 -0
  130. package/dist/engine/entity-handlers.d.ts +13 -0
  131. package/dist/engine/entity-handlers.js +10 -21
  132. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  133. package/dist/engine/entity-list-runtime-options.js +18 -0
  134. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  135. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  136. package/dist/engine/feature-ast/patch.js +4 -182
  137. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  138. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  139. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  140. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  141. package/dist/engine/feature-ast/patterns.d.ts +9 -0
  142. package/dist/engine/feature-ast/patterns.js +43 -51
  143. package/dist/engine/feature-ast/render.js +48 -87
  144. package/dist/engine/feature-entity-handlers.js +3 -0
  145. package/dist/engine/form-derivations.js +18 -7
  146. package/dist/engine/handler-field-references.js +1 -1
  147. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  148. package/dist/engine/project-app-schema-for-roles.js +6 -4
  149. package/dist/engine/registry-ingest.js +1 -1
  150. package/dist/engine/schema-builder.js +228 -212
  151. package/dist/engine/screen-helpers.js +1 -1
  152. package/dist/engine/screen-kind.d.ts +7 -0
  153. package/dist/engine/screen-kind.js +1 -0
  154. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  155. package/dist/engine/validate-projection-allowlist.js +2 -1
  156. package/dist/entrypoint/index.d.ts +4 -1
  157. package/dist/entrypoint/index.js +7 -1
  158. package/dist/errors/docs-url.d.ts +1 -1
  159. package/dist/errors/docs-url.js +22 -2
  160. package/dist/es-ops/context.js +1 -1
  161. package/dist/event-store/admin-api.js +1 -1
  162. package/dist/event-store/event-store.js +10 -4
  163. package/dist/files/bind-pending-file-refs.js +3 -3
  164. package/dist/files/create-file-ref-from-bytes.js +4 -1
  165. package/dist/files/file-handle.js +2 -3
  166. package/dist/files/file-routes.js +11 -8
  167. package/dist/files/resolve-content-type.js +43 -0
  168. package/dist/files/upload-ledger.js +7 -0
  169. package/dist/i18n/required-surface-keys.js +182 -206
  170. package/dist/internal/stack-support.d.ts +2 -1
  171. package/dist/internal/stack-support.js +1 -1
  172. package/dist/jobs/job-runner-context.d.ts +32 -0
  173. package/dist/jobs/job-runner-context.js +305 -0
  174. package/dist/jobs/job-runner-execution.d.ts +7 -0
  175. package/dist/jobs/job-runner-execution.js +160 -0
  176. package/dist/jobs/job-runner-gates.d.ts +19 -0
  177. package/dist/jobs/job-runner-gates.js +198 -0
  178. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  179. package/dist/jobs/job-runner-helpers.js +61 -2
  180. package/dist/jobs/job-runner-scope.d.ts +44 -0
  181. package/dist/jobs/job-runner-scope.js +102 -0
  182. package/dist/jobs/job-runner-types.d.ts +12 -0
  183. package/dist/jobs/job-runner.d.ts +0 -1
  184. package/dist/jobs/job-runner.js +47 -657
  185. package/dist/observability/escape-hatch-report.d.ts +3 -0
  186. package/dist/observability/escape-hatch-report.js +12 -1
  187. package/dist/pipeline/cascade-handler.js +165 -138
  188. package/dist/pipeline/dispatch-query.js +10 -3
  189. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  190. package/dist/pipeline/dispatch-shared.js +58 -710
  191. package/dist/pipeline/dispatch-write.js +296 -189
  192. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  193. package/dist/pipeline/event-dispatcher-delivery.js +3 -5
  194. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  195. package/dist/pipeline/handler-context-auth.js +54 -0
  196. package/dist/pipeline/handler-context-config.d.ts +14 -0
  197. package/dist/pipeline/handler-context-config.js +113 -0
  198. package/dist/pipeline/handler-context-db.d.ts +24 -0
  199. package/dist/pipeline/handler-context-db.js +149 -0
  200. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  201. package/dist/pipeline/handler-context-dispatch.js +89 -0
  202. package/dist/pipeline/handler-context-events.d.ts +43 -0
  203. package/dist/pipeline/handler-context-events.js +237 -0
  204. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  205. package/dist/pipeline/handler-context-notify.js +55 -0
  206. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  207. package/dist/pipeline/handler-context-observability.js +21 -0
  208. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  209. package/dist/pipeline/handler-context-scope.js +88 -0
  210. package/dist/pipeline/notify-scope.js +12 -1
  211. package/dist/pipeline/pre-handler-gates.js +20 -4
  212. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  213. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  214. package/dist/pipeline/projection-rebuild.js +2 -1
  215. package/dist/schema-cli/apply-command.d.ts +2 -0
  216. package/dist/schema-cli/apply-command.js +97 -0
  217. package/dist/schema-cli/baseline-command.d.ts +2 -0
  218. package/dist/schema-cli/baseline-command.js +33 -0
  219. package/dist/schema-cli/generate-command.d.ts +2 -0
  220. package/dist/schema-cli/generate-command.js +90 -0
  221. package/dist/schema-cli/shared.d.ts +39 -0
  222. package/dist/schema-cli/shared.js +95 -0
  223. package/dist/schema-cli/status-command.d.ts +2 -0
  224. package/dist/schema-cli/status-command.js +73 -0
  225. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  226. package/dist/schema-cli/validate-boot-input.js +130 -0
  227. package/dist/schema-cli/validate-command.d.ts +2 -0
  228. package/dist/schema-cli/validate-command.js +130 -0
  229. package/dist/schema-cli.d.ts +2 -18
  230. package/dist/schema-cli.js +47 -604
  231. package/dist/search/meilisearch-adapter.js +2 -1
  232. package/dist/upgrade-cli.js +149 -121
  233. package/package.json +4 -4
  234. package/src/changes.json +310 -0
  235. package/src/scripts/codemod/feature-layout.ts +10 -2
  236. package/src/scripts/codemod/object-form.ts +41 -4
  237. package/src/scripts/codemod/package-merge.ts +58 -8
  238. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  239. package/src/scripts/codemod/testing-injection.ts +122 -42
  240. package/src/scripts/codemod/testing-move.ts +29 -7
@@ -0,0 +1,237 @@
1
+ import { runInSavepoint, selectMany } from "../db/query.js";
2
+ import { hasTenantColumn } from "../db/tenant-db.js";
3
+ import { InternalError, memberResolutionReadOnlyDenied, VersionConflictError, } from "../errors/index.js";
4
+ import { archiveStream as archiveStreamHelper, isStreamArchived, restoreStream as restoreStreamHelper, } from "../event-store/archive.js";
5
+ import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError, } from "../event-store/errors.js";
6
+ import { loadAggregate, loadAggregateAsOf, loadAggregateForWriting, } from "../event-store/event-store.js";
7
+ import { effectiveSnapshotGeneration, loadAggregateWithSnapshot, saveSnapshot, } from "../event-store/snapshot.js";
8
+ import { upcastStoredEvent, upcastStoredEvents } from "../event-store/upcaster.js";
9
+ import { appendDomainEventCore } from "./append-event-core.js";
10
+ import { isMemberResolutionPrincipal, resolveDbSource, } from "./dispatch-shared.js";
11
+ import { createGatedProjectionReader } from "./system-identity-switch.js";
12
+ function requireDbSource(ctx, tx, surfaceCall) {
13
+ const dbSource = resolveDbSource(ctx, tx);
14
+ if (!dbSource) {
15
+ throw new InternalError({
16
+ message: `${surfaceCall} requires a database connection — none is configured.`,
17
+ });
18
+ }
19
+ return dbSource;
20
+ }
21
+ // ctx.appendEvent — append a domain event onto a specific aggregate stream
22
+ // in the current tx, then fire matching inline projections. Core logic
23
+ // lives in appendDomainEventCore; this wrapper just locates dbSource +
24
+ // stringifies the SessionUser id for the shared helper.
25
+ async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
26
+ // Sink behind every append surface, so a resolved member stays read-only even via fetchForWriting handles.
27
+ if (isMemberResolutionPrincipal(user))
28
+ throw memberResolutionReadOnlyDenied();
29
+ const { registry } = ctx;
30
+ const dbSource = requireDbSource(ctx, tx, `ctx.appendEvent("${args.type}")`);
31
+ await appendDomainEventCore({
32
+ registry,
33
+ db: dbSource,
34
+ tenantId: user.tenantId,
35
+ userId: String(user.id),
36
+ callSiteLabel: "ctx.appendEvent",
37
+ callerFeature,
38
+ }, args);
39
+ }
40
+ // Savepoint-scoped append: catches a losing writer's VersionConflictError
41
+ // without poisoning the rest of the handler's transaction. postgres.js
42
+ // aborts the WHOLE begin() on an uncaught statement error
43
+ // (SQLSTATE 25P02) even if the JS error is caught — runInSavepoint
44
+ // wraps the append in a real SAVEPOINT so only that nested scope rolls
45
+ // back on conflict, and subsequent statements in the outer tx (e.g. a
46
+ // dedup-anchor insert) still succeed. Use when a handler must react to
47
+ // losing a concurrent-append race instead of failing the whole write.
48
+ function buildTryAppendEvent(scope) {
49
+ const { type, user, tx, registry } = scope;
50
+ return async (args) => {
51
+ if (!tx) {
52
+ throw new InternalError({
53
+ message: `ctx.tryAppendEvent("${args.type}") requires an active transaction — no tx is threaded through this call.`,
54
+ });
55
+ }
56
+ try {
57
+ const event = await runInSavepoint(tx, (sp) => appendDomainEventCore({
58
+ registry,
59
+ db: sp,
60
+ tenantId: user.tenantId,
61
+ userId: String(user.id),
62
+ callSiteLabel: "ctx.tryAppendEvent",
63
+ callerFeature: registry.getHandlerFeature(type),
64
+ }, args));
65
+ return { ok: true, event };
66
+ }
67
+ catch (e) {
68
+ if (e instanceof EventStoreVersionConflictError) {
69
+ return { ok: false, conflict: e };
70
+ }
71
+ if (e instanceof EventStoreIdempotentAppendConflictError) {
72
+ return { ok: false, conflict: e };
73
+ }
74
+ throw e;
75
+ }
76
+ };
77
+ }
78
+ function buildFetchForWriting(scope) {
79
+ const { ctx, type, user, tx, registry } = scope;
80
+ return async (args) => {
81
+ const dbSource = requireDbSource(ctx, tx, `ctx.fetchForWriting("${args.aggregateId}")`);
82
+ // Stream-version authoritative (same policy as CRUD executor + Block 0), read together
83
+ // with the events so the pinned version covers exactly what the handler's rules saw.
84
+ const { events: storedEvents, streamVersion: fetchedVersion } = await loadAggregateForWriting(dbSource, args.aggregateId, user.tenantId, { aggregateType: args.aggregateType });
85
+ const events = await upcastStoredEvents(storedEvents, registry.getEventUpcasters(), {
86
+ db: dbSource,
87
+ tenantId: user.tenantId,
88
+ });
89
+ // Optimistic concurrency: if the caller knows the version they
90
+ // worked against (e.g. from a prior read-model row) and the stream
91
+ // has moved on, fail fast before any downstream work.
92
+ if (args.expectedVersion !== undefined && args.expectedVersion !== fetchedVersion) {
93
+ throw new VersionConflictError({
94
+ entityId: args.aggregateId,
95
+ expectedVersion: args.expectedVersion,
96
+ currentVersion: fetchedVersion,
97
+ });
98
+ }
99
+ // Pinned into every append: a write landing between fetch and append
100
+ // fails with VersionConflictError instead of stacking on a state the
101
+ // caller's business rules never saw. Bumps per appendOne so consecutive
102
+ // appends stay in order without re-reading the DB.
103
+ let handleVersion = fetchedVersion;
104
+ const appendOne = async (appendArgs) => {
105
+ await appendDomainEvent(ctx, {
106
+ aggregateId: args.aggregateId,
107
+ aggregateType: args.aggregateType,
108
+ type: appendArgs.type,
109
+ payload: appendArgs.payload,
110
+ expectedVersion: handleVersion,
111
+ }, user, tx, registry.getHandlerFeature(type));
112
+ handleVersion += 1;
113
+ };
114
+ return {
115
+ events,
116
+ get version() {
117
+ return handleVersion;
118
+ },
119
+ appendOne,
120
+ hasEvent: (type) => events.some((e) => e.type === type),
121
+ };
122
+ };
123
+ }
124
+ function buildLoadAggregate(scope) {
125
+ const { ctx, user, tx, registry } = scope;
126
+ return async (aggregateId, loadOptions) => {
127
+ const dbSource = requireDbSource(ctx, tx, `ctx.loadAggregate("${aggregateId}")`);
128
+ const events = loadOptions?.asOf
129
+ ? await loadAggregateAsOf(dbSource, aggregateId, user.tenantId, loadOptions.asOf)
130
+ : await loadAggregate(dbSource, aggregateId, user.tenantId);
131
+ return upcastStoredEvents(events, registry.getEventUpcasters(), {
132
+ db: dbSource,
133
+ tenantId: user.tenantId,
134
+ });
135
+ };
136
+ }
137
+ function buildSnapshotAggregate(scope) {
138
+ const { ctx, user, tx, registry } = scope;
139
+ return async (snapshotArgs) => {
140
+ const dbSource = requireDbSource(ctx, tx, `ctx.snapshotAggregate("${snapshotArgs.aggregateId}")`);
141
+ await saveSnapshot(dbSource, {
142
+ aggregateId: snapshotArgs.aggregateId,
143
+ tenantId: user.tenantId,
144
+ aggregateType: snapshotArgs.aggregateType,
145
+ version: snapshotArgs.version,
146
+ state: snapshotArgs.state,
147
+ snapshotVersion: effectiveSnapshotGeneration(snapshotArgs.snapshotVersion ?? 1, registry.getEntity(snapshotArgs.aggregateType)?.eventVersion),
148
+ });
149
+ };
150
+ }
151
+ function buildLoadAggregateWithSnapshot(scope) {
152
+ const { ctx, user, tx, registry } = scope;
153
+ return async (aggregateId, reducer, initial, loadOptions) => {
154
+ const dbSource = requireDbSource(ctx, tx, `ctx.loadAggregateWithSnapshot("${aggregateId}")`);
155
+ // Upcaster-aware: pass an upcastEvent callback so loadAggregateWithSnapshot
156
+ // walks every delta through the registered chain before invoking the
157
+ // user's (sync) reducer. Async upcasters (DB-enrichment) are awaited
158
+ // inside loadAggregateWithSnapshot — feature authors never see legacy
159
+ // payload shapes regardless of which load path they chose.
160
+ const upcasters = registry.getEventUpcasters();
161
+ const upcastCtx = { db: dbSource, tenantId: user.tenantId };
162
+ return loadAggregateWithSnapshot(dbSource, aggregateId, user.tenantId, reducer, initial, {
163
+ ...loadOptions,
164
+ entityEventVersionOf: (aggregateType) => registry.getEntity(aggregateType)?.eventVersion,
165
+ upcastEvent: (event) => upcastStoredEvent(event, upcasters, upcastCtx), // @wrapper-known semantic-alias
166
+ });
167
+ };
168
+ }
169
+ // queryProjection works against both single-stream and multi-stream
170
+ // projections. MSPs without a table cannot be queried — those are
171
+ // side-effect-only consumers (no state to read back).
172
+ function buildHandlerProjectionReader(scope) {
173
+ const { ctx, type, user, tx, registry, hasIdentitySwitchGrant, identitySwitchAudit } = scope;
174
+ const findProjectionTable = (qualifiedName) => registry.getAllProjections().get(qualifiedName)?.table ??
175
+ registry.getAllMultiStreamProjections().get(qualifiedName)?.table;
176
+ const ungatedQueryProjection = async (qualifiedName, queryOptions) => {
177
+ const projTable = findProjectionTable(qualifiedName);
178
+ if (!projTable) {
179
+ const singleNames = [...registry.getAllProjections().keys()];
180
+ const mspNames = [...registry.getAllMultiStreamProjections().keys()].filter((n) => registry.getAllMultiStreamProjections().get(n)?.table);
181
+ const all = [...singleNames, ...mspNames];
182
+ throw new InternalError({
183
+ message: `ctx.queryProjection("${qualifiedName}") — projection not registered, or it is a ` +
184
+ `table-less MSP (side-effect-only). Known queryable projections: ${all.join(", ") || "(none)"}`,
185
+ });
186
+ }
187
+ const dbSource = requireDbSource(ctx, tx, `ctx.queryProjection("${qualifiedName}")`);
188
+ const where = hasTenantColumn(projTable) && queryOptions?.unsafeAllTenants !== true
189
+ ? { tenantId: user.tenantId }
190
+ : undefined;
191
+ const rows = await selectMany(dbSource, projTable, where);
192
+ return rows; // @cast-boundary engine-payload
193
+ };
194
+ return createGatedProjectionReader(`handler "${type}"`, hasIdentitySwitchGrant, ungatedQueryProjection, identitySwitchAudit, (qualifiedName) => {
195
+ const projTable = findProjectionTable(qualifiedName);
196
+ // Unknown projection: keep the grant check first, the ungated reader reports the real error.
197
+ return projTable === undefined || hasTenantColumn(projTable);
198
+ });
199
+ }
200
+ export function buildEventStoreFacet(scope) {
201
+ const { ctx, type, user, tx, registry } = scope;
202
+ return {
203
+ // Strict + unsafe share the same runtime — only the type-surface
204
+ // differs. The strict signature is what's exposed to typed callers;
205
+ // unsafe is the explicit escape-hatch for runtime-pluggable events.
206
+ appendEvent: (async (args) => {
207
+ await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
208
+ }), // @cast-boundary engine-bridge
209
+ unsafeAppendEvent: async (args) => {
210
+ await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
211
+ },
212
+ tryAppendEvent: buildTryAppendEvent(scope),
213
+ fetchForWriting: buildFetchForWriting(scope),
214
+ loadAggregate: buildLoadAggregate(scope),
215
+ archiveStream: async (aggregateId, archiveArgs) => {
216
+ const dbSource = requireDbSource(ctx, tx, `ctx.archiveStream("${aggregateId}")`);
217
+ await archiveStreamHelper(dbSource, {
218
+ tenantId: user.tenantId,
219
+ aggregateId,
220
+ aggregateType: archiveArgs.aggregateType,
221
+ archivedBy: user.id,
222
+ reason: archiveArgs.reason,
223
+ });
224
+ },
225
+ restoreStream: async (aggregateId) => {
226
+ const dbSource = requireDbSource(ctx, tx, `ctx.restoreStream("${aggregateId}")`);
227
+ await restoreStreamHelper(dbSource, user.tenantId, aggregateId);
228
+ },
229
+ isStreamArchived: async (aggregateId) => {
230
+ const dbSource = requireDbSource(ctx, tx, `ctx.isStreamArchived("${aggregateId}")`);
231
+ return isStreamArchived(dbSource, user.tenantId, aggregateId);
232
+ },
233
+ snapshotAggregate: buildSnapshotAggregate(scope),
234
+ loadAggregateWithSnapshot: buildLoadAggregateWithSnapshot(scope),
235
+ queryProjection: buildHandlerProjectionReader(scope),
236
+ };
237
+ }
@@ -0,0 +1,12 @@
1
+ import type { DbConnection } from "../db/connection.ts";
2
+ import type { TenantId } from "../engine/types/identifiers.ts";
3
+ import type { NotifyFn, SessionUser } from "../engine/types/index.ts";
4
+ import type { CommitSinks } from "./handler-context-dispatch.ts";
5
+ import type { HandlerContextScope } from "./handler-context-scope.ts";
6
+ export declare function buildNotifyFacet(scope: HandlerContextScope, sinks: CommitSinks, outsideTxSource: DbConnection | undefined): {
7
+ notify: NotifyFn | undefined;
8
+ factoryFragment: {
9
+ _notifyInTransactionFactory?: ((notifyUser: SessionUser, notifyTenantId: TenantId) => NotifyFn) | undefined;
10
+ _notifyAfterCommitFactory?: ((notifyUser: SessionUser, notifyTenantId: TenantId) => NotifyFn) | undefined;
11
+ };
12
+ };
@@ -0,0 +1,55 @@
1
+ import { createDeferringNotify } from "./notify-scope.js";
2
+ // DispatchContext.jobRunner is typed as the handleEvent-only JobRunnerRef but
3
+ // holds the concrete runner at boot; narrow instead of casting.
4
+ function isNotifyJobDispatcher(runner) {
5
+ return (typeof runner === "object" &&
6
+ runner !== null &&
7
+ "dispatch" in runner &&
8
+ typeof runner.dispatch === "function");
9
+ }
10
+ function buildNotify(scope, sinks, notifyAfterCommitFactory, notifyInTransactionFactory) {
11
+ const { user, registry } = scope;
12
+ if (!notifyAfterCommitFactory)
13
+ return undefined;
14
+ const notifyAfterCommit = notifyAfterCommitFactory(user, user.tenantId);
15
+ if (!notifyInTransactionFactory)
16
+ return notifyAfterCommit;
17
+ const notifyInTransaction = notifyInTransactionFactory(user, user.tenantId);
18
+ // A rolled-back write must leave no notification behind. Without any commit to wait for (a
19
+ // post-commit context) delivery stays immediate.
20
+ const { notifyCommitSink } = sinks;
21
+ if (notifyCommitSink === undefined)
22
+ return notifyInTransaction;
23
+ return createDeferringNotify({
24
+ registry,
25
+ tenantId: user.tenantId,
26
+ notifyAfterCommit,
27
+ notifyNow: notifyInTransaction,
28
+ scheduleAfterCommit: (hook) => {
29
+ notifyCommitSink.push(hook);
30
+ },
31
+ });
32
+ }
33
+ export function buildNotifyFacet(scope, sinks, outsideTxSource) {
34
+ const { ctx, tx, context } = scope;
35
+ const notifyFactory = context._notifyFactory;
36
+ const notifyJobDispatcher = isNotifyJobDispatcher(ctx.jobRunner) ? ctx.jobRunner : undefined;
37
+ // Inside the write tx the app pool's slots can all be held by waiting write txs, so the
38
+ // delivery's own DB work (broadcast lookup, attempt log, channel sends) runs on the outside-tx pool.
39
+ const notifyInTransactionFactory = notifyFactory && tx !== undefined
40
+ ? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, outsideTxSource, context._fileProviderResolver)
41
+ : undefined;
42
+ const notifyAfterCommitFactory = notifyFactory
43
+ ? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, undefined, context._fileProviderResolver)
44
+ : undefined;
45
+ const notify = buildNotify(scope, sinks, notifyAfterCommitFactory, notifyInTransactionFactory);
46
+ return {
47
+ notify,
48
+ factoryFragment: {
49
+ ...(notifyInTransactionFactory && {
50
+ _notifyInTransactionFactory: notifyInTransactionFactory,
51
+ }),
52
+ ...(notifyAfterCommitFactory && { _notifyAfterCommitFactory: notifyAfterCommitFactory }),
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,7 @@
1
+ import type { HandlerContextScope } from "./handler-context-scope.ts";
2
+ export declare function buildObservabilityFacet(scope: HandlerContextScope): {
3
+ log: import("@cosmicdrift/kumiko-types/logger").Logger | undefined;
4
+ tracer: import("@cosmicdrift/kumiko-types/observability/span").Tracer;
5
+ metrics: import("@cosmicdrift/kumiko-types/observability/metric").MetricsHandle;
6
+ metricsFor: (featureName: string) => import("@cosmicdrift/kumiko-types/observability/metric").MetricsHandle;
7
+ };
@@ -0,0 +1,21 @@
1
+ import { createMetricsForFactory, createMetricsHandle, createNoopMetricsHandle, getFallbackTracer, } from "../observability/index.js";
2
+ export function buildObservabilityFacet(scope) {
3
+ const { type, user, context, reqCtx, featureName } = scope;
4
+ const log = context.log?.child({
5
+ handler: type,
6
+ tenantId: user.tenantId,
7
+ userId: user.id,
8
+ ...(reqCtx && { requestId: reqCtx.requestId }),
9
+ });
10
+ // Observability — feature-bound metrics handle, so ctx.metrics.inc("foo")
11
+ // resolves to kumiko_<feature>_foo. Unknown feature falls back to noop
12
+ // so legacy internal handlers don't crash.
13
+ const tracer = context.tracer ?? getFallbackTracer();
14
+ const meter = context.meter;
15
+ const metrics = meter && featureName ? createMetricsHandle(meter, featureName) : createNoopMetricsHandle();
16
+ // ctx.metricsFor(featureName) — shared/library code binds to a feature
17
+ // name of its own choosing instead of the dispatching handler's.
18
+ // Unregistered names no-op rather than throw.
19
+ const metricsFor = createMetricsForFactory(meter);
20
+ return { log, tracer, metrics, metricsFor };
21
+ }
@@ -0,0 +1,48 @@
1
+ import { type EscapeHatchDeclaration, type EscapeHatchReporter } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { type RequestContextData } from "../api/request-context.ts";
3
+ import type { DbConnection, DbTx } from "../db/connection.ts";
4
+ import type { AppContext, Registry, SessionUser } from "../engine/types/index.ts";
5
+ import { type DispatchContext, type HandlerTransactionScope } from "./dispatch-shared.ts";
6
+ import type { WriteOrigin } from "./write-origin.ts";
7
+ export type HandlerContextScope = {
8
+ readonly ctx: DispatchContext;
9
+ readonly type: string;
10
+ readonly user: SessionUser;
11
+ readonly origin: WriteOrigin;
12
+ readonly tx: DbTx | undefined;
13
+ readonly transactionScope: HandlerTransactionScope;
14
+ readonly registry: Registry;
15
+ readonly context: AppContext;
16
+ readonly isSystem: boolean;
17
+ readonly dbSource: DbConnection | DbTx | undefined;
18
+ readonly reqCtx: RequestContextData | undefined;
19
+ readonly writeEscapeHatch: EscapeHatchDeclaration | undefined;
20
+ readonly handlerEscapeHatch: EscapeHatchDeclaration | undefined;
21
+ readonly isWriteHandler: boolean;
22
+ readonly isPerCommand: boolean;
23
+ readonly denyDbWrites: boolean;
24
+ readonly featureName: string | undefined;
25
+ readonly hasIdentitySwitchGrant: boolean;
26
+ readonly reportEscapeHatch: EscapeHatchReporter;
27
+ readonly identitySwitchAudit: {
28
+ readonly reason: string | undefined;
29
+ readonly report: EscapeHatchReporter;
30
+ };
31
+ };
32
+ export declare function resolveHandlerContextScope(input: {
33
+ readonly ctx: DispatchContext;
34
+ readonly type: string;
35
+ readonly user: SessionUser;
36
+ readonly origin: WriteOrigin;
37
+ readonly tx: DbTx | undefined;
38
+ readonly transactionScope: HandlerTransactionScope;
39
+ }): HandlerContextScope;
40
+ export declare function buildPreSaveFragment(scope: HandlerContextScope): {
41
+ runPreSave?: ((handlerName: string, changes: Record<string, unknown>, previous: Readonly<Record<string, unknown>>, isNew: boolean) => Promise<Record<string, unknown>>) | undefined;
42
+ };
43
+ export declare function buildRuntimeFragment(scope: HandlerContextScope): {
44
+ signal?: AbortSignal | undefined;
45
+ effectiveFeatures?: import("@cosmicdrift/kumiko-framework/engine").EffectiveFeaturesResolver | undefined;
46
+ cacheSync?: import("@cosmicdrift/kumiko-types/cache-sync-types").CacheSyncBus | undefined;
47
+ jobRunner?: import("@cosmicdrift/kumiko-types/handlers").JobRunnerRef | undefined;
48
+ };
@@ -0,0 +1,88 @@
1
+ import { escapeHatchFor, } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { requestContext } from "../api/request-context.js";
3
+ import { createFallbackLogger } from "../logging/utils.js";
4
+ import { createEscapeHatchReporter } from "../observability/index.js";
5
+ import { resolveDbSource, } from "./dispatch-shared.js";
6
+ export function resolveHandlerContextScope(input) {
7
+ const { ctx, type, user, origin, tx, transactionScope } = input;
8
+ const { registry, appContext: context } = ctx;
9
+ const isSystem = registry.isHandlerSystemScoped(type);
10
+ // The outer dispatcher receives a DbConnection from the server/stack;
11
+ // AppContext's `db` union also allows TenantDb (for downstream hook calls),
12
+ // but at this point we're the root of the pipeline — cast is safe.
13
+ const dbSource = resolveDbSource(ctx, tx);
14
+ const reqCtx = requestContext.get();
15
+ // global() writes are write-handler-only; SYSTEM identity switch and unsafeRaw accept write, query or stream escapeHatch.
16
+ const writeHandler = registry.getWriteHandler(type);
17
+ const writeEscapeHatch = writeHandler?.escapeHatch;
18
+ const isPerCommand = writeHandler?.transaction === "perCommand";
19
+ const denyDbWrites = transactionScope === "withoutTransaction";
20
+ const handlerEscapeHatch = writeEscapeHatch ??
21
+ registry.getQueryHandler(type)?.escapeHatch ??
22
+ registry.getStreamHandler(type)?.escapeHatch;
23
+ const identitySwitchEscapeHatch = escapeHatchFor(handlerEscapeHatch, "systemIdentity");
24
+ const hasIdentitySwitchGrant = isSystem || identitySwitchEscapeHatch !== undefined;
25
+ const featureName = registry.getHandlerFeature(type);
26
+ const reportEscapeHatch = createEscapeHatchReporter({
27
+ handler: type,
28
+ tenantId: user.tenantId,
29
+ actor: user.id,
30
+ sink: context._escapeHatchAuditSink,
31
+ log: context.log ?? createFallbackLogger("dispatcher"),
32
+ window: ctx.escapeHatchReportWindow,
33
+ meter: context.meter,
34
+ });
35
+ const identitySwitchGrantReason = identitySwitchEscapeHatch?.reason ??
36
+ (isSystem ? `r.systemScope() feature "${featureName ?? "unknown"}"` : undefined);
37
+ return {
38
+ ctx,
39
+ type,
40
+ user,
41
+ origin,
42
+ tx,
43
+ transactionScope,
44
+ registry,
45
+ context,
46
+ isSystem,
47
+ dbSource,
48
+ reqCtx,
49
+ writeEscapeHatch,
50
+ handlerEscapeHatch,
51
+ isWriteHandler: writeHandler !== undefined,
52
+ isPerCommand,
53
+ denyDbWrites,
54
+ featureName,
55
+ hasIdentitySwitchGrant,
56
+ reportEscapeHatch,
57
+ identitySwitchAudit: { reason: identitySwitchGrantReason, report: reportEscapeHatch },
58
+ };
59
+ }
60
+ // preSave hooks need `changes`/`previous`/`isNew`, which only exist once
61
+ // a handler actually starts building its write — bound here so entity
62
+ // CRUD handlers (entity-handlers.ts) can forward it to the executor.
63
+ export function buildPreSaveFragment(scope) {
64
+ const { lifecycle } = scope.ctx;
65
+ return {
66
+ ...(lifecycle && {
67
+ runPreSave: (handlerName, changes, previous, isNew) => lifecycle.runPreSave(handlerName, changes, previous, isNew, scope.context),
68
+ }),
69
+ };
70
+ }
71
+ export function buildRuntimeFragment(scope) {
72
+ const { ctx, reqCtx } = scope;
73
+ const { effectiveFeatures, jobRunner } = ctx;
74
+ return {
75
+ // Cancellation signal flows from the HTTP middleware via
76
+ // requestContext. Conditional spread so non-HTTP entry-points
77
+ // (jobs, dispatcher MSP-applies) don't get a phantom signal that
78
+ // would always read aborted=false but feel meaningful.
79
+ ...(reqCtx?.signal ? { signal: reqCtx.signal } : {}),
80
+ // Propagate the feature-toggle resolver so the lifecycle pipeline,
81
+ // MSP runner, and ctx.hasFeature all pull from the same source.
82
+ ...(effectiveFeatures && { effectiveFeatures }),
83
+ ...(ctx.cacheSync && { cacheSync: ctx.cacheSync }),
84
+ // Lets write handlers call ctx.jobRunner.dispatch(...) directly, same
85
+ // as a follow-up job would (test-stack.ts wires the matching runner).
86
+ ...(jobRunner && { jobRunner }),
87
+ };
88
+ }
@@ -1,5 +1,6 @@
1
1
  import { assertNotificationVariables } from "../engine/template-variables.js";
2
2
  import { AccessDeniedError, FrameworkReasons } from "../errors/index.js";
3
+ import { parseJsonOrThrow } from "../utils/safe-json.js";
3
4
  function isTenantBroadcast(to) {
4
5
  return typeof to === "object" && to !== null && !Array.isArray(to) && "tenant" in to;
5
6
  }
@@ -12,6 +13,16 @@ export function assertNotifyTargetsOwnTenant(options, tenantId) {
12
13
  });
13
14
  }
14
15
  }
16
+ // Temporal values, functions and class instances in `data` are not structured-cloneable; the
17
+ // job pipeline serializes them to JSON on enqueue anyway, so the JSON form is the faithful snapshot.
18
+ function snapshotNotifyOptions(options) {
19
+ try {
20
+ return structuredClone(options);
21
+ }
22
+ catch {
23
+ return parseJsonOrThrow(JSON.stringify(options), "notify options snapshot");
24
+ }
25
+ }
15
26
  // Without `immediate`, a notify inside a write transaction waits for the commit so a
16
27
  // rolled-back write leaves no notification behind.
17
28
  export function createDeferringNotify(deps) {
@@ -24,7 +35,7 @@ export function createDeferringNotify(deps) {
24
35
  if (options.immediate === true)
25
36
  return notifyNow(notificationType, options);
26
37
  // The hook runs after the handler returned; a caller reusing `data` meanwhile must not change what is sent.
27
- const optionsAtCallTime = structuredClone(options);
38
+ const optionsAtCallTime = snapshotNotifyOptions(options);
28
39
  scheduleAfterCommit(async () => {
29
40
  await notifyAfterCommit(notificationType, optionsAtCallTime);
30
41
  });
@@ -2,16 +2,17 @@ import { hasAccess } from "../engine/access.js";
2
2
  import { tenantOverrideDenied } from "../engine/cross-tenant.js";
3
3
  import { checkWriteFieldRoles } from "../engine/field-access.js";
4
4
  import { runValidation } from "../engine/validation.js";
5
- import { AccessDeniedError, FrameworkReasons, isKumikoError, ValidationError, validationErrorFromZod, writeFailure, } from "../errors/index.js";
5
+ import { AccessDeniedError, FrameworkReasons, InternalError, isKumikoError, ValidationError, validationErrorFromZod, writeFailure, } from "../errors/index.js";
6
6
  import { checkFeatureEnabled, enforcePayloadRateLimits, enforceRateLimit, } from "./dispatch-shared.js";
7
7
  import { handlerAccessError } from "./handler-access-error.js";
8
8
  async function rejectDisabledFeature(ctx, type, user) {
9
9
  const disabledErr = await checkFeatureEnabled(ctx, type, user.tenantId);
10
10
  return disabledErr ? writeFailure(disabledErr) : undefined;
11
11
  }
12
- async function rejectRateLimited(ctx, handler, type, user) {
12
+ async function rejectRateLimited(ctx, handler, type, user, resolveCostOverride) {
13
13
  try {
14
- await enforceRateLimit(ctx, handler.rateLimit, type, user, ctx.registry.isHandlerSystemScoped(type));
14
+ const costOverride = resolveCostOverride?.();
15
+ await enforceRateLimit(ctx, handler.rateLimit, type, user, ctx.registry.isHandlerSystemScoped(type), costOverride);
15
16
  }
16
17
  catch (e) {
17
18
  if (isKumikoError(e))
@@ -20,6 +21,14 @@ async function rejectRateLimited(ctx, handler, type, user) {
20
21
  }
21
22
  return undefined;
22
23
  }
24
+ function normalizeRateLimitCost(handlerName, cost) {
25
+ if (!Number.isFinite(cost)) {
26
+ throw new InternalError({
27
+ message: `Handler "${handlerName}" rateLimitCost returned ${cost}; expected a finite number.`,
28
+ });
29
+ }
30
+ return Math.max(1, Math.ceil(cost));
31
+ }
23
32
  async function rejectPayloadRateLimited(ctx, handler, type, user, payload) {
24
33
  try {
25
34
  await enforcePayloadRateLimits(ctx, handler.additionalRateLimits, type, user, payload);
@@ -70,7 +79,7 @@ async function rejectBeforeParse(ctx, handler, type, user, mode) {
70
79
  const disabled = await rejectDisabledFeature(ctx, type, user);
71
80
  if (disabled)
72
81
  return disabled;
73
- if (mode === "charge") {
82
+ if (mode === "charge" && !handler.rateLimitCost) {
74
83
  const rateLimited = await rejectRateLimited(ctx, handler, type, user);
75
84
  if (rateLimited)
76
85
  return rateLimited;
@@ -85,6 +94,13 @@ async function rejectAfterParse(ctx, handler, type, user, payload, mode) {
85
94
  const overrideDenied = tenantOverrideDenied(user, payload);
86
95
  if (overrideDenied)
87
96
  return writeFailure(overrideDenied);
97
+ if (mode === "charge" && handler.rateLimitCost) {
98
+ // Cost comes from the validated payload, so this bucket is billed after the parse instead of before it.
99
+ const { rateLimitCost } = handler;
100
+ const rateLimited = await rejectRateLimited(ctx, handler, type, user, () => normalizeRateLimitCost(type, rateLimitCost(payload)));
101
+ if (rateLimited)
102
+ return rateLimited;
103
+ }
88
104
  if (mode === "charge") {
89
105
  const rateLimited = await rejectPayloadRateLimited(ctx, handler, type, user, payload);
90
106
  if (rateLimited)
@@ -7,6 +7,7 @@ type ReservationConfirm = ReservationHandle["confirmInTransaction"];
7
7
  export type ConsumedReservation = {
8
8
  readonly confirmInTransaction: ReservationConfirm | undefined;
9
9
  readonly prepared: unknown;
10
+ readonly parsedPayload: unknown;
10
11
  };
11
12
  export declare function hasPreTransactionPhase(handler: WriteHandlerDef): boolean;
12
13
  export declare function runAsCommand<T>(reserved: {
@@ -62,10 +62,10 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
62
62
  return { isSuccess: false, error: gates.error, failedIndex: index };
63
63
  }
64
64
  const origin = effectiveWriteOrigin(rootWriteOrigin(ctx.registry, command.type, user), inheritedOrigin);
65
- const handlerContext = await buildHandlerContext(ctx, command.type, user, origin);
66
65
  const event = { type: command.type, payload: gates.payload, user };
67
66
  let confirmInTransaction;
68
67
  if (handler.reserveBeforeTransaction) {
68
+ const handlerContext = await buildHandlerContext(ctx, command.type, user, origin);
69
69
  const handle = toReservationHandle(await handler.reserveBeforeTransaction(event, denyTransactionBoundSurfaces(handlerContext)));
70
70
  if (handle)
71
71
  releases.push(handle.release);
@@ -73,7 +73,9 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
73
73
  }
74
74
  let prepared;
75
75
  if (handler.beforeTransaction) {
76
- const phaseContext = await buildHandlerContext(ctx, command.type, user, origin, undefined, undefined, undefined, undefined, undefined, true);
76
+ const phaseContext = await buildHandlerContext(ctx, command.type, user, origin, {
77
+ readOnlyForTenant: true,
78
+ });
77
79
  const phase = await handler.beforeTransaction(event, applyBeforeTransactionReadOnly(phaseContext));
78
80
  if (!phase.isSuccess) {
79
81
  await releaseAll(ctx, releases);
@@ -83,7 +85,11 @@ export async function reserveBeforeTransaction(ctx, commands, user, inheritedOri
83
85
  releases.push(phase.release);
84
86
  prepared = phase.data;
85
87
  }
86
- reservedCommands.set(index, { confirmInTransaction, prepared });
88
+ reservedCommands.set(index, {
89
+ confirmInTransaction,
90
+ prepared,
91
+ parsedPayload: gates.payload,
92
+ });
87
93
  }
88
94
  catch (error) {
89
95
  await releaseAll(ctx, releases);
@@ -1,7 +1,8 @@
1
1
  import { countSubscribedEvents, finalizeProjectionRebuild, markProjectionRebuildFailed, markProjectionRebuilding, selectEventsForProjectionRebuildBatch, } from "../db/queries/projection-rebuild.js";
2
2
  import { assertLiveColumnsMatchMeta, assertLiveTableHasNoRowLevelSecurity, assertNoBlindIndexLoss, assertNoUnreachableLiveRows, buildShadowTable, countColumnDrift, ensureRebuildSchema, fenceLiveTable, rebuildMetaOrThrow, swapShadowIntoLive, } from "../db/queries/shadow-swap.js";
3
- import { coerceRow, extractTableInfo, runInSavepoint, selectMany } from "../db/query.js";
3
+ import { coerceRow, runInSavepoint, selectMany } from "../db/query.js";
4
4
  import { withoutStatementTimeout } from "../db/statement-timeout.js";
5
+ import { extractTableInfo } from "../db/table-info.js";
5
6
  import { withExecutorManagedWrites } from "../db/tenant-db.js";
6
7
  import { eventsTable, getEventsHighWaterMark, upcastStoredEvent, } from "../event-store/index.js";
7
8
  import { createRebuildDeadLetterTable, recordRebuildDeadLetters, } from "../event-store/rebuild-dead-letter.js";
@@ -0,0 +1,2 @@
1
+ import { type SchemaCliContext } from "./shared.ts";
2
+ export declare function runApplyCommand(ctx: SchemaCliContext): Promise<number>;