@getstrata/core 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/database/dialect.d.ts +4 -0
  3. package/dist/core/database/mysqlConnection.d.ts +12 -2
  4. package/dist/core/database/sqliteConnection.d.ts +2 -1
  5. package/dist/core/database/transaction.d.ts +2 -2
  6. package/dist/core/database/transactionControl.d.ts +2 -1
  7. package/dist/core/events/outbox/index.d.ts +54 -0
  8. package/dist/core/events/outbox/migration.d.ts +7 -0
  9. package/dist/core/http/requestMetaContext.d.ts +2 -0
  10. package/dist/core/http/response.d.ts +2 -1
  11. package/dist/core/security/safeFetch.d.ts +1 -0
  12. package/dist/entries/audit/exportAuditLogs.js +119 -17
  13. package/dist/entries/auth/basicAuthGuard.js +18 -0
  14. package/dist/entries/auth/oauth/oidcProvider.js +100 -17
  15. package/dist/entries/auth/oauth/providers.js +100 -17
  16. package/dist/entries/auth/oauth/samlProvider.js +18 -0
  17. package/dist/entries/auth/oneTimeToken.js +18 -0
  18. package/dist/entries/auth/passwordLogin.js +18 -0
  19. package/dist/entries/auth/saml/samlServiceProvider.js +18 -0
  20. package/dist/entries/auth/scimAuthMiddleware.js +19 -0
  21. package/dist/entries/database/migrations.js +18 -0
  22. package/dist/entries/database/sqliteConnection.js +101 -7
  23. package/dist/entries/events/outbox.js +1 -0
  24. package/dist/entries/http/response.js +39 -5
  25. package/dist/entries/jobs/exportAuditLogsJob.js +119 -17
  26. package/dist/entries/security/safeFetch.js +100 -17
  27. package/dist/entries/tenant/databaseTenantContext.js +19 -0
  28. package/dist/entries/tenant/tenantDatabaseScope.js +19 -0
  29. package/dist/entries/tracing/tracingMiddleware.js +100 -17
  30. package/dist/framework/public-api.d.ts +3 -1
  31. package/dist/index.js +591 -50
  32. package/package.json +6 -1
@@ -325,50 +325,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
325
325
 
326
326
  // ../../src/core/security/safeFetch.ts
327
327
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
328
+ function untilAborted(work, signal) {
329
+ signal.throwIfAborted();
330
+ return new Promise((resolve, reject) => {
331
+ const onAbort = () => reject(signal.reason);
332
+ signal.addEventListener("abort", onAbort, { once: true });
333
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
334
+ });
335
+ }
336
+ function preserveResponseMetadata(response, original) {
337
+ Object.defineProperties(response, {
338
+ url: { value: original.url },
339
+ redirected: { value: original.redirected },
340
+ type: { value: original.type }
341
+ });
342
+ const clone = response.clone.bind(response);
343
+ response.clone = () => preserveResponseMetadata(clone(), original);
344
+ return response;
345
+ }
346
+ function withBodyDeadline(response, signal, release) {
347
+ if (!response.body) {
348
+ release();
349
+ return response;
350
+ }
351
+ const reader = response.body.getReader();
352
+ let stopped = false;
353
+ let bodyController;
354
+ const stop = (reason, errorBody) => {
355
+ if (stopped)
356
+ return;
357
+ stopped = true;
358
+ release();
359
+ signal.removeEventListener("abort", onAbort);
360
+ if (errorBody)
361
+ bodyController.error(reason);
362
+ reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
363
+ };
364
+ const onAbort = () => stop(signal.reason, true);
365
+ const body = new ReadableStream({
366
+ start(controller) {
367
+ bodyController = controller;
368
+ signal.addEventListener("abort", onAbort, { once: true });
369
+ if (signal.aborted)
370
+ onAbort();
371
+ },
372
+ async pull(controller) {
373
+ try {
374
+ const result = await untilAborted(reader.read(), signal);
375
+ if (stopped)
376
+ return;
377
+ if (result.done) {
378
+ stopped = true;
379
+ release();
380
+ signal.removeEventListener("abort", onAbort);
381
+ reader.releaseLock();
382
+ controller.close();
383
+ } else {
384
+ controller.enqueue(result.value);
385
+ }
386
+ } catch (error) {
387
+ stop(error, true);
388
+ }
389
+ },
390
+ cancel(reason) {
391
+ stop(reason, false);
392
+ }
393
+ }, { highWaterMark: 0 });
394
+ return preserveResponseMetadata(new Response(body, {
395
+ status: response.status,
396
+ statusText: response.statusText,
397
+ headers: response.headers
398
+ }), response);
399
+ }
328
400
  async function safeFetch(input, init = {}, options = {}) {
401
+ init.signal?.throwIfAborted();
329
402
  const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
403
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
404
+ throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
405
+ }
330
406
  const maxRedirects = options.maxRedirects ?? 0;
331
- const resolveDns = options.resolveDns ?? true;
332
407
  const urlOptions = {
333
408
  allowHttp: options.allowHttp,
334
- resolveDns,
409
+ resolveDns: options.resolveDns ?? true,
335
410
  allowPrivate: options.allowPrivate
336
411
  };
337
412
  const controller = new AbortController;
338
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
413
+ const forwardAbort = () => controller.abort(init.signal?.reason);
414
+ init.signal?.addEventListener("abort", forwardAbort, { once: true });
415
+ const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
416
+ const release = () => {
417
+ clearTimeout(timeout);
418
+ init.signal?.removeEventListener("abort", forwardAbort);
419
+ };
339
420
  try {
340
- let current = await resolveSafeOutboundTarget(input, urlOptions);
421
+ let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
341
422
  let redirectCount = 0;
342
423
  while (true) {
424
+ controller.signal.throwIfAborted();
343
425
  const address = current.addresses[0];
344
426
  const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
345
427
  const headers = new Headers(init.headers);
346
- if (address && !headers.has("host")) {
428
+ if (address && !headers.has("host"))
347
429
  headers.set("Host", current.url.host);
348
- }
349
430
  const fetchInit = {
350
431
  ...init,
351
432
  headers,
352
433
  signal: controller.signal,
353
434
  redirect: "manual"
354
435
  };
355
- if (address) {
436
+ if (address)
356
437
  fetchInit.tls = { serverName: current.url.hostname };
357
- }
358
- const response = await fetch(fetchUrl, fetchInit);
438
+ const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
359
439
  if (response.status >= 300 && response.status < 400) {
360
440
  const location = response.headers.get("location");
361
- if (!location || redirectCount >= maxRedirects) {
362
- return response;
441
+ if (location && redirectCount < maxRedirects) {
442
+ if (response.body)
443
+ await untilAborted(response.body.cancel(), controller.signal);
444
+ current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
445
+ redirectCount++;
446
+ continue;
363
447
  }
364
- current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
365
- redirectCount += 1;
366
- continue;
367
448
  }
368
- return response;
449
+ return withBodyDeadline(response, controller.signal, release);
369
450
  }
370
- } finally {
371
- clearTimeout(timeout);
451
+ } catch (error) {
452
+ controller.abort(error);
453
+ release();
454
+ throw error;
372
455
  }
373
456
  }
374
457
 
