@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
|
@@ -298,8 +298,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
298
298
|
}
|
|
299
299
|
return select.map((item) => {
|
|
300
300
|
if (item.kind === "column") {
|
|
301
|
-
const
|
|
302
|
-
return item.as ? `${
|
|
301
|
+
const column = qualifyColumn(item.table, item.column);
|
|
302
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
303
303
|
}
|
|
304
304
|
if (item.kind === "literalText") {
|
|
305
305
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -1919,8 +1919,8 @@ class Model {
|
|
|
1919
1919
|
}
|
|
1920
1920
|
if (updating) {
|
|
1921
1921
|
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
1922
|
-
const
|
|
1923
|
-
this.attributes = ModelClass.hydrateAttributes(
|
|
1922
|
+
const record = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
1923
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1924
1924
|
await runObservers(this, "updated");
|
|
1925
1925
|
await runObservers(this, "saved");
|
|
1926
1926
|
return this;
|
|
@@ -23,13 +23,29 @@ function createMysqlConnectionFromPool(pool) {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
|
|
27
|
+
function pinSessionToUtc(connection) {
|
|
28
|
+
connection.query(MYSQL_SESSION_UTC, (error) => {
|
|
29
|
+
if (error) {
|
|
30
|
+
console.warn(`[mysql] Could not set the session time zone to UTC; DATETIME comparisons may drift: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function createMysqlPool(url) {
|
|
35
|
+
const pool = mysql.createPool({ uri: url, timezone: "Z" });
|
|
36
|
+
pool.on("connection", (connection) => {
|
|
37
|
+
pinSessionToUtc(connection);
|
|
38
|
+
});
|
|
39
|
+
return pool;
|
|
40
|
+
}
|
|
26
41
|
function createMysqlConnection(url) {
|
|
27
42
|
if (!url.trim()) {
|
|
28
43
|
throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
|
|
29
44
|
}
|
|
30
|
-
return createMysqlConnectionFromPool(
|
|
45
|
+
return createMysqlConnectionFromPool(createMysqlPool(url));
|
|
31
46
|
}
|
|
32
47
|
export {
|
|
33
48
|
createMysqlConnection,
|
|
34
|
-
createMysqlConnectionFromPool
|
|
49
|
+
createMysqlConnectionFromPool,
|
|
50
|
+
createMysqlPool
|
|
35
51
|
};
|
|
@@ -275,8 +275,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
275
275
|
}
|
|
276
276
|
return select.map((item) => {
|
|
277
277
|
if (item.kind === "column") {
|
|
278
|
-
const
|
|
279
|
-
return item.as ? `${
|
|
278
|
+
const column = qualifyColumn(item.table, item.column);
|
|
279
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
280
280
|
}
|
|
281
281
|
if (item.kind === "literalText") {
|
|
282
282
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -275,8 +275,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
275
275
|
}
|
|
276
276
|
return select.map((item) => {
|
|
277
277
|
if (item.kind === "column") {
|
|
278
|
-
const
|
|
279
|
-
return item.as ? `${
|
|
278
|
+
const column = qualifyColumn(item.table, item.column);
|
|
279
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
280
280
|
}
|
|
281
281
|
if (item.kind === "literalText") {
|
|
282
282
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -925,50 +925,50 @@ class RepositoryQuery {
|
|
|
925
925
|
}
|
|
926
926
|
async hydrateEagerLoad(rows, result, load) {
|
|
927
927
|
if (load.kind === "hasMany") {
|
|
928
|
-
const
|
|
929
|
-
const
|
|
928
|
+
const relation = load.relation;
|
|
929
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
|
|
930
930
|
for (const row of result) {
|
|
931
|
-
row[load.as] = getByRelationKey(
|
|
931
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
932
932
|
}
|
|
933
933
|
return;
|
|
934
934
|
}
|
|
935
935
|
if (load.kind === "morphMany") {
|
|
936
|
-
const
|
|
937
|
-
const
|
|
936
|
+
const relation = load.relation;
|
|
937
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
|
|
938
938
|
for (const row of result) {
|
|
939
|
-
row[load.as] = getByRelationKey(
|
|
939
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
940
940
|
}
|
|
941
941
|
return;
|
|
942
942
|
}
|
|
943
943
|
if (load.kind === "morphOne") {
|
|
944
|
-
const
|
|
945
|
-
const
|
|
944
|
+
const relation = load.relation;
|
|
945
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
|
|
946
946
|
for (const row of result) {
|
|
947
|
-
row[load.as] = getByRelationKey(
|
|
947
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
|
|
948
948
|
}
|
|
949
949
|
return;
|
|
950
950
|
}
|
|
951
951
|
if (load.kind === "hasManyThrough") {
|
|
952
|
-
const
|
|
953
|
-
const
|
|
952
|
+
const relation = load.relation;
|
|
953
|
+
const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
|
|
954
954
|
for (const row of result) {
|
|
955
|
-
row[load.as] = getByRelationKey(
|
|
955
|
+
row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
|
|
956
956
|
}
|
|
957
957
|
return;
|
|
958
958
|
}
|
|
959
959
|
if (load.kind === "belongsToMany") {
|
|
960
|
-
const
|
|
961
|
-
const
|
|
960
|
+
const relation = load.relation;
|
|
961
|
+
const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
|
|
962
962
|
for (const row of result) {
|
|
963
|
-
row[load.as] = getByRelationKey(
|
|
963
|
+
row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
|
|
964
964
|
}
|
|
965
965
|
return;
|
|
966
966
|
}
|
|
967
967
|
if (load.kind === "morphTo") {
|
|
968
|
-
const
|
|
969
|
-
const
|
|
968
|
+
const relation = load.relation;
|
|
969
|
+
const grouped = await this.repository.loadMorphToForChildren(rows, relation, load.morphRepositories ?? new Map, load.options);
|
|
970
970
|
for (const row of result) {
|
|
971
|
-
row[load.as] = getByRelationKey(
|
|
971
|
+
row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
|
|
972
972
|
}
|
|
973
973
|
return;
|
|
974
974
|
}
|
|
@@ -542,8 +542,8 @@ function buildSelectList(table, select, params = []) {
|
|
|
542
542
|
}
|
|
543
543
|
return select.map((item) => {
|
|
544
544
|
if (item.kind === "column") {
|
|
545
|
-
const
|
|
546
|
-
return item.as ? `${
|
|
545
|
+
const column = qualifyColumn(item.table, item.column);
|
|
546
|
+
return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
|
|
547
547
|
}
|
|
548
548
|
if (item.kind === "literalText") {
|
|
549
549
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
@@ -14,6 +14,11 @@ function createSqliteConnection(filename) {
|
|
|
14
14
|
}
|
|
15
15
|
const db = new Database(filename, { create: true });
|
|
16
16
|
db.exec("PRAGMA foreign_keys = ON");
|
|
17
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
18
|
+
if (filename !== ":memory:") {
|
|
19
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
20
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
21
|
+
}
|
|
17
22
|
return {
|
|
18
23
|
async unsafe(query, params = []) {
|
|
19
24
|
const statement = db.query(query);
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/clientIp.ts
|
|
3
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
3
4
|
function trustForwardedFor(env = process.env) {
|
|
4
5
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
5
6
|
}
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function isPrivateAddress(address) {
|
|
8
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
9
|
+
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);
|
|
10
|
+
}
|
|
11
|
+
function forwardedClientIp(request) {
|
|
12
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
13
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
14
|
+
const hop = hops[index];
|
|
15
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
16
|
+
return hop;
|
|
17
|
+
}
|
|
9
18
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return forwarded;
|
|
19
|
+
if (hops.length > 0) {
|
|
20
|
+
return hops[hops.length - 1];
|
|
13
21
|
}
|
|
14
22
|
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
15
23
|
}
|
|
24
|
+
function readClientIp(request, env = process.env) {
|
|
25
|
+
if (trustForwardedFor(env)) {
|
|
26
|
+
const forwarded = forwardedClientIp(request);
|
|
27
|
+
if (forwarded) {
|
|
28
|
+
return forwarded;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
32
|
+
}
|
|
16
33
|
export {
|
|
34
|
+
isPrivateAddress,
|
|
17
35
|
readClientIp,
|
|
18
36
|
trustForwardedFor
|
|
19
37
|
};
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/runtime/appEnv.ts
|
|
3
|
+
function isProductionEnv(env = process.env) {
|
|
4
|
+
return env.APP_ENV === "production" || env.NODE_ENV === "production";
|
|
5
|
+
}
|
|
6
|
+
|
|
2
7
|
// ../../src/core/http/corsMiddleware.ts
|
|
8
|
+
function defaultAllowedOrigins() {
|
|
9
|
+
return isProductionEnv() ? "" : "*";
|
|
10
|
+
}
|
|
3
11
|
function resolveCorsConfig() {
|
|
4
12
|
return {
|
|
5
|
-
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ??
|
|
13
|
+
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
6
14
|
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
7
15
|
allowedHeaders: [
|
|
8
16
|
"Authorization",
|
|
@@ -42,12 +50,15 @@ function buildCorsHeaders(request) {
|
|
|
42
50
|
const origin = request.headers.get("origin");
|
|
43
51
|
const corsConfig = resolveCorsConfig();
|
|
44
52
|
const allowedOrigins = corsConfig.allowedOrigins;
|
|
45
|
-
|
|
53
|
+
headers.set("Vary", "Origin");
|
|
54
|
+
const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
|
|
55
|
+
if (!allowOrigin) {
|
|
56
|
+
return headers;
|
|
57
|
+
}
|
|
46
58
|
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
47
59
|
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
48
60
|
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
49
61
|
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
50
|
-
headers.set("Vary", "Origin");
|
|
51
62
|
return headers;
|
|
52
63
|
}
|
|
53
64
|
export {
|
|
@@ -57,19 +57,36 @@ function sdkClientClassName() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ../../src/core/http/clientIp.ts
|
|
60
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
60
61
|
function trustForwardedFor(env = process.env) {
|
|
61
62
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
62
63
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
function isPrivateAddress(address) {
|
|
65
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
66
|
+
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);
|
|
67
|
+
}
|
|
68
|
+
function forwardedClientIp(request) {
|
|
69
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
70
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
71
|
+
const hop = hops[index];
|
|
72
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
73
|
+
return hop;
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return forwarded;
|
|
76
|
+
if (hops.length > 0) {
|
|
77
|
+
return hops[hops.length - 1];
|
|
70
78
|
}
|
|
71
79
|
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
72
80
|
}
|
|
81
|
+
function readClientIp(request, env = process.env) {
|
|
82
|
+
if (trustForwardedFor(env)) {
|
|
83
|
+
const forwarded = forwardedClientIp(request);
|
|
84
|
+
if (forwarded) {
|
|
85
|
+
return forwarded;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
89
|
+
}
|
|
73
90
|
|
|
74
91
|
// ../../src/core/runtime/frontendMode.ts
|
|
75
92
|
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
@@ -111,7 +128,7 @@ function readSpaPrefix() {
|
|
|
111
128
|
}
|
|
112
129
|
|
|
113
130
|
// ../../src/core/view/webErrorView.ts
|
|
114
|
-
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
131
|
+
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
115
132
|
|
|
116
133
|
// ../../src/core/view/htmlResponse.ts
|
|
117
134
|
function withCharset(contentType) {
|
|
@@ -212,7 +229,7 @@ async function renderWebErrorHtml(input) {
|
|
|
212
229
|
try {
|
|
213
230
|
return await render({
|
|
214
231
|
...input,
|
|
215
|
-
request: input.request ??
|
|
232
|
+
request: input.request ?? currentRequestMeta2().request
|
|
216
233
|
});
|
|
217
234
|
} catch {
|
|
218
235
|
return renderKernelErrorChrome(input);
|
|
@@ -54,19 +54,36 @@ function sdkClientClassName() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// ../../src/core/http/clientIp.ts
|
|
57
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
57
58
|
function trustForwardedFor(env = process.env) {
|
|
58
59
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
59
60
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
function isPrivateAddress(address) {
|
|
62
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
63
|
+
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);
|
|
64
|
+
}
|
|
65
|
+
function forwardedClientIp(request) {
|
|
66
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
67
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
68
|
+
const hop = hops[index];
|
|
69
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
70
|
+
return hop;
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return forwarded;
|
|
73
|
+
if (hops.length > 0) {
|
|
74
|
+
return hops[hops.length - 1];
|
|
67
75
|
}
|
|
68
76
|
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
69
77
|
}
|
|
78
|
+
function readClientIp(request, env = process.env) {
|
|
79
|
+
if (trustForwardedFor(env)) {
|
|
80
|
+
const forwarded = forwardedClientIp(request);
|
|
81
|
+
if (forwarded) {
|
|
82
|
+
return forwarded;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
86
|
+
}
|
|
70
87
|
|
|
71
88
|
// ../../src/core/runtime/frontendMode.ts
|
|
72
89
|
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
@@ -108,7 +125,7 @@ function readSpaPrefix() {
|
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
// ../../src/core/view/webErrorView.ts
|
|
111
|
-
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
128
|
+
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
112
129
|
|
|
113
130
|
// ../../src/core/view/htmlResponse.ts
|
|
114
131
|
function withCharset(contentType) {
|
|
@@ -209,7 +226,7 @@ async function renderWebErrorHtml(input) {
|
|
|
209
226
|
try {
|
|
210
227
|
return await render({
|
|
211
228
|
...input,
|
|
212
|
-
request: input.request ??
|
|
229
|
+
request: input.request ?? currentRequestMeta2().request
|
|
213
230
|
});
|
|
214
231
|
} catch {
|
|
215
232
|
return renderKernelErrorChrome(input);
|
|
@@ -6,6 +6,7 @@ import { toHttpError as toHttpError3 } from "@getstrata/core/errors/http";
|
|
|
6
6
|
import {
|
|
7
7
|
BadRequestError,
|
|
8
8
|
ConflictError,
|
|
9
|
+
InternalServerError,
|
|
9
10
|
toHttpError,
|
|
10
11
|
UnprocessableEntityError
|
|
11
12
|
} from "@getstrata/core/errors/http";
|
|
@@ -24,14 +25,55 @@ function getPostgresSqlState(error) {
|
|
|
24
25
|
}
|
|
25
26
|
return;
|
|
26
27
|
}
|
|
28
|
+
var MYSQL_ERRNO_MESSAGES = {
|
|
29
|
+
1062: () => new ConflictError("A record with these values already exists."),
|
|
30
|
+
1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
|
|
31
|
+
1452: () => new UnprocessableEntityError("Referenced record does not exist."),
|
|
32
|
+
1048: () => new BadRequestError("Required field is missing."),
|
|
33
|
+
3819: () => new BadRequestError("Value violates a database constraint.")
|
|
34
|
+
};
|
|
35
|
+
function mapSqliteError(error) {
|
|
36
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
37
|
+
if (!code.startsWith("SQLITE_")) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
41
|
+
return new ConflictError("A record with these values already exists.");
|
|
42
|
+
}
|
|
43
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
|
|
44
|
+
return new UnprocessableEntityError("Referenced record does not exist.");
|
|
45
|
+
}
|
|
46
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") {
|
|
47
|
+
return new BadRequestError("Required field is missing.");
|
|
48
|
+
}
|
|
49
|
+
if (code.startsWith("SQLITE_CONSTRAINT")) {
|
|
50
|
+
return new BadRequestError("Value violates a database constraint.");
|
|
51
|
+
}
|
|
52
|
+
return new InternalServerError("Database operation failed.");
|
|
53
|
+
}
|
|
54
|
+
function mapMysqlError(error) {
|
|
55
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
56
|
+
if (!code.startsWith("ER_")) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
|
|
60
|
+
return factory ? factory() : new InternalServerError("Database operation failed.");
|
|
61
|
+
}
|
|
27
62
|
function mapDatabaseError(error) {
|
|
28
63
|
const httpError = toHttpError(error);
|
|
29
64
|
if (httpError) {
|
|
30
65
|
return httpError;
|
|
31
66
|
}
|
|
32
67
|
if (!isPostgresError(error)) {
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
return new InternalServerError;
|
|
69
|
+
}
|
|
70
|
+
const sqlite = mapSqliteError(error);
|
|
71
|
+
if (sqlite) {
|
|
72
|
+
return sqlite;
|
|
73
|
+
}
|
|
74
|
+
const mysql = mapMysqlError(error);
|
|
75
|
+
if (mysql) {
|
|
76
|
+
return mysql;
|
|
35
77
|
}
|
|
36
78
|
const sqlState = getPostgresSqlState(error);
|
|
37
79
|
switch (sqlState) {
|
|
@@ -52,10 +94,7 @@ function mapDatabaseError(error) {
|
|
|
52
94
|
constraint: error.constraint
|
|
53
95
|
});
|
|
54
96
|
default:
|
|
55
|
-
return new
|
|
56
|
-
code: error.code,
|
|
57
|
-
sqlState
|
|
58
|
-
});
|
|
97
|
+
return new InternalServerError("Database operation failed.");
|
|
59
98
|
}
|
|
60
99
|
}
|
|
61
100
|
async function withDatabaseErrorHandling(operation) {
|
|
@@ -68,6 +107,12 @@ async function withDatabaseErrorHandling(operation) {
|
|
|
68
107
|
|
|
69
108
|
// ../../src/core/http/webErrorResponse.ts
|
|
70
109
|
import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
|
|
110
|
+
import { appLogger } from "@getstrata/core/logging/logger";
|
|
111
|
+
|
|
112
|
+
// ../../src/core/runtime/appEnv.ts
|
|
113
|
+
function isProductionEnv(env = process.env) {
|
|
114
|
+
return env.APP_ENV === "production" || env.NODE_ENV === "production";
|
|
115
|
+
}
|
|
71
116
|
|
|
72
117
|
// ../../src/core/runtime/frontendMode.ts
|
|
73
118
|
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
@@ -319,14 +364,27 @@ function errorPageTitle(status, message) {
|
|
|
319
364
|
return message;
|
|
320
365
|
}
|
|
321
366
|
function publicErrorMessage(status, message) {
|
|
322
|
-
if (status >= 500 &&
|
|
367
|
+
if (status >= 500 && isProductionEnv()) {
|
|
368
|
+
return "Something went wrong.";
|
|
369
|
+
}
|
|
323
370
|
return message;
|
|
324
371
|
}
|
|
372
|
+
function logServerError(error, mappedError) {
|
|
373
|
+
if (mappedError.status < 500) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
appLogger.error("Unhandled request error", {
|
|
377
|
+
status: mappedError.status,
|
|
378
|
+
error: error instanceof Error ? error.message : String(error),
|
|
379
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
380
|
+
});
|
|
381
|
+
}
|
|
325
382
|
async function webErrorResponse(error, request) {
|
|
326
383
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
327
384
|
return null;
|
|
328
385
|
}
|
|
329
386
|
const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
|
|
387
|
+
logServerError(error, mappedError);
|
|
330
388
|
if (mappedError.status === 401) {
|
|
331
389
|
return Response.redirect(loginRedirectLocation(request), 302);
|
|
332
390
|
}
|
|
@@ -355,6 +413,7 @@ function noContentResponse() {
|
|
|
355
413
|
}
|
|
356
414
|
function errorResponse(error) {
|
|
357
415
|
const mappedError = toHttpError3(error) ?? mapDatabaseError(error);
|
|
416
|
+
logServerError(error, mappedError);
|
|
358
417
|
return Response.json({
|
|
359
418
|
error: mappedError.message,
|
|
360
419
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
@@ -57,19 +57,36 @@ function sdkClientClassName() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ../../src/core/http/clientIp.ts
|
|
60
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
60
61
|
function trustForwardedFor(env = process.env) {
|
|
61
62
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
62
63
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
function isPrivateAddress(address) {
|
|
65
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
66
|
+
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);
|
|
67
|
+
}
|
|
68
|
+
function forwardedClientIp(request) {
|
|
69
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
70
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
71
|
+
const hop = hops[index];
|
|
72
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
73
|
+
return hop;
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return forwarded;
|
|
76
|
+
if (hops.length > 0) {
|
|
77
|
+
return hops[hops.length - 1];
|
|
70
78
|
}
|
|
71
79
|
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
72
80
|
}
|
|
81
|
+
function readClientIp(request, env = process.env) {
|
|
82
|
+
if (trustForwardedFor(env)) {
|
|
83
|
+
const forwarded = forwardedClientIp(request);
|
|
84
|
+
if (forwarded) {
|
|
85
|
+
return forwarded;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
89
|
+
}
|
|
73
90
|
|
|
74
91
|
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
75
92
|
var memoryBuckets = new Map;
|
|
@@ -59,19 +59,36 @@ function sdkClientClassName() {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// ../../src/core/http/clientIp.ts
|
|
62
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
62
63
|
function trustForwardedFor(env = process.env) {
|
|
63
64
|
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
64
65
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
function isPrivateAddress(address) {
|
|
67
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
68
|
+
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);
|
|
69
|
+
}
|
|
70
|
+
function forwardedClientIp(request) {
|
|
71
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
72
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
73
|
+
const hop = hops[index];
|
|
74
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
75
|
+
return hop;
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return forwarded;
|
|
78
|
+
if (hops.length > 0) {
|
|
79
|
+
return hops[hops.length - 1];
|
|
72
80
|
}
|
|
73
81
|
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
74
82
|
}
|
|
83
|
+
function readClientIp(request, env = process.env) {
|
|
84
|
+
if (trustForwardedFor(env)) {
|
|
85
|
+
const forwarded = forwardedClientIp(request);
|
|
86
|
+
if (forwarded) {
|
|
87
|
+
return forwarded;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
91
|
+
}
|
|
75
92
|
|
|
76
93
|
// ../../src/core/runtime/frontendMode.ts
|
|
77
94
|
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
@@ -113,7 +130,7 @@ function readSpaPrefix() {
|
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
// ../../src/core/view/webErrorView.ts
|
|
116
|
-
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
133
|
+
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
117
134
|
|
|
118
135
|
// ../../src/core/view/htmlResponse.ts
|
|
119
136
|
function withCharset(contentType) {
|
|
@@ -214,7 +231,7 @@ async function renderWebErrorHtml(input) {
|
|
|
214
231
|
try {
|
|
215
232
|
return await render({
|
|
216
233
|
...input,
|
|
217
|
-
request: input.request ??
|
|
234
|
+
request: input.request ?? currentRequestMeta2().request
|
|
218
235
|
});
|
|
219
236
|
} catch {
|
|
220
237
|
return renderKernelErrorChrome(input);
|