@getstrata/core 2.2.28 → 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/CHANGELOG.md +4 -0
- package/dist/core/auth/basicAuthGuard.d.ts +1 -0
- package/dist/entries/auth/basicAuthGuard.js +622 -6
- package/dist/entries/auth/passwordLogin.js +27 -4
- package/dist/index.js +541 -517
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.29
|
|
4
|
+
|
|
5
|
+
MFA recovery-code persistence atomically claims the validated snapshot and rejects stale requests before credential issuance. Basic authentication fails closed and consumes under the verified RLS identifier scope. Adds native SQLite/Postgres/MySQL concurrency and restricted-role regressions.
|
|
6
|
+
|
|
3
7
|
## 2.2.28
|
|
4
8
|
|
|
5
9
|
Generated JSON and HTML password resets validate input before consuming signed links and atomically commit token consumption, password updates and credential revocation. Adds real generated-app failure, retry, replay and cookie/token/JWT revocation regressions.
|
|
@@ -4,6 +4,7 @@ import type { AuthGuard } from "./guard";
|
|
|
4
4
|
declare class BasicAuthGuard implements AuthGuard {
|
|
5
5
|
private readonly container;
|
|
6
6
|
constructor(container: ServiceContainerLike);
|
|
7
|
+
private persistRecovery;
|
|
7
8
|
resolve(request: Request): Promise<AuthUser | null>;
|
|
8
9
|
}
|
|
9
10
|
export { BasicAuthGuard };
|
|
@@ -99,6 +99,371 @@ var init_defaultConnection = __esm(() => {
|
|
|
99
99
|
DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
103
|
+
function runWithTenant(tenant, callback) {
|
|
104
|
+
return tenantContext.run(tenant, callback);
|
|
105
|
+
}
|
|
106
|
+
function currentTenant() {
|
|
107
|
+
return tenantContext.getStore() ?? null;
|
|
108
|
+
}
|
|
109
|
+
var tenantContext;
|
|
110
|
+
var init_tenantContext = __esm(() => {
|
|
111
|
+
init_asyncContextStore();
|
|
112
|
+
tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ../../src/core/events/eventBus.ts
|
|
116
|
+
class EventBus {
|
|
117
|
+
constructor() {}
|
|
118
|
+
transactionalListeners = new Map;
|
|
119
|
+
listenTransactional(event, listener) {
|
|
120
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
121
|
+
handlers.add(listener);
|
|
122
|
+
this.transactionalListeners.set(event, handlers);
|
|
123
|
+
return () => {
|
|
124
|
+
handlers.delete(listener);
|
|
125
|
+
if (!handlers.size)
|
|
126
|
+
this.transactionalListeners.delete(event);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
hasTransactionalListeners(event) {
|
|
130
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
131
|
+
}
|
|
132
|
+
async dispatchTransactional(event, payload) {
|
|
133
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
134
|
+
await listener(payload);
|
|
135
|
+
}
|
|
136
|
+
listeners = new Map;
|
|
137
|
+
on(event, listener) {
|
|
138
|
+
return this.listen(event, listener);
|
|
139
|
+
}
|
|
140
|
+
listen(event, listener) {
|
|
141
|
+
const handlers = this.listeners.get(event) ?? new Set;
|
|
142
|
+
handlers.add(listener);
|
|
143
|
+
this.listeners.set(event, handlers);
|
|
144
|
+
return () => {
|
|
145
|
+
handlers.delete(listener);
|
|
146
|
+
if (handlers.size === 0) {
|
|
147
|
+
this.listeners.delete(event);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async emit(event, payload) {
|
|
152
|
+
await this.dispatch(event, payload);
|
|
153
|
+
}
|
|
154
|
+
async dispatch(event, payload) {
|
|
155
|
+
const handlers = this.listeners.get(event);
|
|
156
|
+
if (!handlers || handlers.size === 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const handler of handlers) {
|
|
160
|
+
await handler(payload);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function readSharedEventBus() {
|
|
165
|
+
const globalBus = globalThis[EVENT_BUS_KEY];
|
|
166
|
+
if (globalBus) {
|
|
167
|
+
return globalBus;
|
|
168
|
+
}
|
|
169
|
+
const bus = new EventBus;
|
|
170
|
+
globalThis[EVENT_BUS_KEY] = bus;
|
|
171
|
+
return bus;
|
|
172
|
+
}
|
|
173
|
+
var EVENT_BUS_KEY, eventBus;
|
|
174
|
+
var init_eventBus = __esm(() => {
|
|
175
|
+
EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
|
|
176
|
+
eventBus = readSharedEventBus();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ../../src/core/database/transactionControl.ts
|
|
180
|
+
function unwrapTransactionRollback(error, depth = 0) {
|
|
181
|
+
if (error instanceof TransactionRollback) {
|
|
182
|
+
return error;
|
|
183
|
+
}
|
|
184
|
+
if (depth >= 5 || !(error instanceof Error) || error.cause === undefined) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return unwrapTransactionRollback(error.cause, depth + 1);
|
|
188
|
+
}
|
|
189
|
+
function hasActiveTransaction() {
|
|
190
|
+
return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
|
|
191
|
+
}
|
|
192
|
+
function requestTransactionRollback() {
|
|
193
|
+
const state = transactionScopes.getStore();
|
|
194
|
+
const index = state ? state.rollback.length - 1 : -1;
|
|
195
|
+
if (!state || index < 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const flags = state.rollback;
|
|
199
|
+
const current = flags[index];
|
|
200
|
+
if (current === undefined) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
flags[index] = true;
|
|
204
|
+
}
|
|
205
|
+
function scopeRequestsRollback() {
|
|
206
|
+
const state = transactionScopes.getStore();
|
|
207
|
+
if (!state || state.rollback.length === 0) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return state.rollback[state.rollback.length - 1] === true;
|
|
211
|
+
}
|
|
212
|
+
async function commitOrRollbackScope(result) {
|
|
213
|
+
if (scopeRequestsRollback()) {
|
|
214
|
+
throw new TransactionRollback(result);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
async function settleTransaction(work) {
|
|
219
|
+
try {
|
|
220
|
+
return await work();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const rollback = unwrapTransactionRollback(error);
|
|
223
|
+
if (rollback) {
|
|
224
|
+
return rollback.result;
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function runWithTransactionScope(callback) {
|
|
230
|
+
const existing = transactionScopes.getStore();
|
|
231
|
+
if (existing) {
|
|
232
|
+
existing.rollback.push(false);
|
|
233
|
+
try {
|
|
234
|
+
return await callback();
|
|
235
|
+
} finally {
|
|
236
|
+
existing.rollback.pop();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const state = { rollback: [false] };
|
|
240
|
+
return await transactionScopes.run(state, callback);
|
|
241
|
+
}
|
|
242
|
+
var TransactionRollback, transactionScopes;
|
|
243
|
+
var init_transactionControl = __esm(() => {
|
|
244
|
+
init_asyncContextStore();
|
|
245
|
+
init_connectionContext();
|
|
246
|
+
TransactionRollback = class TransactionRollback extends Error {
|
|
247
|
+
result;
|
|
248
|
+
constructor(result) {
|
|
249
|
+
super("Transaction rolled back");
|
|
250
|
+
this.name = "TransactionRollback";
|
|
251
|
+
this.result = result;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ../../src/core/tenant/tenancyConfig.ts
|
|
258
|
+
function readTenancyDriver(env = process.env) {
|
|
259
|
+
const raw = env.TENANCY_DRIVER?.trim();
|
|
260
|
+
if (raw === undefined || raw === "") {
|
|
261
|
+
return "rls";
|
|
262
|
+
}
|
|
263
|
+
if (TENANCY_DRIVERS.includes(raw)) {
|
|
264
|
+
return raw;
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
267
|
+
}
|
|
268
|
+
function isTenancyEnabled(env = process.env) {
|
|
269
|
+
return readTenancyDriver(env) !== "none";
|
|
270
|
+
}
|
|
271
|
+
function isRlsTenancy(env = process.env) {
|
|
272
|
+
return readTenancyDriver(env) === "rls";
|
|
273
|
+
}
|
|
274
|
+
var TENANCY_DRIVERS;
|
|
275
|
+
var init_tenancyConfig = __esm(() => {
|
|
276
|
+
TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
280
|
+
import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
|
|
281
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
282
|
+
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
283
|
+
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
284
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
285
|
+
}
|
|
286
|
+
async function runWithTenantDatabase(tenant, callback) {
|
|
287
|
+
const outerTenant = currentTenant();
|
|
288
|
+
if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
|
|
289
|
+
throw new Error("Cannot switch tenants inside an active database transaction.");
|
|
290
|
+
}
|
|
291
|
+
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
292
|
+
return await runWithTenant(tenant, callback);
|
|
293
|
+
}
|
|
294
|
+
if (hasActiveDatabaseConnection()) {
|
|
295
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool2());
|
|
296
|
+
return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
|
|
297
|
+
}
|
|
298
|
+
const pool = getDefaultDatabasePool2();
|
|
299
|
+
const begin = pool.begin;
|
|
300
|
+
if (typeof begin !== "function") {
|
|
301
|
+
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
302
|
+
}
|
|
303
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
304
|
+
return await begin.bind(pool)(AsyncLocalStorage2.bind(async (transaction) => {
|
|
305
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
306
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
307
|
+
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
308
|
+
});
|
|
309
|
+
}));
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
313
|
+
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
314
|
+
}
|
|
315
|
+
var init_tenantDatabaseScope = __esm(() => {
|
|
316
|
+
init_connectionContext();
|
|
317
|
+
init_transactionControl();
|
|
318
|
+
init_deferredModelEvents();
|
|
319
|
+
init_databaseTenantContext();
|
|
320
|
+
init_tenancyConfig();
|
|
321
|
+
init_tenantContext();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ../../src/core/events/deferredModelEvents.ts
|
|
325
|
+
function sameTenant(left, right) {
|
|
326
|
+
if (left === null && right === null) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (left === null || right === null) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
return left.id === right.id;
|
|
333
|
+
}
|
|
334
|
+
function groupQueuedModelEvents(queued) {
|
|
335
|
+
const groups = [];
|
|
336
|
+
for (const item of queued) {
|
|
337
|
+
const lastGroup = groups[groups.length - 1];
|
|
338
|
+
const last = lastGroup?.[lastGroup.length - 1];
|
|
339
|
+
if (!lastGroup || !last || !sameTenant(last.tenant, item.tenant)) {
|
|
340
|
+
groups.push([item]);
|
|
341
|
+
} else {
|
|
342
|
+
lastGroup.push(item);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return groups;
|
|
346
|
+
}
|
|
347
|
+
async function dispatchEventBatch(batch) {
|
|
348
|
+
for (const item of batch) {
|
|
349
|
+
await eventBus.dispatch(item.event, item.payload);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function dispatchQueuedModelEvents(queued) {
|
|
353
|
+
if (queued.length === 0) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
await Promise.resolve().then(() => init_tenantDatabaseScope());
|
|
357
|
+
for (const batch of groupQueuedModelEvents(queued)) {
|
|
358
|
+
const tenant = batch[0]?.tenant ?? null;
|
|
359
|
+
if (tenant) {
|
|
360
|
+
await runWithTenantDatabase(tenant, () => dispatchEventBatch(batch));
|
|
361
|
+
} else {
|
|
362
|
+
await dispatchEventBatch(batch);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function runWithDeferredModelEvents(callback) {
|
|
367
|
+
const existing = deferredModelEvents.getStore();
|
|
368
|
+
if (existing) {
|
|
369
|
+
existing.frames.push([]);
|
|
370
|
+
existing.rollbackFrames.push([]);
|
|
371
|
+
try {
|
|
372
|
+
const result = await callback();
|
|
373
|
+
const nested = existing.frames.pop() ?? [];
|
|
374
|
+
const rollback = existing.rollbackFrames.pop() ?? [];
|
|
375
|
+
existing.rollbackFrames.at(-1)?.push(...rollback);
|
|
376
|
+
const parent = existing.frames[existing.frames.length - 1];
|
|
377
|
+
if (parent) {
|
|
378
|
+
parent.push(...nested);
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
existing.frames.pop();
|
|
383
|
+
for (const rollback of (existing.rollbackFrames.pop() ?? []).reverse())
|
|
384
|
+
rollback();
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
389
|
+
let result;
|
|
390
|
+
try {
|
|
391
|
+
result = await deferredModelEvents.run(state, callback);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
394
|
+
rollback();
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
var deferredModelEvents;
|
|
401
|
+
var init_deferredModelEvents = __esm(() => {
|
|
402
|
+
init_asyncContextStore();
|
|
403
|
+
init_tenantContext();
|
|
404
|
+
init_eventBus();
|
|
405
|
+
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ../../src/core/database/acquisitionMetrics.ts
|
|
409
|
+
async function observeTransactionAcquisition(metrics, acquire, signal) {
|
|
410
|
+
const state = states.get(metrics);
|
|
411
|
+
if (!state)
|
|
412
|
+
throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
|
|
413
|
+
const started = performance.now();
|
|
414
|
+
state.inflight++;
|
|
415
|
+
let outcome = "acquired";
|
|
416
|
+
try {
|
|
417
|
+
return await acquire();
|
|
418
|
+
} catch (error) {
|
|
419
|
+
outcome = signal?.aborted ? "aborted" : "failed";
|
|
420
|
+
throw error;
|
|
421
|
+
} finally {
|
|
422
|
+
state.inflight--;
|
|
423
|
+
const elapsed = Math.max(0, performance.now() - started) / 1000;
|
|
424
|
+
const histogram = state.outcomes[outcomes.indexOf(outcome)];
|
|
425
|
+
if (histogram) {
|
|
426
|
+
histogram.count++;
|
|
427
|
+
histogram.sumSeconds += elapsed;
|
|
428
|
+
for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
|
|
429
|
+
const bound = ACQUISITION_BUCKETS_SECONDS[index];
|
|
430
|
+
if (bound !== undefined && elapsed <= bound)
|
|
431
|
+
histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
|
|
437
|
+
var init_acquisitionMetrics = __esm(() => {
|
|
438
|
+
ACQUISITION_BUCKETS_SECONDS = [
|
|
439
|
+
0.001,
|
|
440
|
+
0.005,
|
|
441
|
+
0.01,
|
|
442
|
+
0.025,
|
|
443
|
+
0.05,
|
|
444
|
+
0.1,
|
|
445
|
+
0.25,
|
|
446
|
+
0.5,
|
|
447
|
+
1,
|
|
448
|
+
2.5,
|
|
449
|
+
5,
|
|
450
|
+
10
|
|
451
|
+
];
|
|
452
|
+
outcomes = ["acquired", "failed", "aborted"];
|
|
453
|
+
metricsBrand = Symbol("transaction acquisition metrics");
|
|
454
|
+
states = new WeakMap;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ../../src/core/database/connection.ts
|
|
458
|
+
function createDatabaseConnection(source) {
|
|
459
|
+
return {
|
|
460
|
+
async unsafe(query, params = []) {
|
|
461
|
+
return await source.unsafe(query, params);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
var init_connection = () => {};
|
|
466
|
+
|
|
102
467
|
// ../../src/core/database/schema/driver.ts
|
|
103
468
|
function normalizeConnectionName(connection) {
|
|
104
469
|
const normalized = connection.trim().toLowerCase();
|
|
@@ -300,6 +665,216 @@ var init_dialect = __esm(() => {
|
|
|
300
665
|
dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
301
666
|
});
|
|
302
667
|
|
|
668
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
669
|
+
function resolveRepositoryConnection() {
|
|
670
|
+
return getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabaseQuery());
|
|
671
|
+
}
|
|
672
|
+
function compileTaggedSql(strings, values) {
|
|
673
|
+
const dialect = currentSqlDialect();
|
|
674
|
+
let query = strings[0] ?? "";
|
|
675
|
+
const params = [];
|
|
676
|
+
for (let index = 0;index < values.length; index += 1) {
|
|
677
|
+
params.push(values[index]);
|
|
678
|
+
query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
|
|
679
|
+
}
|
|
680
|
+
return { query, params };
|
|
681
|
+
}
|
|
682
|
+
function isTaggedStrings(value) {
|
|
683
|
+
if (!Array.isArray(value) || typeof value[0] !== "string") {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
return typeof value.raw !== "undefined";
|
|
687
|
+
}
|
|
688
|
+
function runOnConnection(connection, args) {
|
|
689
|
+
if (typeof connection === "function") {
|
|
690
|
+
return connection(...args);
|
|
691
|
+
}
|
|
692
|
+
if (typeof connection.unsafe !== "function") {
|
|
693
|
+
throw new Error("Database connection cannot run SQL.");
|
|
694
|
+
}
|
|
695
|
+
const strings = args[0];
|
|
696
|
+
if (!isTaggedStrings(strings)) {
|
|
697
|
+
throw new Error("Database connection cannot run tagged SQL.");
|
|
698
|
+
}
|
|
699
|
+
const compiled = compileTaggedSql(strings, args.slice(1));
|
|
700
|
+
return connection.unsafe(compiled.query, compiled.params);
|
|
701
|
+
}
|
|
702
|
+
var repositoryConnection;
|
|
703
|
+
var init_repositoryConnection = __esm(() => {
|
|
704
|
+
init_boundConnection();
|
|
705
|
+
init_connectionContext();
|
|
706
|
+
init_defaultConnection();
|
|
707
|
+
init_dialect();
|
|
708
|
+
repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
709
|
+
apply(_target, _thisArg, args) {
|
|
710
|
+
return runOnConnection(resolveRepositoryConnection(), args);
|
|
711
|
+
},
|
|
712
|
+
get(_target, property) {
|
|
713
|
+
const connection = resolveRepositoryConnection();
|
|
714
|
+
const value = connection[property];
|
|
715
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// ../../src/core/database/transaction.ts
|
|
721
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
722
|
+
async function runInSavepoint(connection, operation) {
|
|
723
|
+
const inherited = transactionFrame.getStore();
|
|
724
|
+
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
725
|
+
if (!parent) {
|
|
726
|
+
parent = { connection, childActive: false };
|
|
727
|
+
outerFrames.set(connection, parent);
|
|
728
|
+
}
|
|
729
|
+
if (parent.childActive) {
|
|
730
|
+
throw new Error("Concurrent nested transactions on one connection are not supported. Await each transaction before starting the next.");
|
|
731
|
+
}
|
|
732
|
+
parent.childActive = true;
|
|
733
|
+
const name = `strata_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
734
|
+
try {
|
|
735
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
736
|
+
await connection.unsafe(`SAVEPOINT ${name}`);
|
|
737
|
+
try {
|
|
738
|
+
const result = await transactionFrame.run({ connection, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(connection)))));
|
|
739
|
+
await connection.unsafe(`RELEASE SAVEPOINT ${name}`);
|
|
740
|
+
return result;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
await connection.unsafe(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
743
|
+
await connection.unsafe(`RELEASE SAVEPOINT ${name}`);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}));
|
|
747
|
+
} finally {
|
|
748
|
+
parent.childActive = false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function supportsTransactions(connection) {
|
|
752
|
+
return typeof connection.begin === "function";
|
|
753
|
+
}
|
|
754
|
+
async function runInTransaction(operation, options = {}) {
|
|
755
|
+
const signal = options.acquisitionSignal;
|
|
756
|
+
signal?.throwIfAborted();
|
|
757
|
+
const admittedOperation = (connection) => {
|
|
758
|
+
signal?.throwIfAborted();
|
|
759
|
+
return operation(connection);
|
|
760
|
+
};
|
|
761
|
+
const pool = resolveRepositoryConnection();
|
|
762
|
+
if (hasActiveDatabaseConnection()) {
|
|
763
|
+
return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
|
|
764
|
+
}
|
|
765
|
+
if (!supportsTransactions(pool)) {
|
|
766
|
+
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
767
|
+
}
|
|
768
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
769
|
+
let reserved;
|
|
770
|
+
if (signal || options.acquisitionMetrics) {
|
|
771
|
+
if (!pool.reserve)
|
|
772
|
+
throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
|
|
773
|
+
const reserve = pool.reserve.bind(pool);
|
|
774
|
+
reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
|
|
775
|
+
}
|
|
776
|
+
const connection = reserved ?? pool;
|
|
777
|
+
try {
|
|
778
|
+
signal?.throwIfAborted();
|
|
779
|
+
if (!supportsTransactions(connection))
|
|
780
|
+
throw new Error("Reserved database connection does not support transactions.");
|
|
781
|
+
return await connection.begin(AsyncLocalStorage3.bind(async (transaction) => {
|
|
782
|
+
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
|
|
783
|
+
}));
|
|
784
|
+
} finally {
|
|
785
|
+
await reserved?.release();
|
|
786
|
+
}
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
var transactionFrame, outerFrames;
|
|
790
|
+
var init_transaction = __esm(() => {
|
|
791
|
+
init_deferredModelEvents();
|
|
792
|
+
init_asyncContextStore();
|
|
793
|
+
init_acquisitionMetrics();
|
|
794
|
+
init_connection();
|
|
795
|
+
init_connectionContext();
|
|
796
|
+
init_repositoryConnection();
|
|
797
|
+
init_transactionControl();
|
|
798
|
+
init_acquisitionMetrics();
|
|
799
|
+
transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
|
|
800
|
+
outerFrames = new WeakMap;
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
804
|
+
import { getDefaultDatabasePool as getDefaultDatabasePool3 } from "@getstrata/core/database/defaultConnection";
|
|
805
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
806
|
+
async function applyBypassToTransaction(transaction, bypass) {
|
|
807
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
808
|
+
bypass ? "true" : "false"
|
|
809
|
+
]);
|
|
810
|
+
}
|
|
811
|
+
function stringifyBypassIdentifier(identifier) {
|
|
812
|
+
if (typeof identifier === "number") {
|
|
813
|
+
return String(identifier);
|
|
814
|
+
}
|
|
815
|
+
return identifier.trim();
|
|
816
|
+
}
|
|
817
|
+
async function applyIdentifierToTransaction(transaction, identifier) {
|
|
818
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_identifier', $1, true)`, [
|
|
819
|
+
stringifyBypassIdentifier(identifier)
|
|
820
|
+
]);
|
|
821
|
+
}
|
|
822
|
+
function assertBypassIdentifier(identifier) {
|
|
823
|
+
if (typeof identifier === "number") {
|
|
824
|
+
if (!Number.isInteger(identifier) || identifier <= 0) {
|
|
825
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (typeof identifier === "string" && identifier.trim() !== "") {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
833
|
+
}
|
|
834
|
+
async function runWithScopedTenantTransaction(apply, callback) {
|
|
835
|
+
if (!isRlsTenancy()) {
|
|
836
|
+
return await callback();
|
|
837
|
+
}
|
|
838
|
+
if (hasActiveDatabaseConnection()) {
|
|
839
|
+
const connection = getActiveDatabaseConnection(getDefaultDatabasePool3());
|
|
840
|
+
return await runInTransaction(async () => {
|
|
841
|
+
const [previous] = await connection.unsafe(`SELECT current_setting('app.tenant_id', true) AS tenant_id,
|
|
842
|
+
current_setting('app.bypass_rls', true) AS bypass_rls,
|
|
843
|
+
current_setting('app.bypass_identifier', true) AS bypass_identifier`);
|
|
844
|
+
await apply(connection);
|
|
845
|
+
const result = await callback();
|
|
846
|
+
await connection.unsafe(`SELECT set_config('app.tenant_id', $1, true),
|
|
847
|
+
set_config('app.bypass_rls', $2, true), set_config('app.bypass_identifier', $3, true)`, [previous?.tenant_id ?? "", previous?.bypass_rls ?? "", previous?.bypass_identifier ?? ""]);
|
|
848
|
+
return result;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const pool = getDefaultDatabasePool3();
|
|
852
|
+
const begin = pool.begin;
|
|
853
|
+
if (typeof begin !== "function") {
|
|
854
|
+
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
855
|
+
}
|
|
856
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
857
|
+
return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
|
|
858
|
+
await apply(transaction);
|
|
859
|
+
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
860
|
+
}));
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
async function runWithMigrationBypass(callback) {
|
|
864
|
+
return await runWithScopedTenantTransaction((transaction) => applyBypassToTransaction(transaction, true), callback);
|
|
865
|
+
}
|
|
866
|
+
async function runWithMigrationBypassForIdentifier(identifier, callback) {
|
|
867
|
+
assertBypassIdentifier(identifier);
|
|
868
|
+
return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
|
|
869
|
+
}
|
|
870
|
+
var init_databaseTenantContext = __esm(() => {
|
|
871
|
+
init_connectionContext();
|
|
872
|
+
init_transaction();
|
|
873
|
+
init_transactionControl();
|
|
874
|
+
init_deferredModelEvents();
|
|
875
|
+
init_tenancyConfig();
|
|
876
|
+
});
|
|
877
|
+
|
|
303
878
|
// ../../src/core/contracts/container.ts
|
|
304
879
|
function createServiceToken(key) {
|
|
305
880
|
if (!key.trim())
|
|
@@ -428,8 +1003,26 @@ function resolveAuthUserDirectory(container) {
|
|
|
428
1003
|
|
|
429
1004
|
// ../../src/core/auth/basicAuthGuard.ts
|
|
430
1005
|
init_boundConnection();
|
|
1006
|
+
init_connectionContext();
|
|
431
1007
|
init_defaultConnection();
|
|
432
1008
|
|
|
1009
|
+
// ../../src/core/errors/http.ts
|
|
1010
|
+
class HttpError extends Error {
|
|
1011
|
+
status;
|
|
1012
|
+
details;
|
|
1013
|
+
constructor(status, message, details) {
|
|
1014
|
+
super(message);
|
|
1015
|
+
this.name = new.target.name;
|
|
1016
|
+
this.status = status;
|
|
1017
|
+
this.details = details;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
class UnprocessableEntityError extends HttpError {
|
|
1021
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
1022
|
+
super(422, message, details);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
433
1026
|
// ../../src/core/http/statelessAuth.ts
|
|
434
1027
|
function authorizationScheme(request) {
|
|
435
1028
|
const header = request.headers.get("authorization")?.trim() ?? "";
|
|
@@ -472,6 +1065,9 @@ function readBasicCredentials(request) {
|
|
|
472
1065
|
}
|
|
473
1066
|
}
|
|
474
1067
|
|
|
1068
|
+
// ../../src/core/auth/basicAuthGuard.ts
|
|
1069
|
+
init_databaseTenantContext();
|
|
1070
|
+
|
|
475
1071
|
// ../../src/core/auth/password.ts
|
|
476
1072
|
async function hashPassword(password) {
|
|
477
1073
|
return await Bun.password.hash(password, {
|
|
@@ -986,11 +1582,17 @@ async function persistConsumedRecoveryHash(sql, userId, currentRaw, consumedHash
|
|
|
986
1582
|
if (!consumedHash) {
|
|
987
1583
|
return;
|
|
988
1584
|
}
|
|
1585
|
+
const hashes = parseRecoveryHashes(currentRaw);
|
|
1586
|
+
if (typeof currentRaw !== "string" || !hashes.includes(consumedHash)) {
|
|
1587
|
+
throw new UnprocessableEntityError("mfa_invalid");
|
|
1588
|
+
}
|
|
989
1589
|
const dialect = currentSqlDialect();
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1590
|
+
const returning = dialect.returningClause("id");
|
|
1591
|
+
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]);
|
|
1592
|
+
const header = rows[0];
|
|
1593
|
+
const affected = returning ? rows.length : typeof header === "object" && header !== null && ("affectedRows" in header) ? Number(header.affectedRows) : 0;
|
|
1594
|
+
if (affected !== 1)
|
|
1595
|
+
throw new UnprocessableEntityError("mfa_invalid");
|
|
994
1596
|
}
|
|
995
1597
|
|
|
996
1598
|
// ../../src/core/auth/basicAuthGuard.ts
|
|
@@ -999,6 +1601,18 @@ class BasicAuthGuard {
|
|
|
999
1601
|
constructor(container) {
|
|
1000
1602
|
this.container = container;
|
|
1001
1603
|
}
|
|
1604
|
+
async persistRecovery(record, hash) {
|
|
1605
|
+
try {
|
|
1606
|
+
await runWithMigrationBypassForIdentifier(record.id, async () => {
|
|
1607
|
+
await persistConsumedRecoveryHash(getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabasePool()), record.id, record.mfa_recovery_codes, hash);
|
|
1608
|
+
});
|
|
1609
|
+
return true;
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
if (error instanceof UnprocessableEntityError && error.message === "mfa_invalid")
|
|
1612
|
+
return false;
|
|
1613
|
+
throw error;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1002
1616
|
async resolve(request) {
|
|
1003
1617
|
const credentials = readBasicCredentials(request);
|
|
1004
1618
|
if (!credentials) {
|
|
@@ -1020,7 +1634,8 @@ class BasicAuthGuard {
|
|
|
1020
1634
|
return null;
|
|
1021
1635
|
}
|
|
1022
1636
|
if (mfa.consumedRecoveryHash) {
|
|
1023
|
-
|
|
1637
|
+
if (!await this.persistRecovery(record, mfa.consumedRecoveryHash))
|
|
1638
|
+
return null;
|
|
1024
1639
|
}
|
|
1025
1640
|
return {
|
|
1026
1641
|
id: record.id,
|
|
@@ -1048,7 +1663,8 @@ class BasicAuthGuard {
|
|
|
1048
1663
|
return null;
|
|
1049
1664
|
}
|
|
1050
1665
|
if (mfa.consumedRecoveryHash) {
|
|
1051
|
-
|
|
1666
|
+
if (!await this.persistRecovery(record, mfa.consumedRecoveryHash))
|
|
1667
|
+
return null;
|
|
1052
1668
|
}
|
|
1053
1669
|
return user;
|
|
1054
1670
|
}
|