@getstrata/core 1.1.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/core/database/transaction.d.ts +2 -1
  3. package/dist/core/database/transactionControl.d.ts +5 -0
  4. package/dist/core/events/deferredModelEvents.d.ts +3 -0
  5. package/dist/core/events/index.d.ts +1 -0
  6. package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
  7. package/dist/core/http/metricsMiddleware.d.ts +1 -2
  8. package/dist/core/http/requestMetaContext.d.ts +2 -0
  9. package/dist/core/http/throttleMiddleware.d.ts +11 -2
  10. package/dist/core/metrics/prometheus.d.ts +11 -1
  11. package/dist/core/queue/jobRunner.d.ts +3 -1
  12. package/dist/core/queue/redisQueue.d.ts +20 -3
  13. package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
  14. package/dist/entries/audit/exportAuditLogs.js +751 -97
  15. package/dist/entries/auth/accessControl.js +16 -2
  16. package/dist/entries/auth/basicAuthGuard.js +271 -224
  17. package/dist/entries/auth/membershipMiddleware.js +17 -0
  18. package/dist/entries/auth/membershipScope.js +21 -6
  19. package/dist/entries/auth/membershipService.js +20 -5
  20. package/dist/entries/auth/oauth/samlProvider.js +187 -142
  21. package/dist/entries/auth/oneTimeToken.js +142 -123
  22. package/dist/entries/auth/passwordLogin.js +212 -191
  23. package/dist/entries/auth/policy.js +16 -2
  24. package/dist/entries/auth/saml/samlServiceProvider.js +187 -142
  25. package/dist/entries/auth/scimAuthMiddleware.js +732 -64
  26. package/dist/entries/database/migrations.js +144 -125
  27. package/dist/entries/database/schema.js +58 -39
  28. package/dist/entries/http/csrfMiddleware.js +28 -14
  29. package/dist/entries/http/csrfToken.js +28 -14
  30. package/dist/entries/http/flashMiddleware.js +28 -14
  31. package/dist/entries/http/metricsMiddleware.js +124 -27
  32. package/dist/entries/http/response.js +107 -0
  33. package/dist/entries/http/routeMiddleware.js +40 -0
  34. package/dist/entries/http/throttleMiddleware.js +124 -11
  35. package/dist/entries/jobs/exportAuditLogsJob.js +751 -97
  36. package/dist/entries/metrics/prometheus.js +71 -11
  37. package/dist/entries/queue/createAppQueue.js +215 -21
  38. package/dist/entries/queue/jobRunner.js +3 -2
  39. package/dist/entries/queue/publicQueue.js +215 -21
  40. package/dist/entries/queue/queueMetrics.js +215 -21
  41. package/dist/entries/queue/redisQueue.js +223 -22
  42. package/dist/entries/runtime/asyncContextStore.js +16 -0
  43. package/dist/entries/tenant/databaseTenantContext.js +666 -11
  44. package/dist/entries/tenant/tenancyConfig.js +19 -1
  45. package/dist/entries/tenant/tenantDatabaseScope.js +688 -18
  46. package/dist/entries/tracing/tracingMiddleware.js +28 -14
  47. package/dist/framework/public-api.d.ts +3 -3
  48. package/dist/index.js +1344 -651
  49. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,825 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/runtime/asyncContextStore.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ function createAsyncContextStore(key) {
17
+ const symbol = Symbol.for(key);
18
+ const globalRecord = globalThis;
19
+ const existing = globalRecord[symbol];
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const store = new AsyncLocalStorage;
24
+ globalRecord[symbol] = store;
25
+ return store;
26
+ }
27
+ var init_asyncContextStore = () => {};
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
+
54
+ // ../../src/core/database/connectionContext.ts
55
+ function runWithDatabaseConnection(connection, callback) {
56
+ return activeConnection.run(connection, callback);
57
+ }
58
+ function getActiveDatabaseConnection(fallback) {
59
+ return activeConnection.getStore() ?? fallback;
60
+ }
61
+ function hasActiveDatabaseConnection() {
62
+ return activeConnection.getStore() !== undefined;
63
+ }
64
+ var activeConnection;
65
+ var init_connectionContext = __esm(() => {
66
+ init_asyncContextStore();
67
+ activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
68
+ });
69
+
70
+ // ../../src/core/database/queryProxy.ts
71
+ function createDatabaseQueryProxy(pool) {
72
+ function resolveDatabase() {
73
+ return getActiveDatabaseConnection(pool);
74
+ }
75
+ function resolveDatabaseForProperty(property) {
76
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
77
+ return pool;
78
+ }
79
+ return resolveDatabase();
80
+ }
81
+ return new Proxy(function database() {}, {
82
+ apply(_target, _thisArg, args) {
83
+ return resolveDatabase()(...args);
84
+ },
85
+ get(_target, property) {
86
+ const connection = resolveDatabaseForProperty(property);
87
+ const value = connection[property];
88
+ return typeof value === "function" ? value.bind(connection) : value;
89
+ }
90
+ });
91
+ }
92
+ var POOL_CONNECTION_METHODS;
93
+ var init_queryProxy = __esm(() => {
94
+ init_connectionContext();
95
+ POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
96
+ });
97
+
98
+ // ../../src/core/database/defaultConnection.ts
99
+ function defaultConnectionState() {
100
+ const globalRecord = globalThis;
101
+ const existing = globalRecord[DEFAULT_CONNECTION_KEY];
102
+ if (existing) {
103
+ return existing;
104
+ }
105
+ const created = { pool: null, query: null };
106
+ globalRecord[DEFAULT_CONNECTION_KEY] = created;
107
+ return created;
108
+ }
109
+ function registerDefaultDatabasePool(connection) {
110
+ const holder = defaultConnectionState();
111
+ holder.pool = connection;
112
+ holder.query = createDatabaseQueryProxy(connection);
113
+ }
114
+ function getDefaultDatabasePool() {
115
+ const pool = defaultConnectionState().pool;
116
+ if (!pool) {
117
+ throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
118
+ }
119
+ return pool;
120
+ }
121
+ function getDefaultDatabaseQuery() {
122
+ const query = defaultConnectionState().query;
123
+ if (!query) {
124
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
125
+ }
126
+ return query;
127
+ }
128
+ function resetDefaultDatabasePoolForTests() {
129
+ const holder = defaultConnectionState();
130
+ holder.pool = null;
131
+ holder.query = null;
132
+ }
133
+ var DEFAULT_CONNECTION_KEY;
134
+ var init_defaultConnection = __esm(() => {
135
+ init_queryProxy();
136
+ DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
137
+ });
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
+
335
+ // ../../src/core/tenant/tenantContext.ts
336
+ function runWithTenant(tenant, callback) {
337
+ return tenantContext.run(tenant, callback);
338
+ }
339
+ function currentTenant() {
340
+ return tenantContext.getStore() ?? null;
341
+ }
342
+ function currentTenantId() {
343
+ const tenant = currentTenant();
344
+ if (!tenant) {
345
+ throw new Error("Tenant context is required.");
346
+ }
347
+ return tenant.id;
348
+ }
349
+ function rateLimitMultiplierForPlan(plan) {
350
+ switch (plan) {
351
+ case "enterprise":
352
+ return 4;
353
+ case "pro":
354
+ return 2;
355
+ default:
356
+ return 1;
357
+ }
358
+ }
359
+ var tenantContext;
360
+ var init_tenantContext = __esm(() => {
361
+ init_asyncContextStore();
362
+ tenantContext = createAsyncContextStore("@getstrata/tenantContext");
363
+ });
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
+
417
+ // ../../src/core/tenant/tenancyConfig.ts
418
+ function readTenancyDriver(env = process.env) {
419
+ const raw = env.TENANCY_DRIVER?.trim();
420
+ if (raw === undefined || raw === "") {
421
+ return "rls";
422
+ }
423
+ if (TENANCY_DRIVERS.includes(raw)) {
424
+ return raw;
425
+ }
426
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
427
+ }
428
+ function isTenancyEnabled(env = process.env) {
429
+ return readTenancyDriver(env) !== "none";
430
+ }
431
+ function isRlsTenancy(env = process.env) {
432
+ return readTenancyDriver(env) === "rls";
433
+ }
434
+ var TENANCY_DRIVERS;
435
+ var init_tenancyConfig = __esm(() => {
436
+ TENANCY_DRIVERS = ["rls", "column", "none"];
437
+ });
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
+
513
+ // ../../src/core/events/eventBus.ts
514
+ class EventBus {
515
+ constructor() {}
516
+ listeners = new Map;
517
+ on(event, listener) {
518
+ return this.listen(event, listener);
519
+ }
520
+ listen(event, listener) {
521
+ const handlers = this.listeners.get(event) ?? new Set;
522
+ handlers.add(listener);
523
+ this.listeners.set(event, handlers);
524
+ return () => {
525
+ handlers.delete(listener);
526
+ if (handlers.size === 0) {
527
+ this.listeners.delete(event);
528
+ }
529
+ };
530
+ }
531
+ async emit(event, payload) {
532
+ await this.dispatch(event, payload);
533
+ }
534
+ async dispatch(event, payload) {
535
+ const handlers = this.listeners.get(event);
536
+ if (!handlers || handlers.size === 0) {
537
+ return;
538
+ }
539
+ for (const handler of handlers) {
540
+ await handler(payload);
541
+ }
542
+ }
543
+ }
544
+ function readSharedEventBus() {
545
+ const globalBus = globalThis[EVENT_BUS_KEY];
546
+ if (globalBus) {
547
+ return globalBus;
548
+ }
549
+ const bus = new EventBus;
550
+ globalThis[EVENT_BUS_KEY] = bus;
551
+ return bus;
552
+ }
553
+ var EVENT_BUS_KEY, eventBus;
554
+ var init_eventBus = __esm(() => {
555
+ EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
556
+ eventBus = readSharedEventBus();
557
+ });
558
+
559
+ // ../../src/core/events/deferredModelEvents.ts
560
+ function sameTenant(left, right) {
561
+ if (left === null && right === null) {
562
+ return true;
563
+ }
564
+ if (left === null || right === null) {
565
+ return false;
566
+ }
567
+ return left.id === right.id;
568
+ }
569
+ function groupQueuedModelEvents(queued) {
570
+ const groups = [];
571
+ for (const item of queued) {
572
+ const lastGroup = groups[groups.length - 1];
573
+ const last = lastGroup?.[lastGroup.length - 1];
574
+ if (!lastGroup || !last || !sameTenant(last.tenant, item.tenant)) {
575
+ groups.push([item]);
576
+ } else {
577
+ lastGroup.push(item);
578
+ }
579
+ }
580
+ return groups;
581
+ }
582
+ async function dispatchEventBatch(batch) {
583
+ for (const item of batch) {
584
+ await eventBus.dispatch(item.event, item.payload);
585
+ }
586
+ }
587
+ async function dispatchQueuedModelEvents(queued) {
588
+ if (queued.length === 0) {
589
+ return;
590
+ }
591
+ await Promise.resolve().then(() => init_tenantDatabaseScope());
592
+ for (const batch of groupQueuedModelEvents(queued)) {
593
+ const tenant = batch[0]?.tenant ?? null;
594
+ if (tenant) {
595
+ await runWithTenantDatabase(tenant, () => dispatchEventBatch(batch));
596
+ } else {
597
+ await dispatchEventBatch(batch);
598
+ }
599
+ }
600
+ }
601
+ async function dispatchModelEvent(event, payload) {
602
+ const state = deferredModelEvents.getStore();
603
+ const frame = state?.frames[state.frames.length - 1];
604
+ if (frame) {
605
+ frame.push({
606
+ event,
607
+ payload,
608
+ tenant: currentTenant()
609
+ });
610
+ return;
611
+ }
612
+ await eventBus.dispatch(event, payload);
613
+ }
614
+ async function runWithDeferredModelEvents(callback) {
615
+ const existing = deferredModelEvents.getStore();
616
+ if (existing) {
617
+ existing.frames.push([]);
618
+ try {
619
+ const result = await callback();
620
+ const nested = existing.frames.pop() ?? [];
621
+ const parent = existing.frames[existing.frames.length - 1];
622
+ if (parent) {
623
+ parent.push(...nested);
624
+ }
625
+ return result;
626
+ } catch (error) {
627
+ existing.frames.pop();
628
+ throw error;
629
+ }
630
+ }
631
+ const state = { frames: [[]] };
632
+ const result = await deferredModelEvents.run(state, callback);
633
+ await dispatchQueuedModelEvents(state.frames[0] ?? []);
634
+ return result;
635
+ }
636
+ var deferredModelEvents;
637
+ var init_deferredModelEvents = __esm(() => {
638
+ init_asyncContextStore();
639
+ init_tenantContext();
640
+ init_eventBus();
641
+ deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
642
+ });
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
+
779
+ // ../../src/core/tenant/tenantDatabaseScope.ts
780
+ async function applyTenantContextToTransaction(transaction, tenantId) {
781
+ await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
782
+ await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
783
+ }
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
+ }
789
+ if (!isTenancyEnabled() || !isRlsTenancy()) {
790
+ return await runWithTenant(tenant, callback);
791
+ }
792
+ if (hasActiveDatabaseConnection()) {
793
+ const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
794
+ return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
795
+ }
796
+ const pool = getDefaultDatabasePool();
797
+ const begin = pool.begin;
798
+ if (typeof begin !== "function") {
799
+ throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
800
+ }
801
+ return await settleTransaction(() => runWithDeferredModelEvents(async () => {
802
+ return await begin.bind(pool)(async (transaction) => {
803
+ await applyTenantContextToTransaction(transaction, tenant.id);
804
+ return await runWithDatabaseConnection(transaction, async () => {
805
+ return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
806
+ });
807
+ });
808
+ }));
809
+ }
810
+ function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
811
+ return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
812
+ }
813
+ var init_tenantDatabaseScope = __esm(() => {
814
+ init_defaultConnection();
815
+ init_connectionContext();
816
+ init_transactionControl();
817
+ init_deferredModelEvents();
818
+ init_databaseTenantContext();
819
+ init_tenancyConfig();
820
+ init_tenantContext();
821
+ });
822
+
2
823
  // ../../src/core/admin/formatValue.ts
