@cosmicdrift/kumiko-framework 0.361.0 → 0.363.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 (126) hide show
  1. package/dist/api/auth-middleware.js +7 -5
  2. package/dist/api/redis-sse-broker.js +10 -8
  3. package/dist/api/routes.js +2 -2
  4. package/dist/api/server.js +3 -0
  5. package/dist/bun-db/query.d.ts +6 -4
  6. package/dist/bun-db/query.js +66 -92
  7. package/dist/crypto/kek-source.js +5 -3
  8. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  9. package/dist/crypto/pii-field-encryption.js +2 -2
  10. package/dist/db/apply-entity-event.js +2 -2
  11. package/dist/db/cursor.js +2 -1
  12. package/dist/db/dialect.d.ts +1 -0
  13. package/dist/db/dialect.js +28 -13
  14. package/dist/db/entity-table-from-registry.js +1 -1
  15. package/dist/db/entity-table-meta.js +5 -13
  16. package/dist/db/event-store-executor-context.js +6 -6
  17. package/dist/db/event-store-executor-read.js +9 -9
  18. package/dist/db/migrate-generator.js +5 -3
  19. package/dist/db/migrate-runner.js +31 -35
  20. package/dist/db/parent-ref-clause.js +2 -2
  21. package/dist/db/queries/backfill-pii.js +11 -9
  22. package/dist/db/queries/es-ops.js +1 -1
  23. package/dist/db/queries/event-consumer.d.ts +6 -6
  24. package/dist/db/queries/event-consumer.js +17 -17
  25. package/dist/db/queries/event-store-admin.js +6 -6
  26. package/dist/db/queries/event-store-transfer.js +2 -2
  27. package/dist/db/queries/event-store.d.ts +1 -1
  28. package/dist/db/queries/event-store.js +2 -2
  29. package/dist/db/queries/pii-backfill-state.js +2 -2
  30. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  31. package/dist/db/queries/raw-sql.js +1 -1
  32. package/dist/db/queries/test-stack.js +1 -1
  33. package/dist/db/reference-data.js +3 -5
  34. package/dist/db/table-builder.js +6 -4
  35. package/dist/db/table-introspection.d.ts +16 -0
  36. package/dist/db/table-introspection.js +76 -0
  37. package/dist/db/tenant-db.d.ts +3 -2
  38. package/dist/db/tenant-db.js +4 -10
  39. package/dist/derivatives/variant-key.js +2 -1
  40. package/dist/engine/boot-validator/access-declarations.js +29 -18
  41. package/dist/engine/boot-validator/card-meta-overflow.js +5 -2
  42. package/dist/engine/boot-validator/entity-list-screens.js +1 -1
  43. package/dist/engine/boot-validator/pii-retention.js +1 -1
  44. package/dist/engine/boot-validator/screens.js +2 -1
  45. package/dist/engine/build-app-schema.js +1 -0
  46. package/dist/engine/changeset-changes.js +3 -3
  47. package/dist/engine/define-handler.js +3 -6
  48. package/dist/engine/embedded-derived.js +10 -6
  49. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  50. package/dist/engine/factories.js +5 -6
  51. package/dist/engine/feature-ast/extractors/events.js +43 -33
  52. package/dist/engine/feature-ast/extractors/round3.d.ts +2 -2
  53. package/dist/engine/feature-ast/extractors/round3.js +4 -4
  54. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  55. package/dist/engine/feature-ast/patcher.js +3 -3
  56. package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -8
  57. package/dist/engine/feature-ast/pattern-change-schema.js +3 -3
  58. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  59. package/dist/engine/feature-ast/render.js +6 -3
  60. package/dist/engine/feature-changelog.d.ts +2 -1
  61. package/dist/engine/feature-changelog.js +8 -6
  62. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  63. package/dist/engine/feature-config-events-jobs.js +20 -5
  64. package/dist/engine/feature-ui-extensions.js +2 -3
  65. package/dist/engine/index.d.ts +2 -1
  66. package/dist/engine/index.js +1 -0
  67. package/dist/engine/ownership.js +7 -20
  68. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  69. package/dist/engine/registry-ingest.d.ts +2 -1
  70. package/dist/engine/registry-ingest.js +85 -2
  71. package/dist/engine/registry-validate.d.ts +1 -0
  72. package/dist/engine/registry-validate.js +38 -8
  73. package/dist/engine/registry.js +2 -1
  74. package/dist/engine/schema-builder.js +1 -3
  75. package/dist/engine/soft-delete-cleanup.js +2 -1
  76. package/dist/engine/steps/call-feature.js +1 -3
  77. package/dist/engine/steps/unsafe-projection-upsert.js +2 -13
  78. package/dist/engine/template-variables.d.ts +29 -0
  79. package/dist/engine/template-variables.js +77 -0
  80. package/dist/engine/tier-resolver-extension.js +2 -3
  81. package/dist/engine/types/index.d.ts +2 -2
  82. package/dist/engine/validate-projection-allowlist.js +12 -13
  83. package/dist/engine/where-rule-lint.js +8 -7
  84. package/dist/errors/reasons.d.ts +1 -0
  85. package/dist/errors/reasons.js +2 -0
  86. package/dist/files/bind-referenced-files.write.d.ts +16 -0
  87. package/dist/files/bind-referenced-files.write.js +109 -0
  88. package/dist/files/feature.d.ts +1 -0
  89. package/dist/files/feature.js +2 -0
  90. package/dist/files/file-routes.d.ts +2 -1
  91. package/dist/files/file-routes.js +10 -3
  92. package/dist/files/local-provider.js +2 -1
  93. package/dist/files/record-write-check.d.ts +1 -0
  94. package/dist/files/record-write-check.js +9 -9
  95. package/dist/i18n/index.d.ts +1 -1
  96. package/dist/i18n/index.js +1 -1
  97. package/dist/i18n/mail-registry.d.ts +2 -0
  98. package/dist/i18n/mail-registry.js +7 -0
  99. package/dist/jobs/job-runner.js +5 -5
  100. package/dist/pipeline/dispatch-query.js +2 -3
  101. package/dist/pipeline/dispatch-shared.js +45 -14
  102. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  103. package/dist/pipeline/index.d.ts +1 -0
  104. package/dist/pipeline/index.js +1 -0
  105. package/dist/pipeline/member-read-only-transaction.js +1 -1
  106. package/dist/pipeline/notify-scope.d.ts +13 -0
  107. package/dist/pipeline/notify-scope.js +32 -0
  108. package/dist/pipeline/system-hooks.js +2 -1
  109. package/dist/pipeline/system-identity-switch.js +2 -2
  110. package/dist/pipeline/write-origin.js +2 -2
  111. package/dist/search/meilisearch-adapter.js +3 -4
  112. package/dist/secrets/stored-envelope.js +7 -7
  113. package/dist/stack/push-entity-projection-tables.js +2 -3
  114. package/dist/stack/table-helpers.js +10 -26
  115. package/dist/stack/test-stack.js +2 -5
  116. package/dist/testing/multipart-helper.d.ts +0 -6
  117. package/dist/testing/multipart-helper.js +5 -5
  118. package/dist/testing/without-ambient-temporal.js +4 -4
  119. package/dist/ui-types/card-column-roles.d.ts +5 -0
  120. package/dist/ui-types/card-column-roles.js +22 -4
  121. package/dist/ui-types/index.d.ts +1 -1
  122. package/dist/ui-types/index.js +1 -1
  123. package/dist/utils/is-plain-object.d.ts +1 -2
  124. package/dist/utils/is-plain-object.js +1 -4
  125. package/package.json +4 -4
  126. package/src/changes.json +88 -0
