@getstrata/core 2.0.2 → 2.0.4

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 (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/database/baseRepository.d.ts +2 -0
  3. package/dist/core/database/dialect.d.ts +4 -0
  4. package/dist/core/database/mysqlConnection.d.ts +12 -2
  5. package/dist/core/database/sqliteConnection.d.ts +2 -1
  6. package/dist/core/database/transaction.d.ts +2 -2
  7. package/dist/core/database/transactionControl.d.ts +2 -1
  8. package/dist/core/events/eventBus.d.ts +4 -0
  9. package/dist/core/events/outbox/index.d.ts +55 -0
  10. package/dist/core/events/outbox/migration.d.ts +7 -0
  11. package/dist/core/http/requestMetaContext.d.ts +2 -0
  12. package/dist/core/http/response.d.ts +2 -1
  13. package/dist/core/openapi/registeredRoute.d.ts +40 -1
  14. package/dist/core/security/safeFetch.d.ts +1 -0
  15. package/dist/entries/audit/exportAuditLogs.js +137 -17
  16. package/dist/entries/auth/basicAuthGuard.js +18 -0
  17. package/dist/entries/auth/oauth/oidcProvider.js +100 -17
  18. package/dist/entries/auth/oauth/providers.js +100 -17
  19. package/dist/entries/auth/oauth/samlProvider.js +18 -0
  20. package/dist/entries/auth/oneTimeToken.js +18 -0
  21. package/dist/entries/auth/passwordLogin.js +18 -0
  22. package/dist/entries/auth/saml/samlServiceProvider.js +18 -0
  23. package/dist/entries/auth/scimAuthMiddleware.js +37 -0
  24. package/dist/entries/database/migrations.js +18 -0
  25. package/dist/entries/database/sqliteConnection.js +101 -7
  26. package/dist/entries/events/outbox.js +1 -0
  27. package/dist/entries/http/response.js +39 -5
  28. package/dist/entries/jobs/exportAuditLogsJob.js +137 -17
  29. package/dist/entries/openapi/generator.js +42 -12
  30. package/dist/entries/openapi/validate.js +10 -0
  31. package/dist/entries/security/safeFetch.js +100 -17
  32. package/dist/entries/tenant/databaseTenantContext.js +37 -0
  33. package/dist/entries/tenant/tenantDatabaseScope.js +37 -0
  34. package/dist/entries/tracing/tracingMiddleware.js +100 -17
  35. package/dist/framework/public-api.d.ts +3 -1
  36. package/dist/index.js +649 -60
  37. package/package.json +6 -1
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -164,6 +182,7 @@ async function runWithTransactionScope(callback) {
164
182
  var TransactionRollback, transactionScopes;
165
183
  var init_transactionControl = __esm(() => {
166
184
  init_asyncContextStore();
185
+ init_connectionContext();
167
186
  TransactionRollback = class TransactionRollback extends Error {
168
187
  result;
169
188
  constructor(result) {
@@ -452,6 +471,12 @@ var init_dialect = __esm(() => {
452
471
  nowExpression() {
453
472
  return "NOW()";
454
473
  },
474
+ epochMillisecondsExpression() {
475
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
476
+ },
477
+ rowLockClause(skipLocked = false) {
478
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
479
+ },
455
480
  timestampValue(value) {
456
481
  return value.toISOString();
457
482
  },
@@ -490,6 +515,12 @@ var init_dialect = __esm(() => {
490
515
  nowExpression() {
491
516
  return "CURRENT_TIMESTAMP";
492
517
  },
518
+ epochMillisecondsExpression() {
519
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
520
+ },
521
+ rowLockClause(skipLocked = false) {
522
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
523
+ },
493
524
  timestampValue(value) {
494
525
  return value.toISOString().slice(0, 19).replace("T", " ");
495
526
  },
@@ -528,6 +559,12 @@ var init_dialect = __esm(() => {
528
559
  nowExpression() {
529
560
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
530
561
  },
562
+ epochMillisecondsExpression() {
563
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
564
+ },
565
+ rowLockClause() {
566
+ return "";
567
+ },
531
568
  timestampValue(value) {
532
569
  return value.toISOString();
533
570
  },
@@ -1077,50 +1114,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
1077
1114
 
1078
1115
  // ../../src/core/security/safeFetch.ts
1079
1116
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1117
+ function untilAborted(work, signal) {
1118
+ signal.throwIfAborted();
1119
+ return new Promise((resolve, reject) => {
1120
+ const onAbort = () => reject(signal.reason);
1121
+ signal.addEventListener("abort", onAbort, { once: true });
1122
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
1123
+ });
1124
+ }
1125
+ function preserveResponseMetadata(response, original) {
1126
+ Object.defineProperties(response, {
1127
+ url: { value: original.url },
1128
+ redirected: { value: original.redirected },
1129
+ type: { value: original.type }
1130
+ });
1131
+ const clone = response.clone.bind(response);
1132
+ response.clone = () => preserveResponseMetadata(clone(), original);
1133
+ return response;
1134
+ }
1135
+ function withBodyDeadline(response, signal, release) {
1136
+ if (!response.body) {
1137
+ release();
1138
+ return response;
1139
+ }
1140
+ const reader = response.body.getReader();
1141
+ let stopped = false;
1142
+ let bodyController;
1143
+ const stop = (reason, errorBody) => {
1144
+ if (stopped)
1145
+ return;
1146
+ stopped = true;
1147
+ release();
1148
+ signal.removeEventListener("abort", onAbort);
1149
+ if (errorBody)
1150
+ bodyController.error(reason);
1151
+ reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
1152
+ };
1153
+ const onAbort = () => stop(signal.reason, true);
1154
+ const body = new ReadableStream({
1155
+ start(controller) {
1156
+ bodyController = controller;
1157
+ signal.addEventListener("abort", onAbort, { once: true });
1158
+ if (signal.aborted)
1159
+ onAbort();
1160
+ },
1161
+ async pull(controller) {
1162
+ try {
1163
+ const result = await untilAborted(reader.read(), signal);
1164
+ if (stopped)
1165
+ return;
1166
+ if (result.done) {
1167
+ stopped = true;
1168
+ release();
1169
+ signal.removeEventListener("abort", onAbort);
1170
+ reader.releaseLock();
1171
+ controller.close();
1172
+ } else {
1173
+ controller.enqueue(result.value);
1174
+ }
1175
+ } catch (error) {
1176
+ stop(error, true);
1177
+ }
1178
+ },
1179
+ cancel(reason) {
1180
+ stop(reason, false);
1181
+ }
1182
+ }, { highWaterMark: 0 });
1183
+ return preserveResponseMetadata(new Response(body, {
1184
+ status: response.status,
1185
+ statusText: response.statusText,
1186
+ headers: response.headers
1187
+ }), response);
1188
+ }
1080
1189
  async function safeFetch(input, init = {}, options = {}) {
1190
+ init.signal?.throwIfAborted();
1081
1191
  const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
1192
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1193
+ throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
1194
+ }
1082
1195
  const maxRedirects = options.maxRedirects ?? 0;
1083
- const resolveDns = options.resolveDns ?? true;
1084
1196
  const urlOptions = {
1085
1197
  allowHttp: options.allowHttp,
1086
- resolveDns,
1198
+ resolveDns: options.resolveDns ?? true,
1087
1199
  allowPrivate: options.allowPrivate
1088
1200
  };
1089
1201
  const controller = new AbortController;
1090
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
1202
+ const forwardAbort = () => controller.abort(init.signal?.reason);
1203
+ init.signal?.addEventListener("abort", forwardAbort, { once: true });
1204
+ const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
1205
+ const release = () => {
1206
+ clearTimeout(timeout);
1207
+ init.signal?.removeEventListener("abort", forwardAbort);
1208
+ };
1091
1209
  try {
1092
- let current = await resolveSafeOutboundTarget(input, urlOptions);
1210
+ let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
1093
1211
  let redirectCount = 0;
1094
1212
  while (true) {
1213
+ controller.signal.throwIfAborted();
1095
1214
  const address = current.addresses[0];
1096
1215
  const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
1097
1216
  const headers = new Headers(init.headers);
1098
- if (address && !headers.has("host")) {
1217
+ if (address && !headers.has("host"))
1099
1218
  headers.set("Host", current.url.host);
1100
- }
1101
1219
  const fetchInit = {
1102
1220
  ...init,
1103
1221
  headers,
1104
1222
  signal: controller.signal,
1105
1223
  redirect: "manual"
1106
1224
  };
1107
- if (address) {
1225
+ if (address)
1108
1226
  fetchInit.tls = { serverName: current.url.hostname };
1109
- }
1110
- const response = await fetch(fetchUrl, fetchInit);
1227
+ const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
1111
1228
  if (response.status >= 300 && response.status < 400) {
1112
1229
  const location = response.headers.get("location");
1113
- if (!location || redirectCount >= maxRedirects) {
1114
- return response;
1230
+ if (location && redirectCount < maxRedirects) {
1231
+ if (response.body)
1232
+ await untilAborted(response.body.cancel(), controller.signal);
1233
+ current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
1234
+ redirectCount++;
1235
+ continue;
1115
1236
  }
1116
- current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
1117
- redirectCount += 1;
1118
- continue;
1119
1237
  }
1120
- return response;
1238
+ return withBodyDeadline(response, controller.signal, release);
1121
1239
  }
1122
- } finally {
1123
- clearTimeout(timeout);
1240
+ } catch (error) {
1241
+ controller.abort(error);
1242
+ release();
1243
+ throw error;
1124
1244
  }
1125
1245
  }
1126
1246
 
@@ -168,7 +168,7 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
168
168
  "GET /scim/v2/Groups": "SCIM list groups (departments)"
169
169
  };
170
170
  function toOpenApiPath(path) {
171
- return path.replace(/:([A-Za-z_]+)/g, "{$1}");
171
+ return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
172
172
  }
173
173
  function toRelativeApiPath(path) {
174
174
  const prefix = apiPrefix();
@@ -196,6 +196,16 @@ function requiresBearerAuth(path, method) {
196
196
  }
197
197
  return relative.startsWith("/auth/") || relative.startsWith("/users/me") || path.startsWith("/users/me") || relative === "/user" || relative.startsWith("/integrations/") || relative.startsWith("/audit-logs") || ["POST", "PATCH", "PUT", "DELETE"].includes(method);
198
198
  }
199
+ var DEFAULT_RESPONSES = {
200
+ "200": { description: "OK" },
201
+ "201": { description: "Created" },
202
+ "204": { description: "No Content" },
203
+ "400": { description: "Bad Request" },
204
+ "401": { description: "Unauthorized" },
205
+ "403": { description: "Forbidden" },
206
+ "404": { description: "Not Found" },
207
+ "422": { description: "Validation Error" }
208
+ };
199
209
  function generateOpenApiSpec(routes) {
200
210
  const paths = {};
201
211
  for (const route of routes) {
@@ -206,16 +216,8 @@ function generateOpenApiSpec(routes) {
206
216
  paths[openApiPath][method] = {
207
217
  summary: description,
208
218
  ...requiresBearerAuth(route.path, route.method) ? { security: [{ bearerAuth: [] }] } : {},
209
- responses: {
210
- "200": { description: "OK" },
211
- "201": { description: "Created" },
212
- "204": { description: "No Content" },
213
- "400": { description: "Bad Request" },
214
- "401": { description: "Unauthorized" },
215
- "403": { description: "Forbidden" },
216
- "404": { description: "Not Found" },
217
- "422": { description: "Validation Error" }
218
- }
219
+ ...route.openApi,
220
+ responses: route.openApi?.responses ?? structuredClone(DEFAULT_RESPONSES)
219
221
  };
220
222
  }
221
223
  return {
@@ -297,10 +299,38 @@ function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
297
299
  " }",
298
300
  ""
299
301
  ];
302
+ const contracts = Object.fromEntries(Object.entries(spec.paths).map(([path, methods]) => [
303
+ path,
304
+ Object.fromEntries(Object.entries(methods).filter(([, value]) => {
305
+ const operation = value;
306
+ return operation.parameters || operation.requestBody || operation.operationId || JSON.stringify(operation.responses) !== JSON.stringify(DEFAULT_RESPONSES);
307
+ }))
308
+ ]).filter(([, methods]) => Object.keys(methods).length));
309
+ if (Object.keys(contracts).length)
310
+ lines.splice(1, 0, ` static readonly operationContracts = ${JSON.stringify(contracts)} as const;`);
300
311
  for (const [path, methods] of Object.entries(spec.paths)) {
301
312
  const requestPath = toRequestPath(path, prefix);
302
- for (const method of Object.keys(methods)) {
313
+ for (const [method, operation] of Object.entries(methods)) {
314
+ const metadata = operation;
315
+ const headers = metadata.parameters?.filter((parameter) => parameter.in === "header") ?? [];
303
316
  const functionName = toMethodName(method, path, prefix);
317
+ const pathParameters = metadata.parameters?.filter((parameter) => parameter.in === "path") ?? [];
318
+ const queryParameters = metadata.parameters?.filter((parameter) => parameter.in === "query") ?? [];
319
+ const groups = [
320
+ { field: "operationHeaders", parameters: headers },
321
+ { field: "operationPath", parameters: pathParameters },
322
+ { field: "operationQuery", parameters: queryParameters }
323
+ ].filter((group) => group.parameters.length);
324
+ if (groups.length) {
325
+ const type = groups.map((group) => `${group.field}${group.parameters.some((parameter) => parameter.required) ? "" : "?"}: { ${group.parameters.map((parameter) => `${JSON.stringify(parameter.name)}${parameter.required ? "" : "?"}: ${parameter.schema?.type === "integer" || parameter.schema?.type === "number" ? "number" : parameter.schema?.type === "boolean" ? "boolean" : "string"}`).join("; ")} }`).join("; ");
326
+ const required = groups.some((group) => group.parameters.some((parameter) => parameter.required));
327
+ lines.push(` async ${functionName}(init: RequestInit & { ${type} }${required ? "" : " = {}"}): Promise<Response> {`, " const headers = new Headers(init.headers);", ` let path = ${JSON.stringify(requestPath)};`, ...groups.flatMap((group) => group.parameters.filter((parameter) => parameter.required).map((parameter) => ` if (init.${group.field}?.[${JSON.stringify(parameter.name)}] === undefined) throw new TypeError(${JSON.stringify(`Required operation ${group.field === "operationHeaders" ? "header" : group.field === "operationPath" ? "path parameter" : "query parameter"}: ${parameter.name}`)});`)), ...headers.map((parameter) => ` if (init.operationHeaders?.[${JSON.stringify(parameter.name)}] !== undefined) headers.set(${JSON.stringify(parameter.name)}, String(init.operationHeaders[${JSON.stringify(parameter.name)}]));`), ...pathParameters.map((parameter) => ` if (init.operationPath?.[${JSON.stringify(parameter.name)}] !== undefined) path = path.replaceAll(${JSON.stringify(`{${parameter.name}}`)}, encodeURIComponent(String(init.operationPath[${JSON.stringify(parameter.name)}])));`), ...queryParameters.length ? [
328
+ " const query = new URLSearchParams();",
329
+ ...queryParameters.map((parameter) => ` if (init.operationQuery?.[${JSON.stringify(parameter.name)}] !== undefined) query.set(${JSON.stringify(parameter.name)}, String(init.operationQuery[${JSON.stringify(parameter.name)}]));`),
330
+ " if (query.size) path += '?' + query.toString();"
331
+ ] : [], ` const { ${groups.map((group) => `${group.field}: _${group.field}`).join(", ")}, ...requestInit } = init;`, ` return await this.request(path, { ...requestInit, headers, method: ${JSON.stringify(method.toUpperCase())} });`, " }", "");
332
+ continue;
333
+ }
304
334
  lines.push(` async ${functionName}(init: RequestInit = {}): Promise<Response> {`, ` return await this.request("${requestPath}", { ...init, method: "${method.toUpperCase()}" });`, " }", "");
305
335
  }
306
336
  }
@@ -13,6 +13,16 @@ function validateOpenApiSpec(spec) {
13
13
  errors.push(`Path must start with '/': ${path}`);
14
14
  }
15
15
  for (const [method, operation] of Object.entries(methods)) {
16
+ const details = operation;
17
+ const seen = new Set;
18
+ for (const parameter of details.parameters ?? []) {
19
+ const identity = `${parameter.in}:${parameter.in === "header" ? parameter.name.toLowerCase() : parameter.name}`;
20
+ if (seen.has(identity))
21
+ errors.push(`Duplicate parameter ${parameter.name} on ${method.toUpperCase()} ${path}.`);
22
+ seen.add(identity);
23
+ if (parameter.in === "path" && (!parameter.required || !path.includes(`{${parameter.name}}`)))
24
+ errors.push(`Path parameter ${parameter.name} must be required and match ${path}.`);
25
+ }
16
26
  if (!("responses" in operation)) {
17
27
  errors.push(`Operation ${method.toUpperCase()} ${path} is missing responses.`);
18
28
  }
@@ -261,50 +261,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
261
261
 
262
262
  // ../../src/core/security/safeFetch.ts
263
263
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
264
+ function untilAborted(work, signal) {
265
+ signal.throwIfAborted();
266
+ return new Promise((resolve, reject) => {
267
+ const onAbort = () => reject(signal.reason);
268
+ signal.addEventListener("abort", onAbort, { once: true });
269
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
270
+ });
271
+ }
272
+ function preserveResponseMetadata(response, original) {
273
+ Object.defineProperties(response, {
274
+ url: { value: original.url },
275
+ redirected: { value: original.redirected },
276
+ type: { value: original.type }
277
+ });
278
+ const clone = response.clone.bind(response);
279
+ response.clone = () => preserveResponseMetadata(clone(), original);
280
+ return response;
281
+ }
282
+ function withBodyDeadline(response, signal, release) {
283
+ if (!response.body) {
284
+ release();
285
+ return response;
286
+ }
287
+ const reader = response.body.getReader();
288
+ let stopped = false;
289
+ let bodyController;
290
+ const stop = (reason, errorBody) => {
291
+ if (stopped)
292
+ return;
293
+ stopped = true;
294
+ release();
295
+ signal.removeEventListener("abort", onAbort);
296
+ if (errorBody)
297
+ bodyController.error(reason);
298
+ reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
299
+ };
300
+ const onAbort = () => stop(signal.reason, true);
301
+ const body = new ReadableStream({
302
+ start(controller) {
303
+ bodyController = controller;
304
+ signal.addEventListener("abort", onAbort, { once: true });
305
+ if (signal.aborted)
306
+ onAbort();
307
+ },
308
+ async pull(controller) {
309
+ try {
310
+ const result = await untilAborted(reader.read(), signal);
311
+ if (stopped)
312
+ return;
313
+ if (result.done) {
314
+ stopped = true;
315
+ release();
316
+ signal.removeEventListener("abort", onAbort);
317
+ reader.releaseLock();
318
+ controller.close();
319
+ } else {
320
+ controller.enqueue(result.value);
321
+ }
322
+ } catch (error) {
323
+ stop(error, true);
324
+ }
325
+ },
326
+ cancel(reason) {
327
+ stop(reason, false);
328
+ }
329
+ }, { highWaterMark: 0 });
330
+ return preserveResponseMetadata(new Response(body, {
331
+ status: response.status,
332
+ statusText: response.statusText,
333
+ headers: response.headers
334
+ }), response);
335
+ }
264
336
  async function safeFetch(input, init = {}, options = {}) {
337
+ init.signal?.throwIfAborted();
265
338
  const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
339
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
340
+ throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
341
+ }
266
342
  const maxRedirects = options.maxRedirects ?? 0;
267
- const resolveDns = options.resolveDns ?? true;
268
343
  const urlOptions = {
269
344
  allowHttp: options.allowHttp,
270
- resolveDns,
345
+ resolveDns: options.resolveDns ?? true,
271
346
  allowPrivate: options.allowPrivate
272
347
  };
273
348
  const controller = new AbortController;
274
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
349
+ const forwardAbort = () => controller.abort(init.signal?.reason);
350
+ init.signal?.addEventListener("abort", forwardAbort, { once: true });
351
+ const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
352
+ const release = () => {
353
+ clearTimeout(timeout);
354
+ init.signal?.removeEventListener("abort", forwardAbort);
355
+ };
275
356
  try {
276
- let current = await resolveSafeOutboundTarget(input, urlOptions);
357
+ let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
277
358
  let redirectCount = 0;
278
359
  while (true) {
360
+ controller.signal.throwIfAborted();
279
361
  const address = current.addresses[0];
280
362
  const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
281
363
  const headers = new Headers(init.headers);
282
- if (address && !headers.has("host")) {
364
+ if (address && !headers.has("host"))
283
365
  headers.set("Host", current.url.host);
284
- }
285
366
  const fetchInit = {
286
367
  ...init,
287
368
  headers,
288
369
  signal: controller.signal,
289
370
  redirect: "manual"
290
371
  };
291
- if (address) {
372
+ if (address)
292
373
  fetchInit.tls = { serverName: current.url.hostname };
293
- }
294
- const response = await fetch(fetchUrl, fetchInit);
374
+ const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
295
375
  if (response.status >= 300 && response.status < 400) {
296
376
  const location = response.headers.get("location");
297
- if (!location || redirectCount >= maxRedirects) {
298
- return response;
377
+ if (location && redirectCount < maxRedirects) {
378
+ if (response.body)
379
+ await untilAborted(response.body.cancel(), controller.signal);
380
+ current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
381
+ redirectCount++;
382
+ continue;
299
383
  }
300
- current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
301
- redirectCount += 1;
302
- continue;
303
384
  }
304
- return response;
385
+ return withBodyDeadline(response, controller.signal, release);
305
386
  }
306
- } finally {
307
- clearTimeout(timeout);
387
+ } catch (error) {
388
+ controller.abort(error);
389
+ release();
390
+ throw error;
308
391
  }
309
392
  }
310
393
  export {
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
58
58
  // ../../src/core/events/eventBus.ts
59
59
  class EventBus {
60
60
  constructor() {}
61
+ transactionalListeners = new Map;
62
+ listenTransactional(event, listener) {
63
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
64
+ handlers.add(listener);
65
+ this.transactionalListeners.set(event, handlers);
66
+ return () => {
67
+ handlers.delete(listener);
68
+ if (!handlers.size)
69
+ this.transactionalListeners.delete(event);
70
+ };
71
+ }
72
+ hasTransactionalListeners(event) {
73
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
74
+ }
75
+ async dispatchTransactional(event, payload) {
76
+ for (const listener of this.transactionalListeners.get(event) ?? [])
77
+ await listener(payload);
78
+ }
61
79
  listeners = new Map;
62
80
  on(event, listener) {
63
81
  return this.listen(event, listener);
@@ -164,6 +182,7 @@ async function runWithTransactionScope(callback) {
164
182
  var TransactionRollback, transactionScopes;
165
183
  var init_transactionControl = __esm(() => {
166
184
  init_asyncContextStore();
185
+ init_connectionContext();
167
186
  TransactionRollback = class TransactionRollback extends Error {
168
187
  result;
169
188
  constructor(result) {
@@ -452,6 +471,12 @@ var init_dialect = __esm(() => {
452
471
  nowExpression() {
453
472
  return "NOW()";
454
473
  },
474
+ epochMillisecondsExpression() {
475
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
476
+ },
477
+ rowLockClause(skipLocked = false) {
478
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
479
+ },
455
480
  timestampValue(value) {
456
481
  return value.toISOString();
457
482
  },
@@ -490,6 +515,12 @@ var init_dialect = __esm(() => {
490
515
  nowExpression() {
491
516
  return "CURRENT_TIMESTAMP";
492
517
  },
518
+ epochMillisecondsExpression() {
519
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
520
+ },
521
+ rowLockClause(skipLocked = false) {
522
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
523
+ },
493
524
  timestampValue(value) {
494
525
  return value.toISOString().slice(0, 19).replace("T", " ");
495
526
  },
@@ -528,6 +559,12 @@ var init_dialect = __esm(() => {
528
559
  nowExpression() {
529
560
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
530
561
  },
562
+ epochMillisecondsExpression() {
563
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
564
+ },
565
+ rowLockClause() {
566
+ return "";
567
+ },
531
568
  timestampValue(value) {
532
569
  return value.toISOString();
533
570
  },
@@ -105,6 +105,7 @@ async function runWithTransactionScope(callback) {
105
105
  var TransactionRollback, transactionScopes;
106
106
  var init_transactionControl = __esm(() => {
107
107
  init_asyncContextStore();
108
+ init_connectionContext();
108
109
  TransactionRollback = class TransactionRollback extends Error {
109
110
  result;
110
111
  constructor(result) {
@@ -132,6 +133,24 @@ var init_tenantContext = __esm(() => {
132
133
  // ../../src/core/events/eventBus.ts
133
134
  class EventBus {
134
135
  constructor() {}
136
+ transactionalListeners = new Map;
137
+ listenTransactional(event, listener) {
138
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
139
+ handlers.add(listener);
140
+ this.transactionalListeners.set(event, handlers);
141
+ return () => {
142
+ handlers.delete(listener);
143
+ if (!handlers.size)
144
+ this.transactionalListeners.delete(event);
145
+ };
146
+ }
147
+ hasTransactionalListeners(event) {
148
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
149
+ }
150
+ async dispatchTransactional(event, payload) {
151
+ for (const listener of this.transactionalListeners.get(event) ?? [])
152
+ await listener(payload);
153
+ }
135
154
  listeners = new Map;
136
155
  on(event, listener) {
137
156
  return this.listen(event, listener);
@@ -386,6 +405,12 @@ var init_dialect = __esm(() => {
386
405
  nowExpression() {
387
406
  return "NOW()";
388
407
  },
408
+ epochMillisecondsExpression() {
409
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
410
+ },
411
+ rowLockClause(skipLocked = false) {
412
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
413
+ },
389
414
  timestampValue(value) {
390
415
  return value.toISOString();
391
416
  },
@@ -424,6 +449,12 @@ var init_dialect = __esm(() => {
424
449
  nowExpression() {
425
450
  return "CURRENT_TIMESTAMP";
426
451
  },
452
+ epochMillisecondsExpression() {
453
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
454
+ },
455
+ rowLockClause(skipLocked = false) {
456
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
457
+ },
427
458
  timestampValue(value) {
428
459
  return value.toISOString().slice(0, 19).replace("T", " ");
429
460
  },
@@ -462,6 +493,12 @@ var init_dialect = __esm(() => {
462
493
  nowExpression() {
463
494
  return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
464
495
  },
496
+ epochMillisecondsExpression() {
497
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
498
+ },
499
+ rowLockClause() {
500
+ return "";
501
+ },
465
502
  timestampValue(value) {
466
503
  return value.toISOString();
467
504
  },