@getstrata/core 2.2.41 → 2.2.42
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 +6 -0
- package/dist/core/http/response.d.ts +3 -1
- package/dist/core/view/index.d.ts +1 -1
- package/dist/core/view/viewRenderer.d.ts +6 -2
- package/dist/entries/http/authorizeMiddleware.js +2 -1
- package/dist/entries/http/bodySizeLimitMiddleware.js +2 -1
- package/dist/entries/http/requireAbilityMiddleware.js +3 -2
- package/dist/entries/http/requireAuthMiddleware.js +2 -1
- package/dist/entries/http/requireGlobalAdminMiddleware.js +2 -1
- package/dist/entries/http/requirePasswordConfirmMiddleware.js +4 -2
- package/dist/entries/http/requireVerifiedMiddleware.js +3 -1
- package/dist/entries/http/response.js +6 -0
- package/dist/framework/public-api.d.ts +2 -2
- package/dist/index.js +1299 -1276
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11545,98 +11545,32 @@ function mail() {
|
|
|
11545
11545
|
function storageFacade() {
|
|
11546
11546
|
return storage();
|
|
11547
11547
|
}
|
|
11548
|
-
// ../../src/core/
|
|
11549
|
-
var
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
const body = request.body;
|
|
11556
|
-
if (!body)
|
|
11557
|
-
return state;
|
|
11558
|
-
let consumed = 0;
|
|
11559
|
-
const stream = body.pipeThrough(new TransformStream({
|
|
11560
|
-
transform(chunk, controller) {
|
|
11561
|
-
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
11562
|
-
consumed += bytes.byteLength;
|
|
11563
|
-
if (consumed > maxBytes) {
|
|
11564
|
-
state.exceeded = true;
|
|
11565
|
-
throw new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
11566
|
-
}
|
|
11567
|
-
controller.enqueue(bytes);
|
|
11568
|
-
}
|
|
11569
|
-
}));
|
|
11570
|
-
const bounded = new Request(request, { body: stream });
|
|
11571
|
-
const decorateClone = (clone) => {
|
|
11572
|
-
for (const field of ["params", "cookies"]) {
|
|
11573
|
-
if (field in request)
|
|
11574
|
-
Object.defineProperty(clone, field, { value: Reflect.get(request, field) });
|
|
11575
|
-
}
|
|
11576
|
-
const nativeClone = () => Reflect.apply(Request.prototype.clone, clone, []);
|
|
11577
|
-
Object.defineProperty(clone, "clone", {
|
|
11578
|
-
value: () => decorateClone(nativeClone())
|
|
11579
|
-
});
|
|
11580
|
-
guards.set(clone, state);
|
|
11581
|
-
return clone;
|
|
11582
|
-
};
|
|
11583
|
-
const descriptors = {
|
|
11584
|
-
body: { configurable: true, get: () => bounded.body },
|
|
11585
|
-
bodyUsed: { configurable: true, get: () => bounded.bodyUsed },
|
|
11586
|
-
clone: {
|
|
11587
|
-
configurable: true,
|
|
11588
|
-
value: () => decorateClone(Reflect.apply(Request.prototype.clone, bounded, []))
|
|
11589
|
-
}
|
|
11590
|
-
};
|
|
11591
|
-
for (const method of [
|
|
11592
|
-
"text",
|
|
11593
|
-
"json",
|
|
11594
|
-
"formData",
|
|
11595
|
-
"arrayBuffer",
|
|
11596
|
-
"blob",
|
|
11597
|
-
"bytes",
|
|
11598
|
-
"textStream"
|
|
11599
|
-
]) {
|
|
11600
|
-
descriptors[method] = { configurable: true, value: bounded[method].bind(bounded) };
|
|
11548
|
+
// ../../src/core/runtime/frontendMode.ts
|
|
11549
|
+
var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
|
|
11550
|
+
var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
|
|
11551
|
+
function parseFrontendMode(value) {
|
|
11552
|
+
const mode = (value ?? "api").trim();
|
|
11553
|
+
if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
|
|
11554
|
+
return mode;
|
|
11601
11555
|
}
|
|
11602
|
-
|
|
11603
|
-
guards.set(request, state);
|
|
11604
|
-
return state;
|
|
11556
|
+
return "api";
|
|
11605
11557
|
}
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
11609
|
-
if (!raw) {
|
|
11610
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
11611
|
-
}
|
|
11612
|
-
const parsed = Number(raw);
|
|
11613
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
11614
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
11615
|
-
}
|
|
11616
|
-
return parsed;
|
|
11558
|
+
function readFrontendMode() {
|
|
11559
|
+
return parseFrontendMode(process.env.FRONTEND_MODE);
|
|
11617
11560
|
}
|
|
11618
|
-
function
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
const state = guardBody(request, maxBytes);
|
|
11630
|
-
try {
|
|
11631
|
-
const response = await next();
|
|
11632
|
-
return state.exceeded ? tooLarge() : response;
|
|
11633
|
-
} catch (error) {
|
|
11634
|
-
if (state.exceeded || error instanceof PayloadTooLargeError)
|
|
11635
|
-
return tooLarge();
|
|
11636
|
-
throw error;
|
|
11637
|
-
}
|
|
11638
|
-
};
|
|
11561
|
+
function isViewsMode(mode) {
|
|
11562
|
+
return mode === "server-htmx" || mode === "hybrid";
|
|
11563
|
+
}
|
|
11564
|
+
function isSpaMode(mode) {
|
|
11565
|
+
return mode === "spa-react" || mode === "hybrid";
|
|
11566
|
+
}
|
|
11567
|
+
function isViewsEnabled() {
|
|
11568
|
+
return isViewsMode(readFrontendMode());
|
|
11569
|
+
}
|
|
11570
|
+
function isSpaEnabled() {
|
|
11571
|
+
return isSpaMode(readFrontendMode());
|
|
11639
11572
|
}
|
|
11573
|
+
|
|
11640
11574
|
// ../../src/core/http/requestMetaContext.ts
|
|
11641
11575
|
init_asyncContextStore();
|
|
11642
11576
|
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
@@ -11650,1428 +11584,1500 @@ function currentRequestMeta() {
|
|
|
11650
11584
|
};
|
|
11651
11585
|
}
|
|
11652
11586
|
|
|
11653
|
-
// ../../src/core/http/
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
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);
|
|
11660
|
-
}
|
|
11661
|
-
function forwardedClientIp(request) {
|
|
11662
|
-
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
11663
|
-
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
11664
|
-
const hop = hops[index];
|
|
11665
|
-
if (hop && !isPrivateAddress(hop)) {
|
|
11666
|
-
return hop;
|
|
11667
|
-
}
|
|
11587
|
+
// ../../src/core/http/safeInternalPath.ts
|
|
11588
|
+
var NESTED_REDIRECT_KEYS = new Set(["redirect", "next", "return", "returnTo", "return_to"]);
|
|
11589
|
+
function looksLikeExternalTarget(value) {
|
|
11590
|
+
const trimmed = value.trim();
|
|
11591
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
11592
|
+
return true;
|
|
11668
11593
|
}
|
|
11669
|
-
if (
|
|
11670
|
-
return
|
|
11594
|
+
if (trimmed.includes("://") || trimmed.includes(":/") || trimmed.includes(":\\")) {
|
|
11595
|
+
return true;
|
|
11671
11596
|
}
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
const forwarded = forwardedClientIp(request);
|
|
11677
|
-
if (forwarded) {
|
|
11678
|
-
return forwarded;
|
|
11597
|
+
try {
|
|
11598
|
+
const decoded = decodeURIComponent(trimmed);
|
|
11599
|
+
if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
|
|
11600
|
+
return true;
|
|
11679
11601
|
}
|
|
11602
|
+
} catch {
|
|
11603
|
+
return true;
|
|
11680
11604
|
}
|
|
11681
|
-
return
|
|
11605
|
+
return false;
|
|
11682
11606
|
}
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11607
|
+
function sanitizeNestedQuery(search) {
|
|
11608
|
+
if (!search) {
|
|
11609
|
+
return "";
|
|
11610
|
+
}
|
|
11611
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
11612
|
+
const next = new URLSearchParams;
|
|
11613
|
+
for (const [key, value] of params.entries()) {
|
|
11614
|
+
if (NESTED_REDIRECT_KEYS.has(key)) {
|
|
11615
|
+
const sanitized = sanitizeInternalPath(value, "/");
|
|
11616
|
+
next.set(key, sanitized);
|
|
11617
|
+
continue;
|
|
11618
|
+
}
|
|
11619
|
+
if (looksLikeExternalTarget(value) && value.includes("://")) {
|
|
11620
|
+
continue;
|
|
11621
|
+
}
|
|
11622
|
+
next.set(key, value);
|
|
11623
|
+
}
|
|
11624
|
+
const serialized = next.toString();
|
|
11625
|
+
return serialized ? `?${serialized}` : "";
|
|
11686
11626
|
}
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11627
|
+
function sanitizeInternalPath(raw, fallback = "/") {
|
|
11628
|
+
const trimmed = raw.trim();
|
|
11629
|
+
if (looksLikeExternalTarget(trimmed)) {
|
|
11630
|
+
return fallback;
|
|
11631
|
+
}
|
|
11632
|
+
const queryIndex = trimmed.indexOf("?");
|
|
11633
|
+
const hashIndex = trimmed.indexOf("#");
|
|
11634
|
+
const end = [queryIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
11635
|
+
const pathname = end === undefined ? trimmed : trimmed.slice(0, end);
|
|
11636
|
+
if (looksLikeExternalTarget(pathname)) {
|
|
11637
|
+
return fallback;
|
|
11638
|
+
}
|
|
11639
|
+
const search = queryIndex >= 0 ? trimmed.slice(queryIndex, hashIndex >= 0 ? hashIndex : undefined) : "";
|
|
11640
|
+
return `${pathname}${sanitizeNestedQuery(search)}`;
|
|
11691
11641
|
}
|
|
11692
|
-
function
|
|
11693
|
-
|
|
11642
|
+
function safeInternalRedirectPath(request, fallback = "/") {
|
|
11643
|
+
const url = new URL(request.url);
|
|
11644
|
+
return sanitizeInternalPath(`${url.pathname}${url.search}`, fallback);
|
|
11694
11645
|
}
|
|
11695
|
-
function
|
|
11696
|
-
|
|
11697
|
-
return formatWeakEtag(digest);
|
|
11646
|
+
function loginRedirectLocation(request) {
|
|
11647
|
+
return `/login?redirect=${encodeURIComponent(safeInternalRedirectPath(request))}`;
|
|
11698
11648
|
}
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
return formatWeakEtag(digest);
|
|
11649
|
+
|
|
11650
|
+
// ../../src/core/view/htmlResponse.ts
|
|
11651
|
+
function withCharset(contentType) {
|
|
11652
|
+
return contentType.includes("charset=") ? contentType : `${contentType}; charset=utf-8`;
|
|
11704
11653
|
}
|
|
11705
|
-
function
|
|
11706
|
-
return
|
|
11654
|
+
function htmlResponse(html, init = {}) {
|
|
11655
|
+
return new Response(html, {
|
|
11656
|
+
status: init.status ?? 200,
|
|
11657
|
+
statusText: init.statusText,
|
|
11658
|
+
headers: {
|
|
11659
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
11660
|
+
}
|
|
11661
|
+
});
|
|
11707
11662
|
}
|
|
11708
|
-
function
|
|
11709
|
-
return
|
|
11663
|
+
function isHtmxRequest(request) {
|
|
11664
|
+
return request.headers.get("HX-Request") === "true";
|
|
11710
11665
|
}
|
|
11711
|
-
function
|
|
11712
|
-
if (!
|
|
11713
|
-
|
|
11666
|
+
function redirectResponse(location, status = 302) {
|
|
11667
|
+
if (!location.startsWith("/") || location.startsWith("//")) {
|
|
11668
|
+
throw new Error("Redirect location must be an internal path.");
|
|
11714
11669
|
}
|
|
11715
|
-
return
|
|
11670
|
+
return new Response(null, {
|
|
11671
|
+
status,
|
|
11672
|
+
headers: {
|
|
11673
|
+
Location: sanitizeInternalPath(location, "/")
|
|
11674
|
+
}
|
|
11675
|
+
});
|
|
11716
11676
|
}
|
|
11717
|
-
function
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
}
|
|
11725
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
11677
|
+
function textResponse(body, init = {}) {
|
|
11678
|
+
return new Response(body, {
|
|
11679
|
+
status: init.status ?? 200,
|
|
11680
|
+
headers: {
|
|
11681
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
11682
|
+
}
|
|
11683
|
+
});
|
|
11726
11684
|
}
|
|
11727
|
-
function
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11685
|
+
function xmlResponse(body, init = {}) {
|
|
11686
|
+
return new Response(body, {
|
|
11687
|
+
status: init.status ?? 200,
|
|
11688
|
+
headers: {
|
|
11689
|
+
"Content-Type": withCharset(init.contentType ?? "application/xml")
|
|
11690
|
+
}
|
|
11691
|
+
});
|
|
11692
|
+
}
|
|
11693
|
+
function rssResponse(body, init = {}) {
|
|
11694
|
+
return xmlResponse(body, { ...init, contentType: "application/rss+xml" });
|
|
11695
|
+
}
|
|
11696
|
+
|
|
11697
|
+
// ../../src/core/view/webErrorView.ts
|
|
11698
|
+
var configuredErrorView = {};
|
|
11699
|
+
function configureWebErrorView(options) {
|
|
11700
|
+
configuredErrorView = { ...options };
|
|
11701
|
+
}
|
|
11702
|
+
function escapeHtml(value) {
|
|
11703
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
11704
|
+
}
|
|
11705
|
+
function errorReturnPath(request) {
|
|
11706
|
+
if (!request)
|
|
11707
|
+
return "/";
|
|
11708
|
+
try {
|
|
11709
|
+
const current = new URL(request.url);
|
|
11710
|
+
const previous = new URL(request.headers.get("referer") ?? "");
|
|
11711
|
+
if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
|
|
11712
|
+
return "/";
|
|
11713
|
+
return sanitizeInternalPath(previous.pathname, "/");
|
|
11714
|
+
} catch {
|
|
11715
|
+
return "/";
|
|
11716
|
+
}
|
|
11717
|
+
}
|
|
11718
|
+
function renderKernelErrorChrome(input) {
|
|
11719
|
+
const title = escapeHtml(input.title);
|
|
11720
|
+
const message = escapeHtml(input.message);
|
|
11721
|
+
const errorLines = Object.entries(input.errors ?? {}).flatMap(([field, messages]) => messages.map((item) => `${field}: ${item}`)).map((line) => `<li>${escapeHtml(line)}</li>`).join("");
|
|
11722
|
+
const details = errorLines ? `<ul class="error-list">${errorLines}</ul>` : "";
|
|
11723
|
+
const goBack = input.status === 422 ? `<p><a href="${escapeHtml(errorReturnPath(input.request ?? currentRequestMeta().request))}">Go back</a></p>` : "";
|
|
11724
|
+
return `<!doctype html>
|
|
11725
|
+
<html lang="en">
|
|
11726
|
+
<head>
|
|
11727
|
+
<meta charset="UTF-8" />
|
|
11728
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
11729
|
+
<title>${title}</title>
|
|
11730
|
+
<link rel="stylesheet" href="/assets/app.css" />
|
|
11731
|
+
</head>
|
|
11732
|
+
<body>
|
|
11733
|
+
<header class="site-header">
|
|
11734
|
+
<a class="brand" href="/">Home</a>
|
|
11735
|
+
</header>
|
|
11736
|
+
<main class="site-main">
|
|
11737
|
+
<section class="page-header">
|
|
11738
|
+
<h1>${title}</h1>
|
|
11739
|
+
<p>${message}</p>
|
|
11740
|
+
${details}
|
|
11741
|
+
${goBack}
|
|
11742
|
+
</section>
|
|
11743
|
+
</main>
|
|
11744
|
+
</body>
|
|
11745
|
+
</html>
|
|
11746
|
+
`;
|
|
11747
|
+
}
|
|
11748
|
+
function errorTemplateName(status) {
|
|
11749
|
+
if (status === 404) {
|
|
11750
|
+
return "errors/not-found";
|
|
11751
|
+
}
|
|
11752
|
+
if (status === 403) {
|
|
11753
|
+
return "errors/forbidden";
|
|
11754
|
+
}
|
|
11755
|
+
return "errors/error";
|
|
11756
|
+
}
|
|
11757
|
+
async function renderWebErrorHtml(input) {
|
|
11758
|
+
const render = configuredErrorView.render;
|
|
11759
|
+
if (!render) {
|
|
11760
|
+
return renderKernelErrorChrome(input);
|
|
11761
|
+
}
|
|
11762
|
+
try {
|
|
11763
|
+
return await render({
|
|
11764
|
+
...input,
|
|
11765
|
+
request: input.request ?? currentRequestMeta().request
|
|
11766
|
+
});
|
|
11767
|
+
} catch {
|
|
11768
|
+
return renderKernelErrorChrome(input);
|
|
11769
|
+
}
|
|
11770
|
+
}
|
|
11771
|
+
async function htmlErrorResponse(input) {
|
|
11772
|
+
const response = htmlResponse(await renderWebErrorHtml(input), { status: input.status });
|
|
11773
|
+
response.headers.set("Cache-Control", "no-store");
|
|
11774
|
+
return response;
|
|
11775
|
+
}
|
|
11776
|
+
async function notFoundHtmlResponse(body) {
|
|
11777
|
+
if (body !== undefined) {
|
|
11778
|
+
const response = htmlResponse(body, { status: 404 });
|
|
11779
|
+
response.headers.set("Cache-Control", "no-store");
|
|
11780
|
+
return response;
|
|
11781
|
+
}
|
|
11782
|
+
return htmlErrorResponse({
|
|
11783
|
+
status: 404,
|
|
11784
|
+
title: "Not Found",
|
|
11785
|
+
message: "The page you requested was not found."
|
|
11786
|
+
});
|
|
11787
|
+
}
|
|
11788
|
+
|
|
11789
|
+
// ../../src/core/http/contentNegotiation.ts
|
|
11790
|
+
function requestPrefersJson(request) {
|
|
11791
|
+
if (!request) {
|
|
11733
11792
|
return true;
|
|
11734
11793
|
}
|
|
11735
|
-
|
|
11794
|
+
if (request.headers.get("HX-Request") === "true") {
|
|
11795
|
+
return false;
|
|
11796
|
+
}
|
|
11797
|
+
const pathname = new URL(request.url).pathname;
|
|
11798
|
+
if (pathname.startsWith("/api/")) {
|
|
11799
|
+
return true;
|
|
11800
|
+
}
|
|
11801
|
+
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
11802
|
+
if (accept.includes("text/html")) {
|
|
11803
|
+
return false;
|
|
11804
|
+
}
|
|
11805
|
+
if (accept.includes("application/json")) {
|
|
11806
|
+
return true;
|
|
11807
|
+
}
|
|
11808
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
11809
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
11810
|
+
return false;
|
|
11811
|
+
}
|
|
11812
|
+
return false;
|
|
11736
11813
|
}
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11814
|
+
|
|
11815
|
+
// ../../src/core/http/webErrorResponse.ts
|
|
11816
|
+
function normalizeFieldErrors(details) {
|
|
11817
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
11818
|
+
return {};
|
|
11819
|
+
}
|
|
11820
|
+
const errors = {};
|
|
11821
|
+
for (const [field, messages] of Object.entries(details)) {
|
|
11822
|
+
if (Array.isArray(messages)) {
|
|
11823
|
+
errors[field] = messages.map(String);
|
|
11824
|
+
continue;
|
|
11825
|
+
}
|
|
11826
|
+
if (typeof messages === "string") {
|
|
11827
|
+
errors[field] = [messages];
|
|
11742
11828
|
}
|
|
11743
|
-
return;
|
|
11744
11829
|
}
|
|
11745
|
-
|
|
11746
|
-
|
|
11830
|
+
return errors;
|
|
11831
|
+
}
|
|
11832
|
+
function errorPageTitle(status, message) {
|
|
11833
|
+
if (status === 404) {
|
|
11834
|
+
return "Not Found";
|
|
11835
|
+
}
|
|
11836
|
+
if (status === 403) {
|
|
11837
|
+
return "Forbidden";
|
|
11838
|
+
}
|
|
11839
|
+
if (status >= 500) {
|
|
11840
|
+
return "Server Error";
|
|
11747
11841
|
}
|
|
11842
|
+
return message;
|
|
11748
11843
|
}
|
|
11749
|
-
function
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
next.append("Vary", "X-Tenant-Id");
|
|
11755
|
-
return next;
|
|
11844
|
+
function publicErrorMessage(status, message) {
|
|
11845
|
+
if (status >= 500 && isProductionEnv()) {
|
|
11846
|
+
return "Something went wrong.";
|
|
11847
|
+
}
|
|
11848
|
+
return message;
|
|
11756
11849
|
}
|
|
11757
|
-
function
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11850
|
+
function logServerError(error, mappedError) {
|
|
11851
|
+
if (mappedError.status < 500) {
|
|
11852
|
+
return;
|
|
11853
|
+
}
|
|
11854
|
+
appLogger.error("Unhandled request error", {
|
|
11855
|
+
status: mappedError.status,
|
|
11856
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11857
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
11761
11858
|
});
|
|
11762
11859
|
}
|
|
11763
|
-
function
|
|
11764
|
-
if (!
|
|
11765
|
-
return
|
|
11860
|
+
async function webErrorResponse(error, request) {
|
|
11861
|
+
if (currentRequestMeta().errorFormat === "json" || !request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
11862
|
+
return null;
|
|
11766
11863
|
}
|
|
11767
|
-
|
|
11768
|
-
|
|
11864
|
+
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
11865
|
+
logServerError(error, mappedError);
|
|
11866
|
+
if (mappedError.status === 401) {
|
|
11867
|
+
return Response.redirect(loginRedirectLocation(request), 302);
|
|
11769
11868
|
}
|
|
11770
|
-
const
|
|
11771
|
-
return
|
|
11772
|
-
status:
|
|
11773
|
-
|
|
11774
|
-
|
|
11869
|
+
const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
11870
|
+
return htmlErrorResponse({
|
|
11871
|
+
status: mappedError.status,
|
|
11872
|
+
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
11873
|
+
message: publicErrorMessage(mappedError.status, mappedError.message),
|
|
11874
|
+
errors,
|
|
11875
|
+
request
|
|
11775
11876
|
});
|
|
11776
11877
|
}
|
|
11777
11878
|
|
|
11778
|
-
// ../../src/core/http/
|
|
11879
|
+
// ../../src/core/http/response.ts
|
|
11880
|
+
init_transactionControl();
|
|
11779
11881
|
function jsonResponse(data, init = {}) {
|
|
11780
11882
|
return Response.json(data, {
|
|
11781
11883
|
status: init.status ?? 200,
|
|
11782
11884
|
headers: init.headers
|
|
11783
11885
|
});
|
|
11784
11886
|
}
|
|
11785
|
-
function
|
|
11786
|
-
if (!request || !isEtagEnabled()) {
|
|
11787
|
-
return jsonResponse(data, init);
|
|
11788
|
-
}
|
|
11789
|
-
const etag = computeEtagFromJson(data);
|
|
11790
|
-
if (ifNoneMatchSatisfied(request, etag)) {
|
|
11791
|
-
return notModifiedResponse(etag);
|
|
11792
|
-
}
|
|
11887
|
+
function privateJsonResponse(data, init = {}) {
|
|
11793
11888
|
const response = jsonResponse(data, init);
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
headers.set("Cache-Control", "private, must-revalidate");
|
|
11797
|
-
headers.append("Vary", "Authorization");
|
|
11798
|
-
headers.append("Vary", "X-Tenant-Id");
|
|
11799
|
-
return new Response(response.body, {
|
|
11800
|
-
status: response.status,
|
|
11801
|
-
statusText: response.statusText,
|
|
11802
|
-
headers
|
|
11803
|
-
});
|
|
11889
|
+
response.headers.set("Cache-Control", "private, no-store");
|
|
11890
|
+
return response;
|
|
11804
11891
|
}
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
var FRONTEND_MODE_PATTERN = new RegExp(`^(${FRONTEND_MODES.join("|")})$`);
|
|
11808
|
-
function parseFrontendMode(value) {
|
|
11809
|
-
const mode = (value ?? "api").trim();
|
|
11810
|
-
if (mode === "server-htmx" || mode === "spa-react" || mode === "hybrid") {
|
|
11811
|
-
return mode;
|
|
11812
|
-
}
|
|
11813
|
-
return "api";
|
|
11892
|
+
function createdResponse(data, init = {}) {
|
|
11893
|
+
return jsonResponse(data, { ...init, status: init.status ?? 201 });
|
|
11814
11894
|
}
|
|
11815
|
-
function
|
|
11816
|
-
return
|
|
11895
|
+
function noContentResponse() {
|
|
11896
|
+
return new Response(null, { status: 204 });
|
|
11817
11897
|
}
|
|
11818
|
-
function
|
|
11819
|
-
|
|
11898
|
+
function errorResponse(error) {
|
|
11899
|
+
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
11900
|
+
logServerError(error, mappedError);
|
|
11901
|
+
return Response.json({
|
|
11902
|
+
error: mappedError.message,
|
|
11903
|
+
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
11904
|
+
}, { status: mappedError.status, headers: { "Cache-Control": "no-store" } });
|
|
11820
11905
|
}
|
|
11821
|
-
function
|
|
11822
|
-
return
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11906
|
+
function withErrorHandling(handler) {
|
|
11907
|
+
return async (...args) => {
|
|
11908
|
+
try {
|
|
11909
|
+
return await handler(...args);
|
|
11910
|
+
} catch (error) {
|
|
11911
|
+
requestTransactionRollback();
|
|
11912
|
+
const request = args.find((arg) => arg instanceof Request);
|
|
11913
|
+
const webResponse = await webErrorResponse(error, request);
|
|
11914
|
+
if (webResponse) {
|
|
11915
|
+
return webResponse;
|
|
11916
|
+
}
|
|
11917
|
+
return errorResponse(error);
|
|
11918
|
+
}
|
|
11919
|
+
};
|
|
11829
11920
|
}
|
|
11830
11921
|
|
|
11831
|
-
// ../../src/core/http/
|
|
11832
|
-
var
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
};
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
"form-action": ["'self'"],
|
|
11852
|
-
"frame-ancestors": ["'none'"],
|
|
11853
|
-
"base-uri": ["'self'"]
|
|
11854
|
-
};
|
|
11855
|
-
var SPA_HTML_DIRECTIVES = {
|
|
11856
|
-
"default-src": ["'self'"],
|
|
11857
|
-
"script-src": ["'self'"],
|
|
11858
|
-
"style-src": ["'self'"],
|
|
11859
|
-
"connect-src": ["'self'"],
|
|
11860
|
-
"img-src": ["'self'", "data:", "https:"],
|
|
11861
|
-
"font-src": ["'self'"],
|
|
11862
|
-
"frame-ancestors": ["'none'"],
|
|
11863
|
-
"base-uri": ["'self'"]
|
|
11864
|
-
};
|
|
11865
|
-
var configuredHtmlOptions = {};
|
|
11866
|
-
function generateCspNonce() {
|
|
11867
|
-
const bytes = new Uint8Array(16);
|
|
11868
|
-
crypto.getRandomValues(bytes);
|
|
11869
|
-
return Buffer.from(bytes).toString("base64");
|
|
11870
|
-
}
|
|
11871
|
-
function configureContentSecurityPolicy(options) {
|
|
11872
|
-
configuredHtmlOptions = { ...options };
|
|
11873
|
-
}
|
|
11874
|
-
function resetContentSecurityPolicyForTests() {
|
|
11875
|
-
configuredHtmlOptions = {};
|
|
11876
|
-
}
|
|
11877
|
-
function cloneDirectives(source) {
|
|
11878
|
-
const copy = {};
|
|
11879
|
-
for (const [directive, values] of Object.entries(source)) {
|
|
11880
|
-
copy[directive] = [...values];
|
|
11881
|
-
}
|
|
11882
|
-
return copy;
|
|
11883
|
-
}
|
|
11884
|
-
function normalizeSources(value) {
|
|
11885
|
-
if (Array.isArray(value)) {
|
|
11886
|
-
return value.flatMap((item) => item.split(/\s+/).filter(Boolean));
|
|
11887
|
-
}
|
|
11888
|
-
return value.split(/\s+/).filter(Boolean);
|
|
11889
|
-
}
|
|
11890
|
-
function mergeDirectives(base, extra) {
|
|
11891
|
-
const merged = cloneDirectives(base);
|
|
11892
|
-
for (const [directive, value] of Object.entries(extra ?? {})) {
|
|
11893
|
-
if (value === false) {
|
|
11894
|
-
delete merged[directive];
|
|
11895
|
-
continue;
|
|
11922
|
+
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
11923
|
+
var guards = new WeakMap;
|
|
11924
|
+
function guardBody(request, maxBytes) {
|
|
11925
|
+
const existing = guards.get(request);
|
|
11926
|
+
if (existing && existing.maxBytes <= maxBytes)
|
|
11927
|
+
return existing;
|
|
11928
|
+
const state = { maxBytes, exceeded: false };
|
|
11929
|
+
const body = request.body;
|
|
11930
|
+
if (!body)
|
|
11931
|
+
return state;
|
|
11932
|
+
let consumed = 0;
|
|
11933
|
+
const stream = body.pipeThrough(new TransformStream({
|
|
11934
|
+
transform(chunk, controller) {
|
|
11935
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
11936
|
+
consumed += bytes.byteLength;
|
|
11937
|
+
if (consumed > maxBytes) {
|
|
11938
|
+
state.exceeded = true;
|
|
11939
|
+
throw new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
11940
|
+
}
|
|
11941
|
+
controller.enqueue(bytes);
|
|
11896
11942
|
}
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
}
|
|
11903
|
-
function serializeDirectives(directives) {
|
|
11904
|
-
return Object.entries(directives).map(([directive, values]) => values.length > 0 ? `${directive} ${values.join(" ")}` : directive).join("; ");
|
|
11905
|
-
}
|
|
11906
|
-
function applyNonce(directives, nonce) {
|
|
11907
|
-
if (!nonce) {
|
|
11908
|
-
return directives;
|
|
11909
|
-
}
|
|
11910
|
-
const token = `'nonce-${nonce}'`;
|
|
11911
|
-
const next = cloneDirectives(directives);
|
|
11912
|
-
for (const directive of ["script-src", "style-src"]) {
|
|
11913
|
-
const values = next[directive] ?? ["'self'"];
|
|
11914
|
-
if (!values.includes(token)) {
|
|
11915
|
-
next[directive] = [...values, token];
|
|
11943
|
+
}));
|
|
11944
|
+
const bounded = new Request(request, { body: stream });
|
|
11945
|
+
const decorateClone = (clone) => {
|
|
11946
|
+
for (const field of ["params", "cookies"]) {
|
|
11947
|
+
if (field in request)
|
|
11948
|
+
Object.defineProperty(clone, field, { value: Reflect.get(request, field) });
|
|
11916
11949
|
}
|
|
11950
|
+
const nativeClone = () => Reflect.apply(Request.prototype.clone, clone, []);
|
|
11951
|
+
Object.defineProperty(clone, "clone", {
|
|
11952
|
+
value: () => decorateClone(nativeClone())
|
|
11953
|
+
});
|
|
11954
|
+
guards.set(clone, state);
|
|
11955
|
+
return clone;
|
|
11956
|
+
};
|
|
11957
|
+
const descriptors = {
|
|
11958
|
+
body: { configurable: true, get: () => bounded.body },
|
|
11959
|
+
bodyUsed: { configurable: true, get: () => bounded.bodyUsed },
|
|
11960
|
+
clone: {
|
|
11961
|
+
configurable: true,
|
|
11962
|
+
value: () => decorateClone(Reflect.apply(Request.prototype.clone, bounded, []))
|
|
11963
|
+
}
|
|
11964
|
+
};
|
|
11965
|
+
for (const method of [
|
|
11966
|
+
"text",
|
|
11967
|
+
"json",
|
|
11968
|
+
"formData",
|
|
11969
|
+
"arrayBuffer",
|
|
11970
|
+
"blob",
|
|
11971
|
+
"bytes",
|
|
11972
|
+
"textStream"
|
|
11973
|
+
]) {
|
|
11974
|
+
descriptors[method] = { configurable: true, value: bounded[method].bind(bounded) };
|
|
11917
11975
|
}
|
|
11918
|
-
|
|
11976
|
+
Object.defineProperties(request, descriptors);
|
|
11977
|
+
guards.set(request, state);
|
|
11978
|
+
return state;
|
|
11919
11979
|
}
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11980
|
+
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
11981
|
+
function resolveMaxBodyBytes() {
|
|
11982
|
+
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
11983
|
+
if (!raw) {
|
|
11984
|
+
return DEFAULT_MAX_BODY_BYTES;
|
|
11923
11985
|
}
|
|
11924
|
-
|
|
11925
|
-
|
|
11986
|
+
const parsed = Number(raw);
|
|
11987
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
11988
|
+
return DEFAULT_MAX_BODY_BYTES;
|
|
11926
11989
|
}
|
|
11927
|
-
return
|
|
11990
|
+
return parsed;
|
|
11928
11991
|
}
|
|
11929
|
-
function
|
|
11930
|
-
|
|
11992
|
+
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
11993
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
11994
|
+
throw new RangeError("Body limit must be a positive safe integer.");
|
|
11995
|
+
return async (request, next) => {
|
|
11996
|
+
const tooLarge = () => {
|
|
11997
|
+
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
11998
|
+
return errorResponse(error);
|
|
11999
|
+
};
|
|
12000
|
+
const length = Number(request.headers.get("content-length"));
|
|
12001
|
+
if (Number.isFinite(length) && length > maxBytes)
|
|
12002
|
+
return tooLarge();
|
|
12003
|
+
const state = guardBody(request, maxBytes);
|
|
12004
|
+
try {
|
|
12005
|
+
const response = await next();
|
|
12006
|
+
return state.exceeded ? tooLarge() : response;
|
|
12007
|
+
} catch (error) {
|
|
12008
|
+
if (state.exceeded || error instanceof PayloadTooLargeError)
|
|
12009
|
+
return tooLarge();
|
|
12010
|
+
throw error;
|
|
12011
|
+
}
|
|
12012
|
+
};
|
|
11931
12013
|
}
|
|
11932
|
-
|
|
11933
|
-
|
|
12014
|
+
// ../../src/core/http/clientIp.ts
|
|
12015
|
+
function trustForwardedFor(env = process.env) {
|
|
12016
|
+
return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
|
|
11934
12017
|
}
|
|
11935
|
-
function
|
|
11936
|
-
|
|
12018
|
+
function isPrivateAddress(address) {
|
|
12019
|
+
const ip = address.replace(/^::ffff:/i, "");
|
|
12020
|
+
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);
|
|
11937
12021
|
}
|
|
11938
|
-
function
|
|
11939
|
-
const
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
...options.directives
|
|
12022
|
+
function forwardedClientIp(request) {
|
|
12023
|
+
const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
|
|
12024
|
+
for (let index = hops.length - 1;index >= 0; index -= 1) {
|
|
12025
|
+
const hop = hops[index];
|
|
12026
|
+
if (hop && !isPrivateAddress(hop)) {
|
|
12027
|
+
return hop;
|
|
11945
12028
|
}
|
|
11946
|
-
};
|
|
11947
|
-
const override = resolved.htmlCsp ?? resolved.csp;
|
|
11948
|
-
if (override) {
|
|
11949
|
-
return override;
|
|
11950
12029
|
}
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
function resolveContentSecurityPolicy(response, options = {}) {
|
|
11954
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
11955
|
-
if (!contentType.includes("text/html")) {
|
|
11956
|
-
return strictApiContentSecurityPolicy();
|
|
12030
|
+
if (hops.length > 0) {
|
|
12031
|
+
return hops[hops.length - 1];
|
|
11957
12032
|
}
|
|
11958
|
-
return
|
|
12033
|
+
return request.headers.get("x-real-ip")?.trim() || undefined;
|
|
11959
12034
|
}
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
throw new TypeError("CORS additionalAllowedHeaders must be an array of header names.");
|
|
11966
|
-
}
|
|
11967
|
-
const names = [
|
|
11968
|
-
...configured ? configured.split(",").map((name) => name.trim()) : [],
|
|
11969
|
-
...supplied
|
|
11970
|
-
];
|
|
11971
|
-
return names.map((name) => {
|
|
11972
|
-
if (typeof name !== "string" || name === "*" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
11973
|
-
throw new TypeError("CORS additional headers must be explicit valid HTTP header names; wildcards are not allowed.");
|
|
12035
|
+
function readClientIp(request, env = process.env) {
|
|
12036
|
+
if (trustForwardedFor(env)) {
|
|
12037
|
+
const forwarded = forwardedClientIp(request);
|
|
12038
|
+
if (forwarded) {
|
|
12039
|
+
return forwarded;
|
|
11974
12040
|
}
|
|
11975
|
-
|
|
11976
|
-
|
|
12041
|
+
}
|
|
12042
|
+
return currentRequestMeta().ipAddress ?? undefined;
|
|
11977
12043
|
}
|
|
11978
|
-
|
|
11979
|
-
|
|
12044
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
12045
|
+
function nonCryptographicDigest(input) {
|
|
12046
|
+
return Bun.hash(input).toString(16);
|
|
11980
12047
|
}
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
11986
|
-
allowedHeaders: [
|
|
11987
|
-
"Authorization",
|
|
11988
|
-
"Content-Type",
|
|
11989
|
-
"X-Request-Id",
|
|
11990
|
-
"X-Tenant-Id",
|
|
11991
|
-
"X-Authenticated-User-Id",
|
|
11992
|
-
"X-Authenticated-User-Role",
|
|
11993
|
-
"If-Match",
|
|
11994
|
-
"If-None-Match",
|
|
11995
|
-
"X-CSRF-Token",
|
|
11996
|
-
...extraHeaders
|
|
11997
|
-
].filter((name, index, names) => names.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index),
|
|
11998
|
-
maxAgeSeconds: 86400
|
|
11999
|
-
};
|
|
12048
|
+
|
|
12049
|
+
// ../../src/core/http/etag.ts
|
|
12050
|
+
function isEtagEnabled() {
|
|
12051
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
12000
12052
|
}
|
|
12001
|
-
function
|
|
12002
|
-
|
|
12003
|
-
const captured = {
|
|
12004
|
-
additionalAllowedHeaders: [...options.additionalAllowedHeaders ?? []]
|
|
12005
|
-
};
|
|
12006
|
-
return async (request, next) => {
|
|
12007
|
-
if (request.method === "OPTIONS") {
|
|
12008
|
-
return new Response(null, {
|
|
12009
|
-
status: 204,
|
|
12010
|
-
headers: buildCorsHeaders(request, captured)
|
|
12011
|
-
});
|
|
12012
|
-
}
|
|
12013
|
-
const response = await next();
|
|
12014
|
-
const headers = new Headers(response.headers);
|
|
12015
|
-
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
12016
|
-
if (key === "vary") {
|
|
12017
|
-
const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
12018
|
-
if (fields.includes("*"))
|
|
12019
|
-
continue;
|
|
12020
|
-
if (!fields.some((field) => field.toLowerCase() === "origin"))
|
|
12021
|
-
fields.push(value);
|
|
12022
|
-
headers.set("Vary", fields.join(", "));
|
|
12023
|
-
} else {
|
|
12024
|
-
headers.set(key, value);
|
|
12025
|
-
}
|
|
12026
|
-
}
|
|
12027
|
-
return new Response(response.body, {
|
|
12028
|
-
status: response.status,
|
|
12029
|
-
statusText: response.statusText,
|
|
12030
|
-
headers
|
|
12031
|
-
});
|
|
12032
|
-
};
|
|
12053
|
+
function formatWeakEtag(digest) {
|
|
12054
|
+
return `W/"${digest}"`;
|
|
12033
12055
|
}
|
|
12034
|
-
function
|
|
12035
|
-
const
|
|
12036
|
-
|
|
12037
|
-
const corsConfig = resolveCorsConfig(options);
|
|
12038
|
-
const allowedOrigins = corsConfig.allowedOrigins;
|
|
12039
|
-
headers.set("Vary", "Origin");
|
|
12040
|
-
const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
|
|
12041
|
-
if (!allowOrigin) {
|
|
12042
|
-
return headers;
|
|
12043
|
-
}
|
|
12044
|
-
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
12045
|
-
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
12046
|
-
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
12047
|
-
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
12048
|
-
if (allowOrigin !== "*") {
|
|
12049
|
-
headers.set("Access-Control-Allow-Credentials", "true");
|
|
12050
|
-
}
|
|
12051
|
-
return headers;
|
|
12056
|
+
function computeEtagFromJson(data) {
|
|
12057
|
+
const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
|
|
12058
|
+
return formatWeakEtag(digest);
|
|
12052
12059
|
}
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
return process.env.CSRF_COOKIE_NAME?.trim() || appCookieName("csrf");
|
|
12060
|
+
function etagFromResource(resource) {
|
|
12061
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
12062
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
12063
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
12064
|
+
return formatWeakEtag(digest);
|
|
12059
12065
|
}
|
|
12060
|
-
function
|
|
12061
|
-
return
|
|
12066
|
+
function normalizeEtag(value) {
|
|
12067
|
+
return value.trim();
|
|
12062
12068
|
}
|
|
12063
|
-
function
|
|
12064
|
-
return
|
|
12069
|
+
function etagValuesMatch(left, right) {
|
|
12070
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
12065
12071
|
}
|
|
12066
|
-
function
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
if (leftBuffer.length !== rightBuffer.length) {
|
|
12070
|
-
return false;
|
|
12072
|
+
function parseEtagList(header) {
|
|
12073
|
+
if (!header) {
|
|
12074
|
+
return [];
|
|
12071
12075
|
}
|
|
12072
|
-
return
|
|
12073
|
-
}
|
|
12074
|
-
function createCsrfTokenCookie() {
|
|
12075
|
-
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
12076
|
-
const secure = isProductionEnv() ? "; Secure" : "";
|
|
12077
|
-
return {
|
|
12078
|
-
token,
|
|
12079
|
-
cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
|
|
12080
|
-
};
|
|
12076
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
12081
12077
|
}
|
|
12082
|
-
function
|
|
12083
|
-
const
|
|
12084
|
-
if (
|
|
12085
|
-
return
|
|
12078
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
12079
|
+
const header = request.headers.get("if-none-match");
|
|
12080
|
+
if (!header) {
|
|
12081
|
+
return false;
|
|
12086
12082
|
}
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
function readSubmittedCsrfToken(request) {
|
|
12090
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
12091
|
-
if (headerToken) {
|
|
12092
|
-
return headerToken;
|
|
12083
|
+
if (header.trim() === "*") {
|
|
12084
|
+
return true;
|
|
12093
12085
|
}
|
|
12094
|
-
return
|
|
12086
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
12095
12087
|
}
|
|
12096
|
-
|
|
12097
|
-
const
|
|
12098
|
-
if (
|
|
12099
|
-
return
|
|
12088
|
+
function ifMatchSatisfied(request, etag) {
|
|
12089
|
+
const header = request.headers.get("if-match");
|
|
12090
|
+
if (!header) {
|
|
12091
|
+
return false;
|
|
12100
12092
|
}
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
const formData = await request.clone().formData();
|
|
12104
|
-
const field = formData.get("_token");
|
|
12105
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
12106
|
-
return field.trim();
|
|
12107
|
-
}
|
|
12108
|
-
const legacyField = formData.get("_csrf");
|
|
12109
|
-
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
12110
|
-
return legacyField.trim();
|
|
12111
|
-
}
|
|
12093
|
+
if (header.trim() === "*") {
|
|
12094
|
+
return true;
|
|
12112
12095
|
}
|
|
12113
|
-
return
|
|
12096
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
12114
12097
|
}
|
|
12115
|
-
function
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
return
|
|
12098
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
12099
|
+
const header = request.headers.get("if-match");
|
|
12100
|
+
if (!header) {
|
|
12101
|
+
if (options.required) {
|
|
12102
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
12103
|
+
}
|
|
12104
|
+
return;
|
|
12122
12105
|
}
|
|
12123
|
-
if (!
|
|
12124
|
-
|
|
12106
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
12107
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
12125
12108
|
}
|
|
12126
|
-
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
12127
12109
|
}
|
|
12128
|
-
function
|
|
12129
|
-
const
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12110
|
+
function applyEtagHeaders(headers, etag) {
|
|
12111
|
+
const next = new Headers(headers);
|
|
12112
|
+
next.set("ETag", etag);
|
|
12113
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
12114
|
+
next.append("Vary", "Authorization");
|
|
12115
|
+
next.append("Vary", "X-Tenant-Id");
|
|
12116
|
+
return next;
|
|
12134
12117
|
}
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12118
|
+
function notModifiedResponse(etag) {
|
|
12119
|
+
return new Response(null, {
|
|
12120
|
+
status: 304,
|
|
12121
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
12122
|
+
});
|
|
12123
|
+
}
|
|
12124
|
+
function applyConditionalGet(request, response, etag) {
|
|
12125
|
+
if (!isEtagEnabled()) {
|
|
12126
|
+
return response;
|
|
12142
12127
|
}
|
|
12143
|
-
|
|
12144
|
-
return
|
|
12145
|
-
} catch {
|
|
12146
|
-
return "/auth/saml/acs";
|
|
12128
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
12129
|
+
return notModifiedResponse(etag);
|
|
12147
12130
|
}
|
|
12131
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
12132
|
+
return new Response(response.body, {
|
|
12133
|
+
status: response.status,
|
|
12134
|
+
statusText: response.statusText,
|
|
12135
|
+
headers
|
|
12136
|
+
});
|
|
12148
12137
|
}
|
|
12149
|
-
|
|
12150
|
-
|
|
12138
|
+
|
|
12139
|
+
// ../../src/core/http/conditionalResponse.ts
|
|
12140
|
+
function jsonResponse2(data, init = {}) {
|
|
12141
|
+
return Response.json(data, {
|
|
12142
|
+
status: init.status ?? 200,
|
|
12143
|
+
headers: init.headers
|
|
12144
|
+
});
|
|
12151
12145
|
}
|
|
12152
|
-
function
|
|
12146
|
+
function conditionalJsonResponse(request, data, init = {}) {
|
|
12147
|
+
if (!request || !isEtagEnabled()) {
|
|
12148
|
+
return jsonResponse2(data, init);
|
|
12149
|
+
}
|
|
12150
|
+
const etag = computeEtagFromJson(data);
|
|
12151
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
12152
|
+
return notModifiedResponse(etag);
|
|
12153
|
+
}
|
|
12154
|
+
const response = jsonResponse2(data, init);
|
|
12153
12155
|
const headers = new Headers(response.headers);
|
|
12154
|
-
headers.
|
|
12156
|
+
headers.set("ETag", etag);
|
|
12157
|
+
headers.set("Cache-Control", "private, must-revalidate");
|
|
12158
|
+
headers.append("Vary", "Authorization");
|
|
12159
|
+
headers.append("Vary", "X-Tenant-Id");
|
|
12155
12160
|
return new Response(response.body, {
|
|
12156
12161
|
status: response.status,
|
|
12157
12162
|
statusText: response.statusText,
|
|
12158
12163
|
headers
|
|
12159
12164
|
});
|
|
12160
12165
|
}
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
}
|
|
12201
|
-
// ../../src/core/http/csrfProtection.ts
|
|
12202
|
-
var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
|
|
12203
|
-
function createCsrfProtection(secret, options = {}) {
|
|
12204
|
-
const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
|
|
12205
|
-
const maxAge = options.maxAge ?? expiresIn;
|
|
12206
|
-
return {
|
|
12207
|
-
generate(_sessionKey) {
|
|
12208
|
-
return Bun.CSRF.generate(secret, { expiresIn });
|
|
12209
|
-
},
|
|
12210
|
-
verify(token, _sessionKey) {
|
|
12211
|
-
if (!token) {
|
|
12212
|
-
return false;
|
|
12213
|
-
}
|
|
12214
|
-
return Bun.CSRF.verify(token, { secret, maxAge });
|
|
12215
|
-
},
|
|
12216
|
-
secret
|
|
12217
|
-
};
|
|
12166
|
+
// ../../src/core/http/contentSecurityPolicy.ts
|
|
12167
|
+
var HTMX_2_0_4_INDICATOR_STYLE_HASH = "'sha256-bsV5JivYxvGywDAZ22EZJKBFip65Ng9xoJVLbBg7bdo='";
|
|
12168
|
+
var API_DIRECTIVES = {
|
|
12169
|
+
"default-src": ["'none'"],
|
|
12170
|
+
"frame-ancestors": ["'none'"],
|
|
12171
|
+
"base-uri": ["'none'"]
|
|
12172
|
+
};
|
|
12173
|
+
var HTMX_HTML_DIRECTIVES = {
|
|
12174
|
+
"default-src": ["'self'"],
|
|
12175
|
+
"script-src": ["'self'"],
|
|
12176
|
+
"style-src": ["'self'", HTMX_2_0_4_INDICATOR_STYLE_HASH],
|
|
12177
|
+
"connect-src": ["'self'"],
|
|
12178
|
+
"img-src": ["'self'", "data:", "https:"],
|
|
12179
|
+
"font-src": ["'self'"],
|
|
12180
|
+
"media-src": ["'self'", "https:"],
|
|
12181
|
+
"frame-src": [
|
|
12182
|
+
"https://www.youtube.com",
|
|
12183
|
+
"https://www.youtube-nocookie.com",
|
|
12184
|
+
"https://player.vimeo.com"
|
|
12185
|
+
],
|
|
12186
|
+
"form-action": ["'self'"],
|
|
12187
|
+
"frame-ancestors": ["'none'"],
|
|
12188
|
+
"base-uri": ["'self'"]
|
|
12189
|
+
};
|
|
12190
|
+
var SPA_HTML_DIRECTIVES = {
|
|
12191
|
+
"default-src": ["'self'"],
|
|
12192
|
+
"script-src": ["'self'"],
|
|
12193
|
+
"style-src": ["'self'"],
|
|
12194
|
+
"connect-src": ["'self'"],
|
|
12195
|
+
"img-src": ["'self'", "data:", "https:"],
|
|
12196
|
+
"font-src": ["'self'"],
|
|
12197
|
+
"frame-ancestors": ["'none'"],
|
|
12198
|
+
"base-uri": ["'self'"]
|
|
12199
|
+
};
|
|
12200
|
+
var configuredHtmlOptions = {};
|
|
12201
|
+
function generateCspNonce() {
|
|
12202
|
+
const bytes = new Uint8Array(16);
|
|
12203
|
+
crypto.getRandomValues(bytes);
|
|
12204
|
+
return Buffer.from(bytes).toString("base64");
|
|
12218
12205
|
}
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
var FLASH_COOKIE = appCookieName("flash");
|
|
12222
|
-
var FLASH_TTL_MS = 60 * 1000;
|
|
12223
|
-
function flashCookieName() {
|
|
12224
|
-
return process.env.FLASH_COOKIE_NAME?.trim() || appCookieName("flash");
|
|
12206
|
+
function configureContentSecurityPolicy(options) {
|
|
12207
|
+
configuredHtmlOptions = { ...options };
|
|
12225
12208
|
}
|
|
12226
|
-
function
|
|
12227
|
-
|
|
12209
|
+
function resetContentSecurityPolicyForTests() {
|
|
12210
|
+
configuredHtmlOptions = {};
|
|
12228
12211
|
}
|
|
12229
|
-
function
|
|
12230
|
-
const
|
|
12231
|
-
|
|
12212
|
+
function cloneDirectives(source) {
|
|
12213
|
+
const copy = {};
|
|
12214
|
+
for (const [directive, values] of Object.entries(source)) {
|
|
12215
|
+
copy[directive] = [...values];
|
|
12216
|
+
}
|
|
12217
|
+
return copy;
|
|
12232
12218
|
}
|
|
12233
|
-
function
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
return null;
|
|
12219
|
+
function normalizeSources(value) {
|
|
12220
|
+
if (Array.isArray(value)) {
|
|
12221
|
+
return value.flatMap((item) => item.split(/\s+/).filter(Boolean));
|
|
12237
12222
|
}
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12223
|
+
return value.split(/\s+/).filter(Boolean);
|
|
12224
|
+
}
|
|
12225
|
+
function mergeDirectives(base, extra) {
|
|
12226
|
+
const merged = cloneDirectives(base);
|
|
12227
|
+
for (const [directive, value] of Object.entries(extra ?? {})) {
|
|
12228
|
+
if (value === false) {
|
|
12229
|
+
delete merged[directive];
|
|
12230
|
+
continue;
|
|
12242
12231
|
}
|
|
12232
|
+
const sources = normalizeSources(value);
|
|
12233
|
+
const existing = merged[directive] ?? [];
|
|
12234
|
+
merged[directive] = [...existing, ...sources.filter((source) => !existing.includes(source))];
|
|
12243
12235
|
}
|
|
12244
|
-
return
|
|
12236
|
+
return merged;
|
|
12245
12237
|
}
|
|
12246
|
-
function
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
const issuedAtRaw = parts.pop();
|
|
12253
|
-
const payload = parts.join(".");
|
|
12254
|
-
if (!signature || !issuedAtRaw || !payload) {
|
|
12255
|
-
return null;
|
|
12256
|
-
}
|
|
12257
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
12258
|
-
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
12259
|
-
return null;
|
|
12238
|
+
function serializeDirectives(directives) {
|
|
12239
|
+
return Object.entries(directives).map(([directive, values]) => values.length > 0 ? `${directive} ${values.join(" ")}` : directive).join("; ");
|
|
12240
|
+
}
|
|
12241
|
+
function applyNonce(directives, nonce) {
|
|
12242
|
+
if (!nonce) {
|
|
12243
|
+
return directives;
|
|
12260
12244
|
}
|
|
12261
|
-
const
|
|
12262
|
-
|
|
12263
|
-
|
|
12245
|
+
const token = `'nonce-${nonce}'`;
|
|
12246
|
+
const next = cloneDirectives(directives);
|
|
12247
|
+
for (const directive of ["script-src", "style-src"]) {
|
|
12248
|
+
const values = next[directive] ?? ["'self'"];
|
|
12249
|
+
if (!values.includes(token)) {
|
|
12250
|
+
next[directive] = [...values, token];
|
|
12251
|
+
}
|
|
12264
12252
|
}
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12253
|
+
return next;
|
|
12254
|
+
}
|
|
12255
|
+
function htmlBaselineDirectives() {
|
|
12256
|
+
if (isViewsEnabled()) {
|
|
12257
|
+
return cloneDirectives(HTMX_HTML_DIRECTIVES);
|
|
12269
12258
|
}
|
|
12270
|
-
if (
|
|
12271
|
-
return
|
|
12259
|
+
if (isSpaEnabled()) {
|
|
12260
|
+
return cloneDirectives(SPA_HTML_DIRECTIVES);
|
|
12272
12261
|
}
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12262
|
+
return cloneDirectives(API_DIRECTIVES);
|
|
12263
|
+
}
|
|
12264
|
+
function strictApiContentSecurityPolicy() {
|
|
12265
|
+
return serializeDirectives(API_DIRECTIVES);
|
|
12266
|
+
}
|
|
12267
|
+
function serverHtmxContentSecurityPolicy(nonce) {
|
|
12268
|
+
return serializeDirectives(applyNonce(cloneDirectives(HTMX_HTML_DIRECTIVES), nonce));
|
|
12269
|
+
}
|
|
12270
|
+
function spaContentSecurityPolicy(nonce) {
|
|
12271
|
+
return serializeDirectives(applyNonce(cloneDirectives(SPA_HTML_DIRECTIVES), nonce));
|
|
12272
|
+
}
|
|
12273
|
+
function resolveHtmlContentSecurityPolicy(options = {}) {
|
|
12274
|
+
const resolved = {
|
|
12275
|
+
...configuredHtmlOptions,
|
|
12276
|
+
...options,
|
|
12277
|
+
directives: {
|
|
12278
|
+
...configuredHtmlOptions.directives,
|
|
12279
|
+
...options.directives
|
|
12280
12280
|
}
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12281
|
+
};
|
|
12282
|
+
const override = resolved.htmlCsp ?? resolved.csp;
|
|
12283
|
+
if (override) {
|
|
12284
|
+
return override;
|
|
12284
12285
|
}
|
|
12286
|
+
return serializeDirectives(applyNonce(mergeDirectives(htmlBaselineDirectives(), resolved.directives), options.nonce));
|
|
12285
12287
|
}
|
|
12286
|
-
function
|
|
12287
|
-
const
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
function pullFlash(request) {
|
|
12291
|
-
const cookieValue = readFlashCookie(request);
|
|
12292
|
-
if (!cookieValue) {
|
|
12293
|
-
return null;
|
|
12288
|
+
function resolveContentSecurityPolicy(response, options = {}) {
|
|
12289
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
12290
|
+
if (!contentType.includes("text/html")) {
|
|
12291
|
+
return strictApiContentSecurityPolicy();
|
|
12294
12292
|
}
|
|
12295
|
-
return
|
|
12293
|
+
return resolveHtmlContentSecurityPolicy(options);
|
|
12296
12294
|
}
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12295
|
+
// ../../src/core/http/corsMiddleware.ts
|
|
12296
|
+
function additionalHeaders(options) {
|
|
12297
|
+
const configured = process.env.CORS_ADDITIONAL_ALLOWED_HEADERS?.trim();
|
|
12298
|
+
const supplied = options.additionalAllowedHeaders === undefined ? [] : options.additionalAllowedHeaders;
|
|
12299
|
+
if (!Array.isArray(supplied)) {
|
|
12300
|
+
throw new TypeError("CORS additionalAllowedHeaders must be an array of header names.");
|
|
12301
|
+
}
|
|
12302
|
+
const names = [
|
|
12303
|
+
...configured ? configured.split(",").map((name) => name.trim()) : [],
|
|
12304
|
+
...supplied
|
|
12305
|
+
];
|
|
12306
|
+
return names.map((name) => {
|
|
12307
|
+
if (typeof name !== "string" || name === "*" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
12308
|
+
throw new TypeError("CORS additional headers must be explicit valid HTTP header names; wildcards are not allowed.");
|
|
12309
|
+
}
|
|
12310
|
+
return name;
|
|
12304
12311
|
});
|
|
12305
12312
|
}
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12313
|
+
function defaultAllowedOrigins() {
|
|
12314
|
+
return appUrl();
|
|
12315
|
+
}
|
|
12316
|
+
function resolveCorsConfig(options = {}) {
|
|
12317
|
+
const extraHeaders = additionalHeaders(options);
|
|
12318
|
+
return {
|
|
12319
|
+
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
12320
|
+
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
12321
|
+
allowedHeaders: [
|
|
12322
|
+
"Authorization",
|
|
12323
|
+
"Content-Type",
|
|
12324
|
+
"X-Request-Id",
|
|
12325
|
+
"X-Tenant-Id",
|
|
12326
|
+
"X-Authenticated-User-Id",
|
|
12327
|
+
"X-Authenticated-User-Role",
|
|
12328
|
+
"If-Match",
|
|
12329
|
+
"If-None-Match",
|
|
12330
|
+
"X-CSRF-Token",
|
|
12331
|
+
...extraHeaders
|
|
12332
|
+
].filter((name, index, names) => names.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index),
|
|
12333
|
+
maxAgeSeconds: 86400
|
|
12334
|
+
};
|
|
12335
|
+
}
|
|
12336
|
+
function createCorsMiddleware(options = {}) {
|
|
12337
|
+
resolveCorsConfig(options);
|
|
12338
|
+
const captured = {
|
|
12339
|
+
additionalAllowedHeaders: [...options.additionalAllowedHeaders ?? []]
|
|
12340
|
+
};
|
|
12309
12341
|
return async (request, next) => {
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12342
|
+
if (request.method === "OPTIONS") {
|
|
12343
|
+
return new Response(null, {
|
|
12344
|
+
status: 204,
|
|
12345
|
+
headers: buildCorsHeaders(request, captured)
|
|
12346
|
+
});
|
|
12347
|
+
}
|
|
12348
|
+
const response = await next();
|
|
12349
|
+
const headers = new Headers(response.headers);
|
|
12350
|
+
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
12351
|
+
if (key === "vary") {
|
|
12352
|
+
const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
12353
|
+
if (fields.includes("*"))
|
|
12354
|
+
continue;
|
|
12355
|
+
if (!fields.some((field) => field.toLowerCase() === "origin"))
|
|
12356
|
+
fields.push(value);
|
|
12357
|
+
headers.set("Vary", fields.join(", "));
|
|
12358
|
+
} else {
|
|
12359
|
+
headers.set(key, value);
|
|
12316
12360
|
}
|
|
12317
|
-
|
|
12361
|
+
}
|
|
12362
|
+
return new Response(response.body, {
|
|
12363
|
+
status: response.status,
|
|
12364
|
+
statusText: response.statusText,
|
|
12365
|
+
headers
|
|
12318
12366
|
});
|
|
12319
12367
|
};
|
|
12320
12368
|
}
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12369
|
+
function buildCorsHeaders(request, options) {
|
|
12370
|
+
const headers = new Headers;
|
|
12371
|
+
const origin = request.headers.get("origin");
|
|
12372
|
+
const corsConfig = resolveCorsConfig(options);
|
|
12373
|
+
const allowedOrigins = corsConfig.allowedOrigins;
|
|
12374
|
+
headers.set("Vary", "Origin");
|
|
12375
|
+
const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
|
|
12376
|
+
if (!allowOrigin) {
|
|
12377
|
+
return headers;
|
|
12326
12378
|
}
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
function getQueryParams(request) {
|
|
12334
|
-
if (!request) {
|
|
12335
|
-
return new URLSearchParams;
|
|
12379
|
+
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
12380
|
+
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
12381
|
+
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
12382
|
+
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
12383
|
+
if (allowOrigin !== "*") {
|
|
12384
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
12336
12385
|
}
|
|
12337
|
-
return
|
|
12386
|
+
return headers;
|
|
12338
12387
|
}
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12346
|
-
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
12347
|
-
}
|
|
12348
|
-
return parsed;
|
|
12388
|
+
// ../../src/core/http/csrfToken.ts
|
|
12389
|
+
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
12390
|
+
var CSRF_COOKIE = appCookieName("csrf");
|
|
12391
|
+
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
12392
|
+
function csrfCookieName() {
|
|
12393
|
+
return process.env.CSRF_COOKIE_NAME?.trim() || appCookieName("csrf");
|
|
12349
12394
|
}
|
|
12350
|
-
function
|
|
12351
|
-
|
|
12352
|
-
if (value === null || value.trim() === "") {
|
|
12353
|
-
return;
|
|
12354
|
-
}
|
|
12355
|
-
switch (value.toLowerCase()) {
|
|
12356
|
-
case "true":
|
|
12357
|
-
case "1":
|
|
12358
|
-
return true;
|
|
12359
|
-
case "false":
|
|
12360
|
-
case "0":
|
|
12361
|
-
return false;
|
|
12362
|
-
default:
|
|
12363
|
-
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
12364
|
-
}
|
|
12395
|
+
function resolveCsrfSecret() {
|
|
12396
|
+
return requireConfiguredSecret(["SESSION_SECRET", "CSRF_SECRET"], "csrf-secret");
|
|
12365
12397
|
}
|
|
12366
|
-
function
|
|
12367
|
-
|
|
12368
|
-
if (value === null || value.trim() === "") {
|
|
12369
|
-
return;
|
|
12370
|
-
}
|
|
12371
|
-
if (!allowedValues.includes(value)) {
|
|
12372
|
-
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
12373
|
-
}
|
|
12374
|
-
return value;
|
|
12398
|
+
function csrfVerifyOptions() {
|
|
12399
|
+
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
12375
12400
|
}
|
|
12376
|
-
function
|
|
12377
|
-
|
|
12378
|
-
|
|
12401
|
+
function tokensMatch(left, right) {
|
|
12402
|
+
const leftBuffer = Buffer.from(left);
|
|
12403
|
+
const rightBuffer = Buffer.from(right);
|
|
12404
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
12405
|
+
return false;
|
|
12379
12406
|
}
|
|
12380
|
-
return
|
|
12407
|
+
return timingSafeEqual3(leftBuffer, rightBuffer);
|
|
12381
12408
|
}
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
}
|
|
12389
|
-
return validator(payload);
|
|
12409
|
+
function createCsrfTokenCookie() {
|
|
12410
|
+
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
12411
|
+
const secure = isProductionEnv() ? "; Secure" : "";
|
|
12412
|
+
return {
|
|
12413
|
+
token,
|
|
12414
|
+
cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
|
|
12415
|
+
};
|
|
12390
12416
|
}
|
|
12391
|
-
function
|
|
12392
|
-
const
|
|
12393
|
-
if (
|
|
12394
|
-
|
|
12395
|
-
}
|
|
12396
|
-
const trimmed = value.trim();
|
|
12397
|
-
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
12398
|
-
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
12399
|
-
}
|
|
12400
|
-
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
12401
|
-
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
12402
|
-
}
|
|
12403
|
-
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
12404
|
-
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
12417
|
+
function resolveCsrfToken(request) {
|
|
12418
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
12419
|
+
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
12420
|
+
return { token: cookieValue };
|
|
12405
12421
|
}
|
|
12406
|
-
return
|
|
12422
|
+
return createCsrfTokenCookie();
|
|
12407
12423
|
}
|
|
12408
|
-
function
|
|
12409
|
-
|
|
12410
|
-
|
|
12424
|
+
function readSubmittedCsrfToken(request) {
|
|
12425
|
+
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
12426
|
+
if (headerToken) {
|
|
12427
|
+
return headerToken;
|
|
12411
12428
|
}
|
|
12412
|
-
return
|
|
12429
|
+
return null;
|
|
12413
12430
|
}
|
|
12414
|
-
function
|
|
12415
|
-
const
|
|
12416
|
-
if (
|
|
12417
|
-
|
|
12431
|
+
async function readSubmittedCsrfTokenFromBody(request) {
|
|
12432
|
+
const headerToken = readSubmittedCsrfToken(request);
|
|
12433
|
+
if (headerToken) {
|
|
12434
|
+
return headerToken;
|
|
12418
12435
|
}
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12436
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
12437
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
12438
|
+
const formData = await request.clone().formData();
|
|
12439
|
+
const field = formData.get("_token");
|
|
12440
|
+
if (typeof field === "string" && field.trim().length > 0) {
|
|
12441
|
+
return field.trim();
|
|
12442
|
+
}
|
|
12443
|
+
const legacyField = formData.get("_csrf");
|
|
12444
|
+
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
12445
|
+
return legacyField.trim();
|
|
12446
|
+
}
|
|
12424
12447
|
}
|
|
12425
|
-
return
|
|
12448
|
+
return null;
|
|
12426
12449
|
}
|
|
12427
|
-
function
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
12450
|
+
function verifyCsrfToken(request, submittedToken) {
|
|
12451
|
+
if (!submittedToken) {
|
|
12452
|
+
return false;
|
|
12431
12453
|
}
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
if (!(field in payload) || payload[field] === undefined) {
|
|
12436
|
-
return;
|
|
12454
|
+
const cookieValue = readRequestCookie(request, csrfCookieName());
|
|
12455
|
+
if (!cookieValue) {
|
|
12456
|
+
return false;
|
|
12437
12457
|
}
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
function parsePositiveIntParam(value, name = "id") {
|
|
12441
|
-
const parsed = Number.parseInt(value, 10);
|
|
12442
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12443
|
-
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
12458
|
+
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
12459
|
+
return false;
|
|
12444
12460
|
}
|
|
12445
|
-
return
|
|
12461
|
+
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
12446
12462
|
}
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
return true;
|
|
12452
|
-
}
|
|
12453
|
-
async validate(request) {
|
|
12454
|
-
if (!await this.authorize(request)) {
|
|
12455
|
-
throw new ForbiddenError;
|
|
12456
|
-
}
|
|
12457
|
-
return await parseJsonBody(request, (payload) => this.parse(payload));
|
|
12463
|
+
function resolveCsrfTokenForRequest(request) {
|
|
12464
|
+
const metaToken = currentRequestMeta().csrfToken;
|
|
12465
|
+
if (metaToken) {
|
|
12466
|
+
return metaToken;
|
|
12458
12467
|
}
|
|
12468
|
+
return resolveCsrfToken(request).token;
|
|
12459
12469
|
}
|
|
12460
12470
|
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12471
|
+
// ../../src/core/http/csrfMiddleware.ts
|
|
12472
|
+
var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
12473
|
+
function samlAcsPathname() {
|
|
12474
|
+
const configured = process.env.SAML_ACS_URL?.trim();
|
|
12475
|
+
if (!configured) {
|
|
12476
|
+
return "/auth/saml/acs";
|
|
12464
12477
|
}
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
const resolved = typeof auth.resolveWithSource === "function" ? await auth.resolveWithSource(request) : { user: await auth.resolve(request), credentialSource: null };
|
|
12470
|
-
return await runWithAuthContext(resolved.user, resolved.credentialSource, async () => {
|
|
12471
|
-
return await next();
|
|
12472
|
-
});
|
|
12473
|
-
};
|
|
12474
|
-
}
|
|
12475
|
-
// ../../src/core/http/authorizeMiddleware.ts
|
|
12476
|
-
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
12477
|
-
return async (request, next) => {
|
|
12478
|
-
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
12479
|
-
if (!gate.allows(resource, action, user)) {
|
|
12480
|
-
const error = new ForbiddenError;
|
|
12481
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
12482
|
-
}
|
|
12483
|
-
return await next();
|
|
12484
|
-
};
|
|
12485
|
-
}
|
|
12486
|
-
// ../../src/core/http/middleware.ts
|
|
12487
|
-
function isRouteHandler(value) {
|
|
12488
|
-
return typeof value === "function";
|
|
12489
|
-
}
|
|
12490
|
-
function isMethodRouteMap(value) {
|
|
12491
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
12492
|
-
return false;
|
|
12478
|
+
try {
|
|
12479
|
+
return new URL(configured, "http://strata.invalid").pathname;
|
|
12480
|
+
} catch {
|
|
12481
|
+
return "/auth/saml/acs";
|
|
12493
12482
|
}
|
|
12494
|
-
const entries = Object.entries(value);
|
|
12495
|
-
return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
|
|
12496
12483
|
}
|
|
12497
|
-
function
|
|
12498
|
-
return (
|
|
12499
|
-
return async (request) => {
|
|
12500
|
-
let index = 0;
|
|
12501
|
-
const dispatch = async () => {
|
|
12502
|
-
if (index >= middleware.length) {
|
|
12503
|
-
return await handler(request);
|
|
12504
|
-
}
|
|
12505
|
-
const current = middleware[index];
|
|
12506
|
-
index += 1;
|
|
12507
|
-
if (!current) {
|
|
12508
|
-
return await handler(request);
|
|
12509
|
-
}
|
|
12510
|
-
return await current(request, dispatch);
|
|
12511
|
-
};
|
|
12512
|
-
return await dispatch();
|
|
12513
|
-
};
|
|
12514
|
-
};
|
|
12484
|
+
function skipsCsrfPath(pathname) {
|
|
12485
|
+
return pathname === samlAcsPathname() || pathname.startsWith("/scim/") || pathname.startsWith("/billing/webhooks/");
|
|
12515
12486
|
}
|
|
12516
|
-
|
|
12517
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
12518
|
-
const response = await next();
|
|
12487
|
+
function appendSetCookie(response, cookie) {
|
|
12519
12488
|
const headers = new Headers(response.headers);
|
|
12520
|
-
headers.
|
|
12489
|
+
headers.append("set-cookie", cookie);
|
|
12521
12490
|
return new Response(response.body, {
|
|
12522
12491
|
status: response.status,
|
|
12523
12492
|
statusText: response.statusText,
|
|
12524
12493
|
headers
|
|
12525
12494
|
});
|
|
12526
12495
|
}
|
|
12527
|
-
function
|
|
12528
|
-
|
|
12529
|
-
const
|
|
12530
|
-
|
|
12531
|
-
|
|
12496
|
+
function createCsrfMiddleware() {
|
|
12497
|
+
return async (request, next) => {
|
|
12498
|
+
const credentialSource = currentCredentialSource();
|
|
12499
|
+
if (credentialSource === "bearer" || credentialSource === "basic") {
|
|
12500
|
+
return await next();
|
|
12532
12501
|
}
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
12537
|
-
|
|
12538
|
-
|
|
12502
|
+
const pathname = new URL(request.url).pathname;
|
|
12503
|
+
if (skipsCsrfPath(pathname)) {
|
|
12504
|
+
return await next();
|
|
12505
|
+
}
|
|
12506
|
+
const method = request.method.toUpperCase();
|
|
12507
|
+
if (!MUTATING_METHODS.has(method)) {
|
|
12508
|
+
const meta = currentRequestMeta();
|
|
12509
|
+
const csrf = meta.csrfToken ? { token: meta.csrfToken } : resolveCsrfToken(request);
|
|
12510
|
+
meta.csrfToken = csrf.token;
|
|
12511
|
+
const response = await next();
|
|
12512
|
+
if (!csrf.cookie) {
|
|
12513
|
+
return response;
|
|
12514
|
+
}
|
|
12515
|
+
return appendSetCookie(response, csrf.cookie);
|
|
12516
|
+
}
|
|
12517
|
+
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
12518
|
+
if (!verifyCsrfToken(request, submitted)) {
|
|
12519
|
+
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
12520
|
+
}
|
|
12521
|
+
const originHeader = request.headers.get("origin");
|
|
12522
|
+
let headerOrigin = null;
|
|
12523
|
+
if (originHeader) {
|
|
12524
|
+
try {
|
|
12525
|
+
headerOrigin = new URL(originHeader).origin;
|
|
12526
|
+
} catch {
|
|
12527
|
+
headerOrigin = null;
|
|
12528
|
+
}
|
|
12529
|
+
}
|
|
12530
|
+
if (!headerOrigin || headerOrigin !== new URL(request.url).origin) {
|
|
12531
|
+
throw new ForbiddenError("Cross-site cookie requests are not allowed.");
|
|
12532
|
+
}
|
|
12533
|
+
return await next();
|
|
12534
|
+
};
|
|
12539
12535
|
}
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12536
|
+
// ../../src/core/http/csrfProtection.ts
|
|
12537
|
+
var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
|
|
12538
|
+
function createCsrfProtection(secret, options = {}) {
|
|
12539
|
+
const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
|
|
12540
|
+
const maxAge = options.maxAge ?? expiresIn;
|
|
12541
|
+
return {
|
|
12542
|
+
generate(_sessionKey) {
|
|
12543
|
+
return Bun.CSRF.generate(secret, { expiresIn });
|
|
12544
|
+
},
|
|
12545
|
+
verify(token, _sessionKey) {
|
|
12546
|
+
if (!token) {
|
|
12547
|
+
return false;
|
|
12548
|
+
}
|
|
12549
|
+
return Bun.CSRF.verify(token, { secret, maxAge });
|
|
12550
|
+
},
|
|
12551
|
+
secret
|
|
12552
|
+
};
|
|
12547
12553
|
}
|
|
12548
|
-
// ../../src/core/http/
|
|
12549
|
-
|
|
12550
|
-
var
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
12555
|
-
}
|
|
12556
|
-
const parsed = Number.parseInt(value, 10);
|
|
12557
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12558
|
-
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
12559
|
-
}
|
|
12560
|
-
return parsed;
|
|
12554
|
+
// ../../src/core/http/flashSession.ts
|
|
12555
|
+
import { createHmac as createHmac5, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
12556
|
+
var FLASH_COOKIE = appCookieName("flash");
|
|
12557
|
+
var FLASH_TTL_MS = 60 * 1000;
|
|
12558
|
+
function flashCookieName() {
|
|
12559
|
+
return process.env.FLASH_COOKIE_NAME?.trim() || appCookieName("flash");
|
|
12561
12560
|
}
|
|
12562
|
-
function
|
|
12563
|
-
|
|
12564
|
-
const pageParam = params.get("page");
|
|
12565
|
-
const perPageParam = params.get("per_page");
|
|
12566
|
-
const page = pageParam === null || pageParam.trim() === "" ? 1 : parseRequiredPositiveIntQueryParam(params, "page");
|
|
12567
|
-
if (perPageParam === null || perPageParam.trim() === "") {
|
|
12568
|
-
return { page, perPage: DEFAULT_PER_PAGE };
|
|
12569
|
-
}
|
|
12570
|
-
const perPage = parseRequiredPositiveIntQueryParam(params, "per_page");
|
|
12571
|
-
if (perPage > MAX_PER_PAGE) {
|
|
12572
|
-
throw new BadRequestError(`Invalid query parameter "per_page". Maximum allowed value is ${MAX_PER_PAGE}.`);
|
|
12573
|
-
}
|
|
12574
|
-
return { page, perPage };
|
|
12561
|
+
function resolveFlashSecret() {
|
|
12562
|
+
return requireConfiguredSecret(["SESSION_SECRET", "OAUTH_STATE_SECRET"], "flash-secret");
|
|
12575
12563
|
}
|
|
12576
|
-
function
|
|
12577
|
-
|
|
12564
|
+
function signFlashPayload(payload, issuedAt) {
|
|
12565
|
+
const signature = createHmac5("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
12566
|
+
return `${payload}.${issuedAt}.${signature}`;
|
|
12578
12567
|
}
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
"application/json",
|
|
12584
|
-
"application/zip",
|
|
12585
|
-
"application/x-zip-compressed",
|
|
12586
|
-
"image/jpeg",
|
|
12587
|
-
"image/png",
|
|
12588
|
-
"image/gif",
|
|
12589
|
-
"image/webp",
|
|
12590
|
-
"text/plain",
|
|
12591
|
-
"text/csv"
|
|
12592
|
-
]);
|
|
12593
|
-
function resolveMaxUploadBytes() {
|
|
12594
|
-
const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
12595
|
-
if (!raw) {
|
|
12596
|
-
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
12597
|
-
}
|
|
12598
|
-
const parsed = Number.parseInt(raw, 10);
|
|
12599
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12600
|
-
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
12568
|
+
function readFlashCookie(request) {
|
|
12569
|
+
const cookieHeader = request.headers.get("cookie");
|
|
12570
|
+
if (!cookieHeader) {
|
|
12571
|
+
return null;
|
|
12601
12572
|
}
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
}
|
|
12607
|
-
function isAllowedMimeType(mimeType) {
|
|
12608
|
-
const normalized = normalizeMimeType(mimeType);
|
|
12609
|
-
if (!normalized || normalized === "application/octet-stream") {
|
|
12610
|
-
return envFlagEnabled(process.env.UPLOAD_ALLOW_UNKNOWN_MIME);
|
|
12573
|
+
for (const part of cookieHeader.split(";")) {
|
|
12574
|
+
const [name, ...rest] = part.trim().split("=");
|
|
12575
|
+
if (name === flashCookieName()) {
|
|
12576
|
+
return decodeURIComponent(rest.join("="));
|
|
12577
|
+
}
|
|
12611
12578
|
}
|
|
12612
|
-
return
|
|
12613
|
-
}
|
|
12614
|
-
|
|
12615
|
-
// ../../src/core/http/parseMultipartUpload.ts
|
|
12616
|
-
function normalizeMimeType2(mimeType) {
|
|
12617
|
-
return mimeType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
|
|
12618
|
-
}
|
|
12619
|
-
function sanitizeUploadFileName(name) {
|
|
12620
|
-
const base = name.split(/[/\\]/).pop()?.trim() ?? "upload";
|
|
12621
|
-
const sanitized = base.replace(/[^\w.\-()+ ]+/g, "_").slice(0, 200);
|
|
12622
|
-
return sanitized.length > 0 ? sanitized : "upload";
|
|
12579
|
+
return null;
|
|
12623
12580
|
}
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12581
|
+
function parseFlashCookie(cookieValue) {
|
|
12582
|
+
const parts = cookieValue.split(".");
|
|
12583
|
+
if (parts.length < 3) {
|
|
12584
|
+
return null;
|
|
12627
12585
|
}
|
|
12628
|
-
|
|
12629
|
-
|
|
12586
|
+
const signature = parts.pop();
|
|
12587
|
+
const issuedAtRaw = parts.pop();
|
|
12588
|
+
const payload = parts.join(".");
|
|
12589
|
+
if (!signature || !issuedAtRaw || !payload) {
|
|
12590
|
+
return null;
|
|
12630
12591
|
}
|
|
12631
|
-
const
|
|
12632
|
-
if (
|
|
12633
|
-
|
|
12592
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
12593
|
+
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
12594
|
+
return null;
|
|
12634
12595
|
}
|
|
12635
|
-
const
|
|
12636
|
-
if (!
|
|
12637
|
-
|
|
12596
|
+
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
12597
|
+
if (!expectedSignature) {
|
|
12598
|
+
return null;
|
|
12638
12599
|
}
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
contents: new Uint8Array(await file.arrayBuffer())
|
|
12644
|
-
};
|
|
12645
|
-
}
|
|
12646
|
-
async function parseMultipartUpload(request, fieldName = "file") {
|
|
12647
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
12648
|
-
if (!contentType.includes("multipart/form-data")) {
|
|
12649
|
-
throw new BadRequestError("Expected multipart form data.");
|
|
12600
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
12601
|
+
const actualBuffer = Buffer.from(signature);
|
|
12602
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
12603
|
+
return null;
|
|
12650
12604
|
}
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
if (!(value instanceof File)) {
|
|
12654
|
-
throw new BadRequestError(`Missing upload field "${fieldName}".`);
|
|
12605
|
+
if (!timingSafeEqual4(expectedBuffer, actualBuffer)) {
|
|
12606
|
+
return null;
|
|
12655
12607
|
}
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
return async (request, next) => {
|
|
12661
|
-
if (!await auth.check(request)) {
|
|
12662
|
-
const error = new UnauthorizedError;
|
|
12663
|
-
return Response.json({ error: error.message }, { status: error.status });
|
|
12608
|
+
try {
|
|
12609
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
12610
|
+
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
12611
|
+
return null;
|
|
12664
12612
|
}
|
|
12665
|
-
|
|
12666
|
-
|
|
12613
|
+
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
12614
|
+
return null;
|
|
12615
|
+
}
|
|
12616
|
+
return parsed;
|
|
12617
|
+
} catch {
|
|
12618
|
+
return null;
|
|
12619
|
+
}
|
|
12667
12620
|
}
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
return
|
|
12621
|
+
function clearFlashCookie() {
|
|
12622
|
+
const secure = isProductionEnv() ? "; Secure" : "";
|
|
12623
|
+
return `${flashCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`;
|
|
12671
12624
|
}
|
|
12672
|
-
function
|
|
12673
|
-
const
|
|
12674
|
-
if (
|
|
12675
|
-
return;
|
|
12676
|
-
}
|
|
12677
|
-
if (value === null) {
|
|
12625
|
+
function pullFlash(request) {
|
|
12626
|
+
const cookieValue = readFlashCookie(request);
|
|
12627
|
+
if (!cookieValue) {
|
|
12678
12628
|
return null;
|
|
12679
12629
|
}
|
|
12680
|
-
return
|
|
12630
|
+
return parseFlashCookie(cookieValue);
|
|
12631
|
+
}
|
|
12632
|
+
function withFlashClear(response) {
|
|
12633
|
+
const headers = new Headers(response.headers);
|
|
12634
|
+
headers.append("set-cookie", clearFlashCookie());
|
|
12635
|
+
return new Response(response.body, {
|
|
12636
|
+
status: response.status,
|
|
12637
|
+
statusText: response.statusText,
|
|
12638
|
+
headers
|
|
12639
|
+
});
|
|
12681
12640
|
}
|
|
12682
12641
|
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12642
|
+
// ../../src/core/http/flashMiddleware.ts
|
|
12643
|
+
function createFlashMiddleware() {
|
|
12644
|
+
return async (request, next) => {
|
|
12645
|
+
const flash = pullFlash(request);
|
|
12646
|
+
const meta = currentRequestMeta();
|
|
12647
|
+
return await runWithRequestMeta({ ...meta, request, flash }, async () => {
|
|
12648
|
+
const response = await next();
|
|
12649
|
+
if (flash) {
|
|
12650
|
+
return withFlashClear(response);
|
|
12651
|
+
}
|
|
12652
|
+
return response;
|
|
12653
|
+
});
|
|
12654
|
+
};
|
|
12655
|
+
}
|
|
12656
|
+
// ../../src/core/http/validation.ts
|
|
12657
|
+
init_tenantContext();
|
|
12658
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
12659
|
+
if (!request) {
|
|
12660
|
+
return fallbackPath;
|
|
12695
12661
|
}
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12662
|
+
const url = new URL(request.url);
|
|
12663
|
+
const user = currentAuthUser();
|
|
12664
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
12665
|
+
const tenantScope = `t:${currentTenant()?.id ?? "none"}`;
|
|
12666
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
12667
|
+
}
|
|
12668
|
+
function getQueryParams(request) {
|
|
12669
|
+
if (!request) {
|
|
12670
|
+
return new URLSearchParams;
|
|
12699
12671
|
}
|
|
12700
|
-
|
|
12701
|
-
|
|
12672
|
+
return new URL(request.url).searchParams;
|
|
12673
|
+
}
|
|
12674
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
12675
|
+
const value = params.get(name);
|
|
12676
|
+
if (value === null || value.trim() === "") {
|
|
12677
|
+
return;
|
|
12702
12678
|
}
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
return;
|
|
12707
|
-
}
|
|
12708
|
-
return whenLoaded(model, relation, transform);
|
|
12679
|
+
const parsed = Number.parseInt(value, 10);
|
|
12680
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12681
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
12709
12682
|
}
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12683
|
+
return parsed;
|
|
12684
|
+
}
|
|
12685
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
12686
|
+
const value = params.get(name);
|
|
12687
|
+
if (value === null || value.trim() === "") {
|
|
12688
|
+
return;
|
|
12715
12689
|
}
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12690
|
+
switch (value.toLowerCase()) {
|
|
12691
|
+
case "true":
|
|
12692
|
+
case "1":
|
|
12693
|
+
return true;
|
|
12694
|
+
case "false":
|
|
12695
|
+
case "0":
|
|
12696
|
+
return false;
|
|
12697
|
+
default:
|
|
12698
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
12723
12699
|
}
|
|
12724
12700
|
}
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
const wrap = this.constructor.wrap;
|
|
12730
|
-
if (wrap === null) {
|
|
12731
|
-
return { data: items };
|
|
12732
|
-
}
|
|
12733
|
-
return { [wrap]: items };
|
|
12701
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
12702
|
+
const value = params.get(name);
|
|
12703
|
+
if (value === null || value.trim() === "") {
|
|
12704
|
+
return;
|
|
12734
12705
|
}
|
|
12735
|
-
|
|
12736
|
-
|
|
12706
|
+
if (!allowedValues.includes(value)) {
|
|
12707
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
12737
12708
|
}
|
|
12709
|
+
return value;
|
|
12738
12710
|
}
|
|
12739
|
-
function
|
|
12740
|
-
|
|
12711
|
+
function expectObject(value, label = "request body") {
|
|
12712
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
12713
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
12714
|
+
}
|
|
12715
|
+
return value;
|
|
12741
12716
|
}
|
|
12742
|
-
function
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
}
|
|
12717
|
+
async function parseJsonBody(request, validator) {
|
|
12718
|
+
let payload;
|
|
12719
|
+
try {
|
|
12720
|
+
payload = await request.json();
|
|
12721
|
+
} catch {
|
|
12722
|
+
throw new BadRequestError("Request body must be valid JSON.");
|
|
12723
|
+
}
|
|
12724
|
+
return validator(payload);
|
|
12747
12725
|
}
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12726
|
+
function readRequiredString(payload, field, options = {}) {
|
|
12727
|
+
const value = payload[field];
|
|
12728
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
12729
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
12730
|
+
}
|
|
12751
12731
|
const trimmed = value.trim();
|
|
12752
|
-
if (
|
|
12753
|
-
|
|
12732
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
12733
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
12754
12734
|
}
|
|
12755
|
-
if (
|
|
12756
|
-
|
|
12735
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
12736
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
12757
12737
|
}
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
|
|
12761
|
-
return true;
|
|
12762
|
-
}
|
|
12763
|
-
} catch {
|
|
12764
|
-
return true;
|
|
12738
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
12739
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
12765
12740
|
}
|
|
12766
|
-
return
|
|
12741
|
+
return trimmed;
|
|
12767
12742
|
}
|
|
12768
|
-
function
|
|
12769
|
-
if (!
|
|
12770
|
-
return
|
|
12743
|
+
function readOptionalString(payload, field, options = {}) {
|
|
12744
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
12745
|
+
return;
|
|
12771
12746
|
}
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12747
|
+
return readRequiredString(payload, field, options);
|
|
12748
|
+
}
|
|
12749
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
12750
|
+
const value = readRequiredString(payload, field);
|
|
12751
|
+
if (!allowedValues.includes(value)) {
|
|
12752
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
12753
|
+
}
|
|
12754
|
+
return value;
|
|
12755
|
+
}
|
|
12756
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
12757
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
12758
|
+
return;
|
|
12784
12759
|
}
|
|
12785
|
-
|
|
12786
|
-
return serialized ? `?${serialized}` : "";
|
|
12760
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
12787
12761
|
}
|
|
12788
|
-
function
|
|
12789
|
-
const
|
|
12790
|
-
if (
|
|
12791
|
-
|
|
12762
|
+
function readRequiredPositiveInt(payload, field) {
|
|
12763
|
+
const value = payload[field];
|
|
12764
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
12765
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
12792
12766
|
}
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
return fallback;
|
|
12767
|
+
return value;
|
|
12768
|
+
}
|
|
12769
|
+
function readOptionalPositiveInt(payload, field) {
|
|
12770
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
12771
|
+
return;
|
|
12799
12772
|
}
|
|
12800
|
-
|
|
12801
|
-
return `${pathname}${sanitizeNestedQuery(search)}`;
|
|
12773
|
+
return readRequiredPositiveInt(payload, field);
|
|
12802
12774
|
}
|
|
12803
|
-
function
|
|
12804
|
-
const
|
|
12805
|
-
|
|
12775
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
12776
|
+
const parsed = Number.parseInt(value, 10);
|
|
12777
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12778
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
12779
|
+
}
|
|
12780
|
+
return parsed;
|
|
12806
12781
|
}
|
|
12807
|
-
|
|
12808
|
-
|
|
12782
|
+
|
|
12783
|
+
// ../../src/core/http/formRequest.ts
|
|
12784
|
+
class FormRequest {
|
|
12785
|
+
authorize(_request) {
|
|
12786
|
+
return true;
|
|
12787
|
+
}
|
|
12788
|
+
async validate(request) {
|
|
12789
|
+
if (!await this.authorize(request)) {
|
|
12790
|
+
throw new ForbiddenError;
|
|
12791
|
+
}
|
|
12792
|
+
return await parseJsonBody(request, (payload) => this.parse(payload));
|
|
12793
|
+
}
|
|
12809
12794
|
}
|
|
12810
12795
|
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12796
|
+
class QueryFormRequest {
|
|
12797
|
+
validate(request) {
|
|
12798
|
+
return this.parseQuery(request);
|
|
12799
|
+
}
|
|
12814
12800
|
}
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12801
|
+
// ../../src/core/http/authMiddleware.ts
|
|
12802
|
+
function createAuthMiddleware(auth) {
|
|
12803
|
+
return async (request, next) => {
|
|
12804
|
+
const resolved = typeof auth.resolveWithSource === "function" ? await auth.resolveWithSource(request) : { user: await auth.resolve(request), credentialSource: null };
|
|
12805
|
+
return await runWithAuthContext(resolved.user, resolved.credentialSource, async () => {
|
|
12806
|
+
return await next();
|
|
12807
|
+
});
|
|
12808
|
+
};
|
|
12809
|
+
}
|
|
12810
|
+
// ../../src/core/http/authorizeMiddleware.ts
|
|
12811
|
+
function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
12812
|
+
return async (request, next) => {
|
|
12813
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
12814
|
+
if (!gate.allows(resource, action, user)) {
|
|
12815
|
+
const error = new ForbiddenError;
|
|
12816
|
+
return errorResponse(error);
|
|
12821
12817
|
}
|
|
12822
|
-
|
|
12818
|
+
return await next();
|
|
12819
|
+
};
|
|
12823
12820
|
}
|
|
12824
|
-
|
|
12825
|
-
|
|
12821
|
+
// ../../src/core/http/middleware.ts
|
|
12822
|
+
function isRouteHandler(value) {
|
|
12823
|
+
return typeof value === "function";
|
|
12826
12824
|
}
|
|
12827
|
-
function
|
|
12828
|
-
if (
|
|
12829
|
-
|
|
12825
|
+
function isMethodRouteMap(value) {
|
|
12826
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
12827
|
+
return false;
|
|
12830
12828
|
}
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
headers: {
|
|
12834
|
-
Location: sanitizeInternalPath(location, "/")
|
|
12835
|
-
}
|
|
12836
|
-
});
|
|
12829
|
+
const entries = Object.entries(value);
|
|
12830
|
+
return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
|
|
12837
12831
|
}
|
|
12838
|
-
function
|
|
12839
|
-
return
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12832
|
+
function composeMiddleware(...middleware) {
|
|
12833
|
+
return (handler) => {
|
|
12834
|
+
return async (request) => {
|
|
12835
|
+
let index = 0;
|
|
12836
|
+
const dispatch = async () => {
|
|
12837
|
+
if (index >= middleware.length) {
|
|
12838
|
+
return await handler(request);
|
|
12839
|
+
}
|
|
12840
|
+
const current = middleware[index];
|
|
12841
|
+
index += 1;
|
|
12842
|
+
if (!current) {
|
|
12843
|
+
return await handler(request);
|
|
12844
|
+
}
|
|
12845
|
+
return await current(request, dispatch);
|
|
12846
|
+
};
|
|
12847
|
+
return await dispatch();
|
|
12848
|
+
};
|
|
12849
|
+
};
|
|
12845
12850
|
}
|
|
12846
|
-
function
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12851
|
+
async function requestIdMiddleware(request, next) {
|
|
12852
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
12853
|
+
const response = await next();
|
|
12854
|
+
const headers = new Headers(response.headers);
|
|
12855
|
+
headers.set("x-request-id", requestId);
|
|
12856
|
+
return new Response(response.body, {
|
|
12857
|
+
status: response.status,
|
|
12858
|
+
statusText: response.statusText,
|
|
12859
|
+
headers
|
|
12852
12860
|
});
|
|
12853
12861
|
}
|
|
12854
|
-
function
|
|
12855
|
-
|
|
12856
|
-
}
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12862
|
+
function wrapRouteHandler(handler, middleware) {
|
|
12863
|
+
if (isMethodRouteMap(handler)) {
|
|
12864
|
+
const wrapped = {};
|
|
12865
|
+
for (const [method, routeHandler] of Object.entries(handler)) {
|
|
12866
|
+
wrapped[method] = composeMiddleware(...middleware)(routeHandler);
|
|
12867
|
+
}
|
|
12868
|
+
return wrapped;
|
|
12869
|
+
}
|
|
12870
|
+
if (isRouteHandler(handler)) {
|
|
12871
|
+
return composeMiddleware(...middleware)(handler);
|
|
12872
|
+
}
|
|
12873
|
+
return handler;
|
|
12865
12874
|
}
|
|
12866
|
-
function
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
const previous = new URL(request.headers.get("referer") ?? "");
|
|
12872
|
-
if (previous.origin !== current.origin || (request.method === "GET" || request.method === "HEAD") && previous.pathname === current.pathname)
|
|
12873
|
-
return "/";
|
|
12874
|
-
return sanitizeInternalPath(previous.pathname, "/");
|
|
12875
|
-
} catch {
|
|
12876
|
-
return "/";
|
|
12875
|
+
function applyMiddlewareToRoutes(routes, middleware) {
|
|
12876
|
+
const wrapped = {};
|
|
12877
|
+
for (const [path, routeHandler] of Object.entries(routes)) {
|
|
12878
|
+
const identity = async (_request, next) => await runWithRequestMeta({ ...currentRequestMeta(), routeTemplate: path }, next);
|
|
12879
|
+
wrapped[path] = wrapRouteHandler(routeHandler, [identity, ...middleware]);
|
|
12877
12880
|
}
|
|
12881
|
+
return wrapped;
|
|
12878
12882
|
}
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
const
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
</head>
|
|
12893
|
-
<body>
|
|
12894
|
-
<header class="site-header">
|
|
12895
|
-
<a class="brand" href="/">Home</a>
|
|
12896
|
-
</header>
|
|
12897
|
-
<main class="site-main">
|
|
12898
|
-
<section class="page-header">
|
|
12899
|
-
<h1>${title}</h1>
|
|
12900
|
-
<p>${message}</p>
|
|
12901
|
-
${details}
|
|
12902
|
-
${goBack}
|
|
12903
|
-
</section>
|
|
12904
|
-
</main>
|
|
12905
|
-
</body>
|
|
12906
|
-
</html>
|
|
12907
|
-
`;
|
|
12883
|
+
// ../../src/core/http/pagination.ts
|
|
12884
|
+
var DEFAULT_PER_PAGE = 15;
|
|
12885
|
+
var MAX_PER_PAGE = 100;
|
|
12886
|
+
function parseRequiredPositiveIntQueryParam(params, name) {
|
|
12887
|
+
const value = params.get(name);
|
|
12888
|
+
if (value === null || value.trim() === "") {
|
|
12889
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
12890
|
+
}
|
|
12891
|
+
const parsed = Number.parseInt(value, 10);
|
|
12892
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12893
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
12894
|
+
}
|
|
12895
|
+
return parsed;
|
|
12908
12896
|
}
|
|
12909
|
-
function
|
|
12910
|
-
|
|
12911
|
-
|
|
12897
|
+
function parsePaginationQuery(request) {
|
|
12898
|
+
const params = getQueryParams(request);
|
|
12899
|
+
const pageParam = params.get("page");
|
|
12900
|
+
const perPageParam = params.get("per_page");
|
|
12901
|
+
const page = pageParam === null || pageParam.trim() === "" ? 1 : parseRequiredPositiveIntQueryParam(params, "page");
|
|
12902
|
+
if (perPageParam === null || perPageParam.trim() === "") {
|
|
12903
|
+
return { page, perPage: DEFAULT_PER_PAGE };
|
|
12912
12904
|
}
|
|
12913
|
-
|
|
12914
|
-
|
|
12905
|
+
const perPage = parseRequiredPositiveIntQueryParam(params, "per_page");
|
|
12906
|
+
if (perPage > MAX_PER_PAGE) {
|
|
12907
|
+
throw new BadRequestError(`Invalid query parameter "per_page". Maximum allowed value is ${MAX_PER_PAGE}.`);
|
|
12915
12908
|
}
|
|
12916
|
-
return
|
|
12909
|
+
return { page, perPage };
|
|
12917
12910
|
}
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12911
|
+
function paginatedResponse(data, meta, init = {}) {
|
|
12912
|
+
return Response.json({ data, meta }, init);
|
|
12913
|
+
}
|
|
12914
|
+
// ../../src/core/http/uploads.ts
|
|
12915
|
+
var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
12916
|
+
var ALLOWED_UPLOAD_MIME_TYPES = new Set([
|
|
12917
|
+
"application/pdf",
|
|
12918
|
+
"application/json",
|
|
12919
|
+
"application/zip",
|
|
12920
|
+
"application/x-zip-compressed",
|
|
12921
|
+
"image/jpeg",
|
|
12922
|
+
"image/png",
|
|
12923
|
+
"image/gif",
|
|
12924
|
+
"image/webp",
|
|
12925
|
+
"text/plain",
|
|
12926
|
+
"text/csv"
|
|
12927
|
+
]);
|
|
12928
|
+
function resolveMaxUploadBytes() {
|
|
12929
|
+
const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
12930
|
+
if (!raw) {
|
|
12931
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
12922
12932
|
}
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
request: input.request ?? currentRequestMeta().request
|
|
12927
|
-
});
|
|
12928
|
-
} catch {
|
|
12929
|
-
return renderKernelErrorChrome(input);
|
|
12933
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12934
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12935
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
12930
12936
|
}
|
|
12937
|
+
return parsed;
|
|
12931
12938
|
}
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
response.headers.set("Cache-Control", "no-store");
|
|
12935
|
-
return response;
|
|
12939
|
+
function normalizeMimeType(mimeType) {
|
|
12940
|
+
return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
12936
12941
|
}
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
return response;
|
|
12942
|
+
function isAllowedMimeType(mimeType) {
|
|
12943
|
+
const normalized = normalizeMimeType(mimeType);
|
|
12944
|
+
if (!normalized || normalized === "application/octet-stream") {
|
|
12945
|
+
return envFlagEnabled(process.env.UPLOAD_ALLOW_UNKNOWN_MIME);
|
|
12942
12946
|
}
|
|
12943
|
-
return
|
|
12944
|
-
status: 404,
|
|
12945
|
-
title: "Not Found",
|
|
12946
|
-
message: "The page you requested was not found."
|
|
12947
|
-
});
|
|
12947
|
+
return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
|
|
12948
12948
|
}
|
|
12949
12949
|
|
|
12950
|
-
// ../../src/core/http/
|
|
12951
|
-
function
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
12950
|
+
// ../../src/core/http/parseMultipartUpload.ts
|
|
12951
|
+
function normalizeMimeType2(mimeType) {
|
|
12952
|
+
return mimeType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
|
|
12953
|
+
}
|
|
12954
|
+
function sanitizeUploadFileName(name) {
|
|
12955
|
+
const base = name.split(/[/\\]/).pop()?.trim() ?? "upload";
|
|
12956
|
+
const sanitized = base.replace(/[^\w.\-()+ ]+/g, "_").slice(0, 200);
|
|
12957
|
+
return sanitized.length > 0 ? sanitized : "upload";
|
|
12958
|
+
}
|
|
12959
|
+
async function validateUploadFile(file, fieldName = "file") {
|
|
12960
|
+
if (!(file instanceof File)) {
|
|
12961
|
+
throw new BadRequestError(`Missing upload field "${fieldName}".`);
|
|
12957
12962
|
}
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
return true;
|
|
12963
|
+
if (file.size <= 0) {
|
|
12964
|
+
throw new BadRequestError("Uploaded file is empty.");
|
|
12961
12965
|
}
|
|
12962
|
-
const
|
|
12963
|
-
if (
|
|
12964
|
-
|
|
12966
|
+
const maxBytes = resolveMaxUploadBytes();
|
|
12967
|
+
if (file.size > maxBytes) {
|
|
12968
|
+
throw new PayloadTooLargeError(`Upload exceeds the ${maxBytes} byte limit.`);
|
|
12965
12969
|
}
|
|
12966
|
-
|
|
12967
|
-
|
|
12970
|
+
const mimeType = normalizeMimeType2(file.type.trim() || "application/octet-stream");
|
|
12971
|
+
if (!isAllowedMimeType(mimeType)) {
|
|
12972
|
+
throw new BadRequestError(`File type "${mimeType}" is not allowed.`);
|
|
12968
12973
|
}
|
|
12974
|
+
return {
|
|
12975
|
+
fileName: sanitizeUploadFileName(file.name),
|
|
12976
|
+
mimeType,
|
|
12977
|
+
size: file.size,
|
|
12978
|
+
contents: new Uint8Array(await file.arrayBuffer())
|
|
12979
|
+
};
|
|
12980
|
+
}
|
|
12981
|
+
async function parseMultipartUpload(request, fieldName = "file") {
|
|
12969
12982
|
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
12970
|
-
if (contentType.includes("
|
|
12971
|
-
|
|
12983
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
12984
|
+
throw new BadRequestError("Expected multipart form data.");
|
|
12972
12985
|
}
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
function normalizeFieldErrors(details) {
|
|
12978
|
-
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
12979
|
-
return {};
|
|
12986
|
+
const formData = await request.formData();
|
|
12987
|
+
const value = formData.get(fieldName);
|
|
12988
|
+
if (!(value instanceof File)) {
|
|
12989
|
+
throw new BadRequestError(`Missing upload field "${fieldName}".`);
|
|
12980
12990
|
}
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12991
|
+
return validateUploadFile(value, fieldName);
|
|
12992
|
+
}
|
|
12993
|
+
// ../../src/core/http/requireAuthMiddleware.ts
|
|
12994
|
+
function createRequireAuthMiddleware(auth) {
|
|
12995
|
+
return async (request, next) => {
|
|
12996
|
+
if (!await auth.check(request)) {
|
|
12997
|
+
const error = new UnauthorizedError;
|
|
12998
|
+
return errorResponse(error);
|
|
12989
12999
|
}
|
|
13000
|
+
return await next();
|
|
13001
|
+
};
|
|
13002
|
+
}
|
|
13003
|
+
// ../../src/core/http/resources.ts
|
|
13004
|
+
function serializeDate(value) {
|
|
13005
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
13006
|
+
}
|
|
13007
|
+
function whenLoaded(model, relation, transform) {
|
|
13008
|
+
const value = model.loaded(relation);
|
|
13009
|
+
if (value === undefined) {
|
|
13010
|
+
return;
|
|
12990
13011
|
}
|
|
12991
|
-
|
|
13012
|
+
if (value === null) {
|
|
13013
|
+
return null;
|
|
13014
|
+
}
|
|
13015
|
+
return transform ? transform(value) : value;
|
|
12992
13016
|
}
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
13017
|
+
|
|
13018
|
+
class JsonResource {
|
|
13019
|
+
resource;
|
|
13020
|
+
static wrap = "data";
|
|
13021
|
+
extra = {};
|
|
13022
|
+
constructor(resource) {
|
|
13023
|
+
this.resource = resource;
|
|
12996
13024
|
}
|
|
12997
|
-
|
|
12998
|
-
return
|
|
13025
|
+
static make(resource) {
|
|
13026
|
+
return new JsonResource(resource);
|
|
12999
13027
|
}
|
|
13000
|
-
|
|
13001
|
-
return
|
|
13028
|
+
static collection(items) {
|
|
13029
|
+
return new ResourceCollection(items);
|
|
13002
13030
|
}
|
|
13003
|
-
|
|
13004
|
-
}
|
|
13005
|
-
|
|
13006
|
-
if (status >= 500 && isProductionEnv()) {
|
|
13007
|
-
return "Something went wrong.";
|
|
13031
|
+
additional(data) {
|
|
13032
|
+
this.extra = { ...this.extra, ...data };
|
|
13033
|
+
return this;
|
|
13008
13034
|
}
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
function logServerError(error, mappedError) {
|
|
13012
|
-
if (mappedError.status < 500) {
|
|
13013
|
-
return;
|
|
13035
|
+
when(condition, value) {
|
|
13036
|
+
return condition ? value : undefined;
|
|
13014
13037
|
}
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
async function webErrorResponse(error, request) {
|
|
13022
|
-
if (currentRequestMeta().errorFormat === "json" || !request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
13023
|
-
return null;
|
|
13038
|
+
whenLoaded(relation, transform) {
|
|
13039
|
+
const model = this.resource;
|
|
13040
|
+
if (this.resource == null || typeof model.loaded !== "function") {
|
|
13041
|
+
return;
|
|
13042
|
+
}
|
|
13043
|
+
return whenLoaded(model, relation, transform);
|
|
13024
13044
|
}
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13045
|
+
toArray() {
|
|
13046
|
+
if (this.resource && typeof this.resource === "object" && "toArray" in this.resource) {
|
|
13047
|
+
return this.resource.toArray();
|
|
13048
|
+
}
|
|
13049
|
+
return { ...this.resource };
|
|
13050
|
+
}
|
|
13051
|
+
toResponse() {
|
|
13052
|
+
const wrap = this.constructor.wrap;
|
|
13053
|
+
const payload = this.toArray();
|
|
13054
|
+
if (wrap === null) {
|
|
13055
|
+
return { ...payload, ...this.extra };
|
|
13056
|
+
}
|
|
13057
|
+
return { [wrap]: payload, ...this.extra };
|
|
13029
13058
|
}
|
|
13030
|
-
const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
13031
|
-
return htmlErrorResponse({
|
|
13032
|
-
status: mappedError.status,
|
|
13033
|
-
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
13034
|
-
message: publicErrorMessage(mappedError.status, mappedError.message),
|
|
13035
|
-
errors,
|
|
13036
|
-
request
|
|
13037
|
-
});
|
|
13038
13059
|
}
|
|
13039
13060
|
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
}
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
}
|
|
13051
|
-
|
|
13052
|
-
return new Response(null, { status: 204 });
|
|
13061
|
+
class ResourceCollection extends JsonResource {
|
|
13062
|
+
toArray() {
|
|
13063
|
+
const items = this.resource.map((item) => item instanceof JsonResource ? item.toArray() : { ...item });
|
|
13064
|
+
const wrap = this.constructor.wrap;
|
|
13065
|
+
if (wrap === null) {
|
|
13066
|
+
return { data: items };
|
|
13067
|
+
}
|
|
13068
|
+
return { [wrap]: items };
|
|
13069
|
+
}
|
|
13070
|
+
toResponse() {
|
|
13071
|
+
return { ...this.toArray(), ...this.extra };
|
|
13072
|
+
}
|
|
13053
13073
|
}
|
|
13054
|
-
function
|
|
13055
|
-
|
|
13056
|
-
logServerError(error, mappedError);
|
|
13057
|
-
return Response.json({
|
|
13058
|
-
error: mappedError.message,
|
|
13059
|
-
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
13060
|
-
}, { status: mappedError.status, headers: { "Cache-Control": "no-store" } });
|
|
13074
|
+
function toResourceCollection(items, transformer) {
|
|
13075
|
+
return items.map(transformer);
|
|
13061
13076
|
}
|
|
13062
|
-
function
|
|
13063
|
-
return
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
} catch (error) {
|
|
13067
|
-
requestTransactionRollback();
|
|
13068
|
-
const request = args.find((arg) => arg instanceof Request);
|
|
13069
|
-
const webResponse = await webErrorResponse(error, request);
|
|
13070
|
-
if (webResponse) {
|
|
13071
|
-
return webResponse;
|
|
13072
|
-
}
|
|
13073
|
-
return errorResponse(error);
|
|
13074
|
-
}
|
|
13077
|
+
function toPaginatedResourceCollection(items, meta, transformer) {
|
|
13078
|
+
return {
|
|
13079
|
+
data: toResourceCollection(items, transformer),
|
|
13080
|
+
meta
|
|
13075
13081
|
};
|
|
13076
13082
|
}
|
|
13077
13083
|
// ../../src/core/http/route.ts
|
|
@@ -13691,7 +13697,7 @@ function createRequireAbilityMiddleware(abilityChecker) {
|
|
|
13691
13697
|
if (isViewsEnabled() && !requestPrefersJson(request)) {
|
|
13692
13698
|
throw error;
|
|
13693
13699
|
}
|
|
13694
|
-
return
|
|
13700
|
+
return errorResponse(new HttpError(error.status, error.message));
|
|
13695
13701
|
}
|
|
13696
13702
|
throw error;
|
|
13697
13703
|
}
|
|
@@ -13724,7 +13730,7 @@ function createRequireGlobalAdminMiddleware() {
|
|
|
13724
13730
|
path: new URL(_request.url).pathname
|
|
13725
13731
|
});
|
|
13726
13732
|
const error = new ForbiddenError("Platform admin access required.");
|
|
13727
|
-
return
|
|
13733
|
+
return errorResponse(error);
|
|
13728
13734
|
}
|
|
13729
13735
|
return await next();
|
|
13730
13736
|
};
|
|
@@ -16977,16 +16983,32 @@ class ViewRenderer {
|
|
|
16977
16983
|
this.engine = engine;
|
|
16978
16984
|
this.defaultLayout = defaultLayout;
|
|
16979
16985
|
}
|
|
16980
|
-
namespace(prefix) {
|
|
16986
|
+
namespace(prefix, defaults = {}) {
|
|
16981
16987
|
validateRelativeViewName(prefix);
|
|
16988
|
+
const defaultHeaders = new Headers(defaults.headers);
|
|
16989
|
+
const responseHeaders = (headers) => {
|
|
16990
|
+
const merged = new Headers(defaultHeaders);
|
|
16991
|
+
const overrides = new Headers(headers);
|
|
16992
|
+
for (const [name, value] of overrides) {
|
|
16993
|
+
if (name !== "set-cookie")
|
|
16994
|
+
merged.set(name, value);
|
|
16995
|
+
}
|
|
16996
|
+
if (overrides.has("set-cookie")) {
|
|
16997
|
+
merged.delete("set-cookie");
|
|
16998
|
+
for (const cookie of overrides.getSetCookie())
|
|
16999
|
+
merged.append("set-cookie", cookie);
|
|
17000
|
+
}
|
|
17001
|
+
return merged;
|
|
17002
|
+
};
|
|
16982
17003
|
const qualify = (name) => {
|
|
16983
17004
|
validateRelativeViewName(name);
|
|
16984
17005
|
return `${prefix}/${name}`;
|
|
16985
17006
|
};
|
|
16986
17007
|
return {
|
|
16987
|
-
render: async (name, data, options) => this.render(qualify(name), data, options),
|
|
17008
|
+
render: async (name, data, options = {}) => this.render(qualify(name), data, { ...options, headers: responseHeaders(options.headers) }),
|
|
16988
17009
|
renderHtmx: async (name, data, options = {}) => this.renderHtmx(qualify(name), data, {
|
|
16989
17010
|
...options,
|
|
17011
|
+
headers: responseHeaders(options.headers),
|
|
16990
17012
|
...options.fragment === undefined ? {} : { fragment: qualify(options.fragment) }
|
|
16991
17013
|
})
|
|
16992
17014
|
};
|
|
@@ -17374,7 +17396,7 @@ export {
|
|
|
17374
17396
|
isRlsTenancy,
|
|
17375
17397
|
isTenancyEnabled,
|
|
17376
17398
|
jobRegistry,
|
|
17377
|
-
|
|
17399
|
+
jsonResponse,
|
|
17378
17400
|
jwtTtlSeconds,
|
|
17379
17401
|
loadMigrationsFromDirectory,
|
|
17380
17402
|
loadSeedersFromDirectory,
|
|
@@ -17415,6 +17437,7 @@ export {
|
|
|
17415
17437
|
pinUrlToAddress,
|
|
17416
17438
|
pivotTableName,
|
|
17417
17439
|
policyGate,
|
|
17440
|
+
privateJsonResponse,
|
|
17418
17441
|
projectPluck,
|
|
17419
17442
|
prometheusRegistry,
|
|
17420
17443
|
qualifyColumn,
|