@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.
- package/CHANGELOG.md +4 -0
- package/dist/core/database/dialect.d.ts +4 -0
- package/dist/core/database/mysqlConnection.d.ts +12 -2
- package/dist/core/database/sqliteConnection.d.ts +2 -1
- package/dist/core/database/transaction.d.ts +2 -2
- package/dist/core/database/transactionControl.d.ts +2 -1
- package/dist/core/events/outbox/index.d.ts +54 -0
- package/dist/core/events/outbox/migration.d.ts +7 -0
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/response.d.ts +2 -1
- package/dist/core/security/safeFetch.d.ts +1 -0
- package/dist/entries/audit/exportAuditLogs.js +119 -17
- package/dist/entries/auth/basicAuthGuard.js +18 -0
- package/dist/entries/auth/oauth/oidcProvider.js +100 -17
- package/dist/entries/auth/oauth/providers.js +100 -17
- package/dist/entries/auth/oauth/samlProvider.js +18 -0
- package/dist/entries/auth/oneTimeToken.js +18 -0
- package/dist/entries/auth/passwordLogin.js +18 -0
- package/dist/entries/auth/saml/samlServiceProvider.js +18 -0
- package/dist/entries/auth/scimAuthMiddleware.js +19 -0
- package/dist/entries/database/migrations.js +18 -0
- package/dist/entries/database/sqliteConnection.js +101 -7
- package/dist/entries/events/outbox.js +1 -0
- package/dist/entries/http/response.js +39 -5
- package/dist/entries/jobs/exportAuditLogsJob.js +119 -17
- package/dist/entries/security/safeFetch.js +100 -17
- package/dist/entries/tenant/databaseTenantContext.js +19 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +19 -0
- package/dist/entries/tracing/tracingMiddleware.js +100 -17
- package/dist/framework/public-api.d.ts +3 -1
- package/dist/index.js +591 -50
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
|
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
|
|
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, };
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
publish(name: string, payload: unknown, options?: {
|
|
34
|
+
id?: string;
|
|
35
|
+
version?: number;
|
|
36
|
+
}): Promise<string>;
|
|
37
|
+
private persist;
|
|
38
|
+
private claim;
|
|
39
|
+
private renew;
|
|
40
|
+
private finish;
|
|
41
|
+
processNext(options?: {
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}): Promise<boolean>;
|
|
44
|
+
/** Stop admission on cancellation; handlers must observe the supplied signal to drain. */
|
|
45
|
+
work(options: {
|
|
46
|
+
signal: AbortSignal;
|
|
47
|
+
pollMs?: number;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
/** Operator operation: retains the event and atomically makes one failed delivery eligible. */
|
|
50
|
+
replay(eventId: string, listenerName: string): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
export { createOutboxMigration } from "./migration";
|
|
53
|
+
export type { DurableEvent, DurableListener, SqlOutboxOptions };
|
|
54
|
+
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
|
-
|
|
9
|
+
declare function createWebErrorMiddleware(): Middleware;
|
|
10
|
+
export { createdResponse, createJsonErrorMiddleware, createWebErrorMiddleware, errorResponse, jsonResponse, noContentResponse, withErrorHandling, withJsonErrorHandling, };
|
|
@@ -164,6 +164,7 @@ async function runWithTransactionScope(callback) {
|
|
|
164
164
|
var TransactionRollback, transactionScopes;
|
|
165
165
|
var init_transactionControl = __esm(() => {
|
|
166
166
|
init_asyncContextStore();
|
|
167
|
+
init_connectionContext();
|
|
167
168
|
TransactionRollback = class TransactionRollback extends Error {
|
|
168
169
|
result;
|
|
169
170
|
constructor(result) {
|
|
@@ -452,6 +453,12 @@ var init_dialect = __esm(() => {
|
|
|
452
453
|
nowExpression() {
|
|
453
454
|
return "NOW()";
|
|
454
455
|
},
|
|
456
|
+
epochMillisecondsExpression() {
|
|
457
|
+
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
458
|
+
},
|
|
459
|
+
rowLockClause(skipLocked = false) {
|
|
460
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
461
|
+
},
|
|
455
462
|
timestampValue(value) {
|
|
456
463
|
return value.toISOString();
|
|
457
464
|
},
|
|
@@ -490,6 +497,12 @@ var init_dialect = __esm(() => {
|
|
|
490
497
|
nowExpression() {
|
|
491
498
|
return "CURRENT_TIMESTAMP";
|
|
492
499
|
},
|
|
500
|
+
epochMillisecondsExpression() {
|
|
501
|
+
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
502
|
+
},
|
|
503
|
+
rowLockClause(skipLocked = false) {
|
|
504
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
505
|
+
},
|
|
493
506
|
timestampValue(value) {
|
|
494
507
|
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
495
508
|
},
|
|
@@ -528,6 +541,12 @@ var init_dialect = __esm(() => {
|
|
|
528
541
|
nowExpression() {
|
|
529
542
|
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
530
543
|
},
|
|
544
|
+
epochMillisecondsExpression() {
|
|
545
|
+
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
546
|
+
},
|
|
547
|
+
rowLockClause() {
|
|
548
|
+
return "";
|
|
549
|
+
},
|
|
531
550
|
timestampValue(value) {
|
|
532
551
|
return value.toISOString();
|
|
533
552
|
},
|
|
@@ -1077,50 +1096,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
|
|
|
1077
1096
|
|
|
1078
1097
|
// ../../src/core/security/safeFetch.ts
|
|
1079
1098
|
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
1099
|
+
function untilAborted(work, signal) {
|
|
1100
|
+
signal.throwIfAborted();
|
|
1101
|
+
return new Promise((resolve, reject) => {
|
|
1102
|
+
const onAbort = () => reject(signal.reason);
|
|
1103
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1104
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function preserveResponseMetadata(response, original) {
|
|
1108
|
+
Object.defineProperties(response, {
|
|
1109
|
+
url: { value: original.url },
|
|
1110
|
+
redirected: { value: original.redirected },
|
|
1111
|
+
type: { value: original.type }
|
|
1112
|
+
});
|
|
1113
|
+
const clone = response.clone.bind(response);
|
|
1114
|
+
response.clone = () => preserveResponseMetadata(clone(), original);
|
|
1115
|
+
return response;
|
|
1116
|
+
}
|
|
1117
|
+
function withBodyDeadline(response, signal, release) {
|
|
1118
|
+
if (!response.body) {
|
|
1119
|
+
release();
|
|
1120
|
+
return response;
|
|
1121
|
+
}
|
|
1122
|
+
const reader = response.body.getReader();
|
|
1123
|
+
let stopped = false;
|
|
1124
|
+
let bodyController;
|
|
1125
|
+
const stop = (reason, errorBody) => {
|
|
1126
|
+
if (stopped)
|
|
1127
|
+
return;
|
|
1128
|
+
stopped = true;
|
|
1129
|
+
release();
|
|
1130
|
+
signal.removeEventListener("abort", onAbort);
|
|
1131
|
+
if (errorBody)
|
|
1132
|
+
bodyController.error(reason);
|
|
1133
|
+
reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
|
|
1134
|
+
};
|
|
1135
|
+
const onAbort = () => stop(signal.reason, true);
|
|
1136
|
+
const body = new ReadableStream({
|
|
1137
|
+
start(controller) {
|
|
1138
|
+
bodyController = controller;
|
|
1139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1140
|
+
if (signal.aborted)
|
|
1141
|
+
onAbort();
|
|
1142
|
+
},
|
|
1143
|
+
async pull(controller) {
|
|
1144
|
+
try {
|
|
1145
|
+
const result = await untilAborted(reader.read(), signal);
|
|
1146
|
+
if (stopped)
|
|
1147
|
+
return;
|
|
1148
|
+
if (result.done) {
|
|
1149
|
+
stopped = true;
|
|
1150
|
+
release();
|
|
1151
|
+
signal.removeEventListener("abort", onAbort);
|
|
1152
|
+
reader.releaseLock();
|
|
1153
|
+
controller.close();
|
|
1154
|
+
} else {
|
|
1155
|
+
controller.enqueue(result.value);
|
|
1156
|
+
}
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
stop(error, true);
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
cancel(reason) {
|
|
1162
|
+
stop(reason, false);
|
|
1163
|
+
}
|
|
1164
|
+
}, { highWaterMark: 0 });
|
|
1165
|
+
return preserveResponseMetadata(new Response(body, {
|
|
1166
|
+
status: response.status,
|
|
1167
|
+
statusText: response.statusText,
|
|
1168
|
+
headers: response.headers
|
|
1169
|
+
}), response);
|
|
1170
|
+
}
|
|
1080
1171
|
async function safeFetch(input, init = {}, options = {}) {
|
|
1172
|
+
init.signal?.throwIfAborted();
|
|
1081
1173
|
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
1174
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1175
|
+
throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
|
|
1176
|
+
}
|
|
1082
1177
|
const maxRedirects = options.maxRedirects ?? 0;
|
|
1083
|
-
const resolveDns = options.resolveDns ?? true;
|
|
1084
1178
|
const urlOptions = {
|
|
1085
1179
|
allowHttp: options.allowHttp,
|
|
1086
|
-
resolveDns,
|
|
1180
|
+
resolveDns: options.resolveDns ?? true,
|
|
1087
1181
|
allowPrivate: options.allowPrivate
|
|
1088
1182
|
};
|
|
1089
1183
|
const controller = new AbortController;
|
|
1090
|
-
const
|
|
1184
|
+
const forwardAbort = () => controller.abort(init.signal?.reason);
|
|
1185
|
+
init.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1186
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
1187
|
+
const release = () => {
|
|
1188
|
+
clearTimeout(timeout);
|
|
1189
|
+
init.signal?.removeEventListener("abort", forwardAbort);
|
|
1190
|
+
};
|
|
1091
1191
|
try {
|
|
1092
|
-
let current = await resolveSafeOutboundTarget(input, urlOptions);
|
|
1192
|
+
let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
|
|
1093
1193
|
let redirectCount = 0;
|
|
1094
1194
|
while (true) {
|
|
1195
|
+
controller.signal.throwIfAborted();
|
|
1095
1196
|
const address = current.addresses[0];
|
|
1096
1197
|
const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
|
|
1097
1198
|
const headers = new Headers(init.headers);
|
|
1098
|
-
if (address && !headers.has("host"))
|
|
1199
|
+
if (address && !headers.has("host"))
|
|
1099
1200
|
headers.set("Host", current.url.host);
|
|
1100
|
-
}
|
|
1101
1201
|
const fetchInit = {
|
|
1102
1202
|
...init,
|
|
1103
1203
|
headers,
|
|
1104
1204
|
signal: controller.signal,
|
|
1105
1205
|
redirect: "manual"
|
|
1106
1206
|
};
|
|
1107
|
-
if (address)
|
|
1207
|
+
if (address)
|
|
1108
1208
|
fetchInit.tls = { serverName: current.url.hostname };
|
|
1109
|
-
|
|
1110
|
-
const response = await fetch(fetchUrl, fetchInit);
|
|
1209
|
+
const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
|
|
1111
1210
|
if (response.status >= 300 && response.status < 400) {
|
|
1112
1211
|
const location = response.headers.get("location");
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1212
|
+
if (location && redirectCount < maxRedirects) {
|
|
1213
|
+
if (response.body)
|
|
1214
|
+
await untilAborted(response.body.cancel(), controller.signal);
|
|
1215
|
+
current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
|
|
1216
|
+
redirectCount++;
|
|
1217
|
+
continue;
|
|
1115
1218
|
}
|
|
1116
|
-
current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
|
|
1117
|
-
redirectCount += 1;
|
|
1118
|
-
continue;
|
|
1119
1219
|
}
|
|
1120
|
-
return response;
|
|
1220
|
+
return withBodyDeadline(response, controller.signal, release);
|
|
1121
1221
|
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
controller.abort(error);
|
|
1224
|
+
release();
|
|
1225
|
+
throw error;
|
|
1124
1226
|
}
|
|
1125
1227
|
}
|
|
1126
1228
|
|
|
@@ -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
|
|
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 (
|
|
301
|
-
|
|
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
|
-
}
|
|
310
|
-
|
|
390
|
+
} catch (error) {
|
|
391
|
+
controller.abort(error);
|
|
392
|
+
release();
|
|
393
|
+
throw error;
|
|
311
394
|
}
|
|
312
395
|
}
|
|
313
396
|
|