@getstrata/core 1.1.9 → 2.0.1
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 +19 -0
- package/dist/core/database/transaction.d.ts +2 -1
- package/dist/core/database/transactionControl.d.ts +5 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
- package/dist/core/http/metricsMiddleware.d.ts +1 -2
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/throttleMiddleware.d.ts +11 -2
- package/dist/core/metrics/prometheus.d.ts +11 -1
- package/dist/core/queue/jobRunner.d.ts +3 -1
- package/dist/core/queue/redisQueue.d.ts +20 -3
- package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
- package/dist/entries/audit/exportAuditLogs.js +522 -62
- package/dist/entries/auth/basicAuthGuard.js +245 -219
- package/dist/entries/auth/oauth/samlProvider.js +185 -161
- package/dist/entries/auth/oneTimeToken.js +130 -127
- package/dist/entries/auth/passwordLogin.js +185 -180
- package/dist/entries/auth/saml/samlServiceProvider.js +185 -161
- package/dist/entries/auth/scimAuthMiddleware.js +525 -11
- package/dist/entries/database/migrations.js +132 -129
- package/dist/entries/database/schema.js +58 -39
- package/dist/entries/http/metricsMiddleware.js +124 -27
- package/dist/entries/http/response.js +107 -0
- package/dist/entries/http/routeMiddleware.js +40 -0
- package/dist/entries/http/throttleMiddleware.js +124 -11
- package/dist/entries/jobs/exportAuditLogsJob.js +522 -62
- package/dist/entries/metrics/prometheus.js +71 -11
- package/dist/entries/queue/createAppQueue.js +215 -21
- package/dist/entries/queue/jobRunner.js +3 -2
- package/dist/entries/queue/publicQueue.js +215 -21
- package/dist/entries/queue/queueMetrics.js +215 -21
- package/dist/entries/queue/redisQueue.js +223 -22
- package/dist/entries/tenant/databaseTenantContext.js +477 -16
- package/dist/entries/tenant/tenantDatabaseScope.js +523 -9
- package/dist/framework/public-api.d.ts +2 -2
- package/dist/index.js +954 -457
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,31 @@ function createAsyncContextStore(key) {
|
|
|
26
26
|
}
|
|
27
27
|
var init_asyncContextStore = () => {};
|
|
28
28
|
|
|
29
|
+
// ../../src/core/database/boundConnection.ts
|
|
30
|
+
function boundConnectionState() {
|
|
31
|
+
const globalRecord = globalThis;
|
|
32
|
+
const existing = globalRecord[BOUND_CONNECTION_KEY];
|
|
33
|
+
if (existing) {
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
36
|
+
const created = { connection: null };
|
|
37
|
+
globalRecord[BOUND_CONNECTION_KEY] = created;
|
|
38
|
+
return created;
|
|
39
|
+
}
|
|
40
|
+
function bindDatabaseConnection(connection) {
|
|
41
|
+
boundConnectionState().connection = connection;
|
|
42
|
+
}
|
|
43
|
+
function getBoundDatabaseConnection() {
|
|
44
|
+
return boundConnectionState().connection;
|
|
45
|
+
}
|
|
46
|
+
function resetBoundDatabaseConnection() {
|
|
47
|
+
boundConnectionState().connection = null;
|
|
48
|
+
}
|
|
49
|
+
var BOUND_CONNECTION_KEY;
|
|
50
|
+
var init_boundConnection = __esm(() => {
|
|
51
|
+
BOUND_CONNECTION_KEY = Symbol.for("@getstrata/boundDatabaseConnection");
|
|
52
|
+
});
|
|
53
|
+
|
|
29
54
|
// ../../src/core/database/connectionContext.ts
|
|
30
55
|
function runWithDatabaseConnection(connection, callback) {
|
|
31
56
|
return activeConnection.run(connection, callback);
|
|
@@ -111,6 +136,202 @@ var init_defaultConnection = __esm(() => {
|
|
|
111
136
|
DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
|
|
112
137
|
});
|
|
113
138
|
|
|
139
|
+
// ../../src/core/database/schema/driver.ts
|
|
140
|
+
function normalizeConnectionName(connection) {
|
|
141
|
+
const normalized = connection.trim().toLowerCase();
|
|
142
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
143
|
+
return "pgsql";
|
|
144
|
+
}
|
|
145
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
146
|
+
return "mysql";
|
|
147
|
+
}
|
|
148
|
+
if (normalized === "sqlite") {
|
|
149
|
+
return "sqlite";
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
152
|
+
}
|
|
153
|
+
function resolveDriverFromUrl(url) {
|
|
154
|
+
const normalized = url.trim().toLowerCase();
|
|
155
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
156
|
+
return "pgsql";
|
|
157
|
+
}
|
|
158
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
159
|
+
return "mysql";
|
|
160
|
+
}
|
|
161
|
+
if (normalized.startsWith("sqlite:")) {
|
|
162
|
+
return "sqlite";
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function resolveDatabaseDriver(options = {}) {
|
|
167
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
168
|
+
if (connection) {
|
|
169
|
+
return normalizeConnectionName(connection);
|
|
170
|
+
}
|
|
171
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
172
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
173
|
+
if (fromUrl) {
|
|
174
|
+
return fromUrl;
|
|
175
|
+
}
|
|
176
|
+
return "pgsql";
|
|
177
|
+
}
|
|
178
|
+
var init_driver = () => {};
|
|
179
|
+
|
|
180
|
+
// ../../src/core/database/dialect.ts
|
|
181
|
+
function assertSafeIdentifier(identifier) {
|
|
182
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
183
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
184
|
+
}
|
|
185
|
+
return identifier;
|
|
186
|
+
}
|
|
187
|
+
function quoteIdentifierFor(dialect, column) {
|
|
188
|
+
return dialect.quoteIdentifier(column);
|
|
189
|
+
}
|
|
190
|
+
function dialectFor(driver) {
|
|
191
|
+
return dialects[driver];
|
|
192
|
+
}
|
|
193
|
+
function currentSqlDialect() {
|
|
194
|
+
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
195
|
+
}
|
|
196
|
+
function useSqlDialect(driver) {
|
|
197
|
+
dialectOverride = dialectFor(driver);
|
|
198
|
+
return dialectOverride;
|
|
199
|
+
}
|
|
200
|
+
function sqlTimestamp(value = new Date) {
|
|
201
|
+
return currentSqlDialect().timestampValue(value);
|
|
202
|
+
}
|
|
203
|
+
function resetSqlDialect() {
|
|
204
|
+
dialectOverride = null;
|
|
205
|
+
}
|
|
206
|
+
function runWithSqlDialect(driver, callback) {
|
|
207
|
+
return dialectContext.run(dialectFor(driver), callback);
|
|
208
|
+
}
|
|
209
|
+
var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
|
|
210
|
+
var init_dialect = __esm(() => {
|
|
211
|
+
init_asyncContextStore();
|
|
212
|
+
init_driver();
|
|
213
|
+
postgresDialect = {
|
|
214
|
+
driver: "pgsql",
|
|
215
|
+
placeholder(index) {
|
|
216
|
+
return `$${index}`;
|
|
217
|
+
},
|
|
218
|
+
quoteIdentifier(identifier) {
|
|
219
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
220
|
+
},
|
|
221
|
+
nowExpression() {
|
|
222
|
+
return "NOW()";
|
|
223
|
+
},
|
|
224
|
+
timestampValue(value) {
|
|
225
|
+
return value.toISOString();
|
|
226
|
+
},
|
|
227
|
+
returningClause(columns) {
|
|
228
|
+
return ` RETURNING ${columns}`;
|
|
229
|
+
},
|
|
230
|
+
ilikeOperator() {
|
|
231
|
+
return "ILIKE";
|
|
232
|
+
},
|
|
233
|
+
nullsLastSuffix() {
|
|
234
|
+
return " NULLS LAST";
|
|
235
|
+
},
|
|
236
|
+
castToText(expression) {
|
|
237
|
+
return `${expression}::text`;
|
|
238
|
+
},
|
|
239
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
240
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
241
|
+
if (updateColumns.length === 0) {
|
|
242
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
243
|
+
}
|
|
244
|
+
const assignments = updateColumns.map((column) => {
|
|
245
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
246
|
+
return `${quoted} = excluded.${quoted}`;
|
|
247
|
+
}).join(", ");
|
|
248
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
mysqlDialect = {
|
|
252
|
+
driver: "mysql",
|
|
253
|
+
placeholder() {
|
|
254
|
+
return "?";
|
|
255
|
+
},
|
|
256
|
+
quoteIdentifier(identifier) {
|
|
257
|
+
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
258
|
+
},
|
|
259
|
+
nowExpression() {
|
|
260
|
+
return "CURRENT_TIMESTAMP";
|
|
261
|
+
},
|
|
262
|
+
timestampValue(value) {
|
|
263
|
+
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
264
|
+
},
|
|
265
|
+
returningClause() {
|
|
266
|
+
return "";
|
|
267
|
+
},
|
|
268
|
+
ilikeOperator() {
|
|
269
|
+
return "LIKE";
|
|
270
|
+
},
|
|
271
|
+
nullsLastSuffix() {
|
|
272
|
+
return "";
|
|
273
|
+
},
|
|
274
|
+
castToText(expression) {
|
|
275
|
+
return `CAST(${expression} AS CHAR)`;
|
|
276
|
+
},
|
|
277
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
278
|
+
if (updateColumns.length === 0) {
|
|
279
|
+
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
280
|
+
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
281
|
+
}
|
|
282
|
+
const assignments = updateColumns.map((column) => {
|
|
283
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
284
|
+
return `${quoted} = VALUES(${quoted})`;
|
|
285
|
+
}).join(", ");
|
|
286
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
sqliteDialect = {
|
|
290
|
+
driver: "sqlite",
|
|
291
|
+
placeholder() {
|
|
292
|
+
return "?";
|
|
293
|
+
},
|
|
294
|
+
quoteIdentifier(identifier) {
|
|
295
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
296
|
+
},
|
|
297
|
+
nowExpression() {
|
|
298
|
+
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
299
|
+
},
|
|
300
|
+
timestampValue(value) {
|
|
301
|
+
return value.toISOString();
|
|
302
|
+
},
|
|
303
|
+
returningClause(columns) {
|
|
304
|
+
return ` RETURNING ${columns}`;
|
|
305
|
+
},
|
|
306
|
+
ilikeOperator() {
|
|
307
|
+
return "LIKE";
|
|
308
|
+
},
|
|
309
|
+
nullsLastSuffix() {
|
|
310
|
+
return "";
|
|
311
|
+
},
|
|
312
|
+
castToText(expression) {
|
|
313
|
+
return `CAST(${expression} AS TEXT)`;
|
|
314
|
+
},
|
|
315
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
316
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
317
|
+
if (updateColumns.length === 0) {
|
|
318
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
319
|
+
}
|
|
320
|
+
const assignments = updateColumns.map((column) => {
|
|
321
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
322
|
+
return `${quoted} = excluded.${quoted}`;
|
|
323
|
+
}).join(", ");
|
|
324
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
dialects = {
|
|
328
|
+
pgsql: postgresDialect,
|
|
329
|
+
mysql: mysqlDialect,
|
|
330
|
+
sqlite: sqliteDialect
|
|
331
|
+
};
|
|
332
|
+
dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
333
|
+
});
|
|
334
|
+
|
|
114
335
|
// ../../src/core/tenant/tenantContext.ts
|
|
115
336
|
function runWithTenant(tenant, callback) {
|
|
116
337
|
return tenantContext.run(tenant, callback);
|
|
@@ -141,6 +362,58 @@ var init_tenantContext = __esm(() => {
|
|
|
141
362
|
tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
142
363
|
});
|
|
143
364
|
|
|
365
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
366
|
+
function resolveRepositoryConnection() {
|
|
367
|
+
return getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabaseQuery());
|
|
368
|
+
}
|
|
369
|
+
function compileTaggedSql(strings, values) {
|
|
370
|
+
const dialect = currentSqlDialect();
|
|
371
|
+
let query = strings[0] ?? "";
|
|
372
|
+
const params = [];
|
|
373
|
+
for (let index = 0;index < values.length; index += 1) {
|
|
374
|
+
params.push(values[index]);
|
|
375
|
+
query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
|
|
376
|
+
}
|
|
377
|
+
return { query, params };
|
|
378
|
+
}
|
|
379
|
+
function isTaggedStrings(value) {
|
|
380
|
+
if (!Array.isArray(value) || typeof value[0] !== "string") {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
return typeof value.raw !== "undefined";
|
|
384
|
+
}
|
|
385
|
+
function runOnConnection(connection, args) {
|
|
386
|
+
if (typeof connection === "function") {
|
|
387
|
+
return connection(...args);
|
|
388
|
+
}
|
|
389
|
+
if (typeof connection.unsafe !== "function") {
|
|
390
|
+
throw new Error("Database connection cannot run SQL.");
|
|
391
|
+
}
|
|
392
|
+
const strings = args[0];
|
|
393
|
+
if (!isTaggedStrings(strings)) {
|
|
394
|
+
throw new Error("Database connection cannot run tagged SQL.");
|
|
395
|
+
}
|
|
396
|
+
const compiled = compileTaggedSql(strings, args.slice(1));
|
|
397
|
+
return connection.unsafe(compiled.query, compiled.params);
|
|
398
|
+
}
|
|
399
|
+
var repositoryConnection;
|
|
400
|
+
var init_repositoryConnection = __esm(() => {
|
|
401
|
+
init_boundConnection();
|
|
402
|
+
init_connectionContext();
|
|
403
|
+
init_defaultConnection();
|
|
404
|
+
init_dialect();
|
|
405
|
+
repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
406
|
+
apply(_target, _thisArg, args) {
|
|
407
|
+
return runOnConnection(resolveRepositoryConnection(), args);
|
|
408
|
+
},
|
|
409
|
+
get(_target, property) {
|
|
410
|
+
const connection = resolveRepositoryConnection();
|
|
411
|
+
const value = connection[property];
|
|
412
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
144
417
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
145
418
|
function readTenancyDriver(env = process.env) {
|
|
146
419
|
const raw = env.TENANCY_DRIVER?.trim();
|
|
@@ -163,6 +436,80 @@ var init_tenancyConfig = __esm(() => {
|
|
|
163
436
|
TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
164
437
|
});
|
|
165
438
|
|
|
439
|
+
// ../../src/core/database/transactionControl.ts
|
|
440
|
+
function unwrapTransactionRollback(error, depth = 0) {
|
|
441
|
+
if (error instanceof TransactionRollback) {
|
|
442
|
+
return error;
|
|
443
|
+
}
|
|
444
|
+
if (depth >= 5 || !(error instanceof Error) || error.cause === undefined) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
return unwrapTransactionRollback(error.cause, depth + 1);
|
|
448
|
+
}
|
|
449
|
+
function requestTransactionRollback() {
|
|
450
|
+
const state = transactionScopes.getStore();
|
|
451
|
+
const index = state ? state.rollback.length - 1 : -1;
|
|
452
|
+
if (!state || index < 0) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const flags = state.rollback;
|
|
456
|
+
const current = flags[index];
|
|
457
|
+
if (current === undefined) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
flags[index] = true;
|
|
461
|
+
}
|
|
462
|
+
function scopeRequestsRollback() {
|
|
463
|
+
const state = transactionScopes.getStore();
|
|
464
|
+
if (!state || state.rollback.length === 0) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
return state.rollback[state.rollback.length - 1] === true;
|
|
468
|
+
}
|
|
469
|
+
async function commitOrRollbackScope(result) {
|
|
470
|
+
if (scopeRequestsRollback()) {
|
|
471
|
+
throw new TransactionRollback(result);
|
|
472
|
+
}
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
async function settleTransaction(work) {
|
|
476
|
+
try {
|
|
477
|
+
return await work();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
const rollback = unwrapTransactionRollback(error);
|
|
480
|
+
if (rollback) {
|
|
481
|
+
return rollback.result;
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function runWithTransactionScope(callback) {
|
|
487
|
+
const existing = transactionScopes.getStore();
|
|
488
|
+
if (existing) {
|
|
489
|
+
existing.rollback.push(false);
|
|
490
|
+
try {
|
|
491
|
+
return await callback();
|
|
492
|
+
} finally {
|
|
493
|
+
existing.rollback.pop();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const state = { rollback: [false] };
|
|
497
|
+
return await transactionScopes.run(state, callback);
|
|
498
|
+
}
|
|
499
|
+
var TransactionRollback, transactionScopes;
|
|
500
|
+
var init_transactionControl = __esm(() => {
|
|
501
|
+
init_asyncContextStore();
|
|
502
|
+
TransactionRollback = class TransactionRollback extends Error {
|
|
503
|
+
result;
|
|
504
|
+
constructor(result) {
|
|
505
|
+
super("Transaction rolled back");
|
|
506
|
+
this.name = "TransactionRollback";
|
|
507
|
+
this.result = result;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
|
|
511
|
+
});
|
|
512
|
+
|
|
166
513
|
// ../../src/core/events/eventBus.ts
|
|
167
514
|
class EventBus {
|
|
168
515
|
constructor() {}
|
|
@@ -294,33 +641,171 @@ var init_deferredModelEvents = __esm(() => {
|
|
|
294
641
|
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
295
642
|
});
|
|
296
643
|
|
|
644
|
+
// ../../src/core/database/connection.ts
|
|
645
|
+
function createDatabaseConnection(source) {
|
|
646
|
+
return {
|
|
647
|
+
async unsafe(query, params = []) {
|
|
648
|
+
return await source.unsafe(query, params);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
var init_connection = () => {};
|
|
653
|
+
|
|
654
|
+
// ../../src/core/database/transaction.ts
|
|
655
|
+
async function runInSavepoint(connection, operation) {
|
|
656
|
+
const inherited = transactionFrame.getStore();
|
|
657
|
+
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
658
|
+
if (!parent) {
|
|
659
|
+
parent = { connection, childActive: false };
|
|
660
|
+
outerFrames.set(connection, parent);
|
|
661
|
+
}
|
|
662
|
+
if (parent.childActive) {
|
|
663
|
+
throw new Error("Concurrent nested transactions on one connection are not supported. Await each transaction before starting the next.");
|
|
664
|
+
}
|
|
665
|
+
parent.childActive = true;
|
|
666
|
+
const name = `strata_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
667
|
+
try {
|
|
668
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
669
|
+
await connection.unsafe(`SAVEPOINT ${name}`);
|
|
670
|
+
try {
|
|
671
|
+
const result = await transactionFrame.run({ connection, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(connection)))));
|
|
672
|
+
await connection.unsafe(`RELEASE SAVEPOINT ${name}`);
|
|
673
|
+
return result;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
await connection.unsafe(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
676
|
+
await connection.unsafe(`RELEASE SAVEPOINT ${name}`);
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}));
|
|
680
|
+
} finally {
|
|
681
|
+
parent.childActive = false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function supportsTransactions(connection) {
|
|
685
|
+
return typeof connection.begin === "function";
|
|
686
|
+
}
|
|
687
|
+
async function runInTransaction(operation) {
|
|
688
|
+
const pool = resolveRepositoryConnection();
|
|
689
|
+
if (hasActiveDatabaseConnection()) {
|
|
690
|
+
return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
|
|
691
|
+
}
|
|
692
|
+
if (!supportsTransactions(pool)) {
|
|
693
|
+
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
694
|
+
}
|
|
695
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
696
|
+
return await pool.begin(async (transaction) => {
|
|
697
|
+
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
|
|
698
|
+
});
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
var transactionFrame, outerFrames;
|
|
702
|
+
var init_transaction = __esm(() => {
|
|
703
|
+
init_deferredModelEvents();
|
|
704
|
+
init_asyncContextStore();
|
|
705
|
+
init_connection();
|
|
706
|
+
init_connectionContext();
|
|
707
|
+
init_repositoryConnection();
|
|
708
|
+
init_transactionControl();
|
|
709
|
+
transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
|
|
710
|
+
outerFrames = new WeakMap;
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
714
|
+
function stringifyBypassIdentifier(identifier) {
|
|
715
|
+
if (typeof identifier === "number") {
|
|
716
|
+
return String(identifier);
|
|
717
|
+
}
|
|
718
|
+
return identifier.trim();
|
|
719
|
+
}
|
|
720
|
+
async function applyIdentifierToTransaction(transaction, identifier) {
|
|
721
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_identifier', $1, true)`, [
|
|
722
|
+
stringifyBypassIdentifier(identifier)
|
|
723
|
+
]);
|
|
724
|
+
}
|
|
725
|
+
function assertBypassIdentifier(identifier) {
|
|
726
|
+
if (typeof identifier === "number") {
|
|
727
|
+
if (!Number.isInteger(identifier) || identifier <= 0) {
|
|
728
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (typeof identifier === "string" && identifier.trim() !== "") {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
736
|
+
}
|
|
737
|
+
async function runWithScopedTenantTransaction(apply, callback) {
|
|
738
|
+
if (!isRlsTenancy()) {
|
|
739
|
+
return await callback();
|
|
740
|
+
}
|
|
741
|
+
if (hasActiveDatabaseConnection()) {
|
|
742
|
+
const connection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
743
|
+
return await runInTransaction(async () => {
|
|
744
|
+
const [previous] = await connection.unsafe(`SELECT current_setting('app.tenant_id', true) AS tenant_id,
|
|
745
|
+
current_setting('app.bypass_rls', true) AS bypass_rls,
|
|
746
|
+
current_setting('app.bypass_identifier', true) AS bypass_identifier`);
|
|
747
|
+
await apply(connection);
|
|
748
|
+
const result = await callback();
|
|
749
|
+
await connection.unsafe(`SELECT set_config('app.tenant_id', $1, true),
|
|
750
|
+
set_config('app.bypass_rls', $2, true), set_config('app.bypass_identifier', $3, true)`, [previous?.tenant_id ?? "", previous?.bypass_rls ?? "", previous?.bypass_identifier ?? ""]);
|
|
751
|
+
return result;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
const pool = getDefaultDatabasePool();
|
|
755
|
+
const begin = pool.begin;
|
|
756
|
+
if (typeof begin !== "function") {
|
|
757
|
+
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
758
|
+
}
|
|
759
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
760
|
+
return await begin.bind(pool)(async (transaction) => {
|
|
761
|
+
await apply(transaction);
|
|
762
|
+
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
763
|
+
});
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
async function runWithMigrationBypassForIdentifier(identifier, callback) {
|
|
767
|
+
assertBypassIdentifier(identifier);
|
|
768
|
+
return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
|
|
769
|
+
}
|
|
770
|
+
var init_databaseTenantContext = __esm(() => {
|
|
771
|
+
init_defaultConnection();
|
|
772
|
+
init_connectionContext();
|
|
773
|
+
init_transaction();
|
|
774
|
+
init_transactionControl();
|
|
775
|
+
init_deferredModelEvents();
|
|
776
|
+
init_tenancyConfig();
|
|
777
|
+
});
|
|
778
|
+
|
|
297
779
|
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
298
780
|
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
299
781
|
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
300
782
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
301
783
|
}
|
|
302
784
|
async function runWithTenantDatabase(tenant, callback) {
|
|
785
|
+
const outerTenant = currentTenant();
|
|
786
|
+
if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
|
|
787
|
+
throw new Error("Cannot switch tenants inside an active database transaction.");
|
|
788
|
+
}
|
|
303
789
|
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
304
790
|
return await runWithTenant(tenant, callback);
|
|
305
791
|
}
|
|
306
792
|
if (hasActiveDatabaseConnection()) {
|
|
307
793
|
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
308
|
-
await applyTenantContextToTransaction(activeConnection, tenant.id);
|
|
309
|
-
return await runWithTenant(tenant, callback);
|
|
794
|
+
return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
|
|
310
795
|
}
|
|
311
796
|
const pool = getDefaultDatabasePool();
|
|
312
797
|
const begin = pool.begin;
|
|
313
798
|
if (typeof begin !== "function") {
|
|
314
799
|
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
315
800
|
}
|
|
316
|
-
return await runWithDeferredModelEvents(async () => {
|
|
317
|
-
return await begin(async (transaction) => {
|
|
801
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
802
|
+
return await begin.bind(pool)(async (transaction) => {
|
|
318
803
|
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
319
804
|
return await runWithDatabaseConnection(transaction, async () => {
|
|
320
|
-
return await runWithTenant(tenant, callback);
|
|
805
|
+
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
321
806
|
});
|
|
322
807
|
});
|
|
323
|
-
});
|
|
808
|
+
}));
|
|
324
809
|
}
|
|
325
810
|
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
326
811
|
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
@@ -328,7 +813,9 @@ function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
|
328
813
|
var init_tenantDatabaseScope = __esm(() => {
|
|
329
814
|
init_defaultConnection();
|
|
330
815
|
init_connectionContext();
|
|
816
|
+
init_transactionControl();
|
|
331
817
|
init_deferredModelEvents();
|
|
818
|
+
init_databaseTenantContext();
|
|
332
819
|
init_tenancyConfig();
|
|
333
820
|
init_tenantContext();
|
|
334
821
|
});
|
|
@@ -545,29 +1032,8 @@ function resolveAuthUserDirectory(container) {
|
|
|
545
1032
|
return null;
|
|
546
1033
|
}
|
|
547
1034
|
|
|
548
|
-
// ../../src/core/database/boundConnection.ts
|
|
549
|
-
var BOUND_CONNECTION_KEY = Symbol.for("@getstrata/boundDatabaseConnection");
|
|
550
|
-
function boundConnectionState() {
|
|
551
|
-
const globalRecord = globalThis;
|
|
552
|
-
const existing = globalRecord[BOUND_CONNECTION_KEY];
|
|
553
|
-
if (existing) {
|
|
554
|
-
return existing;
|
|
555
|
-
}
|
|
556
|
-
const created = { connection: null };
|
|
557
|
-
globalRecord[BOUND_CONNECTION_KEY] = created;
|
|
558
|
-
return created;
|
|
559
|
-
}
|
|
560
|
-
function bindDatabaseConnection(connection) {
|
|
561
|
-
boundConnectionState().connection = connection;
|
|
562
|
-
}
|
|
563
|
-
function getBoundDatabaseConnection() {
|
|
564
|
-
return boundConnectionState().connection;
|
|
565
|
-
}
|
|
566
|
-
function resetBoundDatabaseConnection() {
|
|
567
|
-
boundConnectionState().connection = null;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
1035
|
// ../../src/core/auth/basicAuthGuard.ts
|
|
1036
|
+
init_boundConnection();
|
|
571
1037
|
init_defaultConnection();
|
|
572
1038
|
|
|
573
1039
|
// ../../src/core/http/statelessAuth.ts
|
|
@@ -683,199 +1149,8 @@ function revealMfaSecret(stored) {
|
|
|
683
1149
|
return decryptField(stored, key);
|
|
684
1150
|
}
|
|
685
1151
|
|
|
686
|
-
// ../../src/core/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
// ../../src/core/database/schema/driver.ts
|
|
690
|
-
function normalizeConnectionName(connection) {
|
|
691
|
-
const normalized = connection.trim().toLowerCase();
|
|
692
|
-
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
693
|
-
return "pgsql";
|
|
694
|
-
}
|
|
695
|
-
if (normalized === "mysql" || normalized === "mariadb") {
|
|
696
|
-
return "mysql";
|
|
697
|
-
}
|
|
698
|
-
if (normalized === "sqlite") {
|
|
699
|
-
return "sqlite";
|
|
700
|
-
}
|
|
701
|
-
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
702
|
-
}
|
|
703
|
-
function resolveDriverFromUrl(url) {
|
|
704
|
-
const normalized = url.trim().toLowerCase();
|
|
705
|
-
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
706
|
-
return "pgsql";
|
|
707
|
-
}
|
|
708
|
-
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
709
|
-
return "mysql";
|
|
710
|
-
}
|
|
711
|
-
if (normalized.startsWith("sqlite:")) {
|
|
712
|
-
return "sqlite";
|
|
713
|
-
}
|
|
714
|
-
return null;
|
|
715
|
-
}
|
|
716
|
-
function resolveDatabaseDriver(options = {}) {
|
|
717
|
-
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
718
|
-
if (connection) {
|
|
719
|
-
return normalizeConnectionName(connection);
|
|
720
|
-
}
|
|
721
|
-
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
722
|
-
const fromUrl = resolveDriverFromUrl(url);
|
|
723
|
-
if (fromUrl) {
|
|
724
|
-
return fromUrl;
|
|
725
|
-
}
|
|
726
|
-
return "pgsql";
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// ../../src/core/database/dialect.ts
|
|
730
|
-
function assertSafeIdentifier(identifier) {
|
|
731
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
732
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
733
|
-
}
|
|
734
|
-
return identifier;
|
|
735
|
-
}
|
|
736
|
-
function quoteIdentifierFor(dialect, column) {
|
|
737
|
-
return dialect.quoteIdentifier(column);
|
|
738
|
-
}
|
|
739
|
-
var postgresDialect = {
|
|
740
|
-
driver: "pgsql",
|
|
741
|
-
placeholder(index) {
|
|
742
|
-
return `$${index}`;
|
|
743
|
-
},
|
|
744
|
-
quoteIdentifier(identifier) {
|
|
745
|
-
return `"${assertSafeIdentifier(identifier)}"`;
|
|
746
|
-
},
|
|
747
|
-
nowExpression() {
|
|
748
|
-
return "NOW()";
|
|
749
|
-
},
|
|
750
|
-
timestampValue(value) {
|
|
751
|
-
return value.toISOString();
|
|
752
|
-
},
|
|
753
|
-
returningClause(columns) {
|
|
754
|
-
return ` RETURNING ${columns}`;
|
|
755
|
-
},
|
|
756
|
-
ilikeOperator() {
|
|
757
|
-
return "ILIKE";
|
|
758
|
-
},
|
|
759
|
-
nullsLastSuffix() {
|
|
760
|
-
return " NULLS LAST";
|
|
761
|
-
},
|
|
762
|
-
castToText(expression) {
|
|
763
|
-
return `${expression}::text`;
|
|
764
|
-
},
|
|
765
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
766
|
-
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
767
|
-
if (updateColumns.length === 0) {
|
|
768
|
-
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
769
|
-
}
|
|
770
|
-
const assignments = updateColumns.map((column) => {
|
|
771
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
772
|
-
return `${quoted} = excluded.${quoted}`;
|
|
773
|
-
}).join(", ");
|
|
774
|
-
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
var mysqlDialect = {
|
|
778
|
-
driver: "mysql",
|
|
779
|
-
placeholder() {
|
|
780
|
-
return "?";
|
|
781
|
-
},
|
|
782
|
-
quoteIdentifier(identifier) {
|
|
783
|
-
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
784
|
-
},
|
|
785
|
-
nowExpression() {
|
|
786
|
-
return "CURRENT_TIMESTAMP";
|
|
787
|
-
},
|
|
788
|
-
timestampValue(value) {
|
|
789
|
-
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
790
|
-
},
|
|
791
|
-
returningClause() {
|
|
792
|
-
return "";
|
|
793
|
-
},
|
|
794
|
-
ilikeOperator() {
|
|
795
|
-
return "LIKE";
|
|
796
|
-
},
|
|
797
|
-
nullsLastSuffix() {
|
|
798
|
-
return "";
|
|
799
|
-
},
|
|
800
|
-
castToText(expression) {
|
|
801
|
-
return `CAST(${expression} AS CHAR)`;
|
|
802
|
-
},
|
|
803
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
804
|
-
if (updateColumns.length === 0) {
|
|
805
|
-
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
806
|
-
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
807
|
-
}
|
|
808
|
-
const assignments = updateColumns.map((column) => {
|
|
809
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
810
|
-
return `${quoted} = VALUES(${quoted})`;
|
|
811
|
-
}).join(", ");
|
|
812
|
-
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
var sqliteDialect = {
|
|
816
|
-
driver: "sqlite",
|
|
817
|
-
placeholder() {
|
|
818
|
-
return "?";
|
|
819
|
-
},
|
|
820
|
-
quoteIdentifier(identifier) {
|
|
821
|
-
return `"${assertSafeIdentifier(identifier)}"`;
|
|
822
|
-
},
|
|
823
|
-
nowExpression() {
|
|
824
|
-
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
825
|
-
},
|
|
826
|
-
timestampValue(value) {
|
|
827
|
-
return value.toISOString();
|
|
828
|
-
},
|
|
829
|
-
returningClause(columns) {
|
|
830
|
-
return ` RETURNING ${columns}`;
|
|
831
|
-
},
|
|
832
|
-
ilikeOperator() {
|
|
833
|
-
return "LIKE";
|
|
834
|
-
},
|
|
835
|
-
nullsLastSuffix() {
|
|
836
|
-
return "";
|
|
837
|
-
},
|
|
838
|
-
castToText(expression) {
|
|
839
|
-
return `CAST(${expression} AS TEXT)`;
|
|
840
|
-
},
|
|
841
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
842
|
-
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
843
|
-
if (updateColumns.length === 0) {
|
|
844
|
-
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
845
|
-
}
|
|
846
|
-
const assignments = updateColumns.map((column) => {
|
|
847
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
848
|
-
return `${quoted} = excluded.${quoted}`;
|
|
849
|
-
}).join(", ");
|
|
850
|
-
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
var dialects = {
|
|
854
|
-
pgsql: postgresDialect,
|
|
855
|
-
mysql: mysqlDialect,
|
|
856
|
-
sqlite: sqliteDialect
|
|
857
|
-
};
|
|
858
|
-
var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
859
|
-
var dialectOverride = null;
|
|
860
|
-
function dialectFor(driver) {
|
|
861
|
-
return dialects[driver];
|
|
862
|
-
}
|
|
863
|
-
function currentSqlDialect() {
|
|
864
|
-
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
865
|
-
}
|
|
866
|
-
function useSqlDialect(driver) {
|
|
867
|
-
dialectOverride = dialectFor(driver);
|
|
868
|
-
return dialectOverride;
|
|
869
|
-
}
|
|
870
|
-
function sqlTimestamp(value = new Date) {
|
|
871
|
-
return currentSqlDialect().timestampValue(value);
|
|
872
|
-
}
|
|
873
|
-
function resetSqlDialect() {
|
|
874
|
-
dialectOverride = null;
|
|
875
|
-
}
|
|
876
|
-
function runWithSqlDialect(driver, callback) {
|
|
877
|
-
return dialectContext.run(dialectFor(driver), callback);
|
|
878
|
-
}
|
|
1152
|
+
// ../../src/core/auth/passwordLogin.ts
|
|
1153
|
+
init_dialect();
|
|
879
1154
|
|
|
880
1155
|
// ../../src/core/auth/tokenHash.ts
|
|
881
1156
|
import { createHmac as createHmac2 } from "crypto";
|
|
@@ -2393,7 +2668,9 @@ function missingOptionalPeer(packageName, reason, error) {
|
|
|
2393
2668
|
}
|
|
2394
2669
|
|
|
2395
2670
|
// ../../src/core/auth/saml/samlAssertionReplay.ts
|
|
2671
|
+
init_boundConnection();
|
|
2396
2672
|
init_defaultConnection();
|
|
2673
|
+
init_dialect();
|
|
2397
2674
|
|
|
2398
2675
|
// ../../src/core/database/errors.ts
|
|
2399
2676
|
function isPostgresError(error) {
|
|
@@ -2711,6 +2988,7 @@ class SamlProvider {
|
|
|
2711
2988
|
}
|
|
2712
2989
|
}
|
|
2713
2990
|
// ../../src/core/auth/oneTimeToken.ts
|
|
2991
|
+
init_dialect();
|
|
2714
2992
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2715
2993
|
var AUTH_ONE_TIME_PURPOSES = {
|
|
2716
2994
|
passwordReset: "password_reset",
|
|
@@ -2776,7 +3054,11 @@ class PolicyGate {
|
|
|
2776
3054
|
}
|
|
2777
3055
|
}
|
|
2778
3056
|
}
|
|
3057
|
+
// ../../src/core/database/bindConnection.ts
|
|
3058
|
+
init_boundConnection();
|
|
3059
|
+
|
|
2779
3060
|
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
3061
|
+
init_boundConnection();
|
|
2780
3062
|
init_defaultConnection();
|
|
2781
3063
|
init_connectionContext();
|
|
2782
3064
|
|
|
@@ -2795,78 +3077,33 @@ function parseScimTenantTokens(raw) {
|
|
|
2795
3077
|
const token = tokenPart.trim();
|
|
2796
3078
|
if (Number.isInteger(tenantId) && tenantId > 0 && token.length > 0) {
|
|
2797
3079
|
tokens.set(tenantId, token);
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
return tokens;
|
|
2801
|
-
}
|
|
2802
|
-
function resolveScimTenantFromToken(token) {
|
|
2803
|
-
const presented = token.trim();
|
|
2804
|
-
if (presented.length === 0) {
|
|
2805
|
-
return null;
|
|
2806
|
-
}
|
|
2807
|
-
const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
|
|
2808
|
-
for (const [tenantId, expectedToken] of tenantTokens) {
|
|
2809
|
-
if (timingSafeCompareString(presented, expectedToken)) {
|
|
2810
|
-
return tenantId;
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
const fallbackToken = process.env.SCIM_BEARER_TOKEN?.trim() ?? "";
|
|
2814
|
-
if (fallbackToken.length === 0) {
|
|
2815
|
-
return null;
|
|
2816
|
-
}
|
|
2817
|
-
if (timingSafeCompareString(presented, fallbackToken)) {
|
|
2818
|
-
return 1;
|
|
2819
|
-
}
|
|
2820
|
-
return null;
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
2824
|
-
init_defaultConnection();
|
|
2825
|
-
function resolveRepositoryConnection() {
|
|
2826
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
2827
|
-
}
|
|
2828
|
-
function compileTaggedSql(strings, values) {
|
|
2829
|
-
const dialect = currentSqlDialect();
|
|
2830
|
-
let query = strings[0] ?? "";
|
|
2831
|
-
const params = [];
|
|
2832
|
-
for (let index = 0;index < values.length; index += 1) {
|
|
2833
|
-
params.push(values[index]);
|
|
2834
|
-
query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
|
|
2835
|
-
}
|
|
2836
|
-
return { query, params };
|
|
2837
|
-
}
|
|
2838
|
-
function isTaggedStrings(value) {
|
|
2839
|
-
if (!Array.isArray(value) || typeof value[0] !== "string") {
|
|
2840
|
-
return false;
|
|
3080
|
+
}
|
|
2841
3081
|
}
|
|
2842
|
-
return
|
|
3082
|
+
return tokens;
|
|
2843
3083
|
}
|
|
2844
|
-
function
|
|
2845
|
-
|
|
2846
|
-
|
|
3084
|
+
function resolveScimTenantFromToken(token) {
|
|
3085
|
+
const presented = token.trim();
|
|
3086
|
+
if (presented.length === 0) {
|
|
3087
|
+
return null;
|
|
2847
3088
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
3089
|
+
const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
|
|
3090
|
+
for (const [tenantId, expectedToken] of tenantTokens) {
|
|
3091
|
+
if (timingSafeCompareString(presented, expectedToken)) {
|
|
3092
|
+
return tenantId;
|
|
3093
|
+
}
|
|
2850
3094
|
}
|
|
2851
|
-
const
|
|
2852
|
-
if (
|
|
2853
|
-
|
|
3095
|
+
const fallbackToken = process.env.SCIM_BEARER_TOKEN?.trim() ?? "";
|
|
3096
|
+
if (fallbackToken.length === 0) {
|
|
3097
|
+
return null;
|
|
2854
3098
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
}
|
|
2858
|
-
var repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
2859
|
-
apply(_target, _thisArg, args) {
|
|
2860
|
-
return runOnConnection(resolveRepositoryConnection(), args);
|
|
2861
|
-
},
|
|
2862
|
-
get(_target, property) {
|
|
2863
|
-
const connection = resolveRepositoryConnection();
|
|
2864
|
-
const value = connection[property];
|
|
2865
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
3099
|
+
if (timingSafeCompareString(presented, fallbackToken)) {
|
|
3100
|
+
return 1;
|
|
2866
3101
|
}
|
|
2867
|
-
|
|
3102
|
+
return null;
|
|
3103
|
+
}
|
|
2868
3104
|
|
|
2869
3105
|
// ../../src/core/tenant/resolveTenant.ts
|
|
3106
|
+
init_repositoryConnection();
|
|
2870
3107
|
init_tenancyConfig();
|
|
2871
3108
|
async function resolveTenant(tenantId) {
|
|
2872
3109
|
if (!isTenancyEnabled()) {
|
|
@@ -3479,6 +3716,7 @@ function buildPaginationMeta(input) {
|
|
|
3479
3716
|
}
|
|
3480
3717
|
|
|
3481
3718
|
// ../../src/core/database/query.ts
|
|
3719
|
+
init_dialect();
|
|
3482
3720
|
function quoteIdentifier(identifier) {
|
|
3483
3721
|
return currentSqlDialect().quoteIdentifier(identifier);
|
|
3484
3722
|
}
|
|
@@ -4233,6 +4471,9 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
4233
4471
|
return result;
|
|
4234
4472
|
}
|
|
4235
4473
|
|
|
4474
|
+
// ../../src/core/database/baseRepository.ts
|
|
4475
|
+
init_repositoryConnection();
|
|
4476
|
+
|
|
4236
4477
|
// ../../src/core/database/pluck.ts
|
|
4237
4478
|
function uniqueColumnSelect(table, columns) {
|
|
4238
4479
|
const seen = new Set;
|
|
@@ -5042,8 +5283,9 @@ class BaseRepository {
|
|
|
5042
5283
|
}
|
|
5043
5284
|
var baseRepository_default = BaseRepository;
|
|
5044
5285
|
// ../../src/core/database/bunSql.ts
|
|
5045
|
-
|
|
5286
|
+
init_boundConnection();
|
|
5046
5287
|
init_defaultConnection();
|
|
5288
|
+
var {SQL } = globalThis.Bun;
|
|
5047
5289
|
function createBunSqlPool(options) {
|
|
5048
5290
|
if (!options.url) {
|
|
5049
5291
|
throw new Error("DATABASE_URL is not configured. Set url before creating a Bun SQL pool.");
|
|
@@ -5061,18 +5303,12 @@ function bindBunSql(sql) {
|
|
|
5061
5303
|
registerDefaultDatabasePool(sql);
|
|
5062
5304
|
return sql;
|
|
5063
5305
|
}
|
|
5064
|
-
// ../../src/core/database/connection.ts
|
|
5065
|
-
function createDatabaseConnection(source) {
|
|
5066
|
-
return {
|
|
5067
|
-
async unsafe(query, params = []) {
|
|
5068
|
-
return await source.unsafe(query, params);
|
|
5069
|
-
}
|
|
5070
|
-
};
|
|
5071
|
-
}
|
|
5072
5306
|
|
|
5073
5307
|
// ../../src/framework/public-api.ts
|
|
5308
|
+
init_connection();
|
|
5074
5309
|
init_connectionContext();
|
|
5075
5310
|
init_defaultConnection();
|
|
5311
|
+
init_dialect();
|
|
5076
5312
|
|
|
5077
5313
|
// ../../src/core/database/inflection.ts
|
|
5078
5314
|
function singularize(word) {
|
|
@@ -5265,6 +5501,7 @@ async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
|
5265
5501
|
}
|
|
5266
5502
|
}
|
|
5267
5503
|
// ../../src/core/database/migrations/runner.ts
|
|
5504
|
+
init_dialect();
|
|
5268
5505
|
import { readdir } from "fs/promises";
|
|
5269
5506
|
import { join } from "path";
|
|
5270
5507
|
import { pathToFileURL } from "url";
|
|
@@ -7151,6 +7388,7 @@ function createMysqlConnection(url) {
|
|
|
7151
7388
|
}
|
|
7152
7389
|
// ../../src/core/database/namedConnections.ts
|
|
7153
7390
|
init_connectionContext();
|
|
7391
|
+
init_dialect();
|
|
7154
7392
|
var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
|
|
7155
7393
|
function registry() {
|
|
7156
7394
|
const globalRecord = globalThis;
|
|
@@ -7191,6 +7429,7 @@ function runOnNamedConnection(name, callback) {
|
|
|
7191
7429
|
|
|
7192
7430
|
// ../../src/framework/public-api.ts
|
|
7193
7431
|
init_queryProxy();
|
|
7432
|
+
init_repositoryConnection();
|
|
7194
7433
|
|
|
7195
7434
|
// ../../src/core/database/schema/columnDefinition.ts
|
|
7196
7435
|
class ColumnDefinition {
|
|
@@ -7413,6 +7652,10 @@ class Blueprint {
|
|
|
7413
7652
|
});
|
|
7414
7653
|
}
|
|
7415
7654
|
}
|
|
7655
|
+
|
|
7656
|
+
// ../../src/core/database/schema/index.ts
|
|
7657
|
+
init_driver();
|
|
7658
|
+
|
|
7416
7659
|
// ../../src/core/database/schema/errors.ts
|
|
7417
7660
|
class UnsupportedSchemaFeatureError extends Error {
|
|
7418
7661
|
constructor(feature, driver) {
|
|
@@ -7725,6 +7968,7 @@ function grammarForDriver(driver) {
|
|
|
7725
7968
|
}
|
|
7726
7969
|
}
|
|
7727
7970
|
// ../../src/core/database/schema/schema.ts
|
|
7971
|
+
init_driver();
|
|
7728
7972
|
class SchemaBuilder {
|
|
7729
7973
|
#driver;
|
|
7730
7974
|
#statements = [];
|
|
@@ -7840,25 +8084,9 @@ function createSqliteConnection(filename) {
|
|
|
7840
8084
|
function defineTable(definition) {
|
|
7841
8085
|
return definition;
|
|
7842
8086
|
}
|
|
7843
|
-
// ../../src/core/database/transaction.ts
|
|
7844
|
-
init_deferredModelEvents();
|
|
7845
|
-
function supportsTransactions(connection) {
|
|
7846
|
-
return typeof connection.begin === "function";
|
|
7847
|
-
}
|
|
7848
|
-
async function runInTransaction(operation) {
|
|
7849
|
-
const pool = resolveRepositoryConnection();
|
|
7850
|
-
if (!supportsTransactions(pool)) {
|
|
7851
|
-
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
7852
|
-
}
|
|
7853
|
-
const begin = pool.begin;
|
|
7854
|
-
return await runWithDeferredModelEvents(async () => {
|
|
7855
|
-
return await begin(async (transaction) => {
|
|
7856
|
-
return await operation(createDatabaseConnection(transaction));
|
|
7857
|
-
});
|
|
7858
|
-
});
|
|
7859
|
-
}
|
|
7860
8087
|
|
|
7861
8088
|
// ../../src/framework/public-api.ts
|
|
8089
|
+
init_transaction();
|
|
7862
8090
|
init_eventBus();
|
|
7863
8091
|
|
|
7864
8092
|
// ../../src/core/mail/mailer.ts
|
|
@@ -9202,7 +9430,8 @@ function wrapRouteHandler(handler, middleware) {
|
|
|
9202
9430
|
function applyMiddlewareToRoutes(routes, middleware) {
|
|
9203
9431
|
const wrapped = {};
|
|
9204
9432
|
for (const [path, routeHandler] of Object.entries(routes)) {
|
|
9205
|
-
|
|
9433
|
+
const identity = async (_request, next) => await runWithRequestMeta({ ...currentRequestMeta(), routeTemplate: path }, next);
|
|
9434
|
+
wrapped[path] = wrapRouteHandler(routeHandler, [identity, ...middleware]);
|
|
9206
9435
|
}
|
|
9207
9436
|
return wrapped;
|
|
9208
9437
|
}
|
|
@@ -9406,6 +9635,9 @@ function toPaginatedResourceCollection(items, meta, transformer) {
|
|
|
9406
9635
|
meta
|
|
9407
9636
|
};
|
|
9408
9637
|
}
|
|
9638
|
+
// ../../src/core/http/response.ts
|
|
9639
|
+
init_transactionControl();
|
|
9640
|
+
|
|
9409
9641
|
// ../../src/core/http/safeInternalPath.ts
|
|
9410
9642
|
var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
|
|
9411
9643
|
function looksLikeExternalTarget(value) {
|
|
@@ -9707,6 +9939,7 @@ function withErrorHandling(handler) {
|
|
|
9707
9939
|
try {
|
|
9708
9940
|
return await handler(...args);
|
|
9709
9941
|
} catch (error) {
|
|
9942
|
+
requestTransactionRollback();
|
|
9710
9943
|
const request = args.find((arg) => arg instanceof Request);
|
|
9711
9944
|
const webResponse = await webErrorResponse(error, request);
|
|
9712
9945
|
if (webResponse) {
|
|
@@ -9804,7 +10037,9 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
9804
10037
|
return response;
|
|
9805
10038
|
};
|
|
9806
10039
|
}
|
|
9807
|
-
// ../../src/core/http/
|
|
10040
|
+
// ../../src/core/http/throttleMiddleware.ts
|
|
10041
|
+
import { createHash as createHash3 } from "crypto";
|
|
10042
|
+
init_tenantContext();
|
|
9808
10043
|
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
9809
10044
|
|
|
9810
10045
|
// ../../src/core/http/throttleResponse.ts
|
|
@@ -9827,6 +10062,104 @@ async function tooManyRequestsResponse(request, message, decaySeconds) {
|
|
|
9827
10062
|
return new Response(html.body, { status: 429, headers });
|
|
9828
10063
|
}
|
|
9829
10064
|
|
|
10065
|
+
// ../../src/core/http/throttleMiddleware.ts
|
|
10066
|
+
function resolveThrottleIdentity(request) {
|
|
10067
|
+
const user = currentAuthUser();
|
|
10068
|
+
if (user?.tokenId !== undefined) {
|
|
10069
|
+
return `token:${user.tokenId}`;
|
|
10070
|
+
}
|
|
10071
|
+
if (user) {
|
|
10072
|
+
return `user:${user.id}`;
|
|
10073
|
+
}
|
|
10074
|
+
return readClientIp(request) ?? "unknown";
|
|
10075
|
+
}
|
|
10076
|
+
var CONSUME_THROTTLE = `
|
|
10077
|
+
local count = redis.call('INCR', KEYS[1])
|
|
10078
|
+
local ttl = redis.call('PTTL', KEYS[1])
|
|
10079
|
+
if ttl < 0 then
|
|
10080
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
10081
|
+
end
|
|
10082
|
+
return count
|
|
10083
|
+
`;
|
|
10084
|
+
function redisThrottleKey(request, prefix, identity) {
|
|
10085
|
+
const bucket = JSON.stringify([
|
|
10086
|
+
currentTenant()?.id ?? null,
|
|
10087
|
+
request.method,
|
|
10088
|
+
currentRequestMeta().routeTemplate ?? "__unmatched__",
|
|
10089
|
+
identity
|
|
10090
|
+
]);
|
|
10091
|
+
return namespacedRedisKey(`${prefix}:${createHash3("sha256").update(bucket).digest("hex")}`);
|
|
10092
|
+
}
|
|
10093
|
+
async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000) {
|
|
10094
|
+
if (!Number.isFinite(decaySeconds) || decaySeconds < 0 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
10095
|
+
throw new Error("Invalid throttle expiry or command timeout.");
|
|
10096
|
+
}
|
|
10097
|
+
let timer;
|
|
10098
|
+
try {
|
|
10099
|
+
const result = await Promise.race([
|
|
10100
|
+
client.send("EVAL", [
|
|
10101
|
+
CONSUME_THROTTLE,
|
|
10102
|
+
"1",
|
|
10103
|
+
key,
|
|
10104
|
+
String(Math.max(1, Math.ceil(decaySeconds * 1000)))
|
|
10105
|
+
]),
|
|
10106
|
+
new Promise((_resolve, reject) => {
|
|
10107
|
+
timer = setTimeout(() => reject(new Error("Throttle store deadline exceeded.")), timeoutMs);
|
|
10108
|
+
})
|
|
10109
|
+
]);
|
|
10110
|
+
const count = Number(result);
|
|
10111
|
+
if (!Number.isSafeInteger(count) || count < 1)
|
|
10112
|
+
throw new Error("Invalid throttle store response.");
|
|
10113
|
+
return count;
|
|
10114
|
+
} finally {
|
|
10115
|
+
clearTimeout(timer);
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
function createRedisThrottleConsumer(options) {
|
|
10119
|
+
let ownedClient;
|
|
10120
|
+
return async (key, decaySeconds) => {
|
|
10121
|
+
if (!options.redisClient && !ownedClient) {
|
|
10122
|
+
ownedClient = new RedisClient2(options.redisUrl, { autoReconnect: false, maxRetries: 0 });
|
|
10123
|
+
}
|
|
10124
|
+
const client = options.redisClient ?? ownedClient;
|
|
10125
|
+
if (!client)
|
|
10126
|
+
throw new Error("Throttle client is required.");
|
|
10127
|
+
try {
|
|
10128
|
+
return await consumeRedisThrottle(client, key, decaySeconds, options.commandTimeoutMs);
|
|
10129
|
+
} catch (error) {
|
|
10130
|
+
if (client === ownedClient) {
|
|
10131
|
+
ownedClient.close();
|
|
10132
|
+
ownedClient = undefined;
|
|
10133
|
+
}
|
|
10134
|
+
throw error;
|
|
10135
|
+
}
|
|
10136
|
+
};
|
|
10137
|
+
}
|
|
10138
|
+
function throttleUnavailableResponse() {
|
|
10139
|
+
return Response.json({ error: "Rate limiting is temporarily unavailable." }, {
|
|
10140
|
+
status: 503,
|
|
10141
|
+
headers: { "retry-after": "1" }
|
|
10142
|
+
});
|
|
10143
|
+
}
|
|
10144
|
+
function createThrottleMiddleware(options) {
|
|
10145
|
+
const consume = createRedisThrottleConsumer(options);
|
|
10146
|
+
const prefix = options.keyPrefix ?? "throttle";
|
|
10147
|
+
return async (request, next) => {
|
|
10148
|
+
const key = redisThrottleKey(request, prefix, resolveThrottleIdentity(request));
|
|
10149
|
+
let attempts;
|
|
10150
|
+
try {
|
|
10151
|
+
attempts = await consume(key, options.decaySeconds);
|
|
10152
|
+
} catch {
|
|
10153
|
+
return throttleUnavailableResponse();
|
|
10154
|
+
}
|
|
10155
|
+
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
10156
|
+
if (attempts > maxAttempts) {
|
|
10157
|
+
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
10158
|
+
}
|
|
10159
|
+
return await next();
|
|
10160
|
+
};
|
|
10161
|
+
}
|
|
10162
|
+
|
|
9830
10163
|
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
9831
10164
|
var memoryLoginBuckets = new Map;
|
|
9832
10165
|
function resolveLoginIdentity(request) {
|
|
@@ -9870,15 +10203,17 @@ function createMemoryLoginThrottleMiddleware(options) {
|
|
|
9870
10203
|
};
|
|
9871
10204
|
}
|
|
9872
10205
|
function createRedisLoginThrottleMiddleware(options) {
|
|
9873
|
-
const
|
|
9874
|
-
const prefix = options.keyPrefix ??
|
|
10206
|
+
const consume = createRedisThrottleConsumer(options);
|
|
10207
|
+
const prefix = options.keyPrefix ?? "login-throttle";
|
|
9875
10208
|
return async (request, next) => {
|
|
9876
10209
|
const identity = resolveLoginIdentity(request);
|
|
9877
10210
|
const email = await resolveLoginEmail(request);
|
|
9878
|
-
const throttleKey =
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
await
|
|
10211
|
+
const throttleKey = redisThrottleKey(request, prefix, JSON.stringify([identity, email]));
|
|
10212
|
+
let attempts;
|
|
10213
|
+
try {
|
|
10214
|
+
attempts = await consume(throttleKey, options.decaySeconds);
|
|
10215
|
+
} catch {
|
|
10216
|
+
return throttleUnavailableResponse();
|
|
9882
10217
|
}
|
|
9883
10218
|
if (attempts > options.maxAttempts) {
|
|
9884
10219
|
return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
|
|
@@ -9925,7 +10260,24 @@ function resetMemoryThrottleForTests() {
|
|
|
9925
10260
|
}
|
|
9926
10261
|
}
|
|
9927
10262
|
// ../../src/core/metrics/prometheus.ts
|
|
10263
|
+
var HTTP_DURATION_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 1e4];
|
|
10264
|
+
function emptyHistogram() {
|
|
10265
|
+
return {
|
|
10266
|
+
cumulative: HTTP_DURATION_BUCKETS_MS.map(() => 0),
|
|
10267
|
+
sum: 0,
|
|
10268
|
+
count: 0
|
|
10269
|
+
};
|
|
10270
|
+
}
|
|
10271
|
+
|
|
9928
10272
|
class PrometheusRegistry {
|
|
10273
|
+
maxSeries;
|
|
10274
|
+
seriesLabels = new Map;
|
|
10275
|
+
constructor(maxSeries = 4096) {
|
|
10276
|
+
this.maxSeries = maxSeries;
|
|
10277
|
+
if (!Number.isSafeInteger(maxSeries) || maxSeries < 1) {
|
|
10278
|
+
throw new Error("Metrics maxSeries must be a positive safe integer.");
|
|
10279
|
+
}
|
|
10280
|
+
}
|
|
9929
10281
|
httpRequestsTotal = new Map;
|
|
9930
10282
|
httpRequestDurationMs = new Map;
|
|
9931
10283
|
incrementHttpRequest(labels) {
|
|
@@ -9933,10 +10285,20 @@ class PrometheusRegistry {
|
|
|
9933
10285
|
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
9934
10286
|
}
|
|
9935
10287
|
observeHttpDuration(labels, durationMs) {
|
|
10288
|
+
if (!Number.isFinite(durationMs) || durationMs < 0)
|
|
10289
|
+
return;
|
|
9936
10290
|
const key = this.metricKey(labels);
|
|
9937
|
-
const
|
|
9938
|
-
|
|
9939
|
-
|
|
10291
|
+
const histogram = this.httpRequestDurationMs.get(key) ?? emptyHistogram();
|
|
10292
|
+
histogram.count += 1;
|
|
10293
|
+
histogram.sum += durationMs;
|
|
10294
|
+
for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
|
|
10295
|
+
const bound = HTTP_DURATION_BUCKETS_MS[index];
|
|
10296
|
+
if (bound !== undefined && durationMs <= bound) {
|
|
10297
|
+
const count = histogram.cumulative[index] ?? 0;
|
|
10298
|
+
histogram.cumulative[index] = count + 1;
|
|
10299
|
+
}
|
|
10300
|
+
}
|
|
10301
|
+
this.httpRequestDurationMs.set(key, histogram);
|
|
9940
10302
|
}
|
|
9941
10303
|
renderMetrics() {
|
|
9942
10304
|
const lines = [
|
|
@@ -9946,16 +10308,22 @@ class PrometheusRegistry {
|
|
|
9946
10308
|
for (const [key, value] of this.httpRequestsTotal) {
|
|
9947
10309
|
lines.push(`http_requests_total{${key}} ${value}`);
|
|
9948
10310
|
}
|
|
9949
|
-
lines.push("# HELP
|
|
9950
|
-
for (const [key,
|
|
9951
|
-
|
|
9952
|
-
|
|
10311
|
+
lines.push("# HELP http_request_duration_ms HTTP request duration in milliseconds.", "# TYPE http_request_duration_ms histogram");
|
|
10312
|
+
for (const [key, histogram] of this.httpRequestDurationMs) {
|
|
10313
|
+
for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
|
|
10314
|
+
const bound = HTTP_DURATION_BUCKETS_MS[index];
|
|
10315
|
+
lines.push(`http_request_duration_ms_bucket{${key},le="${bound}"} ${histogram.cumulative[index] ?? 0}`);
|
|
10316
|
+
}
|
|
10317
|
+
lines.push(`http_request_duration_ms_bucket{${key},le="+Inf"} ${histogram.count}`);
|
|
10318
|
+
lines.push(`http_request_duration_ms_sum{${key}} ${histogram.sum}`);
|
|
10319
|
+
lines.push(`http_request_duration_ms_count{${key}} ${histogram.count}`);
|
|
9953
10320
|
}
|
|
9954
10321
|
return `${lines.join(`
|
|
9955
10322
|
`)}
|
|
9956
10323
|
`;
|
|
9957
10324
|
}
|
|
9958
10325
|
resetForTests() {
|
|
10326
|
+
this.seriesLabels.clear();
|
|
9959
10327
|
this.httpRequestsTotal.clear();
|
|
9960
10328
|
this.httpRequestDurationMs.clear();
|
|
9961
10329
|
}
|
|
@@ -9965,9 +10333,10 @@ class PrometheusRegistry {
|
|
|
9965
10333
|
let totalRequests = 0;
|
|
9966
10334
|
for (const [key, count] of this.httpRequestsTotal) {
|
|
9967
10335
|
totalRequests += count;
|
|
9968
|
-
const
|
|
9969
|
-
|
|
9970
|
-
|
|
10336
|
+
const labels = this.seriesLabels.get(key);
|
|
10337
|
+
if (!labels)
|
|
10338
|
+
continue;
|
|
10339
|
+
const { method, path, status } = labels;
|
|
9971
10340
|
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
9972
10341
|
const pathKey = `${method} ${path}`;
|
|
9973
10342
|
const existing = pathCounts.get(pathKey);
|
|
@@ -9984,30 +10353,54 @@ class PrometheusRegistry {
|
|
|
9984
10353
|
topPaths
|
|
9985
10354
|
};
|
|
9986
10355
|
}
|
|
10356
|
+
getStorageStats() {
|
|
10357
|
+
return {
|
|
10358
|
+
series: this.seriesLabels.size,
|
|
10359
|
+
histograms: this.httpRequestDurationMs.size,
|
|
10360
|
+
bucketCounters: this.httpRequestDurationMs.size * HTTP_DURATION_BUCKETS_MS.length
|
|
10361
|
+
};
|
|
10362
|
+
}
|
|
9987
10363
|
metricKey(labels) {
|
|
9988
|
-
|
|
10364
|
+
const normalized = {
|
|
10365
|
+
method: /^(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)$/.test(labels.method) ? labels.method : "OTHER",
|
|
10366
|
+
path: labels.path.length <= 512 ? labels.path : "__overflow__",
|
|
10367
|
+
status: /^[1-5][0-9]{2}$/.test(labels.status) ? labels.status : "000"
|
|
10368
|
+
};
|
|
10369
|
+
const escapeLabel = (value) => value.replaceAll("\\", "\\\\").replaceAll(`
|
|
10370
|
+
`, "\\n").replaceAll('"', "\\\"");
|
|
10371
|
+
const encode = (value) => `method="${value.method}",path="${escapeLabel(value.path)}",status="${value.status}"`;
|
|
10372
|
+
let key = encode(normalized);
|
|
10373
|
+
if (!this.seriesLabels.has(key)) {
|
|
10374
|
+
if (this.seriesLabels.size >= this.maxSeries) {
|
|
10375
|
+
const overflow = { method: "OTHER", path: "__overflow__", status: "000" };
|
|
10376
|
+
key = encode(overflow);
|
|
10377
|
+
this.seriesLabels.set(key, overflow);
|
|
10378
|
+
} else {
|
|
10379
|
+
this.seriesLabels.set(key, normalized);
|
|
10380
|
+
}
|
|
10381
|
+
}
|
|
10382
|
+
return key;
|
|
9989
10383
|
}
|
|
9990
10384
|
}
|
|
9991
10385
|
var prometheusRegistry = new PrometheusRegistry;
|
|
9992
10386
|
|
|
9993
10387
|
// ../../src/core/http/metricsMiddleware.ts
|
|
9994
|
-
function normalizeMetricPath(pathname) {
|
|
9995
|
-
return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
|
|
9996
|
-
}
|
|
9997
10388
|
function createMetricsMiddleware() {
|
|
9998
10389
|
return async (request, next) => {
|
|
9999
10390
|
const startedAt = performance.now();
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10391
|
+
let response;
|
|
10392
|
+
try {
|
|
10393
|
+
response = await next();
|
|
10394
|
+
return response;
|
|
10395
|
+
} finally {
|
|
10396
|
+
const labels = {
|
|
10397
|
+
method: request.method,
|
|
10398
|
+
path: currentRequestMeta().routeTemplate ?? "__unmatched__",
|
|
10399
|
+
status: String(response?.status ?? 500)
|
|
10400
|
+
};
|
|
10401
|
+
prometheusRegistry.incrementHttpRequest(labels);
|
|
10402
|
+
prometheusRegistry.observeHttpDuration(labels, performance.now() - startedAt);
|
|
10403
|
+
}
|
|
10011
10404
|
};
|
|
10012
10405
|
}
|
|
10013
10406
|
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
@@ -10133,14 +10526,14 @@ function createRequireWebAuthMiddleware(auth) {
|
|
|
10133
10526
|
};
|
|
10134
10527
|
}
|
|
10135
10528
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
10136
|
-
import { createHash as
|
|
10529
|
+
import { createHash as createHash4 } from "crypto";
|
|
10137
10530
|
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
10138
10531
|
var memoryBuckets = new Map;
|
|
10139
10532
|
function resolveScimIdentity(request) {
|
|
10140
10533
|
const authorization = request.headers.get("authorization") ?? "";
|
|
10141
10534
|
const presented = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
|
|
10142
10535
|
if (presented.length > 0) {
|
|
10143
|
-
return
|
|
10536
|
+
return createHash4("sha256").update(presented).digest("hex");
|
|
10144
10537
|
}
|
|
10145
10538
|
return readClientIp(request) ?? "unknown";
|
|
10146
10539
|
}
|
|
@@ -10303,37 +10696,6 @@ function createValidateSignatureMiddleware() {
|
|
|
10303
10696
|
return await next();
|
|
10304
10697
|
};
|
|
10305
10698
|
}
|
|
10306
|
-
// ../../src/core/http/throttleMiddleware.ts
|
|
10307
|
-
init_tenantContext();
|
|
10308
|
-
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
10309
|
-
function resolveThrottleIdentity(request) {
|
|
10310
|
-
const user = currentAuthUser();
|
|
10311
|
-
if (user?.tokenId !== undefined) {
|
|
10312
|
-
return `token:${user.tokenId}`;
|
|
10313
|
-
}
|
|
10314
|
-
if (user) {
|
|
10315
|
-
return `user:${user.id}`;
|
|
10316
|
-
}
|
|
10317
|
-
return readClientIp(request) ?? "unknown";
|
|
10318
|
-
}
|
|
10319
|
-
function createThrottleMiddleware(options) {
|
|
10320
|
-
const client = new RedisClient4(options.redisUrl);
|
|
10321
|
-
const prefix = options.keyPrefix ?? namespacedRedisKey("throttle:");
|
|
10322
|
-
return async (request, next) => {
|
|
10323
|
-
const identity = resolveThrottleIdentity(request);
|
|
10324
|
-
const path = new URL(request.url).pathname;
|
|
10325
|
-
const throttleKey = `${prefix}${identity}:${path}`;
|
|
10326
|
-
const attempts = Number(await client.incr(throttleKey));
|
|
10327
|
-
if (attempts === 1) {
|
|
10328
|
-
await client.expire(throttleKey, options.decaySeconds);
|
|
10329
|
-
}
|
|
10330
|
-
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
10331
|
-
if (attempts > maxAttempts) {
|
|
10332
|
-
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
10333
|
-
}
|
|
10334
|
-
return await next();
|
|
10335
|
-
};
|
|
10336
|
-
}
|
|
10337
10699
|
// ../../src/core/http/parseFormBody.ts
|
|
10338
10700
|
async function parseFormBody(request) {
|
|
10339
10701
|
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -10790,7 +11152,7 @@ var queueConfig = {
|
|
|
10790
11152
|
};
|
|
10791
11153
|
|
|
10792
11154
|
// ../../src/core/queue/jobRunner.ts
|
|
10793
|
-
async function runQueueJob(envelope, failedJobs) {
|
|
11155
|
+
async function runQueueJob(envelope, failedJobs, options = {}) {
|
|
10794
11156
|
const job = jobRegistry.create(envelope.name);
|
|
10795
11157
|
if (!job) {
|
|
10796
11158
|
throw new Error(`Unknown job "${envelope.name}".`);
|
|
@@ -10807,7 +11169,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
10807
11169
|
await runQueueJob({
|
|
10808
11170
|
...envelope,
|
|
10809
11171
|
attempts: nextAttempt
|
|
10810
|
-
}, failedJobs);
|
|
11172
|
+
}, failedJobs, options);
|
|
10811
11173
|
return;
|
|
10812
11174
|
}
|
|
10813
11175
|
await failedJobs.recordFailure({
|
|
@@ -10815,12 +11177,13 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
10815
11177
|
payload: envelope.payload,
|
|
10816
11178
|
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
10817
11179
|
});
|
|
11180
|
+
options.onFailureRecorded?.();
|
|
10818
11181
|
throw error;
|
|
10819
11182
|
}
|
|
10820
11183
|
}
|
|
10821
11184
|
|
|
10822
11185
|
// ../../src/core/queue/redisQueue.ts
|
|
10823
|
-
var {RedisClient:
|
|
11186
|
+
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
10824
11187
|
function queueListKey() {
|
|
10825
11188
|
return namespacedRedisKey("queue:default");
|
|
10826
11189
|
}
|
|
@@ -10834,6 +11197,20 @@ var QUEUE_LIST_KEY = queueListKey();
|
|
|
10834
11197
|
var QUEUE_HIGH_KEY = queueHighKey();
|
|
10835
11198
|
var QUEUE_LOW_KEY = queueLowKey();
|
|
10836
11199
|
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
11200
|
+
var DEFAULT_VISIBILITY_MS = 60000;
|
|
11201
|
+
function queueProcessingKey(queueKey) {
|
|
11202
|
+
return `${queueKey}:processing`;
|
|
11203
|
+
}
|
|
11204
|
+
function queueProcessingLeaseKey(queueKey) {
|
|
11205
|
+
return `${queueKey}:processing:leases`;
|
|
11206
|
+
}
|
|
11207
|
+
function readVisibilityMs() {
|
|
11208
|
+
const parsed = Number(process.env.QUEUE_VISIBILITY_MS ?? String(DEFAULT_VISIBILITY_MS));
|
|
11209
|
+
if (!Number.isSafeInteger(parsed) || parsed < 30) {
|
|
11210
|
+
throw new Error("QUEUE_VISIBILITY_MS must be an integer of at least 30 milliseconds.");
|
|
11211
|
+
}
|
|
11212
|
+
return parsed;
|
|
11213
|
+
}
|
|
10837
11214
|
function queueKeyForPriority(priority = "default") {
|
|
10838
11215
|
switch (priority) {
|
|
10839
11216
|
case "high":
|
|
@@ -10875,11 +11252,134 @@ function parseQueueJobEnvelope(rawPayload) {
|
|
|
10875
11252
|
attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
|
|
10876
11253
|
};
|
|
10877
11254
|
}
|
|
11255
|
+
var SCRIPT_HEADER = `
|
|
11256
|
+
local function checkType(key, expected)
|
|
11257
|
+
local actual = redis.call('TYPE', key).ok
|
|
11258
|
+
if actual ~= 'none' and actual ~= expected then
|
|
11259
|
+
return redis.error_reply('Unexpected queue key type: ' .. key)
|
|
11260
|
+
end
|
|
11261
|
+
end
|
|
11262
|
+
local function nowMs()
|
|
11263
|
+
local time = redis.call('TIME')
|
|
11264
|
+
return tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
|
|
11265
|
+
end
|
|
11266
|
+
`;
|
|
11267
|
+
var RESERVE_JOB = `${SCRIPT_HEADER}
|
|
11268
|
+
local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'list') or checkType(KEYS[3], 'hash')
|
|
11269
|
+
if err then return err end
|
|
11270
|
+
local payload = redis.call('LINDEX', KEYS[1], -1)
|
|
11271
|
+
if not payload then return nil end
|
|
11272
|
+
local record = cjson.encode({strataReservation=1, id=ARGV[1], owner=ARGV[2], payload=payload})
|
|
11273
|
+
local lease = cjson.encode({owner=ARGV[2], expiresAt=nowMs()+tonumber(ARGV[3])})
|
|
11274
|
+
redis.call('LPUSH', KEYS[2], record)
|
|
11275
|
+
redis.call('HSET', KEYS[3], ARGV[1], lease)
|
|
11276
|
+
redis.call('RPOP', KEYS[1])
|
|
11277
|
+
return record
|
|
11278
|
+
`;
|
|
11279
|
+
var RENEW_OR_ACK = `${SCRIPT_HEADER}
|
|
11280
|
+
local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'hash') or checkType(KEYS[3], 'list')
|
|
11281
|
+
if err then return err end
|
|
11282
|
+
local raw = redis.call('HGET', KEYS[2], ARGV[1])
|
|
11283
|
+
if not raw then return 0 end
|
|
11284
|
+
local ok, lease = pcall(cjson.decode, raw)
|
|
11285
|
+
local now = nowMs()
|
|
11286
|
+
if not ok or type(lease) ~= 'table' or lease.owner ~= ARGV[2] or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now then
|
|
11287
|
+
return 0
|
|
11288
|
+
end
|
|
11289
|
+
if ARGV[4] == 'renew' then
|
|
11290
|
+
redis.call('HSET', KEYS[2], ARGV[1], cjson.encode({owner=ARGV[2], expiresAt=now+tonumber(ARGV[5])}))
|
|
11291
|
+
return 1
|
|
11292
|
+
end
|
|
11293
|
+
if ARGV[4] == 'quarantine' then redis.call('LPUSH', KEYS[3], ARGV[6]) end
|
|
11294
|
+
local removed = redis.call('LREM', KEYS[1], 1, ARGV[3])
|
|
11295
|
+
redis.call('HDEL', KEYS[2], ARGV[1])
|
|
11296
|
+
return removed
|
|
11297
|
+
`;
|
|
11298
|
+
var RECLAIM_JOBS = `${SCRIPT_HEADER}
|
|
11299
|
+
local err = checkType(KEYS[1], 'list') or checkType(KEYS[2], 'list') or checkType(KEYS[3], 'hash')
|
|
11300
|
+
if err then return err end
|
|
11301
|
+
local now = nowMs()
|
|
11302
|
+
local reclaimed = 0
|
|
11303
|
+
for _, record in ipairs(redis.call('LRANGE', KEYS[2], 0, -1)) do
|
|
11304
|
+
local payload, field = record, record
|
|
11305
|
+
local modern = false
|
|
11306
|
+
local ok, reservation = pcall(cjson.decode, record)
|
|
11307
|
+
if ok and type(reservation) == 'table' and reservation.strataReservation == 1 then
|
|
11308
|
+
payload, field, modern = reservation.payload, reservation.id, true
|
|
11309
|
+
end
|
|
11310
|
+
local raw = redis.call('HGET', KEYS[3], field)
|
|
11311
|
+
local expired = true
|
|
11312
|
+
if raw then
|
|
11313
|
+
if modern then
|
|
11314
|
+
local leaseOk, lease = pcall(cjson.decode, raw)
|
|
11315
|
+
expired = not leaseOk or type(lease) ~= 'table' or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now
|
|
11316
|
+
else
|
|
11317
|
+
local reservedAt = tonumber(raw)
|
|
11318
|
+
expired = not reservedAt or now - reservedAt >= tonumber(ARGV[1])
|
|
11319
|
+
end
|
|
11320
|
+
end
|
|
11321
|
+
if expired then
|
|
11322
|
+
redis.call('LPUSH', KEYS[1], payload)
|
|
11323
|
+
redis.call('LREM', KEYS[2], 1, record)
|
|
11324
|
+
redis.call('HDEL', KEYS[3], field)
|
|
11325
|
+
reclaimed = reclaimed + 1
|
|
11326
|
+
end
|
|
11327
|
+
end
|
|
11328
|
+
return reclaimed
|
|
11329
|
+
`;
|
|
11330
|
+
function queueInvalidKey(queueKey) {
|
|
11331
|
+
return `${queueKey}:invalid`;
|
|
11332
|
+
}
|
|
11333
|
+
async function reserveQueueJob(client, queueKey, owner) {
|
|
11334
|
+
const raw = await client.send("EVAL", [
|
|
11335
|
+
RESERVE_JOB,
|
|
11336
|
+
"3",
|
|
11337
|
+
queueKey,
|
|
11338
|
+
queueProcessingKey(queueKey),
|
|
11339
|
+
queueProcessingLeaseKey(queueKey),
|
|
11340
|
+
crypto.randomUUID(),
|
|
11341
|
+
owner,
|
|
11342
|
+
String(readVisibilityMs())
|
|
11343
|
+
]);
|
|
11344
|
+
if (typeof raw !== "string")
|
|
11345
|
+
return null;
|
|
11346
|
+
const reservation = JSON.parse(raw);
|
|
11347
|
+
return { ...reservation, record: raw };
|
|
11348
|
+
}
|
|
11349
|
+
async function updateQueueReservation(client, queueKey, reservation, action) {
|
|
11350
|
+
return Number(await client.send("EVAL", [
|
|
11351
|
+
RENEW_OR_ACK,
|
|
11352
|
+
"3",
|
|
11353
|
+
queueProcessingKey(queueKey),
|
|
11354
|
+
queueProcessingLeaseKey(queueKey),
|
|
11355
|
+
queueInvalidKey(queueKey),
|
|
11356
|
+
reservation.id,
|
|
11357
|
+
reservation.owner,
|
|
11358
|
+
reservation.record,
|
|
11359
|
+
action,
|
|
11360
|
+
String(readVisibilityMs()),
|
|
11361
|
+
reservation.payload
|
|
11362
|
+
])) === 1;
|
|
11363
|
+
}
|
|
11364
|
+
async function reclaimExpiredQueueReservations(client, queueKeys = QUEUE_KEYS) {
|
|
11365
|
+
let reclaimed = 0;
|
|
11366
|
+
for (const key of queueKeys) {
|
|
11367
|
+
reclaimed += Number(await client.send("EVAL", [
|
|
11368
|
+
RECLAIM_JOBS,
|
|
11369
|
+
"3",
|
|
11370
|
+
key,
|
|
11371
|
+
queueProcessingKey(key),
|
|
11372
|
+
queueProcessingLeaseKey(key),
|
|
11373
|
+
String(readVisibilityMs())
|
|
11374
|
+
]));
|
|
11375
|
+
}
|
|
11376
|
+
return reclaimed;
|
|
11377
|
+
}
|
|
10878
11378
|
|
|
10879
11379
|
class RedisQueue {
|
|
10880
11380
|
client;
|
|
10881
11381
|
constructor(redisUrl) {
|
|
10882
|
-
this.client = new
|
|
11382
|
+
this.client = new RedisClient4(redisUrl);
|
|
10883
11383
|
}
|
|
10884
11384
|
async dispatch(job, payload) {
|
|
10885
11385
|
const name = jobRegistry.resolveName(job);
|
|
@@ -10899,13 +11399,16 @@ class RedisQueue {
|
|
|
10899
11399
|
class QueueWorker {
|
|
10900
11400
|
failedJobs;
|
|
10901
11401
|
timeoutSeconds;
|
|
11402
|
+
queueKeys;
|
|
11403
|
+
owner = crypto.randomUUID();
|
|
10902
11404
|
running = false;
|
|
10903
11405
|
stopping = false;
|
|
10904
11406
|
client;
|
|
10905
|
-
constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
|
|
11407
|
+
constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
|
|
10906
11408
|
this.failedJobs = failedJobs;
|
|
10907
11409
|
this.timeoutSeconds = timeoutSeconds;
|
|
10908
|
-
this.
|
|
11410
|
+
this.queueKeys = queueKeys;
|
|
11411
|
+
this.client = new RedisClient4(redisUrl);
|
|
10909
11412
|
}
|
|
10910
11413
|
requestStop() {
|
|
10911
11414
|
this.stopping = true;
|
|
@@ -10913,38 +11416,81 @@ class QueueWorker {
|
|
|
10913
11416
|
isRunning() {
|
|
10914
11417
|
return this.running;
|
|
10915
11418
|
}
|
|
10916
|
-
async
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
11419
|
+
async reserve() {
|
|
11420
|
+
const deadline = performance.now() + this.timeoutSeconds * 1000;
|
|
11421
|
+
do {
|
|
11422
|
+
for (const key of this.queueKeys) {
|
|
11423
|
+
if (this.stopping)
|
|
11424
|
+
return null;
|
|
11425
|
+
const reservation = await reserveQueueJob(this.client, key, this.owner);
|
|
11426
|
+
if (reservation)
|
|
11427
|
+
return { queueKey: key, reservation };
|
|
10922
11428
|
}
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
}
|
|
10927
|
-
|
|
11429
|
+
if (this.stopping || performance.now() >= deadline)
|
|
11430
|
+
return null;
|
|
11431
|
+
await Bun.sleep(Math.min(100, Math.max(1, deadline - performance.now())));
|
|
11432
|
+
} while (!this.stopping);
|
|
11433
|
+
return null;
|
|
11434
|
+
}
|
|
11435
|
+
async processNext() {
|
|
11436
|
+
if (this.stopping)
|
|
10928
11437
|
return false;
|
|
10929
|
-
|
|
10930
|
-
const
|
|
10931
|
-
|
|
11438
|
+
await reclaimExpiredQueueReservations(this.client, this.queueKeys);
|
|
11439
|
+
const reserved = await this.reserve();
|
|
11440
|
+
if (!reserved)
|
|
11441
|
+
return false;
|
|
11442
|
+
const { queueKey, reservation } = reserved;
|
|
11443
|
+
const envelope = parseQueueJobEnvelope(reservation.payload);
|
|
10932
11444
|
if (!envelope) {
|
|
11445
|
+
await updateQueueReservation(this.client, queueKey, reservation, "quarantine");
|
|
10933
11446
|
return true;
|
|
10934
11447
|
}
|
|
11448
|
+
let acknowledge = false;
|
|
11449
|
+
let ownershipLost = false;
|
|
11450
|
+
let renewal;
|
|
11451
|
+
const heartbeat = setInterval(() => {
|
|
11452
|
+
if (renewal || ownershipLost)
|
|
11453
|
+
return;
|
|
11454
|
+
renewal = (async () => {
|
|
11455
|
+
try {
|
|
11456
|
+
if (!await updateQueueReservation(this.client, queueKey, reservation, "renew")) {
|
|
11457
|
+
ownershipLost = true;
|
|
11458
|
+
}
|
|
11459
|
+
} catch (error) {
|
|
11460
|
+
ownershipLost = true;
|
|
11461
|
+
console.error("[QueueWorker] Lease renewal failed:", error);
|
|
11462
|
+
}
|
|
11463
|
+
})().finally(() => {
|
|
11464
|
+
renewal = undefined;
|
|
11465
|
+
});
|
|
11466
|
+
}, Math.max(1, Math.floor(readVisibilityMs() / 3)));
|
|
10935
11467
|
try {
|
|
10936
|
-
await runQueueJob(envelope, this.failedJobs
|
|
11468
|
+
await runQueueJob(envelope, this.failedJobs, {
|
|
11469
|
+
onFailureRecorded: () => {
|
|
11470
|
+
acknowledge = true;
|
|
11471
|
+
}
|
|
11472
|
+
});
|
|
11473
|
+
acknowledge = true;
|
|
10937
11474
|
} catch (error) {
|
|
10938
11475
|
console.error("[QueueWorker] Job failed:", error);
|
|
11476
|
+
} finally {
|
|
11477
|
+
clearInterval(heartbeat);
|
|
11478
|
+
await renewal;
|
|
11479
|
+
}
|
|
11480
|
+
if (acknowledge && !ownershipLost) {
|
|
11481
|
+
await updateQueueReservation(this.client, queueKey, reservation, "ack");
|
|
10939
11482
|
}
|
|
10940
11483
|
return true;
|
|
10941
11484
|
}
|
|
10942
11485
|
async run() {
|
|
10943
11486
|
this.running = true;
|
|
10944
|
-
|
|
10945
|
-
|
|
11487
|
+
try {
|
|
11488
|
+
while (!this.stopping) {
|
|
11489
|
+
await this.processNext();
|
|
11490
|
+
}
|
|
11491
|
+
} finally {
|
|
11492
|
+
this.running = false;
|
|
10946
11493
|
}
|
|
10947
|
-
this.running = false;
|
|
10948
11494
|
}
|
|
10949
11495
|
close() {
|
|
10950
11496
|
this.client.close();
|
|
@@ -11003,9 +11549,9 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
|
11003
11549
|
return new QueueWorker(redisUrl, failedJobs);
|
|
11004
11550
|
}
|
|
11005
11551
|
// ../../src/core/queue/queueMetrics.ts
|
|
11006
|
-
var {RedisClient:
|
|
11552
|
+
var {RedisClient: RedisClient5 } = globalThis.Bun;
|
|
11007
11553
|
async function readRedisQueueDepth(redisUrl) {
|
|
11008
|
-
const client = new
|
|
11554
|
+
const client = new RedisClient5(redisUrl);
|
|
11009
11555
|
const [high, defaultQueue, low] = await Promise.all([
|
|
11010
11556
|
client.llen(QUEUE_HIGH_KEY),
|
|
11011
11557
|
client.llen(QUEUE_LIST_KEY),
|
|
@@ -11225,58 +11771,9 @@ init_tenantContext();
|
|
|
11225
11771
|
init_tenantDatabaseScope();
|
|
11226
11772
|
|
|
11227
11773
|
// ../../src/core/tenant/tenantMiddleware.ts
|
|
11228
|
-
import { createHash as
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
init_defaultConnection();
|
|
11232
|
-
init_connectionContext();
|
|
11233
|
-
init_deferredModelEvents();
|
|
11234
|
-
init_tenancyConfig();
|
|
11235
|
-
function stringifyBypassIdentifier(identifier) {
|
|
11236
|
-
if (typeof identifier === "number") {
|
|
11237
|
-
return String(identifier);
|
|
11238
|
-
}
|
|
11239
|
-
return identifier.trim();
|
|
11240
|
-
}
|
|
11241
|
-
async function applyIdentifierToTransaction(transaction, identifier) {
|
|
11242
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_identifier', $1, true)`, [
|
|
11243
|
-
stringifyBypassIdentifier(identifier)
|
|
11244
|
-
]);
|
|
11245
|
-
}
|
|
11246
|
-
function assertBypassIdentifier(identifier) {
|
|
11247
|
-
if (typeof identifier === "number") {
|
|
11248
|
-
if (!Number.isInteger(identifier) || identifier <= 0) {
|
|
11249
|
-
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
11250
|
-
}
|
|
11251
|
-
return;
|
|
11252
|
-
}
|
|
11253
|
-
if (typeof identifier === "string" && identifier.trim() !== "") {
|
|
11254
|
-
return;
|
|
11255
|
-
}
|
|
11256
|
-
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
11257
|
-
}
|
|
11258
|
-
async function runWithScopedTenantTransaction(apply, callback) {
|
|
11259
|
-
if (!isRlsTenancy()) {
|
|
11260
|
-
return await callback();
|
|
11261
|
-
}
|
|
11262
|
-
const pool = getDefaultDatabasePool();
|
|
11263
|
-
const begin = pool.begin;
|
|
11264
|
-
if (typeof begin !== "function") {
|
|
11265
|
-
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
11266
|
-
}
|
|
11267
|
-
return await runWithDeferredModelEvents(async () => {
|
|
11268
|
-
return await begin(async (transaction) => {
|
|
11269
|
-
await apply(transaction);
|
|
11270
|
-
return await runWithDatabaseConnection(transaction, callback);
|
|
11271
|
-
});
|
|
11272
|
-
});
|
|
11273
|
-
}
|
|
11274
|
-
async function runWithMigrationBypassForIdentifier(identifier, callback) {
|
|
11275
|
-
assertBypassIdentifier(identifier);
|
|
11276
|
-
return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
|
|
11277
|
-
}
|
|
11278
|
-
|
|
11279
|
-
// ../../src/core/tenant/tenantMiddleware.ts
|
|
11774
|
+
import { createHash as createHash5 } from "crypto";
|
|
11775
|
+
init_repositoryConnection();
|
|
11776
|
+
init_databaseTenantContext();
|
|
11280
11777
|
init_tenancyConfig();
|
|
11281
11778
|
init_tenantContext();
|
|
11282
11779
|
init_tenantDatabaseScope();
|
|
@@ -11301,7 +11798,7 @@ async function resolveUserTenantId(userId) {
|
|
|
11301
11798
|
});
|
|
11302
11799
|
}
|
|
11303
11800
|
function auditChecksum(payload) {
|
|
11304
|
-
return
|
|
11801
|
+
return createHash5("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
11305
11802
|
}
|
|
11306
11803
|
async function resolveTenantForRequest(request) {
|
|
11307
11804
|
const user = currentAuthUser();
|
|
@@ -12129,7 +12626,6 @@ export {
|
|
|
12129
12626
|
namespacedRedisKey,
|
|
12130
12627
|
noContentResponse,
|
|
12131
12628
|
normalizeFieldErrors,
|
|
12132
|
-
normalizeMetricPath,
|
|
12133
12629
|
notFoundHtmlResponse,
|
|
12134
12630
|
notModifiedResponse,
|
|
12135
12631
|
otelServiceName,
|
|
@@ -12177,6 +12673,7 @@ export {
|
|
|
12177
12673
|
repositoryConnection,
|
|
12178
12674
|
requestIdMiddleware,
|
|
12179
12675
|
requestMetaContext,
|
|
12676
|
+
requestTransactionRollback,
|
|
12180
12677
|
requireConfiguredSecret,
|
|
12181
12678
|
required,
|
|
12182
12679
|
resetBoundDatabaseConnection,
|