bosia 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -54
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +7 -9
  8. package/src/cli/feat.ts +266 -258
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -156
  15. package/src/core/client/appState.svelte.ts +33 -31
  16. package/src/core/client/enhance.ts +83 -78
  17. package/src/core/client/hydrate.ts +95 -81
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +196 -168
  25. package/src/core/env.ts +160 -148
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +184 -145
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -122
  34. package/src/core/renderer.ts +359 -286
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +538 -424
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/index.ts +8 -8
  41. package/src/lib/utils.ts +44 -30
  42. package/templates/default/.prettierignore +5 -0
  43. package/templates/default/.prettierrc.json +9 -0
  44. package/templates/default/README.md +5 -5
  45. package/templates/default/package.json +22 -18
  46. package/templates/default/src/app.css +80 -80
  47. package/templates/default/src/app.d.ts +3 -3
  48. package/templates/default/src/routes/+error.svelte +7 -10
  49. package/templates/default/src/routes/+layout.svelte +2 -2
  50. package/templates/default/src/routes/+page.svelte +30 -32
  51. package/templates/default/src/routes/about/+page.svelte +3 -3
  52. package/templates/default/tsconfig.json +20 -20
  53. package/templates/demo/.prettierignore +5 -0
  54. package/templates/demo/.prettierrc.json +9 -0
  55. package/templates/demo/README.md +9 -9
  56. package/templates/demo/package.json +22 -17
  57. package/templates/demo/src/app.css +80 -80
  58. package/templates/demo/src/app.d.ts +3 -3
  59. package/templates/demo/src/hooks.server.ts +9 -9
  60. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  61. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  62. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  63. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  64. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  65. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  67. package/templates/demo/src/routes/+error.svelte +10 -7
  68. package/templates/demo/src/routes/+layout.server.ts +4 -4
  69. package/templates/demo/src/routes/+layout.svelte +2 -2
  70. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  71. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  72. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  73. package/templates/demo/tsconfig.json +20 -20
  74. package/templates/todo/.prettierignore +5 -0
  75. package/templates/todo/.prettierrc.json +9 -0
  76. package/templates/todo/README.md +9 -9
  77. package/templates/todo/package.json +22 -17
  78. package/templates/todo/src/app.css +80 -80
  79. package/templates/todo/src/app.d.ts +7 -7
  80. package/templates/todo/src/hooks.server.ts +9 -9
  81. package/templates/todo/src/routes/+error.svelte +10 -7
  82. package/templates/todo/src/routes/+layout.server.ts +4 -4
  83. package/templates/todo/src/routes/+layout.svelte +2 -2
  84. package/templates/todo/src/routes/+page.svelte +44 -44
  85. package/templates/todo/template.json +1 -1
  86. package/templates/todo/tsconfig.json +20 -20
@@ -2,56 +2,69 @@
2
2
  // Svelte 5 rune-based reactive router.
3
3
  // Singleton used by App.svelte and hydrate.ts.
4
4
 
5
- import { findMatch } from "../matcher.ts";
5
+ import { findMatch, canonicalPathname } from "../matcher.ts";
6
6
  import { clientRoutes } from "bosia:routes";
7
7
 
8
- export const router = new class Router {
9
- currentRoute = $state(typeof window !== "undefined" ? window.location.pathname + window.location.search + window.location.hash : "/");
10
- params = $state<Record<string, string>>({});
11
- /** True when navigation was triggered by a link click / navigate() call, false on popstate (back/forward). */
12
- isPush = $state(true);
13
-
14
- navigate(path: string) {
15
- if (this.currentRoute === path) return;
16
- // Unknown route — let the server handle it (renders +error.svelte with 404)
17
- const pathname = path.split("?")[0].split("#")[0];
18
- if (!findMatch(clientRoutes, pathname)) {
19
- window.location.href = path;
20
- return;
21
- }
22
- this.isPush = true;
23
- this.currentRoute = path;
24
- if (typeof history !== "undefined") {
25
- history.pushState({}, "", path);
26
- }
27
- }
28
-
29
- init() {
30
- if (typeof window === "undefined") return;
31
-
32
- // Intercept <a> clicks for client-side navigation
33
- window.addEventListener("click", (e) => {
34
- // Let browser handle non-primary buttons, modifier-clicks, already-handled events
35
- if (e.button !== 0) return;
36
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
37
- if (e.defaultPrevented) return;
38
-
39
- const anchor = (e.target as HTMLElement).closest("a");
40
- if (!anchor) return;
41
- if (anchor.origin !== window.location.origin) return;
42
- if (anchor.target) return;
43
- if (anchor.hasAttribute("download")) return;
44
- if (anchor.rel.split(/\s+/).includes("external")) return;
45
- if (anchor.protocol !== "https:" && anchor.protocol !== "http:") return;
46
-
47
- e.preventDefault();
48
- this.navigate(anchor.pathname + anchor.search + anchor.hash);
49
- });
50
-
51
- // Browser back/forward
52
- window.addEventListener("popstate", () => {
53
- this.isPush = false;
54
- this.currentRoute = window.location.pathname + window.location.search + window.location.hash;
55
- });
56
- }
57
- }();
8
+ export const router = new (class Router {
9
+ currentRoute = $state(
10
+ typeof window !== "undefined"
11
+ ? window.location.pathname + window.location.search + window.location.hash
12
+ : "/",
13
+ );
14
+ params = $state<Record<string, string>>({});
15
+ /** True when navigation was triggered by a link click / navigate() call, false on popstate (back/forward). */
16
+ isPush = $state(true);
17
+
18
+ navigate(path: string) {
19
+ if (this.currentRoute === path) return;
20
+ // Unknown route — let the server handle it (renders +error.svelte with 404)
21
+ const queryHash = path.slice(path.split("?")[0].split("#")[0].length);
22
+ const pathname = path.split("?")[0].split("#")[0];
23
+ const match = findMatch(clientRoutes, pathname);
24
+ if (!match) {
25
+ window.location.href = path;
26
+ return;
27
+ }
28
+ // Canonicalize trailing slash before navigating (matches server 308 behavior)
29
+ const canonical = canonicalPathname(
30
+ pathname,
31
+ (match.route as any).trailingSlash ?? "never",
32
+ );
33
+ const finalPath = canonical !== null ? canonical + queryHash : path;
34
+ this.isPush = true;
35
+ this.currentRoute = finalPath;
36
+ if (typeof history !== "undefined") {
37
+ history.pushState({}, "", finalPath);
38
+ }
39
+ }
40
+
41
+ init() {
42
+ if (typeof window === "undefined") return;
43
+
44
+ // Intercept <a> clicks for client-side navigation
45
+ window.addEventListener("click", (e) => {
46
+ // Let browser handle non-primary buttons, modifier-clicks, already-handled events
47
+ if (e.button !== 0) return;
48
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
49
+ if (e.defaultPrevented) return;
50
+
51
+ const anchor = (e.target as HTMLElement).closest("a");
52
+ if (!anchor) return;
53
+ if (anchor.origin !== window.location.origin) return;
54
+ if (anchor.target) return;
55
+ if (anchor.hasAttribute("download")) return;
56
+ if (anchor.rel.split(/\s+/).includes("external")) return;
57
+ if (anchor.protocol !== "https:" && anchor.protocol !== "http:") return;
58
+
59
+ e.preventDefault();
60
+ this.navigate(anchor.pathname + anchor.search + anchor.hash);
61
+ });
62
+
63
+ // Browser back/forward
64
+ window.addEventListener("popstate", () => {
65
+ this.isPush = false;
66
+ this.currentRoute =
67
+ window.location.pathname + window.location.search + window.location.hash;
68
+ });
69
+ }
70
+ })();
@@ -14,85 +14,89 @@ const VALID_COOKIE_NAME = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
14
14
  // ─── Cookie Defaults ─────────────────────────────────────
15
15
  /** Secure defaults matching SvelteKit conventions. */
16
16
  const COOKIE_DEFAULTS: CookieOptions = {
17
- path: "/",
18
- httpOnly: true,
19
- secure: true,
20
- sameSite: "Lax",
17
+ path: "/",
18
+ httpOnly: true,
19
+ secure: true,
20
+ sameSite: "Lax",
21
21
  };
22
22
 
23
23
  // ─── Cookie Helpers ──────────────────────────────────────
24
24
 
25
25
  function parseCookies(header: string): Record<string, string> {
26
- const result: Record<string, string> = {};
27
- for (const pair of header.split(";")) {
28
- const idx = pair.indexOf("=");
29
- if (idx === -1) continue;
30
- const name = pair.slice(0, idx).trim();
31
- const value = pair.slice(idx + 1).trim();
32
- if (name) {
33
- try { result[name] = decodeURIComponent(value); }
34
- catch { result[name] = value; }
35
- }
36
- }
37
- return result;
26
+ const result: Record<string, string> = {};
27
+ for (const pair of header.split(";")) {
28
+ const idx = pair.indexOf("=");
29
+ if (idx === -1) continue;
30
+ const name = pair.slice(0, idx).trim();
31
+ const value = pair.slice(idx + 1).trim();
32
+ if (name) {
33
+ try {
34
+ result[name] = decodeURIComponent(value);
35
+ } catch {
36
+ result[name] = value;
37
+ }
38
+ }
39
+ }
40
+ return result;
38
41
  }
39
42
 
40
43
  export class CookieJar implements Cookies {
41
- private _incoming: Record<string, string>;
42
- private _outgoing: string[] = [];
43
- private _defaults: CookieOptions;
44
- private _accessed = false;
44
+ private _incoming: Record<string, string>;
45
+ private _outgoing: string[] = [];
46
+ private _defaults: CookieOptions;
47
+ private _accessed = false;
45
48
 
46
- constructor(cookieHeader: string, dev = false) {
47
- this._incoming = parseCookies(cookieHeader);
48
- // In dev mode, omit Secure — browsers reject Secure cookies over http://localhost
49
- this._defaults = dev
50
- ? { ...COOKIE_DEFAULTS, secure: false }
51
- : COOKIE_DEFAULTS;
52
- }
49
+ constructor(cookieHeader: string, dev = false) {
50
+ 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;
53
+ }
53
54
 
54
- get(name: string): string | undefined {
55
- this._accessed = true;
56
- return this._incoming[name];
57
- }
55
+ get(name: string): string | undefined {
56
+ this._accessed = true;
57
+ return this._incoming[name];
58
+ }
58
59
 
59
- getAll(): Record<string, string> {
60
- this._accessed = true;
61
- return { ...this._incoming };
62
- }
60
+ getAll(): Record<string, string> {
61
+ this._accessed = true;
62
+ return { ...this._incoming };
63
+ }
63
64
 
64
- get accessed(): boolean {
65
- return this._accessed;
66
- }
65
+ get accessed(): boolean {
66
+ return this._accessed;
67
+ }
67
68
 
68
- set(name: string, value: string, options?: CookieOptions): void {
69
- if (!VALID_COOKIE_NAME.test(name)) throw new Error(`Invalid cookie name: ${name}`);
70
- const opts = { ...this._defaults, ...options };
71
- let header = `${name}=${encodeURIComponent(value)}`;
72
- if (opts.path) {
73
- if (UNSAFE_COOKIE_VALUE.test(opts.path)) throw new Error(`Invalid cookie path: ${opts.path}`);
74
- header += `; Path=${opts.path}`;
75
- }
76
- if (opts.domain) {
77
- if (UNSAFE_COOKIE_VALUE.test(opts.domain)) throw new Error(`Invalid cookie domain: ${opts.domain}`);
78
- header += `; Domain=${opts.domain}`;
79
- }
80
- if (opts.maxAge != null) header += `; Max-Age=${opts.maxAge}`;
81
- if (opts.expires) header += `; Expires=${opts.expires.toUTCString()}`;
82
- if (opts.httpOnly) header += "; HttpOnly";
83
- if (opts.secure) header += "; Secure";
84
- if (opts.sameSite) {
85
- if (!VALID_SAMESITE.has(opts.sameSite)) throw new Error(`Invalid cookie sameSite: ${opts.sameSite}`);
86
- header += `; SameSite=${opts.sameSite}`;
87
- }
88
- this._outgoing.push(header);
89
- }
69
+ set(name: string, value: string, options?: CookieOptions): void {
70
+ if (!VALID_COOKIE_NAME.test(name)) throw new Error(`Invalid cookie name: ${name}`);
71
+ const opts = { ...this._defaults, ...options };
72
+ let header = `${name}=${encodeURIComponent(value)}`;
73
+ if (opts.path) {
74
+ if (UNSAFE_COOKIE_VALUE.test(opts.path))
75
+ throw new Error(`Invalid cookie path: ${opts.path}`);
76
+ header += `; Path=${opts.path}`;
77
+ }
78
+ if (opts.domain) {
79
+ if (UNSAFE_COOKIE_VALUE.test(opts.domain))
80
+ throw new Error(`Invalid cookie domain: ${opts.domain}`);
81
+ header += `; Domain=${opts.domain}`;
82
+ }
83
+ if (opts.maxAge != null) header += `; Max-Age=${opts.maxAge}`;
84
+ if (opts.expires) header += `; Expires=${opts.expires.toUTCString()}`;
85
+ if (opts.httpOnly) header += "; HttpOnly";
86
+ if (opts.secure) header += "; Secure";
87
+ if (opts.sameSite) {
88
+ if (!VALID_SAMESITE.has(opts.sameSite))
89
+ throw new Error(`Invalid cookie sameSite: ${opts.sameSite}`);
90
+ header += `; SameSite=${opts.sameSite}`;
91
+ }
92
+ this._outgoing.push(header);
93
+ }
90
94
 
91
- delete(name: string, options?: Pick<CookieOptions, "path" | "domain">): void {
92
- this.set(name, "", { ...options, maxAge: 0 });
93
- }
95
+ delete(name: string, options?: Pick<CookieOptions, "path" | "domain">): void {
96
+ this.set(name, "", { ...options, maxAge: 0 });
97
+ }
94
98
 
95
- get outgoing(): readonly string[] {
96
- return this._outgoing;
97
- }
99
+ get outgoing(): readonly string[] {
100
+ return this._outgoing;
101
+ }
98
102
  }
