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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.6.4",
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": [
@@ -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
- const VALID_SAMESITE = new Set(["Strict", "Lax", "None"]);
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, dev = false) {
63
+ constructor(cookieHeader: string, isHttps = false) {
50
64
  this._incoming = parseCookies(cookieHeader);
51
- // In dev mode, omit Secure — browsers reject Secure cookies over http://localhost
52
- this._defaults = dev ? { ...COOKIE_DEFAULTS, secure: false } : COOKIE_DEFAULTS;
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
- if (!VALID_SAMESITE.has(opts.sameSite))
89
- throw new Error(`Invalid cookie sameSite: ${opts.sameSite}`);
90
- header += `; SameSite=${opts.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
@@ -15,7 +15,7 @@ export interface CookieOptions {
15
15
  expires?: Date;
16
16
  httpOnly?: boolean;
17
17
  secure?: boolean;
18
- sameSite?: "Strict" | "Lax" | "None";
18
+ sameSite?: "Strict" | "Lax" | "None" | "strict" | "lax" | "none";
19
19
  }
20
20
 
21
21
  export interface Cookies {
@@ -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 cookieJar = new CookieJar(request.headers.get("cookie") ?? "", isDev);
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,