@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/{db-readable-mhPp-MPv.d.cts → db-readable-BepVc21V.d.cts} +196 -3
- package/dist/{db-readable-CXdHBtF6.d.mts → db-readable-Ds01ezjj.d.mts} +196 -3
- package/dist/{db-space-BYyVZnL1.d.mts → db-space-0U_4PiNS.d.mts} +33 -4
- package/dist/{db-space-BhOc9_OO.d.cts → db-space-CWhIEC7E.d.cts} +33 -4
- package/dist/{db-view-GcbegVBD.cjs → db-view-Bw_hlrWw.cjs} +477 -6
- package/dist/{db-view-Sq4wCceR.mjs → db-view-TyzB5VFb.mjs} +442 -7
- package/dist/index.cjs +162 -5
- package/dist/index.d.cts +34 -4
- package/dist/index.d.mts +34 -4
- package/dist/index.mjs +156 -6
- package/dist/plugin.cjs +48 -0
- package/dist/plugin.mjs +48 -0
- package/dist/rel.d.cts +1 -1
- package/dist/rel.d.mts +1 -1
- package/dist/sync.cjs +2 -1
- package/dist/sync.d.cts +4 -2
- package/dist/sync.d.mts +4 -2
- package/dist/sync.mjs +2 -1
- package/dist/{validator-f43UcYiY.cjs → validator-BSk8lPH1.cjs} +15 -0
- package/dist/{validator-bLsSgi0N.mjs → validator-C7plt6Kf.mjs} +15 -0
- package/dist/validator.cjs +1 -1
- package/dist/validator.mjs +1 -1
- package/package.json +6 -6
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-
|
|
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-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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 =
|
|
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 };
|