3
824
  function formatAdminValue(value, type = "text") {
4
825
  if (value === null || value === undefined) {
@@ -143,21 +964,8 @@ function toHttpError(error) {
143
964
  return new HttpError(error.status, error.message, error.details);
144
965
  }
145
966
 
146
- // ../../src/core/runtime/asyncContextStore.ts
147
- import { AsyncLocalStorage } from "async_hooks";
148
- function createAsyncContextStore(key) {
149
- const symbol = Symbol.for(key);
150
- const globalRecord = globalThis;
151
- const existing = globalRecord[symbol];
152
- if (existing) {
153
- return existing;
154
- }
155
- const store = new AsyncLocalStorage;
156
- globalRecord[symbol] = store;
157
- return store;
158
- }
159
-
160
967
  // ../../src/core/auth/authContext.ts
968
+ init_asyncContextStore();
161
969
  var authContext = createAsyncContextStore("@getstrata/authContext");
162
970
  var credentialSourceContext = createAsyncContextStore("@getstrata/credentialSource");
163
971
  function runWithAuthUser(user, callback) {
@@ -188,137 +996,46 @@ function hasMinimumOrgRole(role, minimum) {
188
996
  }
189
997
  return ROLE_RANK[role] >= ROLE_RANK[minimum];
190
998
  }
191
- function resolveUserId(user) {
192
- const userId = typeof user.id === "number" ? user.id : Number(user.id);
193
- if (!Number.isInteger(userId) || userId <= 0) {
194
- throw new ForbiddenError("Invalid authenticated user.");
195
- }
196
- return userId;
197
- }
198
- // ../../src/core/contracts/serviceTokens.ts
199
- var CORE_QUEUE_TOKEN = "core.queue";
200
- var CORE_EVENT_BUS_TOKEN = "core.eventBus";
201
- var CORE_POLICY_GATE_TOKEN = "core.policyGate";
202
- var CORE_AUTH_TOKEN = "core.auth";
203
- var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
204
- var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
205
- function isAuthUserDirectory(value) {
206
- if (!value || typeof value !== "object") {
207
- return false;
208
- }
209
- return typeof value.findByIdOrThrow === "function";
210
- }
211
- function resolveAuthUserDirectory(container) {
212
- if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
213
- const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
214
- if (isAuthUserDirectory(directory)) {
215
- return directory;
216
- }
217
- }
218
- if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
219
- const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
220
- if (isAuthUserDirectory(tokenService)) {
221
- return tokenService;
222
- }
223
- }
224
- return null;
225
- }
226
-
227
- // ../../src/core/database/boundConnection.ts
228
- var BOUND_CONNECTION_KEY = Symbol.for("@getstrata/boundDatabaseConnection");
229
- function boundConnectionState() {
230
- const globalRecord = globalThis;
231
- const existing = globalRecord[BOUND_CONNECTION_KEY];
232
- if (existing) {
233
- return existing;
234
- }
235
- const created = { connection: null };
236
- globalRecord[BOUND_CONNECTION_KEY] = created;
237
- return created;
238
- }
239
- function bindDatabaseConnection(connection) {
240
- boundConnectionState().connection = connection;
241
- }
242
- function getBoundDatabaseConnection() {
243
- return boundConnectionState().connection;
244
- }
245
- function resetBoundDatabaseConnection() {
246
- boundConnectionState().connection = null;
247
- }
248
-
249
- // ../../src/core/database/connectionContext.ts
250
- var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
251
- function runWithDatabaseConnection(connection, callback) {
252
- return activeConnection.run(connection, callback);
253
- }
254
- function getActiveDatabaseConnection(fallback) {
255
- return activeConnection.getStore() ?? fallback;
256
- }
257
- function hasActiveDatabaseConnection() {
258
- return activeConnection.getStore() !== undefined;
259
- }
260
-
261
- // ../../src/core/database/queryProxy.ts
262
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
263
- function createDatabaseQueryProxy(pool) {
264
- function resolveDatabase() {
265
- return getActiveDatabaseConnection(pool);
266
- }
267
- function resolveDatabaseForProperty(property) {
268
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
269
- return pool;
270
- }
271
- return resolveDatabase();
272
- }
273
- return new Proxy(function database() {}, {
274
- apply(_target, _thisArg, args) {
275
- return resolveDatabase()(...args);
276
- },
277
- get(_target, property) {
278
- const connection = resolveDatabaseForProperty(property);
279
- const value = connection[property];
280
- return typeof value === "function" ? value.bind(connection) : value;
281
- }
282
- });
283
- }
284
-
285
- // ../../src/core/database/defaultConnection.ts
286
- var DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
287
- function defaultConnectionState() {
288
- const globalRecord = globalThis;
289
- const existing = globalRecord[DEFAULT_CONNECTION_KEY];
290
- if (existing) {
291
- return existing;
292
- }
293
- const created = { pool: null, query: null };
294
- globalRecord[DEFAULT_CONNECTION_KEY] = created;
295
- return created;
296
- }
297
- function registerDefaultDatabasePool(connection) {
298
- const holder = defaultConnectionState();
299
- holder.pool = connection;
300
- holder.query = createDatabaseQueryProxy(connection);
301
- }
302
- function getDefaultDatabasePool() {
303
- const pool = defaultConnectionState().pool;
304
- if (!pool) {
305
- throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
999
+ function resolveUserId(user) {
1000
+ const userId = typeof user.id === "number" ? user.id : Number(user.id);
1001
+ if (!Number.isInteger(userId) || userId <= 0) {
1002
+ throw new ForbiddenError("Invalid authenticated user.");
306
1003
  }
307
- return pool;
1004
+ return userId;
308
1005
  }
309
- function getDefaultDatabaseQuery() {
310
- const query = defaultConnectionState().query;
311
- if (!query) {
312
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
1006
+ // ../../src/core/contracts/serviceTokens.ts
1007
+ var CORE_QUEUE_TOKEN = "core.queue";
1008
+ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
1009
+ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
1010
+ var CORE_AUTH_TOKEN = "core.auth";
1011
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
1012
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
1013
+ function isAuthUserDirectory(value) {
1014
+ if (!value || typeof value !== "object") {
1015
+ return false;
313
1016
  }
314
- return query;
1017
+ return typeof value.findByIdOrThrow === "function";
315
1018
  }
316
- function resetDefaultDatabasePoolForTests() {
317
- const holder = defaultConnectionState();
318
- holder.pool = null;
319
- holder.query = null;
1019
+ function resolveAuthUserDirectory(container) {
1020
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
1021
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
1022
+ if (isAuthUserDirectory(directory)) {
1023
+ return directory;
1024
+ }
1025
+ }
1026
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
1027
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
1028
+ if (isAuthUserDirectory(tokenService)) {
1029
+ return tokenService;
1030
+ }
1031
+ }
1032
+ return null;
320
1033
  }
321
1034
 
1035
+ // ../../src/core/auth/basicAuthGuard.ts
1036
+ init_boundConnection();
1037
+ init_defaultConnection();
1038
+
322
1039
  // ../../src/core/http/statelessAuth.ts
323
1040
  function authorizationScheme(request) {
324
1041
  const header = request.headers.get("authorization")?.trim() ?? "";
@@ -432,196 +1149,8 @@ function revealMfaSecret(stored) {
432
1149
  return decryptField(stored, key);
433
1150
  }
434
1151
 
435
- // ../../src/core/database/schema/driver.ts
436
- function normalizeConnectionName(connection) {
437
- const normalized = connection.trim().toLowerCase();
438
- if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
439
- return "pgsql";
440
- }
441
- if (normalized === "mysql" || normalized === "mariadb") {
442
- return "mysql";
443
- }
444
- if (normalized === "sqlite") {
445
- return "sqlite";
446
- }
447
- throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
448
- }
449
- function resolveDriverFromUrl(url) {
450
- const normalized = url.trim().toLowerCase();
451
- if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
452
- return "pgsql";
453
- }
454
- if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
455
- return "mysql";
456
- }
457
- if (normalized.startsWith("sqlite:")) {
458
- return "sqlite";
459
- }
460
- return null;
461
- }
462
- function resolveDatabaseDriver(options = {}) {
463
- const connection = options.connection ?? process.env.DB_CONNECTION;
464
- if (connection) {
465
- return normalizeConnectionName(connection);
466
- }
467
- const url = options.url ?? process.env.DATABASE_URL ?? "";
468
- const fromUrl = resolveDriverFromUrl(url);
469
- if (fromUrl) {
470
- return fromUrl;
471
- }
472
- return "pgsql";
473
- }
474
-
475
- // ../../src/core/database/dialect.ts
476
- function assertSafeIdentifier(identifier) {
477
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
478
- throw new Error(`Invalid SQL identifier: ${identifier}`);
479
- }
480
- return identifier;
481
- }
482
- function quoteIdentifierFor(dialect, column) {
483
- return dialect.quoteIdentifier(column);
484
- }
485
- var postgresDialect = {
486
- driver: "pgsql",
487
- placeholder(index) {
488
- return `$${index}`;
489
- },
490
- quoteIdentifier(identifier) {
491
- return `"${assertSafeIdentifier(identifier)}"`;
492
- },
493
- nowExpression() {
494
- return "NOW()";
495
- },
496
- timestampValue(value) {
497
- return value.toISOString();
498
- },
499
- returningClause(columns) {
500
- return ` RETURNING ${columns}`;
501
- },
502
- ilikeOperator() {
503
- return "ILIKE";
504
- },
505
- nullsLastSuffix() {
506
- return " NULLS LAST";
507
- },
508
- castToText(expression) {
509
- return `${expression}::text`;
510
- },
511
- upsertSuffix(conflictColumns, updateColumns) {
512
- const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
513
- if (updateColumns.length === 0) {
514
- return ` ON CONFLICT (${target}) DO NOTHING`;
515
- }
516
- const assignments = updateColumns.map((column) => {
517
- const quoted = quoteIdentifierFor(this, column);
518
- return `${quoted} = excluded.${quoted}`;
519
- }).join(", ");
520
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
521
- }
522
- };
523
- var mysqlDialect = {
524
- driver: "mysql",
525
- placeholder() {
526
- return "?";
527
- },
528
- quoteIdentifier(identifier) {
529
- return `\`${assertSafeIdentifier(identifier)}\``;
530
- },
531
- nowExpression() {
532
- return "CURRENT_TIMESTAMP";
533
- },
534
- timestampValue(value) {
535
- return value.toISOString().slice(0, 19).replace("T", " ");
536
- },
537
- returningClause() {
538
- return "";
539
- },
540
- ilikeOperator() {
541
- return "LIKE";
542
- },
543
- nullsLastSuffix() {
544
- return "";
545
- },
546
- castToText(expression) {
547
- return `CAST(${expression} AS CHAR)`;
548
- },
549
- upsertSuffix(conflictColumns, updateColumns) {
550
- if (updateColumns.length === 0) {
551
- const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
552
- return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
553
- }
554
- const assignments = updateColumns.map((column) => {
555
- const quoted = quoteIdentifierFor(this, column);
556
- return `${quoted} = VALUES(${quoted})`;
557
- }).join(", ");
558
- return ` ON DUPLICATE KEY UPDATE ${assignments}`;
559
- }
560
- };
561
- var sqliteDialect = {
562
- driver: "sqlite",
563
- placeholder() {
564
- return "?";
565
- },
566
- quoteIdentifier(identifier) {
567
- return `"${assertSafeIdentifier(identifier)}"`;
568
- },
569
- nowExpression() {
570
- return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
571
- },
572
- timestampValue(value) {
573
- return value.toISOString();
574
- },
575
- returningClause(columns) {
576
- return ` RETURNING ${columns}`;
577
- },
578
- ilikeOperator() {
579
- return "LIKE";
580
- },
581
- nullsLastSuffix() {
582
- return "";
583
- },
584
- castToText(expression) {
585
- return `CAST(${expression} AS TEXT)`;
586
- },
587
- upsertSuffix(conflictColumns, updateColumns) {
588
- const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
589
- if (updateColumns.length === 0) {
590
- return ` ON CONFLICT (${target}) DO NOTHING`;
591
- }
592
- const assignments = updateColumns.map((column) => {
593
- const quoted = quoteIdentifierFor(this, column);
594
- return `${quoted} = excluded.${quoted}`;
595
- }).join(", ");
596
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
597
- }
598
- };
599
- var dialects = {
600
- pgsql: postgresDialect,
601
- mysql: mysqlDialect,
602
- sqlite: sqliteDialect
603
- };
604
- var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
605
- var dialectOverride = null;
606
- function dialectFor(driver) {
607
- return dialects[driver];
608
- }
609
- function currentSqlDialect() {
610
- return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
611
- }
612
- function useSqlDialect(driver) {
613
- dialectOverride = dialectFor(driver);
614
- return dialectOverride;
615
- }
616
- function sqlTimestamp(value = new Date) {
617
- return currentSqlDialect().timestampValue(value);
618
- }
619
- function resetSqlDialect() {
620
- dialectOverride = null;
621
- }
622
- function runWithSqlDialect(driver, callback) {
623
- return dialectContext.run(dialectFor(driver), callback);
624
- }
1152
+ // ../../src/core/auth/passwordLogin.ts
1153
+ init_dialect();
625
1154
 
