@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.0.4
4
+
5
+ - Place cache rollout guidance beside outbox docs without overlapping API additions
6
+
7
+ ## 2.0.3
8
+
9
+ - Persist transactional events with leased tenant-aware SQL outbox workers
10
+
3
11
  ## 2.0.2
4
12
 
5
13
  - Adopt TypeScript 7.0.2 in framework and generated apps; preserve tested TypeScript 5.9 and 6.x consumer compatibility.
@@ -21,6 +21,8 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
21
21
  protected readonly table: TableDefinition<TEntity, PrimaryKey>;
22
22
  protected readonly connection: DatabaseConnection;
23
23
  constructor(table: TableDefinition<TEntity, PrimaryKey>, connection?: DatabaseConnection);
24
+ private assertTransactionalEvent;
25
+ private runModelMutation;
24
26
  count(options?: ExtendedQueryOptions<TEntity>): Promise<number>;
25
27
  findAll(options?: ExtendedQueryOptions<TEntity>): Promise<TEntity[]>;
26
28
  paginate(options: {
@@ -4,6 +4,10 @@ interface SqlDialect {
4
4
  placeholder(index: number): string;
5
5
  quoteIdentifier(identifier: string): string;
6
6
  nowExpression(): string;
7
+ /** Authoritative database wall clock for persisted lease/retry deadlines. */
8
+ epochMillisecondsExpression(): string;
9
+ /** Current reads/row locks; SQLite serializes writes at transaction admission. */
10
+ rowLockClause(skipLocked?: boolean): string;
7
11
  /** Render a Date as a literal this engine accepts for a timestamp column. */
8
12
  timestampValue(value: Date): string;
9
13
  returningClause(columns: string): string;
@@ -1,18 +1,28 @@
1
- import type { ActiveDatabaseHandle } from "./connectionContext.ts";
1
+ import { type ActiveDatabaseHandle } from "./connectionContext.ts";
2
2
  type MysqlExecutable = {
3
3
  execute: (sql: string, params?: unknown[]) => Promise<[unknown, unknown]>;
4
+ query?: (sql: string) => Promise<[unknown, unknown]>;
4
5
  end?: () => Promise<void>;
6
+ getConnection?: () => Promise<MysqlPoolConnection>;
5
7
  };
6
8
  type MysqlPool = MysqlExecutable & {
7
9
  end(): Promise<void>;
8
10
  on(event: "connection", listener: (connection: unknown) => void): void;
9
11
  };
12
+ type MysqlPoolConnection = MysqlExecutable & {
13
+ beginTransaction(): Promise<void>;
14
+ commit(): Promise<void>;
15
+ rollback(): Promise<void>;
16
+ release(): void;
17
+ destroy(): void;
18
+ };
10
19
  type MysqlConnection = ActiveDatabaseHandle & {
20
+ begin<T>(callback: (transaction: ActiveDatabaseHandle) => Promise<T>): Promise<T>;
11
21
  close(): Promise<void>;
12
22
  };
13
23
  declare function resetMysqlLoaderForTests(importer?: () => Promise<unknown>): void;
14
24
  declare function createMysqlConnectionFromPool(pool: MysqlExecutable): MysqlConnection;
15
25
  declare function createMysqlPool(url: string): Promise<MysqlPool>;
16
26
  declare function createMysqlConnection(url: string): MysqlConnection;
17
- export type { MysqlConnection, MysqlExecutable, MysqlPool };
27
+ export type { MysqlConnection, MysqlExecutable, MysqlPool, MysqlPoolConnection };
18
28
  export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool, resetMysqlLoaderForTests, };
@@ -1,5 +1,6 @@
1
- import type { ActiveDatabaseHandle } from "./connectionContext.ts";
1
+ import { type ActiveDatabaseHandle } from "./connectionContext.ts";
2
2
  type SqliteConnection = ActiveDatabaseHandle & {
3
+ begin<T>(callback: (transaction: ActiveDatabaseHandle) => Promise<T>): Promise<T>;
3
4
  close(): void;
4
5
  };
5
6
  declare function createSqliteConnection(filename: string): SqliteConnection;
@@ -1,4 +1,4 @@
1
1
  import type { DatabaseConnection } from "./baseRepository.ts";
2
- import { requestTransactionRollback } from "./transactionControl.ts";
2
+ import { hasActiveTransaction, requestTransactionRollback } from "./transactionControl.ts";
3
3
  declare function runInTransaction<TValue>(operation: (connection: DatabaseConnection) => Promise<TValue>): Promise<TValue>;
4
- export { requestTransactionRollback, runInTransaction };
4
+ export { hasActiveTransaction, requestTransactionRollback, runInTransaction };
@@ -1,5 +1,6 @@
1
+ declare function hasActiveTransaction(): boolean;
1
2
  declare function requestTransactionRollback(): void;
2
3
  declare function commitOrRollbackScope<T>(result: T): Promise<T>;
3
4
  declare function settleTransaction<T>(work: () => Promise<T>): Promise<T>;
4
5
  declare function runWithTransactionScope<T>(callback: () => T | Promise<T>): Promise<T>;
5
- export { commitOrRollbackScope, requestTransactionRollback, runWithTransactionScope, settleTransaction, };
6
+ export { commitOrRollbackScope, hasActiveTransaction, requestTransactionRollback, runWithTransactionScope, settleTransaction, };
@@ -1,6 +1,10 @@
1
1
  type EventListener = (payload: unknown) => void | Promise<void>;
2
2
  declare class EventBus {
3
3
  constructor();
4
+ private readonly transactionalListeners;
5
+ listenTransactional(event: string, listener: EventListener): () => void;
6
+ hasTransactionalListeners(event: string): boolean;
7
+ dispatchTransactional(event: string, payload: unknown): Promise<void>;
4
8
  private readonly listeners;
5
9
  on(event: string, listener: EventListener): () => void;
6
10
  listen(event: string, listener: EventListener): () => void;
@@ -0,0 +1,55 @@
1
+ import { type TenantContext } from "../../tenant/tenantContext";
2
+ interface DurableEvent {
3
+ readonly id: string;
4
+ readonly name: string;
5
+ readonly version: number;
6
+ readonly tenantId: number | null;
7
+ readonly payload: unknown;
8
+ }
9
+ interface DurableListener {
10
+ readonly name: string;
11
+ readonly event: string;
12
+ handle(event: DurableEvent, context: {
13
+ signal: AbortSignal;
14
+ }): Promise<void>;
15
+ }
16
+ interface SqlOutboxOptions {
17
+ listeners: readonly DurableListener[];
18
+ leaseMs?: number;
19
+ maxAttempts?: number;
20
+ retryDelayMs?: number;
21
+ maxPayloadBytes?: number;
22
+ resolveTenant?: (id: number) => Promise<TenantContext | null>;
23
+ }
24
+ /** Opt-in SQL effects. Publishing writes only; workers execute listeners at least once. */
25
+ declare class SqlOutbox {
26
+ private readonly listeners;
27
+ private readonly leaseMs;
28
+ private readonly maxAttempts;
29
+ private readonly retryDelayMs;
30
+ private readonly maxPayloadBytes;
31
+ private readonly tenantResolver;
32
+ constructor(options: SqlOutboxOptions);
33
+ hasListener(name: string, event?: string): boolean;
34
+ publish(name: string, payload: unknown, options?: {
35
+ id?: string;
36
+ version?: number;
37
+ }): Promise<string>;
38
+ private persist;
39
+ private claim;
40
+ private renew;
41
+ private finish;
42
+ processNext(options?: {
43
+ signal?: AbortSignal;
44
+ }): Promise<boolean>;
45
+ /** Stop admission on cancellation; handlers must observe the supplied signal to drain. */
46
+ work(options: {
47
+ signal: AbortSignal;
48
+ pollMs?: number;
49
+ }): Promise<void>;
50
+ /** Operator operation: retains the event and atomically makes one failed delivery eligible. */
51
+ replay(eventId: string, listenerName: string): Promise<void>;
52
+ }
53
+ export { createOutboxMigration } from "./migration";
54
+ export type { DurableEvent, DurableListener, SqlOutboxOptions };
55
+ export { SqlOutbox };
@@ -0,0 +1,7 @@
1
+ import type { Migration } from "../../database/migrations/types";
2
+ import type { DatabaseDriver } from "../../database/schema/driver";
3
+ /** Infrastructure tables belong to core; apps choose the file migration name and RLS mode. */
4
+ declare function createOutboxMigration(name: string, driver: DatabaseDriver, options?: {
5
+ rls?: boolean;
6
+ }): Migration;
7
+ export { createOutboxMigration };
@@ -4,6 +4,8 @@ type RequestMeta = {
4
4
  request?: Request;
5
5
  /** Registered route template; never a URL supplied by the client. */
6
6
  routeTemplate?: string;
7
+ /** API error contracts take precedence over client content negotiation. */
8
+ errorFormat?: "json";
7
9
  flash?: {
8
10
  level: string;
9
11
  message: string;
@@ -6,4 +6,5 @@ declare function errorResponse(error: unknown): Response;
6
6
  declare function withErrorHandling<TArgs extends unknown[]>(handler: (...args: TArgs) => Response | Promise<Response>): (...args: TArgs) => Promise<Response>;
7
7
  declare function withJsonErrorHandling<TArgs extends unknown[]>(handler: (...args: TArgs) => Response | Promise<Response>): (...args: TArgs) => Promise<Response>;
8
8
  declare function createJsonErrorMiddleware(): Middleware;
9
- export { createdResponse, createJsonErrorMiddleware, errorResponse, jsonResponse, noContentResponse, withErrorHandling, withJsonErrorHandling, };
9
+ declare function createWebErrorMiddleware(): Middleware;
10
+ export { createdResponse, createJsonErrorMiddleware, createWebErrorMiddleware, errorResponse, jsonResponse, noContentResponse, withErrorHandling, withJsonErrorHandling, };
@@ -1,6 +1,45 @@
1
+ /** JSON Schema keywords remain extensible; metadata cannot contain executable values. */
2
+ type OpenApiJson = null | boolean | number | string | OpenApiJson[] | {
3
+ [key: string]: OpenApiJson | undefined;
4
+ };
5
+ type OpenApiSchema = boolean | {
6
+ [keyword: string]: OpenApiJson | undefined;
7
+ };
8
+ interface OpenApiParameter {
9
+ name: string;
10
+ in: "header" | "query" | "path" | "cookie";
11
+ required?: boolean;
12
+ description?: string;
13
+ schema: OpenApiSchema;
14
+ }
15
+ interface OpenApiMediaType {
16
+ schema?: OpenApiSchema;
17
+ example?: OpenApiJson;
18
+ }
19
+ interface OpenApiResponse {
20
+ description: string;
21
+ content?: Record<string, OpenApiMediaType>;
22
+ }
23
+ interface OpenApiOperation {
24
+ summary?: string;
25
+ description?: string;
26
+ operationId?: string;
27
+ tags?: string[];
28
+ deprecated?: boolean;
29
+ parameters?: OpenApiParameter[];
30
+ requestBody?: {
31
+ required?: boolean;
32
+ description?: string;
33
+ content: Record<string, OpenApiMediaType>;
34
+ };
35
+ responses?: Record<string, OpenApiResponse>;
36
+ security?: Array<Record<string, string[]>>;
37
+ }
38
+ type OpenApiRouteMap = Record<string, Partial<Record<"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS", OpenApiOperation>>>;
1
39
  interface RegisteredRoute {
2
40
  method: string;
3
41
  path: string;
4
42
  middleware: string[];
43
+ openApi?: OpenApiOperation;
5
44
  }
6
- export type { RegisteredRoute };
45
+ export type { OpenApiJson, OpenApiMediaType, OpenApiOperation, OpenApiParameter, OpenApiResponse, OpenApiRouteMap, OpenApiSchema, RegisteredRoute, };
@@ -1,5 +1,6 @@
1
1
  declare const DEFAULT_FETCH_TIMEOUT_MS = 10000;
2
2
  interface SafeFetchOptions {
3
+ /** One deadline covering DNS, redirects, headers, and response body consumption. */
3
4
  timeoutMs?: number;
4
5
  maxRedirects?: number;
5
6
  allowHttp?: boolean;
@@ -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
 
@@ -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
  },
@@ -264,50 +264,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
264
264
 
265
265
  // ../../src/core/security/safeFetch.ts
266
266
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
267
+ function untilAborted(work, signal) {
268
+ signal.throwIfAborted();
269
+ return new Promise((resolve, reject) => {
270
+ const onAbort = () => reject(signal.reason);
271
+ signal.addEventListener("abort", onAbort, { once: true });
272
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
273
+ });
274
+ }
275
+ function preserveResponseMetadata(response, original) {
276
+ Object.defineProperties(response, {
277
+ url: { value: original.url },
278
+ redirected: { value: original.redirected },
279
+ type: { value: original.type }
280
+ });
281
+ const clone = response.clone.bind(response);
282
+ response.clone = () => preserveResponseMetadata(clone(), original);
283
+ return response;
284
+ }
285
+ function withBodyDeadline(response, signal, release) {
286
+ if (!response.body) {
287
+ release();
288
+ return response;
289
+ }
290
+ const reader = response.body.getReader();
291
+ let stopped = false;
292
+ let bodyController;
293
+ const stop = (reason, errorBody) => {
294
+ if (stopped)
295
+ return;
296
+ stopped = true;
297
+ release();
298
+ signal.removeEventListener("abort", onAbort);
299
+ if (errorBody)
300
+ bodyController.error(reason);
301
+ reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
302
+ };
303
+ const onAbort = () => stop(signal.reason, true);
304
+ const body = new ReadableStream({
305
+ start(controller) {
306
+ bodyController = controller;
307
+ signal.addEventListener("abort", onAbort, { once: true });
308
+ if (signal.aborted)
309
+ onAbort();
310
+ },
311
+ async pull(controller) {
312
+ try {
313
+ const result = await untilAborted(reader.read(), signal);
314
+ if (stopped)
315
+ return;
316
+ if (result.done) {
317
+ stopped = true;
318
+ release();
319
+ signal.removeEventListener("abort", onAbort);
320
+ reader.releaseLock();
321
+ controller.close();
322
+ } else {
323
+ controller.enqueue(result.value);
324
+ }
325
+ } catch (error) {
326
+ stop(error, true);
327
+ }
328
+ },
329
+ cancel(reason) {
330
+ stop(reason, false);
331
+ }
332
+ }, { highWaterMark: 0 });
333
+ return preserveResponseMetadata(new Response(body, {
334
+ status: response.status,
335
+ statusText: response.statusText,
336
+ headers: response.headers
337
+ }), response);
338
+ }
267
339
  async function safeFetch(input, init = {}, options = {}) {
340
+ init.signal?.throwIfAborted();
268
341
  const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
342
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
343
+ throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
344
+ }
269
345
  const maxRedirects = options.maxRedirects ?? 0;
270
- const resolveDns = options.resolveDns ?? true;
271
346
  const urlOptions = {
272
347
  allowHttp: options.allowHttp,
273
- resolveDns,
348
+ resolveDns: options.resolveDns ?? true,
274
349
  allowPrivate: options.allowPrivate
275
350
  };
276
351
  const controller = new AbortController;
277
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
352
+ const forwardAbort = () => controller.abort(init.signal?.reason);
353
+ init.signal?.addEventListener("abort", forwardAbort, { once: true });
354
+ const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
355
+ const release = () => {
356
+ clearTimeout(timeout);
357
+ init.signal?.removeEventListener("abort", forwardAbort);
358
+ };
278
359
  try {
279
- let current = await resolveSafeOutboundTarget(input, urlOptions);
360
+ let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
280
361
  let redirectCount = 0;
281
362
  while (true) {
363
+ controller.signal.throwIfAborted();
282
364
  const address = current.addresses[0];
283
365
  const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
284
366
  const headers = new Headers(init.headers);
285
- if (address && !headers.has("host")) {
367
+ if (address && !headers.has("host"))
286
368
  headers.set("Host", current.url.host);
287
- }
288
369
  const fetchInit = {
289
370
  ...init,
290
371
  headers,
291
372
  signal: controller.signal,
292
373
  redirect: "manual"
293
374
  };
294
- if (address) {
375
+ if (address)
295
376
  fetchInit.tls = { serverName: current.url.hostname };
296
- }
297
- const response = await fetch(fetchUrl, fetchInit);
377
+ const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
298
378
  if (response.status >= 300 && response.status < 400) {
299
379
  const location = response.headers.get("location");
300
- if (!location || redirectCount >= maxRedirects) {
301
- return response;
380
+ if (location && redirectCount < maxRedirects) {
381
+ if (response.body)
382
+ await untilAborted(response.body.cancel(), controller.signal);
383
+ current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
384
+ redirectCount++;
385
+ continue;
302
386
  }
303
- current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
304
- redirectCount += 1;
305
- continue;
306
387
  }
307
- return response;
388
+ return withBodyDeadline(response, controller.signal, release);
308
389
  }
309
- } finally {
310
- clearTimeout(timeout);
390
+ } catch (error) {
391
+ controller.abort(error);
392
+ release();
393
+ throw error;
311
394
  }
312
395
  }
313
396