bosia 0.6.4 → 0.6.5
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/package.json +1 -1
- package/src/core/cookies.ts +34 -7
- package/src/core/hooks.ts +1 -1
- package/src/core/server.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
package/src/core/cookies.ts
CHANGED
|
@@ -3,7 +3,18 @@ import type { Cookies, CookieOptions } from "./hooks.ts";
|
|
|
3
3
|
// ─── Cookie Validation (RFC 6265) ────────────────────────
|
|
4
4
|
/** Rejects characters that could inject into Set-Cookie headers. */
|
|
5
5
|
const UNSAFE_COOKIE_VALUE = /[;\r\n]/;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Accept both casings (matches SvelteKit/Express convention) and write the
|
|
8
|
+
* canonical capitalized form into the Set-Cookie header.
|
|
9
|
+
*/
|
|
10
|
+
const SAMESITE_NORMALIZE: Record<string, "Strict" | "Lax" | "None"> = {
|
|
11
|
+
strict: "Strict",
|
|
12
|
+
lax: "Lax",
|
|
13
|
+
none: "None",
|
|
14
|
+
Strict: "Strict",
|
|
15
|
+
Lax: "Lax",
|
|
16
|
+
None: "None",
|
|
17
|
+
};
|
|
7
18
|
|
|
8
19
|
/**
|
|
9
20
|
* RFC 6265 §4.1.1: cookie-name is an HTTP token (RFC 2616 §2.2).
|
|
@@ -41,15 +52,20 @@ function parseCookies(header: string): Record<string, string> {
|
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export class CookieJar implements Cookies {
|
|
55
|
+
private static _warnedSecureOverHttp = false;
|
|
56
|
+
|
|
44
57
|
private _incoming: Record<string, string>;
|
|
45
58
|
private _outgoing: string[] = [];
|
|
46
59
|
private _defaults: CookieOptions;
|
|
47
60
|
private _accessed = false;
|
|
61
|
+
private _isHttps: boolean;
|
|
48
62
|
|
|
49
|
-
constructor(cookieHeader: string,
|
|
63
|
+
constructor(cookieHeader: string, isHttps = false) {
|
|
50
64
|
this._incoming = parseCookies(cookieHeader);
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
this._isHttps = isHttps;
|
|
66
|
+
// Browsers drop Secure cookies sent over HTTP — only default `secure` on
|
|
67
|
+
// when the current request actually arrived over HTTPS.
|
|
68
|
+
this._defaults = isHttps ? COOKIE_DEFAULTS : { ...COOKIE_DEFAULTS, secure: false };
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
get(name: string): string | undefined {
|
|
@@ -69,6 +85,17 @@ export class CookieJar implements Cookies {
|
|
|
69
85
|
set(name: string, value: string, options?: CookieOptions): void {
|
|
70
86
|
if (!VALID_COOKIE_NAME.test(name)) throw new Error(`Invalid cookie name: ${name}`);
|
|
71
87
|
const opts = { ...this._defaults, ...options };
|
|
88
|
+
if (!this._isHttps && opts.secure) {
|
|
89
|
+
opts.secure = false;
|
|
90
|
+
if (!CookieJar._warnedSecureOverHttp) {
|
|
91
|
+
console.warn(
|
|
92
|
+
"[bosia] cookies.set passed secure:true over HTTP — downgrading. " +
|
|
93
|
+
"Browsers drop Secure cookies on non-HTTPS. " +
|
|
94
|
+
"Remove the `secure` option; Bosia auto-applies it when the request is HTTPS.",
|
|
95
|
+
);
|
|
96
|
+
CookieJar._warnedSecureOverHttp = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
72
99
|
let header = `${name}=${encodeURIComponent(value)}`;
|
|
73
100
|
if (opts.path) {
|
|
74
101
|
if (UNSAFE_COOKIE_VALUE.test(opts.path))
|
|
@@ -85,9 +112,9 @@ export class CookieJar implements Cookies {
|
|
|
85
112
|
if (opts.httpOnly) header += "; HttpOnly";
|
|
86
113
|
if (opts.secure) header += "; Secure";
|
|
87
114
|
if (opts.sameSite) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
header += `; SameSite=${
|
|
115
|
+
const canonical = SAMESITE_NORMALIZE[opts.sameSite as string];
|
|
116
|
+
if (!canonical) throw new Error(`Invalid cookie sameSite: ${opts.sameSite}`);
|
|
117
|
+
header += `; SameSite=${canonical}`;
|
|
91
118
|
}
|
|
92
119
|
this._outgoing.push(header);
|
|
93
120
|
}
|
package/src/core/hooks.ts
CHANGED
package/src/core/server.ts
CHANGED
|
@@ -711,6 +711,11 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
711
711
|
// (preview/proxy hubs, design tools, etc.). Other security headers stay on.
|
|
712
712
|
const _xfoDisabled = process.env.DISABLE_X_FRAME_OPTIONS === "true";
|
|
713
713
|
|
|
714
|
+
// Trust `x-forwarded-proto` header behind a TLS-terminating proxy when computing
|
|
715
|
+
// per-request HTTPS-ness (drives `Secure` cookie flag). Off by default — the
|
|
716
|
+
// header is spoofable from any client that talks directly to the app.
|
|
717
|
+
const TRUST_PROXY = process.env.TRUST_PROXY === "true";
|
|
718
|
+
|
|
714
719
|
const SECURITY_HEADERS: Record<string, string> = {
|
|
715
720
|
"X-Content-Type-Options": "nosniff",
|
|
716
721
|
...(_xfoDisabled ? {} : { "X-Frame-Options": "SAMEORIGIN" }),
|
|
@@ -754,7 +759,10 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
|
|
|
754
759
|
return Response.json({ error: "Forbidden", message: csrfError }, { status: 403 });
|
|
755
760
|
}
|
|
756
761
|
|
|
757
|
-
const
|
|
762
|
+
const isHttps =
|
|
763
|
+
(TRUST_PROXY && request.headers.get("x-forwarded-proto") === "https") ||
|
|
764
|
+
url.protocol === "https:";
|
|
765
|
+
const cookieJar = new CookieJar(request.headers.get("cookie") ?? "", isHttps);
|
|
758
766
|
const nonce = CSP_ENABLED ? generateNonce() : "";
|
|
759
767
|
const event: RequestEvent = {
|
|
760
768
|
request,
|