626
1155
  // ../../src/core/auth/tokenHash.ts
627
1156
  import { createHmac as createHmac2 } from "crypto";
@@ -1240,6 +1769,7 @@ function isBearerCapableGuard(guard) {
1240
1769
  return guard instanceof ApiTokenGuard || guard instanceof DatabaseTokenGuard || guard instanceof JwtGuard;
1241
1770
  }
1242
1771
  // ../../src/core/auth/membershipContext.ts
1772
+ init_asyncContextStore();
1243
1773
  var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
1244
1774
  var uninitializedMembershipLookup = {
1245
1775
  async listForUser() {
@@ -1315,33 +1845,8 @@ function createMembershipContextMiddleware() {
1315
1845
  function createMembershipMiddleware() {
1316
1846
  return createMembershipContextMiddleware();
1317
1847
  }
1318
- // ../../src/core/tenant/tenantContext.ts
1319
- var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
1320
- function runWithTenant(tenant, callback) {
1321
- return tenantContext.run(tenant, callback);
1322
- }
1323
- function currentTenant() {
1324
- return tenantContext.getStore() ?? null;
1325
- }
1326
- function currentTenantId() {
1327
- const tenant = currentTenant();
1328
- if (!tenant) {
1329
- throw new Error("Tenant context is required.");
1330
- }
1331
- return tenant.id;
1332
- }
1333
- function rateLimitMultiplierForPlan(plan) {
1334
- switch (plan) {
1335
- case "enterprise":
1336
- return 4;
1337
- case "pro":
1338
- return 2;
1339
- default:
1340
- return 1;
1341
- }
1342
- }
1343
-
1344
1848
  // ../../src/core/auth/membershipScope.ts
1849
+ init_tenantContext();
1345
1850
  function resolveOrganizationScope() {
1346
1851
  const user = currentAuthUser();
1347
1852
  if (!user) {
@@ -2162,6 +2667,11 @@ function missingOptionalPeer(packageName, reason, error) {
2162
2667
  });
2163
2668
  }
2164
2669
 
2670
+ // ../../src/core/auth/saml/samlAssertionReplay.ts
2671
+ init_boundConnection();
2672
+ init_defaultConnection();
2673
+ init_dialect();
2674
+
2165
2675
  // ../../src/core/database/errors.ts
2166
2676
  function isPostgresError(error) {
2167
2677
  return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
@@ -2478,6 +2988,7 @@ class SamlProvider {
2478
2988
  }
2479
2989
  }
2480
2990
  // ../../src/core/auth/oneTimeToken.ts
2991
+ init_dialect();
2481
2992
  import { randomBytes as randomBytes4 } from "crypto";
2482
2993
  var AUTH_ONE_TIME_PURPOSES = {
2483
2994
  passwordReset: "password_reset",
@@ -2543,6 +3054,14 @@ class PolicyGate {
2543
3054
  }
2544
3055
  }
2545
3056
  }
3057
+ // ../../src/core/database/bindConnection.ts
3058
+ init_boundConnection();
3059
+
3060
+ // ../../src/core/auth/scimAuthMiddleware.ts
3061
+ init_boundConnection();
3062
+ init_defaultConnection();
3063
+ init_connectionContext();
3064
+
2546
3065
  // ../../src/core/security/scimTenantTokens.ts
2547
3066
  function parseScimTenantTokens(raw) {
2548
3067
  const tokens = new Map;
@@ -2583,71 +3102,9 @@ function resolveScimTenantFromToken(token) {
2583
3102
  return null;
2584
3103
  }
2585
3104
 
2586
- // ../../src/core/database/repositoryConnection.ts
2587
- function resolveRepositoryConnection() {
2588
- return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
2589
- }
2590
- function compileTaggedSql(strings, values) {
2591
- const dialect = currentSqlDialect();
2592
- let query = strings[0] ?? "";
2593
- const params = [];
2594
- for (let index = 0;index < values.length; index += 1) {
2595
- params.push(values[index]);
2596
- query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
2597
- }
2598
- return { query, params };
2599
- }
2600
- function isTaggedStrings(value) {
2601
- if (!Array.isArray(value) || typeof value[0] !== "string") {
2602
- return false;
2603
- }
2604
- return typeof value.raw !== "undefined";
2605
- }
2606
- function runOnConnection(connection, args) {
2607
- if (typeof connection === "function") {
2608
- return connection(...args);
2609
- }
2610
- if (typeof connection.unsafe !== "function") {
2611
- throw new Error("Database connection cannot run SQL.");
2612
- }
2613
- const strings = args[0];
2614
- if (!isTaggedStrings(strings)) {
2615
- throw new Error("Database connection cannot run tagged SQL.");
2616
- }
2617
- const compiled = compileTaggedSql(strings, args.slice(1));
2618
- return connection.unsafe(compiled.query, compiled.params);
2619
- }
2620
- var repositoryConnection = new Proxy(function repositoryConnection() {}, {
2621
- apply(_target, _thisArg, args) {
2622
- return runOnConnection(resolveRepositoryConnection(), args);
2623
- },
2624
- get(_target, property) {
2625
- const connection = resolveRepositoryConnection();
2626
- const value = connection[property];
2627
- return typeof value === "function" ? value.bind(connection) : value;
2628
- }
2629
- });
2630
-
2631
- // ../../src/core/tenant/tenancyConfig.ts
2632
- var TENANCY_DRIVERS = ["rls", "column", "none"];
2633
- function readTenancyDriver(env = process.env) {
2634
- const raw = env.TENANCY_DRIVER?.trim();
2635
- if (raw === undefined || raw === "") {
2636
- return "rls";
2637
- }
2638
- if (TENANCY_DRIVERS.includes(raw)) {
2639
- return raw;
2640
- }
2641
- throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
2642
- }
2643
- function isTenancyEnabled(env = process.env) {
2644
- return readTenancyDriver(env) !== "none";
2645
- }
2646
- function isRlsTenancy(env = process.env) {
2647
- return readTenancyDriver(env) === "rls";
2648
- }
2649
-
2650
3105
  // ../../src/core/tenant/resolveTenant.ts
3106
+ init_repositoryConnection();
3107
+ init_tenancyConfig();
2651
3108
  async function resolveTenant(tenantId) {
2652
3109
  if (!isTenancyEnabled()) {
2653
3110
  return {
@@ -2663,36 +3120,12 @@ async function resolveTenant(tenantId) {
2663
3120
  WHERE id = ${tenantId}
2664
3121
  LIMIT 1
2665
3122
  `;
2666
- const row = rows[0];
2667
- return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
2668
- }
2669
-
2670
- // ../../src/core/tenant/tenantDatabaseScope.ts
2671
- async function applyTenantContextToTransaction(transaction, tenantId) {
2672
- await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
2673
- await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
2674
- }
2675
- async function runWithTenantDatabase(tenant, callback) {
2676
- if (!isTenancyEnabled() || !isRlsTenancy()) {
2677
- return await runWithTenant(tenant, callback);
2678
- }
2679
- if (hasActiveDatabaseConnection()) {
2680
- const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
2681
- await applyTenantContextToTransaction(activeConnection, tenant.id);
2682
- return await runWithTenant(tenant, callback);
2683
- }
2684
- return await getDefaultDatabasePool().begin(async (transaction) => {
2685
- await applyTenantContextToTransaction(transaction, tenant.id);
2686
- return await runWithDatabaseConnection(transaction, async () => {
2687
- return await runWithTenant(tenant, callback);
2688
- });
2689
- });
2690
- }
2691
- function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
2692
- return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
3123
+ const row = rows[0];
3124
+ return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
2693
3125
  }
2694
3126
 
2695
3127
  // ../../src/core/auth/scimAuthMiddleware.ts
3128
+ init_tenantDatabaseScope();
2696
3129
  function createScimAuthMiddleware() {
2697
3130
  return async (request, next) => {
2698
3131
  const authorization = request.headers.get("authorization");
@@ -3264,50 +3697,9 @@ class ConfigStore {
3264
3697
  return this.values.has(key);
3265
3698
  }
3266
3699
  }
3267
- // ../../src/core/events/eventBus.ts
3268
- class EventBus {
3269
- constructor() {}
3270
- listeners = new Map;
3271
- on(event, listener) {
3272
- return this.listen(event, listener);
3273
- }
3274
- listen(event, listener) {
3275
- const handlers = this.listeners.get(event) ?? new Set;
3276
- handlers.add(listener);
3277
- this.listeners.set(event, handlers);
3278
- return () => {
3279
- handlers.delete(listener);
3280
- if (handlers.size === 0) {
3281
- this.listeners.delete(event);
3282
- }
3283
- };
3284
- }
3285
- async emit(event, payload) {
3286
- await this.dispatch(event, payload);
3287
- }
3288
- async dispatch(event, payload) {
3289
- const handlers = this.listeners.get(event);
3290
- if (!handlers || handlers.size === 0) {
3291
- return;
3292
- }
3293
- for (const handler of handlers) {
3294
- await handler(payload);
3295
- }
3296
- }
3297
- }
3298
- var EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
3299
- function readSharedEventBus() {
3300
- const globalBus = globalThis[EVENT_BUS_KEY];
3301
- if (globalBus) {
3302
- return globalBus;
3303
- }
3304
- const bus = new EventBus;
3305
- globalThis[EVENT_BUS_KEY] = bus;
3306
- return bus;
3307
- }
3308
- var eventBus = readSharedEventBus();
3309
-
3310
3700
  // ../../src/core/events/index.ts
3701
+ init_deferredModelEvents();
3702
+ init_eventBus();
3311
3703
  function modelEventName(tableName, action) {
3312
3704
  return `${tableName}.${action}`;
3313
3705
  }
@@ -3324,6 +3716,7 @@ function buildPaginationMeta(input) {
3324
3716
  }
3325
3717
 
3326
3718
  // ../../src/core/database/query.ts
3719
+ init_dialect();
3327
3720
  function quoteIdentifier(identifier) {
3328
3721
  return currentSqlDialect().quoteIdentifier(identifier);
3329
3722
  }
@@ -4078,6 +4471,9 @@ function indexMorphToRelation(children, parentsByType, relation) {
4078
4471
  return result;
4079
4472
  }
4080
4473
 
4474
+ // ../../src/core/database/baseRepository.ts
4475
+ init_repositoryConnection();
4476
+
4081
4477
  // ../../src/core/database/pluck.ts
4082
4478
  function uniqueColumnSelect(table, columns) {
4083
4479
  const seen = new Set;
@@ -4614,7 +5010,7 @@ class BaseRepository {
4614
5010
  throw new Error(`Insert into ${this.table.name} did not return a record.`);
4615
5011
  }
4616
5012
  const entity = record;
4617
- await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
5013
+ await dispatchModelEvent(modelEventName(this.table.name, "created"), entity);
4618
5014
  return entity;
4619
5015
  });
4620
5016
  }
@@ -4624,7 +5020,7 @@ class BaseRepository {
4624
5020
  const [record] = await this.connection.unsafe(text, params);
4625
5021
  const entity = record ?? null;
4626
5022
  if (entity) {
4627
- await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
5023
+ await dispatchModelEvent(modelEventName(this.table.name, "updated"), entity);
4628
5024
  }
4629
5025
  return entity;
4630
5026
  });
@@ -4649,7 +5045,7 @@ class BaseRepository {
4649
5045
  if (!record) {
4650
5046
  return false;
4651
5047
  }
4652
- await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
5048
+ await dispatchModelEvent(modelEventName(this.table.name, "deleted"), record);
4653
5049
  return true;
4654
5050
  });
4655
5051
  }
@@ -4660,7 +5056,7 @@ class BaseRepository {
4660
5056
  if (!row) {
4661
5057
  return false;
4662
5058
  }
4663
- await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
5059
+ await dispatchModelEvent(modelEventName(this.table.name, "force-deleted"), {
4664
5060
  id
4665
5061
  });
4666
5062
  return true;
@@ -4674,7 +5070,7 @@ class BaseRepository {
4674
5070
  return null;
4675
5071
  }
4676
5072
  const entity = record;
4677
- await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
5073
+ await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
4678
5074
  return entity;
4679
5075
  });
4680
5076
  }
@@ -4887,6 +5283,8 @@ class BaseRepository {
4887
5283
  }
4888
5284
  var baseRepository_default = BaseRepository;
4889
5285
  // ../../src/core/database/bunSql.ts
5286
+ init_boundConnection();
5287
+ init_defaultConnection();
4890
5288
  var {SQL } = globalThis.Bun;
4891
5289
  function createBunSqlPool(options) {
4892
5290
  if (!options.url) {
@@ -4905,14 +5303,13 @@ function bindBunSql(sql) {
4905
5303
  registerDefaultDatabasePool(sql);
4906
5304
  return sql;
4907
5305
  }
4908
- // ../../src/core/database/connection.ts
4909
- function createDatabaseConnection(source) {
4910
- return {
4911
- async unsafe(query, params = []) {
4912
- return await source.unsafe(query, params);
4913
- }
4914
- };
4915
- }
5306
+
5307
+ // ../../src/framework/public-api.ts
5308
+ init_connection();
5309
+ init_connectionContext();
5310
+ init_defaultConnection();
5311
+ init_dialect();
5312
+
4916
5313
  // ../../src/core/database/inflection.ts
4917
5314
  function singularize(word) {
4918
5315
  if (word.endsWith("ies") && word.length > 3) {
@@ -5104,6 +5501,7 @@ async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
5104
5501
  }
5105
5502
  }
5106
5503
  // ../../src/core/database/migrations/runner.ts
5504
+ init_dialect();
5107
5505
  import { readdir } from "fs/promises";
5108
5506
  import { join } from "path";
5109
5507
  import { pathToFileURL } from "url";
@@ -6989,6 +7387,8 @@ function createMysqlConnection(url) {
6989
7387
  };
6990
7388
  }
6991
7389
  // ../../src/core/database/namedConnections.ts
7390
+ init_connectionContext();
7391
+ init_dialect();
6992
7392
  var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
6993
7393
  function registry() {
6994
7394
  const globalRecord = globalThis;
@@ -7026,6 +7426,11 @@ function runOnNamedConnection(name, callback) {
7026
7426
  const entry = getNamedConnection(name);
7027
7427
  return runWithSqlDialect(entry.driver, () => runWithDatabaseConnection(entry.connection, callback));
7028
7428
  }
7429
+
7430
+ // ../../src/framework/public-api.ts
7431
+ init_queryProxy();
7432
+ init_repositoryConnection();
7433
+
7029
7434
  // ../../src/core/database/schema/columnDefinition.ts
7030
7435
  class ColumnDefinition {
7031
7436
  name;
@@ -7247,6 +7652,10 @@ class Blueprint {
7247
7652
  });
7248
7653
  }
7249
7654
  }
7655
+
7656
+ // ../../src/core/database/schema/index.ts
7657
+ init_driver();
7658
+
7250
7659
  // ../../src/core/database/schema/errors.ts
7251
7660
  class UnsupportedSchemaFeatureError extends Error {
7252
7661
  constructor(feature, driver) {
@@ -7559,6 +7968,7 @@ function grammarForDriver(driver) {
7559
7968
  }
7560
7969
  }
7561
7970
  // ../../src/core/database/schema/schema.ts
7971
+ init_driver();
7562
7972
  class SchemaBuilder {
7563
7973
  #driver;
7564
7974
  #statements = [];
@@ -7674,19 +8084,11 @@ function createSqliteConnection(filename) {
7674
8084
  function defineTable(definition) {
7675
8085
  return definition;
7676
8086
  }
7677
- // ../../src/core/database/transaction.ts
7678
- function supportsTransactions(connection) {
7679
- return typeof connection.begin === "function";
7680
- }
7681
- async function runInTransaction(operation) {
7682
- const pool = resolveRepositoryConnection();
7683
- if (!supportsTransactions(pool)) {
7684
- throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
7685
- }
7686
- return await pool.begin(async (transaction) => {
7687
- return await operation(createDatabaseConnection(transaction));
7688
- });
7689
- }
8087
+
8088
+ // ../../src/framework/public-api.ts
8089
+ init_transaction();
8090
+ init_eventBus();
8091
+
7690
8092
  // ../../src/core/mail/mailer.ts
7691
8093
  function resolveSmtpConfig() {
7692
8094
  const host = process.env.MAIL_HOST?.trim();
@@ -8131,6 +8533,7 @@ function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
8131
8533
  };
8132
8534
  }
8133
8535
  // ../../src/core/http/requestMetaContext.ts
8536
+ init_asyncContextStore();
8134
8537
  var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
8135
8538
  function runWithRequestMeta(meta, callback) {
8136
8539
  return requestMetaContext.run(meta, callback);
@@ -8806,6 +9209,7 @@ function createFlashMiddleware() {
8806
9209
  };
8807
9210
  }
8808
9211
  // ../../src/core/http/validation.ts
9212
+ init_tenantContext();
8809
9213
  function buildRequestCacheKey(fallbackPath, request) {
8810
9214
  if (!request) {
8811
9215
  return fallbackPath;
@@ -9026,7 +9430,8 @@ function wrapRouteHandler(handler, middleware) {
9026
9430
  function applyMiddlewareToRoutes(routes, middleware) {
9027
9431
  const wrapped = {};
9028
9432
  for (const [path, routeHandler] of Object.entries(routes)) {
9029
- wrapped[path] = wrapRouteHandler(routeHandler, middleware);
9433
+ const identity = async (_request, next) => await runWithRequestMeta({ ...currentRequestMeta(), routeTemplate: path }, next);
9434
+ wrapped[path] = wrapRouteHandler(routeHandler, [identity, ...middleware]);
9030
9435
  }
9031
9436
  return wrapped;
9032
9437
  }
@@ -9230,6 +9635,9 @@ function toPaginatedResourceCollection(items, meta, transformer) {
9230
9635
  meta
9231
9636
  };
9232
9637
  }
9638
+ // ../../src/core/http/response.ts
9639
+ init_transactionControl();
9640
+
9233
9641
  // ../../src/core/http/safeInternalPath.ts
9234
9642
  var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
9235
9643
  function looksLikeExternalTarget(value) {
@@ -9531,6 +9939,7 @@ function withErrorHandling(handler) {
9531
9939
  try {
9532
9940
  return await handler(...args);
9533
9941
  } catch (error) {
9942
+ requestTransactionRollback();
9534
9943
  const request = args.find((arg) => arg instanceof Request);
9535
9944
  const webResponse = await webErrorResponse(error, request);
9536
9945
  if (webResponse) {
@@ -9628,7 +10037,9 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
9628
10037
  return response;
9629
10038
  };
9630
10039
  }
9631
- // ../../src/core/http/loginThrottleMiddleware.ts
10040
+ // ../../src/core/http/throttleMiddleware.ts
10041
+ import { createHash as createHash3 } from "crypto";
10042
+ init_tenantContext();
9632
10043
  var {RedisClient: RedisClient2 } = globalThis.Bun;
9633
10044
 
9634
10045
  // ../../src/core/http/throttleResponse.ts
@@ -9651,6 +10062,104 @@ async function tooManyRequestsResponse(request, message, decaySeconds) {
9651
10062
  return new Response(html.body, { status: 429, headers });
9652
10063
  }
9653
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
+
9654
10163
  // ../../src/core/http/loginThrottleMiddleware.ts
9655
10164
  var memoryLoginBuckets = new Map;
9656
10165
  function resolveLoginIdentity(request) {
@@ -9694,15 +10203,17 @@ function createMemoryLoginThrottleMiddleware(options) {
9694
10203
  };
9695
10204
  }
9696
10205
  function createRedisLoginThrottleMiddleware(options) {
9697
- const client = new RedisClient2(options.redisUrl);
9698
- const prefix = options.keyPrefix ?? namespacedRedisKey("login-throttle:");
10206
+ const consume = createRedisThrottleConsumer(options);
10207
+ const prefix = options.keyPrefix ?? "login-throttle";
9699
10208
  return async (request, next) => {
9700
10209
  const identity = resolveLoginIdentity(request);
9701
10210
  const email = await resolveLoginEmail(request);
9702
- const throttleKey = `${prefix}${identity}:${email}`;
9703
- const attempts = Number(await client.incr(throttleKey));
9704
- if (attempts === 1) {
9705
- await client.expire(throttleKey, options.decaySeconds);
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();
9706
10217
  }
9707
10218
  if (attempts > options.maxAttempts) {
9708
10219
  return await tooManyRequestsResponse(request, "Too many login attempts. Try again later.", options.decaySeconds);
@@ -9749,7 +10260,24 @@ function resetMemoryThrottleForTests() {
9749
10260
  }
9750
10261
  }
9751
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
+
9752
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
+ }
9753
10281
  httpRequestsTotal = new Map;
9754
10282
  httpRequestDurationMs = new Map;
9755
10283
  incrementHttpRequest(labels) {
@@ -9757,10 +10285,20 @@ class PrometheusRegistry {
9757
10285
  this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
9758
10286
  }
9759
10287
  observeHttpDuration(labels, durationMs) {
10288
+ if (!Number.isFinite(durationMs) || durationMs < 0)
10289
+ return;
9760
10290
  const key = this.metricKey(labels);
9761
- const samples = this.httpRequestDurationMs.get(key) ?? [];
9762
- samples.push(durationMs);
9763
- this.httpRequestDurationMs.set(key, samples);
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);
9764
10302
  }
9765
10303
  renderMetrics() {
9766
10304
  const lines = [
@@ -9770,16 +10308,22 @@ class PrometheusRegistry {
9770
10308
  for (const [key, value] of this.httpRequestsTotal) {
9771
10309
  lines.push(`http_requests_total{${key}} ${value}`);
9772
10310
  }
9773
- lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
9774
- for (const [key, samples] of this.httpRequestDurationMs) {
9775
- const sum = samples.reduce((total, sample) => total + sample, 0);
9776
- lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
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}`);
9777
10320
  }
9778
10321
  return `${lines.join(`
9779
10322
  `)}
9780
10323
  `;
9781
10324
  }
9782
10325
  resetForTests() {
10326
+ this.seriesLabels.clear();
9783
10327
  this.httpRequestsTotal.clear();
9784
10328
  this.httpRequestDurationMs.clear();
9785
10329
  }
@@ -9789,9 +10333,10 @@ class PrometheusRegistry {
9789
10333
  let totalRequests = 0;
9790
10334
  for (const [key, count] of this.httpRequestsTotal) {
9791
10335
  totalRequests += count;
9792
- const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
9793
- const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
9794
- const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
10336
+ const labels = this.seriesLabels.get(key);
10337
+ if (!labels)
10338
+ continue;
10339
+ const { method, path, status } = labels;
9795
10340
  byStatus[status] = (byStatus[status] ?? 0) + count;
9796
10341
  const pathKey = `${method} ${path}`;
9797
10342
  const existing = pathCounts.get(pathKey);
@@ -9808,30 +10353,54 @@ class PrometheusRegistry {
9808
10353
  topPaths
9809
10354
  };
9810
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
+ }
9811
10363
  metricKey(labels) {
9812
- return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
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;
9813
10383
  }
9814
10384
  }
9815
10385
  var prometheusRegistry = new PrometheusRegistry;
9816
10386
 
9817
10387
  // ../../src/core/http/metricsMiddleware.ts
9818
- function normalizeMetricPath(pathname) {
9819
- return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
9820
- }
9821
10388
  function createMetricsMiddleware() {
9822
10389
  return async (request, next) => {
9823
10390
  const startedAt = performance.now();
9824
- const response = await next();
9825
- const durationMs = performance.now() - startedAt;
9826
- const path = normalizeMetricPath(new URL(request.url).pathname);
9827
- const labels = {
9828
- method: request.method,
9829
- path,
9830
- status: String(response.status)
9831
- };
9832
- prometheusRegistry.incrementHttpRequest(labels);
9833
- prometheusRegistry.observeHttpDuration(labels, durationMs);
9834
- return response;
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
+ }
9835
10404
  };
9836
10405
  }
9837
10406
  // ../../src/core/http/requireAbilityMiddleware.ts
@@ -9957,14 +10526,14 @@ function createRequireWebAuthMiddleware(auth) {
9957
10526
  };
9958
10527
  }
9959
10528
  // ../../src/core/http/scimThrottleMiddleware.ts
9960
- import { createHash as createHash3 } from "crypto";
10529
+ import { createHash as createHash4 } from "crypto";
9961
10530
  var {RedisClient: RedisClient3 } = globalThis.Bun;
9962
10531
  var memoryBuckets = new Map;
9963
10532
  function resolveScimIdentity(request) {
9964
10533
  const authorization = request.headers.get("authorization") ?? "";
9965
10534
  const presented = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
9966
10535
  if (presented.length > 0) {
9967
- return createHash3("sha256").update(presented).digest("hex");
10536
+ return createHash4("sha256").update(presented).digest("hex");
9968
10537
  }
9969
10538
  return readClientIp(request) ?? "unknown";
9970
10539
  }
@@ -10127,36 +10696,6 @@ function createValidateSignatureMiddleware() {
10127
10696
  return await next();
10128
10697
  };
10129
10698
  }
10130
- // ../../src/core/http/throttleMiddleware.ts
10131
- var {RedisClient: RedisClient4 } = globalThis.Bun;
10132
- function resolveThrottleIdentity(request) {
10133
- const user = currentAuthUser();
10134
- if (user?.tokenId !== undefined) {
10135
- return `token:${user.tokenId}`;
10136
- }
10137
- if (user) {
10138
- return `user:${user.id}`;
10139
- }
10140
- return readClientIp(request) ?? "unknown";
10141
- }
10142
- function createThrottleMiddleware(options) {
10143
- const client = new RedisClient4(options.redisUrl);
10144
- const prefix = options.keyPrefix ?? namespacedRedisKey("throttle:");
10145
- return async (request, next) => {
10146
- const identity = resolveThrottleIdentity(request);
10147
- const path = new URL(request.url).pathname;
10148
- const throttleKey = `${prefix}${identity}:${path}`;
10149
- const attempts = Number(await client.incr(throttleKey));
10150
- if (attempts === 1) {
10151
- await client.expire(throttleKey, options.decaySeconds);
10152
- }
10153
- const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
10154
- if (attempts > maxAttempts) {
10155
- return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
10156
- }
10157
- return await next();
10158
- };
10159
- }
10160
10699
  // ../../src/core/http/parseFormBody.ts
10161
10700
  async function parseFormBody(request) {
10162
10701
  const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
@@ -10613,7 +11152,7 @@ var queueConfig = {
10613
11152
  };
10614
11153
 
10615
11154
  // ../../src/core/queue/jobRunner.ts
10616
- async function runQueueJob(envelope, failedJobs) {
11155
+ async function runQueueJob(envelope, failedJobs, options = {}) {
10617
11156
  const job = jobRegistry.create(envelope.name);
10618
11157
  if (!job) {
10619
11158
  throw new Error(`Unknown job "${envelope.name}".`);
@@ -10630,7 +11169,7 @@ async function runQueueJob(envelope, failedJobs) {
10630
11169
  await runQueueJob({
10631
11170
  ...envelope,
10632
11171
  attempts: nextAttempt
10633
- }, failedJobs);
11172
+ }, failedJobs, options);
10634
11173
  return;
10635
11174
  }
10636
11175
  await failedJobs.recordFailure({
@@ -10638,12 +11177,13 @@ async function runQueueJob(envelope, failedJobs) {
10638
11177
  payload: envelope.payload,
10639
11178
  exception: error instanceof Error ? error.stack ?? error.message : String(error)
10640
11179
  });
11180
+ options.onFailureRecorded?.();
10641
11181
  throw error;
10642
11182
  }
10643
11183
  }
10644
11184
 
10645
11185
  // ../../src/core/queue/redisQueue.ts
10646
- var {RedisClient: RedisClient5 } = globalThis.Bun;
11186
+ var {RedisClient: RedisClient4 } = globalThis.Bun;
10647
11187
  function queueListKey() {
10648
11188
  return namespacedRedisKey("queue:default");
10649
11189
  }
@@ -10657,6 +11197,20 @@ var QUEUE_LIST_KEY = queueListKey();
10657
11197
  var QUEUE_HIGH_KEY = queueHighKey();
10658
11198
  var QUEUE_LOW_KEY = queueLowKey();
10659
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
+ }
10660
11214
  function queueKeyForPriority(priority = "default") {
10661
11215
  switch (priority) {
10662
11216
  case "high":
@@ -10698,11 +11252,134 @@ function parseQueueJobEnvelope(rawPayload) {
10698
11252
  attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
10699
11253
  };
10700
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
+ }
10701
11378
 
10702
11379
  class RedisQueue {
10703
11380
  client;
10704
11381
  constructor(redisUrl) {
10705
- this.client = new RedisClient5(redisUrl);
11382
+ this.client = new RedisClient4(redisUrl);
10706
11383
  }
10707
11384
  async dispatch(job, payload) {
10708
11385
  const name = jobRegistry.resolveName(job);
@@ -10722,13 +11399,16 @@ class RedisQueue {
10722
11399
  class QueueWorker {
10723
11400
  failedJobs;
10724
11401
  timeoutSeconds;
11402
+ queueKeys;
11403
+ owner = crypto.randomUUID();
10725
11404
  running = false;
10726
11405
  stopping = false;
10727
11406
  client;
10728
- constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
11407
+ constructor(redisUrl, failedJobs, timeoutSeconds = 5, queueKeys = QUEUE_KEYS) {
10729
11408
  this.failedJobs = failedJobs;
10730
11409
  this.timeoutSeconds = timeoutSeconds;
10731
- this.client = new RedisClient5(redisUrl);
11410
+ this.queueKeys = queueKeys;
11411
+ this.client = new RedisClient4(redisUrl);
10732
11412
  }
10733
11413
  requestStop() {
10734
11414
  this.stopping = true;
@@ -10736,38 +11416,81 @@ class QueueWorker {
10736
11416
  isRunning() {
10737
11417
  return this.running;
10738
11418
  }
10739
- async processNext() {
10740
- let result = null;
10741
- for (const queueKey of QUEUE_KEYS) {
10742
- result = await this.client.brpop(queueKey, 1);
10743
- if (result) {
10744
- break;
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 };
10745
11428
  }
10746
- }
10747
- if (!result) {
10748
- result = await this.client.brpop(QUEUE_LIST_KEY, this.timeoutSeconds);
10749
- }
10750
- if (!result) {
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)
10751
11437
  return false;
10752
- }
10753
- const [, rawPayload] = result;
10754
- const envelope = parseQueueJobEnvelope(rawPayload);
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);
10755
11444
  if (!envelope) {
11445
+ await updateQueueReservation(this.client, queueKey, reservation, "quarantine");
10756
11446
  return true;
10757
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)));
10758
11467
  try {
10759
- await runQueueJob(envelope, this.failedJobs);
11468
+ await runQueueJob(envelope, this.failedJobs, {
11469
+ onFailureRecorded: () => {
11470
+ acknowledge = true;
11471
+ }
11472
+ });
11473
+ acknowledge = true;
10760
11474
  } catch (error) {
10761
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");
10762
11482
  }
10763
11483
  return true;
10764
11484
  }
10765
11485
  async run() {
10766
11486
  this.running = true;
10767
- while (!this.stopping) {
10768
- await this.processNext();
11487
+ try {
11488
+ while (!this.stopping) {
11489
+ await this.processNext();
11490
+ }
11491
+ } finally {
11492
+ this.running = false;
10769
11493
  }
10770
- this.running = false;
10771
11494
  }
10772
11495
  close() {
10773
11496
  this.client.close();
@@ -10826,9 +11549,9 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
10826
11549
  return new QueueWorker(redisUrl, failedJobs);
10827
11550
  }
10828
11551
  // ../../src/core/queue/queueMetrics.ts
10829
- var {RedisClient: RedisClient6 } = globalThis.Bun;
11552
+ var {RedisClient: RedisClient5 } = globalThis.Bun;
10830
11553
  async function readRedisQueueDepth(redisUrl) {
10831
- const client = new RedisClient6(redisUrl);
11554
+ const client = new RedisClient5(redisUrl);
10832
11555
  const [high, defaultQueue, low] = await Promise.all([
10833
11556
  client.llen(QUEUE_HIGH_KEY),
10834
11557
  client.llen(QUEUE_LIST_KEY),
@@ -11041,52 +11764,19 @@ function generatedRlsBootstrapSql(tables, userOwnedTables = []) {
11041
11764
  ].join(`
11042
11765
  `);
11043
11766
  }
11044
- // ../../src/core/tenant/tenantMiddleware.ts
11045
- import { createHash as createHash4 } from "crypto";
11046
11767
 
11047
- // ../../src/core/tenant/databaseTenantContext.ts
11048
- function stringifyBypassIdentifier(identifier) {
11049
- if (typeof identifier === "number") {
11050
- return String(identifier);
11051
- }
11052
- return identifier.trim();
11053
- }
11054
- async function applyIdentifierToTransaction(transaction, identifier) {
11055
- await transaction.unsafe(`SELECT set_config('app.bypass_identifier', $1, true)`, [
11056
- stringifyBypassIdentifier(identifier)
11057
- ]);
11058
- }
11059
- function assertBypassIdentifier(identifier) {
11060
- if (typeof identifier === "number") {
11061
- if (!Number.isInteger(identifier) || identifier <= 0) {
11062
- throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
11063
- }
11064
- return;
11065
- }
11066
- if (typeof identifier === "string" && identifier.trim() !== "") {
11067
- return;
11068
- }
11069
- throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
11070
- }
11071
- async function runWithScopedTenantTransaction(apply, callback) {
11072
- if (!isRlsTenancy()) {
11073
- return await callback();
11074
- }
11075
- const pool = getDefaultDatabasePool();
11076
- if (typeof pool.begin !== "function") {
11077
- throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
11078
- }
11079
- return await pool.begin(async (transaction) => {
11080
- await apply(transaction);
11081
- return await runWithDatabaseConnection(transaction, callback);
11082
- });
11083
- }
11084
- async function runWithMigrationBypassForIdentifier(identifier, callback) {
11085
- assertBypassIdentifier(identifier);
11086
- return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
11087
- }
11768
+ // ../../src/framework/public-api.ts
11769
+ init_tenancyConfig();
11770
+ init_tenantContext();
11771
+ init_tenantDatabaseScope();
11088
11772
 
11089
11773
  // ../../src/core/tenant/tenantMiddleware.ts
11774
+ import { createHash as createHash5 } from "crypto";
11775
+ init_repositoryConnection();
11776
+ init_databaseTenantContext();
11777
+ init_tenancyConfig();
11778
+ init_tenantContext();
11779
+ init_tenantDatabaseScope();
11090
11780
  var DEFAULT_TENANT = {
11091
11781
  id: 1,
11092
11782
  slug: "default",
@@ -11108,7 +11798,7 @@ async function resolveUserTenantId(userId) {
11108
11798
  });
11109
11799
  }
11110
11800
  function auditChecksum(payload) {
11111
- return createHash4("sha256").update(JSON.stringify(payload)).digest("hex");
11801
+ return createHash5("sha256").update(JSON.stringify(payload)).digest("hex");
11112
11802
  }
11113
11803
  async function resolveTenantForRequest(request) {
11114
11804
  const user = currentAuthUser();
@@ -11174,6 +11864,7 @@ function createTenantMiddleware() {
11174
11864
  };
11175
11865
  }
11176
11866
  // ../../src/core/tracing/traceContext.ts
11867
+ init_asyncContextStore();
11177
11868
  var traceContextStorage = createAsyncContextStore("@getstrata/traceContext");
11178
11869
  function runWithTraceContext(context, callback) {
11179
11870
  return traceContextStorage.run(context, callback);
@@ -11845,6 +12536,7 @@ export {
11845
12536
  defineTable,
11846
12537
  dehydrateValue,
11847
12538
  dialectFor,
12539
+ dispatchModelEvent,
11848
12540
  emailRule,
11849
12541
  emptyPaginateResult,
11850
12542
  enableTenantRlsSql,
@@ -11934,7 +12626,6 @@ export {
11934
12626
  namespacedRedisKey,
11935
12627
  noContentResponse,
11936
12628
  normalizeFieldErrors,
11937
- normalizeMetricPath,
11938
12629
  notFoundHtmlResponse,
11939
12630
  notModifiedResponse,
11940
12631
  otelServiceName,
@@ -11982,6 +12673,7 @@ export {
11982
12673
  repositoryConnection,
11983
12674
  requestIdMiddleware,
11984
12675
  requestMetaContext,
12676
+ requestTransactionRollback,
11985
12677
  requireConfiguredSecret,
11986
12678
  required,
11987
12679
  resetBoundDatabaseConnection,
@@ -12034,6 +12726,7 @@ export {
12034
12726
  runWithAuthContext,
12035
12727
  runWithAuthUser,
12036
12728
  runWithDatabaseConnection,
12729
+ runWithDeferredModelEvents,
12037
12730
  runWithMembershipContext,
12038
12731
  runWithRequestMeta,
12039
12732
  runWithSqlDialect,