@getstrata/core 0.7.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -33
- package/README.md +36 -43
- package/dist/core/database/baseRepository.d.ts +2 -1
- package/dist/core/database/dialect.d.ts +5 -1
- package/dist/core/database/errors.d.ts +4 -0
- package/dist/core/database/mysqlConnection.d.ts +3 -1
- package/dist/core/errors/http.d.ts +4 -1
- package/dist/core/http/clientIp.d.ts +6 -3
- package/dist/core/http/webErrorResponse.d.ts +3 -1
- package/dist/core/runtime/appEnv.d.ts +4 -0
- package/dist/core/tenant/tenancyConfig.d.ts +1 -0
- package/dist/entries/audit/exportAuditLogs.js +7 -5
- package/dist/entries/auth/scimAuthMiddleware.js +9 -7
- package/dist/entries/auth/sessionCookie.js +2 -2
- package/dist/entries/auth/sessionGuard.js +2 -2
- package/dist/entries/contracts/authUserDirectory.js +1 -0
- package/dist/entries/database/errors.js +45 -6
- package/dist/entries/database/model.js +4 -4
- package/dist/entries/database/mysqlConnection.js +18 -2
- package/dist/entries/database/query.js +2 -2
- package/dist/entries/database/repositoryQuery.js +20 -20
- package/dist/entries/database/schema.js +2 -2
- package/dist/entries/database/sqliteConnection.js +5 -0
- package/dist/entries/http/clientIp.js +24 -6
- package/dist/entries/http/corsMiddleware.js +14 -3
- package/dist/entries/http/loginThrottleMiddleware.js +25 -8
- package/dist/entries/http/memoryThrottleMiddleware.js +25 -8
- package/dist/entries/http/response.js +66 -7
- package/dist/entries/http/scimThrottleMiddleware.js +23 -6
- package/dist/entries/http/throttleMiddleware.js +25 -8
- package/dist/entries/http/webErrorResponse.js +66 -7
- package/dist/entries/jobs/exportAuditLogsJob.js +7 -5
- package/dist/entries/logging/requestLoggingMiddleware.js +29 -10
- package/dist/entries/openapi/generator.js +5 -5
- package/dist/entries/runtime/appEnv.js +8 -0
- package/dist/entries/tenant/databaseTenantContext.js +7 -5
- package/dist/entries/tenant/tenancyConfig.js +7 -5
- package/dist/entries/tenant/tenantDatabaseScope.js +9 -7
- package/dist/framework/public-api.d.ts +3 -3
- package/dist/index.js +212 -90
- package/package.json +16 -3
package/dist/index.js
CHANGED
|
@@ -120,6 +120,12 @@ class PreconditionFailedError extends HttpError {
|
|
|
120
120
|
super(412, message, details);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
class InternalServerError extends HttpError {
|
|
125
|
+
constructor(message = "Internal server error.", details) {
|
|
126
|
+
super(500, message, details);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
123
129
|
function isHttpErrorLike(error) {
|
|
124
130
|
if (typeof error !== "object" || error === null) {
|
|
125
131
|
return false;
|
|
@@ -326,11 +332,11 @@ function abilityCatalog() {
|
|
|
326
332
|
|
|
327
333
|
// ../../src/core/auth/guard.ts
|
|
328
334
|
function devHeaderAbilities(role) {
|
|
329
|
-
const
|
|
335
|
+
const catalog = abilityCatalog();
|
|
330
336
|
if (role === "admin") {
|
|
331
|
-
return [...
|
|
337
|
+
return [...catalog.admin];
|
|
332
338
|
}
|
|
333
|
-
return [...
|
|
339
|
+
return [...catalog.member];
|
|
334
340
|
}
|
|
335
341
|
|
|
336
342
|
class GuestGuard {
|
|
@@ -1144,7 +1150,7 @@ function resolveScimTenantFromToken(token) {
|
|
|
1144
1150
|
function resolveRepositoryConnection() {
|
|
1145
1151
|
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1146
1152
|
}
|
|
1147
|
-
var repositoryConnection = new Proxy(function
|
|
1153
|
+
var repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
1148
1154
|
apply(_target, _thisArg, args) {
|
|
1149
1155
|
return resolveRepositoryConnection()(...args);
|
|
1150
1156
|
},
|
|
@@ -1156,14 +1162,16 @@ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
|
1156
1162
|
});
|
|
1157
1163
|
|
|
1158
1164
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
1165
|
+
var TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
1159
1166
|
function readTenancyDriver(env = process.env) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1167
|
+
const raw = env.TENANCY_DRIVER?.trim();
|
|
1168
|
+
if (raw === undefined || raw === "") {
|
|
1169
|
+
return "rls";
|
|
1162
1170
|
}
|
|
1163
|
-
if (
|
|
1164
|
-
return
|
|
1171
|
+
if (TENANCY_DRIVERS.includes(raw)) {
|
|
1172
|
+
return raw;
|
|
1165
1173
|
}
|
|
1166
|
-
|
|
1174
|
+
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
1167
1175
|
}
|
|
1168
1176
|
function isTenancyEnabled(env = process.env) {
|
|
1169
1177
|
return readTenancyDriver(env) !== "none";
|
|
@@ -1202,8 +1210,8 @@ async function runWithTenantDatabase(tenant, callback) {
|
|
|
1202
1210
|
return await runWithTenant(tenant, callback);
|
|
1203
1211
|
}
|
|
1204
1212
|
if (hasActiveDatabaseConnection()) {
|
|
1205
|
-
const
|
|
1206
|
-
await applyTenantContextToTransaction(
|
|
1213
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
1214
|
+
await applyTenantContextToTransaction(activeConnection, tenant.id);
|
|
1207
1215
|
return await runWithTenant(tenant, callback);
|
|
1208
1216
|
}
|
|
1209
1217
|
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
@@ -1864,14 +1872,55 @@ function getPostgresSqlState(error) {
|
|
|
1864
1872
|
}
|
|
1865
1873
|
return;
|
|
1866
1874
|
}
|
|
1875
|
+
var MYSQL_ERRNO_MESSAGES = {
|
|
1876
|
+
1062: () => new ConflictError("A record with these values already exists."),
|
|
1877
|
+
1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
|
|
1878
|
+
1452: () => new UnprocessableEntityError("Referenced record does not exist."),
|
|
1879
|
+
1048: () => new BadRequestError("Required field is missing."),
|
|
1880
|
+
3819: () => new BadRequestError("Value violates a database constraint.")
|
|
1881
|
+
};
|
|
1882
|
+
function mapSqliteError(error) {
|
|
1883
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
1884
|
+
if (!code.startsWith("SQLITE_")) {
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
1888
|
+
return new ConflictError("A record with these values already exists.");
|
|
1889
|
+
}
|
|
1890
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
|
|
1891
|
+
return new UnprocessableEntityError("Referenced record does not exist.");
|
|
1892
|
+
}
|
|
1893
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") {
|
|
1894
|
+
return new BadRequestError("Required field is missing.");
|
|
1895
|
+
}
|
|
1896
|
+
if (code.startsWith("SQLITE_CONSTRAINT")) {
|
|
1897
|
+
return new BadRequestError("Value violates a database constraint.");
|
|
1898
|
+
}
|
|
1899
|
+
return new InternalServerError("Database operation failed.");
|
|
1900
|
+
}
|
|
1901
|
+
function mapMysqlError(error) {
|
|
1902
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
1903
|
+
if (!code.startsWith("ER_")) {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
|
|
1907
|
+
return factory ? factory() : new InternalServerError("Database operation failed.");
|
|
1908
|
+
}
|
|
1867
1909
|
function mapDatabaseError(error) {
|
|
1868
1910
|
const httpError = toHttpError(error);
|
|
1869
1911
|
if (httpError) {
|
|
1870
1912
|
return httpError;
|
|
1871
1913
|
}
|
|
1872
1914
|
if (!isPostgresError(error)) {
|
|
1873
|
-
|
|
1874
|
-
|
|
1915
|
+
return new InternalServerError;
|
|
1916
|
+
}
|
|
1917
|
+
const sqlite = mapSqliteError(error);
|
|
1918
|
+
if (sqlite) {
|
|
1919
|
+
return sqlite;
|
|
1920
|
+
}
|
|
1921
|
+
const mysql = mapMysqlError(error);
|
|
1922
|
+
if (mysql) {
|
|
1923
|
+
return mysql;
|
|
1875
1924
|
}
|
|
1876
1925
|
const sqlState = getPostgresSqlState(error);
|
|
1877
1926
|
switch (sqlState) {
|
|
@@ -1892,10 +1941,7 @@ function mapDatabaseError(error) {
|
|
|
1892
1941
|
constraint: error.constraint
|
|
1893
1942
|
});
|
|
1894
1943
|
default:
|
|
1895
|
-
return new
|
|
1896
|
-
code: error.code,
|
|
1897
|
-
sqlState
|
|
1898
|
-
});
|
|
1944
|
+
return new InternalServerError("Database operation failed.");
|
|
1899
1945
|
}
|
|
1900
1946
|
}
|
|
1901
1947
|
async function withDatabaseErrorHandling(operation) {
|
|
@@ -1964,6 +2010,9 @@ var postgresDialect = {
|
|
|
1964
2010
|
nowExpression() {
|
|
1965
2011
|
return "NOW()";
|
|
1966
2012
|
},
|
|
2013
|
+
timestampValue(value) {
|
|
2014
|
+
return value.toISOString();
|
|
2015
|
+
},
|
|
1967
2016
|
returningClause(columns) {
|
|
1968
2017
|
return ` RETURNING ${columns}`;
|
|
1969
2018
|
},
|
|
@@ -1988,6 +2037,9 @@ var mysqlDialect = {
|
|
|
1988
2037
|
nowExpression() {
|
|
1989
2038
|
return "CURRENT_TIMESTAMP";
|
|
1990
2039
|
},
|
|
2040
|
+
timestampValue(value) {
|
|
2041
|
+
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
2042
|
+
},
|
|
1991
2043
|
returningClause() {
|
|
1992
2044
|
return "";
|
|
1993
2045
|
},
|
|
@@ -2010,7 +2062,10 @@ var sqliteDialect = {
|
|
|
2010
2062
|
return `"${assertSafeIdentifier(identifier)}"`;
|
|
2011
2063
|
},
|
|
2012
2064
|
nowExpression() {
|
|
2013
|
-
return "
|
|
2065
|
+
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
2066
|
+
},
|
|
2067
|
+
timestampValue(value) {
|
|
2068
|
+
return value.toISOString();
|
|
2014
2069
|
},
|
|
2015
2070
|
returningClause(columns) {
|
|
2016
2071
|
return ` RETURNING ${columns}`;
|
|
@@ -2042,6 +2097,9 @@ function useSqlDialect(driver) {
|
|
|
2042
2097
|
dialectOverride = dialectFor(driver);
|
|
2043
2098
|
return dialectOverride;
|
|
2044
2099
|
}
|
|
2100
|
+
function sqlTimestamp(value = new Date) {
|
|
2101
|
+
return currentSqlDialect().timestampValue(value);
|
|
2102
|
+
}
|
|
2045
2103
|
function resetSqlDialect() {
|
|
2046
2104
|
dialectOverride = null;
|
|
2047
2105
|
}
|
|
@@ -2316,8 +2374,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
2316
2374
|
}
|
|
2317
2375
|
return select.map((item) => {
|
|
2318
2376
|
if (item.kind === "column") {
|
|
2319
|
-
const
|
|
2320
|
-
return item.as ? `${
|
|
2377
|
+
const column = qualifyColumn(item.table, item.column);
|
|
2378
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
2321
2379
|
}
|
|
2322
2380
|
if (item.kind === "literalText") {
|
|
2323
2381
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -2966,50 +3024,50 @@ class RepositoryQuery {
|
|
|
2966
3024
|
}
|
|
2967
3025
|
async hydrateEagerLoad(rows, result, load) {
|
|
2968
3026
|
if (load.kind === "hasMany") {
|
|
2969
|
-
const
|
|
2970
|
-
const
|
|
3027
|
+
const relation = load.relation;
|
|
3028
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
|
|
2971
3029
|
for (const row of result) {
|
|
2972
|
-
row[load.as] = getByRelationKey(
|
|
3030
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2973
3031
|
}
|
|
2974
3032
|
return;
|
|
2975
3033
|
}
|
|
2976
3034
|
if (load.kind === "morphMany") {
|
|
2977
|
-
const
|
|
2978
|
-
const
|
|
3035
|
+
const relation = load.relation;
|
|
3036
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
|
|
2979
3037
|
for (const row of result) {
|
|
2980
|
-
row[load.as] = getByRelationKey(
|
|
3038
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2981
3039
|
}
|
|
2982
3040
|
return;
|
|
2983
3041
|
}
|
|
2984
3042
|
if (load.kind === "morphOne") {
|
|
2985
|
-
const
|
|
2986
|
-
const
|
|
3043
|
+
const relation = load.relation;
|
|
3044
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
|
|
2987
3045
|
for (const row of result) {
|
|
2988
|
-
row[load.as] = getByRelationKey(
|
|
3046
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
|
|
2989
3047
|
}
|
|
2990
3048
|
return;
|
|
2991
3049
|
}
|
|
2992
3050
|
if (load.kind === "hasManyThrough") {
|
|
2993
|
-
const
|
|
2994
|
-
const
|
|
3051
|
+
const relation = load.relation;
|
|
3052
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
|
|
2995
3053
|
for (const row of result) {
|
|
2996
|
-
row[load.as] = getByRelationKey(
|
|
3054
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
2997
3055
|
}
|
|
2998
3056
|
return;
|
|
2999
3057
|
}
|
|
3000
3058
|
if (load.kind === "belongsToMany") {
|
|
3001
|
-
const
|
|
3002
|
-
const
|
|
3059
|
+
const relation = load.relation;
|
|
3060
|
+
const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
|
|
3003
3061
|
for (const row of result) {
|
|
3004
|
-
row[load.as] = getByRelationKey(
|
|
3062
|
+
row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
|
|
3005
3063
|
}
|
|
3006
3064
|
return;
|
|
3007
3065
|
}
|
|
3008
3066
|
if (load.kind === "morphTo") {
|
|
3009
|
-
const
|
|
3010
|
-
const
|
|
3067
|
+
const relation = load.relation;
|
|
3068
|
+
const grouped = await this.repository.loadMorphToForChildren(rows, relation, load.morphRepositories ?? new Map, load.options);
|
|
3011
3069
|
for (const row of result) {
|
|
3012
|
-
row[load.as] = getByRelationKey(
|
|
3070
|
+
row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
|
|
3013
3071
|
}
|
|
3014
3072
|
return;
|
|
3015
3073
|
}
|
|
@@ -4946,8 +5004,8 @@ class Model {
|
|
|
4946
5004
|
}
|
|
4947
5005
|
if (updating) {
|
|
4948
5006
|
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
4949
|
-
const
|
|
4950
|
-
this.attributes = ModelClass.hydrateAttributes(
|
|
5007
|
+
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
5008
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
4951
5009
|
await runObservers(this, "updated");
|
|
4952
5010
|
await runObservers(this, "saved");
|
|
4953
5011
|
return this;
|
|
@@ -5177,11 +5235,26 @@ function createMysqlConnectionFromPool(pool) {
|
|
|
5177
5235
|
}
|
|
5178
5236
|
};
|
|
5179
5237
|
}
|
|
5238
|
+
var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
|
|
5239
|
+
function pinSessionToUtc(connection) {
|
|
5240
|
+
connection.query(MYSQL_SESSION_UTC, (error) => {
|
|
5241
|
+
if (error) {
|
|
5242
|
+
console.warn(`[mysql] Could not set the session time zone to UTC; DATETIME comparisons may drift: ${error instanceof Error ? error.message : String(error)}`);
|
|
5243
|
+
}
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5246
|
+
function createMysqlPool(url) {
|
|
5247
|
+
const pool = mysql.createPool({ uri: url, timezone: "Z" });
|
|
5248
|
+
pool.on("connection", (connection) => {
|
|
5249
|
+
pinSessionToUtc(connection);
|
|
5250
|
+
});
|
|
5251
|
+
return pool;
|
|
5252
|
+
}
|
|
5180
5253
|
function createMysqlConnection(url) {
|
|
5181
5254
|
if (!url.trim()) {
|
|
5182
5255
|
throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
|
|
5183
5256
|
}
|
|
5184
|
-
return createMysqlConnectionFromPool(
|
|
5257
|
+
return createMysqlConnectionFromPool(createMysqlPool(url));
|
|
5185
5258
|
}
|
|
5186
5259
|
// ../../src/core/database/namedConnections.ts
|
|
5187
5260
|
var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
|
|
@@ -5845,6 +5918,11 @@ function createSqliteConnection(filename) {
|
|
|
5845
5918
|
}
|
|
5846
5919
|
const db = new Database(filename, { create: true });
|
|
5847
5920
|
db.exec("PRAGMA foreign_keys = ON");
|
|
5921
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
5922
|
+
if (filename !== ":memory:") {
|
|
5923
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
5924
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
5925
|
+
}
|
|
5848
5926
|
return {
|
|
5849
5927
|
async unsafe(query, params = []) {
|
|
5850
5928
|
const statement = db.query(query);
|
|
@@ -6257,20 +6335,48 @@ function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
|
6257
6335
|
return await next();
|
|
6258
6336
|
};
|
|
6259
6337
|
}
|
|
6338
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
6339
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
6340
|
+
function runWithRequestMeta(meta, callback) {
|
|
6341
|
+
return requestMetaContext.run(meta, callback);
|
|
6342
|
+
}
|
|
6343
|
+
function currentRequestMeta() {
|
|
6344
|
+
return requestMetaContext.getStore() ?? {
|
|
6345
|
+
ipAddress: null,
|
|
6346
|
+
userAgent: null
|
|
6347
|
+
};
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6260
6350
|
// ../../src/core/http/clientIp.ts
|
|
6261
6351
|
function trustForwardedFor(env = process.env) {
|
|
6262
6352
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
6263
6353
|
}
|
|
6264
|
-
function
|
|
6265
|
-
|
|
6266
|
-
|
|
6354
|
+
function isPrivateAddress(address) {
|
|
6355
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
6356
|
+
return ip === "::1" || ip === "localhost" || /^127\./.test(ip) || /^10\./.test(ip) || /^192\.168\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip) || /^169\.254\./.test(ip) || /^f[cd][0-9a-f]{2}:/i.test(ip) || /^fe80:/i.test(ip);
|
|
6357
|
+
}
|
|
6358
|
+
function forwardedClientIp(request) {
|
|
6359
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
6360
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
6361
|
+
const hop = hops[index];
|
|
6362
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
6363
|
+
return hop;
|
|
6364
|
+
}
|
|
6267
6365
|
}
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
return forwarded;
|
|
6366
|
+
if (hops.length > 0) {
|
|
6367
|
+
return hops[hops.length - 1];
|
|
6271
6368
|
}
|
|
6272
6369
|
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
6273
6370
|
}
|
|
6371
|
+
function readClientIp(request, env = process.env) {
|
|
6372
|
+
if (trustForwardedFor(env)) {
|
|
6373
|
+
const forwarded = forwardedClientIp(request);
|
|
6374
|
+
if (forwarded) {
|
|
6375
|
+
return forwarded;
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
6379
|
+
}
|
|
6274
6380
|
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
6275
6381
|
function nonCryptographicDigest(input) {
|
|
6276
6382
|
return Bun.hash(input).toString(16);
|
|
@@ -6572,10 +6678,18 @@ function readRequestCookie(request, name) {
|
|
|
6572
6678
|
function readBunRequestCookie(request, name) {
|
|
6573
6679
|
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
6574
6680
|
}
|
|
6681
|
+
// ../../src/core/runtime/appEnv.ts
|
|
6682
|
+
function isProductionEnv(env = process.env) {
|
|
6683
|
+
return env.APP_ENV === "production" || env.NODE_ENV === "production";
|
|
6684
|
+
}
|
|
6685
|
+
|
|
6575
6686
|
// ../../src/core/http/corsMiddleware.ts
|
|
6687
|
+
function defaultAllowedOrigins() {
|
|
6688
|
+
return isProductionEnv() ? "" : "*";
|
|
6689
|
+
}
|
|
6576
6690
|
function resolveCorsConfig() {
|
|
6577
6691
|
return {
|
|
6578
|
-
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ??
|
|
6692
|
+
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
6579
6693
|
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
6580
6694
|
allowedHeaders: [
|
|
6581
6695
|
"Authorization",
|
|
@@ -6615,30 +6729,19 @@ function buildCorsHeaders(request) {
|
|
|
6615
6729
|
const origin = request.headers.get("origin");
|
|
6616
6730
|
const corsConfig = resolveCorsConfig();
|
|
6617
6731
|
const allowedOrigins = corsConfig.allowedOrigins;
|
|
6618
|
-
|
|
6732
|
+
headers.set("Vary", "Origin");
|
|
6733
|
+
const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
|
|
6734
|
+
if (!allowOrigin) {
|
|
6735
|
+
return headers;
|
|
6736
|
+
}
|
|
6619
6737
|
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
6620
6738
|
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
6621
6739
|
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
6622
6740
|
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
6623
|
-
headers.set("Vary", "Origin");
|
|
6624
6741
|
return headers;
|
|
6625
6742
|
}
|
|
6626
6743
|
// ../../src/core/http/csrfToken.ts
|
|
6627
6744
|
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
6628
|
-
|
|
6629
|
-
// ../../src/core/http/requestMetaContext.ts
|
|
6630
|
-
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
6631
|
-
function runWithRequestMeta(meta, callback) {
|
|
6632
|
-
return requestMetaContext.run(meta, callback);
|
|
6633
|
-
}
|
|
6634
|
-
function currentRequestMeta() {
|
|
6635
|
-
return requestMetaContext.getStore() ?? {
|
|
6636
|
-
ipAddress: null,
|
|
6637
|
-
userAgent: null
|
|
6638
|
-
};
|
|
6639
|
-
}
|
|
6640
|
-
|
|
6641
|
-
// ../../src/core/http/csrfToken.ts
|
|
6642
6745
|
var CSRF_COOKIE = appCookieName("csrf");
|
|
6643
6746
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
6644
6747
|
function csrfCookieName() {
|
|
@@ -6920,9 +7023,9 @@ class FormRequest {
|
|
|
6920
7023
|
}
|
|
6921
7024
|
}
|
|
6922
7025
|
// ../../src/core/http/authMiddleware.ts
|
|
6923
|
-
function createAuthMiddleware(
|
|
7026
|
+
function createAuthMiddleware(auth) {
|
|
6924
7027
|
return async (request, next) => {
|
|
6925
|
-
const user = await
|
|
7028
|
+
const user = await auth.resolve(request);
|
|
6926
7029
|
return await runWithAuthUser(user, async () => {
|
|
6927
7030
|
const response = await next();
|
|
6928
7031
|
if (user) {
|
|
@@ -6939,9 +7042,9 @@ function createAuthMiddleware(auth2) {
|
|
|
6939
7042
|
};
|
|
6940
7043
|
}
|
|
6941
7044
|
// ../../src/core/http/authorizeMiddleware.ts
|
|
6942
|
-
function createAuthorizeMiddleware(gate,
|
|
7045
|
+
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
6943
7046
|
return async (request, next) => {
|
|
6944
|
-
const user = currentAuthUser() ?? await
|
|
7047
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
6945
7048
|
if (!gate.allows(resource, action, user)) {
|
|
6946
7049
|
const error = new ForbiddenError;
|
|
6947
7050
|
return Response.json({ error: error.message }, { status: error.status });
|
|
@@ -7115,9 +7218,9 @@ async function parseMultipartUpload(request, fieldName = "file") {
|
|
|
7115
7218
|
};
|
|
7116
7219
|
}
|
|
7117
7220
|
// ../../src/core/http/requireAuthMiddleware.ts
|
|
7118
|
-
function createRequireAuthMiddleware(
|
|
7221
|
+
function createRequireAuthMiddleware(auth) {
|
|
7119
7222
|
return async (request, next) => {
|
|
7120
|
-
if (!await
|
|
7223
|
+
if (!await auth.check(request)) {
|
|
7121
7224
|
const error = new UnauthorizedError;
|
|
7122
7225
|
return Response.json({ error: error.message }, { status: error.status });
|
|
7123
7226
|
}
|
|
@@ -7412,14 +7515,27 @@ function errorPageTitle(status, message) {
|
|
|
7412
7515
|
return message;
|
|
7413
7516
|
}
|
|
7414
7517
|
function publicErrorMessage(status, message) {
|
|
7415
|
-
if (status >= 500 &&
|
|
7518
|
+
if (status >= 500 && isProductionEnv()) {
|
|
7519
|
+
return "Something went wrong.";
|
|
7520
|
+
}
|
|
7416
7521
|
return message;
|
|
7417
7522
|
}
|
|
7523
|
+
function logServerError(error, mappedError) {
|
|
7524
|
+
if (mappedError.status < 500) {
|
|
7525
|
+
return;
|
|
7526
|
+
}
|
|
7527
|
+
appLogger.error("Unhandled request error", {
|
|
7528
|
+
status: mappedError.status,
|
|
7529
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7530
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
7531
|
+
});
|
|
7532
|
+
}
|
|
7418
7533
|
async function webErrorResponse(error, request) {
|
|
7419
7534
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
7420
7535
|
return null;
|
|
7421
7536
|
}
|
|
7422
7537
|
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
7538
|
+
logServerError(error, mappedError);
|
|
7423
7539
|
if (mappedError.status === 401) {
|
|
7424
7540
|
return Response.redirect(loginRedirectLocation(request), 302);
|
|
7425
7541
|
}
|
|
@@ -7448,6 +7564,7 @@ function noContentResponse() {
|
|
|
7448
7564
|
}
|
|
7449
7565
|
function errorResponse(error) {
|
|
7450
7566
|
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
7567
|
+
logServerError(error, mappedError);
|
|
7451
7568
|
return Response.json({
|
|
7452
7569
|
error: mappedError.message,
|
|
7453
7570
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
@@ -7513,8 +7630,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
7513
7630
|
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
7514
7631
|
const model = requireResolvedModel(await resolver(id, request));
|
|
7515
7632
|
const gate = resolveApplicationPolicyGate();
|
|
7516
|
-
const
|
|
7517
|
-
const user = currentAuthUser() ?? await
|
|
7633
|
+
const auth = resolveApplicationAuth();
|
|
7634
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
7518
7635
|
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
7519
7636
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
7520
7637
|
assertIfMatch(request, etagFromResource(model), {
|
|
@@ -7536,8 +7653,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
7536
7653
|
}
|
|
7537
7654
|
const model = requireResolvedModel(await resolver(key, request));
|
|
7538
7655
|
const gate = resolveApplicationPolicyGate();
|
|
7539
|
-
const
|
|
7540
|
-
const user = currentAuthUser() ?? await
|
|
7656
|
+
const auth = resolveApplicationAuth();
|
|
7657
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
7541
7658
|
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
7542
7659
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
7543
7660
|
assertIfMatch(request, etagFromResource(model), {
|
|
@@ -7859,9 +7976,9 @@ function createIntendedUrlCookieFromRequest(request) {
|
|
|
7859
7976
|
}
|
|
7860
7977
|
|
|
7861
7978
|
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
7862
|
-
function createRequireWebAuthMiddleware(
|
|
7979
|
+
function createRequireWebAuthMiddleware(auth) {
|
|
7863
7980
|
return async (request, next) => {
|
|
7864
|
-
const user = await
|
|
7981
|
+
const user = await auth.resolve(request);
|
|
7865
7982
|
if (user) {
|
|
7866
7983
|
return await runWithAuthUser(user, () => next());
|
|
7867
7984
|
}
|
|
@@ -8150,9 +8267,10 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
|
8150
8267
|
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
8151
8268
|
function createRequestLoggingMiddleware() {
|
|
8152
8269
|
return async (request, next) => {
|
|
8270
|
+
const ipAddress = readClientIp(request) ?? null;
|
|
8153
8271
|
return await runWithRequestMeta({
|
|
8154
8272
|
...currentRequestMeta(),
|
|
8155
|
-
ipAddress
|
|
8273
|
+
ipAddress,
|
|
8156
8274
|
userAgent: request.headers.get("user-agent"),
|
|
8157
8275
|
request
|
|
8158
8276
|
}, async () => {
|
|
@@ -8165,7 +8283,8 @@ function createRequestLoggingMiddleware() {
|
|
|
8165
8283
|
method: request.method,
|
|
8166
8284
|
path: new URL(request.url).pathname,
|
|
8167
8285
|
status: response.status,
|
|
8168
|
-
durationMs
|
|
8286
|
+
durationMs,
|
|
8287
|
+
ipAddress
|
|
8169
8288
|
});
|
|
8170
8289
|
return response;
|
|
8171
8290
|
});
|
|
@@ -8290,15 +8409,15 @@ function buildMarkdownMailMessage(input) {
|
|
|
8290
8409
|
html: rendered.html
|
|
8291
8410
|
};
|
|
8292
8411
|
}
|
|
8293
|
-
async function sendMarkdownMail(
|
|
8294
|
-
await
|
|
8412
|
+
async function sendMarkdownMail(mailer, input) {
|
|
8413
|
+
await mailer.send(buildMarkdownMailMessage(input));
|
|
8295
8414
|
}
|
|
8296
8415
|
// ../../src/core/notifications/dispatcher.ts
|
|
8297
8416
|
class NotificationDispatcher {
|
|
8298
8417
|
mailer;
|
|
8299
8418
|
databaseStore;
|
|
8300
|
-
constructor(
|
|
8301
|
-
this.mailer =
|
|
8419
|
+
constructor(mailer, databaseStore = null) {
|
|
8420
|
+
this.mailer = mailer;
|
|
8302
8421
|
this.databaseStore = databaseStore;
|
|
8303
8422
|
}
|
|
8304
8423
|
async send(notifiable, notification) {
|
|
@@ -8350,8 +8469,8 @@ class NotificationDispatcher {
|
|
|
8350
8469
|
});
|
|
8351
8470
|
}
|
|
8352
8471
|
}
|
|
8353
|
-
function createNotificationDispatcher(
|
|
8354
|
-
return new NotificationDispatcher(
|
|
8472
|
+
function createNotificationDispatcher(mailer, databaseStore) {
|
|
8473
|
+
return new NotificationDispatcher(mailer, databaseStore ?? null);
|
|
8355
8474
|
}
|
|
8356
8475
|
// ../../src/core/notifications/notification.ts
|
|
8357
8476
|
class Notification {
|
|
@@ -8481,9 +8600,9 @@ function readSharedJobRegistry() {
|
|
|
8481
8600
|
if (globalRegistry) {
|
|
8482
8601
|
return globalRegistry;
|
|
8483
8602
|
}
|
|
8484
|
-
const
|
|
8485
|
-
globalThis[JOB_REGISTRY_KEY] =
|
|
8486
|
-
return
|
|
8603
|
+
const registry = new JobRegistry;
|
|
8604
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
8605
|
+
return registry;
|
|
8487
8606
|
}
|
|
8488
8607
|
var jobRegistry = readSharedJobRegistry();
|
|
8489
8608
|
|
|
@@ -9298,9 +9417,9 @@ class EtaViewEngine {
|
|
|
9298
9417
|
autoTrim: false
|
|
9299
9418
|
});
|
|
9300
9419
|
this.resolveLayoutData = resolveLayoutData;
|
|
9301
|
-
const
|
|
9420
|
+
const readFile = this.eta.readFile?.bind(this.eta);
|
|
9302
9421
|
this.eta.readFile = (path) => {
|
|
9303
|
-
const source =
|
|
9422
|
+
const source = readFile ? readFile(path) : "";
|
|
9304
9423
|
assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
|
|
9305
9424
|
return source;
|
|
9306
9425
|
};
|
|
@@ -9406,6 +9525,7 @@ export {
|
|
|
9406
9525
|
HasManyRelationQuery,
|
|
9407
9526
|
HasOneRelationQuery,
|
|
9408
9527
|
HttpError,
|
|
9528
|
+
InternalServerError,
|
|
9409
9529
|
Job,
|
|
9410
9530
|
JsonResource,
|
|
9411
9531
|
JwtGuard,
|
|
@@ -9496,6 +9616,7 @@ export {
|
|
|
9496
9616
|
createMetricsMiddleware,
|
|
9497
9617
|
createMysqlConnection,
|
|
9498
9618
|
createMysqlConnectionFromPool,
|
|
9619
|
+
createMysqlPool,
|
|
9499
9620
|
createNotificationDispatcher,
|
|
9500
9621
|
createProductionQueue,
|
|
9501
9622
|
createQueue,
|
|
@@ -9678,6 +9799,7 @@ export {
|
|
|
9678
9799
|
signedUrl,
|
|
9679
9800
|
singularize,
|
|
9680
9801
|
spaContentSecurityPolicy,
|
|
9802
|
+
sqlTimestamp,
|
|
9681
9803
|
storageFacade as storage,
|
|
9682
9804
|
strictApiContentSecurityPolicy,
|
|
9683
9805
|
stringRule,
|