@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.
- package/CHANGELOG.md +23 -0
- package/dist/core/database/transaction.d.ts +2 -1
- package/dist/core/database/transactionControl.d.ts +5 -0
- package/dist/core/events/deferredModelEvents.d.ts +3 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
- package/dist/core/http/metricsMiddleware.d.ts +1 -2
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/throttleMiddleware.d.ts +11 -2
- package/dist/core/metrics/prometheus.d.ts +11 -1
- package/dist/core/queue/jobRunner.d.ts +3 -1
- package/dist/core/queue/redisQueue.d.ts +20 -3
- package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
- package/dist/entries/audit/exportAuditLogs.js +751 -97
- package/dist/entries/auth/accessControl.js +16 -2
- package/dist/entries/auth/basicAuthGuard.js +271 -224
- package/dist/entries/auth/membershipMiddleware.js +17 -0
- package/dist/entries/auth/membershipScope.js +21 -6
- package/dist/entries/auth/membershipService.js +20 -5
- package/dist/entries/auth/oauth/samlProvider.js +187 -142
- package/dist/entries/auth/oneTimeToken.js +142 -123
- package/dist/entries/auth/passwordLogin.js +212 -191
- package/dist/entries/auth/policy.js +16 -2
- package/dist/entries/auth/saml/samlServiceProvider.js +187 -142
- package/dist/entries/auth/scimAuthMiddleware.js +732 -64
- package/dist/entries/database/migrations.js +144 -125
- package/dist/entries/database/schema.js +58 -39
- package/dist/entries/http/csrfMiddleware.js +28 -14
- package/dist/entries/http/csrfToken.js +28 -14
- package/dist/entries/http/flashMiddleware.js +28 -14
- package/dist/entries/http/metricsMiddleware.js +124 -27
- package/dist/entries/http/response.js +107 -0
- package/dist/entries/http/routeMiddleware.js +40 -0
- package/dist/entries/http/throttleMiddleware.js +124 -11
- package/dist/entries/jobs/exportAuditLogsJob.js +751 -97
- package/dist/entries/metrics/prometheus.js +71 -11
- package/dist/entries/queue/createAppQueue.js +215 -21
- package/dist/entries/queue/jobRunner.js +3 -2
- package/dist/entries/queue/publicQueue.js +215 -21
- package/dist/entries/queue/queueMetrics.js +215 -21
- package/dist/entries/queue/redisQueue.js +223 -22
- package/dist/entries/runtime/asyncContextStore.js +16 -0
- package/dist/entries/tenant/databaseTenantContext.js +666 -11
- package/dist/entries/tenant/tenancyConfig.js +19 -1
- package/dist/entries/tenant/tenantDatabaseScope.js +688 -18
- package/dist/entries/tracing/tracingMiddleware.js +28 -14
- package/dist/framework/public-api.d.ts +3 -3
- package/dist/index.js +1344 -651
- 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
|
|
1004
|
+
return userId;
|
|
308
1005
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
1017
|
+
return typeof value.findByIdOrThrow === "function";
|
|
315
1018
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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/
|
|
436
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
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
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
9698
|
-
const prefix = options.keyPrefix ??
|
|
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 =
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
await
|
|
10211
|
+
const throttleKey = redisThrottleKey(request, prefix, JSON.stringify([identity, email]));
|
|
10212
|
+
let attempts;
|
|
10213
|
+
try {
|
|
10214
|
+
attempts = await consume(throttleKey, options.decaySeconds);
|
|
10215
|
+
} catch {
|
|
10216
|
+
return throttleUnavailableResponse();
|
|
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
|
|
9762
|
-
|
|
9763
|
-
|
|
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
|
|
9774
|
-
for (const [key,
|
|
9775
|
-
|
|
9776
|
-
|
|
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
|
|
9793
|
-
|
|
9794
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
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
|
-
|
|
10748
|
-
|
|
10749
|
-
}
|
|
10750
|
-
|
|
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
|
|
10754
|
-
|
|
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
|
-
|
|
10768
|
-
|
|
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:
|
|
11552
|
+
var {RedisClient: RedisClient5 } = globalThis.Bun;
|
|
10830
11553
|
async function readRedisQueueDepth(redisUrl) {
|
|
10831
|
-
const client = new
|
|
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/
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
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
|
|
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,
|