@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
@@ -40,7 +40,8 @@
40
40
  // connected to any API instance via Redis Pub/Sub, no per-instance
41
41
  // cursor needed. Without REDIS_URL, the in-memory broker still only
42
42
  // reaches clients on the same process.
43
- import { buildServer, withFileProviderResolver } from "../api/server.js";
43
+ import { buildServer } from "../api/server.js";
44
+ import { withFileProviderResolver } from "../api/server-wiring.js";
44
45
  import { createJobRunner } from "../jobs/job-runner.js";
45
46
  import { createLifecycle } from "../lifecycle/index.js";
46
47
  import { createNoopProvider } from "../observability/index.js";
@@ -119,6 +120,8 @@ function buildApiServer(opts, lifecycle, dispatcherOverride, jobRunner, processL
119
120
  jwtIssuer: opts.jwtIssuer,
120
121
  auth: opts.auth,
121
122
  trustedProxyHops: opts.trustedProxyHops,
123
+ errorDocs: opts.errorDocs,
124
+ instanceId: opts.instanceId,
122
125
  anonymousAccess: opts.anonymousAccess,
123
126
  sseBroker: opts.sseBroker,
124
127
  rateLimit: opts.rateLimit,
@@ -145,6 +148,7 @@ function buildWorkerServer(opts, lifecycle, jobRunner) {
145
148
  jwtSecret: opts.jwtSecret,
146
149
  lifecycle,
147
150
  processLane: "worker",
151
+ instanceId: opts.instanceId,
148
152
  observability: opts.observability,
149
153
  dispatcherOptions,
150
154
  systemHooks: opts.systemHooks,
@@ -240,6 +244,7 @@ export function createApiEntrypoint(options) {
240
244
  lifecycle,
241
245
  observability: server.observability,
242
246
  dispatcher: server.dispatcher,
247
+ lifecycleHooks: server.lifecycleHooks,
243
248
  ...(server.eventDispatcher && { eventDispatcher: server.eventDispatcher }),
244
249
  mode: "api",
245
250
  async start() {
@@ -319,6 +324,7 @@ export function createAllInOneEntrypoint(options) {
319
324
  jobRunner: workerJobRunner,
320
325
  observability: server.observability,
321
326
  dispatcher: server.dispatcher,
327
+ lifecycleHooks: server.lifecycleHooks,
322
328
  mode: "all-in-one",
323
329
  async start() {
324
330
  await eventDispatcher.start();
@@ -1,5 +1,5 @@
1
1
  import type { KumikoError } from "./kumiko-error.ts";
2
- /** Links an app's own error reasons to its own docs; without it only framework reasons carry a docsUrl. */
2
+ /** Links an app's own error reasons to its own docs; uncovered reasons fall back to the framework page of the error code. */
3
3
  export type ErrorDocsConfig = {
4
4
  readonly baseUrl: string;
5
5
  readonly reasons?: readonly string[] | "all";
@@ -5,6 +5,26 @@ const frameworkReasons = new Set([
5
5
  ...Object.values(FrameworkReasons),
6
6
  ...Object.values(AgentReasons),
7
7
  ]);
8
+ // Codes of the built-in error classes: only these have a framework docs page.
9
+ const frameworkErrorCodes = new Set([
10
+ "validation_error",
11
+ "feature_disabled",
12
+ "access_denied",
13
+ "unauthenticated",
14
+ "not_found",
15
+ "conflict",
16
+ "version_conflict",
17
+ "precondition_failed",
18
+ "record_locked",
19
+ "idempotent_replay",
20
+ "unique_violation",
21
+ "unprocessable",
22
+ "unconfigured",
23
+ "internal_error",
24
+ "member_read_timeout",
25
+ "rate_limited",
26
+ "rate_limit_unavailable",
27
+ ]);
8
28
  function reasonOf(err) {
9
29
  if (!err.details || typeof err.details !== "object")
10
30
  return undefined;
@@ -22,7 +42,7 @@ function errorsUrl(baseUrl, slug) {
22
42
  }
23
43
  // A reason the framework documents (or no reason, or the code itself) links to the framework docs, overridable via
24
44
  // KUMIKO_DOCS_URL. An app's own reason links to the app docs where `errorDocs` covers it; every
25
- // other reason falls back to the framework page of the error code, which exists for each code.
45
+ // other reason falls back to the framework page of the error code, unless an app subclass overrode the code.
26
46
  export function resolveErrorDocsUrl(err, errorDocs) {
27
47
  const reason = reasonOf(err);
28
48
  const frameworkBaseUrl = process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
@@ -32,5 +52,5 @@ export function resolveErrorDocsUrl(err, errorDocs) {
32
52
  if (errorDocs && isCoveredByConfig(reason, errorDocs)) {
33
53
  return errorsUrl(errorDocs.baseUrl, reason);
34
54
  }
35
- return errorsUrl(frameworkBaseUrl, err.code);
55
+ return frameworkErrorCodes.has(err.code) ? errorsUrl(frameworkBaseUrl, err.code) : undefined;
36
56
  }
@@ -43,7 +43,7 @@ export function createSeedMigrationContext(args) {
43
43
  if (!result.isSuccess) {
44
44
  const code = result.error?.code ?? "unknown";
45
45
  const message = result.error?.message ?? "(no message)";
46
- throw new Error(`[es-ops/seed-migration] systemWriteAs("${handlerQualifiedName}") failed: ${code} — ${message}`);
46
+ throw new Error(`systemWriteAs("${handlerQualifiedName}") failed: ${code} — ${message}`);
47
47
  }
48
48
  return result;
49
49
  },
@@ -6,10 +6,10 @@
6
6
  // path `@cosmicdrift/kumiko-framework/event-store/admin-api`. The guard script
7
7
  // `scripts/guard-admin-api.ts` blocks calls from application code. Allowlist: samples/*/migration/,
8
8
  // scripts/migrations/, this definition, and the guard script itself.
9
+ import { instantToTimestamptzParam } from "../db/driver-value-preparation.js";
9
10
  import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
10
11
  import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
11
12
  import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
12
- import { instantToTimestamptzParam } from "../db/query.js";
13
13
  import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
14
14
  function mapEventUniqueViolation(e, event) {
15
15
  if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
@@ -3,9 +3,10 @@ import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-t
3
3
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  import { requestContext } from "../api/request-context.js";
5
5
  import { encryptEventPayloadPii } from "../crypto/event-pii.js";
6
+ import { instantToTimestamptzParam } from "../db/driver-value-preparation.js";
6
7
  import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
7
8
  import { claimXactId, insertFirstEventRow, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
8
- import { instantToTimestamptzParam, selectMany } from "../db/query.js";
9
+ import { selectMany } from "../db/query.js";
9
10
  import { isStreamArchived } from "./archive.js";
10
11
  import { IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
11
12
  import { eventsTable } from "./events-schema.js";
@@ -14,8 +15,11 @@ import { currentSeedCreatedAt } from "./seed-clock.js";
14
15
  // Append one event atomically. Two guarantees combined:
15
16
  //
16
17
  // 1. UNIQUE (tenant_id, aggregate_id, version) serializes concurrent writers
17
- // within a tenant — a second writer racing the same expectedVersion
18
- // receives a PG unique violation (SQLSTATE 23505) → VersionConflictError.
18
+ // within a tenant. A second writer racing the same expectedVersion hits
19
+ // ON CONFLICT DO NOTHING (no row), then the stream version is read inside
20
+ // the still-intact transaction → VersionConflictError. That relies on
21
+ // READ COMMITTED; REPEATABLE READ/SERIALIZABLE raise 40001 instead. Only
22
+ // the idempotency index still raises 23505.
19
23
  // Cross-tenant aggregate_id collisions are not conflicts by definition:
20
24
  // two tenants owning a row with the same UUID is just isolation, not a
21
25
  // race.
@@ -28,7 +32,7 @@ import { currentSeedCreatedAt } from "./seed-clock.js";
28
32
  // stays semantically obvious when read in isolation.
29
33
  //
30
34
  // Creates (expectedVersion === 0) skip the predecessor check — no predecessor
31
- // exists yet. Colliding creates fall out via UNIQUE (tenant_id, aggregate_id, version=1).
35
+ // exists yet. Colliding creates fall out via the same index (tenant_id, aggregate_id, version=1).
32
36
  // Channel name used by append() → NOTIFY and the event-dispatcher → LISTEN
33
37
  // (Sprint E.4). The event-dispatcher subscribes to this channel on start and
34
38
  // fires a runOnce immediately on each commit, so delivery latency is bounded
@@ -66,6 +70,8 @@ export async function append(db, event) {
66
70
  }
67
71
  catch (e) {
68
72
  if (isUniqueViolation(e)) {
73
+ // The version index normally never throws (ON CONFLICT DO NOTHING); it
74
+ // only lands here for an unknown or renamed constraint.
69
75
  // Two unique constraints on this table: events_aggregate_version_uq
70
76
  // (tenant_id, aggregate_id, version) — a concurrent writer won the
71
77
  // race to the next version — and events_idempotency_uq (tenant_id,
@@ -13,6 +13,9 @@ export function fileRefIdsOf(value) {
13
13
  return value.filter((item) => typeof item === "string" && isUuid(item));
14
14
  }
15
15
  function isPendingUploadFor(row, entityName, fieldName, user) {
16
+ // Every anonymous visitor shares one user id, so insertedById cannot tell their uploads apart.
17
+ if (isAnonymousSessionUser(user))
18
+ return false;
16
19
  return (row["entityId"] === null &&
17
20
  row["entityType"] === entityName &&
18
21
  row["fieldName"] === fieldName &&
@@ -79,9 +82,6 @@ export async function bindPendingFileRefs(args) {
79
82
  }
80
83
  async function loadPendingRow(lookup) {
81
84
  const { fileRefId, entityName, fieldName, user, db } = lookup;
82
- // Every anonymous visitor shares one user id, so insertedById cannot tell their uploads apart.
83
- if (isAnonymousSessionUser(user))
84
- return undefined;
85
85
  if (!isUuid(fileRefId))
86
86
  return undefined;
87
87
  const row = (await loadFileRefRows(db, [fileRefId], user.tenantId)).get(fileRefId);
@@ -27,7 +27,10 @@ export async function createFileRefFromBytes(ctx, input) {
27
27
  registry: ctx.registry,
28
28
  defaultMaxUploadSize: readFilesRouteOptions(ctx.registry).maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE,
29
29
  writeBytes: (key, data, mimeType) => files.writeUploadBytes(key, data, mimeType),
30
- deleteBytes: (key) => files.ref(key).delete(),
30
+ deleteBytes: async (key) => {
31
+ await files.ref(key).delete();
32
+ await files.forgetUploadBytes(key);
33
+ },
31
34
  }, {
32
35
  fileName: input.fileName,
33
36
  declaredMimeType: input.mimeType,
@@ -10,7 +10,7 @@
10
10
  // suffix before the file extension — `foo/bar.jpg` + `"medium-<16 hex>"` →
11
11
  // `foo/bar.medium-<16 hex>.jpg`. The suffix must be a `variantSuffix(...)` so
12
12
  // the erasure sweep recognizes it. Stable, reversible, no extra lookup tables.
13
- import { isDerivativeSuffix } from "../derivatives/variant-key.js";
13
+ import { derivativeListPrefix, isDerivativeSuffix } from "../derivatives/variant-key.js";
14
14
  import { InternalError } from "../errors/index.js";
15
15
  import { assertSafeStorageKey } from "./types.js";
16
16
  // `getProvider` is a lazily-resolved, memoized accessor — the provider is
@@ -127,6 +127,5 @@ export function deriveKey(key, suffix) {
127
127
  // fileRef's original plus every already-rendered derivative, none of which have their own
128
128
  // `file_refs` row.
129
129
  export function storageKeyStemPrefix(key) {
130
- const { stem, ext } = splitKeyExtension(key);
131
- return ext === "" ? `${key}.` : `${stem}.`;
130
+ return derivativeListPrefix(key);
132
131
  }
@@ -12,13 +12,13 @@ import { isFileField, isUuid, } from "../engine/types/index.js";
12
12
  import { listActionFormFileFields, mayUseActionForm } from "./action-form-upload.js";
13
13
  import { buildContentDispositionHeader } from "./content-disposition.js";
14
14
  import { DEFAULT_MAX_UPLOAD_SIZE, getFileRefExecutor, storeFileAsFileRef, } from "./create-file-ref.js";
15
- import { createFileContext } from "./file-handle.js";
15
+ import { createFileContext, createTenantFileContext } from "./file-handle.js";
16
16
  import { fileRefEntity } from "./file-ref-entity.js";
17
17
  import { fileRefsTable } from "./file-ref-table.js";
18
18
  import { RecordWriteDecisions } from "./record-write-check.js";
19
19
  import { neutralFileNameForMimeType } from "./resolve-content-type.js";
20
20
  import { parseMaxSize, resolveServedContentType } from "./types.js";
21
- import { createFileUploadLedger } from "./upload-ledger.js";
21
+ import { withUploadLedger } from "./upload-ledger.js";
22
22
  // Exactly the httpStatus values declared by KumikoError subclasses across packages/*.
23
23
  const ERROR_STATUSES = [400, 401, 403, 404, 409, 422, 429, 500, 503, 504];
24
24
  export function deleteFailureResponseFor(error) {
@@ -102,6 +102,10 @@ export function readFilesRouteOptions(registry) {
102
102
  }
103
103
  export function createFileRoutes(options) {
104
104
  const { db } = options;
105
+ // Plain resolvers (not built via withFileProviderResolver) get the ledger on the route's own root connection.
106
+ const resolveProvider = options.resolveProvider.uploadLedgerFor
107
+ ? options.resolveProvider
108
+ : withUploadLedger(options.resolveProvider, db);
105
109
  const privilegedRoles = options.privilegedRoles ?? DEFAULT_PRIVILEGED_ROLES;
106
110
  const guard = options.accessGuard ??
107
111
  createDefaultGuard(privilegedRoles, options.canReadRecord, options.unclaimedUploadReaders ?? []);
@@ -200,19 +204,18 @@ export function createFileRoutes(options) {
200
204
  }
201
205
  }
202
206
  const data = new Uint8Array(await file.arrayBuffer());
203
- const storageProvider = await options.resolveProvider(user.tenantId);
204
- const uploadLedger = createFileUploadLedger(db, user.tenantId);
207
+ const files = createTenantFileContext(resolveProvider, user.tenantId);
205
208
  const outcome = await storeFileAsFileRef({
206
209
  user,
207
210
  tenantDb: tenantDbFor(user.tenantId),
208
211
  rowInOwnTransaction: true,
209
212
  registry: options.registry,
210
213
  defaultMaxUploadSize: options.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE,
211
- writeBytes: async (key, bytes, mimeType) => {
212
- await uploadLedger.record(key);
213
- await storageProvider.write(key, bytes, mimeType);
214
+ writeBytes: (key, bytes, mimeType) => files.writeUploadBytes(key, bytes, mimeType),
215
+ deleteBytes: async (key) => {
216
+ await files.ref(key).delete();
217
+ await files.forgetUploadBytes(key);
214
218
  },
215
- deleteBytes: (key) => storageProvider.delete(key),
216
219
  }, {
217
220
  fileName: file.name,
218
221
  declaredMimeType: file.type,
@@ -46,6 +46,49 @@ const NON_XML_ROOT_ELEMENTS = new Set([
46
46
  "article",
47
47
  "font",
48
48
  "center",
49
+ "form",
50
+ "iframe",
51
+ "input",
52
+ "button",
53
+ "nav",
54
+ "header",
55
+ "footer",
56
+ "main",
57
+ "pre",
58
+ "code",
59
+ "b",
60
+ "i",
61
+ "strong",
62
+ "em",
63
+ "label",
64
+ "textarea",
65
+ "select",
66
+ "fieldset",
67
+ "legend",
68
+ "tr",
69
+ "td",
70
+ "th",
71
+ "thead",
72
+ "tbody",
73
+ "tfoot",
74
+ "object",
75
+ "embed",
76
+ "video",
77
+ "audio",
78
+ "canvas",
79
+ "math",
80
+ "picture",
81
+ "figure",
82
+ "figcaption",
83
+ "blockquote",
84
+ "dl",
85
+ "dt",
86
+ "dd",
87
+ "aside",
88
+ "details",
89
+ "summary",
90
+ "dialog",
91
+ "noscript",
49
92
  ]);
50
93
  const ROOT_ELEMENT_PATTERN = /^<(?:[A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)/;
51
94
  const EXTENSION_FALLBACK = {
@@ -1,6 +1,8 @@
1
1
  import { defineUnmanagedTable } from "../db/entity-table-meta.js";
2
+ import { runnerCanBeginTransaction } from "../db/query.js";
2
3
  import { createTenantDb } from "../db/tenant-db.js";
3
4
  import { tenantDbRunner } from "../db/tenant-db-runner.js";
5
+ import { InternalError } from "../errors/index.js";
4
6
  const CREATED_AT_INDEX = "store_file_upload_ledger_created_at_idx";
5
7
  // One row per upload whose bytes are in storage but whose fileRef row is not committed yet. The
6
8
  // commit path deletes it inside the transaction that creates the fileRef; the sweep finds the
@@ -21,6 +23,11 @@ function isTenantDb(db) {
21
23
  // handler must not take the row with it, or the bytes it uploaded would be unfindable.
22
24
  export function createFileUploadLedger(db, tenantId) {
23
25
  const runner = isTenantDb(db) ? tenantDbRunner(db) : db;
26
+ if (!runnerCanBeginTransaction(runner)) {
27
+ throw new InternalError({
28
+ message: "createFileUploadLedger: the connection is already inside a transaction; the ledger row would roll back with it and the uploaded bytes become unfindable",
29
+ });
30
+ }
24
31
  const ledgerDb = createTenantDb({ db: runner, tenantId });
25
32
  return {
26
33
  record: async (storageKey) => {