caspian-utils 0.2.2 → 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,30 +291,46 @@ 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
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
307
321
 
308
322
  Current behavior by layer:
309
323
 
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.
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.
311
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.
312
328
  - `SessionMiddleware` provides `request.session` for the rest of the stack.
313
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.
314
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.
315
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.
316
332
 
317
- 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.
318
334
 
319
335
  ## Current `AuthMiddleware` Flow
320
336
 
@@ -42,7 +42,7 @@ Important current `main.py` behaviors AI should keep in mind:
42
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
 
@@ -188,8 +188,16 @@ available at the same root-relative URL—for example,
188
188
  subdirectory does not require a matching route or mount in `main.py`.
189
189
 
190
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.
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.
193
201
 
194
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.
195
203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {