@getstrata/core 2.2.10 → 2.2.12

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.
@@ -26,7 +26,7 @@ function isFieldEncryptionEnabled() {
26
26
  if (featureFlag === "true") {
27
27
  return true;
28
28
  }
29
- return Boolean(resolveEncryptionKey());
29
+ return Boolean(resolveFieldEncryptionKeyring() || resolveEncryptionKey());
30
30
  }
31
31
  function encryptField(plaintext, key) {
32
32
  const iv = randomBytes(IV_LENGTH);
@@ -56,6 +56,13 @@ function normalizeEmail(email) {
56
56
  }
57
57
  function protectEmail(email) {
58
58
  const normalized = normalizeEmail(email);
59
+ const configured = resolveFieldEncryptionKeyring();
60
+ if (configured && isFieldEncryptionEnabled()) {
61
+ return {
62
+ storedEmail: configured.encrypt(normalized, "email"),
63
+ emailLookup: configured.keyring.lookup(normalized)
64
+ };
65
+ }
59
66
  const key = resolveEncryptionKey();
60
67
  if (!key || !isFieldEncryptionEnabled()) {
61
68
  return { storedEmail: normalized, emailLookup: normalized };
@@ -66,6 +73,11 @@ function protectEmail(email) {
66
73
  };
67
74
  }
68
75
  function revealEmail(storedEmail) {
76
+ const configured = resolveFieldEncryptionKeyring();
77
+ if (configured && storedEmail.startsWith("enc:"))
78
+ return configured.keyring.decrypt(storedEmail, "email");
79
+ if (storedEmail.startsWith("enc:") && !storedEmail.startsWith(ENCRYPTION_PREFIX))
80
+ throw new Error("Identified encrypted fields require KMS_ENCRYPTION_KEYRING.");
69
81
  const key = resolveEncryptionKey();
70
82
  if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
71
83
  return storedEmail;
@@ -74,13 +86,144 @@ function revealEmail(storedEmail) {
74
86
  }
75
87
  function emailLookupForQuery(email) {
76
88
  const normalized = normalizeEmail(email);
89
+ const configured = resolveFieldEncryptionKeyring();
90
+ if (configured && isFieldEncryptionEnabled())
91
+ return configured.keyring.lookup(normalized);
77
92
  const key = resolveEncryptionKey();
78
93
  if (!key || !isFieldEncryptionEnabled()) {
79
94
  return normalized;
80
95
  }
81
96
  return hashLookupValue(normalized, key);
82
97
  }
98
+ function createFieldEncryptionKeyring(options) {
99
+ const identifier = /^[A-Za-z0-9_-]{1,64}$/;
100
+ const copyKey = (value) => {
101
+ if (!(value instanceof Uint8Array) || value.byteLength !== 32)
102
+ throw new TypeError("Field encryption keys must be 32 bytes.");
103
+ return Buffer.from(value);
104
+ };
105
+ const entries = Object.entries(options.encryptionKeys);
106
+ if (entries.length < 1 || entries.length > 32)
107
+ throw new TypeError("Field encryption keyrings require 1 to 32 keys.");
108
+ const keys = new Map;
109
+ for (const [id, key] of entries) {
110
+ if (!identifier.test(id))
111
+ throw new TypeError("Invalid field encryption key identifier.");
112
+ keys.set(id, copyKey(key));
113
+ }
114
+ const activeKeyId = options.activeKeyId;
115
+ const active = keys.get(activeKeyId);
116
+ if (!active)
117
+ throw new TypeError("Active field encryption key is not retained.");
118
+ const legacy = options.legacyV1Key === undefined ? undefined : copyKey(options.legacyV1Key);
119
+ const lookup = copyKey(options.lookupKey);
120
+ const associatedData = (id, purpose) => {
121
+ if (!identifier.test(purpose))
122
+ throw new TypeError("Invalid field encryption purpose.");
123
+ return Buffer.from(`strata:field:v2:${id}:${purpose}`, "utf8");
124
+ };
125
+ const decodePayload = (encoded) => {
126
+ const payload = Buffer.from(encoded, "base64");
127
+ if (payload.length < IV_LENGTH + TAG_LENGTH || payload.toString("base64") !== encoded)
128
+ throw new Error("Invalid encrypted field.");
129
+ return payload;
130
+ };
131
+ return Object.freeze({
132
+ activeKeyId,
133
+ encrypt(plaintext, purpose) {
134
+ const aad = associatedData(activeKeyId, purpose);
135
+ const iv = randomBytes(IV_LENGTH);
136
+ const cipher = createCipheriv("aes-256-gcm", active, iv, { authTagLength: TAG_LENGTH });
137
+ cipher.setAAD(aad);
138
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
139
+ const payload = Buffer.concat([iv, ciphertext, cipher.getAuthTag()]).toString("base64");
140
+ return `enc:v2:${activeKeyId}:${payload}`;
141
+ },
142
+ decrypt(value, purpose) {
143
+ associatedData(activeKeyId, purpose);
144
+ try {
145
+ if (value.startsWith(ENCRYPTION_PREFIX)) {
146
+ if (!legacy)
147
+ throw new Error("Missing legacy key");
148
+ decodePayload(value.slice(ENCRYPTION_PREFIX.length));
149
+ return decryptField(value, legacy);
150
+ }
151
+ const parts = value.split(":");
152
+ if (parts.length !== 4 || parts[0] !== "enc" || parts[1] !== "v2")
153
+ throw new Error("Invalid envelope");
154
+ const id = parts[2] ?? "";
155
+ const key = keys.get(id);
156
+ if (!key)
157
+ throw new Error("Missing retained key");
158
+ const payload = decodePayload(parts[3] ?? "");
159
+ const decipher = createDecipheriv("aes-256-gcm", key, payload.subarray(0, IV_LENGTH), {
160
+ authTagLength: TAG_LENGTH
161
+ });
162
+ decipher.setAAD(associatedData(id, purpose));
163
+ decipher.setAuthTag(payload.subarray(payload.length - TAG_LENGTH));
164
+ return Buffer.concat([
165
+ decipher.update(payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH)),
166
+ decipher.final()
167
+ ]).toString("utf8");
168
+ } catch {
169
+ throw new Error("Unable to decrypt encrypted field.");
170
+ }
171
+ },
172
+ lookup(normalizedValue) {
173
+ return hashLookupValue(normalizedValue, lookup);
174
+ }
175
+ });
176
+ }
177
+ function resolveFieldEncryptionKeyring(env = process.env) {
178
+ const raw = env.KMS_ENCRYPTION_KEYRING;
179
+ if (raw === undefined)
180
+ return null;
181
+ try {
182
+ if (!raw.trim() || raw.length > 16384 || env.KMS_ENCRYPTION_KEY?.trim())
183
+ throw new Error("Invalid configuration");
184
+ const parsed = JSON.parse(raw);
185
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
186
+ throw new Error("Invalid configuration");
187
+ const config = parsed;
188
+ if (Object.keys(config).some((key) => !["activeKeyId", "encryptionKeys", "legacyV1Key", "lookupKey", "writeVersion"].includes(key)))
189
+ throw new Error("Unknown option");
190
+ if (typeof config.activeKeyId !== "string" || config.writeVersion !== 1 && config.writeVersion !== 2 || !config.encryptionKeys || typeof config.encryptionKeys !== "object" || Array.isArray(config.encryptionKeys))
191
+ throw new Error("Invalid configuration");
192
+ const decodeKey = (value) => {
193
+ if (typeof value !== "string")
194
+ throw new Error("Invalid key");
195
+ if (/^[0-9a-f]{64}$/i.test(value))
196
+ return Buffer.from(value, "hex");
197
+ const decoded = Buffer.from(value, "base64");
198
+ if (decoded.length !== 32 || decoded.toString("base64") !== value)
199
+ throw new Error("Invalid key");
200
+ return decoded;
201
+ };
202
+ const keys = Object.fromEntries(Object.entries(config.encryptionKeys).map(([id, value]) => [id, decodeKey(value)]));
203
+ const legacy = config.legacyV1Key === undefined ? undefined : decodeKey(config.legacyV1Key);
204
+ const lookup = decodeKey(config.lookupKey);
205
+ if (config.writeVersion === 1 && !legacy || legacy && !legacy.equals(lookup))
206
+ throw new Error("Incompatible lookup key");
207
+ const keyring = createFieldEncryptionKeyring({
208
+ activeKeyId: config.activeKeyId,
209
+ encryptionKeys: keys,
210
+ legacyV1Key: legacy,
211
+ lookupKey: lookup
212
+ });
213
+ const writeVersion = config.writeVersion;
214
+ return Object.freeze({
215
+ keyring,
216
+ writeVersion,
217
+ encrypt(plaintext, purpose) {
218
+ return writeVersion === 1 && legacy ? encryptField(plaintext, legacy) : keyring.encrypt(plaintext, purpose);
219
+ }
220
+ });
221
+ } catch {
222
+ throw new Error("Invalid KMS_ENCRYPTION_KEYRING configuration.");
223
+ }
224
+ }
83
225
  export {
226
+ createFieldEncryptionKeyring,
84
227
  decryptField,
85
228
  emailLookupForQuery,
86
229
  encryptField,
@@ -89,5 +232,6 @@ export {
89
232
  normalizeEmail,
90
233
  protectEmail,
91
234
  resolveEncryptionKey,
235
+ resolveFieldEncryptionKeyring,
92
236
  revealEmail
93
237
  };
@@ -0,0 +1 @@
1
+ export * from "../../index.js";
@@ -46,7 +46,7 @@ function isFieldEncryptionEnabled() {
46
46
  if (featureFlag === "true") {
47
47
  return true;
48
48
  }
49
- return Boolean(resolveEncryptionKey());
49
+ return Boolean(resolveFieldEncryptionKeyring() || resolveEncryptionKey());
50
50
  }
51
51
  function encryptField(plaintext, key) {
52
52
  const iv = randomBytes(IV_LENGTH);
@@ -76,6 +76,13 @@ function normalizeEmail(email) {
76
76
  }
77
77
  function protectEmail(email) {
78
78
  const normalized = normalizeEmail(email);
79
+ const configured = resolveFieldEncryptionKeyring();
80
+ if (configured && isFieldEncryptionEnabled()) {
81
+ return {
82
+ storedEmail: configured.encrypt(normalized, "email"),
83
+ emailLookup: configured.keyring.lookup(normalized)
84
+ };
85
+ }
79
86
  const key = resolveEncryptionKey();
80
87
  if (!key || !isFieldEncryptionEnabled()) {
81
88
  return { storedEmail: normalized, emailLookup: normalized };
@@ -86,6 +93,11 @@ function protectEmail(email) {
86
93
  };
87
94
  }
88
95
  function revealEmail(storedEmail) {
96
+ const configured = resolveFieldEncryptionKeyring();
97
+ if (configured && storedEmail.startsWith("enc:"))
98
+ return configured.keyring.decrypt(storedEmail, "email");
99
+ if (storedEmail.startsWith("enc:") && !storedEmail.startsWith(ENCRYPTION_PREFIX))
100
+ throw new Error("Identified encrypted fields require KMS_ENCRYPTION_KEYRING.");
89
101
  const key = resolveEncryptionKey();
90
102
  if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
91
103
  return storedEmail;
@@ -94,15 +106,148 @@ function revealEmail(storedEmail) {
94
106
  }
95
107
  function emailLookupForQuery(email) {
96
108
  const normalized = normalizeEmail(email);
109
+ const configured = resolveFieldEncryptionKeyring();
110
+ if (configured && isFieldEncryptionEnabled())
111
+ return configured.keyring.lookup(normalized);
97
112
  const key = resolveEncryptionKey();
98
113
  if (!key || !isFieldEncryptionEnabled()) {
99
114
  return normalized;
100
115
  }
101
116
  return hashLookupValue(normalized, key);
102
117
  }
118
+ function createFieldEncryptionKeyring(options) {
119
+ const identifier = /^[A-Za-z0-9_-]{1,64}$/;
120
+ const copyKey = (value) => {
121
+ if (!(value instanceof Uint8Array) || value.byteLength !== 32)
122
+ throw new TypeError("Field encryption keys must be 32 bytes.");
123
+ return Buffer.from(value);
124
+ };
125
+ const entries = Object.entries(options.encryptionKeys);
126
+ if (entries.length < 1 || entries.length > 32)
127
+ throw new TypeError("Field encryption keyrings require 1 to 32 keys.");
128
+ const keys = new Map;
129
+ for (const [id, key] of entries) {
130
+ if (!identifier.test(id))
131
+ throw new TypeError("Invalid field encryption key identifier.");
132
+ keys.set(id, copyKey(key));
133
+ }
134
+ const activeKeyId = options.activeKeyId;
135
+ const active = keys.get(activeKeyId);
136
+ if (!active)
137
+ throw new TypeError("Active field encryption key is not retained.");
138
+ const legacy = options.legacyV1Key === undefined ? undefined : copyKey(options.legacyV1Key);
139
+ const lookup = copyKey(options.lookupKey);
140
+ const associatedData = (id, purpose) => {
141
+ if (!identifier.test(purpose))
142
+ throw new TypeError("Invalid field encryption purpose.");
143
+ return Buffer.from(`strata:field:v2:${id}:${purpose}`, "utf8");
144
+ };
145
+ const decodePayload = (encoded) => {
146
+ const payload = Buffer.from(encoded, "base64");
147
+ if (payload.length < IV_LENGTH + TAG_LENGTH || payload.toString("base64") !== encoded)
148
+ throw new Error("Invalid encrypted field.");
149
+ return payload;
150
+ };
151
+ return Object.freeze({
152
+ activeKeyId,
153
+ encrypt(plaintext, purpose) {
154
+ const aad = associatedData(activeKeyId, purpose);
155
+ const iv = randomBytes(IV_LENGTH);
156
+ const cipher = createCipheriv("aes-256-gcm", active, iv, { authTagLength: TAG_LENGTH });
157
+ cipher.setAAD(aad);
158
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
159
+ const payload = Buffer.concat([iv, ciphertext, cipher.getAuthTag()]).toString("base64");
160
+ return `enc:v2:${activeKeyId}:${payload}`;
161
+ },
162
+ decrypt(value, purpose) {
163
+ associatedData(activeKeyId, purpose);
164
+ try {
165
+ if (value.startsWith(ENCRYPTION_PREFIX)) {
166
+ if (!legacy)
167
+ throw new Error("Missing legacy key");
168
+ decodePayload(value.slice(ENCRYPTION_PREFIX.length));
169
+ return decryptField(value, legacy);
170
+ }
171
+ const parts = value.split(":");
172
+ if (parts.length !== 4 || parts[0] !== "enc" || parts[1] !== "v2")
173
+ throw new Error("Invalid envelope");
174
+ const id = parts[2] ?? "";
175
+ const key = keys.get(id);
176
+ if (!key)
177
+ throw new Error("Missing retained key");
178
+ const payload = decodePayload(parts[3] ?? "");
179
+ const decipher = createDecipheriv("aes-256-gcm", key, payload.subarray(0, IV_LENGTH), {
180
+ authTagLength: TAG_LENGTH
181
+ });
182
+ decipher.setAAD(associatedData(id, purpose));
183
+ decipher.setAuthTag(payload.subarray(payload.length - TAG_LENGTH));
184
+ return Buffer.concat([
185
+ decipher.update(payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH)),
186
+ decipher.final()
187
+ ]).toString("utf8");
188
+ } catch {
189
+ throw new Error("Unable to decrypt encrypted field.");
190
+ }
191
+ },
192
+ lookup(normalizedValue) {
193
+ return hashLookupValue(normalizedValue, lookup);
194
+ }
195
+ });
196
+ }
197
+ function resolveFieldEncryptionKeyring(env = process.env) {
198
+ const raw = env.KMS_ENCRYPTION_KEYRING;
199
+ if (raw === undefined)
200
+ return null;
201
+ try {
202
+ if (!raw.trim() || raw.length > 16384 || env.KMS_ENCRYPTION_KEY?.trim())
203
+ throw new Error("Invalid configuration");
204
+ const parsed = JSON.parse(raw);
205
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
206
+ throw new Error("Invalid configuration");
207
+ const config = parsed;
208
+ if (Object.keys(config).some((key) => !["activeKeyId", "encryptionKeys", "legacyV1Key", "lookupKey", "writeVersion"].includes(key)))
209
+ throw new Error("Unknown option");
210
+ if (typeof config.activeKeyId !== "string" || config.writeVersion !== 1 && config.writeVersion !== 2 || !config.encryptionKeys || typeof config.encryptionKeys !== "object" || Array.isArray(config.encryptionKeys))
211
+ throw new Error("Invalid configuration");
212
+ const decodeKey = (value) => {
213
+ if (typeof value !== "string")
214
+ throw new Error("Invalid key");
215
+ if (/^[0-9a-f]{64}$/i.test(value))
216
+ return Buffer.from(value, "hex");
217
+ const decoded = Buffer.from(value, "base64");
218
+ if (decoded.length !== 32 || decoded.toString("base64") !== value)
219
+ throw new Error("Invalid key");
220
+ return decoded;
221
+ };
222
+ const keys = Object.fromEntries(Object.entries(config.encryptionKeys).map(([id, value]) => [id, decodeKey(value)]));
223
+ const legacy = config.legacyV1Key === undefined ? undefined : decodeKey(config.legacyV1Key);
224
+ const lookup = decodeKey(config.lookupKey);
225
+ if (config.writeVersion === 1 && !legacy || legacy && !legacy.equals(lookup))
226
+ throw new Error("Incompatible lookup key");
227
+ const keyring = createFieldEncryptionKeyring({
228
+ activeKeyId: config.activeKeyId,
229
+ encryptionKeys: keys,
230
+ legacyV1Key: legacy,
231
+ lookupKey: lookup
232
+ });
233
+ const writeVersion = config.writeVersion;
234
+ return Object.freeze({
235
+ keyring,
236
+ writeVersion,
237
+ encrypt(plaintext, purpose) {
238
+ return writeVersion === 1 && legacy ? encryptField(plaintext, legacy) : keyring.encrypt(plaintext, purpose);
239
+ }
240
+ });
241
+ } catch {
242
+ throw new Error("Invalid KMS_ENCRYPTION_KEYRING configuration.");
243
+ }
244
+ }
103
245
 
104
246
  // ../../src/core/crypto/mfaSecret.ts
105
247
  function protectMfaSecret(secret) {
248
+ const configured = resolveFieldEncryptionKeyring();
249
+ if (configured)
250
+ return configured.encrypt(secret, "mfa");
106
251
  const key = resolveEncryptionKey();
107
252
  if (!key) {
108
253
  throw new Error("MFA secrets require KMS_ENCRYPTION_KEY.");
@@ -113,6 +258,14 @@ function revealMfaSecret(stored) {
113
258
  if (!stored) {
114
259
  return null;
115
260
  }
261
+ const configured = resolveFieldEncryptionKeyring();
262
+ if (configured) {
263
+ if (!stored.startsWith("enc:"))
264
+ throw new Error("MFA secrets must be encrypted.");
265
+ return configured.keyring.decrypt(stored, "mfa");
266
+ }
267
+ if (stored.startsWith("enc:") && !stored.startsWith("enc:v1:"))
268
+ throw new Error("Identified MFA secrets require KMS_ENCRYPTION_KEYRING.");
116
269
  if (!stored.startsWith("enc:v1:")) {
117
270
  if (isProductionEnv() || resolveEncryptionKey()) {
118
271
  throw new Error("MFA secrets must be encrypted.");
@@ -694,10 +694,10 @@ function compileColumn(driver, column, mode) {
694
694
  parts[1] = `${parts[1]} AUTO_INCREMENT`;
695
695
  }
696
696
  if (column.isPrimary && mode === "create") {
697
- if (driver === "sqlite") {
697
+ if (driver === "sqlite" && column.autoIncrement) {
698
698
  parts.push("PRIMARY KEY AUTOINCREMENT");
699
699
  } else {
700
- parts.push("PRIMARY KEY");
700
+ parts.push(driver === "sqlite" && !column.isNullable ? "PRIMARY KEY NOT NULL" : "PRIMARY KEY");
701
701
  }
702
702
  } else if (!column.isNullable) {
703
703
  parts.push("NOT NULL");
@@ -45,6 +45,126 @@ var init_connectionContext = __esm(() => {
45
45
  // ../../src/core/database/sqliteConnection.ts
46
46
  init_connectionContext();
47
47
  import { Database } from "bun:sqlite";
48
+ import { resolve } from "path";
49
+
50
+ // ../../src/core/database/isolatedReadOnlyObservation.ts
51
+ var childSource = `
52
+ import { Database } from 'bun:sqlite';
53
+ let db;
54
+ let mysql;
55
+ process.on('message', async message => {
56
+ try {
57
+ if (message.init) {
58
+ if (message.driver === 'mysql') {
59
+ const module = await import(message.module);
60
+ mysql = await module.createConnection({ uri: message.url, timezone: 'Z', connectTimeout: message.timeoutMs });
61
+ await mysql.query("SET time_zone = '+00:00'");
62
+ await mysql.query('SET SESSION max_execution_time = ' + message.timeoutMs);
63
+ await mysql.query('SET SESSION lock_wait_timeout = 1');
64
+ await mysql.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
65
+ await mysql.query('START TRANSACTION READ ONLY');
66
+ } else {
67
+ db = new Database(message.filename, { readonly: true, create: false });
68
+ db.exec('PRAGMA busy_timeout = 0');
69
+ db.exec('PRAGMA query_only = ON');
70
+ db.exec('BEGIN');
71
+ }
72
+ process.send({ ok: true, rows: [] });
73
+ } else {
74
+ const rows = mysql ? (message.params.length ? await mysql.execute(message.query, message.params) : await mysql.query(message.query))[0] : db.query(message.query).all(...message.params);
75
+ process.send({ ok: true, rows });
76
+ }
77
+ } catch {
78
+ // No SQL, file paths, payloads or driver exception text crosses the error channel.
79
+ process.send({ ok: false });
80
+ }
81
+ });
82
+ `;
83
+ async function observeIsolatedReadOnly(profile, operation, options) {
84
+ options.signal.throwIfAborted();
85
+ if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 5000)
86
+ throw new TypeError("SQL observation timeout must be between 1 and 5000 milliseconds.");
87
+ const executable = Bun.which("bun", { PATH: process.env.PATH ?? "" });
88
+ if (!executable)
89
+ throw new Error("SQL observations require the Bun CLI on PATH.");
90
+ let pending;
91
+ let stopped = false;
92
+ const { promise: aborted, reject: rejectAbort } = Promise.withResolvers();
93
+ const fail = (error) => {
94
+ pending?.reject(error);
95
+ pending = undefined;
96
+ };
97
+ const child = Bun.spawn({
98
+ cmd: [
99
+ executable,
100
+ "--no-env-file",
101
+ `--config=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
102
+ "-e",
103
+ childSource
104
+ ],
105
+ env: {},
106
+ stdout: "ignore",
107
+ stderr: "ignore",
108
+ serialization: "advanced",
109
+ ipc(message) {
110
+ const response = message;
111
+ const request = pending;
112
+ pending = undefined;
113
+ if (response?.ok === true && Array.isArray(response.rows))
114
+ request?.resolve(response.rows);
115
+ else
116
+ request?.reject(new Error("Read-only SQL observation failed."));
117
+ }
118
+ });
119
+ const exited = child.exited.then(() => {
120
+ fail(new Error("SQL observation process exited."));
121
+ });
122
+ const abort = (reason) => {
123
+ stopped = true;
124
+ child.kill("SIGKILL");
125
+ fail(reason);
126
+ rejectAbort(reason);
127
+ };
128
+ const onAbort = () => abort(options.signal.reason);
129
+ options.signal.addEventListener("abort", onAbort, { once: true });
130
+ const timer = setTimeout(() => abort(new Error("SQL observation timed out.")), options.timeoutMs);
131
+ const request = (message) => {
132
+ if (stopped)
133
+ return Promise.reject(new Error("SQL observation is closed."));
134
+ if (pending)
135
+ return Promise.reject(new Error("Concurrent SQL observation queries are not supported."));
136
+ return new Promise((resolve, reject) => {
137
+ pending = { resolve, reject };
138
+ child.send(message);
139
+ });
140
+ };
141
+ try {
142
+ if (options.signal.aborted)
143
+ onAbort();
144
+ const work = (async () => {
145
+ await request({ init: true, ...profile, timeoutMs: options.timeoutMs });
146
+ options.signal.throwIfAborted();
147
+ return operation({
148
+ async unsafe(query, params = []) {
149
+ return await request({
150
+ query,
151
+ params: params.map((value) => profile.driver === "sqlite" && value instanceof Date ? value.toISOString() : value)
152
+ });
153
+ }
154
+ });
155
+ })();
156
+ return await Promise.race([work, aborted]);
157
+ } finally {
158
+ stopped = true;
159
+ clearTimeout(timer);
160
+ options.signal.removeEventListener("abort", onAbort);
161
+ child.kill("SIGKILL");
162
+ fail(new Error("SQL observation is closed."));
163
+ await exited;
164
+ }
165
+ }
166
+
167
+ // ../../src/core/database/sqliteConnection.ts
48
168
  function isRowReturning(sql) {
49
169
  const upper = sql.replace(/\s+/g, " ").trim().toUpperCase();
50
170
  if (upper.includes(" RETURNING ")) {
@@ -56,7 +176,8 @@ function createSqliteConnection(filename) {
56
176
  if (!filename.trim()) {
57
177
  throw new Error("SQLite path is not configured. Pass a filename or :memory:.");
58
178
  }
59
- const db = new Database(filename, { create: true });
179
+ const absoluteFilename = filename === ":memory:" ? filename : resolve(filename);
180
+ const db = new Database(absoluteFilename, { create: true });
60
181
  db.exec("PRAGMA foreign_keys = ON");
61
182
  db.exec("PRAGMA busy_timeout = 5000");
62
183
  if (filename !== ":memory:") {
@@ -119,6 +240,21 @@ function createSqliteConnection(filename) {
119
240
  }
120
241
  });
121
242
  },
243
+ async observeReadOnly(operation, options) {
244
+ if (closed)
245
+ throw new Error("SQLite connection is closed.");
246
+ if (absoluteFilename === ":memory:")
247
+ throw new Error("SQL observations require file-backed SQLite.");
248
+ pending++;
249
+ try {
250
+ return await observeIsolatedReadOnly({ driver: "sqlite", filename: absoluteFilename }, async (transaction) => {
251
+ transactions.add(transaction);
252
+ return await runWithDatabaseConnection(transaction, () => operation(transaction));
253
+ }, options);
254
+ } finally {
255
+ pending--;
256
+ }
257
+ },
122
258
  close() {
123
259
  if (pending > 0)
124
260
  throw new Error("Drain SQLite operations before closing the connection.");