@firtoz/socka 4.0.0 → 5.0.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.
- package/docs/auth.md +15 -5
- package/docs/client.md +8 -0
- package/docs/durable-objects.md +18 -4
- package/docs/reference.md +1 -1
- package/package.json +44 -24
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
|
-
##
|
|
5
|
+
## Reject before upgrade (HTTP 401 / 403)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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`**
|
|
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
|
|
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
|
package/docs/durable-objects.md
CHANGED
|
@@ -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
|
|
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`**.
|
|
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
|
-
|
|
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": "
|
|
3
|
+
"version": "5.0.1",
|
|
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,17 +116,17 @@
|
|
|
96
116
|
"access": "public"
|
|
97
117
|
},
|
|
98
118
|
"dependencies": {
|
|
99
|
-
"@firtoz/maybe-error": "^1.6.
|
|
119
|
+
"@firtoz/maybe-error": "^1.6.2",
|
|
100
120
|
"@standard-schema/spec": "^1.1.0",
|
|
101
|
-
"msgpackr": "^2.0.
|
|
121
|
+
"msgpackr": "^2.0.2"
|
|
102
122
|
},
|
|
103
123
|
"peerDependencies": {
|
|
104
|
-
"@cloudflare/workers-types": "^4.
|
|
105
|
-
"@firtoz/websocket-do": "^
|
|
124
|
+
"@cloudflare/workers-types": "^4.20260529.1",
|
|
125
|
+
"@firtoz/websocket-do": "^14.0.1",
|
|
106
126
|
"@hono/node-server": "^1.19.2",
|
|
107
127
|
"@hono/node-ws": "^1.3.0",
|
|
108
128
|
"hono": "^4.12.9",
|
|
109
|
-
"react": "^19.2.
|
|
129
|
+
"react": "^19.2.6",
|
|
110
130
|
"ws": "^8.18.0"
|
|
111
131
|
},
|
|
112
132
|
"peerDependenciesMeta": {
|
|
@@ -133,20 +153,20 @@
|
|
|
133
153
|
}
|
|
134
154
|
},
|
|
135
155
|
"devDependencies": {
|
|
136
|
-
"@cloudflare/workers-types": "^4.
|
|
156
|
+
"@cloudflare/workers-types": "^4.20260529.1",
|
|
137
157
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
138
|
-
"@hono/node-server": "^2.0.
|
|
158
|
+
"@hono/node-server": "^2.0.4",
|
|
139
159
|
"@hono/node-ws": "^1.3.1",
|
|
140
|
-
"@tanstack/intent": "^0.0.
|
|
160
|
+
"@tanstack/intent": "^0.0.41",
|
|
141
161
|
"@testing-library/react": "^16.3.2",
|
|
142
|
-
"@types/react": "^19.2.
|
|
162
|
+
"@types/react": "^19.2.15",
|
|
143
163
|
"@types/ws": "^8.18.1",
|
|
144
|
-
"bun-types": "^1.3.
|
|
164
|
+
"bun-types": "^1.3.14",
|
|
145
165
|
"happy-dom": "^20.9.0",
|
|
146
|
-
"react-dom": "19.2.
|
|
166
|
+
"react-dom": "19.2.6",
|
|
147
167
|
"tsup": "^8.5.1",
|
|
148
168
|
"typescript": "^6.0.3",
|
|
149
|
-
"valibot": "^1.
|
|
150
|
-
"zod": "^4.4.
|
|
169
|
+
"valibot": "^1.4.1",
|
|
170
|
+
"zod": "^4.4.3"
|
|
151
171
|
}
|
|
152
172
|
}
|