bosia 1.1.6 → 1.2.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/ambient.d.ts +3 -0
  5. package/src/cli/sync.ts +14 -1
  6. package/src/core/appHtml.ts +52 -0
  7. package/src/core/build.ts +4 -3
  8. package/src/core/client/App.svelte +82 -12
  9. package/src/core/client/TitleProbe.svelte +15 -0
  10. package/src/core/client/router.svelte.ts +1 -2
  11. package/src/core/client/titleMemory.svelte.ts +64 -0
  12. package/src/core/headScan.ts +132 -0
  13. package/src/core/html.ts +103 -45
  14. package/src/core/renderer.ts +190 -91
  15. package/src/core/routeFile.ts +15 -2
  16. package/src/core/scanner.ts +28 -31
  17. package/src/core/server.ts +95 -80
  18. package/src/lib/utils.ts +1 -8
  19. package/templates/default/package.json +2 -2
  20. package/templates/demo/package.json +2 -2
  21. package/templates/shop/package.json +2 -2
  22. package/templates/shop/src/routes/(private)/dashboard/files/+page.svelte +9 -0
  23. package/templates/shop/src/routes/(private)/dashboard/orders/+page.svelte +9 -0
  24. package/templates/shop/src/routes/(private)/dashboard/orders/pending/+page.svelte +9 -0
  25. package/templates/shop/src/routes/(private)/dashboard/orders/refunds/+page.svelte +9 -0
  26. package/templates/shop/src/routes/(private)/dashboard/products/+page.svelte +9 -0
  27. package/templates/shop/src/routes/(private)/dashboard/profile/+page.svelte +9 -0
  28. package/templates/shop/src/routes/(private)/dashboard/settings/+page.svelte +9 -0
  29. package/templates/shop/src/routes/(private)/dashboard/users/+page.svelte +9 -0
  30. package/templates/shop/src/routes/(public)/+page.svelte +2 -2
  31. package/templates/store/package.json +2 -2
  32. package/templates/store/src/routes/(private)/dashboard/files/+page.svelte +9 -0
  33. package/templates/store/src/routes/(private)/dashboard/orders/+page.svelte +9 -0
  34. package/templates/store/src/routes/(private)/dashboard/orders/pending/+page.svelte +9 -0
  35. package/templates/store/src/routes/(private)/dashboard/orders/refunds/+page.svelte +9 -0
  36. package/templates/store/src/routes/(private)/dashboard/products/+page.svelte +9 -0
  37. package/templates/store/src/routes/(private)/dashboard/profile/+page.svelte +9 -0
  38. package/templates/store/src/routes/(private)/dashboard/settings/+page.svelte +9 -0
  39. package/templates/store/src/routes/(private)/dashboard/users/+page.svelte +9 -0
  40. package/templates/store/src/routes/(public)/+page.svelte +2 -2
@@ -404,33 +404,39 @@ async function resolve(event: RequestEvent): Promise<Response> {
404
404
  )
405
405
  : undefined;
406
406
  const runLoad = async () => {
407
- const data = await loadRouteData(routeUrl, locals, request, cookies, null, pageMatch, mask);
408
-
409
- let metadata = null;
410
- if (pageMatch) {
411
- try {
412
- const meta = await loadMetadata(
407
+ // metadata() runs alongside the loaders; the page load() gets its
408
+ // `data`, as on SSR. A failure stays non-fatal here.
409
+ const metaP = pageMatch
410
+ ? loadMetadata(
413
411
  pageMatch.route,
414
412
  pageMatch.params,
415
413
  routeUrl,
416
414
  locals,
417
415
  cookies,
418
416
  request,
419
- );
420
- // Explicit whitelist, not a spread: `metadata.data` feeds load() on the
421
- // server and may hold secrets — it must not reach the client.
422
- if (meta)
423
- metadata = {
424
- title: meta.title,
425
- description: meta.description,
426
- meta: meta.meta,
427
- link: meta.link,
428
- lang: meta.lang,
429
- };
430
- } catch {
431
- /* non-fatal */
432
- }
433
- }
417
+ ).catch(() => null)
418
+ : Promise.resolve(null);
419
+ const data = await loadRouteData(
420
+ routeUrl,
421
+ locals,
422
+ request,
423
+ cookies,
424
+ metaP.then((m) => m?.data ?? null),
425
+ pageMatch,
426
+ mask,
427
+ );
428
+ const meta = await metaP;
429
+ // Explicit whitelist, not a spread: `metadata.data` feeds load() on the
430
+ // server and may hold secrets — it must not reach the client.
431
+ const metadata = meta
432
+ ? {
433
+ title: meta.title,
434
+ description: meta.description,
435
+ meta: meta.meta,
436
+ link: meta.link,
437
+ lang: meta.lang,
438
+ }
439
+ : null;
434
440
 
435
441
  return { data, metadata, cookiesAccessed: (cookies as CookieJar).accessed };
436
442
  };
