@cosmicdrift/kumiko-framework 0.367.0 → 0.369.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 +1 -7
  2. package/README.md +1 -1
  3. package/agents/examples/access-control.md +2 -2
  4. package/agents/examples/anonymous-access-multitenant.md +2 -2
  5. package/agents/examples/anonymous-access.md +3 -3
  6. package/agents/examples/apex-landing.md +1 -1
  7. package/agents/examples/apex-surface-auth.md +6 -7
  8. package/agents/examples/api-worker-split.md +7 -8
  9. package/agents/examples/auth-claims.md +2 -1
  10. package/agents/examples/auth-foundation-providers.md +3 -3
  11. package/agents/examples/auth-mfa.md +3 -4
  12. package/agents/examples/basic-entity.md +3 -4
  13. package/agents/examples/chat-channels.md +4 -4
  14. package/agents/examples/compliance-profiles-demo.md +2 -3
  15. package/agents/examples/cross-feature-events.md +4 -4
  16. package/agents/examples/crypto-shredding-hr.md +14 -9
  17. package/agents/examples/currencies-global.md +3 -4
  18. package/agents/examples/currencies-per-tenant.md +3 -4
  19. package/agents/examples/custom-fields-basic.md +2 -3
  20. package/agents/examples/custom-handlers.md +3 -5
  21. package/agents/examples/dashboard-stats.md +2 -3
  22. package/agents/examples/delivery-notifications.md +5 -6
  23. package/agents/examples/e2e-generator.md +1 -1
  24. package/agents/examples/embedded-entity-form.md +2 -3
  25. package/agents/examples/embedded.md +2 -3
  26. package/agents/examples/encrypted-tenant-config.md +5 -6
  27. package/agents/examples/error-contract.md +3 -4
  28. package/agents/examples/event-sourcing.md +19 -23
  29. package/agents/examples/feature-toggles.md +4 -4
  30. package/agents/examples/field-access.md +3 -4
  31. package/agents/examples/files-post-processing.md +12 -13
  32. package/agents/examples/golden-path.md +208 -0
  33. package/agents/examples/idempotency.md +2 -2
  34. package/agents/examples/inbound-mail-triage.md +7 -7
  35. package/agents/examples/lane-routing.md +3 -3
  36. package/agents/examples/ledger-banking.md +2 -3
  37. package/agents/examples/ledger-invoicing.md +2 -3
  38. package/agents/examples/legal-pages.md +1 -1
  39. package/agents/examples/lifecycle-hooks.md +3 -3
  40. package/agents/examples/managed-config.md +3 -4
  41. package/agents/examples/managed-pages.md +2 -2
  42. package/agents/examples/notes-history-basic.md +4 -5
  43. package/agents/examples/ownership.md +3 -5
  44. package/agents/examples/pdf-audit-export.md +2 -3
  45. package/agents/examples/pipeline-basics.md +2 -2
  46. package/agents/examples/public-share-token.md +3 -3
  47. package/agents/examples/rate-limiting.md +2 -1
  48. package/agents/examples/realtime-sse.md +2 -3
  49. package/agents/examples/record-detail-layout.md +1 -1
  50. package/agents/examples/reference-data.md +3 -3
  51. package/agents/examples/relations.md +2 -2
  52. package/agents/examples/screens-nav.md +1 -1
  53. package/agents/examples/search.md +2 -2
  54. package/agents/examples/secrets-demo.md +8 -8
  55. package/agents/examples/security-baseline.md +2 -2
  56. package/agents/examples/session-revocation.md +7 -8
  57. package/agents/examples/state-machine.md +181 -165
  58. package/agents/examples/tags-basic.md +2 -3
  59. package/agents/examples/tenant-isolation.md +2 -3
  60. package/agents/examples/tenant-settings.md +2 -2
  61. package/agents/examples/tier-admin.md +3 -4
  62. package/agents/examples/timezones.md +3 -3
  63. package/agents/examples/user-data-rights.md +12 -8
  64. package/agents/examples/user-profile.md +3 -3
  65. package/agents/examples/webhook-step.md +2 -3
  66. package/agents/examples/wizard-form.md +4 -5
  67. package/agents/examples/workflow-engine.md +7 -3
  68. package/agents/examples/writeform-section.md +1 -1
  69. package/agents/examples.md +1 -0
  70. package/agents/features.md +4 -4
  71. package/dist/consumer-cli.js +2 -3
  72. package/dist/crypto/index.d.ts +1 -1
  73. package/dist/crypto/index.js +1 -1
  74. package/dist/crypto/owner-transition-rekey.js +2 -0
  75. package/dist/crypto/pg-kms-adapter.js +1 -1
  76. package/dist/crypto/subject-resolver.d.ts +2 -0
  77. package/dist/crypto/subject-resolver.js +30 -15
  78. package/dist/db/bun-db-deprecated.d.ts +10 -0
  79. package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
  80. package/dist/db/config-seed.js +1 -1
  81. package/dist/db/connection.d.ts +23 -5
  82. package/dist/db/connection.js +10 -5
  83. package/dist/db/dialect.js +1 -1
  84. package/dist/db/entity-field-encryption.d.ts +5 -0
  85. package/dist/db/entity-field-encryption.js +29 -0
  86. package/dist/db/entity-table-meta.js +6 -5
  87. package/dist/db/event-store-executor-read.js +26 -5
  88. package/dist/db/index.d.ts +4 -4
  89. package/dist/db/index.js +1 -1
  90. package/dist/db/migrate-runner.js +1 -1
  91. package/dist/db/queries/backfill-pii.js +16 -9
  92. package/dist/db/queries/ddl.js +1 -1
  93. package/dist/db/queries/event-store.d.ts +2 -0
  94. package/dist/db/queries/event-store.js +26 -0
  95. package/dist/db/queries/test-stack.js +1 -1
  96. package/dist/db/query.d.ts +105 -1
  97. package/dist/db/query.js +1450 -3
  98. package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
  99. package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
  100. package/dist/db/tenant-db.d.ts +11 -0
  101. package/dist/db/tenant-db.js +51 -12
  102. package/dist/derivatives/derivatives-context.d.ts +1 -1
  103. package/dist/derivatives/derivatives-context.js +1 -1
  104. package/dist/engine/boot-validator/gdpr-storage.js +3 -3
  105. package/dist/engine/boot-validator/pii-retention.js +3 -3
  106. package/dist/engine/boot-validator/record-owned.js +3 -3
  107. package/dist/engine/boot-validator/screens.js +37 -1
  108. package/dist/engine/bridge-stub.d.ts +4 -0
  109. package/dist/engine/bridge-stub.js +62 -0
  110. package/dist/engine/build-config-feature-schema.js +197 -65
  111. package/dist/engine/event-helpers.d.ts +2 -1
  112. package/dist/engine/event-helpers.js +1 -0
  113. package/dist/engine/extension-selector-plugins.d.ts +3 -0
  114. package/dist/engine/extension-selector-plugins.js +14 -0
  115. package/dist/engine/feature-config-events-jobs.d.ts +2 -0
  116. package/dist/engine/feature-config-events-jobs.js +3 -2
  117. package/dist/engine/index.d.ts +1 -0
  118. package/dist/engine/index.js +1 -0
  119. package/dist/es-ops/operations-schema.js +1 -1
  120. package/dist/event-store/admin-api.js +1 -1
  121. package/dist/event-store/archive.js +1 -1
  122. package/dist/event-store/errors.d.ts +2 -1
  123. package/dist/event-store/errors.js +6 -2
  124. package/dist/event-store/event-store.js +17 -9
  125. package/dist/event-store/events-schema.js +1 -1
  126. package/dist/event-store/rebuild-dead-letter.js +3 -3
  127. package/dist/event-store/snapshot.js +1 -1
  128. package/dist/event-store/upcaster-dead-letter.js +3 -3
  129. package/dist/files/create-file-ref-from-bytes.js +1 -1
  130. package/dist/files/file-routes.js +1 -1
  131. package/dist/i18n/settings-hub-keys.js +1 -2
  132. package/dist/internal/stack-support.d.ts +12 -0
  133. package/dist/internal/stack-support.js +12 -0
  134. package/dist/internal/testing.d.ts +4 -0
  135. package/dist/internal/testing.js +5 -0
  136. package/dist/jobs/job-runner.js +23 -11
  137. package/dist/migrations/pending-rebuilds.js +1 -1
  138. package/dist/migrations/pii-event-backfill.js +2 -2
  139. package/dist/pipeline/dispatch-shared.js +43 -23
  140. package/dist/pipeline/dispatch-write.js +6 -2
  141. package/dist/pipeline/dispatcher-utils.js +6 -2
  142. package/dist/pipeline/event-consumer-state.js +1 -1
  143. package/dist/pipeline/projection-rebuild.d.ts +1 -0
  144. package/dist/pipeline/projection-rebuild.js +1 -1
  145. package/dist/pipeline/projection-state.js +1 -1
  146. package/dist/pipeline/system-hooks.js +1 -1
  147. package/dist/schema-cli.js +3 -3
  148. package/dist/search/purge-subject.js +20 -10
  149. package/dist/seeding/index.d.ts +1 -0
  150. package/dist/seeding/index.js +1 -0
  151. package/dist/stack/index.d.ts +10 -9
  152. package/dist/stack/index.js +11 -19
  153. package/dist/testing/e2e-generator.d.ts +5 -49
  154. package/dist/testing/e2e-generator.js +5 -382
  155. package/dist/testing/handler-context.d.ts +5 -4
  156. package/dist/testing/handler-context.js +5 -69
  157. package/dist/testing/index.d.ts +10 -26
  158. package/dist/testing/index.js +10 -34
  159. package/dist/testing/real-providers.d.ts +5 -3
  160. package/dist/testing/real-providers.js +5 -21
  161. package/dist/upgrade-cli.js +6 -4
  162. package/package.json +37 -9
  163. package/src/changes.json +64 -0
  164. package/src/scripts/codemod/README.md +43 -0
  165. package/src/scripts/codemod/codemod-support.ts +276 -0
  166. package/src/scripts/codemod/db-factory.ts +810 -0
  167. package/src/scripts/codemod/feature-factory.ts +429 -0
  168. package/src/scripts/codemod/s3-imports.ts +274 -0
  169. package/src/scripts/codemod/tenant-db-options.ts +454 -0
  170. package/src/scripts/codemod/testing-move-targets.ts +161 -0
  171. package/src/scripts/codemod/testing-move.ts +520 -0
  172. package/dist/bun-db/connection.d.ts +0 -17
  173. package/dist/bun-db/connection.js +0 -40
  174. package/dist/bun-db/index.d.ts +0 -4
  175. package/dist/bun-db/query.d.ts +0 -106
  176. package/dist/bun-db/query.js +0 -1457
  177. package/dist/db/api.d.ts +0 -19
  178. package/dist/db/api.js +0 -23
  179. package/dist/db/bun-provider.d.ts +0 -2
  180. package/dist/db/bun-provider.js +0 -28
  181. package/dist/db/postgres-provider.d.ts +0 -2
  182. package/dist/db/postgres-provider.js +0 -21
  183. package/dist/db/query-api.d.ts +0 -2
  184. package/dist/db/query-api.js +0 -6
  185. package/dist/stack/db.d.ts +0 -22
  186. package/dist/stack/db.js +0 -67
  187. package/dist/stack/drain-event-consumers.d.ts +0 -4
  188. package/dist/stack/drain-event-consumers.js +0 -51
  189. package/dist/stack/drain-jobs.d.ts +0 -22
  190. package/dist/stack/drain-jobs.js +0 -141
  191. package/dist/stack/event-collector.d.ts +0 -9
  192. package/dist/stack/event-collector.js +0 -12
  193. package/dist/stack/push-entity-projection-tables.d.ts +0 -24
  194. package/dist/stack/push-entity-projection-tables.js +0 -74
  195. package/dist/stack/redis.d.ts +0 -17
  196. package/dist/stack/redis.js +0 -52
  197. package/dist/stack/request-helper.d.ts +0 -35
  198. package/dist/stack/request-helper.js +0 -144
  199. package/dist/stack/test-stack.d.ts +0 -192
  200. package/dist/stack/test-stack.js +0 -429
  201. package/dist/testing/access-assertions.d.ts +0 -2
  202. package/dist/testing/access-assertions.js +0 -13
  203. package/dist/testing/assertions.d.ts +0 -10
  204. package/dist/testing/assertions.js +0 -22
  205. package/dist/testing/boot-validator-fixture.d.ts +0 -3
  206. package/dist/testing/boot-validator-fixture.js +0 -101
  207. package/dist/testing/closed-connection-error.d.ts +0 -7
  208. package/dist/testing/closed-connection-error.js +0 -46
  209. package/dist/testing/db-cleanup.d.ts +0 -9
  210. package/dist/testing/db-cleanup.js +0 -31
  211. package/dist/testing/expect-error.d.ts +0 -2
  212. package/dist/testing/expect-error.js +0 -22
  213. package/dist/testing/file-provider-contract.d.ts +0 -2
  214. package/dist/testing/file-provider-contract.js +0 -102
  215. package/dist/testing/http-cookies.d.ts +0 -7
  216. package/dist/testing/http-cookies.js +0 -47
  217. package/dist/testing/late-bound.d.ts +0 -9
  218. package/dist/testing/late-bound.js +0 -31
  219. package/dist/testing/manual-clock.d.ts +0 -14
  220. package/dist/testing/manual-clock.js +0 -12
  221. package/dist/testing/multipart-helper.d.ts +0 -22
  222. package/dist/testing/multipart-helper.js +0 -77
  223. package/dist/testing/mutable-master-key-provider.d.ts +0 -7
  224. package/dist/testing/mutable-master-key-provider.js +0 -35
  225. package/dist/testing/observability-recorder.d.ts +0 -9
  226. package/dist/testing/observability-recorder.js +0 -29
  227. package/dist/testing/seed.d.ts +0 -9
  228. package/dist/testing/seed.js +0 -21
  229. package/dist/testing/shared-entities.d.ts +0 -286
  230. package/dist/testing/shared-entities.js +0 -56
  231. package/dist/testing/utils.d.ts +0 -1
  232. package/dist/testing/utils.js +0 -1
  233. package/dist/testing/wait-for.d.ts +0 -20
  234. package/dist/testing/wait-for.js +0 -47
  235. package/dist/testing/without-ambient-temporal.d.ts +0 -5
  236. package/dist/testing/without-ambient-temporal.js +0 -17
  237. package/dist/testing/zip-fixture.d.ts +0 -1
  238. package/dist/testing/zip-fixture.js +0 -33
  239. /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
  240. /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
@@ -446,7 +446,7 @@ function writePartialMarker(out, targetDir, pending, manualEntries, failedVersio
446
446
  });
447
447
  out.err(` ⚠ ${ran.length} codemod(s) already applied; wrote marker at ${version} so a re-run resumes at the failed one.`);
448
448
  }
449
- // Runs every pending breaking entry's codemod, oldest version first (so a
449
+ // Runs every pending entry's codemod, oldest version first (so a
450
450
  // later codemod can assume an earlier one already ran). Stops on the first
451
451
  // failure; codemods that already ran are kept in a partial marker. Writes the
452
452
  // marker whenever dryRun is false —
@@ -474,7 +474,9 @@ markerVersion, previous) {
474
474
  return 0;
475
475
  }
476
476
  const breaking = pending.filter((e) => e.type === "breaking");
477
- const codemodEntries = breaking
477
+ // A codemod is worth running whatever the entry's type: an improvement that
478
+ // moves a module ships one too, and skipping it leaves the deprecated path in place.
479
+ const codemodEntries = pending
478
480
  .filter(hasCodemod)
479
481
  .sort((a, b) => compareVersions(a.version, b.version));
480
482
  const manualEntries = breaking.filter(needsManualMigration);
@@ -485,7 +487,7 @@ markerVersion, previous) {
485
487
  }
