@getstrata/core 2.2.27 → 2.2.29

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/dist/index.js CHANGED
@@ -140,220 +140,6 @@ var init_defaultConnection = __esm(() => {
140
140
  DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
141
141
  });
142
142
 
143
- // ../../src/core/database/schema/driver.ts
144
- function normalizeConnectionName(connection) {
145
- const normalized = connection.trim().toLowerCase();
146
- if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
147
- return "pgsql";
148
- }
149
- if (normalized === "mysql" || normalized === "mariadb") {
150
- return "mysql";
151
- }
152
- if (normalized === "sqlite") {
153
- return "sqlite";
154
- }
155
- throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
156
- }
157
- function resolveDriverFromUrl(url) {
158
- const normalized = url.trim().toLowerCase();
159
- if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
160
- return "pgsql";
161
- }
162
- if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
163
- return "mysql";
164
- }
165
- if (normalized.startsWith("sqlite:")) {
166
- return "sqlite";
167
- }
168
- return null;
169
- }
170
- function resolveDatabaseDriver(options = {}) {
171
- const connection = options.connection ?? process.env.DB_CONNECTION;
172
- if (connection) {
173
- return normalizeConnectionName(connection);
174
- }
175
- const url = options.url ?? process.env.DATABASE_URL ?? "";
176
- const fromUrl = resolveDriverFromUrl(url);
177
- if (fromUrl) {
178
- return fromUrl;
179
- }
180
- return "pgsql";
181
- }
182
- var init_driver = () => {};
183
-
184
- // ../../src/core/database/dialect.ts
185
- function assertSafeIdentifier(identifier) {
186
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
187
- throw new Error(`Invalid SQL identifier: ${identifier}`);
188
- }
189
- return identifier;
190
- }
191
- function quoteIdentifierFor(dialect, column) {
192
- return dialect.quoteIdentifier(column);
193
- }
194
- function dialectFor(driver) {
195
- return dialects[driver];
196
- }
197
- function currentSqlDialect() {
198
- return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
199
- }
200
- function useSqlDialect(driver) {
201
- dialectOverride = dialectFor(driver);
202
- return dialectOverride;
203
- }
204
- function sqlTimestamp(value = new Date) {
205
- return currentSqlDialect().timestampValue(value);
206
- }
207
- function resetSqlDialect() {
208
- dialectOverride = null;
209
- }
210
- function runWithSqlDialect(driver, callback) {
211
- return dialectContext.run(dialectFor(driver), callback);
212
- }
213
- var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
214
- var init_dialect = __esm(() => {
215
- init_asyncContextStore();
216
- init_driver();
217
- postgresDialect = {
218
- driver: "pgsql",
219
- placeholder(index) {
220
- return `$${index}`;
221
- },
222
- quoteIdentifier(identifier) {
223
- return `"${assertSafeIdentifier(identifier)}"`;
224
- },
225
- nowExpression() {
226
- return "NOW()";
227
- },
228
- epochMillisecondsExpression() {
229
- return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
230
- },
231
- rowLockClause(skipLocked = false) {
232
- return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
233
- },
234
- timestampValue(value) {
235
- return value.toISOString();
236
- },
237
- returningClause(columns) {
238
- return ` RETURNING ${columns}`;
239
- },
240
- ilikeOperator() {
241
- return "ILIKE";
242
- },
243
- nullsLastSuffix() {
244
- return " NULLS LAST";
245
- },
246
- castToText(expression) {
247
- return `${expression}::text`;
248
- },
249
- upsertSuffix(conflictColumns, updateColumns) {
250
- const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
251
- if (updateColumns.length === 0) {
252
- return ` ON CONFLICT (${target}) DO NOTHING`;
253
- }
254
- const assignments = updateColumns.map((column) => {
255
- const quoted = quoteIdentifierFor(this, column);
256
- return `${quoted} = excluded.${quoted}`;
257
- }).join(", ");
258
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
259
- }
260
- };
261
- mysqlDialect = {
262
- driver: "mysql",
263
- placeholder() {
264
- return "?";
265
- },
266
- quoteIdentifier(identifier) {
267
- return `\`${assertSafeIdentifier(identifier)}\``;
268
- },
269
- nowExpression() {
270
- return "CURRENT_TIMESTAMP";
271
- },
272
- epochMillisecondsExpression() {
273
- return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
274
- },
275
- rowLockClause(skipLocked = false) {
276
- return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
277
- },
278
- timestampValue(value) {
279
- return value.toISOString().slice(0, 19).replace("T", " ");
280
- },
281
- returningClause() {
282
- return "";
283
- },
284
- ilikeOperator() {
285
- return "LIKE";
286
- },
287
- nullsLastSuffix() {
288
- return "";
289
- },
290
- castToText(expression) {
291
- return `CAST(${expression} AS CHAR)`;
292
- },
293
- upsertSuffix(conflictColumns, updateColumns) {
294
- if (updateColumns.length === 0) {
295
- const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
296
- return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
297
- }
298
- const assignments = updateColumns.map((column) => {
299
- const quoted = quoteIdentifierFor(this, column);
300
- return `${quoted} = VALUES(${quoted})`;
301
- }).join(", ");
302
- return ` ON DUPLICATE KEY UPDATE ${assignments}`;
303
- }
304
- };
305
- sqliteDialect = {
306
- driver: "sqlite",
307
- placeholder() {
308
- return "?";
309
- },
310
- quoteIdentifier(identifier) {
311
- return `"${assertSafeIdentifier(identifier)}"`;
312
- },
313
- nowExpression() {
314
- return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
315
- },
316
- epochMillisecondsExpression() {
317
- return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
318
- },
319
- rowLockClause() {
320
- return "";
321
- },
322
- timestampValue(value) {
323
- return value.toISOString();
324
- },
325
- returningClause(columns) {
326
- return ` RETURNING ${columns}`;
327
- },
328
- ilikeOperator() {
329
- return "LIKE";
330
- },
331
- nullsLastSuffix() {
332
- return "";
333
- },
334
- castToText(expression) {
335
- return `CAST(${expression} AS TEXT)`;
336
- },
337
- upsertSuffix(conflictColumns, updateColumns) {
338
- const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
339
- if (updateColumns.length === 0) {
340
- return ` ON CONFLICT (${target}) DO NOTHING`;
341
- }
342
- const assignments = updateColumns.map((column) => {
343
- const quoted = quoteIdentifierFor(this, column);
344
- return `${quoted} = excluded.${quoted}`;
345
- }).join(", ");
346
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
347
- }
348
- };
349
- dialects = {
350
- pgsql: postgresDialect,
351
- mysql: mysqlDialect,
352
- sqlite: sqliteDialect
353
- };
354
- dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
355
- });
356
-
357
143
  // ../../src/core/tenant/tenantContext.ts
