@cosmicdrift/kumiko-framework 0.366.0 → 0.368.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 +22 -33
  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/seed-context.js +3 -3
  96. package/dist/db/queries/test-stack.js +1 -1
  97. package/dist/db/query.d.ts +105 -1
  98. package/dist/db/query.js +1450 -3
  99. package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
  100. package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
  101. package/dist/db/tenant-db.d.ts +11 -0
  102. package/dist/db/tenant-db.js +51 -12
  103. package/dist/derivatives/derivatives-context.d.ts +1 -1
  104. package/dist/derivatives/derivatives-context.js +1 -1
  105. package/dist/engine/boot-validator/gdpr-storage.js +3 -3
  106. package/dist/engine/boot-validator/pii-retention.js +3 -3
  107. package/dist/engine/boot-validator/record-owned.js +3 -3
  108. package/dist/engine/boot-validator/screens.js +8 -1
  109. package/dist/engine/bridge-stub.d.ts +4 -0
  110. package/dist/engine/bridge-stub.js +62 -0
  111. package/dist/engine/event-helpers.d.ts +2 -1
  112. package/dist/engine/event-helpers.js +1 -0
  113. package/dist/engine/feature-config-events-jobs.d.ts +4 -0
  114. package/dist/engine/feature-config-events-jobs.js +4 -2
  115. package/dist/engine/index.d.ts +1 -0
  116. package/dist/engine/index.js +1 -0
  117. package/dist/engine/registry-ingest.js +3 -0
  118. package/dist/engine/template-variables.d.ts +3 -0
  119. package/dist/engine/template-variables.js +3 -0
  120. package/dist/es-ops/operations-schema.js +1 -1
  121. package/dist/event-store/admin-api.js +1 -1
  122. package/dist/event-store/archive.js +1 -1
  123. package/dist/event-store/errors.d.ts +2 -1
  124. package/dist/event-store/errors.js +6 -2
  125. package/dist/event-store/event-store.js +17 -9
  126. package/dist/event-store/events-schema.js +1 -1
  127. package/dist/event-store/rebuild-dead-letter.js +3 -3
  128. package/dist/event-store/snapshot.js +1 -1
  129. package/dist/event-store/upcaster-dead-letter.js +3 -3
  130. package/dist/files/create-file-ref-from-bytes.d.ts +1 -0
  131. package/dist/files/create-file-ref-from-bytes.js +8 -3
  132. package/dist/files/file-routes.js +1 -1
  133. package/dist/internal/stack-support.d.ts +12 -0
  134. package/dist/internal/stack-support.js +12 -0
  135. package/dist/internal/testing.d.ts +4 -0
  136. package/dist/internal/testing.js +5 -0
  137. package/dist/jobs/job-runner.js +23 -11
  138. package/dist/migrations/pending-rebuilds.js +1 -1
  139. package/dist/migrations/pii-event-backfill.js +2 -2
  140. package/dist/pipeline/dispatch-shared.js +43 -23
  141. package/dist/pipeline/dispatch-write.js +6 -2
  142. package/dist/pipeline/dispatcher-utils.js +6 -2
  143. package/dist/pipeline/event-consumer-state.js +1 -1
  144. package/dist/pipeline/projection-rebuild.d.ts +1 -0
  145. package/dist/pipeline/projection-rebuild.js +1 -1
  146. package/dist/pipeline/projection-state.js +1 -1
  147. package/dist/pipeline/system-hooks.js +1 -1
  148. package/dist/schema-cli.js +3 -3
  149. package/dist/search/purge-subject.js +20 -10
  150. package/dist/seeding/index.d.ts +1 -0
  151. package/dist/seeding/index.js +1 -0
  152. package/dist/stack/index.d.ts +10 -9
  153. package/dist/stack/index.js +11 -19
  154. package/dist/testing/e2e-generator.d.ts +5 -49
  155. package/dist/testing/e2e-generator.js +5 -382
  156. package/dist/testing/handler-context.d.ts +5 -4
  157. package/dist/testing/handler-context.js +5 -69
  158. package/dist/testing/index.d.ts +10 -26
  159. package/dist/testing/index.js +10 -34
  160. package/dist/testing/real-providers.d.ts +5 -3
  161. package/dist/testing/real-providers.js +5 -21
  162. package/package.json +37 -9
  163. package/src/changes.json +70 -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
@@ -1,13 +1,13 @@
1
- import type { DbConnection } from "../db/connection.js";
2
- import type { ColumnMeta, EntityTableMeta, IndexMeta } from "../db/entity-table-meta.js";
3
- import type { EventDispatcher } from "../pipeline/index.js";
1
+ import type { EntityDefinition } from "../engine/types/index.js";
2
+ import type { DbConnection } from "./connection.js";
3
+ import type { ColumnMeta, EntityTableMeta, IndexMeta } from "./entity-table-meta.js";
4
4
  /**
5
5
  * Bypass: creates an entity-table directly without going through the
6
6
  * full registry. Reserved for framework-internal meta-tables and
7
7
  * test setup — apps declare data via `r.entity(...)`.
8
8
  */