486
488
  if (codemodEntries.length === 0) {
487
489
  out.log(breaking.length > 0
488
- ? " No automatable codemods among the pending breaking changes."
490
+ ? " No automatable codemods among the pending changes."
489
491
  : " ✓ No breaking changes pending.");
490
492
  if (!dryRun) {
491
493
  const carried = carryOverManual(previous, manualEntries.map(toMarkerManual));
@@ -636,7 +638,7 @@ kumiko-upgrade --resolve <id|version>[,<id|version>...] --reason "<text>" [--not
636
638
  --dir <path> target directory to check/apply against (default: cwd)
637
639
  --json machine-readable output
638
640
  --verbose include full migration details for each pending entry
639
- --apply run codemods for pending breaking changes and write the upgrade marker
641
+ --apply run codemods for pending changes and write the upgrade marker
640
642
  (one run moves the marker to the installed version; manual migrations stay listed as open)
641
643
  --resolve <refs> mark open manual migrations as done; ref = full id or a version with exactly one open entry
642
644
  --reason <text> required with --resolve, max 500 characters
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.367.0",
3
+ "version": "0.369.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -27,7 +27,10 @@
27
27
  "runtime": "runtime",
28
28
  "bunOnlyExports": [
29
29
  "./engine/codemod",
30
- "./testing"
30
+ "./stack",
31
+ "./testing",
32
+ "./testing/e2e-generator",
33
+ "./testing/real-providers"
31
34
  ]
32
35
  },
33
36
  "exports": {
@@ -64,8 +67,8 @@
64
67
  "default": "./dist/errors/index.js"
65
68
  },
66
69
  "./bun-db": {
67
- "types": "./dist/bun-db/index.d.ts",
68
- "default": "./dist/bun-db/index.js"
70
+ "types": "./dist/db/bun-db-deprecated.d.ts",
71
+ "default": "./dist/db/bun-db-deprecated.js"
69
72
  },
70
73
  "./db": {
71
74
  "types": "./dist/db/index.d.ts",
@@ -191,6 +194,14 @@
191
194
  "types": "./dist/api/request-context.d.ts",
192
195
  "default": "./dist/api/request-context.js"
193
196
  },
197
+ "./internal/stack-support": {
198
+ "types": "./dist/internal/stack-support.d.ts",
199
+ "default": "./dist/internal/stack-support.js"
200
+ },
201
+ "./internal/testing": {
202
+ "types": "./dist/internal/testing.d.ts",
203
+ "default": "./dist/internal/testing.js"
204
+ },
194
205
  "./testing/e2e-generator": {
195
206
  "types": "./dist/testing/e2e-generator.d.ts",
196
207
  "default": "./dist/testing/e2e-generator.js"
@@ -210,8 +221,8 @@
210
221
  "./package.json": "./package.json"
211
222
  },
212
223
  "dependencies": {
213
- "@cosmicdrift/kumiko-http": "0.367.0",
214
- "@cosmicdrift/kumiko-types": "0.367.0",
224
+ "@cosmicdrift/kumiko-http": "0.369.0",
225
+ "@cosmicdrift/kumiko-types": "0.369.0",
215
226
  "bullmq": "^5.76.7",
216
227
  "bun-types": "^1.3.13",
217
228
  "hono": "^4.13.1",
@@ -227,10 +238,19 @@
227
238
  "zod": "^4.4.3"
228
239
  },
229
240
  "devDependencies": {
230
- "@cosmicdrift/kumiko-dispatcher-live": "0.367.0",
241
+ "@cosmicdrift/kumiko-dispatcher-live": "0.369.0",
242
+ "@cosmicdrift/kumiko-testing": "0.369.0",
231
243
  "bun-types": "^1.3.13",
232
244
  "pino-pretty": "^13.1.3"
233
245
  },
246
+ "peerDependencies": {
247
+ "@cosmicdrift/kumiko-testing": ">=0.366.0 <1.0.0"
248
+ },
249
+ "peerDependenciesMeta": {
250
+ "@cosmicdrift/kumiko-testing": {
251
+ "optional": true
252
+ }
253
+ },
234
254
  "publishConfig": {
235
255
  "registry": "https://registry.npmjs.org",
236
256
  "access": "public",
@@ -268,8 +288,8 @@
268
288
  "default": "./dist/errors/index.js"
269
289
  },
270
290
  "./bun-db": {
271
- "types": "./dist/bun-db/index.d.ts",
272
- "default": "./dist/bun-db/index.js"
291
+ "types": "./dist/db/bun-db-deprecated.d.ts",
292
+ "default": "./dist/db/bun-db-deprecated.js"
273
293
  },
274
294
  "./db": {
275
295
  "types": "./dist/db/index.d.ts",
@@ -395,6 +415,14 @@
395
415
  "types": "./dist/api/request-context.d.ts",
396
416
  "default": "./dist/api/request-context.js"
397
417
  },
418
+ "./internal/stack-support": {
419
+ "types": "./dist/internal/stack-support.d.ts",
420
+ "default": "./dist/internal/stack-support.js"
421
+ },
422
+ "./internal/testing": {
423
+ "types": "./dist/internal/testing.d.ts",
424
+ "default": "./dist/internal/testing.js"
425
+ },
398
426
  "./testing/e2e-generator": {
399
427
  "types": "./dist/testing/e2e-generator.d.ts",
400
428
  "default": "./dist/testing/e2e-generator.js"
package/src/changes.json CHANGED
@@ -1,4 +1,68 @@
1
1
  [
2
+ {
3
+ "version": "0.369.0",
4
+ "type": "fix",
5
+ "title": "kumiko-upgrade --apply runs codemods of improvement entries too",
6
+ "detail": "The apply step only ran codemods attached to breaking entries. The 0.368.0 entry that moves the test stack to kumiko-testing is an improvement with a codemod (testing-move), so `bun kumiko-upgrade --apply` skipped it and left the deprecated framework paths in place. Every pending entry with a codemod now runs, in version order; manual-migration reporting is unchanged. Apps whose marker is already at 0.368.0 no longer see that entry as pending, so they run `bun kumiko-upgrade --apply --from 0.367.0` once, or the codemod directly: `bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/testing-move.ts . --write`."
7
+ },
8
+ {
9
+ "version": "0.368.0",
10
+ "type": "fix",
11
+ "title": "Append version conflicts carry currentVersion and no longer poison the transaction",
12
+ "detail": "append() inserts with ON CONFLICT (tenant_id, aggregate_id, version) DO NOTHING and reads the stream version in the still-intact transaction. VersionConflictError gets an optional currentVersion, and custom writes (ctx.appendEvent, stream.append, appendEventInTenantDb) now answer 409 with the winner's version instead of -1, so a client can rebase. A missing predecessor reports the real stream version too.\n\nBehaviour change: a lost append race no longer raises a unique violation, so the outer transaction is not aborted (25P02). A handler that catches the VersionConflictError keeps writing and commits. Idempotency-key collisions still throw IdempotentAppendConflictError. Relies on READ COMMITTED, the Postgres default."
13
+ },
14
+ {
15
+ "version": "0.368.0",
16
+ "type": "breaking",
17
+ "title": "createDbConnection({ url, ... }) is the only connection factory, query helpers move from /bun-db to /db",
18
+ "detail": "postgres-js is the only DB driver. createDbConnection takes one options object, is synchronous and accepts the new options ssl and onNotice. The Bun.SQL provider, createConnection, createPgConnection and DB_PROVIDER are removed. The query helpers (selectMany, insertOne, transaction, asRawClient, ...) keep their API and are now exported from @cosmicdrift/kumiko-framework/db. The `tx` handed to the transaction() callback is typed as DbTx (the postgres-js transaction handle it always was at runtime) instead of the Bun.SQL instance type; a helper that takes `db: DbConnection` and receives that tx needs `DbRunner` or `DbTx`.\n\nThe positional form createDbConnection(url, options) and the @cosmicdrift/kumiko-framework/bun-db subpath keep working as deprecated aliases and are removed in the first release after 2026-10-29.\n\nold -> new:\n createConnection(url, options) (async) -> createDbConnection({ url, ...options }) (sync)\n createPgConnection(url, options) -> createDbConnection({ url, ...options })\n createDbConnection(url, options) -> createDbConnection({ url, ...options }) (positional form is deprecated)\n @cosmicdrift/kumiko-framework/bun-db -> @cosmicdrift/kumiko-framework/db (deprecated alias until the first release after 2026-10-29)\n\nRemoved without replacement: createBunDbConnection, bunDbConnectionOptionsFromEnv, the BunDbConnection, BunDbTx, BunDbRunner and BunDbConnectionOptions types, DbPoolHandle (use DbConnectionHandle), the PgListenClient re-export from /bun-db (import it from @cosmicdrift/kumiko-types/db-connection) and the DB_PROVIDER environment variable.",
19
+ "migration": "Run `bun kumiko-upgrade --apply`. It rewrites bun-db imports to /db, createDbConnection(url, options) and createConnection/createPgConnection calls to the object form, and reports leftovers (removed Bun.SQL exports, DB_PROVIDER) with file and line. Remove DB_PROVIDER from your env files.",
20
+ "codemod": "scripts/codemod/db-factory.ts"
21
+ },
22
+ {
23
+ "version": "0.368.0",
24
+ "type": "breaking",
25
+ "title": "Bundled feature constants become create*Feature() factories",
26
+ "detail": "Every bundled feature that was mounted as a constant (tagsFeature, ledgerFeature, authFoundationFeature, ...) now has a create<Name>Feature() factory exported from the same subpath, and apps mount `createTagsFeature()` instead of `tagsFeature`. Factories of features without options take no arguments; the ones that already had options keep them. Two calls of a factory give interchangeable instances, so mounting the same feature from two stacks does not trip the \"mounted twice as distinct instances\" check.\n\nThe old constants keep working as deprecated aliases and are removed in the first release after 2026-10-29.\n\nold -> new:\n tagsFeature -> createTagsFeature()\n ledgerFeature -> createLedgerFeature()\n authFoundationFeature -> createAuthFoundationFeature()\n fooBarFeature -> createFooBarFeature() (same rule for every bundled feature constant)\n typeof tagsFeature -> ReturnType<typeof createTagsFeature>",
27
+ "migration": "Run `bun kumiko-upgrade --apply`. It renames the imports and replaces every use with the factory call. It reports aliased imports, re-exports, namespace access, dynamic imports and files that compare a feature by identity with file and line; handle those by hand (one factory call per mount, or keep one shared instance).",
28
+ "codemod": "scripts/codemod/feature-factory.ts"
29
+ },
30
+ {
31
+ "version": "0.368.0",
32
+ "type": "fix",
33
+ "title": "List filters can no longer probe secret, encrypted or personal fields",
34
+ "detail": "A `filter`/`filters` entry on a field with `find: \"secret\"`, `encrypted: true` or a personal-data annotation made hit count and `totalCount` a value oracle (a prefix oracle for legacy plaintext rows before the backfill). The list handler now answers 400 `validation_error` with a field issue `field_not_filterable` (path `filter.<field>`). The one exception is `op: \"eq\"` with a string on a personal field with `find: \"exact\"` or `\"fuzzy\"`, which matches through the blind index. A sort on such a field falls back to id order instead of failing. Unknown fields are still dropped and fields without read access still match nothing; both checks run first.\n\nThe entityList boot validator rejects a screen `filter` the handler would reject.\n\nBehaviour change: clients that filter on such a field (or send `in`, range or `ne` on a blind-indexed one) now get a 4xx instead of an empty or matching result."
35
+ },
36
+ {
37
+ "version": "0.368.0",
38
+ "type": "improvement",
39
+ "title": "whenAbsent \"self\" keys ownerless personal fields on the record instead of the tenant",
40
+ "detail": "A personal field with a nullable owner can now declare `personal: { of: \"ownerId\", whenAbsent: \"self\" }` (entity fields and `defineEvent` piiFields). While the owner is empty, the value is encrypted under `record:<entity>:<id>`, the same subject as `personal: \"self\"` on events and `personal: { of: \"id\" }` on entities. `forget-subject` on that record then erases the value, its blind index and its search document; with `\"tenant\"` the value stayed readable until the tenant was destroyed. A row can mix subjects: the ciphertext names its key inline, so owner-keyed, tenant-keyed and record-keyed fields decrypt side by side.\n\nThe boot validator accepts \"self\" next to \"tenant\" and rejects it on `idType: \"serial\"` entities, because a forget-subject request needs a UUID record id. Owner transitions behave as for \"tenant\": when an update assigns an owner (absent to X) the value moves from the record key to the user key; the created event stays record-keyed. The data-retention hardDelete also erases the record key of an ownerless \"self\" row; a row with an owner keeps its owner key.",
41
+ "migration": "Switching an existing field from `whenAbsent: \"tenant\"` to `\"self\"` changes only new writes. Events and rows written earlier stay tenant-keyed (still readable) until the PII event backfill re-keys them. The backfill fingerprint is bumped (`owner-rekey-v2`), so the boot-time backfill runs one more full pass: tenant-keyed values of rows without an owner move under their record key, tenant-keyed values of rows with an owner still move under that owner. The pass rewrites event payloads in place through the existing backfill path and queues the affected projections for rebuild. Apps that run the backfill manually call `backfillEventPiiEncryption` with `resolveOwnerFromProjection: true, rekeyTenantKeyedOwnerFields: true`, then rebuild the projections. Values of a record forgotten earlier become `[[erased]]`. The re-key covers entity lifecycle events only: custom `defineEvent` piiFields that switch from `whenAbsent: \"tenant\"` to `\"self\"` keep their old events tenant-keyed.\n\nOld events are never reinterpreted at read time: an old created event of a row that got an owner later keeps its record key until a forget of that record, which erases it, while the later owner-keyed copies stay readable."
42
+ },
43
+ {
44
+ "version": "0.368.0",
45
+ "type": "breaking",
46
+ "title": "createTenantDb({ db, tenantId, ... }) takes one options object",
47
+ "detail": "createTenantDb takes one CreateTenantDbOptions object ({ db, tenantId, mode?, tracer?, meter?, signal?, grants? }); mode still defaults to \"tenant\". The positional form createTenantDb(db, tenantId, mode, tracer, meter, signal, grants) keeps working as a deprecated overload and is removed in the first release after 2026-10-29.\n\nold -> new:\n createTenantDb(db, tenantId) -> createTenantDb({ db, tenantId })\n createTenantDb(db, tenantId, \"system\", tracer, meter, signal, grants) -> createTenantDb({ db, tenantId, mode: \"system\", tracer, meter, signal, grants })",
48
+ "migration": "Run `bun kumiko-upgrade --apply`. It rewrites positional calls (literal undefined arguments are dropped) and reports spread arguments, calls with more than seven arguments and uses of createTenantDb as a value (callback, .bind/.call/.apply) with file and line.",
49
+ "codemod": "scripts/codemod/tenant-db-options.ts"
50
+ },
51
+ {
52
+ "version": "0.368.0",
53
+ "type": "breaking",
54
+ "title": "HandlerContext and AppContext no longer expose _userId and _handlerType",
55
+ "detail": "The dispatcher no longer sets _userId and _handlerType on the context of write handlers, query handlers and hooks. The caller identity is ctx.user.id. requireSecretsContext reads ctx._userId first and falls back to ctx.user.id, so passing a handler context to it keeps the same audit identity. Provider contexts (FileProviderContext, MailTransportContext, InboundMailContext) keep their own _userId. JobContext._userId is unchanged.",
56
+ "migration": "Replace ctx._userId with ctx.user.id in write/query handlers and hooks. Known sites: one file each in money-horse, offlot-app, phronexsis and kumiko-enterprise. _handlerType had no readers."
57
+ },
58
+ {
59
+ "version": "0.368.0",
60
+ "type": "improvement",
61
+ "title": "Test stack and helpers moved to kumiko-testing, old framework paths are deprecated",
62
+ "detail": "Everything a test needs now lives in @cosmicdrift/kumiko-testing. Its core (stack, helpers, preload, bunfig, integration-runner) depends only on kumiko-framework and kumiko-types; bundled-features and dev-server are optional peers used by the root entry and /e2e. Install kumiko-testing as a dev dependency where tests import it.\n\nThe old framework paths re-export the new ones and are removed in the first release after 2026-10-29. Production symbols that tests also use stay in the framework.\n\nold -> new:\n @cosmicdrift/kumiko-framework/stack -> @cosmicdrift/kumiko-testing/stack\n TestUsers, testTenantId, testUserId, createTestUser from /stack -> @cosmicdrift/kumiko-framework/seeding\n @cosmicdrift/kumiko-framework/testing -> @cosmicdrift/kumiko-testing/helpers\n bridgeStub from /testing and /testing/handler-context -> @cosmicdrift/kumiko-framework/engine\n @cosmicdrift/kumiko-framework/testing/real-providers -> @cosmicdrift/kumiko-testing/helpers/real-providers\n @cosmicdrift/kumiko-framework/testing/e2e-generator -> @cosmicdrift/kumiko-testing/helpers/e2e-generator",
63
+ "migration": "Run `bun kumiko-upgrade --apply`. It rewrites imports from the old paths and splits mixed imports by target. Add @cosmicdrift/kumiko-testing to devDependencies where it is missing.",
64
+ "codemod": "scripts/codemod/testing-move.ts"
65
+ },
2
66
  {
3
67
  "version": "0.367.0",
4
68
  "type": "fix",
@@ -57,3 +57,46 @@ Runs automatically via `bun kumiko-upgrade --apply`. Without a mode flag it writ
57
57
  formats the changed files with the Biome binary found in `node_modules/.bin` (`--no-format`
58
58
  skips that). `--dry-run` only reports; `--check` reports and exits 1 while any rewritable
59
59
  or manual site is left.
60
+
61
+ ## db-factory.ts
62
+
63
+ Moves consumers to the single DB connection factory and the relocated query helpers.
64
+ `@cosmicdrift/kumiko-framework/bun-db` imports (including `import type`, `export … from` and
65
+ `import("…")` types) are repointed to `@cosmicdrift/kumiko-framework/db`, merged into an existing
66
+ `/db` import of the same kind. `createDbConnection(url, options)` becomes
67
+ `createDbConnection({ url, ...options })`; `createConnection`/`createPgConnection` from the
68
+ framework become `createDbConnection` in the object form, and the now-pointless `await` is dropped.
69
+ Idempotent. Local functions named `createConnection` are left alone.
70
+
71
+ Reported as `manual` instead of guessed at: imports of `createBunDbConnection`,
72
+ `bunDbConnectionOptionsFromEnv`, `BunDbConnection`, `BunDbTx`, `BunDbRunner`,
73
+ `BunDbConnectionOptions` and `DbPoolHandle` (removed without replacement), uses of `DB_PROVIDER`,
74
+ and `createConnection` results that are not awaited directly.
75
+
76
+ ```bash
77
+ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/db-factory.ts <dir> [--check | --dry-run | --write] [--no-format]
78
+ ```
79
+
80
+ Runs automatically via `bun kumiko-upgrade --apply`. Without a mode flag it writes, then formats the
81
+ changed files with Biome. `--check` exits 1 while any rewritable or manual site is left.
82
+
83
+ ## tenant-db-options.ts
84
+
85
+ Moves `createTenantDb(db, tenantId, mode, tracer, meter, signal, grants)` to the options object
86
+ `createTenantDb({ db, tenantId, mode, tracer, meter, signal, grants })`. A call is only touched when
87
+ the callee resolves (import, aliased import, namespace import or `await import(…)` destructuring) to
88
+ the framework export; a local function with the same name is left alone. Arguments that are the
89
+ identifier of the same name become shorthand, literal `undefined` arguments after `tenantId` are
90
+ dropped, an explicit `mode` is kept, comments between the arguments are preserved. Idempotent —
91
+ calls already passing one object literal are left alone.
92
+
93
+ Reported as `manual` instead of guessed at: spread arguments, more than seven arguments, a single
94
+ argument that is not an object literal, and `createTenantDb` used as a value (callback, assignment,
95
+ `.bind`/`.call`/`.apply`).
96
+
97
+ ```bash
98
+ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/tenant-db-options.ts <dir> [--check | --dry-run | --write] [--no-format]
99
+ ```
100
+
101
+ Runs automatically via `bun kumiko-upgrade --apply`. Without a mode flag it writes, then formats the
102
+ changed files with Biome. `--check` exits 1 while any rewritable or manual site is left.
@@ -0,0 +1,276 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import { type ImportDeclaration, type ImportSpecifier, Node, type SourceFile } from "ts-morph";
4
+ import { collectSourceFiles } from "./collect-source-files.js";
5
+
6
+ export interface CodemodSite {
7
+ readonly file: string;
8
+ readonly line: number;
9
+ readonly description: string;
10
+ }
11
+
12
+ export interface CodemodResult {
13
+ readonly output: string;
14
+ readonly rewrites: CodemodSite[];
15
+ readonly manual: CodemodSite[];
16
+ }
17
+
18
+ export type SiteLists = { rewrites: CodemodSite[]; manual: CodemodSite[] };
19
+
20
+ export const SCRIPT_EXTENSION = /\.(js|ts|tsx)$/;
21
+
22
+ // A relative specifier below `<root><marker>...` is a module of that package's own source tree.
23
+ export function sourceRelativeModule(
24
+ specifier: string,
25
+ filePath: string,
26
+ sourceMarker: string,
27
+ ): { readonly root: string; readonly module: string; readonly extension: string } | undefined {
28
+ if (!specifier.startsWith(".")) return undefined;
29
+ const resolved = resolve(dirname(filePath), specifier);
30
+ const extension = SCRIPT_EXTENSION.exec(resolved)?.[0] ?? "";
31
+ const stripped = resolved.slice(0, resolved.length - extension.length);
32
+ const markerIndex = stripped.lastIndexOf(sourceMarker);
33
+ if (markerIndex === -1) return undefined;
34
+ return {
35
+ root: stripped.slice(0, markerIndex + sourceMarker.length),
36
+ module: stripped.slice(markerIndex + sourceMarker.length),
37
+ extension,
38
+ };
39
+ }
40
+
41
+ export function relativeSpecifier(
42
+ fromFile: string,
43
+ root: string,
44
+ module: string,
45
+ extension: string,
46
+ ): string {
47
+ const path = relative(dirname(fromFile), join(root, module)).split("\\").join("/");
48
+ return `${path.startsWith(".") ? path : `./${path}`}${extension}`;
49
+ }
50
+
51
+ export function isPlainNamedImport(declaration: Node): boolean {
52
+ return (
53
+ Node.isImportDeclaration(declaration) &&
54
+ declaration.getNamespaceImport() === undefined &&
55
+ declaration.getDefaultImport() === undefined
56
+ );
57
+ }
58
+
59
+ export function hasImportBinding(sourceFile: SourceFile, localName: string): boolean {
60
+ return sourceFile
61
+ .getImportDeclarations()
62
+ .some(
63
+ (declaration) =>
64
+ declaration.getDefaultImport()?.getText() === localName ||
65
+ declaration.getNamespaceImport()?.getText() === localName ||
66
+ declaration
67
+ .getNamedImports()
68
+ .some((named) => (named.getAliasNode()?.getText() ?? named.getName()) === localName),
69
+ );
70
+ }
71
+
72
+ // remove() would drop leading comments with the declaration (e.g. a file-level lint marker that
73
+ // has to stay attached to the next statement), so only the declaration's own line goes.
74
+ function removeDeclarationLine(
75
+ sourceFile: SourceFile,
76
+ declaration: { getStart(): number; getEnd(): number },
77
+ ): void {
78
+ const text = sourceFile.getFullText();
79
+ const end = declaration.getEnd();
80
+ const lineEnd = text[end] === "\n" ? end + 1 : end;
81
+ sourceFile.replaceWithText(text.slice(0, declaration.getStart()) + text.slice(lineEnd));
82
+ }
83
+
84
+ export function removeNamedImport(
85
+ sourceFile: SourceFile,
86
+ declaration: ImportDeclaration,
87
+ named: ImportSpecifier,
88
+ ): void {
89
+ if (declaration.getNamedImports().length > 1) {
90
+ named.remove();
91
+ } else {
92
+ removeDeclarationLine(sourceFile, declaration);
93
+ }
94
+ }
95
+
96
+ export function ensureNamedImport(
97
+ sourceFile: SourceFile,
98
+ specifier: string,
99
+ name: string,
100
+ isTypeOnly: boolean,
101
+ ): void {
102
+ const declarations = sourceFile.getImportDeclarations();
103
+ const target = declarations.find(
104
+ (declaration) =>
105
+ declaration.getModuleSpecifierValue() === specifier &&
106
+ declaration.isTypeOnly() === isTypeOnly &&
107
+ isPlainNamedImport(declaration) &&
108
+ declaration.getNamedImports().length > 0,
109
+ );
110
+ if (target === undefined) {
111
+ sourceFile.insertImportDeclaration(declarations.length, {
112
+ moduleSpecifier: specifier,
113
+ namedImports: [name],
114
+ isTypeOnly,
115
+ });
116
+ } else if (
117
+ !target.getNamedImports().some((named) => named.getName() === name && !named.getAliasNode())
118
+ ) {
119
+ target.addNamedImport(name);
120
+ }
121
+ }
122
+
123
+ function findBiomeBinary(startDir: string): string | undefined {
124
+ let dir = resolve(startDir);
125
+ for (;;) {
126
+ const candidate = join(dir, "node_modules", ".bin", "biome");
127
+ if (existsSync(candidate)) return candidate;
128
+ const parent = dirname(dir);
129
+ if (parent === dir) return undefined;
130
+ dir = parent;
131
+ }
132
+ }
133
+
134
+ const FORMAT_BATCH_SIZE = 50;
135
+
136
+ async function formatWithBiome(
137
+ codemodName: string,
138
+ files: readonly string[],
139
+ cwd: string,
140
+ ): Promise<void> {
141
+ const biome = findBiomeBinary(cwd);
142
+ if (biome === undefined) {
143
+ console.log(
144
+ `[${codemodName}] biome not found in node_modules/.bin — format and sort imports of the changed files yourself`,
145
+ );
146
+ // skip: formatting is a convenience, the rewrite itself is already on disk
147
+ return;
148
+ }
149
+ for (let i = 0; i < files.length; i += FORMAT_BATCH_SIZE) {
150
+ const batch = files.slice(i, i + FORMAT_BATCH_SIZE);
151
+ const proc = Bun.spawn({
152
+ cmd: [
153
+ biome,
154
+ "check",
155
+ "--write",
156
+ "--linter-enabled=false",
157
+ "--no-errors-on-unmatched",
158
+ ...batch,
159
+ ],
160
+ cwd,
161
+ stdout: "pipe",
162
+ stderr: "pipe",
163
+ });
164
+ const [stderr, exitCode] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
165
+ if (exitCode !== 0) {
166
+ console.log(
167
+ `[${codemodName}] biome exited ${exitCode}; run it on the changed files yourself\n${stderr.trim()}`,
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ type Mode = "write" | "dry-run" | "check";
174
+
175
+ interface CliOptions {
176
+ readonly mode: Mode;
177
+ readonly format: boolean;
178
+ readonly paths: string[];
179
+ }
180
+
181
+ class CliError extends Error {}
182
+
183
+ function parseArgs(argv: readonly string[]): CliOptions {
184
+ let mode: Mode | undefined;
185
+ let format = true;
186
+ const paths: string[] = [];
187
+ const setMode = (next: Mode): void => {
188
+ if (mode !== undefined && mode !== next) {
189
+ throw new CliError("--check, --dry-run and --write are mutually exclusive");
190
+ }
191
+ mode = next;
192
+ };
193
+ for (const arg of argv) {
194
+ if (arg === "--check") setMode("check");
195
+ else if (arg === "--dry-run") setMode("dry-run");
196
+ else if (arg === "--write") setMode("write");
197
+ else if (arg === "--no-format") format = false;
198
+ else if (arg.startsWith("--")) throw new CliError(`unknown flag: ${arg}`);
199
+ else paths.push(arg);
200
+ }
201
+ if (paths.length === 0) throw new CliError("at least one target directory is required");
202
+ return { mode: mode ?? "write", format, paths };
203
+ }
204
+
205
+ function isMigratableFile(name: string): boolean {
206
+ return /\.(ts|tsx)$/.test(name) && !name.endsWith(".d.ts");
207
+ }
208
+
209
+ function displayPath(file: string): string {
210
+ const fromCwd = relative(process.cwd(), file);
211
+ return fromCwd.startsWith("..") ? file : fromCwd;
212
+ }
213
+
214
+ export interface CodemodCliConfig {
215
+ readonly name: string;
216
+ readonly transform: (source: string, filePath: string) => CodemodResult;
217
+ readonly isSkippedPath: (file: string) => boolean;
218
+ }
219
+
220
+ function parseArgsOrExit(codemodName: string, argv: readonly string[]): CliOptions {
221
+ try {
222
+ return parseArgs(argv);
223
+ } catch (err) {
224
+ if (err instanceof CliError) {
225
+ console.error(`[${codemodName}] ${err.message}`);
226
+ process.exit(2);
227
+ }
228
+ throw err;
229
+ }
230
+ }
231
+
232
+ function transformFiles(
233
+ config: CodemodCliConfig,
234
+ files: readonly string[],
235
+ mode: Mode,
236
+ ): { rewrites: CodemodSite[]; manual: CodemodSite[]; changedFiles: string[] } {
237
+ const rewrites: CodemodSite[] = [];
238
+ const manual: CodemodSite[] = [];
239
+ const changedFiles: string[] = [];
240
+ for (const file of files) {
241
+ const before = readFileSync(file, "utf8");
242
+ const result = config.transform(before, file);
243
+ const shown = displayPath(file);
244
+ for (const site of result.rewrites)
245
+ console.log(`${shown}:${site.line} rewritten ${site.description}`);
246
+ for (const site of result.manual)
247
+ console.log(`${shown}:${site.line} manual ${site.description}`);
248
+ rewrites.push(...result.rewrites);
249
+ manual.push(...result.manual);
250
+ if (mode === "write" && result.output !== before) {
251
+ writeFileSync(file, result.output);
252
+ changedFiles.push(file);
253
+ }
254
+ }
255
+ return { rewrites, manual, changedFiles };
256
+ }
257
+
258
+ // Without a mode flag it writes (kumiko-upgrade --apply calls it that way). --check exits 1
259
+ // while any rewritable or manual site is left; --dry-run only reports.
260
+ export async function runCodemodCli(
261
+ config: CodemodCliConfig,
262
+ argv: readonly string[],
263
+ ): Promise<void> {
264
+ const options = parseArgsOrExit(config.name, argv);
265
+ const files = collectSourceFiles(options.paths, { isMigratableFile }).filter(
266
+ (file) => !(file.includes("/__tests__/fixtures/") || config.isSkippedPath(file)),
267
+ );
268
+ const { rewrites, manual, changedFiles } = transformFiles(config, files, options.mode);
269
+ console.log(
270
+ `\n[${config.name}] ${options.mode === "write" ? "rewritten" : "rewritable"}: ${rewrites.length} site(s) in ${new Set(rewrites.map((site) => site.file)).size} file(s); manual: ${manual.length}`,
271
+ );
272
+ if (options.mode === "write" && options.format && changedFiles.length > 0) {
273
+ await formatWithBiome(config.name, changedFiles, resolve(options.paths[0] ?? "."));
274
+ }
275
+ process.exit(options.mode === "check" && rewrites.length + manual.length > 0 ? 1 : 0);
276
+ }