@firtoz/socka 4.0.0 → 5.0.0

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/docs/auth.md CHANGED
@@ -2,18 +2,27 @@
2
2
 
3
3
  Socka does not ship a built-in auth layer: you decide **who** may open a WebSocket and **what** each RPC may do. Typical patterns:
4
4
 
5
- ## Read credentials on upgrade
5
+ ## Reject before upgrade (HTTP 401 / 403)
6
6
 
7
- - **`@firtoz/socka/server`** by default **`createData`** receives **`SockaStrictWebSocketInit`**; read **`init.request`** (cookies via **`Cookie`**, **`Authorization`**, URL query, path segments).
7
+ When identity is attested on a **web worker** and forwarded to the DO (headers, shared secret, signed query), reject **before** `101 Switching Protocols`:
8
+
9
+ - **`BaseWebSocketDO.beforeWebSocket(ctx)`** — override on your DO (inherited by **`SockaWebSocketDO`**). Return **`401` / `403`** to stop the upgrade; return **`void`** to proceed. Runs before **`WebSocketPair`** is created.
10
+ - **Hono middleware** on a chained **`app = this.getBaseApp().use(…)`** — same HTTP semantics for routes you mount before **`/websocket`**.
11
+
12
+ This is preferable to **`override fetch()`** on the whole DO when you only need to gate the WebSocket path.
13
+
14
+ ## Read credentials after upgrade (`createData`)
15
+
16
+ - **`@firtoz/socka/server`** — **`createData`** receives **`SockaStrictWebSocketInit`**; read **`init.request`** (cookies, **`Authorization`**, query, path).
8
17
  - **`SockaDoSession`** with **`createData: (ctx) => …`** — use Hono **`ctx.req`**, **`ctx.get("…")`**, or **`ctx.req.raw.headers`**.
9
18
 
