caspian-utils 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docs/auth.md CHANGED
@@ -329,6 +329,8 @@ The pasted `main.py` auth middleware currently behaves like this:
329
329
 
330
330
  Because `AuthMiddleware` initializes `StateManager` on every request, auth flows can also use [state.md](./state.md) for transient request-scoped success or error state. Keep identity, session lifetime, and authorization decisions in `auth.sign_in(...)`, `auth.sign_out(...)`, and the auth decorators rather than moving them into the state manager.
331
331
 
332
+ `AuthMiddleware` only runs on `scope["type"] == "http"`; it early-returns on WebSocket scopes. A private page route therefore does not protect a WebSocket. To keep socket auth aligned with page auth, reuse the same `Auth` instance inside the socket guard: bind the socket with `Auth.set_request(websocket)` (a `WebSocket` exposes `.session`, the one piece `Auth` reads) and call `auth.is_authenticated()`, `auth.get_payload()`, and `auth.check_role(...)`. See [websockets.md](./websockets.md) for the full guard pattern and the read-only-session caveat.
333
+
332
334
  ## The `auth` Object
333
335
 
334
336
  The global `auth` singleton owns the session lifecycle.
@@ -113,7 +113,7 @@ Use these behavior checkpoints when AI needs the fastest verification path for a
113
113
  | Runtime area | Verify these behaviors |
114
114
  | --- | --- |
115
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 |
116
- | `main.py` WebSockets | `cfg.websocket` route-registration gate, `@app.websocket(...)` paths, origin checks before `accept()`, auth/session extraction, message-size and idle-timeout handling, close codes, and connection cleanup |
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 |
119
119
  | `casp.rpc` and streamed RPC responses | middleware interception, CSRF and session expectations, registry behavior, and helper-level RPC contracts |
@@ -346,11 +346,12 @@ Important:
346
346
  - The script lookup walks the current root and skips nested `pp-component` boundaries, so a parent does not consume a child component's script.
347
347
  - If multiple matching runtime scripts exist in the same root, the first matching owned script wins. Generate one script per root.
348
348
  - Authored route, layout, and component templates still need one top-level parent node so Caspian can inject the component boundary correctly after component expansion.
349
- - A scriptless component root still mounts and can receive props, refs, events, and nested children, but it has no local runtime scope beyond its props.
349
+ - A scriptless component root still mounts and can receive props, refs, events, and nested children, but it does not create local bindings from those props on its own.
350
350
  - Component scripts are plain JavaScript executed with `new Function(...)`. Do not use `import`, `export`, or top-level `await` inside them.
351
351
  - The runtime auto-returns supported top-level bindings from the script. Do not rely on manual `return { ... }` objects.
352
- - `pp.props` contains the current prop bag for the component.
353
- - `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
352
+ - `pp.props` contains the current prop bag for the component.
353
+ - `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
354
+ - Props are not auto-injected as standalone top-level template or handler variables. Read them through `pp.props` or explicitly destructure them in the component script.
354
355
 
355
356
  Bindings exported to the template:
356
357
 
@@ -569,12 +570,13 @@ When a child component needs the same token object, pass it from the provider sc
569
570
 
570
571
  - Child component props are derived from DOM attributes.
571
572
  - Attribute names are converted from kebab-case to camelCase for the prop bag.
572
- - Native `on*` attributes and `pp-component` are not included in props.
573
- - Empty attributes become boolean `true` props.
574
- - Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
575
- - Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
576
- - Non-expression attribute values are passed through as strings.
577
- - `children` is injected into props using the root's initial inner HTML.
573
+ - Native `on*` attributes and `pp-component` are not included in props.
574
+ - Empty attributes become boolean `true` props.
575
+ - Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
576
+ - Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
577
+ - Non-expression attribute values are passed through as strings.
578
+ - `children` is injected into props using the root's initial inner HTML.
579
+ - Inside the child component, those values should be accessed through `pp.props` or a top-level destructure such as `const { title } = pp.props`; they are not implicitly available as bare identifiers.
578
580
 
579
581
  Nested components:
580
582
 
@@ -43,8 +43,9 @@ WEBSOCKET_PATH = "/ws/channel"
43
43
 
44
44
  @app.websocket(WEBSOCKET_PATH)
45
45
  async def websocket_live_endpoint(websocket: WebSocket):