358
144
  function runWithTenant(tenant, callback) {
359
145
  return tenantContext.run(tenant, callback);
@@ -374,78 +160,68 @@ var init_tenantContext = __esm(() => {
374
160
  tenantContext = createAsyncContextStore("@getstrata/tenantContext");
375
161
  });
376
162
 
377
- // ../../src/core/database/repositoryConnection.ts
378
- function resolveRepositoryConnection() {
379
- return getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabaseQuery());
380
- }
381
- function compileTaggedSql(strings, values) {
382
- const dialect = currentSqlDialect();
383
- let query = strings[0] ?? "";
384
- const params = [];
385
- for (let index = 0;index < values.length; index += 1) {
386
- params.push(values[index]);
387
- query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
388
- }
389
- return { query, params };
390
- }
391
- function isTaggedStrings(value) {
392
- if (!Array.isArray(value) || typeof value[0] !== "string") {
393
- return false;
163
+ // ../../src/core/events/eventBus.ts
164
+ class EventBus {
165
+ constructor() {}
166
+ transactionalListeners = new Map;
167
+ listenTransactional(event, listener) {
168
+ const handlers = this.transactionalListeners.get(event) ?? new Set;
169
+ handlers.add(listener);
170
+ this.transactionalListeners.set(event, handlers);
171
+ return () => {
172
+ handlers.delete(listener);
173
+ if (!handlers.size)
174
+ this.transactionalListeners.delete(event);
175
+ };
394
176
  }
395
- return typeof value.raw !== "undefined";
396
- }
397
- function runOnConnection(connection, args) {
398
- if (typeof connection === "function") {
399
- return connection(...args);
177
+ hasTransactionalListeners(event) {
178
+ return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
400
179
  }
401
- if (typeof connection.unsafe !== "function") {
402
- throw new Error("Database connection cannot run SQL.");
180
+ async dispatchTransactional(event, payload) {
181
+ for (const listener of this.transactionalListeners.get(event) ?? [])
182
+ await listener(payload);
403
183
  }
404
- const strings = args[0];
405
- if (!isTaggedStrings(strings)) {
406
- throw new Error("Database connection cannot run tagged SQL.");
184
+ listeners = new Map;
185
+ on(event, listener) {
186
+ return this.listen(event, listener);
407
187
  }
408
- const compiled = compileTaggedSql(strings, args.slice(1));
409
- return connection.unsafe(compiled.query, compiled.params);
410
- }
411
- var repositoryConnection;
412
- var init_repositoryConnection = __esm(() => {
413
- init_boundConnection();
414
- init_connectionContext();
415
- init_defaultConnection();
416
- init_dialect();
417
- repositoryConnection = new Proxy(function repositoryConnection() {}, {
418
- apply(_target, _thisArg, args) {
419
- return runOnConnection(resolveRepositoryConnection(), args);
420
- },
421
- get(_target, property) {
422
- const connection = resolveRepositoryConnection();
423
- const value = connection[property];
424
- return typeof value === "function" ? value.bind(connection) : value;
425
- }
426
- });
427
- });
428
-
429
- // ../../src/core/tenant/tenancyConfig.ts
430
- function readTenancyDriver(env = process.env) {
431
- const raw = env.TENANCY_DRIVER?.trim();
432
- if (raw === undefined || raw === "") {
433
- return "rls";
188
+ listen(event, listener) {
189
+ const handlers = this.listeners.get(event) ?? new Set;
190
+ handlers.add(listener);
191
+ this.listeners.set(event, handlers);
192
+ return () => {
193
+ handlers.delete(listener);
194
+ if (handlers.size === 0) {
195
+ this.listeners.delete(event);
196
+ }
197
+ };
434
198
  }
435
- if (TENANCY_DRIVERS.includes(raw)) {
436
- return raw;
199
+ async emit(event, payload) {
200
+ await this.dispatch(event, payload);
201
+ }
202
+ async dispatch(event, payload) {
203
+ const handlers = this.listeners.get(event);
204
+ if (!handlers || handlers.size === 0) {
205
+ return;
206
+ }
207
+ for (const handler of handlers) {
208
+ await handler(payload);
209
+ }
437
210
  }
438
- throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
439
- }
440
- function isTenancyEnabled(env = process.env) {
441
- return readTenancyDriver(env) !== "none";
442
211
  }
443
- function isRlsTenancy(env = process.env) {
444
- return readTenancyDriver(env) === "rls";
212
+ function readSharedEventBus() {
213
+ const globalBus = globalThis[EVENT_BUS_KEY];
214
+ if (globalBus) {
215
+ return globalBus;
216
+ }
217
+ const bus = new EventBus;
218
+ globalThis[EVENT_BUS_KEY] = bus;
219
+ return bus;
445
220
  }
446
- var TENANCY_DRIVERS;
447
- var init_tenancyConfig = __esm(() => {
448
- TENANCY_DRIVERS = ["rls", "column", "none"];
221
+ var EVENT_BUS_KEY, eventBus;
222
+ var init_eventBus = __esm(() => {
223
+ EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
224
+ eventBus = readSharedEventBus();
449
225
  });
450
226
 
451
227
  // ../../src/core/database/transactionControl.ts
@@ -526,68 +302,71 @@ var init_transactionControl = __esm(() => {
526
302
  transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
527
303
  });
528
304
 
529
- // ../../src/core/events/eventBus.ts
530
- class EventBus {
531
- constructor() {}
532
- transactionalListeners = new Map;
533
- listenTransactional(event, listener) {
534
- const handlers = this.transactionalListeners.get(event) ?? new Set;
535
- handlers.add(listener);
536
- this.transactionalListeners.set(event, handlers);
537
- return () => {
538
- handlers.delete(listener);
539
- if (!handlers.size)
540
- this.transactionalListeners.delete(event);
541
- };
542
- }
543
- hasTransactionalListeners(event) {
544
- return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
305
+ // ../../src/core/tenant/tenancyConfig.ts
306
+ function readTenancyDriver(env = process.env) {
307
+ const raw = env.TENANCY_DRIVER?.trim();
308
+ if (raw === undefined || raw === "") {
309
+ return "rls";
545
310
  }
546
- async dispatchTransactional(event, payload) {
547
- for (const listener of this.transactionalListeners.get(event) ?? [])
548
- await listener(payload);
311
+ if (TENANCY_DRIVERS.includes(raw)) {
312
+ return raw;
549
313
  }
550
- listeners = new Map;
551
- on(event, listener) {
552
- return this.listen(event, listener);
314
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
315
+ }
316
+ function isTenancyEnabled(env = process.env) {
317
+ return readTenancyDriver(env) !== "none";
318
+ }
319
+ function isRlsTenancy(env = process.env) {
320
+ return readTenancyDriver(env) === "rls";
321
+ }
322
+ var TENANCY_DRIVERS;
323
+ var init_tenancyConfig = __esm(() => {
324
+ TENANCY_DRIVERS = ["rls", "column", "none"];
325
+ });
326
+
327
+ // ../../src/core/tenant/tenantDatabaseScope.ts
328
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
329
+ async function applyTenantContextToTransaction(transaction, tenantId) {
330
+ await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
331
+ await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
332
+ }
333
+ async function runWithTenantDatabase(tenant, callback) {
334
+ const outerTenant = currentTenant();
335
+ if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
336
+ throw new Error("Cannot switch tenants inside an active database transaction.");
553
337
  }
554
- listen(event, listener) {
555
- const handlers = this.listeners.get(event) ?? new Set;
556
- handlers.add(listener);
557
- this.listeners.set(event, handlers);
558
- return () => {
559
- handlers.delete(listener);
560
- if (handlers.size === 0) {
561
- this.listeners.delete(event);
562
- }
563
- };
338
+ if (!isTenancyEnabled() || !isRlsTenancy()) {
339
+ return await runWithTenant(tenant, callback);
564
340
  }
565
- async emit(event, payload) {
566
- await this.dispatch(event, payload);
341
+ if (hasActiveDatabaseConnection()) {
342
+ const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
343
+ return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
567
344
  }
568
- async dispatch(event, payload) {
569
- const handlers = this.listeners.get(event);
570
- if (!handlers || handlers.size === 0) {
571
- return;
572
- }
573
- for (const handler of handlers) {
574
- await handler(payload);
575
- }
345
+ const pool = getDefaultDatabasePool();
346
+ const begin = pool.begin;
347
+ if (typeof begin !== "function") {
348
+ throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
576
349
  }
350
+ return await settleTransaction(() => runWithDeferredModelEvents(async () => {
351
+ return await begin.bind(pool)(AsyncLocalStorage2.bind(async (transaction) => {
352
+ await applyTenantContextToTransaction(transaction, tenant.id);
353
+ return await runWithDatabaseConnection(transaction, async () => {
354
+ return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
355
+ });
356
+ }));
357
+ }));
577
358
  }
578
- function readSharedEventBus() {
579
- const globalBus = globalThis[EVENT_BUS_KEY];
580
- if (globalBus) {
581
- return globalBus;
582
- }
583
- const bus = new EventBus;
584
- globalThis[EVENT_BUS_KEY] = bus;
585
- return bus;
359
+ function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
360
+ return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
586
361
  }
587
- var EVENT_BUS_KEY, eventBus;
588
- var init_eventBus = __esm(() => {
589
- EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
590
- eventBus = readSharedEventBus();
362
+ var init_tenantDatabaseScope = __esm(() => {
363
+ init_defaultConnection();
364
+ init_connectionContext();
365
+ init_transactionControl();
366
+ init_deferredModelEvents();
367
+ init_databaseTenantContext();
368
+ init_tenancyConfig();
369
+ init_tenantContext();
591
370
  });
592
371
 
593
372
  // ../../src/core/events/deferredModelEvents.ts
@@ -667,147 +446,413 @@ async function runWithDeferredModelEvents(callback) {
667
446
  rollback();
668
447
  throw error;
669
448
  }
670
- }
671
- const state = { frames: [[]], rollbackFrames: [[]] };
672
- let result;
673
- try {
674
- result = await deferredModelEvents.run(state, callback);
675
- } catch (error) {
676
- for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
677
- rollback();
678
- throw error;
679
- }
680
- await dispatchQueuedModelEvents(state.frames[0] ?? []);
681
- return result;
682
- }
683
- function onDeferredModelRollback(callback) {
684
- deferredModelEvents.getStore()?.rollbackFrames.at(-1)?.push(callback);
685
- }
686
- var deferredModelEvents;
687
- var init_deferredModelEvents = __esm(() => {
688
- init_asyncContextStore();
689
- init_tenantContext();
690
- init_eventBus();
691
- deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
692
- });
693
-
694
- // ../../src/core/database/acquisitionMetrics.ts
695
- function createTransactionAcquisitionMetrics() {
696
- const state = {
697
- inflight: 0,
698
- outcomes: outcomes.map((outcome) => ({
699
- outcome,
700
- count: 0,
701
- sumSeconds: 0,
702
- buckets: ACQUISITION_BUCKETS_SECONDS.map(() => 0)
703
- }))
449
+ }
450
+ const state = { frames: [[]], rollbackFrames: [[]] };
451
+ let result;
452
+ try {
453
+ result = await deferredModelEvents.run(state, callback);
454
+ } catch (error) {
455
+ for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
456
+ rollback();
457
+ throw error;
458
+ }
459
+ await dispatchQueuedModelEvents(state.frames[0] ?? []);
460
+ return result;
461
+ }
462
+ function onDeferredModelRollback(callback) {
463
+ deferredModelEvents.getStore()?.rollbackFrames.at(-1)?.push(callback);
464
+ }
465
+ var deferredModelEvents;
466
+ var init_deferredModelEvents = __esm(() => {
467
+ init_asyncContextStore();
468
+ init_tenantContext();
469
+ init_eventBus();
470
+ deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
471
+ });
472
+
473
+ // ../../src/core/database/acquisitionMetrics.ts
474
+ function createTransactionAcquisitionMetrics() {
475
+ const state = {
476
+ inflight: 0,
477
+ outcomes: outcomes.map((outcome) => ({
478
+ outcome,
479
+ count: 0,
480
+ sumSeconds: 0,
481
+ buckets: ACQUISITION_BUCKETS_SECONDS.map(() => 0)
482
+ }))
483
+ };
484
+ const metrics = {
485
+ [metricsBrand]: true,
486
+ snapshot: () => ({
487
+ inflight: state.inflight,
488
+ outcomes: state.outcomes.map((histogram) => ({
489
+ ...histogram,
490
+ buckets: [...histogram.buckets]
491
+ }))
492
+ })
493
+ };
494
+ states.set(metrics, state);
495
+ return metrics;
496
+ }
497
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
498
+ const state = states.get(metrics);
499
+ if (!state)
500
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
501
+ const started = performance.now();
502
+ state.inflight++;
503
+ let outcome = "acquired";
504
+ try {
505
+ return await acquire();
506
+ } catch (error) {
507
+ outcome = signal?.aborted ? "aborted" : "failed";
508
+ throw error;
509
+ } finally {
510
+ state.inflight--;
511
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
512
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
513
+ if (histogram) {
514
+ histogram.count++;
515
+ histogram.sumSeconds += elapsed;
516
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
517
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
518
+ if (bound !== undefined && elapsed <= bound)
519
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
520
+ }
521
+ }
522
+ }
523
+ }
524
+ function renderTransactionAcquisitionMetrics(snapshot) {
525
+ const invalid = () => new TypeError("Invalid transaction acquisition metrics.");
526
+ if (!Number.isSafeInteger(snapshot.inflight) || snapshot.inflight < 0 || snapshot.outcomes.length !== outcomes.length)
527
+ throw invalid();
528
+ const name = "strata_database_transaction_acquisition_duration_seconds";
529
+ const lines = [
530
+ "# HELP strata_database_acquisition_collector_success Whether transaction acquisition observation succeeded.",
531
+ "# TYPE strata_database_acquisition_collector_success gauge",
532
+ "strata_database_acquisition_collector_success 1",
533
+ "# HELP strata_database_transaction_acquisition_inflight Native transaction checkouts awaiting settlement in this process.",
534
+ "# TYPE strata_database_transaction_acquisition_inflight gauge",
535
+ `strata_database_transaction_acquisition_inflight ${snapshot.inflight}`,
536
+ `# HELP ${name} Native checkout duration, including pool wait and connection establishment, for explicitly observed transactions.`,
537
+ `# TYPE ${name} histogram`
538
+ ];
539
+ for (let i = 0;i < outcomes.length; i++) {
540
+ const histogram = snapshot.outcomes[i];
541
+ if (!histogram || histogram.outcome !== outcomes[i] || !Number.isSafeInteger(histogram.count) || histogram.count < 0 || !Number.isFinite(histogram.sumSeconds) || histogram.sumSeconds < 0 || histogram.buckets.length !== ACQUISITION_BUCKETS_SECONDS.length)
542
+ throw invalid();
543
+ let previous = 0;
544
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
545
+ const value = histogram.buckets[index];
546
+ if (value === undefined || !Number.isSafeInteger(value) || value < previous || value > histogram.count)
547
+ throw invalid();
548
+ previous = value;
549
+ lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="${ACQUISITION_BUCKETS_SECONDS[index]}"} ${value}`);
550
+ }
551
+ lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="+Inf"} ${histogram.count}`, `${name}_count{outcome="${histogram.outcome}"} ${histogram.count}`, `${name}_sum{outcome="${histogram.outcome}"} ${histogram.sumSeconds}`);
552
+ }
553
+ return `${lines.join(`
554
+ `)}
555
+ `;
556
+ }
557
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
558
+ var init_acquisitionMetrics = __esm(() => {
559
+ ACQUISITION_BUCKETS_SECONDS = [
560
+ 0.001,
561
+ 0.005,
562
+ 0.01,
563
+ 0.025,
564
+ 0.05,
565
+ 0.1,
566
+ 0.25,
567
+ 0.5,
568
+ 1,
569
+ 2.5,
570
+ 5,
571
+ 10
572
+ ];
573
+ outcomes = ["acquired", "failed", "aborted"];
574
+ metricsBrand = Symbol("transaction acquisition metrics");
575
+ states = new WeakMap;
576
+ });
577
+
578
+ // ../../src/core/database/connection.ts
579
+ function createDatabaseConnection(source) {
580
+ return {
581
+ async unsafe(query, params = []) {
582
+ return await source.unsafe(query, params);
583
+ }
584
+ };
585
+ }
586
+ var init_connection = () => {};
587
+
588
+ // ../../src/core/database/schema/driver.ts
589
+ function normalizeConnectionName(connection) {
590
+ const normalized = connection.trim().toLowerCase();
591
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
592
+ return "pgsql";
593
+ }
594
+ if (normalized === "mysql" || normalized === "mariadb") {
595
+ return "mysql";
596
+ }
597
+ if (normalized === "sqlite") {
598
+ return "sqlite";
599
+ }
600
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
601
+ }
602
+ function resolveDriverFromUrl(url) {
603
+ const normalized = url.trim().toLowerCase();
604
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
605
+ return "pgsql";
606
+ }
607
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
608
+ return "mysql";
609
+ }
610
+ if (normalized.startsWith("sqlite:")) {
611
+ return "sqlite";
612
+ }
613
+ return null;
614
+ }
615
+ function resolveDatabaseDriver(options = {}) {
616
+ const connection = options.connection ?? process.env.DB_CONNECTION;
617
+ if (connection) {
618
+ return normalizeConnectionName(connection);
619
+ }
620
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
621
+ const fromUrl = resolveDriverFromUrl(url);
622
+ if (fromUrl) {
623
+ return fromUrl;
624
+ }
625
+ return "pgsql";
626
+ }
627
+ var init_driver = () => {};
628
+
629
+ // ../../src/core/database/dialect.ts
630
+ function assertSafeIdentifier(identifier) {
631
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
632
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
633
+ }
634
+ return identifier;
635
+ }
636
+ function quoteIdentifierFor(dialect, column) {
637
+ return dialect.quoteIdentifier(column);
638
+ }
639
+ function dialectFor(driver) {
640
+ return dialects[driver];
641
+ }
642
+ function currentSqlDialect() {
643
+ return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
644
+ }
645
+ function useSqlDialect(driver) {
646
+ dialectOverride = dialectFor(driver);
647
+ return dialectOverride;
648
+ }
649
+ function sqlTimestamp(value = new Date) {
650
+ return currentSqlDialect().timestampValue(value);
651
+ }
652
+ function resetSqlDialect() {
653
+ dialectOverride = null;
654
+ }
655
+ function runWithSqlDialect(driver, callback) {
656
+ return dialectContext.run(dialectFor(driver), callback);
657
+ }
658
+ var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
659
+ var init_dialect = __esm(() => {
660
+ init_asyncContextStore();
661
+ init_driver();
662
+ postgresDialect = {
663
+ driver: "pgsql",
664
+ placeholder(index) {
665
+ return `$${index}`;
666
+ },
667
+ quoteIdentifier(identifier) {
668
+ return `"${assertSafeIdentifier(identifier)}"`;
669
+ },
670
+ nowExpression() {
671
+ return "NOW()";
672
+ },
673
+ epochMillisecondsExpression() {
674
+ return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
675
+ },
676
+ rowLockClause(skipLocked = false) {
677
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
678
+ },
679
+ timestampValue(value) {
680
+ return value.toISOString();
681
+ },
682
+ returningClause(columns) {
683
+ return ` RETURNING ${columns}`;
684
+ },
685
+ ilikeOperator() {
686
+ return "ILIKE";
687
+ },
688
+ nullsLastSuffix() {
689
+ return " NULLS LAST";
690
+ },
691
+ castToText(expression) {
692
+ return `${expression}::text`;
693
+ },
694
+ upsertSuffix(conflictColumns, updateColumns) {
695
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
696
+ if (updateColumns.length === 0) {
697
+ return ` ON CONFLICT (${target}) DO NOTHING`;
698
+ }
699
+ const assignments = updateColumns.map((column) => {
700
+ const quoted = quoteIdentifierFor(this, column);
701
+ return `${quoted} = excluded.${quoted}`;
702
+ }).join(", ");
703
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
704
+ }
705
+ };
706
+ mysqlDialect = {
707
+ driver: "mysql",
708
+ placeholder() {
709
+ return "?";
710
+ },
711
+ quoteIdentifier(identifier) {
712
+ return `\`${assertSafeIdentifier(identifier)}\``;
713
+ },
714
+ nowExpression() {
715
+ return "CURRENT_TIMESTAMP";
716
+ },
717
+ epochMillisecondsExpression() {
718
+ return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
719
+ },
720
+ rowLockClause(skipLocked = false) {
721
+ return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
722
+ },
723
+ timestampValue(value) {
724
+ return value.toISOString().slice(0, 19).replace("T", " ");
725
+ },
726
+ returningClause() {
727
+ return "";
728
+ },
729
+ ilikeOperator() {
730
+ return "LIKE";
731
+ },
732
+ nullsLastSuffix() {
733
+ return "";
734
+ },
735
+ castToText(expression) {
736
+ return `CAST(${expression} AS CHAR)`;
737
+ },
738
+ upsertSuffix(conflictColumns, updateColumns) {
739
+ if (updateColumns.length === 0) {
740
+ const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
741
+ return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
742
+ }
743
+ const assignments = updateColumns.map((column) => {
744
+ const quoted = quoteIdentifierFor(this, column);
745
+ return `${quoted} = VALUES(${quoted})`;
746
+ }).join(", ");
747
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
748
+ }
749
+ };
750
+ sqliteDialect = {
751
+ driver: "sqlite",
752
+ placeholder() {
753
+ return "?";
754
+ },
755
+ quoteIdentifier(identifier) {
756
+ return `"${assertSafeIdentifier(identifier)}"`;
757
+ },
758
+ nowExpression() {
759
+ return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
760
+ },
761
+ epochMillisecondsExpression() {
762
+ return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
763
+ },
764
+ rowLockClause() {
765
+ return "";
766
+ },
767
+ timestampValue(value) {
768
+ return value.toISOString();
769
+ },
770
+ returningClause(columns) {
771
+ return ` RETURNING ${columns}`;
772
+ },
773
+ ilikeOperator() {
774
+ return "LIKE";
775
+ },
776
+ nullsLastSuffix() {
777
+ return "";
778
+ },
779
+ castToText(expression) {
780
+ return `CAST(${expression} AS TEXT)`;
781
+ },
782
+ upsertSuffix(conflictColumns, updateColumns) {
783
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
784
+ if (updateColumns.length === 0) {
785
+ return ` ON CONFLICT (${target}) DO NOTHING`;
786
+ }
787
+ const assignments = updateColumns.map((column) => {
788
+ const quoted = quoteIdentifierFor(this, column);
789
+ return `${quoted} = excluded.${quoted}`;
790
+ }).join(", ");
791
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
792
+ }
704
793
  };
705
- const metrics = {
706
- [metricsBrand]: true,
707
- snapshot: () => ({
708
- inflight: state.inflight,
709
- outcomes: state.outcomes.map((histogram) => ({
710
- ...histogram,
711
- buckets: [...histogram.buckets]
712
- }))
713
- })
794
+ dialects = {
795
+ pgsql: postgresDialect,
796
+ mysql: mysqlDialect,
797
+ sqlite: sqliteDialect
714
798
  };
715
- states.set(metrics, state);
716
- return metrics;
799
+ dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
800
+ });
801
+
802
+ // ../../src/core/database/repositoryConnection.ts
803
+ function resolveRepositoryConnection() {
804
+ return getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabaseQuery());
717
805
  }
718
- async function observeTransactionAcquisition(metrics, acquire, signal) {
719
- const state = states.get(metrics);
720
- if (!state)
721
- throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
722
- const started = performance.now();
723
- state.inflight++;
724
- let outcome = "acquired";
725
- try {
726
- return await acquire();
727
- } catch (error) {
728
- outcome = signal?.aborted ? "aborted" : "failed";
729
- throw error;
730
- } finally {
731
- state.inflight--;
732
- const elapsed = Math.max(0, performance.now() - started) / 1000;
733
- const histogram = state.outcomes[outcomes.indexOf(outcome)];
734
- if (histogram) {
735
- histogram.count++;
736
- histogram.sumSeconds += elapsed;
737
- for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
738
- const bound = ACQUISITION_BUCKETS_SECONDS[index];
739
- if (bound !== undefined && elapsed <= bound)
740
- histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
741
- }
742
- }
806
+ function compileTaggedSql(strings, values) {
807
+ const dialect = currentSqlDialect();
808
+ let query = strings[0] ?? "";
809
+ const params = [];
810
+ for (let index = 0;index < values.length; index += 1) {
811
+ params.push(values[index]);
812
+ query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
743
813
  }
814
+ return { query, params };
744
815
  }
745
- function renderTransactionAcquisitionMetrics(snapshot) {
746
- const invalid = () => new TypeError("Invalid transaction acquisition metrics.");
747
- if (!Number.isSafeInteger(snapshot.inflight) || snapshot.inflight < 0 || snapshot.outcomes.length !== outcomes.length)
748
- throw invalid();
749
- const name = "strata_database_transaction_acquisition_duration_seconds";
750
- const lines = [
751
- "# HELP strata_database_acquisition_collector_success Whether transaction acquisition observation succeeded.",
752
- "# TYPE strata_database_acquisition_collector_success gauge",
753
- "strata_database_acquisition_collector_success 1",
754
- "# HELP strata_database_transaction_acquisition_inflight Native transaction checkouts awaiting settlement in this process.",
755
- "# TYPE strata_database_transaction_acquisition_inflight gauge",
756
- `strata_database_transaction_acquisition_inflight ${snapshot.inflight}`,
757
- `# HELP ${name} Native checkout duration, including pool wait and connection establishment, for explicitly observed transactions.`,
758
- `# TYPE ${name} histogram`
759
- ];
760
- for (let i = 0;i < outcomes.length; i++) {
761
- const histogram = snapshot.outcomes[i];
762
- if (!histogram || histogram.outcome !== outcomes[i] || !Number.isSafeInteger(histogram.count) || histogram.count < 0 || !Number.isFinite(histogram.sumSeconds) || histogram.sumSeconds < 0 || histogram.buckets.length !== ACQUISITION_BUCKETS_SECONDS.length)
763
- throw invalid();
764
- let previous = 0;
765
- for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
766
- const value = histogram.buckets[index];
767
- if (value === undefined || !Number.isSafeInteger(value) || value < previous || value > histogram.count)
768
- throw invalid();
769
- previous = value;
770
- lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="${ACQUISITION_BUCKETS_SECONDS[index]}"} ${value}`);
771
- }
772
- lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="+Inf"} ${histogram.count}`, `${name}_count{outcome="${histogram.outcome}"} ${histogram.count}`, `${name}_sum{outcome="${histogram.outcome}"} ${histogram.sumSeconds}`);
816
+ function isTaggedStrings(value) {
817
+ if (!Array.isArray(value) || typeof value[0] !== "string") {
818
+ return false;
773
819
  }
774
- return `${lines.join(`
775
- `)}
776
- `;
820
+ return typeof value.raw !== "undefined";
777
821
  }
778
- var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
779
- var init_acquisitionMetrics = __esm(() => {
780
- ACQUISITION_BUCKETS_SECONDS = [
781
- 0.001,
782
- 0.005,
783
- 0.01,
784
- 0.025,
785
- 0.05,
786
- 0.1,
787
- 0.25,
788
- 0.5,
789
- 1,
790
- 2.5,
791
- 5,
792
- 10
793
- ];
794
- outcomes = ["acquired", "failed", "aborted"];
795
- metricsBrand = Symbol("transaction acquisition metrics");
796
- states = new WeakMap;
797
- });
798
-
799
- // ../../src/core/database/connection.ts
800
- function createDatabaseConnection(source) {
801
- return {
802
- async unsafe(query, params = []) {
803
- return await source.unsafe(query, params);
804
- }
805
- };
822
+ function runOnConnection(connection, args) {
823
+ if (typeof connection === "function") {
824
+ return connection(...args);
825
+ }
826
+ if (typeof connection.unsafe !== "function") {
827
+ throw new Error("Database connection cannot run SQL.");
828
+ }
829
+ const strings = args[0];
830
+ if (!isTaggedStrings(strings)) {
831
+ throw new Error("Database connection cannot run tagged SQL.");
832
+ }
833
+ const compiled = compileTaggedSql(strings, args.slice(1));
834
+ return connection.unsafe(compiled.query, compiled.params);
806
835
  }
807
- var init_connection = () => {};
836
+ var repositoryConnection;
837
+ var init_repositoryConnection = __esm(() => {
838
+ init_boundConnection();
839
+ init_connectionContext();
840
+ init_defaultConnection();
841
+ init_dialect();
842
+ repositoryConnection = new Proxy(function repositoryConnection() {}, {
843
+ apply(_target, _thisArg, args) {
844
+ return runOnConnection(resolveRepositoryConnection(), args);
845
+ },
846
+ get(_target, property) {
847
+ const connection = resolveRepositoryConnection();
848
+ const value = connection[property];
849
+ return typeof value === "function" ? value.bind(connection) : value;
850
+ }
851
+ });
852
+ });
808
853
 
809
854
  // ../../src/core/database/transaction.ts
810
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
855
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
811
856
  async function runInSavepoint(connection, operation) {
812
857
  const inherited = transactionFrame.getStore();
813
858
  let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
@@ -867,7 +912,7 @@ async function runInTransaction(operation, options = {}) {
867
912
  signal?.throwIfAborted();
868
913
  if (!supportsTransactions(connection))
869
914
  throw new Error("Reserved database connection does not support transactions.");
870
- return await connection.begin(AsyncLocalStorage2.bind(async (transaction) => {
915
+ return await connection.begin(AsyncLocalStorage3.bind(async (transaction) => {
871
916
  return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
872
917
  }));
873
918
  } finally {
@@ -890,7 +935,7 @@ var init_transaction = __esm(() => {
890
935
  });
891
936
 
892
937
  // ../../src/core/tenant/databaseTenantContext.ts
893
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
938
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
894
939
  async function applyBypassToTransaction(transaction, bypass) {
895
940
  await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
896
941
  bypass ? "true" : "false"
@@ -942,7 +987,7 @@ async function runWithScopedTenantTransaction(apply, callback) {
942
987
  throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
943
988
  }
944
989
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
945
- return await begin.bind(pool)(AsyncLocalStorage3.bind(async (transaction) => {
990
+ return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
946
991
  await apply(transaction);
947
992
  return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
948
993
  }));
@@ -964,51 +1009,6 @@ var init_databaseTenantContext = __esm(() => {
964
1009
  init_tenancyConfig();
965
1010
  });
966
1011
 
967
- // ../../src/core/tenant/tenantDatabaseScope.ts
968
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
969
- async function applyTenantContextToTransaction(transaction, tenantId) {
970
- await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
971
- await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
972
- }
973
- async function runWithTenantDatabase(tenant, callback) {
974
- const outerTenant = currentTenant();
975
- if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
976
- throw new Error("Cannot switch tenants inside an active database transaction.");
977
- }
978
- if (!isTenancyEnabled() || !isRlsTenancy()) {
979
- return await runWithTenant(tenant, callback);
980
- }
981
- if (hasActiveDatabaseConnection()) {
982
- const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
983
- return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
984
- }
985
- const pool = getDefaultDatabasePool();
986
- const begin = pool.begin;
987
- if (typeof begin !== "function") {
988
- throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
989
- }
990
- return await settleTransaction(() => runWithDeferredModelEvents(async () => {
991
- return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
992
- await applyTenantContextToTransaction(transaction, tenant.id);
993
- return await runWithDatabaseConnection(transaction, async () => {
994
- return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
995
- });
996
- }));
997
- }));
998
- }
999
- function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
1000
- return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
1001
- }
1002
- var init_tenantDatabaseScope = __esm(() => {
1003
- init_defaultConnection();
1004
- init_connectionContext();
1005
- init_transactionControl();
1006
- init_deferredModelEvents();
1007
- init_databaseTenantContext();
1008
- init_tenancyConfig();
1009
- init_tenantContext();
1010
- });
1011
-
1012
1012
  // ../../src/core/admin/formatValue.ts
1013
1013
  function formatAdminValue(value, type = "text") {
1014
1014
  if (value === null || value === undefined) {
@@ -1304,6 +1304,7 @@ function resolveAuthUserDirectory(container) {
1304
1304
 
1305
1305
  // ../../src/core/auth/basicAuthGuard.ts
1306
1306
  init_boundConnection();
1307
+ init_connectionContext();
1307
1308
  init_defaultConnection();
1308
1309
 
1309
1310
  // ../../src/core/http/statelessAuth.ts
@@ -1344,6 +1345,9 @@ function readBasicCredentials(request) {
1344
1345
  }
1345
1346
  }
1346
1347
 
1348
+ // ../../src/core/auth/basicAuthGuard.ts
1349
+ init_databaseTenantContext();
1350
+
1347
1351
  // ../../src/core/auth/password.ts
1348
1352
  async function hashPassword(password) {
1349
1353
  return await Bun.password.hash(password, {
@@ -1759,11 +1763,17 @@ async function persistConsumedRecoveryHash(sql, userId, currentRaw, consumedHash
1759
1763
  if (!consumedHash) {
1760
1764
  return;
1761
1765
  }
1766
+ const hashes = parseRecoveryHashes(currentRaw);
1767
+ if (typeof currentRaw !== "string" || !hashes.includes(consumedHash)) {
1768
+ throw new UnprocessableEntityError("mfa_invalid");
1769
+ }
1762
1770
  const dialect = currentSqlDialect();
1763
- await sql.unsafe(`UPDATE users SET mfa_recovery_codes = ${dialect.placeholder(1)} WHERE id = ${dialect.placeholder(2)}`, [
1764
- JSON.stringify(parseRecoveryHashes(currentRaw).filter((hash) => hash !== consumedHash)),
1765
- userId
1766
- ]);
1771
+ const returning = dialect.returningClause("id");
1772
+ const rows = await sql.unsafe(`UPDATE users SET mfa_recovery_codes = ${dialect.placeholder(1)} WHERE id = ${dialect.placeholder(2)} AND mfa_recovery_codes = ${dialect.placeholder(3)}${returning}`, [JSON.stringify(hashes.filter((hash) => hash !== consumedHash)), userId, currentRaw]);
1773
+ const header = rows[0];
1774
+ const affected = returning ? rows.length : typeof header === "object" && header !== null && ("affectedRows" in header) ? Number(header.affectedRows) : 0;
1775
+ if (affected !== 1)
1776
+ throw new UnprocessableEntityError("mfa_invalid");
1767
1777
  }
1768
1778
 
1769
1779
  // ../../src/core/auth/basicAuthGuard.ts
@@ -1772,6 +1782,18 @@ class BasicAuthGuard {
1772
1782
  constructor(container) {
1773
1783
  this.container = container;
1774
1784
  }
1785
+ async persistRecovery(record, hash) {
1786
+ try {
1787
+ await runWithMigrationBypassForIdentifier(record.id, async () => {
1788
+ await persistConsumedRecoveryHash(getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabasePool()), record.id, record.mfa_recovery_codes, hash);
1789
+ });
1790
+ return true;
1791
+ } catch (error) {
1792
+ if (error instanceof UnprocessableEntityError && error.message === "mfa_invalid")
1793
+ return false;
1794
+ throw error;
1795
+ }
1796
+ }
1775
1797
  async resolve(request) {
1776
1798
  const credentials = readBasicCredentials(request);
1777
1799
  if (!credentials) {
@@ -1793,7 +1815,8 @@ class BasicAuthGuard {
1793
1815
  return null;
1794
1816
  }
1795
1817
  if (mfa.consumedRecoveryHash) {
1796
- await persistConsumedRecoveryHash(getBoundDatabaseConnection() ?? getDefaultDatabasePool(), record.id, record.mfa_recovery_codes, mfa.consumedRecoveryHash);
1818
+ if (!await this.persistRecovery(record, mfa.consumedRecoveryHash))
1819
+ return null;
1797
1820
  }
1798
1821
  return {
1799
1822
  id: record.id,
@@ -1821,7 +1844,8 @@ class BasicAuthGuard {
1821
1844
  return null;
1822
1845
  }
1823
1846
  if (mfa.consumedRecoveryHash) {
1824
- await persistConsumedRecoveryHash(getBoundDatabaseConnection() ?? getDefaultDatabasePool(), record.id, record.mfa_recovery_codes, mfa.consumedRecoveryHash);
1847
+ if (!await this.persistRecovery(record, mfa.consumedRecoveryHash))
1848
+ return null;
1825
1849
  }
1826
1850
  return user;
1827
1851
  }