caspian-utils 0.2.1 → 0.2.3

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.
@@ -56,7 +56,9 @@ Use prompts like these to check whether AI lands on the correct docs and files.
56
56
  | Make a grouped shell keep sidebar scroll while resetting page content on child-route navigation. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/layout.html`, `public/js/pp-reactive-v2.js`, `main.py` | `pp-reset-scroll` placement, push-vs-history scroll behavior, and shared-shell ownership |
57
57
  | Create a new contact page with interactive form behavior. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `src/app/**/index.html`, `main.py`, `.venv/Lib/site-packages/casp/components_compiler.py`, `.venv/Lib/site-packages/casp/scripts_type.py` | single-root template shape, script-inside-root authoring, and authored-vs-runtime boundaries |
58
58
  | Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
59
- | Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and mandatory Prisma Python ORM use for database reads and writes when Prisma is enabled |
59
+ | Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and mandatory Prisma Python ORM use for database reads and writes when Prisma is enabled |
60
+ | Add `public/icons/app.png` and make it browser-accessible without editing routes. | [index.md](./index.md), [project-structure.md](./project-structure.md), [core-runtime-map.md](./core-runtime-map.md), [auth.md](./auth.md) | `public/**`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py` | root-relative URL mapping, no per-directory route or prefix registry, traversal and symlink containment, `GET`/`HEAD` only, and middleware order |
61
+ | Store untrusted uploads in a custom public directory. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [project-structure.md](./project-structure.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/index.py`, `src/lib/**`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py` | top-level `inline_safe_subdirectories` registration, attachment mode for HTML/SVG/unknown types, `nosniff`, path containment, and BrowserSync ignore rules |
60
62
  | Explain why authored Caspian templates use a plain `<script>` instead of `type="text/pp"`. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [routing.md](./routing.md) | `main.py`, `.venv/Lib/site-packages/casp/scripts_type.py`, `.venv/Lib/site-packages/casp/components_compiler.py` | script rewriting, `pp-component` injection, and authored-vs-runtime boundaries |
61
63
  | Debug why `StateManager` does not persist across a full redirect. | [index.md](./index.md), [state.md](./state.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/state_manager.py` | wire vs non-wire reset behavior and `request.state.session` dependency |
62
64
  | Trace the auth cookie and CSRF cookie names used in development. | [index.md](./index.md), [auth.md](./auth.md), [core-runtime-map.md](./core-runtime-map.md) | `main.py`, `.venv/Lib/site-packages/casp/auth.py` | development cookie scoping and CSRF or session naming ownership |
package/dist/docs/auth.md CHANGED
@@ -291,35 +291,52 @@ The current `main.py` adds middleware in this source order:
291
291
  ```python
292
292
  app.add_middleware(RPCMiddleware)
293
293
  app.add_middleware(AuthMiddleware)
294
- app.add_middleware(CSRFMiddleware)
295
- app.add_middleware(SessionMiddleware, ...)
296
- app.add_middleware(SecurityHeadersMiddleware)
294
+ app.add_middleware(CSRFMiddleware)
295
+ app.add_middleware(SessionMiddleware, ...)
296
+ app.add_middleware(BodySizeLimitMiddleware)
297
+ app.add_middleware(RateLimitMiddleware)
298
+ app.add_middleware(
299
+ PublicFilesMiddleware,
300
+ directory="public",
301
+ inline_safe_subdirectories={
302
+ "uploads": INLINE_SAFE_UPLOAD_MEDIA_TYPES,
303
+ },
304
+ )
305
+ app.add_middleware(SecurityHeadersMiddleware)
306
+ # RequestDiagnosticsMiddleware is added last outside production.
297
307
  ```
298
308
 
299
309
  Because Starlette runs the last-added middleware first, the effective request order is:
300
310
 
301
- 1. `SecurityHeadersMiddleware`
302
- 2. `SessionMiddleware`
303
- 3. `CSRFMiddleware`
304
- 4. `AuthMiddleware`
305
- 5. `RPCMiddleware`
306
- 6. route handler or RPC endpoint
307
-
308
- Current behavior by layer:
309
-
310
- - `SecurityHeadersMiddleware` can attach baseline response headers from `casp.runtime_security`, such as `X-Content-Type-Options`, framing policy, referrer policy, permissions policy, and production HSTS, while preserving any headers already set by the response. Do not assume this layer emits CSP or owns third-party browser resource allowlists; verify the installed package helper first.
311
- - `SessionMiddleware` provides `request.session` for the rest of the stack.
311
+ 1. `RequestDiagnosticsMiddleware` outside production
312
+ 2. `SecurityHeadersMiddleware`
313
+ 3. `PublicFilesMiddleware`
314
+ 4. `RateLimitMiddleware`
315
+ 5. `BodySizeLimitMiddleware`
316
+ 6. `SessionMiddleware`
317
+ 7. `CSRFMiddleware`
318
+ 8. `AuthMiddleware`
319
+ 9. `RPCMiddleware`
320
+ 10. route handler or RPC endpoint
321
+
322
+ Current behavior by layer:
323
+
324
+ - `SecurityHeadersMiddleware` attaches baseline response headers from `casp.runtime_security`, including the Content-Security-Policy, `X-Content-Type-Options`, framing policy, referrer policy, permissions policy, and production HSTS, while preserving headers already set by the response. `CONTENT_SECURITY_POLICY` replaces the default CSP wholesale when the app needs a different policy.
325
+ - `PublicFilesMiddleware` serves any existing `GET`/`HEAD` file under `public/**` before rate limiting, sessions, CSRF, auth, RPC, or page routing. It falls through for missing files and preserves restricted inline-media handling for configured upload directories.
326
+ - `RateLimitMiddleware` applies the page budget to requests that were not already served as existing public files; `/health` is explicitly exempt.
327
+ - `BodySizeLimitMiddleware` rejects oversized request bodies before session, auth, RPC, or route parsing.
328
+ - `SessionMiddleware` provides `request.session` for the rest of the stack.
312
329
  - `CSRFMiddleware` ensures `request.session["csrf_token"]` exists and emits a scoped CSRF cookie based on `pp_csrf`, for example `pp_csrf_5091` in development or plain `pp_csrf` when no dev scope is active.
313
- - `AuthMiddleware` sets request context with `Auth.set_request(request)`, initializes `StateManager`, runs provider callbacks, skips configured static asset paths, and enforces public, auth, private, and role-based route redirects.
330
+ - `AuthMiddleware` sets request context with `Auth.set_request(request)`, initializes `StateManager`, runs provider callbacks, and enforces public, auth, private, and role-based route redirects. Existing public files never reach it because `PublicFilesMiddleware` serves them first.
314
331
  - `RPCMiddleware` handles `POST` requests with `X-PP-RPC: true` and forwards them to Caspian's RPC handler after auth and session setup are already available.
315
332
 
316
- Keep `SessionMiddleware` immediately inside any outer response-header wrapper such as `SecurityHeadersMiddleware`. If CSRF, auth, or RPC handling runs before the session layer, `request.session` will not be available.
333
+ Keep `SessionMiddleware` outside CSRF, auth, and RPC so those inner layers can access `request.session`. Keep `PublicFilesMiddleware` outside rate limiting, body parsing, sessions, CSRF, auth, and RPC, but inside `SecurityHeadersMiddleware`, so public files avoid per-user work and cookies while retaining security headers.
317
334
 
318
335
  ## Current `AuthMiddleware` Flow
319
336
 
320
- The pasted `main.py` auth middleware currently behaves like this:
321
-
322
- - bypasses auth checks for `/css/*`, `/js/*`, `/assets/*`, and `/favicon.ico`
337
+ The pasted `main.py` auth middleware currently behaves like this:
338
+
339
+ - receives no request for an existing public file because the outer `PublicFilesMiddleware` has already served it; missing paths continue through normal auth and routing behavior
323
340
  - initializes request-bound auth state with `StateManager.init(request)` and `Auth.set_request(request)`
324
341
  - runs OAuth provider signin and callback handling before public or private route checks
325
342
  - lets public routes through immediately
@@ -39,10 +39,10 @@ Important current `main.py` behaviors AI should keep in mind:
39
39
  - `register_single_route(...)` passes path params to `page()` as one positional dict, injects matching query params by name, and injects `request` by keyword when declared.
40
40
  - The render pipeline is `transform_components(...)`, then `render_with_nested_layouts(...)`, then `transform_scripts(...)`.
41
41
  - Route-level generators returned from `page()` are wrapped in `SSE(...)` before the response is sent.
42
- - Safe public-file helpers live in `casp.runtime_security` and should reject `..` traversal before serving from `public/**`.
42
+ - Safe public-file helpers live in `casp.runtime_security`. `PublicFilesMiddleware` maps every existing nested file under `public/**` to the same root-relative URL without per-directory routes, rejects traversal and symlink escape, handles only `GET`/`HEAD`, and falls through when no file exists so page and mounted-app routing still works. User-upload directories must retain their restricted inline-media policy.
43
43
  - Session middleware secrets may be resolved through `casp.runtime_security` so production can fail fast when `AUTH_SECRET` is missing or still on a default placeholder.
44
44
  - Baseline response headers are built by `casp.runtime_security` and attached through `SecurityHeadersMiddleware`. That set now includes a Content-Security-Policy from `build_content_security_policy()`, which the `CONTENT_SECURITY_POLICY` environment variable replaces wholesale. The policy must keep `'unsafe-eval'` and `'unsafe-inline'` in `script-src`, because the PulsePoint runtime compiles templates with `new Function`; read the current helper before tightening it. Outside production the helper also adds loopback origins to `connect-src` so the BrowserSync live-reload client, which polls a different port than the proxied page, is not blocked; an environment override replaces that too.
45
- - Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `SecurityHeadersMiddleware`, plus `RequestDiagnosticsMiddleware` outside production. The effective request order is reversed at runtime, so security headers and the rate limit run first and `RPCMiddleware` runs last. Verify the current list in `main.py` rather than trusting this order.
45
+ - Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `PublicFilesMiddleware`, `SecurityHeadersMiddleware`, plus `RequestDiagnosticsMiddleware` outside production. The effective production request order is reversed: security headers, public files, rate limit, body limit, session, CSRF, auth, then RPC and routing. Development diagnostics are outermost. Existing public-file `GET`/`HEAD` requests stop at `PublicFilesMiddleware`; missing paths fall through. Verify the current list in `main.py` rather than trusting an older order.
46
46
  - `public/js/pp-reactive-v2.js` saves scroll positions per history entry, resets window scroll on push navigation, and uses `pp-reset-scroll="true"` to opt specific containers or the whole body into reset behavior.
47
47
 
48
48
  ## Caspian Core Feature Map
@@ -112,7 +112,7 @@ Use these behavior checkpoints when AI needs the fastest verification path for a
112
112
 
113
113
  | Runtime area | Verify these behaviors |
114
114
  | --- | --- |
115
- | `main.py` routing and request flow | route registration, path and query injection, static asset handling, session-middleware wiring, response-header middleware, and exception rendering |
115
+ | `main.py` routing and request flow | route registration, path and query injection, generic public-root file handling and middleware placement, session-middleware wiring, response-header middleware, and exception rendering |
116
116
  | `main.py` WebSockets | `cfg.websocket` route-registration gate, `@app.websocket(...)` paths, and the shared transport loop (message-size, idle-timeout, close codes, connection cleanup). Authorization is a single guard in `src/lib/websocket/websocket_security.py` (`authorize_websocket`) that runs the origin check before `accept()` and delegates auth to `Auth`. The HTTP middleware stack skips `scope["type"] == "websocket"`, so socket auth is enforced by that guard, not by `AuthMiddleware` |
117
117
  | `public/js/pp-reactive-v2.js` browser runtime | SPA interception, history-vs-push scroll behavior, `pp-reset-scroll` semantics, and browser-side `pp.rpc()` behavior |
118
118
  | `casp.auth` | auth settings, signin and signout flow, provider wiring, and page protection behavior |
@@ -325,7 +325,8 @@ Use this pattern for real file managers:
325
325
 
326
326
  - Keep upload and delete actions in the owning route `index.py`, not in `main.py`.
327
327
  - Use `page()` to render the initial manager payload.
328
- - Store durable metadata in Prisma and store the browser-accessible blob separately under a public upload directory such as `public/uploads/`.
328
+ - Store durable metadata in Prisma and store the browser-accessible blob separately under a public upload directory such as `public/uploads/`.
329
+ - Before accepting untrusted uploads, verify that the directory's top-level public name is configured in `PublicFilesMiddleware.inline_safe_subdirectories`; the conventional `public/uploads/` location uses the `uploads` entry so unsafe HTML, SVG, and unknown types download instead of rendering inline.
329
330
  - Create `public/uploads` on demand in the shared helper if the directory does not exist yet.
330
331
  - Use `pp.state(...)` plus `pp-for` for the list UI instead of manual `innerHTML` writes.
331
332
  - Add the public-root-relative directory name `uploads` to `PUBLIC_IGNORE_DIRS` in `settings/bs-config.ts` when `public/uploads/` is the active upload root so runtime uploads do not trigger BrowserSync reloads during `npm run dev`.
@@ -34,7 +34,8 @@ If the file manager lives inside a grouped subtree such as a dashboard, account
34
34
  - Keep first-render file manager data in `page()` so the initial HTML already contains the current asset list and storage summary.
35
35
  - Keep upload and delete actions in the route's `index.py`; do not move ordinary upload flows into `main.py`.
36
36
  - Keep reusable file-manager helpers in `src/lib/`.
37
- - Store uploaded blobs under a project-owned public directory such as `public/uploads/...` when the files should be browser-accessible.
37
+ - Store uploaded blobs under a project-owned public directory such as `public/uploads/...` when the files should be browser-accessible.
38
+ - Confirm that the directory's top-level public name is present in `PublicFilesMiddleware.inline_safe_subdirectories` before storing untrusted uploads there. The conventional `public/uploads/**` path uses the `uploads` mapping.
38
39
  - Create the upload directory on demand in the shared helper when it does not exist yet; do not assume the folder is committed.
39
40
  - Store durable metadata in Prisma, not in JSON manifests or ad hoc metadata files.
40
41
  - Use `pp.state(...)` plus `pp-for` to render and update the file list from returned server payloads.
@@ -48,7 +49,7 @@ If the file manager lives inside a grouped subtree such as a dashboard, account
48
49
  | Upload and delete `@rpc()` actions | `src/app/**/index.py` | Keep these route-local so they stay close to the owning page. |
49
50
  | Shared storage, normalization, and persistence helpers | `src/lib/**` | Reuse helpers across routes without pushing route behavior into app bootstrap. |
50
51
  | Upload metadata model | `prisma/schema.prisma` | Persist owner, file name, MIME type, path, size, collection, and timestamps in Prisma. |
51
- | Browser-accessible uploaded blobs | `public/uploads/**` or another app-owned public directory | Keep the public path predictable and derived from stored metadata, and create the directory on demand if it may not exist yet. |
52
+ | Browser-accessible uploaded blobs | `public/uploads/**` or another explicitly protected public directory | Keep the public path predictable and derived from stored metadata, create it on demand, and configure its top-level name in `PublicFilesMiddleware.inline_safe_subdirectories` before accepting untrusted files. |
52
53
  | BrowserSync upload ignore | `settings/bs-config.ts` | Keep the active public upload directory in `PUBLIC_IGNORE_DIRS`. |
53
54
 
54
55
  ## Route Flow
@@ -167,7 +168,32 @@ Typical metadata fields include:
167
168
  - size in bytes
168
169
  - uploaded timestamp
169
170
 
170
- ## BrowserSync And Uploaded Public Files
171
+ ## Public-Serving Security
172
+
173
+ Every existing file under `public/**` is reachable at the matching
174
+ root-relative URL without a directory-specific route. That is correct for
175
+ trusted first-party assets, but runtime uploads cross a different trust
176
+ boundary. In `main.py`, configure each top-level upload directory with the
177
+ allow-list used by `PublicFilesMiddleware`:
178
+
179
+ ```python
180
+ app.add_middleware(
181
+ PublicFilesMiddleware,
182
+ directory="public",
183
+ inline_safe_subdirectories={
184
+ "uploads": INLINE_SAFE_UPLOAD_MEDIA_TYPES,
185
+ },
186
+ )
187
+ ```
188
+
189
+ The resolver rejects traversal and symlink escape. Within configured upload
190
+ directories, allow-listed raster image types render inline; HTML, SVG, unknown,
191
+ and other executable or unsafe types download as
192
+ `application/octet-stream` with `Content-Disposition: attachment` and
193
+ `X-Content-Type-Options: nosniff`. Do not place untrusted uploads in a different
194
+ top-level public directory until that directory is added to the mapping.
195
+
196
+ ## BrowserSync And Uploaded Public Files
171
197
 
172
198
  If runtime uploads write into `public/uploads/`, BrowserSync should ignore that directory during local development. Otherwise every upload can trigger a full browser reload.
173
199
 
@@ -187,7 +213,8 @@ const PUBLIC_IGNORE_DIRS = ["uploads"];
187
213
  - Do not use JSON files under `storage/` as the active metadata store when Prisma is enabled.
188
214
  - Do not treat `onUploadProgress` callbacks as the source of truth for the final asset list.
189
215
  - Do not manually repaint the file list with `innerHTML` when PulsePoint state can own the list.
190
- - Do not leave uploaded public directories out of BrowserSync ignore rules.
216
+ - Do not leave uploaded public directories out of BrowserSync ignore rules.
217
+ - Do not store untrusted runtime uploads in an unconfigured top-level public directory; generic first-party public files render inline.
191
218
 
192
219
  ## AI Retrieval Notes
193
220
 
@@ -195,6 +222,6 @@ const PUBLIC_IGNORE_DIRS = ["uploads"];
195
222
  - Use `fetch-data.md` for the route-render versus RPC split.
196
223
  - Use `database.md` when Prisma models or relations must change for upload metadata.
197
224
  - Use `validation.md` for MIME, extension, and other boundary checks, then keep explicit size and auth checks in the owning RPC action.
198
- - Use `project-structure.md` for placement rules, especially `src/app/` versus `src/lib/` and `public/uploads/`.
225
+ - Use `project-structure.md` for placement rules, especially `src/app/` versus `src/lib/` and `public/uploads/`; verify public serving and the restricted-inline mapping in `main.py` plus `casp.runtime_security`.
199
226
  - For grouped file-manager sections, follow the section layout pattern in [routing.md](./routing.md) and keep the upload page in a child route folder beneath the shared shell.
200
227
  - Use `commands.md` and `settings/bs-config.ts` when uploads should not trigger BrowserSync reloads during `npm run dev`.
@@ -39,7 +39,8 @@ When generating or editing a Caspian app, treat these as the default choices unl
39
39
  - Use `@rpc()` plus `pp.rpc()` for browser-triggered reads, writes, streaming, and uploads.
40
40
  - When `caspian.config.json` has `websocket: true`, use app-owned FastAPI WebSocket endpoints only for long-lived bidirectional live channels; keep normal browser-triggered data work on RPC.
41
41
  - When `caspian.config.json` has `prisma: true`, use the generated Prisma Python ORM from `src/lib/prisma/**` for Python-side database reads and writes. Do not invent a second fetch layer, raw driver wrapper, JSON active store, or browser-side data path that bypasses Prisma.
42
- - Use `Validate` and `Rule` from `casp.validate` for server-side input validation and sanitization.
42
+ - Use `Validate` and `Rule` from `casp.validate` for server-side input validation and sanitization.
43
+ - Treat `public/` as a direct URL-root mapping: `public/icons/app.png` is `/icons/app.png` without a per-directory route. Use the restricted-inline mapping in `PublicFilesMiddleware` for every top-level directory that receives untrusted runtime uploads.
43
44
 
44
45
  ## AI Doc Shape
45
46
 
@@ -180,11 +180,24 @@ This folder contains your database model definitions in `schema.prisma` and any
180
180
 
181
181
  ### `public/`
182
182
 
183
- Store static assets here, including images, fonts, and generated frontend assets that should be served directly.
184
-
185
- Runtime-uploaded public blobs can also live here. Confirm the actual upload path in the project code, commonly `public/uploads/`, and keep that directory aligned with any BrowserSync ignore rules.
186
-
187
- If the upload directory is created only at runtime, create it on demand in the owning upload helper instead of assuming it is already committed.
183
+ Store static assets here, including images, fonts, and generated frontend assets that should be served directly.
184
+
185
+ The public root maps directly to the URL root. Any existing nested file is
186
+ available at the same root-relative URL—for example,
187
+ `public/icons/app.png` is served as `/icons/app.png`. Adding a public
188
+ subdirectory does not require a matching route or mount in `main.py`.
189
+
190
+ Runtime-uploaded public blobs can also live here. Confirm the actual upload path in the project code, commonly `public/uploads/`, and keep that directory aligned with any BrowserSync ignore rules.
191
+
192
+ If the upload directory is created only at runtime, create it on demand in the owning upload helper instead of assuming it is already committed.
193
+
194
+ Public means browser-accessible, not automatically safe to render inline.
195
+ `PublicFilesMiddleware.inline_safe_subdirectories` must include every top-level
196
+ public directory that receives untrusted runtime uploads. Unsafe types in a
197
+ configured upload directory, notably HTML and SVG, are sent as attachments with
198
+ `nosniff`; trusted first-party assets are served inline. The standard
199
+ `public/uploads/**` location is protected only when `main.py` configures the
200
+ top-level `uploads` entry.
188
201
 
189
202
  If the local BrowserSync stack is running, keep that upload directory in `settings/bs-config.ts` `PUBLIC_IGNORE_DIRS` so new uploads do not force a full browser reload.
190
203
 
@@ -218,7 +231,14 @@ Use this package module when the task is about:
218
231
  - user-facing vs production-safe exception messaging helpers
219
232
  - baseline response headers such as permissions, referrer, MIME sniffing, framing, or HSTS behavior
220
233
 
221
- Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
234
+ Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
235
+
236
+ `PublicFilesMiddleware` serves only existing `GET`/`HEAD` files that resolve
237
+ inside the configured public root and falls through for every other request.
238
+ Install it inside the security-header layer and outside rate limiting, sessions,
239
+ CSRF, auth, RPC, and page routing. Preserve a restricted inline-media policy for
240
+ user-upload directories so executable uploads such as HTML and SVG download
241
+ instead of rendering with the application's origin.
222
242
 
223
243
  ### `caspian.config.json`
224
244
 
@@ -62,7 +62,7 @@ The script is composed so the export always runs against a **fresh route/compone
62
62
 
63
63
  - boots the real ASGI app in-process and requests every route through Starlette's `TestClient`, so exported HTML is byte-identical to what the dev server serves (layouts, components, PulsePoint deferral, security headers — the whole pipeline),
64
64
  - writes each page to `static/<route>/index.html` (`/` → `static/index.html`),
65
- - mirrors public asset trees (`css`, `js`, `assets`, `uploads`, `favicon.ico`) into `static/`,
65
+ - mirrors the complete `public/**` tree into `static/`, including app-defined directories such as `icons` and `fonts`,
66
66
  - runs in `APP_ENV=development` so the build does not require production secrets.
67
67
 
68
68
  **Scope policy is "warn & skip"** — anything that cannot be fully static is reported and NOT written, so nothing broken ships silently:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {