@cosmicdrift/kumiko-framework 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -3,7 +3,8 @@ import { streamSSE } from "hono/streaming";
3
3
  import { tenantChannel } from "../engine/constants.js";
4
4
  import { isAnonymousSessionUser } from "../engine/system-user.js";
5
5
  import { Routes } from "./api-constants.js";
6
- import { getAuthTokenExpiry, getUser } from "./auth-middleware.js";
6
+ import { getAuthTokenExpiry, getStaleCredentialCode, getUser } from "./auth-middleware.js";
7
+ import { clearSessionCookies, getSessionCookies } from "./session-cookies.js";
7
8
  import { accessInvalidationCredentialFor } from "./sse-broker.js";
8
9
  import { scheduleTokenExpiry } from "./token-expiry-timer.js";
9
10
  /**
@@ -35,6 +36,10 @@ export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
35
36
  function isEntityEventData(data) {
36
37
  return typeof data["aggregateType"] === "string";
37
38
  }
39
+ // First frame on a stream that was downgraded to anonymous because the browser's auth
40
+ // cookie is stale. A downgraded stream answers 200, so EventSource never reports the
41
+ // session end by itself.
42
+ const SESSION_ENDED_SSE_EVENT = "session-ended";
38
43
  // Whitelist, never a spread: the wire frame is a signal without field
39
44
  // values even if a broker or pusher put a payload into event.data.
40
45
  function toEntitySignalWire(event, data) {
@@ -92,7 +97,17 @@ export function createSseRoute(broker, options) {
92
97
  // JWT expiry raises no access-invalidation event, so the stream must end
93
98
  // itself when the token it was opened with expires.
94
99
  const tokenExpiresAtSec = getAuthTokenExpiry(c);
100
+ const staleCredentialCode = getStaleCredentialCode(c);
101
+ // JS cannot remove the HttpOnly auth cookie, so the response does.
102
+ if (staleCredentialCode !== undefined)
103
+ clearSessionCookies(c, getSessionCookies(c));
95
104
  return streamSSE(c, async (stream) => {
105
+ if (staleCredentialCode !== undefined) {
106
+ await stream.writeSSE({
107
+ event: SESSION_ENDED_SSE_EVENT,
108
+ data: JSON.stringify({ code: staleCredentialCode }),
109
+ });
110
+ }
96
111
  let resolveEnded;
97
112
  const ended = new Promise((resolve) => {
98
113
  resolveEnded = resolve;
@@ -1,4 +1,4 @@
1
1
  export type { BunDbConnection, BunDbConnectionOptions, BunDbRunner, BunDbTx, PgListenClient, } from "./connection.js";
2
2
  export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
3
3
  export type { InnerJoinRow, InnerJoinSpec, InsertOnConflictDoNothingOptions, JoinColumnPair, JsonTextMatch, SelectOptions, TableInfo, WhereObject, WhereOperator, WhereValue, } from "./query.js";
4
- export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, selectInnerJoin, selectMany, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
4
+ export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
@@ -1,4 +1,4 @@
1
1
  // bun-db: Bun.sql-basierte DB-API ohne drizzle.
2
2
  // Production-Pfad nach drizzle-removal.
3
3
  export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
4
- export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
4
+ export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
@@ -2,15 +2,22 @@ import type { AggregateRow, AggregateSpec } from "@cosmicdrift/kumiko-types/aggr
2
2
  import type { SelectOptions, WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
3
3
  import type { EntityTableMeta } from "../db/entity-table-meta.js";
4
4
  import { type NotExecutorOnly } from "../db/table-builder.js";
5
+ import { asEntityTableMeta, requireEntityTableMeta } from "../db/table-introspection.js";
5
6
  import type { DbRunner } from "@cosmicdrift/kumiko-types/db-connection";
6
7
  import type { BunDbRunner } from "./connection.js";
7
- export declare function asEntityTableMeta(table: unknown): EntityTableMeta | undefined;
8
- export declare function requireEntityTableMeta(table: unknown, tableName: string): EntityTableMeta;
9
- type RawClient = {
8
+ export { asEntityTableMeta, requireEntityTableMeta };
9
+ export type RawClient = {
10
10
  unsafe: <TRow = unknown>(sql: string, params?: readonly unknown[]) => Promise<readonly TRow[]>;
11
- begin: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>;
11
+ begin?: <T>(fn: (tx: BunDbRunner) => Promise<T>) => Promise<T>;
12
+ savepoint?: <T>(fn: (sp: unknown) => Promise<T>) => Promise<T>;
13
+ release?: unknown;
12
14
  };
15
+ export type RawClientWithBegin = RawClient & {
16
+ begin: NonNullable<RawClient["begin"]>;
17
+ };
18
+ export declare function hasBegin(client: RawClient): client is RawClientWithBegin;
13
19
  export declare function asRawClient(db: unknown): RawClient;
20
+ export declare function runnerCanBeginTransaction(db: unknown): boolean;
14
21
  export declare function runInSavepoint<T>(tx: unknown, fn: (sp: unknown) => Promise<T>): Promise<T>;
15
22
  export declare function runInSavepointIfSupported<T>(db: unknown, fn: (sp: DbRunner) => Promise<T>): Promise<T>;
16
23
  export type AnyDb = BunDbRunner | unknown;
@@ -26,7 +33,7 @@ export type TableInfo = {
26
33
  readonly hasColumn: (fieldOrColumn: string) => boolean;
27
34
  };
28
35
  export declare function extractTableInfo(table: TableLike): TableInfo;
29
- export declare function coerceRow<T extends Record<string, unknown>>(row: T, info: TableInfo): T;
36
+ export declare function coerceRow<TOut = Record<string, unknown>>(row: Record<string, unknown>, info: TableInfo): TOut;
30
37
  export declare function isClosedConnectionError(err: unknown): boolean;
31
38
  export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
32
39
  export declare function unsafeReadRetrying<TRow>(db: AnyDb, sqlText: string, params: readonly unknown[]): Promise<readonly TRow[]>;
@@ -18,16 +18,17 @@
18
18
  // - drizzle pgTable (legacy, has Symbol-based tableName) — extracted via
19
19
  // drizzle's getTableName + getTableColumns (drizzle kept only as a type
20
20
  // reference, never as a runtime API call)
21
- import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
22
21
  // Static import (not getTemporal()): coercion works without an installed
23
22
  // global and returns the same Temporal classes as the rest of the repo (#1480).
24
23
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
24
+ import { isOneOf, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
25
25
  import { requestContext } from "../api/request-context.js";
26
26
  import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
27
27
  import { isPiiCiphertext } from "../crypto/pii-ciphertext-format.js";
28
28
  import { SQL_EXPR_BRAND } from "../db/dialect.js";
29
29
  import { extractPgError } from "../db/pg-error.js";
30
30
  import { toSnakeCase } from "../db/table-builder.js";
31
+ import { asEntityTableMeta, requireEntityTableMeta } from "../db/table-introspection.js";
31
32
  import { camelCase as envCamelCase } from "../env/index.js";
32
33
  import { InternalError } from "../errors/index.js";
33
34
  import { createFallbackLogger } from "../logging/index.js";
@@ -48,43 +49,20 @@ function snakeToCamel(key) {
48
49
  // enumerable props, so an entity field named `source`/`columns`/`tableName`/…
49
50
  // would overwrite the matching meta key — reading the meta from the symbol is
50
51
  // the only collision-safe path.
51
- function isEntityTableMeta(v) {
52
- return (v !== null &&
53
- typeof v === "object" &&
54
- typeof v.tableName === "string" &&
55
- Array.isArray(v.columns) &&
56
- Array.isArray(v.indexes) &&
57
- (v.source === "managed" || v.source === "unmanaged"));
58
- }
59
- // Resolve any framework table input to its canonical EntityTableMeta:
60
- // - table()/buildEntityTable outputs carry it under KUMIKO_META_SYMBOL, immune
61
- // to a column-handle shadowing a meta key.
62
- // - deriveEntityTableMeta / defineUnmanagedTable return a plain meta with no
63
- // handle-spread, so its structural shape is itself unshadowable.
64
- export function asEntityTableMeta(table) {
65
- if (table === null || typeof table !== "object")
66
- return undefined;
67
- const fromSymbol = table[KUMIKO_META_SYMBOL];
68
- if (isEntityTableMeta(fromSymbol))
69
- return fromSymbol;
70
- return isEntityTableMeta(table) ? table : undefined;
52
+ export { asEntityTableMeta, requireEntityTableMeta };
53
+ export function hasBegin(client) {
54
+ return typeof client.begin === "function";
71
55
  }
72
- export function requireEntityTableMeta(table, tableName) {
73
- const meta = asEntityTableMeta(table);
74
- if (!meta) {
75
- throw new Error(`${tableName}: table carries no EntityTableMeta — built via table()?`);
76
- }
77
- return meta;
56
+ function isRawClient(value) {
57
+ return typeof propertyOf(value, "unsafe") === "function";
78
58
  }
79
59
  export function asRawClient(db) {
80
- const dbAny = db;
81
60
  // Direct: Bun.SQL / postgres-js Sql / postgres-js TransactionSql. All three
82
61
  // expose `.unsafe`; only Sql/Bun.SQL have `.begin` — TransactionSql uses
83
62
  // `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
84
63
  // need `.begin` (e.g. `transaction()`) verify it themselves.
85
- if (typeof dbAny["unsafe"] === "function") {
86
- return dbAny;
87
- }
64
+ if (isRawClient(db))
65
+ return db;
88
66
  // TenantDb (ctx.db) has no unfiltered escape via asRawClient anymore — fail
89
67
  // closed instead of silently unwrapping a tenant-scoped handle.
90
68
  if (isTenantDbShape(db)) {
@@ -93,18 +71,18 @@ export function asRawClient(db) {
93
71
  "{ reason } on the handler/hook (r.systemScope() features: ctx.systemDb.unsafeRaw(reason)).");
94
72
  }
95
73
  // Drizzle DbConnection (legacy compat): $client = postgres-js Sql.
96
- const $client = dbAny["$client"];
97
- if ($client && typeof $client["unsafe"] === "function") {
74
+ const $client = propertyOf(db, "$client");
75
+ if (isRawClient($client))
98
76
  return $client;
99
- }
100
77
  // Drizzle pg-transaction (legacy compat): session.client = postgres-js Sql.
101
- const session = dbAny["session"];
102
- const sessionClient = session?.["client"];
103
- if (sessionClient && typeof sessionClient["unsafe"] === "function") {
78
+ const sessionClient = propertyOf(propertyOf(db, "session"), "client");
79
+ if (isRawClient(sessionClient))
104
80
  return sessionClient;
105
- }
106
81
  throw new Error("bun-db: db argument has no .unsafe() — pass Bun.SQL, postgres-js Sql, or a transaction handle.");
107
82
  }
83
+ export function runnerCanBeginTransaction(db) {
84
+ return hasBegin(asRawClient(db));
85
+ }
108
86
  // postgres.js / Bun.SQL reject the WHOLE begin() when any statement inside
109
87
  // it errored — even if the JS error was caught (the driver tracks per-block
110
88
  // query failures). Error confinement therefore must go through the driver's
@@ -131,7 +109,7 @@ export async function runInSavepoint(tx, fn) {
131
109
  // to paper over by re-running `fn` without the savepoint's isolation.
132
110
  export async function runInSavepointIfSupported(db, fn) {
133
111
  const raw = asRawClient(db);
134
- // @cast-boundary driver savepoint handle → DbRunner at the single seam
112
+ // @cast-boundary db-runner savepoint handle → DbRunner at the single seam
135
113
  const asRunner = (sp) => sp;
136
114
  if (typeof raw.savepoint !== "function")
137
115
  return fn(asRunner(db));
@@ -156,15 +134,14 @@ export async function runInSavepointIfSupported(db, fn) {
156
134
  function isTenantDbShape(db) {
157
135
  if (typeof db !== "object" || db === null)
158
136
  return false;
159
- const d = db;
160
- if (typeof d["unsafe"] === "function")
137
+ if (typeof propertyOf(db, "unsafe") === "function")
161
138
  return false;
162
- return (typeof d["selectMany"] === "function" &&
163
- typeof d["fetchOne"] === "function" &&
164
- typeof d["insertOne"] === "function" &&
165
- typeof d["updateMany"] === "function" &&
166
- typeof d["deleteMany"] === "function" &&
167
- "tenantId" in d);
139
+ return (typeof propertyOf(db, "selectMany") === "function" &&
140
+ typeof propertyOf(db, "fetchOne") === "function" &&
141
+ typeof propertyOf(db, "insertOne") === "function" &&
142
+ typeof propertyOf(db, "updateMany") === "function" &&
143
+ typeof propertyOf(db, "deleteMany") === "function" &&
144
+ "tenantId" in db);
168
145
  }
169
146
  function tenantDbDelegate(db) {
170
147
  return isTenantDbShape(db) ? db : undefined;
@@ -182,6 +159,9 @@ function assertNotTenantScoped(db, fnName) {
182
159
  const WHERE_OPERATOR_KEYS = ["gt", "gte", "lt", "lte", "ne", "in", "like", "jsonText"];
183
160
  const _whereOperatorKeysExhaustive = true;
184
161
  void _whereOperatorKeysExhaustive;
162
+ function isJsonTextMatchList(value) {
163
+ return Array.isArray(value);
164
+ }
185
165
  function isWhereOperator(v) {
186
166
  if (v === null || typeof v !== "object" || Array.isArray(v))
187
167
  return false;
@@ -190,7 +170,7 @@ function isWhereOperator(v) {
190
170
  const keys = Object.keys(v);
191
171
  if (keys.length === 0)
192
172
  return false;
193
- return keys.every((k) => WHERE_OPERATOR_KEYS.includes(k));
173
+ return keys.every((k) => isOneOf(WHERE_OPERATOR_KEYS, k));
194
174
  }
195
175
  export function extractTableInfo(table) {
196
176
  const meta = asEntityTableMeta(table);
@@ -236,10 +216,13 @@ function quoteIdent(name) {
236
216
  //
237
217
  // Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
238
218
  // params — convert to ISO string when the column pgType is timestamptz.
219
+ function toStringTagOf(v) {
220
+ return typeof v === "object" && v !== null && Symbol.toStringTag in v
221
+ ? v[Symbol.toStringTag]
222
+ : undefined;
223
+ }
239
224
  function isTemporalInstant(v) {
240
- return (typeof v === "object" &&
241
- v !== null &&
242
- v[Symbol.toStringTag] === "Temporal.Instant");
225
+ return toStringTagOf(v) === "Temporal.Instant";
243
226
  }
244
227
  // Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
245
228
  // drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
@@ -289,15 +272,14 @@ function instantFromDriver(value) {
289
272
  return null;
290
273
  }
291
274
  function isTemporalPlainDate(v) {
292
- return (typeof v === "object" &&
293
- v !== null &&
294
- v[Symbol.toStringTag] === "Temporal.PlainDate");
275
+ return toStringTagOf(v) === "Temporal.PlainDate";
295
276
  }
296
277
  function isTemporalPlainDateTimeLike(v) {
297
- return (typeof v === "object" &&
278
+ const tag = toStringTagOf(v);
279
+ return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
280
+ typeof v === "object" &&
298
281
  v !== null &&
299
- (v[Symbol.toStringTag] === "Temporal.PlainDateTime" ||
300
- v[Symbol.toStringTag] === "Temporal.ZonedDateTime") &&
282
+ "toPlainDate" in v &&
301
283
  typeof v.toPlainDate === "function");
302
284
  }
303
285
  // Verified empirically against Bun.SQL (kumiko-framework#1924): a `date`
@@ -353,6 +335,10 @@ function plainDateFromDriver(value, column) {
353
335
  //
354
336
  // Unknown columns (computed/aliased) pass through unchanged.
355
337
  export function coerceRow(row, info) {
338
+ // @cast-boundary db-row — the driver row (raw column names/types) becomes the caller's entity row shape here, and only here
339
+ return coerceDriverRow(row, info);
340
+ }
341
+ function coerceDriverRow(row, info) {
356
342
  const out = {};
357
343
  let changed = false;
358
344
  for (const key of Object.keys(row)) {
@@ -422,7 +408,7 @@ export function coerceRow(row, info) {
422
408
  changed = true;
423
409
  out[fieldName] = coerced;
424
410
  }
425
- return (changed ? out : row);
411
+ return changed ? out : row;
426
412
  }
427
413
  function coerceRows(rows, info) {
428
414
  return rows.map((r) => coerceRow(r, info));
@@ -568,9 +554,15 @@ function buildWhereClause(info, where, startIndex, qualifier) {
568
554
  if (pgType !== "jsonb") {
569
555
  throw new Error(`where: jsonText requires a jsonb column, "${col}" is ${pgType}`);
570
556
  }
571
- const matches = Array.isArray(value.jsonText) ? value.jsonText : [value.jsonText];
557
+ const matches = isJsonTextMatchList(value.jsonText) ? value.jsonText : [value.jsonText];
558
+ if (matches.length === 0) {
559
+ conditions.push("FALSE");
560
+ continue;
561
+ }
572
562
  for (const match of matches) {
573
- if (!Array.isArray(match.keys) ||
563
+ if (match === null ||
564
+ typeof match !== "object" ||
565
+ !Array.isArray(match.keys) ||
574
566
  match.keys.length === 0 ||
575
567
  !match.keys.every((k) => typeof k === "string") ||
576
568
  typeof match.eq !== "string") {
@@ -599,7 +591,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
599
591
  like: "LIKE",
600
592
  };
601
593
  for (const [opKey, opSym] of Object.entries(opMap)) {
602
- const opVal = value[opKey];
594
+ const opVal = Reflect.get(value, opKey);
603
595
  if (opVal === undefined)
604
596
  continue;
605
597
  // Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
@@ -638,7 +630,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
638
630
  values.push(p.bound);
639
631
  }
640
632
  }
641
- const inVal = value["in"];
633
+ const inVal = value.in;
642
634
  if (Array.isArray(inVal)) {
643
635
  if (pgType === "jsonb") {
644
636
  conditions.push(jsonbContainsAny(col, inVal));
@@ -711,11 +703,9 @@ export function isClosedConnectionError(err) {
711
703
  function isPooledClient(raw) {
712
704
  if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
713
705
  return false;
714
- // @cast-boundary driver handle shape — begin/savepoint/release are optional across drivers
715
- const r = raw;
716
- return (typeof r.begin === "function" &&
717
- typeof r.savepoint !== "function" &&
718
- typeof r.release !== "function");
706
+ return (typeof propertyOf(raw, "begin") === "function" &&
707
+ typeof propertyOf(raw, "savepoint") !== "function" &&
708
+ typeof propertyOf(raw, "release") !== "function");
719
709
  }
720
710
  const closedConnectionLog = createFallbackLogger("bun-db");
721
711
  // The driver rejects in-flight queries with this AbortError (no code) when the
@@ -803,8 +793,7 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
803
793
  }
804
794
  const DEFAULT_POOL_MAX = 10;
805
795
  function poolMaxOf(raw) {
806
- // @cast-boundary driver pool options — both postgres-js and Bun.SQL expose options.max
807
- const max = raw.options?.max;
796
+ const max = propertyOf(propertyOf(raw, "options"), "max");
808
797
  return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
809
798
  }
810
799
  // Exported so raw-SQL query modules outside bun-db (e.g. bundled-features'
@@ -814,7 +803,7 @@ function poolMaxOf(raw) {
814
803
  export async function unsafeReadRetrying(db, sqlText, params) {
815
804
  const raw = asRawClient(db);
816
805
  try {
817
- return (await raw.unsafe(sqlText, params));
806
+ return await raw.unsafe(sqlText, params);
818
807
  }
819
808
  catch (err) {
820
809
  if (!isPooledClient(raw) || !isClosedConnectionError(err)) {
@@ -826,7 +815,7 @@ export async function unsafeReadRetrying(db, sqlText, params) {
826
815
  let lastErr = err;
827
816
  for (let attempt = 0; attempt < maxRetries; attempt++) {
828
817
  try {
829
- return (await raw.unsafe(sqlText, params));
818
+ return await raw.unsafe(sqlText, params);
830
819
  }
831
820
  catch (retryErr) {
832
821
  lastErr = retryErr;
@@ -872,7 +861,7 @@ export async function selectMany(db, table, where, options) {
872
861
  }
873
862
  sqlText += ` LIMIT ${options.limit}`;
874
863
  }
875
- const raw = (await unsafeReadRetrying(db, sqlText, values));
864
+ const raw = await unsafeReadRetrying(db, sqlText, values);
876
865
  return coerceRows(raw, info);
877
866
  }
878
867
  // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
@@ -937,7 +926,7 @@ export async function selectInnerJoin(db, spec) {
937
926
  sqlText += ` WHERE ${conditions.join(" AND ")}`;
938
927
  if (spec.limit !== undefined)
939
928
  sqlText += ` LIMIT ${spec.limit}`;
940
- const raw = (await unsafeReadRetrying(db, sqlText, values));
929
+ const raw = await unsafeReadRetrying(db, sqlText, values);
941
930
  return raw.map((row) => {
942
931
  const leftRow = {};
943
932
  const rightRow = {};
@@ -987,10 +976,10 @@ export async function insertMany(db, table, rows) {
987
976
  valuesClauses.push(`(${placeholders.join(", ")})`);
988
977
  }
989
978
  const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES ${valuesClauses.join(", ")} RETURNING *`;
990
- const raw = (await unsafeLoggingClosedConnection(db, sqlText, params, {
979
+ const raw = await unsafeLoggingClosedConnection(db, sqlText, params, {
991
980
  operation: "insertMany",
992
981
  table: info.name,
993
- }));
982
+ });
994
983
  return coerceRows(raw, info);
995
984
  }
996
985
  // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
@@ -1020,10 +1009,10 @@ export async function insertOne(db, table, values) {
1020
1009
  })
1021
1010
  .join(", ");
1022
1011
  const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders}) RETURNING *`;
1023
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1012
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1024
1013
  operation: "insertOne",
1025
1014
  table: info.name,
1026
- }));
1015
+ });
1027
1016
  const first = rows[0];
1028
1017
  if (!first)
1029
1018
  return undefined;
@@ -1063,10 +1052,10 @@ export async function updateMany(db, table, set, where) {
1063
1052
  values.push(v);
1064
1053
  }
1065
1054
  sqlText += " RETURNING *";
1066
- const raw = (await unsafeLoggingClosedConnection(db, sqlText, values, {
1055
+ const raw = await unsafeLoggingClosedConnection(db, sqlText, values, {
1067
1056
  operation: "updateMany",
1068
1057
  table: info.name,
1069
- }));
1058
+ });
1070
1059
  return coerceRows(raw, info);
1071
1060
  }
1072
1061
  export async function deleteMany(db, table, where) {
@@ -1135,7 +1124,7 @@ export async function countWhere(db, table, where = {}) {
1135
1124
  sqlText += ` WHERE ${w.sqlText}`;
1136
1125
  values = w.values;
1137
1126
  }
1138
- const rows = (await unsafeReadRetrying(db, sqlText, values));
1127
+ const rows = await unsafeReadRetrying(db, sqlText, values);
1139
1128
  return rows[0]?.count ?? 0;
1140
1129
  }
1141
1130
  const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
@@ -1171,7 +1160,7 @@ function aggregateMeasureSql(info, measure) {
1171
1160
  case "avg":
1172
1161
  return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
1173
1162
  default:
1174
- throw new Error(`aggregateWhere: unknown measure fn "${String(measure.fn)}"`);
1163
+ throw new Error(`aggregateWhere: unknown measure fn "${String(Reflect.get(measure, "fn"))}"`);
1175
1164
  }
1176
1165
  }
1177
1166
  function aggregateDimensionSql(info, dimension, index, params) {
@@ -1233,7 +1222,7 @@ export async function aggregateWhere(db, table, spec, where = {}) {
1233
1222
  params.push(...w.values);
1234
1223
  }
1235
1224
  sqlText += aggregateGroupOrderLimitSql(spec, dimensions.length);
1236
- const raw = (await unsafeReadRetrying(db, sqlText, params));
1225
+ const raw = await unsafeReadRetrying(db, sqlText, params);
1237
1226
  return raw.map((row) => {
1238
1227
  const value = row["value"];
1239
1228
  return {
@@ -1277,10 +1266,10 @@ export async function upsertOnConflict(db, table, values, options) {
1277
1266
  }
1278
1267
  const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
1279
1268
  const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
1280
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1269
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1281
1270
  operation: "upsertOnConflict",
1282
1271
  table: info.name,
1283
- }));
1272
+ });
1284
1273
  const first = rows[0];
1285
1274
  if (!first)
1286
1275
  return undefined;
@@ -1305,10 +1294,10 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
1305
1294
  const { sqlPrefix, params } = buildInsertSql(info, entries);
1306
1295
  const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
1307
1296
  const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO NOTHING RETURNING *`;
1308
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1297
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1309
1298
  operation: "insertOnConflictDoNothing",
1310
1299
  table: info.name,
1311
- }));
1300
+ });
1312
1301
  const first = rows[0];
1313
1302
  if (!first)
1314
1303
  return undefined;
@@ -1357,10 +1346,10 @@ export async function incrementCounter(db, table, values, increments, options =
1357
1346
  }
1358
1347
  const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
1359
1348
  const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
1360
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1349
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1361
1350
  operation: "incrementCounter",
1362
1351
  table: info.name,
1363
- }));
1352
+ });
1364
1353
  const first = rows[0];
1365
1354
  if (!first)
1366
1355
  return undefined;
@@ -1389,10 +1378,10 @@ export async function deleteManyBatched(db, table, where, options) {
1389
1378
  const sqlText = `DELETE FROM ${tableQ} WHERE ${idQ} IN (
1390
1379
  SELECT ${idQ} FROM ${tableQ} WHERE ${w.sqlText} LIMIT $${limitIdx}
1391
1380
  ) RETURNING ${idQ}`;
1392
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1381
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1393
1382
  operation: "deleteManyBatched",
1394
1383
  table: info.name,
1395
- }));
1384
+ });
1396
1385
  batches++;
1397
1386
  deleted += rows.length;
1398
1387
  if (rows.length < limit)
@@ -1401,16 +1390,21 @@ export async function deleteManyBatched(db, table, where, options) {
1401
1390
  return { deleted, batches };
1402
1391
  }
1403
1392
  export async function transaction(db, fn) {
1404
- return (await asRawClient(db).begin(async (tx) => fn(tx)));
1393
+ const raw = asRawClient(db);
1394
+ if (!hasBegin(raw)) {
1395
+ throw new InternalError({
1396
+ message: "transaction: runner has no begin() — already inside a transaction or savepoint.",
1397
+ });
1398
+ }
1399
+ return raw.begin(fn);
1405
1400
  }
1406
1401
  // A tx/savepoint handle has no begin(); fail closed instead of nesting silently or TypeError-ing.
1407
1402
  export async function runInNewTransaction(db, fn) {
1408
1403
  const raw = asRawClient(db);
1409
- if (typeof raw.begin !== "function") {
1404
+ if (!hasBegin(raw)) {
1410
1405
  throw new InternalError({
1411
1406
  message: "runInNewTransaction: runner has no begin() — already inside a transaction or savepoint.",
1412
1407
  });
1413
1408
  }
1414
- // @cast-boundary driver tx handle → DbRunner at the single seam
1415
- return raw.begin((tx) => fn(tx));
1409
+ return raw.begin(fn);
1416
1410
  }
@@ -3,6 +3,7 @@
3
3
  // A slot's plaintext always wins over its ciphertext sibling, with no request
4
4
  // made at all. Any `*_CIPHERTEXT` in the env that names no requested slot is
5
5
  // ignored — a foreign ciphertext must never fail boot.
6
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  const SCALEWAY_KEY_MANAGER_API_VERSION = "v1alpha1";
7
8
  const DEFAULT_REGION = "fr-par";
8
9
  const DECRYPT_TIMEOUT_MS = 5_000;
@@ -48,15 +49,16 @@ async function decryptCiphertext(ciphertext, keyId, token, region, fetchImpl, lo
48
49
  }
49
50
  let body;
50
51
  try {
51
- body = (await response.json());
52
+ body = await response.json();
52
53
  }
53
54
  catch {
54
55
  throw new Error(`${prefix}Key Manager decrypt for key ${keyId} returned an unparseable response`);
55
56
  }
56
- if (typeof body.plaintext !== "string") {
57
+ const plaintext = propertyOf(body, "plaintext");
58
+ if (typeof plaintext !== "string") {
57
59
  throw new Error(`${prefix}Key Manager decrypt for key ${keyId} returned no plaintext field`);
58
60
  }
59
- return body.plaintext;
61
+ return plaintext;
60
62
  }
61
63
  throw new Error(`${prefix}Key Manager decrypt failed for key ${keyId}: retries exhausted`);
62
64
  }
@@ -14,7 +14,10 @@ export interface EncryptPiiOptions {
14
14
  readonly entityName: string;
15
15
  readonly ownerFallback?: Record<string, unknown>;
16
16
  }
17
- export declare function encryptPiiFieldValues(row: Record<string, unknown>, entity: EntityDefinition, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext, opts: EncryptPiiOptions): Promise<Record<string, unknown>>;
17
+ export type PiiEncryptedRow<TRow extends Record<string, unknown>> = {
18
+ [K in keyof TRow]: TRow[K] extends string ? string : TRow[K] | string;
19
+ };
20
+ export declare function encryptPiiFieldValues<TRow extends Record<string, unknown>>(row: TRow, entity: EntityDefinition, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext, opts: EncryptPiiOptions): Promise<PiiEncryptedRow<TRow>>;
18
21
  export declare function serializeJsonbValue(field: string, value: unknown): string;
19
22
  export declare function decryptPiiFieldValues(row: Record<string, unknown>, piiFields: readonly string[], kms: LocalKeyKmsAdapter, kmsCtx: KmsContext): Promise<Record<string, unknown>>;
20
23
  export declare function configurePiiSubjectKms(adapter: KmsAdapter | undefined): void;
@@ -187,8 +187,8 @@ export async function decryptPiiFieldValues(row, piiFields, kms, kmsCtx) {
187
187
  try {
188
188
  const key = await kms.getKey(subject, kmsCtx);
189
189
  const plain = decryptValue(key, blob, subject, name, hasAad, isJson);
190
- // @cast-boundary crypto-decrypt — AEAD-authenticated plaintext that this module serialized
191
- out[name] = isJson ? JSON.parse(plain) : plain;
190
+ const decoded = isJson ? JSON.parse(plain) : plain;
191
+ out[name] = decoded;
192
192
  }
193
193
  catch (e) {
194
194
  // KeyNotFound deliberately propagates: ciphertext without a key row
@@ -43,6 +43,7 @@
43
43
  // `row` null (DELETE-Statements geben keine returning-Row her).
44
44
  // - "skipped" → Event ist kein Auto-Verb (Domain-Event auf demselben
45
45
  // Aggregate). Caller no-op.
46
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
46
47
  import { collectLookupableFields, computeBlindIndexValues } from "../crypto/blind-index.js";
47
48
  import { deleteMany, insertOne, updateMany } from "../db/query.js";
48
49
  import { InternalError } from "../errors/index.js";
@@ -54,8 +55,7 @@ export function parseAutoVerb(event) {
54
55
  if (!event.type.startsWith(prefix))
55
56
  return null;
56
57
  const verb = event.type.slice(prefix.length);
57
- // @cast-boundary: verb is validated against the AutoVerb list before the cast.
58
- return AUTO_VERBS.includes(verb) ? verb : null;
58
+ return isOneOf(AUTO_VERBS, verb) ? verb : null;
59
59
  }
60
60
  /** Idempotente Anwendung eines Auto-Events auf die Entity-Tabelle.
61
61
  * Wird sowohl beim Live-Append (innerhalb der Write-TX) als auch beim
@@ -15,6 +15,15 @@ function canonicalColumnsKey(meta) {
15
15
  .map((c) => `${c.name}|${c.pgType}|${c.notNull}|${c.defaultSql ?? ""}|${c.primaryKey ?? false}|${c.identity ?? false}|${c.bigintJsMode ?? ""}`)
16
16
  .join("\n");
17
17
  }
18
+ function registerPass1Meta(metas, byName, meta, origin) {
19
+ const existing = byName.get(meta.tableName);
20
+ if (existing) {
21
+ throw new Error(`collectTableMetas: table "${meta.tableName}" is registered by ${origin} and ${existing.origin}. ` +
22
+ "Give one of them a different table name.");
23
+ }
24
+ metas.push(meta);
25
+ byName.set(meta.tableName, { meta, origin });
26
+ }
18
27
  export function collectTableMetas(features) {
19
28
  const metas = [];
20
29
  const byName = new Map();
@@ -37,15 +46,10 @@ export function collectTableMetas(features) {
37
46
  assertBackingTableSuperset(name, fieldMeta, tableMeta);
38
47
  meta = tableMeta;
39
48
  }
40
- metas.push(meta);
41
- byName.set(meta.tableName, { meta, origin: `entity "${name}" (${feature.name})` });
49
+ registerPass1Meta(metas, byName, meta, `entity "${name}" (${feature.name})`);
42
50
  }
43
51
  for (const entry of Object.values(feature.storeTables)) {
44
- metas.push(entry.meta);
45
- byName.set(entry.meta.tableName, {
46
- meta: entry.meta,
47
- origin: `storeTable "${entry.name}" (${feature.name})`,
48
- });
52
+ registerPass1Meta(metas, byName, entry.meta, `storeTable "${entry.name}" (${feature.name})`);
49
53
  }
50
54
  }
51
55
  // Pass 2: table-tragende Registrierungen. Zwei Pässe, damit eine Entity-
package/dist/db/cursor.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  // String-basiert damit UUIDs (Default seit Sprint F) + Integer-Auto-Increment
2
3
  // durch denselben Cursor-Pfad laufen. UUIDv7 erfüllt die lex-Monotonie
3
4
  // (time-ordered Prefix); UUIDv4 nicht — wer den nutzt, kriegt inkorrekte
@@ -28,7 +29,7 @@ function parseKeysetPayload(decoded) {
28
29
  catch {
29
30
  return null;
30
31
  }
31
- if (typeof raw !== "object" || raw === null)
32
+ if (!isPlainObject(raw))
32
33
  return null;
33
34
  const { v, i } = raw;
34
35
  if (typeof i !== "string" || i === "")