@@ -171,6 +171,12 @@ var init_dialect = __esm(() => {
171
171
  nowExpression() {
172
172
  return "NOW()";
173
173
  },
174
+ epochMillisecondsExpression() {
175
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
176
+ },
177
+ rowLockClause(skipLocked = false) {
178
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
179
+ },
174
180
  timestampValue(value) {
175
181
  return value.toISOString();
176
182
  },
@@ -209,6 +215,12 @@ var init_dialect = __esm(() => {
209
215
  nowExpression() {
210
216
  return "CURRENT_TIMESTAMP";
211
217
  },
218
+ epochMillisecondsExpression() {
219
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
220
+ },
221
+ rowLockClause(skipLocked = false) {
222
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
223
+ },
212
224
  timestampValue(value) {
213
225
  return value.toISOString().slice(0, 19).replace("T", " ");
214
226
  },
@@ -247,6 +259,12 @@ var init_dialect = __esm(() => {
247
259
  nowExpression() {
248
260
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
249
261
  },
262
+ epochMillisecondsExpression() {
263
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
264
+ },
265
+ rowLockClause() {
266
+ return "";
267
+ },
250
268
  timestampValue(value) {
251
269
  return value.toISOString();
252
270
  },
@@ -98,6 +98,12 @@ var init_dialect = __esm(() => {
98
98
  nowExpression() {
99
99
  return "NOW()";
100
100
  },
101
+ epochMillisecondsExpression() {
102
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
103
+ },
104
+ rowLockClause(skipLocked = false) {
105
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
106
+ },
101
107
  timestampValue(value) {
102
108
  return value.toISOString();
103
109
  },
@@ -136,6 +142,12 @@ var init_dialect = __esm(() => {
136
142
  nowExpression() {
137
143
  return "CURRENT_TIMESTAMP";
138
144
  },
145
+ epochMillisecondsExpression() {
146
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
147
+ },
148
+ rowLockClause(skipLocked = false) {
149
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
150
+ },
139
151
  timestampValue(value) {
140
152
  return value.toISOString().slice(0, 19).replace("T", " ");
141
153
  },
@@ -174,6 +186,12 @@ var init_dialect = __esm(() => {
174
186
  nowExpression() {
175
187
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
176
188
  },
189
+ epochMillisecondsExpression() {
190
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
191
+ },
192
+ rowLockClause() {
193
+ return "";
194
+ },
177
195
  timestampValue(value) {
178
196
  return value.toISOString();
179
197
  },
@@ -98,6 +98,12 @@ var init_dialect = __esm(() => {
98
98
  nowExpression() {
99
99
  return "NOW()";
100
100
  },
101
+ epochMillisecondsExpression() {
102
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
103
+ },
104
+ rowLockClause(skipLocked = false) {
105
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
106
+ },
101
107
  timestampValue(value) {
102
108
  return value.toISOString();
103
109
  },
@@ -136,6 +142,12 @@ var init_dialect = __esm(() => {
136
142
  nowExpression() {
137
143
  return "CURRENT_TIMESTAMP";
138
144
  },
145
+ epochMillisecondsExpression() {
146
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
147
+ },
148
+ rowLockClause(skipLocked = false) {
149
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
150
+ },
139
151
  timestampValue(value) {
140
152
  return value.toISOString().slice(0, 19).replace("T", " ");
141
153
  },
@@ -174,6 +186,12 @@ var init_dialect = __esm(() => {
174
186
  nowExpression() {
175
187
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
176
188
  },
189
+ epochMillisecondsExpression() {
190
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
191
+ },
192
+ rowLockClause() {
193
+ return "";
194
+ },
177
195
  timestampValue(value) {
178
196
  return value.toISOString();
179
197
  },
@@ -171,6 +171,12 @@ var init_dialect = __esm(() => {
171
171
  nowExpression() {
172
172
  return "NOW()";
173
173
  },
174
+ epochMillisecondsExpression() {
175
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
176
+ },
177
+ rowLockClause(skipLocked = false) {
178
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
179
+ },
174
180
  timestampValue(value) {
175
181
  return value.toISOString();
176
182
  },
@@ -209,6 +215,12 @@ var init_dialect = __esm(() => {
209
215
  nowExpression() {
210
216
  return "CURRENT_TIMESTAMP";
211
217
  },
218
+ epochMillisecondsExpression() {
219
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
220
+ },
221
+ rowLockClause(skipLocked = false) {
222
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
223
+ },
212
224
  timestampValue(value) {
213
225
  return value.toISOString().slice(0, 19).replace("T", " ");
214
226
  },
@@ -247,6 +259,12 @@ var init_dialect = __esm(() => {
247
259
  nowExpression() {
248
260
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
249
261
  },
262
+ epochMillisecondsExpression() {
263
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
264
+ },
265
+ rowLockClause() {
266
+ return "";
267
+ },
250
268
  timestampValue(value) {
251
269
  return value.toISOString();
252
270
  },
@@ -127,6 +127,7 @@ async function runWithTransactionScope(callback) {
127
127
  var TransactionRollback, transactionScopes;
128
128
  var init_transactionControl = __esm(() => {
129
129
  init_asyncContextStore();
130
+ init_connectionContext();
130
131
  TransactionRollback = class TransactionRollback extends Error {
131
132
  result;
132
133
  constructor(result) {
@@ -408,6 +409,12 @@ var init_dialect = __esm(() => {
408
409
  nowExpression() {
409
410
  return "NOW()";
410
411
  },
412
+ epochMillisecondsExpression() {
413
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
414
+ },
415
+ rowLockClause(skipLocked = false) {
416
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
417
+ },
411
418
  timestampValue(value) {
412
419
  return value.toISOString();
413
420
  },
@@ -446,6 +453,12 @@ var init_dialect = __esm(() => {
446
453
  nowExpression() {
447
454
  return "CURRENT_TIMESTAMP";
448
455
  },
456
+ epochMillisecondsExpression() {
457
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
458
+ },
459
+ rowLockClause(skipLocked = false) {
460
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
461
+ },
449
462
  timestampValue(value) {
450
463
  return value.toISOString().slice(0, 19).replace("T", " ");
451
464
  },
@@ -484,6 +497,12 @@ var init_dialect = __esm(() => {
484
497
  nowExpression() {
485
498
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
486
499
  },
500
+ epochMillisecondsExpression() {
501
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
502
+ },
503
+ rowLockClause() {
504
+ return "";
505
+ },
487
506
  timestampValue(value) {
488
507
  return value.toISOString();
489
508
  },
@@ -98,6 +98,12 @@ var init_dialect = __esm(() => {
98
98
  nowExpression() {
99
99
  return "NOW()";
100
100
  },
101
+ epochMillisecondsExpression() {
102
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
103
+ },
104
+ rowLockClause(skipLocked = false) {
105
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
106
+ },
101
107
  timestampValue(value) {
102
108
  return value.toISOString();
103
109
  },
@@ -136,6 +142,12 @@ var init_dialect = __esm(() => {
136
142
  nowExpression() {
137
143
  return "CURRENT_TIMESTAMP";
138
144
  },
145
+ epochMillisecondsExpression() {
146
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
147
+ },
148
+ rowLockClause(skipLocked = false) {
149
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
150
+ },
139
151
  timestampValue(value) {
140
152
  return value.toISOString().slice(0, 19).replace("T", " ");
141
153
  },
@@ -174,6 +186,12 @@ var init_dialect = __esm(() => {
174
186
  nowExpression() {
175
187
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
176
188
  },
189
+ epochMillisecondsExpression() {
190
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
191
+ },
192
+ rowLockClause() {
193
+ return "";
194
+ },
177
195
  timestampValue(value) {
178
196
  return value.toISOString();
179
197
  },
@@ -1,5 +1,49 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/runtime/asyncContextStore.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ function createAsyncContextStore(key) {
17
+ const symbol = Symbol.for(key);
18
+ const globalRecord = globalThis;
19
+ const existing = globalRecord[symbol];
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const store = new AsyncLocalStorage;
24
+ globalRecord[symbol] = store;
25
+ return store;
26
+ }
27
+ var init_asyncContextStore = () => {};
28
+
29
+ // ../../src/core/database/connectionContext.ts
30
+ function runWithDatabaseConnection(connection, callback) {
31
+ return activeConnection.run(connection, callback);
32
+ }
33
+ function getActiveDatabaseConnection(fallback) {
34
+ return activeConnection.getStore() ?? fallback;
35
+ }
36
+ function hasActiveDatabaseConnection() {
37
+ return activeConnection.getStore() !== undefined;
38
+ }
39
+ var activeConnection;
40
+ var init_connectionContext = __esm(() => {
41
+ init_asyncContextStore();
42
+ activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
43
+ });
44
+
2
45
  // ../../src/core/database/sqliteConnection.ts
46
+ init_connectionContext();
3
47
  import { Database } from "bun:sqlite";
4
48
  function isRowReturning(sql) {
5
49
  const upper = sql.replace(/\s+/g, " ").trim().toUpperCase();
@@ -19,20 +63,70 @@ function createSqliteConnection(filename) {
19
63
  db.exec("PRAGMA journal_mode = WAL");
20
64
  db.exec("PRAGMA synchronous = NORMAL");
21
65
  }
22
- return {
66
+ let tail = Promise.resolve();
67
+ let pending = 0;
68
+ let closed = false;
69
+ const transactions = new WeakSet;
70
+ function execute(query, params) {
71
+ const statement = db.query(query);
72
+ const args = [...params];
73
+ if (isRowReturning(query))
74
+ return statement.all(...args);
75
+ statement.run(...args);
76
+ return [];
77
+ }
78
+ function enqueue(operation) {
79
+ if (closed)
80
+ return Promise.reject(new Error("SQLite connection is closed."));
81
+ pending++;
82
+ const result = tail.then(operation).finally(() => {
83
+ pending--;
84
+ });
85
+ tail = result.then(() => {}, () => {});
86
+ return result;
87
+ }
88
+ const connection = {
23
89
  async unsafe(query, params = []) {
24
- const statement = db.query(query);
25
- const args = [...params];
26
- if (isRowReturning(query)) {
27
- return statement.all(...args);
90
+ const active = getActiveDatabaseConnection(connection);
91
+ if (transactions.has(active))
92
+ return active.unsafe(query, params);
93
+ return enqueue(() => execute(query, params));
94
+ },
95
+ async begin(callback) {
96
+ if (transactions.has(getActiveDatabaseConnection(connection))) {
97
+ throw new Error("Use runInTransaction for nested SQLite transactions.");
28
98
  }
29
- statement.run(...args);
30
- return [];
99
+ return enqueue(async () => {
100
+ db.exec("BEGIN IMMEDIATE");
101
+ let open = true;
102
+ const transaction = {
103
+ async unsafe(query, params = []) {
104
+ if (!open)
105
+ throw new Error("SQLite transaction is no longer active.");
106
+ return execute(query, params);
107
+ }
108
+ };
109
+ transactions.add(transaction);
110
+ try {
111
+ const result = await runWithDatabaseConnection(transaction, () => callback(transaction));
112
+ db.exec("COMMIT");
113
+ return result;
114
+ } catch (error) {
115
+ db.exec("ROLLBACK");
116
+ throw error;
117
+ } finally {
118
+ open = false;
119
+ }
120
+ });
31
121
  },
32
122
  close() {
123
+ if (pending > 0)
124
+ throw new Error("Drain SQLite operations before closing the connection.");
125
+ closed = true;
33
126
  db.close();
34
127
  }
35
128
  };
129
+ return connection;
36
130
  }
37
131
  export {
38
132
  createSqliteConnection
@@ -0,0 +1 @@
1
+ export * from "../../index.js";
@@ -26,6 +26,22 @@ function createAsyncContextStore(key) {
26
26
  }
27
27
  var init_asyncContextStore = () => {};
28
28
 
29
+ // ../../src/core/database/connectionContext.ts
30
+ function runWithDatabaseConnection(connection, callback) {
31
+ return activeConnection.run(connection, callback);
32
+ }
33
+ function getActiveDatabaseConnection(fallback) {
34
+ return activeConnection.getStore() ?? fallback;
35
+ }
36
+ function hasActiveDatabaseConnection() {
37
+ return activeConnection.getStore() !== undefined;
38
+ }
39
+ var activeConnection;
40
+ var init_connectionContext = __esm(() => {
41
+ init_asyncContextStore();
42
+ activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
43
+ });
44
+
29
45
  // ../../src/core/database/transactionControl.ts
30
46
  function unwrapTransactionRollback(error, depth = 0) {
31
47
  if (error instanceof TransactionRollback) {
@@ -89,6 +105,7 @@ async function runWithTransactionScope(callback) {
89
105
  var TransactionRollback, transactionScopes;
90
106
  var init_transactionControl = __esm(() => {
91
107
  init_asyncContextStore();
108
+ init_connectionContext();
92
109
  TransactionRollback = class TransactionRollback extends Error {
93
110
  result;
94
111
  constructor(result) {
@@ -225,6 +242,19 @@ function isUniqueConstraintError(error) {
225
242
  // ../../src/core/http/response.ts
226
243
  init_transactionControl();
227
244
 
245
+ // ../../src/core/http/requestMetaContext.ts
246
+ init_asyncContextStore();
247
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
248
+ function runWithRequestMeta(meta, callback) {
249
+ return requestMetaContext.run(meta, callback);
250
+ }
251
+ function currentRequestMeta() {
252
+ return requestMetaContext.getStore() ?? {
253
+ ipAddress: null,
254
+ userAgent: null
255
+ };
256
+ }
257
+
228
258
  // ../../src/core/http/webErrorResponse.ts
229
259
  import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
230
260
  import { appLogger } from "@getstrata/core/logging/logger";
@@ -289,7 +319,7 @@ function readSpaPrefix() {
289
319
  }
290
320
 
291
321
  // ../../src/core/view/webErrorView.ts
292
- import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
322
+ import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
293
323
 
294
324
  // ../../src/core/http/safeInternalPath.ts
295
325
  var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
@@ -408,7 +438,7 @@ async function renderWebErrorHtml(input) {
408
438
  try {
409
439
  return await render({
410
440
  ...input,
411
- request: input.request ?? currentRequestMeta().request
441
+ request: input.request ?? currentRequestMeta2().request
412
442
  });
413
443
  } catch {
414
444
  return renderKernelErrorChrome(input);
@@ -490,7 +520,7 @@ function logServerError(error, mappedError) {
490
520
  });
491
521
  }
492
522
  async function webErrorResponse(error, request) {
493
- if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
523
+ if (currentRequestMeta().errorFormat === "json" || !request || !isViewsEnabled() || requestPrefersJson(request)) {
494
524
  return null;
495
525
  }
496
526
  const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
@@ -555,17 +585,21 @@ function withJsonErrorHandling(handler) {
555
585
  };
556
586
  }
557
587
  function createJsonErrorMiddleware() {
558
- return async (_request, next) => {
588
+ return async (_request, next) => await runWithRequestMeta({ ...currentRequestMeta(), errorFormat: "json" }, async () => {
559
589
  try {
560
590
  return await next();
561
591
  } catch (error) {
562
592
  requestTransactionRollback();
563
593
  return errorResponse(error);
564
594
  }
565
- };
595
+ });
596
+ }
597
+ function createWebErrorMiddleware() {
598
+ return async (request, next) => await withErrorHandling(async (_request) => next())(request);
566
599
  }
567
600
  export {
568
601
  createJsonErrorMiddleware,
602
+ createWebErrorMiddleware,
569
603
  createdResponse,
570
604
  errorResponse,
571
605
  jsonResponse,