10
- Reject before returning session data: throw **`SockaError`** with **`{ code, data }`** so the client receives a correlated **`serverError`** frame (see **[ReferenceRPC handler errors](./reference.md#rpc-handler-errors)**).
19
+ Throwing **`SockaError`** in **`createData`** runs **after** the upgrade (`101` already sent). The client gets a correlated **`serverError`** frame and the socket closes **not** a clean HTTP rejection. Use this for “connected but not allowed” or when credentials are only available post-upgrade; use **`beforeWebSocket`** when you need HTTP status codes before opening the socket.
11
20
 
12
21
  ## Browsers and the WebSocket API
13
22
 
14
23
  The browser **`WebSocket`** constructor cannot set arbitrary headers on the handshake. Common approaches:
15
24
 
16
- - **Cookie** — `SameSite` cookies sent automatically to your origin; read them in **`createData`** from **`init.request`**.
25
+ - **Cookie** — `SameSite` cookies sent automatically to your origin; read them in **`createData`** or validate on the worker and forward attested identity to the DO **`beforeWebSocket`** check.
17
26
  - **Query string** — `wss://app.example.com/ws/room?token=…` (treat tokens as secrets; prefer short-lived tokens and HTTPS/WSS only).
18
27
  - **Subprotocol** — rarely needed; socka uses its own wire framing on the same socket.
19
28
 
@@ -23,5 +32,6 @@ You can also enforce auth inside **RPC handlers** using **`session.data`** (set
23
32
 
24
33
  ## See also
25
34
 
35
+ - **[Durable Objects](./durable-objects.md)** — chaining HTTP routes on **`app`**.
26
36
  - **[Multi-room](./multi-room.md)** — scoping **`sessionMap`** per tenant/room.
27
- - **[Client](./client.md)** — lifecycle and reconnect; re-auth after reconnect may repeat **`listHistory`** / snapshot RPCs.
37
+ - **[Client](./client.md)** — lifecycle and reconnect; re-auth after reconnect may repeat snapshot RPCs.
package/docs/client.md CHANGED
@@ -69,6 +69,14 @@ Optional **`pushHandlers`** in the second argument matches **`Partial<InferSocka
69
69
 
70
70
  **Recommended pattern:** In the route module, set **`const [url, setUrl] = useState<string | null>(null)`**, and in **`useEffect`** (browser-only), build a **`wss://` / `ws://`** URL from **`window.location`** (protocol, host, path to your Worker’s WebSocket route). If **`url === null`**, render a **loading** shell; only render a child component that calls **`useSockaSession(myContract, { url, pushHandlers? }, [url])`** when **`url`** is set. Keep **`url`** in the hook **`deps`** so identity changes re-open the right socket.
71
71
 
72
+ **SSR checklist:**
73
+
74
+ 1. **Never** call **`useSockaSession`** with a placeholder URL during SSR — the hook connects in **`useEffect`** and will throw if **`url`** is missing at runtime.
75
+ 2. **Parent gates the child** — render **`ClientOnly`** / **`url === null ? <Loading /> : <ChatWithSocket url={url} />`** so the hook runs only in the browser with a real URL.
76
+ 3. **Build the URL in `useEffect`** — derive **`wss:`** from **`window.location`**, room id from route params, and optional auth query params there (not during render).
77
+ 4. **Pass `[url]` in hook deps** — room or tenant changes should remount the connection.
78
+ 5. **`SockaSessionProvider`** follows the same rules — wrap only after **`url`** is known.
79
+
72
80
  **React + Durable Object** end-to-end wiring: **[React + Durable Objects](./react-durable-objects.md)**.
73
81
 
74
82
  ### `useSocka` — hold a `SockaSession` ref
@@ -28,7 +28,12 @@ export class MyDO extends SockaWebSocketDO<
28
28
  Env
29
29
  > {
30
30
  protected readonly contract = myContract;
31
- app = this.getBaseApp();
31
+ app = this.getBaseApp().delete("/admin/messages/:messageId", async (c) => {
32
+ const messageId = c.req.param("messageId");
33
+ await this.db.delete(messageId);
34
+ await this.broadcastPushToAll("messageDeleted", { id: messageId });
35
+ return c.json({ ok: true as const });
36
+ });
32
37
 
33
38
  protected buildSockaSessionConfig(
34
39
  ctx: Context<{ Bindings: Env }> | undefined,
@@ -47,7 +52,7 @@ export class MyDO extends SockaWebSocketDO<
47
52
  };
48
53
  }
49
54
 
50
- // HTTP adminno WebSocket session on this path
55
+ // Same logic as the HTTP route above callable from alarms or internal helpers
51
56
  async deleteMessage(id: string) {
52
57
  await this.db.delete(…);
53
58
  await this.broadcastPushToAll("messageDeleted", { id });
@@ -111,9 +116,18 @@ If you mutate **`session.data`** after connect, call **`await session.update()`*
111
116
 
112
117
  ## Pushes from HTTP / non-WebSocket handlers
113
118
 
114
- Many DO apps expose **Hono HTTP routes** on **`app`** (admin moderation, internal APIs, alarms) in addition to **`/websocket`**. After mutating storage from those handlers, you often need a contract-typed push to **every** connected client — with **no** originating WebSocket session.
119
+ Many DO apps expose **Hono HTTP routes** on **`app`** (admin moderation, internal APIs, alarms) in addition to **`/websocket`**. Chain routes on **`this.getBaseApp()`** socka registers **`GET /websocket`**; your handlers share the same **`app`** instance:
115
120
 
116
- Use **`await this.broadcastPushToAll("messageDeleted", { id })`** on your DO subclass. The DO **`contract`** is the single source of truth. See **[Pushes — Pushes from HTTP / non-WebSocket handlers](./pushes.md#pushes-from-http--non-websocket-handlers)**.
121
+ ```ts
122
+ app = this.getBaseApp().delete("/admin/messages/:messageId", async (c) => {
123
+ const id = c.req.param("messageId");
124
+ await this.db.delete(id);
125
+ await this.broadcastPushToAll("messageDeleted", { id });
126
+ return c.json({ ok: true as const });
127
+ });
128
+ ```
129
+
130
+ After mutating storage from those handlers, **`broadcastPushToAll`** fans out to every connected client. The DO **`contract`** is the single source of truth. See **[Pushes — Pushes from HTTP / non-WebSocket handlers](./pushes.md#pushes-from-http--non-websocket-handlers)**.
117
131
 
118
132
  Without a DO subclass, use **`broadcastContractPushToAll(this.sessions, contract, name, body)`** from **`@firtoz/socka/server`**.
119
133
 
package/docs/reference.md CHANGED
@@ -120,7 +120,7 @@ Anything that implements **Standard Schema v1** works — **Zod**, **Valibot**,
120
120
  | Path | Use for |
121
121
  |------|---------|
122
122
  | `@firtoz/socka` | Same as **`@firtoz/socka/core`** — `defineSocka`, wire helpers, errors, types (prefer explicit **`/core`** in examples) |
123
- | `@firtoz/socka/core` | `defineSocka`, wire helpers, `SockaError`, `SockaReportError`, `reportSockaError`, types |
123
+ | `@firtoz/socka/core` | `defineSocka`, wire helpers, `SockaError`, `SockaReportError`, `reportSockaError`, types. Export map includes **`require`** / **`default`** for Node tooling (e.g. drizzle-kit) that resolves CJS. Keep **constants-only** modules (no socka import) beside Drizzle schema when possible. |
124
124
  | `@firtoz/socka/client` | `SockaSession`, `SockaWebSocketClient` (also re-exports `SockaReportError`, `reportSockaError`) |
125
125
  | `@firtoz/socka/test` | `createFakeWebSocket` for unit tests — see **[Testing](./testing.md)** |
126
126
  | `@firtoz/socka/react` | `useSocka`, `useSockaSession`, `useSockaPresence`, provider + context |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/socka",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "description": "Standard Schema–first WebSocket RPC for TypeScript — Bun, Hono, Node ws, Cloudflare Workers, Durable Objects",
6
6
  "main": "./dist/core/index.js",
@@ -9,43 +9,63 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/core/index.d.ts",
12
- "import": "./dist/core/index.js"
12
+ "import": "./dist/core/index.js",
13
+ "require": "./dist/core/index.js",
14
+ "default": "./dist/core/index.js"
13
15
  },
14
16
  "./core": {
15
17
  "types": "./dist/core/index.d.ts",
16
- "import": "./dist/core/index.js"
18
+ "import": "./dist/core/index.js",
19
+ "require": "./dist/core/index.js",
20
+ "default": "./dist/core/index.js"
17
21
  },
18
22
  "./client": {
19
23
  "types": "./dist/client/index.d.ts",
20
- "import": "./dist/client/index.js"
24
+ "import": "./dist/client/index.js",
25
+ "require": "./dist/client/index.js",
26
+ "default": "./dist/client/index.js"
21
27
  },
22
28
  "./react": {
23
29
  "types": "./dist/react/index.d.ts",
24
- "import": "./dist/react/index.js"
30
+ "import": "./dist/react/index.js",
31
+ "require": "./dist/react/index.js",
32
+ "default": "./dist/react/index.js"
25
33
  },
26
34
  "./do": {
27
35
  "types": "./dist/do/index.d.ts",
28
- "import": "./dist/do/index.js"
36
+ "import": "./dist/do/index.js",
37
+ "require": "./dist/do/index.js",
38
+ "default": "./dist/do/index.js"
29
39
  },
30
40
  "./server": {
31
41
  "types": "./dist/server/index.d.ts",
32
- "import": "./dist/server/index.js"
42
+ "import": "./dist/server/index.js",
43
+ "require": "./dist/server/index.js",
44
+ "default": "./dist/server/index.js"
33
45
  },
34
46
  "./bun": {
35
47
  "types": "./dist/bun/index.d.ts",
36
- "import": "./dist/bun/index.js"
48
+ "import": "./dist/bun/index.js",
49
+ "require": "./dist/bun/index.js",
50
+ "default": "./dist/bun/index.js"
37
51
  },
38
52
  "./hono": {
39
53
  "types": "./dist/hono/index.d.ts",
40
- "import": "./dist/hono/index.js"
54
+ "import": "./dist/hono/index.js",
55
+ "require": "./dist/hono/index.js",
56
+ "default": "./dist/hono/index.js"
41
57
  },
42
58
  "./hono/cloudflare": {
43
59
  "types": "./dist/hono/cloudflare-workers.d.ts",
44
- "import": "./dist/hono/cloudflare-workers.js"
60
+ "import": "./dist/hono/cloudflare-workers.js",
61
+ "require": "./dist/hono/cloudflare-workers.js",
62
+ "default": "./dist/hono/cloudflare-workers.js"
45
63
  },
46
64
  "./test": {
47
65
  "types": "./dist/test/index.d.ts",
48
- "import": "./dist/test/index.js"
66
+ "import": "./dist/test/index.js",
67
+ "require": "./dist/test/index.js",
68
+ "default": "./dist/test/index.js"
49
69
  }
50
70
  },
51
71
  "files": [
@@ -96,13 +116,13 @@
96
116
  "access": "public"
97
117
  },
98
118
  "dependencies": {
99
- "@firtoz/maybe-error": "^1.6.1",
119
+ "@firtoz/maybe-error": "^1.6.2",
100
120
  "@standard-schema/spec": "^1.1.0",
101
121
  "msgpackr": "^2.0.1"
102
122
  },
103
123
  "peerDependencies": {
104
124
  "@cloudflare/workers-types": "^4.20260503.1",
105
- "@firtoz/websocket-do": "^13.0.2",
125
+ "@firtoz/websocket-do": "^14.0.0",
106
126
  "@hono/node-server": "^1.19.2",
107
127
  "@hono/node-ws": "^1.3.0",
108
128
  "hono": "^4.12.9",