package/src/core/cors.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  export interface CorsConfig {
2
- /** Origins allowed to make cross-origin requests (e.g. ["https://app.example.com"]) */
3
- allowedOrigins: string[];
4
- /** HTTP methods to allow. Default: GET, HEAD, PUT, PATCH, POST, DELETE */
5
- allowedMethods?: string[];
6
- /** Request headers to allow. Default: Content-Type, Authorization */
7
- allowedHeaders?: string[];
8
- /** Response headers to expose to the browser. Default: none */
9
- exposedHeaders?: string[];
10
- /** Whether to allow cookies/auth credentials. Default: false */
11
- credentials?: boolean;
12
- /** Preflight cache duration in seconds. Default: 86400 (24h) */
13
- maxAge?: number;
2
+ /** Origins allowed to make cross-origin requests (e.g. ["https://app.example.com"]) */
3
+ allowedOrigins: string[];
4
+ /** HTTP methods to allow. Default: GET, HEAD, PUT, PATCH, POST, DELETE */
5
+ allowedMethods?: string[];
6
+ /** Request headers to allow. Default: Content-Type, Authorization */
7
+ allowedHeaders?: string[];
8
+ /** Response headers to expose to the browser. Default: none */
9
+ exposedHeaders?: string[];
10
+ /** Whether to allow cookies/auth credentials. Default: false */
11
+ credentials?: boolean;
12
+ /** Preflight cache duration in seconds. Default: 86400 (24h) */
13
+ maxAge?: number;
14
14
  }
15
15
 
16
16
  const DEFAULT_METHODS = "GET, HEAD, PUT, PATCH, POST, DELETE";
@@ -20,27 +20,30 @@ const DEFAULT_HEADERS = "Content-Type, Authorization";
20
20
  * Returns CORS response headers if the request Origin is in the allowed list.
21
21
  * Returns null if Origin is absent or not allowed.
22
22
  */
23
- export function getCorsHeaders(request: Request, config: CorsConfig): Record<string, string> | null {
24
- const origin = request.headers.get("origin");
25
- if (!origin) return null;
23
+ export function getCorsHeaders(
24
+ request: Request,
25
+ config: CorsConfig,
26
+ ): Record<string, string> | null {
27
+ const origin = request.headers.get("origin");
28
+ if (!origin) return null;
26
29
 
27
- const allowed = config.allowedOrigins.includes(origin);
28
- if (!allowed) return null;
30
+ const allowed = config.allowedOrigins.includes(origin);
31
+ if (!allowed) return null;
29
32
 
30
- const headers: Record<string, string> = {
31
- "Access-Control-Allow-Origin": origin,
32
- "Vary": "Origin",
33
- };
33
+ const headers: Record<string, string> = {
34
+ "Access-Control-Allow-Origin": origin,
35
+ Vary: "Origin",
36
+ };
34
37
 
35
- if (config.credentials) {
36
- headers["Access-Control-Allow-Credentials"] = "true";
37
- }
38
+ if (config.credentials) {
39
+ headers["Access-Control-Allow-Credentials"] = "true";
40
+ }
38
41
 
39
- if (config.exposedHeaders?.length) {
40
- headers["Access-Control-Expose-Headers"] = config.exposedHeaders.join(", ");
41
- }
42
+ if (config.exposedHeaders?.length) {
43
+ headers["Access-Control-Expose-Headers"] = config.exposedHeaders.join(", ");
44
+ }
42
45
 
43
- return headers;
46
+ return headers;
44
47
  }
45
48
 
46
49
  /**
@@ -48,13 +51,19 @@ export function getCorsHeaders(request: Request, config: CorsConfig): Record<str
48
51
  * Returns a 204 response with CORS headers, or null if the origin is not allowed.
49
52
  */
50
53
  export function handlePreflight(request: Request, config: CorsConfig): Response | null {
51
- const base = getCorsHeaders(request, config);
52
- if (!base) return null;
54
+ const base = getCorsHeaders(request, config);
55
+ if (!base) return null;
53
56
 
54
- const headers = new Headers(base as HeadersInit);
55
- headers.set("Access-Control-Allow-Methods", config.allowedMethods?.join(", ") ?? DEFAULT_METHODS);
56
- headers.set("Access-Control-Allow-Headers", config.allowedHeaders?.join(", ") ?? DEFAULT_HEADERS);
57
- headers.set("Access-Control-Max-Age", String(config.maxAge ?? 86400));
57
+ const headers = new Headers(base as HeadersInit);
58
+ headers.set(
59
+ "Access-Control-Allow-Methods",
60
+ config.allowedMethods?.join(", ") ?? DEFAULT_METHODS,
61
+ );
62
+ headers.set(
63
+ "Access-Control-Allow-Headers",
64
+ config.allowedHeaders?.join(", ") ?? DEFAULT_HEADERS,
65
+ );
66
+ headers.set("Access-Control-Max-Age", String(config.maxAge ?? 86400));
58
67
 
59
- return new Response(null, { status: 204, headers });
68
+ return new Response(null, { status: 204, headers });
60
69
  }
package/src/core/csrf.ts CHANGED
@@ -6,14 +6,14 @@
6
6
  // is treated as a cross-origin attack.
7
7
 
8
8
  export interface CsrfConfig {
9
- /** Whether to enforce origin checks. Default: true. */
10
- checkOrigin: boolean;
11
- /** Additional origins to allow (e.g. CDN or mobile app origin). */
12
- allowedOrigins?: string[];
9
+ /** Whether to enforce origin checks. Default: true. */
10
+ checkOrigin: boolean;
11
+ /** Additional origins to allow (e.g. CDN or mobile app origin). */
12
+ allowedOrigins?: string[];
13
13
  }
14
14
 
15
15
  const DEFAULT_CSRF_CONFIG: CsrfConfig = {
16
- checkOrigin: true,
16
+ checkOrigin: true,
17
17
  };
18
18
 
19
19
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
@@ -23,43 +23,43 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
23
23
  * Returns `null` on success, or an error message string to reject with 403.
24
24
  */
25
25
  export function checkCsrf(
26
- request: Request,
27
- url: URL,
28
- config: CsrfConfig = DEFAULT_CSRF_CONFIG,
26
+ request: Request,
27
+ url: URL,
28
+ config: CsrfConfig = DEFAULT_CSRF_CONFIG,
29
29
  ): string | null {
30
- if (!config.checkOrigin) return null;
31
- if (SAFE_METHODS.has(request.method.toUpperCase())) return null;
30
+ if (!config.checkOrigin) return null;
31
+ if (SAFE_METHODS.has(request.method.toUpperCase())) return null;
32
32
 
33
- // Derive the expected origin.
34
- // In dev, the browser hits the proxy on DEV_PORT (e.g. localhost:9000)
35
- // while the Elysia server sees url.origin as localhost:9001.
36
- // X-Forwarded-Host / Host headers reflect the actual host the client used.
37
- const forwardedHost = request.headers.get("x-forwarded-host");
38
- const host = forwardedHost ?? request.headers.get("host");
39
- const protocol = request.headers.get("x-forwarded-proto") ?? url.protocol.replace(":", "");
40
- const expectedOrigin = host ? `${protocol}://${host}` : url.origin;
33
+ // Derive the expected origin.
34
+ // In dev, the browser hits the proxy on DEV_PORT (e.g. localhost:9000)
35
+ // while the Elysia server sees url.origin as localhost:9001.
36
+ // X-Forwarded-Host / Host headers reflect the actual host the client used.
37
+ const forwardedHost = request.headers.get("x-forwarded-host");
38
+ const host = forwardedHost ?? request.headers.get("host");
39
+ const protocol = request.headers.get("x-forwarded-proto") ?? url.protocol.replace(":", "");
40
+ const expectedOrigin = host ? `${protocol}://${host}` : url.origin;
41
41
 
42
- const allowedOrigins = new Set([expectedOrigin, ...(config.allowedOrigins ?? [])]);
42
+ const allowedOrigins = new Set([expectedOrigin, ...(config.allowedOrigins ?? [])]);
43
43
 
44
- // Check Origin header first (sent by all modern browsers on cross-origin requests)
45
- const originHeader = request.headers.get("origin");
46
- if (originHeader) {
47
- if (allowedOrigins.has(originHeader)) return null;
48
- return `Cross-origin request blocked: Origin "${originHeader}" is not allowed`;
49
- }
44
+ // Check Origin header first (sent by all modern browsers on cross-origin requests)
45
+ const originHeader = request.headers.get("origin");
46
+ if (originHeader) {
47
+ if (allowedOrigins.has(originHeader)) return null;
48
+ return `Cross-origin request blocked: Origin "${originHeader}" is not allowed`;
49
+ }
50
50
 
51
- // Fall back to Referer (older browsers, some same-origin form posts)
52
- const refererHeader = request.headers.get("referer");
53
- if (refererHeader) {
54
- try {
55
- const refererOrigin = new URL(refererHeader).origin;
56
- if (allowedOrigins.has(refererOrigin)) return null;
57
- return `Cross-origin request blocked: Referer "${refererHeader}" is not allowed`;
58
- } catch {
59
- return `Cross-origin request blocked: Referer header is malformed`;
60
- }
61
- }
51
+ // Fall back to Referer (older browsers, some same-origin form posts)
52
+ const refererHeader = request.headers.get("referer");
53
+ if (refererHeader) {
54
+ try {
55
+ const refererOrigin = new URL(refererHeader).origin;
56
+ if (allowedOrigins.has(refererOrigin)) return null;
57
+ return `Cross-origin request blocked: Referer "${refererHeader}" is not allowed`;
58
+ } catch {
59
+ return `Cross-origin request blocked: Referer header is malformed`;
60
+ }
61
+ }
62
62
 
63
- // Neither Origin nor Referer present — reject
64
- return "Forbidden: missing Origin or Referer header on non-safe request";
63
+ // Neither Origin nor Referer present — reject
64
+ return "Forbidden: missing Origin or Referer header on non-safe request";
65
65
  }
package/src/core/dedup.ts CHANGED
@@ -7,26 +7,26 @@ const AUTH_COOKIE_RE = /(?:^|;\s*)authorization=([^;]*)/i;
7
7
 
8
8
  /** Build dedup key from route URL + request identity. Sort search params for consistency. */
9
9
  export function dedupKey(url: URL, request: Request): string {
10
- let path = url.pathname;
11
- if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
12
- const sorted = new URLSearchParams([...url.searchParams.entries()].sort());
13
- const search = sorted.toString();
14
- const base = search ? `${path}?${search}` : path;
10
+ let path = url.pathname;
11
+ if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
12
+ const sorted = new URLSearchParams([...url.searchParams.entries()].sort());
13
+ const search = sorted.toString();
14
+ const base = search ? `${path}?${search}` : path;
15
15
 
16
- const authHeader = request.headers.get("authorization") ?? "";
17
- const cookieHeader = request.headers.get("cookie") ?? "";
18
- const match = cookieHeader.match(AUTH_COOKIE_RE);
19
- const authCookie = match?.[1] ?? "";
20
- const identity = authHeader || authCookie;
21
- if (!identity) return base;
22
- return `${base}|${Bun.hash(identity).toString(36)}`;
16
+ const authHeader = request.headers.get("authorization") ?? "";
17
+ const cookieHeader = request.headers.get("cookie") ?? "";
18
+ const match = cookieHeader.match(AUTH_COOKIE_RE);
19
+ const authCookie = match?.[1] ?? "";
20
+ const identity = authHeader || authCookie;
21
+ if (!identity) return base;
22
+ return `${base}|${Bun.hash(identity).toString(36)}`;
23
23
  }
24
24
 
25
25
  /** Run `fn` with dedup. Concurrent calls with same key share the in-flight promise. */
26
26
  export function dedup<T>(key: string, fn: () => Promise<T>): Promise<T> {
27
- const existing = inflight.get(key);
28
- if (existing) return existing;
29
- const promise = fn().finally(() => inflight.delete(key));
30
- inflight.set(key, promise);
31
- return promise;
27
+ const existing = inflight.get(key);
28
+ if (existing) return existing;
29
+ const promise = fn().finally(() => inflight.delete(key));
30
+ inflight.set(key, promise);
31
+ return promise;
32
32
  }