46
- if not is_origin_allowed(websocket):
47
- await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
46
+ # ONE guard authorizes the connection (origin + auth) and returns the
47
+ # identity, or None after closing. Pass require_auth / roles per channel.
48
+ if await authorize_websocket(websocket, require_auth=True) is None:
48
49
  return
49
50
 
50
51
  await manager.connect(websocket)
@@ -58,6 +59,8 @@ async def websocket_live_endpoint(websocket: WebSocket):
58
59
  manager.disconnect(websocket)
59
60
  ```
60
61
 
62
+ Keep the authorization gate in one reusable helper rather than re-deriving auth in each endpoint. A public and a private channel should differ only by their `require_auth`/`roles` policy and their broadcast pool, not by a second copy of origin and session logic. Keep authenticated and guest traffic in separate connection managers so a private broadcast can never fan out to a guest connection.
63
+
61
64
  Keep reusable socket helpers in `src/lib/**` when they are shared by more than one endpoint or route. Good candidates include connection managers, session helpers, auth-payload extraction, payload normalization, and shared broadcast code.
62
65
 
63
66
  Keep route-specific browser UI in `src/app/**/index.html` and route-specific first-render values in the matching `index.py`. For example, an `index.py` can pass `websocket_path` and a development-only BrowserSync-aware `websocket_url` into the template with `render_page(__file__, {...})`.
@@ -108,11 +111,28 @@ This is one of the narrow cases where direct browser APIs and event listeners ar
108
111
 
109
112
  ## Auth And Sessions
110
113
 
111
- HTTP route privacy does not automatically make a WebSocket endpoint private. If a socket is authenticated, the endpoint must explicitly inspect the session or auth payload available on the WebSocket scope and close unauthenticated clients.
114
+ HTTP route privacy does not automatically make a WebSocket endpoint private. The HTTP middleware stack (`AuthMiddleware`, `CSRFMiddleware`, `RPCMiddleware`, body-size, security-header, and diagnostics layers) early-returns on every non-`http` scope, so a WebSocket handshake (`scope["type"] == "websocket"`) is never seen by `AuthMiddleware`. The only middleware that runs on socket scopes is `SessionMiddleware`, which exposes `websocket.session`. Each authenticated socket endpoint must therefore authorize itself.
115
+
116
+ Align that self-check with the rest of the app by delegating to Caspian's `Auth` instead of re-implementing session/`exp`/payload parsing. `Auth` methods read only `request.session`, and a `WebSocket` exposes `.session`, so binding the socket as the request context reuses the exact HTTP auth logic as a single source of truth:
117
+
118
+ ```python
119
+ from casp.auth import Auth
120
+
121
+ Auth.set_request(websocket) # WebSocket exposes .session
122
+ auth = Auth.get_instance()
123
+ if auth.is_authenticated():
124
+ payload = auth.get_payload() or {}
125
+ if roles and not auth.check_role(payload, roles):
126
+ ... # forbidden -> close 1008
127
+ else:
128
+ ... # unauthenticated -> close 1008 if required
129
+ ```
112
130
 
113
131
  Use close code `1008` for policy violations such as failed origin checks or missing authentication. If the endpoint accepts before rejecting, send a small JSON error payload first so the browser UI can show a useful message.
114
132
 
115
- If the app refreshes auth sessions, verify whether the WebSocket helper refreshes session expiry and whether that session mutation is persisted in the current middleware stack. Do not assume HTTP `AuthMiddleware` behavior applies to `scope["type"] == "websocket"`.
133
+ Treat the socket auth check as read-only. `SessionMiddleware` only writes the session cookie on `http.response.start`, which never fires for a WebSocket, so session mutations made during a connection (including `refresh_session` / sliding `exp`) are not persisted back to the cookie. Do not rely on the socket to extend a session; verify identity and, if needed, drive session refresh from an HTTP request. Do not assume HTTP `AuthMiddleware` behavior applies to `scope["type"] == "websocket"`.
134
+
135
+ Path-based `Auth` helpers (`is_private_route`, `get_required_roles`) read the HTTP route configuration and do not know about `/ws/*` paths. For socket RBAC, pass the required roles explicitly into the guard rather than asking `Auth` to classify the socket path.
116
136
 
117
137
  ## Origin And Proxy Checks
118
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {