@atscript/db 0.1.102 → 0.1.104

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.
package/dist/index.cjs CHANGED
@@ -1,9 +1,145 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_db_error = require("./db-error-DXwEzmYJ.cjs");
3
- const require_db_view = require("./db-view-GcbegVBD.cjs");
3
+ const require_db_view = require("./db-view-Bw_hlrWw.cjs");
4
4
  const require_ops = require("./ops.cjs");
5
- const require_validator = require("./validator-f43UcYiY.cjs");
5
+ const require_validator = require("./validator-BSk8lPH1.cjs");
6
+ let node_crypto = require("node:crypto");
6
7
  let _uniqu_core = require("@uniqu/core");
8
+ //#region src/encryption.ts
9
+ const ENVELOPE_VERSION = "aes1";
10
+ const ALGORITHM = "aes-256-gcm";
11
+ const IV_BYTES = 12;
12
+ const KEY_BYTES = 32;
13
+ const TAG_BYTES = 16;
14
+ /** `aes1$<keyId>$<iv>$<tag>$<ciphertext>` — every segment base64url, keyId [A-Za-z0-9_.-]. */
15
+ const ENVELOPE_RE = /^aes1\$[\w.-]+\$[\w-]+\$[\w-]+\$[\w-]+$/;
16
+ const KEY_ID_RE = /^[\w.-]+$/;
17
+ /**
18
+ * Normalizes key material to a 32-byte Buffer.
19
+ * Accepts: a 32-byte Buffer, a 64-char hex string, a base64/base64url string
20
+ * decoding to 32 bytes, or a raw utf8 string of exactly 32 bytes.
21
+ */
22
+ function normalizeKey(keyId, material) {
23
+ if (Buffer.isBuffer(material)) {
24
+ if (material.length === KEY_BYTES) return material;
25
+ throw keyInvalid(keyId, `expected ${KEY_BYTES} bytes, got ${material.length}`);
26
+ }
27
+ if (typeof material === "string") {
28
+ if (/^[0-9a-f]{64}$/i.test(material)) return Buffer.from(material, "hex");
29
+ if (/^[\w+/-]+={0,2}$/.test(material)) {
30
+ const decoded = Buffer.from(material, "base64");
31
+ if (decoded.length === KEY_BYTES) return decoded;
32
+ }
33
+ const utf8 = Buffer.from(material, "utf8");
34
+ if (utf8.length === KEY_BYTES) return utf8;
35
+ throw keyInvalid(keyId, `cannot derive a ${KEY_BYTES}-byte key from the provided string`);
36
+ }
37
+ throw keyInvalid(keyId, "key material must be a string or Buffer");
38
+ }
39
+ function keyInvalid(keyId, reason) {
40
+ return new require_db_error.DbError("ENC_KEY_INVALID", [{
41
+ path: `encryption.keys.${keyId}`,
42
+ message: `Invalid encryption key "${keyId}": ${reason}`
43
+ }]);
44
+ }
45
+ /**
46
+ * AES-256-GCM envelope encryption service for `@db.encrypted` fields.
47
+ *
48
+ * Owned by `DbSpace` and shared across all tables in the space. Values are
49
+ * `JSON.stringify`'d before encryption (type-exact round-trips) and stored as
50
+ * a single ASCII envelope string: `aes1$<keyId>$<iv>$<tag>$<ciphertext>`.
51
+ *
52
+ * Key material is validated eagerly at construction (`ENC_KEY_INVALID`);
53
+ * `resolveKey` lookups are cached per keyId for the process lifetime.
54
+ */
55
+ var DbEncryption = class {
56
+ defaultKeyId;
57
+ onUnencrypted;
58
+ _keys = /* @__PURE__ */ new Map();
59
+ _resolveKey;
60
+ _resolved = /* @__PURE__ */ new Map();
61
+ constructor(options) {
62
+ if (!options.defaultKeyId || !KEY_ID_RE.test(options.defaultKeyId)) throw new require_db_error.DbError("ENC_KEY_INVALID", [{
63
+ path: "encryption.defaultKeyId",
64
+ message: `Invalid defaultKeyId "${options.defaultKeyId}" — expected a non-empty id matching [A-Za-z0-9_.-]+`
65
+ }]);
66
+ this.defaultKeyId = options.defaultKeyId;
67
+ this.onUnencrypted = options.onUnencrypted ?? "error";
68
+ this._resolveKey = options.resolveKey;
69
+ if (options.keys) for (const [keyId, material] of Object.entries(options.keys)) {
70
+ if (!KEY_ID_RE.test(keyId)) throw keyInvalid(keyId, "key ids must match [A-Za-z0-9_.-]+ (no '$')");
71
+ this._keys.set(keyId, normalizeKey(keyId, material));
72
+ }
73
+ if (!this._keys.has(this.defaultKeyId) && !this._resolveKey) throw new require_db_error.DbError("ENC_KEY_INVALID", [{
74
+ path: "encryption.defaultKeyId",
75
+ message: `defaultKeyId "${this.defaultKeyId}" is not in the key registry and no resolveKey() was provided`
76
+ }]);
77
+ }
78
+ /** True when `value` looks like an encryption envelope produced by this service. */
79
+ isEnvelope(value) {
80
+ return typeof value === "string" && ENVELOPE_RE.test(value);
81
+ }
82
+ /** Encrypts a JSON-serializable value into an envelope string using the default key. */
83
+ async encrypt(value) {
84
+ const keyId = this.defaultKeyId;
85
+ const key = await this._getKey(keyId);
86
+ const iv = (0, node_crypto.randomBytes)(IV_BYTES);
87
+ const cipher = (0, node_crypto.createCipheriv)(ALGORITHM, key, iv);
88
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
89
+ const tag = cipher.getAuthTag();
90
+ return [
91
+ ENVELOPE_VERSION,
92
+ keyId,
93
+ iv.toString("base64url"),
94
+ tag.toString("base64url"),
95
+ ciphertext.toString("base64url")
96
+ ].join("$");
97
+ }
98
+ /** Decrypts an envelope string back into its plaintext value. */
99
+ async decrypt(envelope, ctx) {
100
+ const parts = envelope.split("$");
101
+ if (parts.length !== 5 || parts[0] !== ENVELOPE_VERSION) throw this._decryptFailed(ctx, "unknown", "malformed envelope");
102
+ const [, keyId, ivB64, tagB64, ctB64] = parts;
103
+ let key;
104
+ try {
105
+ key = await this._getKey(keyId);
106
+ } catch {
107
+ throw this._decryptFailed(ctx, keyId, `unknown encryption key "${keyId}"`);
108
+ }
109
+ try {
110
+ const iv = Buffer.from(ivB64, "base64url");
111
+ const tag = Buffer.from(tagB64, "base64url");
112
+ if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) throw new Error("bad iv/tag length");
113
+ const decipher = (0, node_crypto.createDecipheriv)(ALGORITHM, key, iv);
114
+ decipher.setAuthTag(tag);
115
+ const plaintext = Buffer.concat([decipher.update(Buffer.from(ctB64, "base64url")), decipher.final()]).toString("utf8");
116
+ return JSON.parse(plaintext);
117
+ } catch {
118
+ throw this._decryptFailed(ctx, keyId, "authentication failed or corrupted ciphertext");
119
+ }
120
+ }
121
+ _decryptFailed(ctx, keyId, reason) {
122
+ const where = ctx?.table || ctx?.field ? ` on ${ctx?.table ?? "?"}.${ctx?.field ?? "?"}` : "";
123
+ return new require_db_error.DbError("ENC_DECRYPT_FAILED", [{
124
+ path: ctx?.field ?? "",
125
+ message: `Decryption failed${where} (keyId: ${keyId}): ${reason}`
126
+ }]);
127
+ }
128
+ _getKey(keyId) {
129
+ const known = this._keys.get(keyId);
130
+ if (known) return Promise.resolve(known);
131
+ let pending = this._resolved.get(keyId);
132
+ if (!pending) {
133
+ const resolver = this._resolveKey;
134
+ if (!resolver) return Promise.reject(keyInvalid(keyId, "not in the key registry"));
135
+ pending = Promise.resolve().then(() => resolver(keyId)).then((material) => normalizeKey(keyId, material));
136
+ pending.catch(() => this._resolved.delete(keyId));
137
+ this._resolved.set(keyId, pending);
138
+ }
139
+ return pending;
140
+ }
141
+ };
142
+ //#endregion
7
143
  //#region src/with-optimistic-retry.ts
8
144
  /**
9
145
  * Runs a read-modify-write loop under optimistic concurrency control (OCC).
@@ -74,15 +210,27 @@ async function withOptimisticRetry(table, filter, mutator, opts) {
74
210
  */
75
211
  var DbSpace = class {
76
212
  adapterFactory;
77
- logger;
78
213
  _readables = /* @__PURE__ */ new WeakMap();
79
214
  /** All tables created in this space — used for reverse FK lookup during cascade. */
80
215
  _allTables = /* @__PURE__ */ new Set();
81
216
  /** Lazily created adapter for administrative ops (drop table/view) that don't need a registered readable. */
82
217
  _adminAdapter;
83
- constructor(adapterFactory, logger = require_db_view.NoopLogger) {
218
+ logger;
219
+ /** Encryption service for `@db.encrypted` fields — validated eagerly at construction. */
220
+ _encryption;
221
+ /**
222
+ * @param adapterFactory - Creates a fresh adapter per table/view.
223
+ * @param loggerOrOptions - Either a logger (legacy signature) or a
224
+ * {@link TDbSpaceOptions} bag carrying `logger` and/or `encryption`.
225
+ */
226
+ constructor(adapterFactory, loggerOrOptions) {
84
227
  this.adapterFactory = adapterFactory;
85
- this.logger = logger;
228
+ if (loggerOrOptions && typeof loggerOrOptions.error === "function") this.logger = loggerOrOptions;
229
+ else {
230
+ const options = loggerOrOptions ?? {};
231
+ this.logger = options.logger ?? require_db_view.NoopLogger;
232
+ if (options.encryption) this._encryption = new DbEncryption(options.encryption);
233
+ }
86
234
  }
87
235
  /**
88
236
  * Auto-detects whether the type is a table or view and returns the
@@ -106,6 +254,7 @@ var DbSpace = class {
106
254
  this._allTables.add(readable);
107
255
  readable.setCascadeResolver((tableName) => this._getCascadeTargets(tableName));
108
256
  readable.setFkLookupResolver((tableName) => this._getFkLookupTarget(tableName));
257
+ readable.setEncryption(this._encryption);
109
258
  this._readables.set(type, readable);
110
259
  }
111
260
  return readable;
@@ -118,6 +267,7 @@ var DbSpace = class {
118
267
  let readable = this._readables.get(type);
119
268
  if (!readable) {
120
269
  readable = new require_db_view.AtscriptDbView(type, this.adapterFactory(), logger || this.logger, (t) => this.get(t));
270
+ readable.setEncryption(this._encryption);
121
271
  this._readables.set(type, readable);
122
272
  }
123
273
  return readable;
@@ -205,6 +355,7 @@ exports.AtscriptDbTable = require_db_view.AtscriptDbTable;
205
355
  exports.AtscriptDbView = require_db_view.AtscriptDbView;
206
356
  exports.BaseDbAdapter = require_db_view.BaseDbAdapter;
207
357
  exports.CasExhaustedError = require_db_error.CasExhaustedError;
358
+ exports.DbEncryption = DbEncryption;
208
359
  exports.DbError = require_db_error.DbError;
209
360
  exports.DbSpace = DbSpace;
210
361
  exports.DocumentFieldMapper = require_db_view.DocumentFieldMapper;
@@ -215,6 +366,7 @@ exports.NoopLogger = require_db_view.NoopLogger;
215
366
  exports.RelationalFieldMapper = require_db_view.RelationalFieldMapper;
216
367
  exports.TableMetadata = require_db_view.TableMetadata;
217
368
  exports.UniquSelect = require_db_view.UniquSelect;
369
+ exports.assertGeoPoint = require_db_view.assertGeoPoint;
218
370
  exports.assertNoVersionWrites = require_db_view.assertNoVersionWrites;
219
371
  exports.buildDbValidator = require_validator.buildDbValidator;
220
372
  exports.buildValidationContext = require_validator.buildValidationContext;
@@ -229,7 +381,12 @@ exports.decomposePatch = require_db_view.decomposePatch;
229
381
  exports.forceNavNonOptional = require_validator.forceNavNonOptional;
230
382
  exports.getDbFieldOp = require_ops.getDbFieldOp;
231
383
  exports.getKeyProps = require_validator.getKeyProps;
384
+ exports.guardAggregate = require_db_view.guardAggregate;
385
+ exports.guardFilter = require_db_view.guardFilter;
386
+ exports.guardQuery = require_db_view.guardQuery;
232
387
  exports.isDbFieldOp = require_ops.isDbFieldOp;
388
+ exports.isGeoIndexableType = require_db_view.isGeoIndexableType;
389
+ exports.isGeoPointType = require_db_view.isGeoPointType;
233
390
  exports.isNavRelation = require_validator.isNavRelation;
234
391
  Object.defineProperty(exports, "isPrimitive", {
235
392
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-mhPp-MPv.cjs";
1
+ import { $ as TIdentification, A as TDbActionLevel, B as TDbIndexType, C as TCascadeResolver, D as TCrudPermissions, E as TCrudOp, F as TDbDeleteResult, G as TDbStorageType, H as TDbInsertResult, I as TDbFieldMeta, J as TExistingTableOption, K as TDbUpdateResult, L as TDbForeignKey, M as TDbCollation, N as TDbDefaultFn, O as TDbActionInfo, P as TDbDefaultValue, Q as TIdDescriptor, R as TDbIndex, S as PrimaryKeyOf, T as TColumnDiff, U as TDbReferentialAction, V as TDbInsertManyResult, W as TDbRelation, X as TFkLookupResolver, Y as TFieldMeta, Z as TFkLookupTarget, _ as FieldOpsFor, _t as TGenericLogger, a as TDbEncryptionOptions, at as TTableOptionDiff, b as NavPropsOf, c as BaseDbAdapter, ct as TWriteTableResolver, d as AggregateFn, dt as UniqueryControls, et as TMetaResponse, f as AggregateQuery, ft as WithRelation, g as DbQuery, gt as NoopLogger, h as DbControls, ht as isGeoPointType, i as DbEncryption, it as TSyncColumnResult, j as TDbActionProcessor, k as TDbActionIntent, l as AggregateControls, lt as TypedWithRelation, m as AtscriptDbWritable, mt as isGeoIndexableType, n as DbResponse, nt as TRelationInfo, o as DocumentFieldMapper, ot as TTableResolver, p as AggregateResult, pt as TableMetadata, q as TExistingColumn, r as resolveDesignType, rt as TSearchIndexInfo, s as FieldMappingStrategy, st as TValueFormatterPair, t as AtscriptDbReadable, tt as TMetadataOverrides, u as AggregateExpr, ut as Uniquery, v as FilterExpr, vt as UniquSelect, w as TCascadeTarget, x as OwnPropsOf, y as FlatOf, z as TDbIndexField } from "./db-readable-BepVc21V.cjs";
2
2
  import { a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateFieldOps, h as separateCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, p as getDbFieldOp, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-DJRnNTVo.cjs";
3
- import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-BhOc9_OO.cjs";
3
+ import { a as TViewColumnMapping, c as AtscriptQueryNode, d as TViewPlan, f as translateQueryTree, h as NativeIntegrity, i as AtscriptDbView, l as AtscriptRef, m as IntegrityStrategy, n as TAdapterFactory, o as AtscriptQueryComparison, p as AtscriptDbTable, r as TDbSpaceOptions, s as AtscriptQueryFieldRef, t as DbSpace, u as TViewJoin } from "./db-space-CWhIEC7E.cjs";
4
4
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.cjs";
5
5
  import { c as TArrayPatch, i as buildValidationContext, l as TDbPatch, n as ValidatorMode, o as forceNavNonOptional, r as buildDbValidator, s as isNavRelation, t as ValidationContext, u as getKeyProps } from "./validator-Crqe6vRW.cjs";
6
6
  import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
@@ -81,7 +81,7 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
81
81
  }
82
82
  //#endregion
83
83
  //#region src/db-error.d.ts
84
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED";
84
+ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH";
85
85
  declare class DbError extends Error {
86
86
  readonly code: DbErrorCode;
87
87
  readonly errors: Array<{
@@ -107,6 +107,36 @@ declare class CasExhaustedError extends DbError {
107
107
  constructor(attempts: number, lastSeenVersion: number | undefined);
108
108
  }
109
109
  //#endregion
110
+ //#region src/query/query-guards.d.ts
111
+ /**
112
+ * Engine-agnostic query-time guards, applied in the core layer BEFORE filter
113
+ * translation (field-encryption spec §6, geo-index spec §4.2):
114
+ *
115
+ * - filters referencing an `@db.encrypted` field (incl. nested paths into an
116
+ * encrypted object) → `ENC_FIELD_FILTER`
117
+ * - `$sort` on an encrypted field → `ENC_FIELD_SORT`
118
+ * - `$groupBy` / aggregate refs on an encrypted field → `ENC_FIELD_AGG`
119
+ * - `$geoWithin` on a non-geoPoint field → `FILTER_TYPE_MISMATCH`
120
+ * - `$geoWithin` with a malformed circle → `INVALID_QUERY`
121
+ * - `$geoWithin` on an adapter without geo support → `GEO_NOT_SUPPORTED`
122
+ */
123
+ /** Validates a `[lng, lat]` tuple (GeoJSON coordinate order). */
124
+ declare function assertGeoPoint(point: unknown, path: string): asserts point is [number, number];
125
+ /**
126
+ * Walks a filter expression, rejecting encrypted-field references and
127
+ * validating `$geoWithin` operator nodes.
128
+ */
129
+ declare function guardFilter(meta: TableMetadata, adapter: BaseDbAdapter, filter: FilterExpr$1 | undefined, encCode?: "ENC_FIELD_FILTER" | "ENC_FIELD_AGG"): void;
130
+ /** Shared read-path guard: filter + $sort. */
131
+ declare function guardQuery(meta: TableMetadata, adapter: BaseDbAdapter, query: {
132
+ filter?: FilterExpr$1;
133
+ controls?: {
134
+ $sort?: unknown;
135
+ };
136
+ } | undefined): void;
137
+ /** Aggregate-path guard: $groupBy / $select / $having refs + filter + $sort. */
138
+ declare function guardAggregate(meta: TableMetadata, adapter: BaseDbAdapter, query: AggregateQuery$1): void;
139
+ //#endregion
110
140
  //#region src/with-optimistic-retry.d.ts
111
141
  interface WithOptimisticRetryOptions {
112
142
  /** Maximum number of attempts before giving up. Defaults to `5`. */
@@ -185,4 +215,4 @@ declare function decomposePatch(payload: Record<string, unknown>, table: Atscrip
185
215
  */
186
216
  declare function assertNoVersionWrites(data: Record<string, unknown>, versionColumn: string): void;
187
217
  //#endregion
188
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
218
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbEncryption, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbEncryptionOptions, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbSpaceOptions, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardQuery, isDbFieldOp, isGeoIndexableType, isGeoPointType, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-CXdHBtF6.mjs";
1
+ import { $ as TIdentification, A as TDbActionLevel, B as TDbIndexType, C as TCascadeResolver, D as TCrudPermissions, E as TCrudOp, F as TDbDeleteResult, G as TDbStorageType, H as TDbInsertResult, I as TDbFieldMeta, J as TExistingTableOption, K as TDbUpdateResult, L as TDbForeignKey, M as TDbCollation, N as TDbDefaultFn, O as TDbActionInfo, P as TDbDefaultValue, Q as TIdDescriptor, R as TDbIndex, S as PrimaryKeyOf, T as TColumnDiff, U as TDbReferentialAction, V as TDbInsertManyResult, W as TDbRelation, X as TFkLookupResolver, Y as TFieldMeta, Z as TFkLookupTarget, _ as FieldOpsFor, _t as TGenericLogger, a as TDbEncryptionOptions, at as TTableOptionDiff, b as NavPropsOf, c as BaseDbAdapter, ct as TWriteTableResolver, d as AggregateFn, dt as UniqueryControls, et as TMetaResponse, f as AggregateQuery, ft as WithRelation, g as DbQuery, gt as NoopLogger, h as DbControls, ht as isGeoPointType, i as DbEncryption, it as TSyncColumnResult, j as TDbActionProcessor, k as TDbActionIntent, l as AggregateControls, lt as TypedWithRelation, m as AtscriptDbWritable, mt as isGeoIndexableType, n as DbResponse, nt as TRelationInfo, o as DocumentFieldMapper, ot as TTableResolver, p as AggregateResult, pt as TableMetadata, q as TExistingColumn, r as resolveDesignType, rt as TSearchIndexInfo, s as FieldMappingStrategy, st as TValueFormatterPair, t as AtscriptDbReadable, tt as TMetadataOverrides, u as AggregateExpr, ut as Uniquery, v as FilterExpr, vt as UniquSelect, w as TCascadeTarget, x as OwnPropsOf, y as FlatOf, z as TDbIndexField } from "./db-readable-Ds01ezjj.mjs";
2
2
  import { a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateFieldOps, h as separateCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, p as getDbFieldOp, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-DJRnNTVo.mjs";
3
- import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-BYyVZnL1.mjs";
3
+ import { a as TViewColumnMapping, c as AtscriptQueryNode, d as TViewPlan, f as translateQueryTree, h as NativeIntegrity, i as AtscriptDbView, l as AtscriptRef, m as IntegrityStrategy, n as TAdapterFactory, o as AtscriptQueryComparison, p as AtscriptDbTable, r as TDbSpaceOptions, s as AtscriptQueryFieldRef, t as DbSpace, u as TViewJoin } from "./db-space-0U_4PiNS.mjs";
4
4
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
5
5
  import { c as TArrayPatch, i as buildValidationContext, l as TDbPatch, n as ValidatorMode, o as forceNavNonOptional, r as buildDbValidator, s as isNavRelation, t as ValidationContext, u as getKeyProps } from "./validator-Crqe6vRW.mjs";
6
6
  import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
@@ -81,7 +81,7 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
81
81
  }
82
82
  //#endregion
83
83
  //#region src/db-error.d.ts
84
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED";
84
+ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH";
85
85
  declare class DbError extends Error {
86
86
  readonly code: DbErrorCode;
87
87
  readonly errors: Array<{
@@ -107,6 +107,36 @@ declare class CasExhaustedError extends DbError {
107
107
  constructor(attempts: number, lastSeenVersion: number | undefined);
108
108
  }
109
109
  //#endregion
110
+ //#region src/query/query-guards.d.ts
111
+ /**
112
+ * Engine-agnostic query-time guards, applied in the core layer BEFORE filter
113
+ * translation (field-encryption spec §6, geo-index spec §4.2):
114
+ *
115
+ * - filters referencing an `@db.encrypted` field (incl. nested paths into an
116
+ * encrypted object) → `ENC_FIELD_FILTER`
117
+ * - `$sort` on an encrypted field → `ENC_FIELD_SORT`
118
+ * - `$groupBy` / aggregate refs on an encrypted field → `ENC_FIELD_AGG`
119
+ * - `$geoWithin` on a non-geoPoint field → `FILTER_TYPE_MISMATCH`
120
+ * - `$geoWithin` with a malformed circle → `INVALID_QUERY`
121
+ * - `$geoWithin` on an adapter without geo support → `GEO_NOT_SUPPORTED`
122
+ */
123
+ /** Validates a `[lng, lat]` tuple (GeoJSON coordinate order). */
124
+ declare function assertGeoPoint(point: unknown, path: string): asserts point is [number, number];
125
+ /**
126
+ * Walks a filter expression, rejecting encrypted-field references and
127
+ * validating `$geoWithin` operator nodes.
128
+ */
129
+ declare function guardFilter(meta: TableMetadata, adapter: BaseDbAdapter, filter: FilterExpr$1 | undefined, encCode?: "ENC_FIELD_FILTER" | "ENC_FIELD_AGG"): void;
130
+ /** Shared read-path guard: filter + $sort. */
131
+ declare function guardQuery(meta: TableMetadata, adapter: BaseDbAdapter, query: {
132
+ filter?: FilterExpr$1;
133
+ controls?: {
134
+ $sort?: unknown;
135
+ };
136
+ } | undefined): void;
137
+ /** Aggregate-path guard: $groupBy / $select / $having refs + filter + $sort. */
138
+ declare function guardAggregate(meta: TableMetadata, adapter: BaseDbAdapter, query: AggregateQuery$1): void;
139
+ //#endregion
110
140
  //#region src/with-optimistic-retry.d.ts
111
141
  interface WithOptimisticRetryOptions {
112
142
  /** Maximum number of attempts before giving up. Defaults to `5`. */
@@ -185,4 +215,4 @@ declare function decomposePatch(payload: Record<string, unknown>, table: Atscrip
185
215
  */
186
216
  declare function assertNoVersionWrites(data: Record<string, unknown>, versionColumn: string): void;
187
217
  //#endregion
188
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
218
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbEncryption, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbEncryptionOptions, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbSpaceOptions, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardQuery, isDbFieldOp, isGeoIndexableType, isGeoPointType, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
package/dist/index.mjs CHANGED
@@ -1,8 +1,144 @@
1
1
  import { n as DbError, t as CasExhaustedError } from "./db-error-BHPXOKzc.mjs";
2
- import { a as ApplicationIntegrity, c as NativeIntegrity, d as RelationalFieldMapper, f as DocumentFieldMapper, g as NoopLogger, h as TableMetadata, i as decomposePatch, l as AtscriptDbReadable, m as UniquSelect, n as AtscriptDbTable, o as BaseDbAdapter, p as FieldMappingStrategy, r as assertNoVersionWrites, s as IntegrityStrategy, t as AtscriptDbView, u as resolveDesignType } from "./db-view-Sq4wCceR.mjs";
2
+ import { S as NoopLogger, _ as FieldMappingStrategy, a as ApplicationIntegrity, b as isGeoIndexableType, c as NativeIntegrity, d as assertGeoPoint, f as guardAggregate, g as DocumentFieldMapper, h as RelationalFieldMapper, i as decomposePatch, l as AtscriptDbReadable, m as guardQuery, n as AtscriptDbTable, o as BaseDbAdapter, p as guardFilter, r as assertNoVersionWrites, s as IntegrityStrategy, t as AtscriptDbView, u as resolveDesignType, v as UniquSelect, x as isGeoPointType, y as TableMetadata } from "./db-view-TyzB5VFb.mjs";
3
3
  import { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, getDbFieldOp, isDbFieldOp, separateCas, separateFieldOps } from "./ops.mjs";
4
- import { a as isNavRelation, i as forceNavNonOptional, n as buildValidationContext, o as createDbValidatorPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-bLsSgi0N.mjs";
4
+ import { a as isNavRelation, i as forceNavNonOptional, n as buildValidationContext, o as createDbValidatorPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-C7plt6Kf.mjs";
5
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
5
6
  import { computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
7
+ //#region src/encryption.ts
8
+ const ENVELOPE_VERSION = "aes1";
9
+ const ALGORITHM = "aes-256-gcm";
10
+ const IV_BYTES = 12;
11
+ const KEY_BYTES = 32;
12
+ const TAG_BYTES = 16;
13
+ /** `aes1$<keyId>$<iv>$<tag>$<ciphertext>` — every segment base64url, keyId [A-Za-z0-9_.-]. */
14
+ const ENVELOPE_RE = /^aes1\$[\w.-]+\$[\w-]+\$[\w-]+\$[\w-]+$/;
15
+ const KEY_ID_RE = /^[\w.-]+$/;
16
+ /**
17
+ * Normalizes key material to a 32-byte Buffer.
18
+ * Accepts: a 32-byte Buffer, a 64-char hex string, a base64/base64url string
19
+ * decoding to 32 bytes, or a raw utf8 string of exactly 32 bytes.
20
+ */
21
+ function normalizeKey(keyId, material) {
22
+ if (Buffer.isBuffer(material)) {
23
+ if (material.length === KEY_BYTES) return material;
24
+ throw keyInvalid(keyId, `expected ${KEY_BYTES} bytes, got ${material.length}`);
25
+ }
26
+ if (typeof material === "string") {
27
+ if (/^[0-9a-f]{64}$/i.test(material)) return Buffer.from(material, "hex");
28
+ if (/^[\w+/-]+={0,2}$/.test(material)) {
29
+ const decoded = Buffer.from(material, "base64");
30
+ if (decoded.length === KEY_BYTES) return decoded;
31
+ }
32
+ const utf8 = Buffer.from(material, "utf8");
33
+ if (utf8.length === KEY_BYTES) return utf8;
34
+ throw keyInvalid(keyId, `cannot derive a ${KEY_BYTES}-byte key from the provided string`);
35
+ }
36
+ throw keyInvalid(keyId, "key material must be a string or Buffer");
37
+ }
38
+ function keyInvalid(keyId, reason) {
39
+ return new DbError("ENC_KEY_INVALID", [{
40
+ path: `encryption.keys.${keyId}`,
41
+ message: `Invalid encryption key "${keyId}": ${reason}`
42
+ }]);
43
+ }
44
+ /**
45
+ * AES-256-GCM envelope encryption service for `@db.encrypted` fields.
46
+ *
47
+ * Owned by `DbSpace` and shared across all tables in the space. Values are
48
+ * `JSON.stringify`'d before encryption (type-exact round-trips) and stored as
49
+ * a single ASCII envelope string: `aes1$<keyId>$<iv>$<tag>$<ciphertext>`.
50
+ *
51
+ * Key material is validated eagerly at construction (`ENC_KEY_INVALID`);
52
+ * `resolveKey` lookups are cached per keyId for the process lifetime.
53
+ */
54
+ var DbEncryption = class {
55
+ defaultKeyId;
56
+ onUnencrypted;
57
+ _keys = /* @__PURE__ */ new Map();
58
+ _resolveKey;
59
+ _resolved = /* @__PURE__ */ new Map();
60
+ constructor(options) {
61
+ if (!options.defaultKeyId || !KEY_ID_RE.test(options.defaultKeyId)) throw new DbError("ENC_KEY_INVALID", [{
62
+ path: "encryption.defaultKeyId",
63
+ message: `Invalid defaultKeyId "${options.defaultKeyId}" — expected a non-empty id matching [A-Za-z0-9_.-]+`
64
+ }]);
65
+ this.defaultKeyId = options.defaultKeyId;
66
+ this.onUnencrypted = options.onUnencrypted ?? "error";
67
+ this._resolveKey = options.resolveKey;
68
+ if (options.keys) for (const [keyId, material] of Object.entries(options.keys)) {
69
+ if (!KEY_ID_RE.test(keyId)) throw keyInvalid(keyId, "key ids must match [A-Za-z0-9_.-]+ (no '$')");
70
+ this._keys.set(keyId, normalizeKey(keyId, material));
71
+ }
72
+ if (!this._keys.has(this.defaultKeyId) && !this._resolveKey) throw new DbError("ENC_KEY_INVALID", [{
73
+ path: "encryption.defaultKeyId",
74
+ message: `defaultKeyId "${this.defaultKeyId}" is not in the key registry and no resolveKey() was provided`
75
+ }]);
76
+ }
77
+ /** True when `value` looks like an encryption envelope produced by this service. */
78
+ isEnvelope(value) {
79
+ return typeof value === "string" && ENVELOPE_RE.test(value);
80
+ }
81
+ /** Encrypts a JSON-serializable value into an envelope string using the default key. */
82
+ async encrypt(value) {
83
+ const keyId = this.defaultKeyId;
84
+ const key = await this._getKey(keyId);
85
+ const iv = randomBytes(IV_BYTES);
86
+ const cipher = createCipheriv(ALGORITHM, key, iv);
87
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
88
+ const tag = cipher.getAuthTag();
89
+ return [
90
+ ENVELOPE_VERSION,
91
+ keyId,
92
+ iv.toString("base64url"),
93
+ tag.toString("base64url"),
94
+ ciphertext.toString("base64url")
95
+ ].join("$");
96
+ }
97
+ /** Decrypts an envelope string back into its plaintext value. */
98
+ async decrypt(envelope, ctx) {
99
+ const parts = envelope.split("$");
100
+ if (parts.length !== 5 || parts[0] !== ENVELOPE_VERSION) throw this._decryptFailed(ctx, "unknown", "malformed envelope");
101
+ const [, keyId, ivB64, tagB64, ctB64] = parts;
102
+ let key;
103
+ try {
104
+ key = await this._getKey(keyId);
105
+ } catch {
106
+ throw this._decryptFailed(ctx, keyId, `unknown encryption key "${keyId}"`);
107
+ }
108
+ try {
109
+ const iv = Buffer.from(ivB64, "base64url");
110
+ const tag = Buffer.from(tagB64, "base64url");
111
+ if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) throw new Error("bad iv/tag length");
112
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
113
+ decipher.setAuthTag(tag);
114
+ const plaintext = Buffer.concat([decipher.update(Buffer.from(ctB64, "base64url")), decipher.final()]).toString("utf8");
115
+ return JSON.parse(plaintext);
116
+ } catch {
117
+ throw this._decryptFailed(ctx, keyId, "authentication failed or corrupted ciphertext");
118
+ }
119
+ }
120
+ _decryptFailed(ctx, keyId, reason) {
121
+ const where = ctx?.table || ctx?.field ? ` on ${ctx?.table ?? "?"}.${ctx?.field ?? "?"}` : "";
122
+ return new DbError("ENC_DECRYPT_FAILED", [{
123
+ path: ctx?.field ?? "",
124
+ message: `Decryption failed${where} (keyId: ${keyId}): ${reason}`
125
+ }]);
126
+ }
127
+ _getKey(keyId) {
128
+ const known = this._keys.get(keyId);
129
+ if (known) return Promise.resolve(known);
130
+ let pending = this._resolved.get(keyId);
131
+ if (!pending) {
132
+ const resolver = this._resolveKey;
133
+ if (!resolver) return Promise.reject(keyInvalid(keyId, "not in the key registry"));
134
+ pending = Promise.resolve().then(() => resolver(keyId)).then((material) => normalizeKey(keyId, material));
135
+ pending.catch(() => this._resolved.delete(keyId));
136
+ this._resolved.set(keyId, pending);
137
+ }
138
+ return pending;
139
+ }
140
+ };
141
+ //#endregion
6
142
  //#region src/with-optimistic-retry.ts
7
143
  /**
8
144
  * Runs a read-modify-write loop under optimistic concurrency control (OCC).
@@ -73,15 +209,27 @@ async function withOptimisticRetry(table, filter, mutator, opts) {
73
209
  */
74
210
  var DbSpace = class {
75
211
  adapterFactory;
76
- logger;
77
212
  _readables = /* @__PURE__ */ new WeakMap();
78
213
  /** All tables created in this space — used for reverse FK lookup during cascade. */
79
214
  _allTables = /* @__PURE__ */ new Set();
80
215
  /** Lazily created adapter for administrative ops (drop table/view) that don't need a registered readable. */
81
216
  _adminAdapter;
82
- constructor(adapterFactory, logger = NoopLogger) {
217
+ logger;
218
+ /** Encryption service for `@db.encrypted` fields — validated eagerly at construction. */
219
+ _encryption;
220
+ /**
221
+ * @param adapterFactory - Creates a fresh adapter per table/view.
222
+ * @param loggerOrOptions - Either a logger (legacy signature) or a
223
+ * {@link TDbSpaceOptions} bag carrying `logger` and/or `encryption`.
224
+ */
225
+ constructor(adapterFactory, loggerOrOptions) {
83
226
  this.adapterFactory = adapterFactory;
84
- this.logger = logger;
227
+ if (loggerOrOptions && typeof loggerOrOptions.error === "function") this.logger = loggerOrOptions;
228
+ else {
229
+ const options = loggerOrOptions ?? {};
230
+ this.logger = options.logger ?? NoopLogger;
231
+ if (options.encryption) this._encryption = new DbEncryption(options.encryption);
232
+ }
85
233
  }
86
234
  /**
87
235
  * Auto-detects whether the type is a table or view and returns the
@@ -105,6 +253,7 @@ var DbSpace = class {
105
253
  this._allTables.add(readable);
106
254
  readable.setCascadeResolver((tableName) => this._getCascadeTargets(tableName));
107
255
  readable.setFkLookupResolver((tableName) => this._getFkLookupTarget(tableName));
256
+ readable.setEncryption(this._encryption);
108
257
  this._readables.set(type, readable);
109
258
  }
110
259
  return readable;
@@ -117,6 +266,7 @@ var DbSpace = class {
117
266
  let readable = this._readables.get(type);
118
267
  if (!readable) {
119
268
  readable = new AtscriptDbView(type, this.adapterFactory(), logger || this.logger, (t) => this.get(t));
269
+ readable.setEncryption(this._encryption);
120
270
  this._readables.set(type, readable);
121
271
  }
122
272
  return readable;
@@ -189,4 +339,4 @@ function translateQueryTree(node, resolveField) {
189
339
  return { [leftField]: { [comp.op]: comp.right } };
190
340
  }
191
341
  //#endregion
192
- export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, DbError, DbSpace, DocumentFieldMapper, FieldMappingStrategy, IntegrityStrategy, NativeIntegrity, NoopLogger, RelationalFieldMapper, TableMetadata, UniquSelect, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
342
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, CasExhaustedError, DbEncryption, DbError, DbSpace, DocumentFieldMapper, FieldMappingStrategy, IntegrityStrategy, NativeIntegrity, NoopLogger, RelationalFieldMapper, TableMetadata, UniquSelect, assertGeoPoint, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, guardAggregate, guardFilter, guardQuery, isDbFieldOp, isGeoIndexableType, isGeoPointType, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };