@getstrata/core 2.2.40 → 2.2.41
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 +8 -0
- package/dist/entries/auth/membershipMiddleware.js +1 -113
- package/dist/entries/auth/membershipScope.js +0 -36
- package/dist/entries/auth/membershipService.js +1 -43
- package/dist/entries/http/memoryThrottleMiddleware.js +3 -1
- package/dist/entries/http/response.js +4 -299
- package/dist/entries/http/scimThrottleMiddleware.js +3 -1
- package/dist/entries/http/throttleMiddleware.js +3 -1
- package/dist/index.js +8 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.41
|
|
4
|
+
|
|
5
|
+
Make framework HTML/JSON error responses non-storeable, including configured/fallback renderers and custom-body 404 responses; retain success-response cache policy and add packed header regressions.
|
|
6
|
+
|
|
7
|
+
Fix packaged error wrappers ignoring the configured application error renderer; preserve API JSON precedence and add packed configured/default rendering regressions.
|
|
8
|
+
|
|
9
|
+
Fix packaged membership middleware and default service ignoring the configured application lookup. Use the canonical membership context while preserving explicit service injection and request isolation. Add mandatory packed external-app coverage for concurrent users, replacement configuration and propagated lookup failures.
|
|
10
|
+
|
|
3
11
|
## 2.2.40
|
|
4
12
|
|
|
5
13
|
Fix shared DNS fixture state in packaged outbound consumers. safeFetch and GitHub OAuth now use the canonical safeUrl resolver while retaining DNS pinning, private-address validation and request deadlines. Add mandatory packed external-app regressions for resolver replacement and private-target refusal; document controlled-provider qualification limits.
|
|
@@ -1,118 +1,6 @@
|
|
|
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/auth/membershipContext.ts
|
|
30
|
-
init_asyncContextStore();
|
|
31
|
-
|
|
32
|
-
// ../../src/core/auth/accessControl.ts
|
|
33
|
-
import { ForbiddenError } from "@getstrata/core/errors/http";
|
|
34
|
-
|
|
35
|
-
// ../../src/core/auth/authContext.ts
|
|
36
|
-
init_asyncContextStore();
|
|
37
|
-
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
38
|
-
var credentialSourceContext = createAsyncContextStore("@getstrata/credentialSource");
|
|
39
|
-
function currentAuthUser() {
|
|
40
|
-
return authContext.getStore() ?? null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ../../src/core/auth/accessControl.ts
|
|
44
|
-
var ROLE_RANK = {
|
|
45
|
-
member: 1,
|
|
46
|
-
admin: 2,
|
|
47
|
-
owner: 3
|
|
48
|
-
};
|
|
49
|
-
function isGlobalAdmin(user) {
|
|
50
|
-
return user?.role === "admin";
|
|
51
|
-
}
|
|
52
|
-
function hasMinimumOrgRole(role, minimum) {
|
|
53
|
-
if (!role) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
return ROLE_RANK[role] >= ROLE_RANK[minimum];
|
|
57
|
-
}
|
|
58
|
-
function requireAuthenticatedUser() {
|
|
59
|
-
const user = currentAuthUser();
|
|
60
|
-
if (!user) {
|
|
61
|
-
throw new ForbiddenError("Authentication required.");
|
|
62
|
-
}
|
|
63
|
-
return user;
|
|
64
|
-
}
|
|
65
|
-
function resolveUserId(user) {
|
|
66
|
-
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
67
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
68
|
-
throw new ForbiddenError("Invalid authenticated user.");
|
|
69
|
-
}
|
|
70
|
-
return userId;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ../../src/core/auth/membershipContext.ts
|
|
74
|
-
var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
|
|
75
|
-
var uninitializedMembershipLookup = {
|
|
76
|
-
async listForUser() {
|
|
77
|
-
return [];
|
|
78
|
-
},
|
|
79
|
-
async findMembership() {
|
|
80
|
-
return null;
|
|
81
|
-
},
|
|
82
|
-
async listForOrganization() {
|
|
83
|
-
return [];
|
|
84
|
-
},
|
|
85
|
-
async addMember() {
|
|
86
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
87
|
-
},
|
|
88
|
-
async removeMember() {
|
|
89
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
90
|
-
},
|
|
91
|
-
async updateMemberRole() {
|
|
92
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
var membershipRepository = uninitializedMembershipLookup;
|
|
96
|
-
function resolveMembershipLookup() {
|
|
97
|
-
return membershipRepository;
|
|
98
|
-
}
|
|
99
|
-
async function runWithMembershipContext(callback) {
|
|
100
|
-
const user = currentAuthUser();
|
|
101
|
-
if (!user || isGlobalAdmin(user)) {
|
|
102
|
-
return await callback();
|
|
103
|
-
}
|
|
104
|
-
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
105
|
-
const context = {
|
|
106
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
107
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
108
|
-
};
|
|
109
|
-
return await membershipContext.run(context, callback);
|
|
110
|
-
}
|
|
111
|
-
function currentOrganizationIds() {
|
|
112
|
-
return membershipContext.getStore()?.organizationIds ?? [];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
2
|
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
3
|
+
import { runWithMembershipContext } from "@getstrata/core/auth/membershipContext";
|
|
116
4
|
function createMembershipContextMiddleware() {
|
|
117
5
|
return async (_request, next) => {
|
|
118
6
|
return await runWithMembershipContext(async () => await next());
|
|
@@ -74,42 +74,6 @@ function resolveUserId(user) {
|
|
|
74
74
|
// ../../src/core/auth/membershipContext.ts
|
|
75
75
|
init_asyncContextStore();
|
|
76
76
|
var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
|
|
77
|
-
var uninitializedMembershipLookup = {
|
|
78
|
-
async listForUser() {
|
|
79
|
-
return [];
|
|
80
|
-
},
|
|
81
|
-
async findMembership() {
|
|
82
|
-
return null;
|
|
83
|
-
},
|
|
84
|
-
async listForOrganization() {
|
|
85
|
-
return [];
|
|
86
|
-
},
|
|
87
|
-
async addMember() {
|
|
88
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
89
|
-
},
|
|
90
|
-
async removeMember() {
|
|
91
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
92
|
-
},
|
|
93
|
-
async updateMemberRole() {
|
|
94
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
var membershipRepository = uninitializedMembershipLookup;
|
|
98
|
-
function resolveMembershipLookup() {
|
|
99
|
-
return membershipRepository;
|
|
100
|
-
}
|
|
101
|
-
async function runWithMembershipContext(callback) {
|
|
102
|
-
const user = currentAuthUser();
|
|
103
|
-
if (!user || isGlobalAdmin(user)) {
|
|
104
|
-
return await callback();
|
|
105
|
-
}
|
|
106
|
-
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
107
|
-
const context = {
|
|
108
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
109
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
110
|
-
};
|
|
111
|
-
return await membershipContext.run(context, callback);
|
|
112
|
-
}
|
|
113
77
|
function currentOrganizationIds() {
|
|
114
78
|
return membershipContext.getStore()?.organizationIds ?? [];
|
|
115
79
|
}
|
|
@@ -27,6 +27,7 @@ function createAsyncContextStore(key) {
|
|
|
27
27
|
var init_asyncContextStore = () => {};
|
|
28
28
|
|
|
29
29
|
// ../../src/core/auth/membershipService.ts
|
|
30
|
+
import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
|
|
30
31
|
import { ForbiddenError as ForbiddenError2 } from "@getstrata/core/errors/http";
|
|
31
32
|
|
|
32
33
|
// ../../src/core/auth/accessControl.ts
|
|
@@ -70,49 +71,6 @@ function resolveUserId(user) {
|
|
|
70
71
|
return userId;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
// ../../src/core/auth/membershipContext.ts
|
|
74
|
-
init_asyncContextStore();
|
|
75
|
-
var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
|
|
76
|
-
var uninitializedMembershipLookup = {
|
|
77
|
-
async listForUser() {
|
|
78
|
-
return [];
|
|
79
|
-
},
|
|
80
|
-
async findMembership() {
|
|
81
|
-
return null;
|
|
82
|
-
},
|
|
83
|
-
async listForOrganization() {
|
|
84
|
-
return [];
|
|
85
|
-
},
|
|
86
|
-
async addMember() {
|
|
87
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
88
|
-
},
|
|
89
|
-
async removeMember() {
|
|
90
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
91
|
-
},
|
|
92
|
-
async updateMemberRole() {
|
|
93
|
-
throw new Error("configureMembershipLookup() must be called before mutating memberships.");
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
var membershipRepository = uninitializedMembershipLookup;
|
|
97
|
-
function resolveMembershipLookup() {
|
|
98
|
-
return membershipRepository;
|
|
99
|
-
}
|
|
100
|
-
async function runWithMembershipContext(callback) {
|
|
101
|
-
const user = currentAuthUser();
|
|
102
|
-
if (!user || isGlobalAdmin(user)) {
|
|
103
|
-
return await callback();
|
|
104
|
-
}
|
|
105
|
-
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
106
|
-
const context = {
|
|
107
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
108
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
109
|
-
};
|
|
110
|
-
return await membershipContext.run(context, callback);
|
|
111
|
-
}
|
|
112
|
-
function currentOrganizationIds() {
|
|
113
|
-
return membershipContext.getStore()?.organizationIds ?? [];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
74
|
// ../../src/core/auth/resolveMembershipService.ts
|
|
117
75
|
import { resolveApplicationDependencies } from "@getstrata/core/runtime/applicationRegistry";
|
|
118
76
|
function resolveMembershipService() {
|
|
@@ -403,7 +403,9 @@ async function renderWebErrorHtml(input) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
async function htmlErrorResponse(input) {
|
|
406
|
-
|
|
406
|
+
const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
407
|
+
response.headers.set("Cache-Control", "no-store");
|
|
408
|
+
return response;
|
|
407
409
|
}
|
|
408
410
|
|
|
409
411
|
// ../../src/core/http/contentNegotiation.ts
|
|
@@ -121,7 +121,8 @@ var init_transactionControl = __esm(() => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// ../../src/core/http/response.ts
|
|
124
|
-
import { toHttpError as
|
|
124
|
+
import { toHttpError as toHttpError2 } from "@getstrata/core/errors/http";
|
|
125
|
+
import { logServerError, webErrorResponse } from "@getstrata/core/http/webErrorResponse";
|
|
125
126
|
|
|
126
127
|
// ../../src/core/database/errors.ts
|
|
127
128
|
import {
|
|
@@ -258,302 +259,6 @@ function currentRequestMeta() {
|
|
|
258
259
|
};
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
// ../../src/core/http/webErrorResponse.ts
|
|
262
|
-
import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
|
|
263
|
-
import { appLogger } from "@getstrata/core/logging/logger";
|
|
264
|
-
|
|
265
|
-
// ../../src/core/runtime/appEnv.ts
|
|
266
|
-
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
267
|
-
function normalizeEnvValue(value) {
|
|
268
|
-
return (value ?? "").trim().toLowerCase();
|
|
269
|
-
}
|
|
270
|
-
function isProductionEnv(env = process.env) {
|
|
271
|
-
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
272
|
-
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
273
|
-
if (appEnv === "production" || nodeEnv === "production") {
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
if (appEnv === "") {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
280
|
-
}
|
|
281
|
-
function envFlagEnabled(value) {
|
|
282
|
-
return value === "true";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ../../src/core/runtime/frontendMode.ts
|
|
286
|
-
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
287
|
-
var DEFAULT_SPA_PREFIX = "/app";
|
|
288
|
-
var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
|
|
289
|
-
function parseFrontendMode(value) {
|
|
290
|
-
const mode = (value ?? "api").trim();
|
|
291
|
-
if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
|
|
292
|
-
return mode;
|
|
293
|
-
}
|
|
294
|
-
return "api";
|
|
295
|
-
}
|
|
296
|
-
function readFrontendMode() {
|
|
297
|
-
return parseFrontendMode(process.env.FRONTEND_MODE);
|
|
298
|
-
}
|
|
299
|
-
function isViewsMode(mode) {
|
|
300
|
-
return mode === "server-htmx" || mode === "hybrid";
|
|
301
|
-
}
|
|
302
|
-
function isSpaMode(mode) {
|
|
303
|
-
return mode === "spa-react" || mode === "hybrid";
|
|
304
|
-
}
|
|
305
|
-
function isViewsEnabled() {
|
|
306
|
-
return isViewsMode(readFrontendMode());
|
|
307
|
-
}
|
|
308
|
-
function isSpaEnabled() {
|
|
309
|
-
return isSpaMode(readFrontendMode());
|
|
310
|
-
}
|
|
311
|
-
function normalizeSpaPrefix(value) {
|
|
312
|
-
const raw = (value ?? DEFAULT_SPA_PREFIX).trim() || DEFAULT_SPA_PREFIX;
|
|
313
|
-
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
314
|
-
const trimmed = withSlash.replace(/\/+$/, "");
|
|
315
|
-
if (trimmed.length === 0 || trimmed === "/") {
|
|
316
|
-
return DEFAULT_SPA_PREFIX;
|
|
317
|
-
}
|
|
318
|
-
return trimmed;
|
|
319
|
-
}
|
|
320
|
-
function readSpaPrefix() {
|
|
321
|
-
return normalizeSpaPrefix(process.env.SPA_PREFIX);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ../../src/core/view/webErrorView.ts
|
|
325
|
-
import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
|
|
326
|
-
|
|
327
|
-
// ../../src/core/http/safeInternalPath.ts
|
|
328
|
-
var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
|
|
329
|
-
function looksLikeExternalTarget(value) {
|
|
330
|
-
const trimmed = value.trim();
|
|
331
|
-
if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
if (trimmed.includes("://") || trimmed.includes(":/") || trimmed.includes(":\\")) {
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
const decoded = decodeURIComponent(trimmed);
|
|
339
|
-
if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
} catch {
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
function sanitizeNestedQuery(search) {
|
|
348
|
-
if (!search) {
|
|
349
|
-
return "";
|
|
350
|
-
}
|
|
351
|
-
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
352
|
-
const next = new URLSearchParams;
|
|
353
|
-
for (const [key, value] of params.entries()) {
|
|
354
|
-
if (NESTED_REDIRECT_KEYS.has(key)) {
|
|
355
|
-
const sanitized = sanitizeInternalPath(value, "/");
|
|
356
|
-
next.set(key, sanitized);
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
if (looksLikeExternalTarget(value) && value.includes("://")) {
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
next.set(key, value);
|
|
363
|
-
}
|
|
364
|
-
const serialized = next.toString();
|
|
365
|
-
return serialized ? `?${serialized}` : "";
|
|
366
|
-
}
|
|
367
|
-
function sanitizeInternalPath(raw, fallback = "/") {
|
|
368
|
-
const trimmed = raw.trim();
|
|
369
|
-
if (looksLikeExternalTarget(trimmed)) {
|
|
370
|
-
return fallback;
|
|
371
|
-
}
|
|
372
|
-
const queryIndex = trimmed.indexOf("?");
|
|
373
|
-
const hashIndex = trimmed.indexOf("#");
|
|
374
|
-
const end = [queryIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
375
|
-
const pathname = end === undefined ? trimmed : trimmed.slice(0, end);
|
|
376
|
-
if (looksLikeExternalTarget(pathname)) {
|
|
377
|
-
return fallback;
|
|
378
|
-
}
|
|
379
|
-
const search = queryIndex >= 0 ? trimmed.slice(queryIndex, hashIndex >= 0 ? hashIndex : undefined) : "";
|
|
380
|
-
return `${pathname}${sanitizeNestedQuery(search)}`;
|
|
381
|
-
}
|
|
382
|
-
function safeInternalRedirectPath(request, fallback = "/") {
|
|
383
|
-
const url = new URL(request.url);
|
|
384
|
-
return sanitizeInternalPath(`${url.pathname}${url.search}`, fallback);
|
|
385
|
-
}
|
|
386
|
-
function loginRedirectLocation(request) {
|
|
387
|
-
return `/login?redirect=${encodeURIComponent(safeInternalRedirectPath(request))}`;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// ../../src/core/view/htmlResponse.ts
|
|
391
|
-
function htmlResponse(html, init = {}) {
|
|
392
|
-
return new Response(html, {
|
|
393
|
-
status: init.status ?? 200,
|
|
394
|
-
statusText: init.statusText,
|
|
395
|
-
headers: {
|
|
396
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ../../src/core/view/webErrorView.ts
|
|
402
|
-
var configuredErrorView = {};
|
|
403
|
-
function escapeHtml(value) {
|
|
404
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
405
|
-
}
|
|
406
|
-
function errorReturnPath(request) {
|
|
407
|
-
if (!request)
|
|
408
|
-
return "/";
|
|
409
|
-
try {
|
|
410
|
-
const current = new URL(request.url);
|
|
411
|
-
const previous = new URL(request.headers.get("referer") ?? "");
|
|
412
|
-
if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
|
|
413
|
-
return "/";
|
|
414
|
-
return sanitizeInternalPath(previous.pathname, "/");
|
|
415
|
-
} catch {
|
|
416
|
-
return "/";
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function renderKernelErrorChrome(input) {
|
|
420
|
-
const title = escapeHtml(input.title);
|
|
421
|
-
const message = escapeHtml(input.message);
|
|
422
|
-
const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
423
|
-
const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
|
|
424
|
-
const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta2().request))}">Go back</a></p>` : "";
|
|
425
|
-
return `<!doctype html>
|
|
426
|
-
<html lang="en">
|
|
427
|
-
<head>
|
|
428
|
-
<meta charset="UTF-8" />
|
|
429
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
430
|
-
<title>${title}</title>
|
|
431
|
-
<link rel="stylesheet" href="/assets/app.css" />
|
|
432
|
-
</head>
|
|
433
|
-
<body>
|
|
434
|
-
<header class="site-header">
|
|
435
|
-
<a class="brand" href="/">Home</a>
|
|
436
|
-
</header>
|
|
437
|
-
<main class="site-main">
|
|
438
|
-
<section class="page-header">
|
|
439
|
-
<h1>${title}</h1>
|
|
440
|
-
<p>${message}</p>
|
|
441
|
-
${details}
|
|
442
|
-
${goBack}
|
|
443
|
-
</section>
|
|
444
|
-
</main>
|
|
445
|
-
</body>
|
|
446
|
-
</html>
|
|
447
|
-
`;
|
|
448
|
-
}
|
|
449
|
-
async function renderWebErrorHtml(input) {
|
|
450
|
-
const render = configuredErrorView.render;
|
|
451
|
-
if (!render) {
|
|
452
|
-
return renderKernelErrorChrome(input);
|
|
453
|
-
}
|
|
454
|
-
try {
|
|
455
|
-
return await render({
|
|
456
|
-
...input,
|
|
457
|
-
request: input.request ?? currentRequestMeta2().request
|
|
458
|
-
});
|
|
459
|
-
} catch {
|
|
460
|
-
return renderKernelErrorChrome(input);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
async function htmlErrorResponse(input) {
|
|
464
|
-
return htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// ../../src/core/http/contentNegotiation.ts
|
|
468
|
-
function requestPrefersJson(request) {
|
|
469
|
-
if (!request) {
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
if (request.headers.get("HX-Request") === "true") {
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
const pathname = new URL(request.url).pathname;
|
|
476
|
-
if (pathname.startsWith("/api/")) {
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
480
|
-
if (accept.includes("text/html")) {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
if (accept.includes("application/json")) {
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
487
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// ../../src/core/http/webErrorResponse.ts
|
|
494
|
-
function normalizeFieldErrors(details) {
|
|
495
|
-
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
496
|
-
return {};
|
|
497
|
-
}
|
|
498
|
-
const errors = {};
|
|
499
|
-
for (const [field, messages] of Object.entries(details)) {
|
|
500
|
-
if (Array.isArray(messages)) {
|
|
501
|
-
errors[field] = messages.map(String);
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
if (typeof messages === "string") {
|
|
505
|
-
errors[field] = [messages];
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return errors;
|
|
509
|
-
}
|
|
510
|
-
function errorPageTitle(status, message) {
|
|
511
|
-
if (status === 404) {
|
|
512
|
-
return "Not Found";
|
|
513
|
-
}
|
|
514
|
-
if (status === 403) {
|
|
515
|
-
return "Forbidden";
|
|
516
|
-
}
|
|
517
|
-
if (status >= 500) {
|
|
518
|
-
return "Server Error";
|
|
519
|
-
}
|
|
520
|
-
return message;
|
|
521
|
-
}
|
|
522
|
-
function publicErrorMessage(status, message) {
|
|
523
|
-
if (status >= 500 && isProductionEnv()) {
|
|
524
|
-
return "Something went wrong.";
|
|
525
|
-
}
|
|
526
|
-
return message;
|
|
527
|
-
}
|
|
528
|
-
function logServerError(error, mappedError) {
|
|
529
|
-
if (mappedError.status < 500) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
appLogger.error("Unhandled request error", {
|
|
533
|
-
status: mappedError.status,
|
|
534
|
-
error: error instanceof Error ? error.message : String(error),
|
|
535
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
async function webErrorResponse(error, request) {
|
|
539
|
-
if (currentRequestMeta().errorFormat === "json" || !request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
|
|
543
|
-
logServerError(error, mappedError);
|
|
544
|
-
if (mappedError.status === 401) {
|
|
545
|
-
return Response.redirect(loginRedirectLocation(request), 302);
|
|
546
|
-
}
|
|
547
|
-
const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
548
|
-
return htmlErrorResponse({
|
|
549
|
-
status: mappedError.status,
|
|
550
|
-
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
551
|
-
message: publicErrorMessage(mappedError.status, mappedError.message),
|
|
552
|
-
errors,
|
|
553
|
-
request
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
262
|
// ../../src/core/http/response.ts
|
|
558
263
|
function jsonResponse(data, init = {}) {
|
|
559
264
|
return Response.json(data, {
|
|
@@ -568,12 +273,12 @@ function noContentResponse() {
|
|
|
568
273
|
return new Response(null, { status: 204 });
|
|
569
274
|
}
|
|
570
275
|
function errorResponse(error) {
|
|
571
|
-
const mappedError =
|
|
276
|
+
const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
|
|
572
277
|
logServerError(error, mappedError);
|
|
573
278
|
return Response.json({
|
|
574
279
|
error: mappedError.message,
|
|
575
280
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
576
|
-
}, { status: mappedError.status });
|
|
281
|
+
}, { status: mappedError.status, headers: { "Cache-Control": "no-store" } });
|
|
577
282
|
}
|
|
578
283
|
function withErrorHandling(handler) {
|
|
579
284
|
return async (...args) => {
|
|
@@ -457,7 +457,9 @@ async function renderWebErrorHtml(input) {
|
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
459
|
async function htmlErrorResponse(input) {
|
|
460
|
-
|
|
460
|
+
const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
461
|
+
response.headers.set("Cache-Control", "no-store");
|
|
462
|
+
return response;
|
|
461
463
|
}
|
|
462
464
|
|
|
463
465
|
// ../../src/core/http/contentNegotiation.ts
|
|
@@ -342,7 +342,9 @@ async function renderWebErrorHtml(input) {
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
async function htmlErrorResponse(input) {
|
|
345
|
-
|
|
345
|
+
const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
346
|
+
response.headers.set("Cache-Control", "no-store");
|
|
347
|
+
return response;
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
// ../../src/core/http/contentNegotiation.ts
|
package/dist/index.js
CHANGED
|
@@ -12745,9 +12745,6 @@ function toPaginatedResourceCollection(items, meta, transformer) {
|
|
|
12745
12745
|
meta
|
|
12746
12746
|
};
|
|
12747
12747
|
}
|
|
12748
|
-
// ../../src/core/http/response.ts
|
|
12749
|
-
init_transactionControl();
|
|
12750
|
-
|
|
12751
12748
|
// ../../src/core/http/safeInternalPath.ts
|
|
12752
12749
|
var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
|
|
12753
12750
|
function looksLikeExternalTarget(value) {
|
|
@@ -12933,11 +12930,15 @@ async function renderWebErrorHtml(input) {
|
|
|
12933
12930
|
}
|
|
12934
12931
|
}
|
|
12935
12932
|
async function htmlErrorResponse(input) {
|
|
12936
|
-
|
|
12933
|
+
const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
12934
|
+
response.headers.set("Cache-Control", "no-store");
|
|
12935
|
+
return response;
|
|
12937
12936
|
}
|
|
12938
12937
|
async function notFoundHtmlResponse(body) {
|
|
12939
12938
|
if (body !== undefined) {
|
|
12940
|
-
|
|
12939
|
+
const response = htmlResponse(body, { status: 404 });
|
|
12940
|
+
response.headers.set("Cache-Control", "no-store");
|
|
12941
|
+
return response;
|
|
12941
12942
|
}
|
|
12942
12943
|
return htmlErrorResponse({
|
|
12943
12944
|
status: 404,
|
|
@@ -13037,6 +13038,7 @@ async function webErrorResponse(error, request) {
|
|
|
13037
13038
|
}
|
|
13038
13039
|
|
|
13039
13040
|
// ../../src/core/http/response.ts
|
|
13041
|
+
init_transactionControl();
|
|
13040
13042
|
function jsonResponse2(data, init = {}) {
|
|
13041
13043
|
return Response.json(data, {
|
|
13042
13044
|
status: init.status ?? 200,
|
|
@@ -13055,7 +13057,7 @@ function errorResponse(error) {
|
|
|
13055
13057
|
return Response.json({
|
|
13056
13058
|
error: mappedError.message,
|
|
13057
13059
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
13058
|
-
}, { status: mappedError.status });
|
|
13060
|
+
}, { status: mappedError.status, headers: { "Cache-Control": "no-store" } });
|
|
13059
13061
|
}
|
|
13060
13062
|
function withErrorHandling(handler) {
|
|
13061
13063
|
return async (...args) => {
|