9
- export declare function unsafeCreateEntityTable(db: DbConnection, entity: import("../engine/types/index.js").EntityDefinition, entityName?: string): Promise<void>;
10
- export declare function unsafeEnsureEntityTable(db: DbConnection, entity: import("../engine/types/index.js").EntityDefinition, entityName?: string): Promise<boolean>;
9
+ export declare function unsafeCreateEntityTable(db: DbConnection, entity: EntityDefinition, entityName?: string): Promise<void>;
10
+ export declare function unsafeEnsureEntityTable(db: DbConnection, entity: EntityDefinition, entityName?: string): Promise<boolean>;
11
11
  export declare function tableToMeta(table: unknown): EntityTableMeta;
12
12
  /** Applies ALTER TABLE ADD COLUMN for exactly the given columns. */
13
13
  export declare function addMissingColumns(db: DbConnection, tableName: string, columns: readonly ColumnMeta[]): Promise<void>;
@@ -22,13 +22,3 @@ export declare function addMissingIndexes(db: DbConnection, tableName: string, i
22
22
  * entities go through `kumiko schema apply` (committed SQL files).
23
23
  */
24
24
  export declare function unsafePushTables(db: DbConnection, tables: Record<string, unknown>, prevTables?: Record<string, unknown>): Promise<void>;
25
- /**
26
- * Wipes event store + framework-state + the given feature read-models in
27
- * one TRUNCATE, then re-registers the event-consumer state rows. Used in
28
- * test beforeEach-hooks to return the stack to a clean slate without
29
- * rebuilding it.
30
- */
31
- export declare function resetEventStore(stack: {
32
- db: unknown;
33
- eventDispatcher?: EventDispatcher;
34
- }, extraTables?: readonly (unknown | string)[]): Promise<void>;
@@ -1,21 +1,9 @@
1
- import { pgTypeToSqlType } from "../db/dialect.js";
2
- import { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement, } from "../db/queries/ddl.js";
3
- import { truncateTablesRestartIdentity } from "../db/queries/test-stack.js";
4
- import { renderTableDdl } from "../db/render-ddl.js";
5
- import { tableExists } from "../db/schema-inspection.js";
6
- import { buildEntityTable, toTableName } from "../db/table-builder.js";
7
- import { asEntityTableMeta, schemaTableName } from "../db/table-introspection.js";
8
- function tableNameOf(table) {
9
- if (typeof table !== "object" || table === null) {
10
- throw new Error("table-helpers: table is not a SchemaTable object");
11
- }
12
- const symbolName = schemaTableName(table);
13
- if (symbolName !== undefined)
14
- return symbolName;
15
- if ("tableName" in table && typeof table.tableName === "string")
16
- return table.tableName;
17
- throw new Error("table-helpers: table has no name");
18
- }
1
+ import { pgTypeToSqlType } from "./dialect.js";
2
+ import { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement } from "./queries/ddl.js";
3
+ import { renderTableDdl } from "./render-ddl.js";
4
+ import { tableExists } from "./schema-inspection.js";
5
+ import { buildEntityTable, toTableName } from "./table-builder.js";
6
+ import { asEntityTableMeta } from "./table-introspection.js";
19
7
  /**
20
8
  * Bypass: creates an entity-table directly without going through the
21
9
  * full registry. Reserved for framework-internal meta-tables and
@@ -96,31 +84,3 @@ export async function unsafePushTables(db, tables, prevTables) {
96
84
  function renderColumnType(col) {
97
85
  return pgTypeToSqlType(col.pgType);
98
86
  }
99
- /**
100
- * Wipes event store + framework-state + the given feature read-models in
101
- * one TRUNCATE, then re-registers the event-consumer state rows. Used in
102
- * test beforeEach-hooks to return the stack to a clean slate without
103
- * rebuilding it.
104
- */
105
- export async function resetEventStore(stack, extraTables = []) {
106
- const frameworkTables = [
107
- "kumiko_events",
108
- "kumiko_event_consumers",
109
- "kumiko_archived_streams",
110
- "kumiko_snapshots",
111
- "kumiko_projections",
112
- ];
113
- const extraNames = extraTables.map((t) => (typeof t === "string" ? t : tableNameOf(t)));
114
- const truncateAndReregister = async () => {
115
- await truncateTablesRestartIdentity(stack.db, [...frameworkTables, ...extraNames]);
116
- await stack.eventDispatcher?.ensureRegistered();
117
- };
118
- // The dispatcher's own timer/LISTEN must not start a pass between the
119
- // TRUNCATE and the consumer re-registration, so they are paused for the whole reset.
120
- if (stack.eventDispatcher) {
121
- await stack.eventDispatcher.withBackgroundPassesPaused(truncateAndReregister);
122
- }
123
- else {
124
- await truncateAndReregister();
125
- }
126
- }
@@ -37,6 +37,17 @@ export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, rea
37
37
  }): TenantDb;
38
38
  export declare function reportConventionCrossTenantUse(acknowledged: TenantDb, target?: EscapeHatchTarget): void;
39
39
  export declare function runInOwnTransaction<T>(tenantDb: TenantDb, fn: (txDb: TenantDb) => Promise<T>): Promise<T>;
40
+ export type CreateTenantDbOptions = {
41
+ readonly db: DbRunner;
42
+ readonly tenantId: TenantId;
43
+ readonly mode?: TenantDbMode;
44
+ readonly tracer?: Tracer;
45
+ readonly meter?: Meter;
46
+ readonly signal?: AbortSignal;
47
+ readonly grants?: TenantDbGrants;
48
+ };
49
+ export declare function createTenantDb(options: CreateTenantDbOptions): TenantDb;
50
+ /** @deprecated Positional form, use createTenantDb({ db, tenantId, ... }). Run `bun kumiko-upgrade --apply` to rewrite call sites. Removed in the first release after 2026-10-29. */
40
51
  export declare function createTenantDb(db: DbRunner, tenantId: TenantId, mode?: TenantDbMode, tracer?: Tracer, meter?: Meter, signal?: AbortSignal, grants?: TenantDbGrants): TenantDb;
41
52
  export declare function createProjectingTenantDb(db: DbRunner, tenantId: TenantId, projectionRegistry: Registry | undefined): TenantDb;
42
53
  export { asRawClient };
@@ -225,7 +225,7 @@ export function createSystemDbView(db, dbOutsideTransaction, report = fallbackEs
225
225
  // same connection/transaction. For code that must run caller-supplied callbacks (e.g. config
226
226
  // validators) without handing them a cross-tenant view.
227
227
  export function scopeTenantDbToTenant(db, tenantId) {
228
- return createTenantDb(tenantDbRunner(db), tenantId, "tenant");
228
+ return createTenantDb({ db: tenantDbRunner(db), tenantId, mode: "tenant" });
229
229
  }
230
230
  // @cast-boundary tenant-db-row
231
231
  export function castTenantRows(rows) {
@@ -311,7 +311,26 @@ export async function runInOwnTransaction(tenantDb, fn) {
311
311
  }
312
312
  return rebind(fn);
313
313
  }
314
- export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, signal, grants) {
314
+ // Runners (postgres-js Sql/tx handles) are functions and RawDbClient mocks carry no own
315
+ // `tenantId`/`db`, so a runner is never mistaken for the options object.
316
+ function isCreateTenantDbOptions(value) {
317
+ return (typeof value === "object" &&
318
+ value !== null &&
319
+ Object.hasOwn(value, "tenantId") &&
320
+ Object.hasOwn(value, "db"));
321
+ }
322
+ export function createTenantDb(dbOrOptions, tenantId, mode, tracer, meter, signal, grants) {
323
+ if (isCreateTenantDbOptions(dbOrOptions))
324
+ return buildTenantDb(dbOrOptions);
325
+ return buildTenantDb({ db: dbOrOptions, tenantId, mode, tracer, meter, signal, grants });
326
+ }
327
+ // Untyped JS callers or an `as` cast can still hand in undefined; a scoped handle
328
+ // without a tenant would bind `tenant_id = NULL` instead of failing loudly.
329
+ function buildTenantDb(options) {
330
+ const { db, tenantId, mode = "tenant", tracer, meter, signal, grants } = options;
331
+ if (tenantId === undefined) {
332
+ throw new InternalError({ message: "createTenantDb: tenantId is required" });
333
+ }
315
334
  if (meter)
316
335
  registerStandardMetrics(meter);
317
336
  const report = grants?.report ?? fallbackEscapeHatchReporter(tenantId);
@@ -585,16 +604,32 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
585
604
  },
586
605
  };
587
606
  declaredUnsafeRawRunners.set(tenantDb, grantedUnsafeRawRunner);
588
- unsafeRawRebinders.set(tenantDb, (grant) => createTenantDb(db, tenantId, mode, tracer, meter, signal, {
589
- ...rebindGrants,
590
- unsafeRaw: grant,
591
- // db.global() writes are write-handler-only: a rebind (a hook's own grant) must not inherit them.
592
- globalWrites: undefined,
607
+ unsafeRawRebinders.set(tenantDb, (grant) => createTenantDb({
608
+ db,
609
+ tenantId,
610
+ mode,
611
+ tracer,
612
+ meter,
613
+ signal,
614
+ grants: {
615
+ ...rebindGrants,
616
+ unsafeRaw: grant,
617
+ // db.global() writes are write-handler-only: a rebind (a hook's own grant) must not inherit them.
618
+ globalWrites: undefined,
619
+ },
593
620
  }));
594
621
  crossTenantRebinders.set(tenantDb, (reason, deferReport) => {
595
622
  if (!deferReport)
596
623
  report("acknowledge-cross-tenant", reason);
597
- const acknowledged = createTenantDb(db, tenantId, "system", tracer, meter, signal, rebindGrants);
624
+ const acknowledged = createTenantDb({
625
+ db,
626
+ tenantId,
627
+ mode: "system",
628
+ tracer,
629
+ meter,
630
+ signal,
631
+ grants: rebindGrants,
632
+ });
598
633
  crossTenantUseReporters.set(acknowledged, (target) => report("acknowledge-cross-tenant", reason, target));
599
634
  return acknowledged;
600
635
  });
@@ -607,7 +642,7 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
607
642
  }
608
643
  // Carries a runner-bound gate (grants.personalDataGate unset, resolved via the fallback above) forward explicitly, since the fresh tx-handle isn't itself registered in runnerPersonalDataGates.
609
644
  const txGrants = personalDataGate ? { ...rebindGrants, personalDataGate } : rebindGrants;
610
- return runInNewTransaction(db, (tx) => fn(createTenantDb(tx, tenantId, mode, tracer, meter, signal, txGrants)));
645
+ return runInNewTransaction(db, (tx) => fn(createTenantDb({ db: tx, tenantId, mode, tracer, meter, signal, grants: txGrants })));
611
646
  });
612
647
  bindTenantDbRunner(tenantDb, db);
613
648
  if (grants?.writesDeniedReason !== undefined) {
@@ -619,9 +654,13 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
619
654
  bindProjectionRegistry(tenantDb, projectionRegistry);
620
655
  return tenantDb;
621
656
  }
622
- // Tenant-mode TenantDb whose writes reach the registry's custom projections. The positional
623
- // undefineds of createTenantDb would hide a moved grants parameter from the type checker.
657
+ // Tenant-mode TenantDb whose writes reach the registry's custom projections.
624
658
  export function createProjectingTenantDb(db, tenantId, projectionRegistry) {
625
- return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, projectionRegistry ? { projectionRegistry } : undefined);
659
+ return createTenantDb({
660
+ db,
661
+ tenantId,
662
+ mode: "tenant",
663
+ grants: projectionRegistry ? { projectionRegistry } : undefined,
664
+ });
626
665
  }
627
666
  export { asRawClient };
@@ -1,5 +1,5 @@
1
1
  import type { DerivativeRendererPlugin, DerivativesContext } from "@cosmicdrift/kumiko-types/derivatives-types";
2
- import { type AnyDb } from "../bun-db/query.js";
2
+ import { type AnyDb } from "../db/query.js";
3
3
  import type { Registry, TenantId } from "../engine/types/index.js";
4
4
  import type { FileContext } from "../files/file-handle.js";
5
5
  export type DerivativesContextDeps = {
@@ -1,4 +1,4 @@
1
- import { fetchOne } from "../bun-db/query.js";
1
+ import { fetchOne } from "../db/query.js";
2
2
  import { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_RENDERER, } from "../engine/extension-names.js";
3
3
  import { InternalError, NotFoundError } from "../errors/index.js";
4
4
  import { fileRefsTable } from "../files/file-ref-table.js";
@@ -1,7 +1,7 @@
1
1
  // Providers whose bytes do not survive a process restart. Only "inmemory"
2
2
  // today; extend if another ephemeral bundled provider lands.
3
3
  const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
4
- // Env vars file-provider-s3-env reads (see files-provider-s3/env-helper).
4
+ // Env vars createFileProviderS3Feature({ fromEnv: true }) reads (see file-provider-s3/lib/env-helper).
5
5
  // Missing any → createS3ProviderFromEnv throws on the first file op.
6
6
  const S3_ENV_REQUIRED = ["S3_BUCKET", "S3_REGION", "S3_ACCESS_KEY", "S3_SECRET_KEY"];
7
7
  // Cross-feature GDPR storage guard (V1). Catches the failure class we shipped
@@ -31,7 +31,7 @@ export function validateGdprStoragePersistence(features) {
31
31
  const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
32
32
  if (persistent.length === 0) {
33
33
  // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
34
- console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount file-provider-s3 or file-provider-s3-env and select it via the file-foundation provider config.");
34
+ console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
35
35
  // skip: ephemeral store already warned; the s3-env env-var check below is moot
36
36
  return;
37
37
  }
@@ -42,7 +42,7 @@ export function validateGdprStoragePersistence(features) {
42
42
  const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
43
43
  if (missing.length > 0) {
44
44
  // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
45
- console.warn(`[kumiko:boot] file-provider-s3-env is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
45
+ console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
46
46
  }
47
47
  }
48
48
  }
@@ -204,11 +204,11 @@ export function validatePiiAndRetention(feature) {
204
204
  throw new Error(`[Feature ${feature.name}] personal.of "${ownerName}" on entity "${entityName}" must be a reference or text (userId) field, got type "${ownerField.type}"`);
205
205
  }
206
206
  const { whenAbsent } = annot.userOwned;
207
- if (whenAbsent !== undefined && whenAbsent !== "tenant") {
208
- throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant".`);
207
+ if (whenAbsent !== undefined && whenAbsent !== "tenant" && whenAbsent !== "self") {
208
+ throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" or "self" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant" (tenant key) or "self" (the row's own record key).`);
209
209
  }
210
210
  if (ownerField.required !== true && whenAbsent === undefined) {
211
- throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
211
+ throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, whenAbsent: "self" to encrypt them under the row's own record key (shreddable per record), or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
212
212
  }
213
213
  // Soft-Warning wenn das reference-target nicht offensichtlich user
214
214
  // ist — custom subject-entities (HR-Mitarbeiter, Patient) sind
@@ -1,4 +1,4 @@
1
- // A recordOwned field is encrypted under `record:<entity>:<id>` (kms-adapter-types.ts)
1
+ // A recordOwned field (or one with whenAbsent: "self") is encrypted under `record:<entity>:<id>` (kms-adapter-types.ts)
2
2
  // and forgetSubject's subjectIdSchema (crypto/kms-adapter.ts) requires that id
3
3
  // to be a UUID — an entity with idType: "serial" would encrypt the field but
4
4
  // could never satisfy a forget-subject request for it.
@@ -8,9 +8,9 @@ export function validateRecordOwnedSubjects(feature) {
8
8
  continue;
9
9
  for (const [fieldName, field] of Object.entries(entity.fields)) {
10
10
  const annot = field; // @cast-boundary schema-walk
11
- if (annot.recordOwned !== true)
11
+ if (annot.recordOwned !== true && annot.userOwned?.whenAbsent !== "self")
12
12
  continue;
13
- throw new Error(`[Feature ${feature.name}] entity "${entityName}" field "${fieldName}" is recordOwned but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field would be encrypted but could never be shredded. Set idType: "uuid" on entity "${entityName}", or give field "${fieldName}" a different personal stance (personal: "self" / "tenant" / { of: "<field>" }).`);
13
+ throw new Error(`[Feature ${feature.name}] entity "${entityName}" field "${fieldName}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field would be encrypted but could never be shredded. Set idType: "uuid" on entity "${entityName}", or give field "${fieldName}" a different personal stance (personal: "self" / "tenant" / { of: "<field>" }).`);
14
14
  }
15
15
  }
16
16
  }
@@ -7,6 +7,7 @@ import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
7
7
  import { NO_WIDGET_FIELD_TYPES, optionsQueryFieldRefs, SELECT_OPTION_TONES, } from "@cosmicdrift/kumiko-types/fields";
8
8
  import { NAV_ICON_KEYS } from "@cosmicdrift/kumiko-types/nav-icon";
9
9
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
10
+ import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
10
11
  import { rowMetaFieldNames } from "../../db/table-builder.js";
11
12
  import { REFERENCE_LOOKUP_SOURCES } from "../../ui-types/list-row-meta.js";
12
13
  import { isEncryptedAtRest } from "../config-helpers.js";
@@ -230,7 +231,7 @@ function validateWizardLayout(featureName, screenId, screenType, layout, feature
230
231
  if (!featureMap.has("form-draft")) {
231
232
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) sets draft: true but the ` +
232
233
  `bundled feature "form-draft" is not mounted — every resume would silently lose its ` +
233
- `values. Add formDraftFeature() from @cosmicdrift/kumiko-bundled-features to the app's ` +
234
+ `values. Add createFormDraftFeature() from @cosmicdrift/kumiko-bundled-features to the app's ` +
234
235
  `feature list.`);
235
236
  }
236
237
  }
@@ -1641,6 +1642,12 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1641
1642
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references unknown ` +
1642
1643
  `field "${filterField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
1643
1644
  }
1645
+ if (!isListFilterAllowed(entityDef, filterField, screen.filter)) {
1646
+ throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
1647
+ `"${filterField}" which is encrypted, personal or secret, so the list handler rejects ` +
1648
+ `this filter at runtime. Only op "eq" works on a personal field with find: "exact" or ` +
1649
+ `"fuzzy"; otherwise drop the filter or filter on another field.`);
1650
+ }
1644
1651
  const fieldDef = entityDef.fields[filterField];
1645
1652
  if (fieldDef !== undefined && !isFieldFilterable(fieldDef)) {
1646
1653
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
@@ -0,0 +1,4 @@
1
+ import type { HandlerContext, SessionUser } from "./types/index.js";
2
+ export declare function bridgeStub(opts?: {
3
+ readonly user?: SessionUser;
4
+ }): Pick<HandlerContext, "query" | "queryAs" | "write" | "writeAs" | "appendEvent" | "unsafeAppendEvent" | "tryAppendEvent" | "fetchForWriting" | "loadAggregate" | "archiveStream" | "restoreStream" | "isStreamArchived" | "snapshotAggregate" | "loadAggregateWithSnapshot" | "queryProjection" | "resolveAuthClaims" | "resolveActiveMembership" | "queryAsMember" | "hasFeature" | "metrics" | "metricsFor" | "tracer" | "tz" | "locale" | "user">;
@@ -0,0 +1,62 @@
1
+ import { DEFAULT_LOCALE } from "../i18n/request-locale.js";
2
+ import { createNoopMetricsHandle, getFallbackTracer } from "../observability/index.js";
3
+ import { createTzContext } from "../time/index.js";
4
+ import { createAnonymousUser } from "./system-user.js";
5
+ // Test/service helper: cross-feature bridge methods that throw on use.
6
+ //
7
+ // Production code always receives a full HandlerContext from the Dispatcher's
8
+ // buildHandlerContext (with real query/write closures). Some internal services
9
+ // and tests construct a mini-context manually (typically just `{ db, registry }`)
10
+ // to invoke a single handler. Those call sites don't use ctx.query/write —
11
+ // the stubs make the TypeScript shape match while still failing loudly if
12
+ // anything downstream accidentally reaches for them.
13
+ //
14
+ // Use: `{ db, registry, ...bridgeStub() }`
15
+ const notAvailable = (what) => async () => {
16
+ throw new Error(`ctx.${what} not available in this context — use the dispatcher, not a stubbed handler context`);
17
+ };
18
+ // Noop observability — hand back the shared fallback tracer so ctx.tracer has
19
+ // a valid Tracer shape. No allocations per call.
20
+ const noopTracer = getFallbackTracer();
21
+ export function bridgeStub(opts) {
22
+ // ctx.user is a convenience alias for event.user (see HandlerContext
23
+ // docs). Caller code expects the field; bridgeStub hands back a stub with
24
+ // anonymous default values when no user is passed explicitly. Test code
25
+ // that cares about identity passes its own SessionUser here and gets it
26
+ // back on ctx.
27
+ const stubUser = opts?.user ??
28
+ createAnonymousUser("00000000-0000-0000-0000-000000000000"); // @cast-boundary engine-bridge
29
+ return {
30
+ user: stubUser,
31
+ query: notAvailable("query"), // @cast-boundary engine-bridge
32
+ queryAs: notAvailable("queryAs"),
33
+ write: notAvailable("write"),
34
+ writeAs: notAvailable("writeAs"),
35
+ appendEvent: notAvailable("appendEvent"),
36
+ unsafeAppendEvent: notAvailable("unsafeAppendEvent"),
37
+ tryAppendEvent: notAvailable("tryAppendEvent"),
38
+ fetchForWriting: notAvailable("fetchForWriting"),
39
+ loadAggregate: notAvailable("loadAggregate"),
40
+ archiveStream: notAvailable("archiveStream"),
41
+ restoreStream: notAvailable("restoreStream"),
42
+ isStreamArchived: notAvailable("isStreamArchived"),
43
+ snapshotAggregate: notAvailable("snapshotAggregate"),
44
+ loadAggregateWithSnapshot: notAvailable("loadAggregateWithSnapshot"),
45
+ queryProjection: notAvailable("queryProjection"),
46
+ resolveAuthClaims: notAvailable("resolveAuthClaims"),
47
+ resolveActiveMembership: notAvailable("resolveActiveMembership"),
48
+ queryAsMember: notAvailable("queryAsMember"),
49
+ // Stub defaults to always-enabled — matches the dispatcher's behaviour
50
+ // when no effectiveFeatures resolver is wired (tests without toggles).
51
+ hasFeature: async () => true,
52
+ metrics: createNoopMetricsHandle(),
53
+ metricsFor: () => createNoopMetricsHandle(),
54
+ tracer: noopTracer,
55
+ // Real TzContext, not notAvailable — test code uses ctx.tz routinely,
56
+ // it isn't a "bridge" concept. Defaults to UTC.
57
+ tz: createTzContext(),
58
+ // Same reasoning as tz above — ctx.locale is always-present, not a
59
+ // bridge method. Defaults to DEFAULT_LOCALE.
60
+ locale: DEFAULT_LOCALE,
61
+ };
62
+ }
@@ -1,9 +1,10 @@
1
- import type { EventDef, HandlerContext } from "./types/handlers.js";
1
+ import type { AppendEventArgs, EventDef, HandlerContext } from "./types/handlers.js";
2
2
  export type EmitCtx = Pick<HandlerContext, "unsafeAppendEvent">;
3
3
  export declare function emitEvent<TPayload>(ctx: EmitCtx, eventDef: EventDef<TPayload>, args: {
4
4
  readonly aggregateId: string;
5
5
  readonly aggregateType: string;
6
6
  readonly payload: TPayload;
7
+ readonly headers?: AppendEventArgs["headers"];
7
8
  }): Promise<void>;
8
9
  export declare function typedPayload<TPayload>(event: {
9
10
  readonly type: string;
@@ -21,6 +21,7 @@ export async function emitEvent(ctx, eventDef, args) {
21
21
  aggregateType: args.aggregateType,
22
22
  type: eventDef.name,
23
23
  payload: args.payload,
24
+ ...(args.headers !== undefined && { headers: args.headers }),
24
25
  };
25
26
  await ctx.unsafeAppendEvent(appendArgs);
26
27
  }
@@ -26,6 +26,8 @@ export declare function buildConfigEventsJobsMethods<TName extends string>(state
26
26
  readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
27
27
  readonly localizedTemplate?: () => NotificationLocalizedTemplate;
28
28
  readonly renamedFrom?: readonly string[];
29
+ readonly systemOnly?: boolean;
30
+ readonly unsubscribe?: boolean;
29
31
  }), definition?: {
30
32
  readonly trigger?: {
31
33
  readonly on: NameOrRef;
@@ -39,6 +41,8 @@ export declare function buildConfigEventsJobsMethods<TName extends string>(state
39
41
  readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
40
42
  readonly localizedTemplate?: () => NotificationLocalizedTemplate;
41
43
  readonly renamedFrom?: readonly string[];
44
+ readonly systemOnly?: boolean;
45
+ readonly unsubscribe?: boolean;
42
46
  }): void;
43
47
  translations(def: TranslationsDef): void;
44
48
  defineEvent: <const TInner extends string, TPayload>(eventName: TInner, schema: ZodType<TPayload>, options: {
@@ -40,12 +40,12 @@ function assertOwnerFieldYieldsPresentString(args) {
40
40
  const { featureName, eventName, field, ownerField, owner } = args;
41
41
  if (neverYieldsString(owner)) {
42
42
  throw new Error(`[Feature ${featureName}] defineEvent("${eventName}"): piiFields."${field}" is owned by "${ownerField}", whose payload schema is not a string — the subject id must be a non-empty string, so every append would fail. ` +
43
- `Use a string owner field, or declare whenAbsent ("tenant" | "plaintext").`);
43
+ `Use a string owner field, or declare whenAbsent ("tenant" | "self" | "plaintext").`);
44
44
  }
45
45
  if (isAbsentable(owner)) {
46
46
  throw new Error(`[Feature ${featureName}] defineEvent("${eventName}"): piiFields."${field}" is owned by "${ownerField}", which the payload schema allows to be null/undefined. ` +
47
47
  `Declare what happens then: { personal: { of: "${ownerField}", whenAbsent: "tenant" } } encrypts under the envelope tenant key, ` +
48
- `whenAbsent: "plaintext" acknowledges that the value ships unencrypted and cannot be crypto-shredded (fw#2776).`);
48
+ `whenAbsent: "self" under the event's own record key, whenAbsent: "plaintext" acknowledges that the value ships unencrypted and cannot be crypto-shredded (fw#2776).`);
49
49
  }
50
50
  }
51
51
  // Builds config/secrets/claims/events/jobs/notifications registrar methods.
@@ -220,6 +220,8 @@ export function buildConfigEventsJobsMethods(state, name) {
220
220
  ...(resolvedDefinition.renamedFrom !== undefined
221
221
  ? { renamedFrom: resolvedDefinition.renamedFrom }
222
222
  : {}),
223
+ ...(resolvedDefinition.systemOnly === true ? { systemOnly: true } : {}),
224
+ ...(resolvedDefinition.unsubscribe === true ? { unsubscribe: true } : {}),
223
225
  };
224
226
  },
225
227
  translations(def) {
@@ -4,6 +4,7 @@ export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalP
4
4
  export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
5
5
  export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
6
6
  export { type CardMetaOverflow, collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
7
+ export { bridgeStub } from "./bridge-stub.js";
7
8
  export { type BuildAppSchemaOptions, buildAppSchema } from "./build-app-schema.js";
8
9
  export type { ConfigFeatureSchema } from "./build-config-feature-schema.js";
9
10
  export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
@@ -5,6 +5,7 @@ export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN
5
5
  export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
6
6
  export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
7
7
  export { collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
8
+ export { bridgeStub } from "./bridge-stub.js";
8
9
  export { buildAppSchema } from "./build-app-schema.js";
9
10
  export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
10
11
  export { buildTarget } from "./build-target.js";
@@ -108,6 +108,9 @@ const RESERVED_TEMPLATE_VARIABLE_NAMES = new Set([
108
108
  "link",
109
109
  "money",
110
110
  "date",
111
+ "richText",
112
+ "stepIs",
113
+ "percent",
111
114
  ]);
112
115
  const KEBAB_NOTIFICATION_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
113
116
  function isTemplateVariableName(value) {
@@ -23,6 +23,9 @@ export declare const templateVariable: {
23
23
  readonly date: (opts?: CommonOptions & {
24
24
  readonly example?: string;
25
25
  }) => TemplateScalarVariableDef;
26
+ readonly percent: (opts?: CommonOptions & {
27
+ readonly example?: number;
28
+ }) => TemplateScalarVariableDef;
26
29
  readonly list: (item: Readonly<Record<string, TemplateScalarVariableDef>>, opts?: CommonOptions & {
27
30
  readonly example?: readonly Readonly<Record<string, unknown>>[];
28
31
  }) => TemplateListVariableDef;
@@ -16,6 +16,7 @@ export const templateVariable = {
16
16
  type: "date",
17
17
  ...opts,
18
18
  }),
19
+ percent: (opts = {}) => ({ type: "percent", ...opts }),
19
20
  list: (item, opts = {}) => ({ type: "list", item, ...opts }),
20
21
  };
21
22
  const LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
@@ -48,6 +49,8 @@ function scalarToZod(def) {
48
49
  return moneySchema;
49
50
  case "date":
50
51
  return dateSchema;
52
+ case "percent":
53
+ return z.number().finite();
51
54
  }
52
55
  }
53
56
  function variableToZod(def) {
@@ -12,7 +12,7 @@
12
12
  import { index, integer, table as pgTable, sql, text, timestamp } from "../db/dialect.js";
13
13
  // sql now comes from native dialect
14
14
  import { tableExists } from "../db/index.js";
15
- import { unsafePushTables } from "../stack/index.js";
15
+ import { unsafePushTables } from "../db/table-push.js";
16
16
  export const esOperationsTable = pgTable("kumiko_es_operations", {
17
17
  // Filename without extension serves as ID. Chronologically sortable
18
18
  // (date-prefix convention), human-meaningful, no separate hash needed.
@@ -7,10 +7,10 @@
7
7
  // Guard-Script `scripts/guard-admin-api.ts` blockt Aufrufe aus App-Code —
8
8
  // Allowlist: samples/*/migration/, scripts/migrations/, die Definition
9
9
  // selbst, das Guard-Script selbst.
10
- import { instantToTimestamptzParam } from "../bun-db/query.js";
11
10
  import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
12
11
  import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
13
12
  import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
13
+ import { instantToTimestamptzParam } from "../db/query.js";
14
14
  import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
15
15
  function mapEventUniqueViolation(e, event) {
16
16
  if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
@@ -3,7 +3,7 @@ import { instant, table as pgTable, sql, text, uniqueIndex, uuid } from "../db/d
3
3
  import { upsertArchivedStream } from "../db/queries/event-store.js";
4
4
  import { deleteMany, fetchOne } from "../db/query.js";
5
5
  import { tableExists } from "../db/schema-inspection.js";
6
- import { unsafePushTables } from "../stack/index.js";
6
+ import { unsafePushTables } from "../db/table-push.js";
7
7
  // Marten-aligned stream archival. Archived streams become read-only: fresh
8
8
  // appendEvent on an archived aggregate throws, and loadAggregate returns
9
9
  // an empty slice unless the caller passes { includeArchived: true }.
@@ -2,7 +2,8 @@
2
2
  export declare class VersionConflictError extends Error {
3
3
  readonly aggregateId: string;
4
4
  readonly expectedVersion: number;
5
- constructor(aggregateId: string, expectedVersion: number);
5
+ readonly currentVersion: number | undefined;
6
+ constructor(aggregateId: string, expectedVersion: number, currentVersion?: number);
6
7
  }
7
8
  export declare class IdempotentAppendConflictError extends Error {
8
9
  readonly tenantId: string;
@@ -9,8 +9,12 @@
9
9
  export class VersionConflictError extends Error {
10
10
  aggregateId;
11
11
  expectedVersion;
12
- constructor(aggregateId, expectedVersion) {
13
- super(`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`);
12
+ // Undefined when the conflict surfaced as a bare unique violation with no lookup possible.
13
+ currentVersion;
14
+ constructor(aggregateId, expectedVersion, currentVersion) {
15
+ super(`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}` +
16
+ (currentVersion === undefined ? "" : `, stream is at version ${currentVersion}`));
17
+ this.currentVersion = currentVersion;
14
18
  this.name = "VersionConflictError";
15
19
  this.aggregateId = aggregateId;
16
20
  this.expectedVersion = expectedVersion;