@@ -1,3 +1,4 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { getCookie } from "hono/cookie";
2
3
  import { TENANT_TEARDOWN_STATUSES } from "../engine/active-membership.js";
3
4
  import { createAnonymousUser } from "../engine/system-user.js";
@@ -482,17 +483,18 @@ async function requestsCancelDestruction(c) {
482
483
  return false;
483
484
  try {
484
485
  const path = c.req.path;
485
- const body = (await c.req.raw.clone().json());
486
+ const body = await c.req.raw.clone().json();
486
487
  if (path === "/api/write") {
487
- return body.type === TENANT_LIFECYCLE_CANCEL_QN;
488
+ return propertyOf(body, "type") === TENANT_LIFECYCLE_CANCEL_QN;
488
489
  }
489
490
  if (path === "/api/batch") {
491
+ const commands = propertyOf(body, "commands");
490
492
  // every(), not some(): a batch mixing the cancel command with other
491
493
  // writes must NOT wave the whole batch through the teardown gate —
492
494
  // only a batch consisting solely of cancel-destruction is exempt.
493
- return (Array.isArray(body.commands) &&
494
- body.commands.length > 0 &&
495
- body.commands.every((command) => command.type === TENANT_LIFECYCLE_CANCEL_QN));
495
+ return (Array.isArray(commands) &&
496
+ commands.length > 0 &&
497
+ commands.every((command) => propertyOf(command, "type") === TENANT_LIFECYCLE_CANCEL_QN));
496
498
  }
497
499
  }
498
500
  catch {
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { redisChannelPrefixFromEnv, redisClientOptionsFromEnv, } from "../redis/client.js";
2
3
  import { createRedisPubSubSignal } from "../redis/pubsub-signal.js";
3
4
  import { createSseBroker, readNonEmptyStringArray, } from "./sse-broker.js";
@@ -37,7 +38,9 @@ export function createDefaultSseBroker(env = process.env) {
37
38
  function isSseEvent(value) {
38
39
  return (typeof value === "object" &&
39
40
  value !== null &&
41
+ "type" in value &&
40
42
  typeof value.type === "string" &&
43
+ "data" in value &&
41
44
  typeof value.data === "object" &&
42
45
  value.data !== null);
43
46
  }
@@ -50,25 +53,24 @@ function isSseEvent(value) {
50
53
  // sparing something it doesn't understand: fail-closed on a mixed rolling
51
54
  // deploy. Anything malformed also falls back to userwide.
52
55
  function parseInvalidationScope(payload) {
53
- if (typeof payload !== "object" || payload === null)
56
+ if (!isPlainObject(payload))
54
57
  return { kind: "user" };
55
- const record = payload;
56
- if ("scope" in record) {
57
- const { scope } = record;
58
+ if ("scope" in payload) {
59
+ const { scope } = payload;
58
60
  if (scope === "sessions") {
59
- const sessionIds = readNonEmptyStringArray(record["sessionIds"]);
61
+ const sessionIds = readNonEmptyStringArray(payload["sessionIds"]);
60
62
  if (sessionIds)
61
63
  return { kind: "sessions", sessionIds };
62
64
  }
63
65
  if (scope === "pat-tokens") {
64
- const tokenIds = readNonEmptyStringArray(record["tokenIds"]);
66
+ const tokenIds = readNonEmptyStringArray(payload["tokenIds"]);
65
67
  if (tokenIds)
66
68
  return { kind: "pat-tokens", tokenIds };
67
69
  }
68
70
  return { kind: "user" };
69
71
  }
70
- if ("keptSessionId" in record) {
71
- const { keptSessionId } = record;
72
+ if ("keptSessionId" in payload) {
73
+ const { keptSessionId } = payload;
72
74
  if (typeof keptSessionId === "string" && keptSessionId.length > 0) {
73
75
  return { kind: "all-except-session", keptSessionId };
74
76
  }
@@ -172,7 +172,7 @@ export function createApiRoutes(dispatcher, options = {}) {
172
172
  // in-flight .next(), so awaiting here would block the response until
173
173
  // that pending pull resolves (which may be never for an idle stream).
174
174
  void generator.return(undefined).catch(() => { });
175
- return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary non-standard client-closed-request status, Hono's union doesn't include it
175
+ return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary third-party-type-gap — non-standard client-closed-request status, Hono's union doesn't include it
176
176
  }
177
177
  return streamSSE(c, async (stream) => {
178
178
  stream.onAbort(() => {
@@ -394,7 +394,7 @@ function clientAbortResponse(c, requestId, type) {
394
394
  logClientWarn("request aborted by client", requestId, type, {
395
395
  status: CLIENT_CLOSED_REQUEST_STATUS,
396
396
  });
397
- return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary non-standard client-closed-request status, Hono's union doesn't include it
397
+ return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary third-party-type-gap — non-standard client-closed-request status, Hono's union doesn't include it
398
398
  }
399
399
  // For /query + /command: no isSuccess on success (just { data } / {ok}), so we
400
400
  // keep the same lean shape on failure — only the `error` key.
@@ -704,6 +704,9 @@ export function buildServer(options) {
704
704
  }),
705
705
  ...(routeOptions.accessGuard ? { accessGuard: routeOptions.accessGuard } : {}),
706
706
  ...(routeOptions.privilegedRoles ? { privilegedRoles: routeOptions.privilegedRoles } : {}),
707
+ ...(routeOptions.unclaimedUploadReaders
708
+ ? { unclaimedUploadReaders: routeOptions.unclaimedUploadReaders }
709
+ : {}),
707
710
  ...(routeOptions.maxUploadSize ? { maxUploadSize: routeOptions.maxUploadSize } : {}),
708
711
  }));
709
712
  }
@@ -2,13 +2,15 @@ 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;
8
+ export { asEntityTableMeta, requireEntityTableMeta };
9
9
  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
  };
13
15
  export declare function asRawClient(db: unknown): RawClient;
14
16
  export declare function runInSavepoint<T>(tx: unknown, fn: (sp: unknown) => Promise<T>): Promise<T>;
@@ -26,7 +28,7 @@ export type TableInfo = {
26
28
  readonly hasColumn: (fieldOrColumn: string) => boolean;
27
29
  };
28
30
  export declare function extractTableInfo(table: TableLike): TableInfo;
29
- export declare function coerceRow<T extends Record<string, unknown>>(row: T, info: TableInfo): T;
31
+ export declare function coerceRow<T>(row: T, info: TableInfo): T;
30
32
  export declare function isClosedConnectionError(err: unknown): boolean;
31
33
  export declare function describeClosedConnectionFailure(raw: unknown, err: unknown, attempts: number): Record<string, unknown> | undefined;
32
34
  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, isPlainObject, 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,17 @@ 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;
71
- }
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;
52
+ export { asEntityTableMeta, requireEntityTableMeta };
53
+ function isRawClient(value) {
54
+ return typeof propertyOf(value, "unsafe") === "function";
78
55
  }
79
56
  export function asRawClient(db) {
80
- const dbAny = db;
81
57
  // Direct: Bun.SQL / postgres-js Sql / postgres-js TransactionSql. All three
82
58
  // expose `.unsafe`; only Sql/Bun.SQL have `.begin` — TransactionSql uses
83
59
  // `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
84
60
  // need `.begin` (e.g. `transaction()`) verify it themselves.
85
- if (typeof dbAny["unsafe"] === "function") {
86
- return dbAny;
87
- }
61
+ if (isRawClient(db))
62
+ return db;
88
63
  // TenantDb (ctx.db) has no unfiltered escape via asRawClient anymore — fail
89
64
  // closed instead of silently unwrapping a tenant-scoped handle.
90
65
  if (isTenantDbShape(db)) {
@@ -93,16 +68,13 @@ export function asRawClient(db) {
93
68
  "{ reason } on the handler/hook (r.systemScope() features: ctx.systemDb.unsafeRaw(reason)).");
94
69
  }
95
70
  // Drizzle DbConnection (legacy compat): $client = postgres-js Sql.
96
- const $client = dbAny["$client"];
97
- if ($client && typeof $client["unsafe"] === "function") {
71
+ const $client = propertyOf(db, "$client");
72
+ if (isRawClient($client))
98
73
  return $client;
99
- }
100
74
  // 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") {
75
+ const sessionClient = propertyOf(propertyOf(db, "session"), "client");
76
+ if (isRawClient(sessionClient))
104
77
  return sessionClient;
105
- }
106
78
  throw new Error("bun-db: db argument has no .unsafe() — pass Bun.SQL, postgres-js Sql, or a transaction handle.");
107
79
  }
108
80
  // postgres.js / Bun.SQL reject the WHOLE begin() when any statement inside
@@ -131,7 +103,7 @@ export async function runInSavepoint(tx, fn) {
131
103
  // to paper over by re-running `fn` without the savepoint's isolation.
132
104
  export async function runInSavepointIfSupported(db, fn) {
133
105
  const raw = asRawClient(db);
134
- // @cast-boundary driver savepoint handle → DbRunner at the single seam
106
+ // @cast-boundary db-runner savepoint handle → DbRunner at the single seam
135
107
  const asRunner = (sp) => sp;
136
108
  if (typeof raw.savepoint !== "function")
137
109
  return fn(asRunner(db));
@@ -156,15 +128,14 @@ export async function runInSavepointIfSupported(db, fn) {
156
128
  function isTenantDbShape(db) {
157
129
  if (typeof db !== "object" || db === null)
158
130
  return false;
159
- const d = db;
160
- if (typeof d["unsafe"] === "function")
131
+ if (typeof propertyOf(db, "unsafe") === "function")
161
132
  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);
133
+ return (typeof propertyOf(db, "selectMany") === "function" &&
134
+ typeof propertyOf(db, "fetchOne") === "function" &&
135
+ typeof propertyOf(db, "insertOne") === "function" &&
136
+ typeof propertyOf(db, "updateMany") === "function" &&
137
+ typeof propertyOf(db, "deleteMany") === "function" &&
138
+ "tenantId" in db);
168
139
  }
169
140
  function tenantDbDelegate(db) {
170
141
  return isTenantDbShape(db) ? db : undefined;
@@ -182,6 +153,9 @@ function assertNotTenantScoped(db, fnName) {
182
153
  const WHERE_OPERATOR_KEYS = ["gt", "gte", "lt", "lte", "ne", "in", "like", "jsonText"];
183
154
  const _whereOperatorKeysExhaustive = true;
184
155
  void _whereOperatorKeysExhaustive;
156
+ function isJsonTextMatchList(value) {
157
+ return Array.isArray(value);
158
+ }
185
159
  function isWhereOperator(v) {
186
160
  if (v === null || typeof v !== "object" || Array.isArray(v))
187
161
  return false;
@@ -190,7 +164,7 @@ function isWhereOperator(v) {
190
164
  const keys = Object.keys(v);
191
165
  if (keys.length === 0)
192
166
  return false;
193
- return keys.every((k) => WHERE_OPERATOR_KEYS.includes(k));
167
+ return keys.every((k) => isOneOf(WHERE_OPERATOR_KEYS, k));
194
168
  }
195
169
  export function extractTableInfo(table) {
196
170
  const meta = asEntityTableMeta(table);
@@ -236,10 +210,13 @@ function quoteIdent(name) {
236
210
  //
237
211
  // Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
238
212
  // params — convert to ISO string when the column pgType is timestamptz.
213
+ function toStringTagOf(v) {
214
+ return typeof v === "object" && v !== null && Symbol.toStringTag in v
215
+ ? v[Symbol.toStringTag]
216
+ : undefined;
217
+ }
239
218
  function isTemporalInstant(v) {
240
- return (typeof v === "object" &&
241
- v !== null &&
242
- v[Symbol.toStringTag] === "Temporal.Instant");
219
+ return toStringTagOf(v) === "Temporal.Instant";
243
220
  }
244
221
  // Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
245
222
  // drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
@@ -289,15 +266,14 @@ function instantFromDriver(value) {
289
266
  return null;
290
267
  }
291
268
  function isTemporalPlainDate(v) {
292
- return (typeof v === "object" &&
293
- v !== null &&
294
- v[Symbol.toStringTag] === "Temporal.PlainDate");
269
+ return toStringTagOf(v) === "Temporal.PlainDate";
295
270
  }
296
271
  function isTemporalPlainDateTimeLike(v) {
297
- return (typeof v === "object" &&
272
+ const tag = toStringTagOf(v);
273
+ return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
274
+ typeof v === "object" &&
298
275
  v !== null &&
299
- (v[Symbol.toStringTag] === "Temporal.PlainDateTime" ||
300
- v[Symbol.toStringTag] === "Temporal.ZonedDateTime") &&
276
+ "toPlainDate" in v &&
301
277
  typeof v.toPlainDate === "function");
302
278
  }
303
279
  // Verified empirically against Bun.SQL (kumiko-framework#1924): a `date`
@@ -353,6 +329,8 @@ function plainDateFromDriver(value, column) {
353
329
  //
354
330
  // Unknown columns (computed/aliased) pass through unchanged.
355
331
  export function coerceRow(row, info) {
332
+ if (!isPlainObject(row))
333
+ return row;
356
334
  const out = {};
357
335
  let changed = false;
358
336
  for (const key of Object.keys(row)) {
@@ -568,7 +546,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
568
546
  if (pgType !== "jsonb") {
569
547
  throw new Error(`where: jsonText requires a jsonb column, "${col}" is ${pgType}`);
570
548
  }
571
- const matches = Array.isArray(value.jsonText) ? value.jsonText : [value.jsonText];
549
+ const matches = isJsonTextMatchList(value.jsonText) ? value.jsonText : [value.jsonText];
572
550
  for (const match of matches) {
573
551
  if (!Array.isArray(match.keys) ||
574
552
  match.keys.length === 0 ||
@@ -599,7 +577,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
599
577
  like: "LIKE",
600
578
  };
601
579
  for (const [opKey, opSym] of Object.entries(opMap)) {
602
- const opVal = value[opKey];
580
+ const opVal = Reflect.get(value, opKey);
603
581
  if (opVal === undefined)
604
582
  continue;
605
583
  // Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
@@ -638,7 +616,7 @@ function buildWhereClause(info, where, startIndex, qualifier) {
638
616
  values.push(p.bound);
639
617
  }
640
618
  }
641
- const inVal = value["in"];
619
+ const inVal = value.in;
642
620
  if (Array.isArray(inVal)) {
643
621
  if (pgType === "jsonb") {
644
622
  conditions.push(jsonbContainsAny(col, inVal));
@@ -711,11 +689,9 @@ export function isClosedConnectionError(err) {
711
689
  function isPooledClient(raw) {
712
690
  if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
713
691
  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");
692
+ return (typeof propertyOf(raw, "begin") === "function" &&
693
+ typeof propertyOf(raw, "savepoint") !== "function" &&
694
+ typeof propertyOf(raw, "release") !== "function");
719
695
  }
720
696
  const closedConnectionLog = createFallbackLogger("bun-db");
721
697
  // The driver rejects in-flight queries with this AbortError (no code) when the
@@ -803,8 +779,7 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
803
779
  }
804
780
  const DEFAULT_POOL_MAX = 10;
805
781
  function poolMaxOf(raw) {
806
- // @cast-boundary driver pool options — both postgres-js and Bun.SQL expose options.max
807
- const max = raw.options?.max;
782
+ const max = propertyOf(propertyOf(raw, "options"), "max");
808
783
  return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
809
784
  }
810
785
  // Exported so raw-SQL query modules outside bun-db (e.g. bundled-features'
@@ -814,7 +789,7 @@ function poolMaxOf(raw) {
814
789
  export async function unsafeReadRetrying(db, sqlText, params) {
815
790
  const raw = asRawClient(db);
816
791
  try {
817
- return (await raw.unsafe(sqlText, params));
792
+ return await raw.unsafe(sqlText, params);
818
793
  }
819
794
  catch (err) {
820
795
  if (!isPooledClient(raw) || !isClosedConnectionError(err)) {
@@ -826,7 +801,7 @@ export async function unsafeReadRetrying(db, sqlText, params) {
826
801
  let lastErr = err;
827
802
  for (let attempt = 0; attempt < maxRetries; attempt++) {
828
803
  try {
829
- return (await raw.unsafe(sqlText, params));
804
+ return await raw.unsafe(sqlText, params);
830
805
  }
831
806
  catch (retryErr) {
832
807
  lastErr = retryErr;
@@ -872,7 +847,7 @@ export async function selectMany(db, table, where, options) {
872
847
  }
873
848
  sqlText += ` LIMIT ${options.limit}`;
874
849
  }
875
- const raw = (await unsafeReadRetrying(db, sqlText, values));
850
+ const raw = await unsafeReadRetrying(db, sqlText, values);
876
851
  return coerceRows(raw, info);
877
852
  }
878
853
  // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
@@ -937,7 +912,7 @@ export async function selectInnerJoin(db, spec) {
937
912
  sqlText += ` WHERE ${conditions.join(" AND ")}`;
938
913
  if (spec.limit !== undefined)
939
914
  sqlText += ` LIMIT ${spec.limit}`;
940
- const raw = (await unsafeReadRetrying(db, sqlText, values));
915
+ const raw = await unsafeReadRetrying(db, sqlText, values);
941
916
  return raw.map((row) => {
942
917
  const leftRow = {};
943
918
  const rightRow = {};
@@ -987,10 +962,10 @@ export async function insertMany(db, table, rows) {
987
962
  valuesClauses.push(`(${placeholders.join(", ")})`);
988
963
  }
989
964
  const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES ${valuesClauses.join(", ")} RETURNING *`;
990
- const raw = (await unsafeLoggingClosedConnection(db, sqlText, params, {
965
+ const raw = await unsafeLoggingClosedConnection(db, sqlText, params, {
991
966
  operation: "insertMany",
992
967
  table: info.name,
993
- }));
968
+ });
994
969
  return coerceRows(raw, info);
995
970
  }
996
971
  // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
@@ -1020,10 +995,10 @@ export async function insertOne(db, table, values) {
1020
995
  })
1021
996
  .join(", ");
1022
997
  const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders}) RETURNING *`;
1023
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
998
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1024
999
  operation: "insertOne",
1025
1000
  table: info.name,
1026
- }));
1001
+ });
1027
1002
  const first = rows[0];
1028
1003
  if (!first)
1029
1004
  return undefined;
@@ -1063,10 +1038,10 @@ export async function updateMany(db, table, set, where) {
1063
1038
  values.push(v);
1064
1039
  }
1065
1040
  sqlText += " RETURNING *";
1066
- const raw = (await unsafeLoggingClosedConnection(db, sqlText, values, {
1041
+ const raw = await unsafeLoggingClosedConnection(db, sqlText, values, {
1067
1042
  operation: "updateMany",
1068
1043
  table: info.name,
1069
- }));
1044
+ });
1070
1045
  return coerceRows(raw, info);
1071
1046
  }
1072
1047
  export async function deleteMany(db, table, where) {
@@ -1135,7 +1110,7 @@ export async function countWhere(db, table, where = {}) {
1135
1110
  sqlText += ` WHERE ${w.sqlText}`;
1136
1111
  values = w.values;
1137
1112
  }
1138
- const rows = (await unsafeReadRetrying(db, sqlText, values));
1113
+ const rows = await unsafeReadRetrying(db, sqlText, values);
1139
1114
  return rows[0]?.count ?? 0;
1140
1115
  }
1141
1116
  const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
@@ -1171,7 +1146,7 @@ function aggregateMeasureSql(info, measure) {
1171
1146
  case "avg":
1172
1147
  return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
1173
1148
  default:
1174
- throw new Error(`aggregateWhere: unknown measure fn "${String(measure.fn)}"`);
1149
+ throw new Error(`aggregateWhere: unknown measure fn "${String(Reflect.get(measure, "fn"))}"`);
1175
1150
  }
1176
1151
  }
1177
1152
  function aggregateDimensionSql(info, dimension, index, params) {
@@ -1233,7 +1208,7 @@ export async function aggregateWhere(db, table, spec, where = {}) {
1233
1208
  params.push(...w.values);
1234
1209
  }
1235
1210
  sqlText += aggregateGroupOrderLimitSql(spec, dimensions.length);
1236
- const raw = (await unsafeReadRetrying(db, sqlText, params));
1211
+ const raw = await unsafeReadRetrying(db, sqlText, params);
1237
1212
  return raw.map((row) => {
1238
1213
  const value = row["value"];
1239
1214
  return {
@@ -1277,10 +1252,10 @@ export async function upsertOnConflict(db, table, values, options) {
1277
1252
  }
1278
1253
  const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
1279
1254
  const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
1280
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1255
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1281
1256
  operation: "upsertOnConflict",
1282
1257
  table: info.name,
1283
- }));
1258
+ });
1284
1259
  const first = rows[0];
1285
1260
  if (!first)
1286
1261
  return undefined;
@@ -1305,10 +1280,10 @@ export async function insertOnConflictDoNothing(db, table, values, options) {
1305
1280
  const { sqlPrefix, params } = buildInsertSql(info, entries);
1306
1281
  const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
1307
1282
  const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO NOTHING RETURNING *`;
1308
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1283
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1309
1284
  operation: "insertOnConflictDoNothing",
1310
1285
  table: info.name,
1311
- }));
1286
+ });
1312
1287
  const first = rows[0];
1313
1288
  if (!first)
1314
1289
  return undefined;
@@ -1357,10 +1332,10 @@ export async function incrementCounter(db, table, values, increments, options =
1357
1332
  }
1358
1333
  const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
1359
1334
  const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
1360
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1335
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1361
1336
  operation: "incrementCounter",
1362
1337
  table: info.name,
1363
- }));
1338
+ });
1364
1339
  const first = rows[0];
1365
1340
  if (!first)
1366
1341
  return undefined;
@@ -1389,10 +1364,10 @@ export async function deleteManyBatched(db, table, where, options) {
1389
1364
  const sqlText = `DELETE FROM ${tableQ} WHERE ${idQ} IN (
1390
1365
  SELECT ${idQ} FROM ${tableQ} WHERE ${w.sqlText} LIMIT $${limitIdx}
1391
1366
  ) RETURNING ${idQ}`;
1392
- const rows = (await unsafeLoggingClosedConnection(db, sqlText, params, {
1367
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
1393
1368
  operation: "deleteManyBatched",
1394
1369
  table: info.name,
1395
- }));
1370
+ });
1396
1371
  batches++;
1397
1372
  deleted += rows.length;
1398
1373
  if (rows.length < limit)
@@ -1401,7 +1376,7 @@ export async function deleteManyBatched(db, table, where, options) {
1401
1376
  return { deleted, batches };
1402
1377
  }
1403
1378
  export async function transaction(db, fn) {
1404
- return (await asRawClient(db).begin(async (tx) => fn(tx)));
1379
+ return asRawClient(db).begin(fn);
1405
1380
  }
1406
1381
  // A tx/savepoint handle has no begin(); fail closed instead of nesting silently or TypeError-ing.
1407
1382
  export async function runInNewTransaction(db, fn) {
@@ -1411,6 +1386,5 @@ export async function runInNewTransaction(db, fn) {
1411
1386
  message: "runInNewTransaction: runner has no begin() — already inside a transaction or savepoint.",
1412
1387
  });
1413
1388
  }
1414
- // @cast-boundary driver tx handle → DbRunner at the single seam
1415
- return raw.begin((tx) => fn(tx));
1389
+ return raw.begin(fn);
1416
1390
  }
@@ -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
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 === "")
@@ -85,6 +85,7 @@ export type SqlExpression = {
85
85
  readonly params: readonly unknown[];
86
86
  readonly [SQL_EXPR_BRAND]: true;
87
87
  };
88
+ export declare function isSqlExpression(value: unknown): value is SqlExpression;
88
89
  export declare function sql(strings: TemplateStringsArray, ...values: readonly unknown[]): SqlExpression;
89
90
  export declare namespace sql {
90
91
  var raw: (text: string) => SqlExpression;