@@ -1041,6 +1047,69 @@ if (_xfoDisabled) {
1041
1047
  console.log("🪟 X-Frame-Options disabled (DISABLE_X_FRAME_OPTIONS=true)");
1042
1048
  }
1043
1049
 
1050
+ // Security headers, CSP, CORS and the request's Set-Cookie headers, applied to
1051
+ // every response handleRequest hands back — resolved ones and the ones a hook
1052
+ // threw (redirect / error()).
1053
+ function finalizeResponse(
1054
+ response: Response,
1055
+ request: Request,
1056
+ nonce: string,
1057
+ cookieJar: CookieJar,
1058
+ ): Response {
1059
+ // Set on the response itself when its headers are mutable, which every
1060
+ // Response the framework builds has. Copying them and rebuilding the
1061
+ // Response cost a Headers copy plus a body re-wrap on every request.
1062
+ // Responses with immutable headers (`fetch()`, `Response.redirect()`)
1063
+ // still get the copy.
1064
+ // On Workers (compressionOn is false only there) an already-encoded body
1065
+ // is rebuilt as before, since the rebuild is what marks it PRECOMPRESSED.
1066
+ let mutable: Headers | null = null;
1067
+ if (compressionOn || !response.headers.has("content-encoding")) {
1068
+ try {
1069
+ response.headers.set("X-Content-Type-Options", SECURITY_HEADERS["X-Content-Type-Options"]!);
1070
+ mutable = response.headers;
1071
+ } catch {
1072
+ // immutable — copied below
1073
+ }
1074
+ }
1075
+ const inPlace = mutable !== null;
1076
+ const headers = mutable ?? new Headers(response.headers);
1077
+ // A handle can mark a response (e.g. a proxied embeddable preview) to opt
1078
+ // out of the frame guard. Strip the internal marker so it never ships, and
1079
+ // skip only X-Frame-Options for that response — other security headers stay.
1080
+ const skipFrameGuard = headers.has(NO_FRAME_GUARD_HEADER);
1081
+ headers.delete(NO_FRAME_GUARD_HEADER);
1082
+ for (const [k, v] of Object.entries(SECURITY_HEADERS)) {
1083
+ if (skipFrameGuard && k === "X-Frame-Options") continue;
1084
+ headers.set(k, v);
1085
+ }
1086
+ const cspHeader = buildCspHeader(nonce);
1087
+ if (cspHeader) headers.set("Content-Security-Policy", cspHeader);
1088
+ // Apply CORS headers for allowed origins. `Vary: Origin` is set whenever
1089
+ // CORS is configured — even on responses to non-allowed origins — so
1090
+ // downstream caches (CDNs, browser HTTP cache) key on the Origin header
1091
+ // instead of serving an Access-Control-Allow-Origin response across origins.
1092
+ if (CORS_CONFIG) {
1093
+ applyCorsVary(headers);
1094
+ const corsHeaders = getCorsHeaders(request, CORS_CONFIG);
1095
+ if (corsHeaders) {
1096
+ for (const [k, v] of Object.entries(corsHeaders)) headers.set(k, v);
1097
+ }
1098
+ }
1099
+ // Apply any Set-Cookie headers accumulated during the request
1100
+ for (const cookie of cookieJar.outgoing) headers.append("Set-Cookie", cookie);
1101
+ if (inPlace) return response;
1102
+ return new Response(response.body, {
1103
+ // A Content-Encoding here means the body is already compressed (cache
1104
+ // hit, compress()); rebuilding drops that flag and Workers would
1105
+ // compress it again.
1106
+ ...(headers.has("content-encoding") ? PRECOMPRESSED : {}),
1107
+ status: response.status,
1108
+ statusText: response.statusText,
1109
+ headers,
1110
+ });
1111
+ }
1112
+
1044
1113
  async function handleRequest(request: Request, url: URL): Promise<Response> {
1045
1114
  // Behind a trusted proxy the inbound `Host`/scheme is the proxy's inner hop
1046
1115
  // (e.g. `localhost:PORT` over plain HTTP), so `url` built from `request.url`
@@ -1188,58 +1257,7 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
1188
1257
  }
1189
1258
  }
1190
1259
 
1191
- // Set on the response itself when its headers are mutable, which every
1192
- // Response the framework builds has. Copying them and rebuilding the
1193
- // Response cost a Headers copy plus a body re-wrap on every request.
1194
- // Responses with immutable headers (`fetch()`, `Response.redirect()`)
1195
- // still get the copy.
1196
- // On Workers (compressionOn is false only there) an already-encoded body
1197
- // is rebuilt as before, since the rebuild is what marks it PRECOMPRESSED.
1198
- let mutable: Headers | null = null;
1199
- if (compressionOn || !response.headers.has("content-encoding")) {
1200
- try {
1201
- response.headers.set("X-Content-Type-Options", SECURITY_HEADERS["X-Content-Type-Options"]!);
1202
- mutable = response.headers;
1203
- } catch {
1204
- // immutable — copied below
1205
- }
1206
- }
1207
- const inPlace = mutable !== null;
1208
- const headers = mutable ?? new Headers(response.headers);
1209
- // A handle can mark a response (e.g. a proxied embeddable preview) to opt
1210
- // out of the frame guard. Strip the internal marker so it never ships, and
1211
- // skip only X-Frame-Options for that response — other security headers stay.
1212
- const skipFrameGuard = headers.has(NO_FRAME_GUARD_HEADER);
1213
- headers.delete(NO_FRAME_GUARD_HEADER);
1214
- for (const [k, v] of Object.entries(SECURITY_HEADERS)) {
1215
- if (skipFrameGuard && k === "X-Frame-Options") continue;
1216
- headers.set(k, v);
1217
- }
1218
- const cspHeader = buildCspHeader(nonce);
1219
- if (cspHeader) headers.set("Content-Security-Policy", cspHeader);
1220
- // Apply CORS headers for allowed origins. `Vary: Origin` is set whenever
1221
- // CORS is configured — even on responses to non-allowed origins — so
1222
- // downstream caches (CDNs, browser HTTP cache) key on the Origin header
1223
- // instead of serving an Access-Control-Allow-Origin response across origins.
1224
- if (CORS_CONFIG) {
1225
- applyCorsVary(headers);
1226
- const corsHeaders = getCorsHeaders(request, CORS_CONFIG);
1227
- if (corsHeaders) {
1228
- for (const [k, v] of Object.entries(corsHeaders)) headers.set(k, v);
1229
- }
1230
- }
1231
- // Apply any Set-Cookie headers accumulated during the request
1232
- for (const cookie of cookieJar.outgoing) headers.append("Set-Cookie", cookie);
1233
- if (inPlace) return response;
1234
- return new Response(response.body, {
1235
- // A Content-Encoding here means the body is already compressed (cache
1236
- // hit, compress()); rebuilding drops that flag and Workers would
1237
- // compress it again.
1238
- ...(headers.has("content-encoding") ? PRECOMPRESSED : {}),
1239
- status: response.status,
1240
- statusText: response.statusText,
1241
- headers,
1242
- });
1260
+ return finalizeResponse(response, request, nonce, cookieJar);
1243
1261
  } catch (err) {
1244
1262
  // `throw redirect()` / `throw error()` from a hook lands here — the same
1245
1263
  // escape hatch loaders have always had. Without these branches both fall
@@ -1273,13 +1291,10 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
1273
1291
  undefined,
1274
1292
  nonce,
1275
1293
  );
1276
- // A hook that expires the session before throwing must not lose the
1277
- // Set-Cookie that does it — the redirect above is a fresh Response,
1278
- // so the jar is re-applied by hand here.
1279
- if (cookieJar) {
1280
- for (const cookie of cookieJar.outgoing) out.headers.append("Set-Cookie", cookie);
1281
- }
1282
- return out;
1294
+ // The same finishing pass as a resolved response: security headers,
1295
+ // CSP, CORS, and the jar's Set-Cookie — a hook that expires the session
1296
+ // before throwing must not lose the cookie that does it.
1297
+ return cookieJar ? finalizeResponse(out, request, nonce, cookieJar) : out;
1283
1298
  }
1284
1299
  if (isDev) console.error("Unhandled request error:", err);
1285
1300
  else console.error("Unhandled request error:", (err as Error).message ?? err);
package/src/lib/utils.ts CHANGED
@@ -3,14 +3,7 @@ import { twMerge } from "tailwind-merge";
3
3
  type ClassDictionary = Record<string, any>;
4
4
  type ClassArray = ClassValue[];
5
5
  type ClassValue =
6
- | ClassArray
7
- | ClassDictionary
8
- | string
9
- | number
10
- | bigint
11
- | null
12
- | boolean
13
- | undefined;
6
+ ClassArray | ClassDictionary | string | number | bigint | null | boolean | undefined;
14
7
 
15
8
  function clsx(...inputs: ClassValue[]): string {
16
9
  let str = "";
@@ -20,8 +20,8 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
23
- "prettier": "^3.3.0",
24
- "prettier-plugin-svelte": "^3.2.0",
23
+ "prettier": "3.9.9",
24
+ "prettier-plugin-svelte": "3.5.2",
25
25
  "svelte-check": "^4.4.8",
26
26
  "typescript": "^5"
27
27
  }
@@ -20,8 +20,8 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
23
- "prettier": "^3.3.0",
24
- "prettier-plugin-svelte": "^3.2.0",
23
+ "prettier": "3.9.9",
24
+ "prettier-plugin-svelte": "3.5.2",
25
25
  "typescript": "^5"
26
26
  }
27
27
  }
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/bun": "latest",
24
- "prettier": "^3.3.0",
25
- "prettier-plugin-svelte": "^3.2.0",
24
+ "prettier": "3.9.9",
25
+ "prettier-plugin-svelte": "3.5.2",
26
26
  "typescript": "^5",
27
27
  "drizzle-kit": "^0.31.0"
28
28
  }
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: uploaded files (the file-upload feature). -->
2
+ <svelte:head>
3
+ <title>Files</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Files</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: list orders and open their details (OrderService in src/features/shop). -->
2
+ <svelte:head>
3
+ <title>Orders</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Orders</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: orders waiting to be paid or shipped. -->
2
+ <svelte:head>
3
+ <title>Pending orders</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Pending orders</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: refund requests and refunded orders. -->
2
+ <svelte:head>
3
+ <title>Refunds</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Refunds</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: list, create and edit products (ProductService in src/features/shop). -->
2
+ <svelte:head>
3
+ <title>Products</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Products</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: the signed-in user's name, email and password. -->
2
+ <svelte:head>
3
+ <title>Profile</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Profile</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: store settings. -->
2
+ <svelte:head>
3
+ <title>Settings</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Settings</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: users and their roles (the auth and rbac features). -->
2
+ <svelte:head>
3
+ <title>Users</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Users</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -17,10 +17,10 @@
17
17
 
18
18
  <div class="mt-4 flex gap-3">
19
19
  <a
20
- href="/products"
20
+ href="/dashboard/products"
21
21
  class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
22
22
  >
23
- Browse products
23
+ Manage products
24
24
  </a>
25
25
  <a
26
26
  href="/login"
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/bun": "latest",
24
- "prettier": "^3.3.0",
25
- "prettier-plugin-svelte": "^3.2.0",
24
+ "prettier": "3.9.9",
25
+ "prettier-plugin-svelte": "3.5.2",
26
26
  "typescript": "^5",
27
27
  "drizzle-kit": "^0.31.0"
28
28
  }
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: uploaded files (the file-upload feature). -->
2
+ <svelte:head>
3
+ <title>Files</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Files</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: list orders and open their details (OrderService in src/features/shop). -->
2
+ <svelte:head>
3
+ <title>Orders</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Orders</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: orders waiting to be paid or shipped. -->
2
+ <svelte:head>
3
+ <title>Pending orders</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Pending orders</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: refund requests and refunded orders. -->
2
+ <svelte:head>
3
+ <title>Refunds</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Refunds</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: list, create and edit products (ProductService in src/features/shop). -->
2
+ <svelte:head>
3
+ <title>Products</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Products</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: the signed-in user's name, email and password. -->
2
+ <svelte:head>
3
+ <title>Profile</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Profile</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: store settings. -->
2
+ <svelte:head>
3
+ <title>Settings</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Settings</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <!-- EDIT THIS FILE: users and their roles (the auth and rbac features). -->
2
+ <svelte:head>
3
+ <title>Users</title>
4
+ </svelte:head>
5
+
6
+ <div class="flex flex-col gap-4">
7
+ <h1 class="text-2xl font-bold tracking-tight">Users</h1>
8
+ <p class="text-muted-foreground text-sm">Nothing here yet. Build this page when you need it.</p>
9
+ </div>
@@ -17,10 +17,10 @@
17
17
 
18
18
  <div class="mt-4 flex gap-3">
19
19
  <a
20
- href="/products"
20
+ href="/dashboard/products"
21
21
  class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
22
22
  >
23
- Browse products
23
+ Manage products
24
24
  </a>
25
25
  <a
26
26
  href="/login"