@openlivesync/server 1.0.2

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.
Files changed (82) hide show
  1. package/README.md +366 -0
  2. package/dist/auth/decode-token.d.ts +40 -0
  3. package/dist/auth/decode-token.d.ts.map +1 -0
  4. package/dist/auth/decode-token.js +93 -0
  5. package/dist/auth/decode-token.js.map +1 -0
  6. package/dist/auth/index.d.ts +6 -0
  7. package/dist/auth/index.d.ts.map +1 -0
  8. package/dist/auth/index.js +6 -0
  9. package/dist/auth/index.js.map +1 -0
  10. package/dist/auth/token-auth.d.ts +18 -0
  11. package/dist/auth/token-auth.d.ts.map +1 -0
  12. package/dist/auth/token-auth.js +45 -0
  13. package/dist/auth/token-auth.js.map +1 -0
  14. package/dist/connection.d.ts +36 -0
  15. package/dist/connection.d.ts.map +1 -0
  16. package/dist/connection.js +170 -0
  17. package/dist/connection.js.map +1 -0
  18. package/dist/index.d.ts +19 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +15 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/protocol.d.ts +135 -0
  23. package/dist/protocol.d.ts.map +1 -0
  24. package/dist/protocol.js +17 -0
  25. package/dist/protocol.js.map +1 -0
  26. package/dist/room-manager.d.ts +19 -0
  27. package/dist/room-manager.d.ts.map +1 -0
  28. package/dist/room-manager.js +34 -0
  29. package/dist/room-manager.js.map +1 -0
  30. package/dist/room.d.ts +41 -0
  31. package/dist/room.d.ts.map +1 -0
  32. package/dist/room.js +150 -0
  33. package/dist/room.js.map +1 -0
  34. package/dist/server.d.ts +46 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +105 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/storage/chat-storage.d.ts +19 -0
  39. package/dist/storage/chat-storage.d.ts.map +1 -0
  40. package/dist/storage/chat-storage.js +6 -0
  41. package/dist/storage/chat-storage.js.map +1 -0
  42. package/dist/storage/in-memory.d.ts +11 -0
  43. package/dist/storage/in-memory.d.ts.map +1 -0
  44. package/dist/storage/in-memory.js +35 -0
  45. package/dist/storage/in-memory.js.map +1 -0
  46. package/dist/storage/mysql.d.ts +19 -0
  47. package/dist/storage/mysql.d.ts.map +1 -0
  48. package/dist/storage/mysql.js +70 -0
  49. package/dist/storage/mysql.js.map +1 -0
  50. package/dist/storage/postgres.d.ts +21 -0
  51. package/dist/storage/postgres.d.ts.map +1 -0
  52. package/dist/storage/postgres.js +70 -0
  53. package/dist/storage/postgres.js.map +1 -0
  54. package/dist/storage/sqlite.d.ts +15 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -0
  56. package/dist/storage/sqlite.js +71 -0
  57. package/dist/storage/sqlite.js.map +1 -0
  58. package/package.json +51 -0
  59. package/src/auth/decode-token.test.ts +119 -0
  60. package/src/auth/decode-token.ts +138 -0
  61. package/src/auth/index.ts +16 -0
  62. package/src/auth/token-auth.test.ts +95 -0
  63. package/src/auth/token-auth.ts +55 -0
  64. package/src/connection.test.ts +339 -0
  65. package/src/connection.ts +204 -0
  66. package/src/index.ts +80 -0
  67. package/src/protocol.test.ts +29 -0
  68. package/src/protocol.ts +137 -0
  69. package/src/room-manager.ts +45 -0
  70. package/src/room.test.ts +175 -0
  71. package/src/room.ts +207 -0
  72. package/src/server.test.ts +223 -0
  73. package/src/server.ts +153 -0
  74. package/src/storage/chat-storage.ts +23 -0
  75. package/src/storage/db-types.d.ts +43 -0
  76. package/src/storage/in-memory.test.ts +96 -0
  77. package/src/storage/in-memory.ts +52 -0
  78. package/src/storage/mysql.ts +117 -0
  79. package/src/storage/postgres.ts +117 -0
  80. package/src/storage/sqlite.ts +120 -0
  81. package/tsconfig.json +11 -0
  82. package/vitest.config.ts +32 -0
package/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # @openlivesync/server
2
+
3
+ Node.js server package for OpenLiveSync. Provides WebSocket-based presence, live collaboration events, and chat with pluggable storage.
4
+
5
+ ## Features
6
+
7
+ - **Presence** — Track who’s in a room and arbitrary presence state (e.g. cursor, name, color). Join/leave and updates are broadcast to the room.
8
+ - **Broadcast** — Send collaboration events to all other clients in the same room.
9
+ - **Chat** — Room-based messages with optional persistence (in-memory, Postgres, MySQL, or SQLite).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @openlivesync/server
15
+ ```
16
+
17
+ For database-backed chat, install the driver you need (optional):
18
+
19
+ ```bash
20
+ # One or more, depending on which storage you use:
21
+ npm install pg
22
+ npm install mysql2
23
+ npm install better-sqlite3
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ **Standalone server** (HTTP + WebSocket on port 3000, default in-memory chat):
29
+
30
+ ```ts
31
+ import { createServer } from "@openlivesync/server";
32
+
33
+ const server = createServer({ port: 3000 });
34
+ // WebSocket endpoint: ws://localhost:3000/live
35
+ // HTTP GET / returns "openlivesync"
36
+ ```
37
+
38
+ **Attach to an existing HTTP server** (e.g. Express, Fastify):
39
+
40
+ ```ts
41
+ import http from "node:http";
42
+ import { createWebSocketServer } from "@openlivesync/server";
43
+
44
+ const httpServer = http.createServer((req, res) => {
45
+ res.writeHead(200, { "Content-Type": "text/plain" });
46
+ res.end("Hello");
47
+ });
48
+
49
+ createWebSocketServer(httpServer, { path: "/live" });
50
+
51
+ httpServer.listen(3000);
52
+ // WebSocket: ws://localhost:3000/live
53
+ ```
54
+
55
+ **Raw upgrade handler** (you control path and routing):
56
+
57
+ ```ts
58
+ import http from "node:http";
59
+ import { createWebSocketHandler } from "@openlivesync/server";
60
+
61
+ const server = http.createServer(/* ... */);
62
+ const handleUpgrade = createWebSocketHandler({ path: "/live" });
63
+ server.on("upgrade", handleUpgrade);
64
+ server.listen(3000);
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### `createServer(options?)`
70
+
71
+ Creates an `http.Server` that serves a simple root response and handles WebSocket upgrades. Returns the server with a `ws` property (the `WebSocketServer`).
72
+
73
+ - **Options**: `ServerOptions` (includes `port`, default `3000`, and all `WebSocketServerOptions`).
74
+
75
+ ### `createWebSocketServer(server, options?)`
76
+
77
+ Attaches WebSocket upgrade handling to an existing Node `http.Server`. Returns the `WebSocketServer` (e.g. for `wss.close()`).
78
+
79
+ - **Options**: `WebSocketServerOptions`.
80
+
81
+ ### `createWebSocketHandler(options?)`
82
+
83
+ Returns a function `(request, socket, head) => void` that you pass to `server.on("upgrade", handler)`. Use this when you want to handle the upgrade path yourself.
84
+
85
+ - **Options**: `WebSocketServerOptions`.
86
+
87
+ ### Options
88
+
89
+ | Option | Type | Default | Description |
90
+ |--------|------|---------|-------------|
91
+ | `path` | `string` | `"/live"` | WebSocket upgrade path. |
92
+ | `onAuth` | `(req) => Promise<UserInfo \| null>` | — | If provided, runs on each upgrade. Return `null` to reject the connection (401). Return `{ userId?, name?, email?, provider?, ... }` to attach user info to the connection. |
93
+ | `auth` | `AuthOptions` | — | Optional: decode/verify access tokens sent in `join_room`. Supports Google, Microsoft, and custom OAuth (see [Access token and OAuth](#access-token-and-oauth)). When tokens are decoded, `name`, `email`, and `provider` appear in presence and chat. |
94
+ | `presenceThrottleMs` | `number` | `100` | Minimum ms between presence updates per connection. |
95
+ | `chat` | `{ storage?, historyLimit? }` | — | Chat config. Omit `storage` to use in-memory. `historyLimit` is how many messages to send to new joiners (default `100`). |
96
+ | `port` | `number` | `3000` | Only for `createServer`: port to listen on. |
97
+
98
+ **Example with auth and custom path:**
99
+
100
+ ```ts
101
+ import { createWebSocketServer } from "@openlivesync/server";
102
+ import type { UserInfo } from "@openlivesync/server";
103
+ import type { IncomingMessage } from "node:http";
104
+
105
+ createWebSocketServer(httpServer, {
106
+ path: "/collab",
107
+ presenceThrottleMs: 50,
108
+ onAuth: async (req: IncomingMessage): Promise<UserInfo | null> => {
109
+ const token = req.headers["authorization"]?.replace(/^Bearer\s+/i, "");
110
+ if (!token) return null;
111
+ const user = await myAuthService.verify(token); // your logic
112
+ return user ? { userId: user.id, ...user } : null;
113
+ },
114
+ chat: { historyLimit: 200 },
115
+ });
116
+ ```
117
+
118
+ ### Access token and OAuth
119
+
120
+ Clients can send an optional **access token** in the `join_room` message payload. If the server is configured with `auth`, it will decode (and optionally verify) the JWT and attach **name**, **email**, and **provider** to the connection. These appear in `PresenceEntry` and in chat messages so other clients can show who is in the room.
121
+
122
+ **Supported providers:** Google, Microsoft, and custom OAuth (JWKS URL or decode-only).
123
+
124
+ ```ts
125
+ import { createWebSocketServer } from "@openlivesync/server";
126
+
127
+ // Decode only (no verification) — e.g. for dev or trusted tokens
128
+ createWebSocketServer(httpServer, {
129
+ auth: {},
130
+ });
131
+
132
+ // Google: verify with Google JWKS; optional clientId to validate audience
133
+ createWebSocketServer(httpServer, {
134
+ auth: { google: { clientId: "your-google-client-id.apps.googleusercontent.com" } },
135
+ });
136
+
137
+ // Microsoft: verify with tenant JWKS
138
+ createWebSocketServer(httpServer, {
139
+ auth: { microsoft: { tenantId: "your-tenant-id", clientId: "your-client-id" } },
140
+ });
141
+
142
+ // Custom: JWKS URL or decode-only
143
+ createWebSocketServer(httpServer, {
144
+ auth: {
145
+ custom: { jwksUrl: "https://your-issuer/.well-known/jwks.json", issuer: "https://your-issuer" },
146
+ // or decode-only (no verification): custom: { decodeOnly: true }
147
+ },
148
+ });
149
+ ```
150
+
151
+ #### Token once at connect (recommended)
152
+
153
+ Use the access token **only once** when the client connects; the server then recognizes the connection for its lifetime and you do not send the token again in `join_room`.
154
+
155
+ 1. Use **`createTokenAuth(authOptions)`** as your `onAuth` function. It reads the token from the upgrade request (query param `access_token` or header `Authorization: Bearer <token>`), decodes it with `decodeAccessToken`, and returns `UserInfo`. The connection gets identity (userId, name, email, provider) at connect time.
156
+ 2. **Client**: Send the token only at connect (e.g. use `getAuthToken` in client config so the token is appended to the WebSocket URL as `?access_token=...`). Do **not** pass `accessToken` to `joinRoom` or `useRoom` when using this flow.
157
+ 3. If the connection already has identity (from `onAuth`), the server ignores any `accessToken` in `join_room` and does not overwrite it.
158
+
159
+ Browser WebSocket cannot send custom headers, so in the browser the token is typically sent as a query param (`access_token`). The default `tokenFromRequest` in `createTokenAuth` supports both query and `Authorization` header (e.g. for Node clients or proxies).
160
+
161
+ ```ts
162
+ import { createWebSocketServer, createTokenAuth } from "@openlivesync/server";
163
+
164
+ const authOptions = { google: { clientId: "your-client-id.apps.googleusercontent.com" } };
165
+ createWebSocketServer(httpServer, {
166
+ onAuth: createTokenAuth(authOptions),
167
+ });
168
+ ```
169
+
170
+ You can also use `decodeAccessToken(token, authOptions)` or `createTokenAuth(authOptions)` from the package. Exported types: `AuthOptions`, `DecodedToken`, `CreateTokenAuthOptions`, `AuthGoogleConfig`, `AuthMicrosoftConfig`, `AuthCustomConfig`.
171
+
172
+ ## Chat storage
173
+
174
+ Chat history can be in-memory (default) or backed by Postgres, MySQL, or SQLite. Pass a `ChatStorage` instance in `chat.storage`. All adapters implement `getHistory(roomId, limit?, offset?)`; messages are returned **oldest first**, and `limit`/`offset` support pagination.
175
+
176
+ ### In-memory (default)
177
+
178
+ No extra install. Keeps the last N messages per room in process memory.
179
+
180
+ ```ts
181
+ import {
182
+ createWebSocketServer,
183
+ createInMemoryChatStorage,
184
+ } from "@openlivesync/server";
185
+
186
+ const storage = createInMemoryChatStorage({ historyLimit: 100 });
187
+ createWebSocketServer(server, {
188
+ chat: { storage, historyLimit: 100 },
189
+ });
190
+ ```
191
+
192
+ ### Postgres
193
+
194
+ Requires `pg`. Creates table `openlivesync_chat` if it doesn’t exist.
195
+
196
+ ```ts
197
+ import {
198
+ createWebSocketServer,
199
+ createPostgresChatStorage,
200
+ } from "@openlivesync/server";
201
+
202
+ const storage = await createPostgresChatStorage(
203
+ { connectionString: process.env.DATABASE_URL },
204
+ { tableName: "openlivesync_chat", historyLimit: 100 }
205
+ );
206
+ createWebSocketServer(server, { chat: { storage, historyLimit: 100 } });
207
+ ```
208
+
209
+ Connection config can be a string or an object:
210
+
211
+ ```ts
212
+ await createPostgresChatStorage(
213
+ { host: "localhost", port: 5432, database: "app", user: "app", password: "secret" },
214
+ { tableName: "my_chat", historyLimit: 200 }
215
+ );
216
+ ```
217
+
218
+ ### MySQL
219
+
220
+ Requires `mysql2`. Creates the chat table if it doesn’t exist.
221
+
222
+ ```ts
223
+ import {
224
+ createWebSocketServer,
225
+ createMySQLChatStorage,
226
+ } from "@openlivesync/server";
227
+
228
+ const storage = await createMySQLChatStorage(
229
+ {
230
+ host: "localhost",
231
+ port: 3306,
232
+ database: "app",
233
+ user: "app",
234
+ password: "secret",
235
+ },
236
+ { tableName: "openlivesync_chat", historyLimit: 100 }
237
+ );
238
+ createWebSocketServer(server, { chat: { storage, historyLimit: 100 } });
239
+ ```
240
+
241
+ ### SQLite
242
+
243
+ Requires `better-sqlite3`. Pass a file path or `{ filename: "path/to/db.sqlite" }`.
244
+
245
+ ```ts
246
+ import {
247
+ createWebSocketServer,
248
+ createSQLiteChatStorage,
249
+ } from "@openlivesync/server";
250
+
251
+ const storage = createSQLiteChatStorage("./data/chat.sqlite", {
252
+ tableName: "openlivesync_chat",
253
+ historyLimit: 100,
254
+ });
255
+ createWebSocketServer(server, { chat: { storage, historyLimit: 100 } });
256
+ ```
257
+
258
+ ### Custom storage
259
+
260
+ Implement the `ChatStorage` interface and pass it as `chat.storage`:
261
+
262
+ - **`append(roomId, message)`** — Persist a chat message.
263
+ - **`getHistory(roomId, limit?, offset?)`** — Return messages for the room, **oldest first**. Use `limit` and `offset` for pagination (e.g. `getHistory(roomId, 20, 40)` returns the third page of 20 messages). Defaults: `limit` from adapter config, `offset` = 0.
264
+ - **`close()`** — Optional cleanup.
265
+
266
+ ```ts
267
+ import type { ChatStorage, ChatMessageInput } from "@openlivesync/server";
268
+ import type { StoredChatMessage } from "@openlivesync/server";
269
+
270
+ const myStorage: ChatStorage = {
271
+ async append(roomId: string, message: ChatMessageInput): Promise<void> {
272
+ // persist to your backend
273
+ },
274
+ async getHistory(
275
+ roomId: string,
276
+ limit?: number,
277
+ offset?: number
278
+ ): Promise<StoredChatMessage[]> {
279
+ // return messages oldest first; use limit/offset for pagination
280
+ return [];
281
+ },
282
+ async close(): Promise<void> {
283
+ // optional cleanup
284
+ },
285
+ };
286
+ ```
287
+
288
+ ## Wire protocol
289
+
290
+ Clients connect over WebSocket and send/receive JSON messages with a `type` field. The server handles these message types:
291
+
292
+ ### Client → Server
293
+
294
+ | `type` | Purpose |
295
+ |--------|--------|
296
+ | `join_room` | Join a room. Payload: `{ roomId, presence?, accessToken? }`. If `accessToken` is sent and server has `auth` (or decode-only), the server decodes it and attaches name, email, provider to the connection; these appear in presence and chat. |
297
+ | `leave_room` | Leave current room. Payload: `{ roomId? }` (optional). |
298
+ | `update_presence` | Update presence. Payload: `{ presence }`. Throttled per connection. |
299
+ | `broadcast_event` | Send collaboration event. Payload: `{ event, payload? }`. |
300
+ | `send_chat` | Send chat message. Payload: `{ message, metadata? }`. |
301
+
302
+ ### Server → Client
303
+
304
+ | `type` | Purpose |
305
+ |--------|--------|
306
+ | `room_joined` | Sent after join. Payload: `{ roomId, connectionId, presence, chatHistory? }`. Each entry in `presence` may include `userId`, `name`, `email`, `provider` when set from auth/token. |
307
+ | `presence_updated` | Broadcast. Payload: `{ roomId, joined?, left?, updated? }`. `joined`/`updated` entries may include `name`, `email`, `provider`. |
308
+ | `broadcast_event` | Relayed event. Payload: `{ roomId, connectionId, userId?, event, payload? }`. |
309
+ | `chat_message` | Chat message. Payload: `{ roomId, connectionId, userId?, message, metadata? }`. |
310
+ | `error` | Error. Payload: `{ code, message }`. |
311
+
312
+ Presence is an arbitrary JSON object per connection (e.g. `{ cursor: { x, y }, name, color }`). The server does not interpret it; it only stores and broadcasts it.
313
+
314
+ Use the same message types and constants in your client; they are exported from this package (see **Types** below).
315
+
316
+ ## Types and constants
317
+
318
+ For building a compatible client or typing your app, the package exports:
319
+
320
+ - **Server API**: `createServer`, `createWebSocketServer`, `createWebSocketHandler`, `ServerOptions`, `WebSocketServerOptions`, `ChatOptions`.
321
+ - **Auth**: `decodeAccessToken`, `AuthOptions`, `DecodedToken`, `AuthGoogleConfig`, `AuthMicrosoftConfig`, `AuthCustomConfig`.
322
+ - **Protocol types**: `Presence`, `UserInfo`, `ClientMessage`, `ServerMessage`, `JoinRoomPayload`, `RoomJoinedPayload`, `PresenceEntry`, `StoredChatMessage`, `ChatMessageInput`, etc.
323
+ - **Message type constants**: `MSG_JOIN_ROOM`, `MSG_LEAVE_ROOM`, `MSG_UPDATE_PRESENCE`, `MSG_BROADCAST_EVENT`, `MSG_SEND_CHAT`, `MSG_ROOM_JOINED`, `MSG_PRESENCE_UPDATED`, `MSG_BROADCAST_EVENT_RELAY`, `MSG_CHAT_MESSAGE`, `MSG_ERROR`.
324
+ - **Storage**: `ChatStorage`, `createInMemoryChatStorage`, `createPostgresChatStorage`, `createMySQLChatStorage`, `createSQLiteChatStorage`, and their option types.
325
+
326
+ Example (client or shared code):
327
+
328
+ ```ts
329
+ import type { ClientMessage, ServerMessage, Presence } from "@openlivesync/server";
330
+ import { MSG_JOIN_ROOM, MSG_ROOM_JOINED } from "@openlivesync/server";
331
+ ```
332
+
333
+ ## Scripts
334
+
335
+ - `npm run build` — Compile TypeScript to `dist/`.
336
+ - `npm run clean` — Remove `dist/`.
337
+ - `npm run test` — Run tests (Vitest).
338
+ - `npm run test:watch` — Run tests in watch mode.
339
+ - `npm run test:coverage` — Run tests with coverage (V8). Reports in `./coverage` (text summary in terminal, HTML in `coverage/index.html`, lcov for CI).
340
+
341
+ ## Testing
342
+
343
+ Tests use [Vitest](https://vitest.dev/) and live next to the source as `*.test.ts` files.
344
+
345
+ ```bash
346
+ npm run test
347
+ ```
348
+
349
+ Coverage (V8) is available via:
350
+
351
+ ```bash
352
+ npm run test:coverage
353
+ ```
354
+
355
+ Reports are written to `./coverage` (text in the terminal, `coverage/index.html` for a browseable report, and `coverage/lcov.info` for CI). Test files, config, and type declarations are excluded from coverage.
356
+
357
+ Coverage includes:
358
+
359
+ - **Protocol** — Message type constants.
360
+ - **In-memory storage** — `append`, `getHistory` (limit, offset), room isolation, cap at `historyLimit`.
361
+ - **Room & RoomManager** — Join (room_joined with presence and chat history), leave (presence_updated), updatePresence, broadcastEvent, sendChat; getOrCreate, get, removeIfEmpty.
362
+ - **WebSocket server** — Integration tests: connect, send `join_room`, receive `room_joined`; two clients in same room, send chat, second client receives `chat_message`.
363
+
364
+ ## License
365
+
366
+ See repository root.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Decode and optionally verify OAuth/OpenID access tokens (JWT).
3
+ * Supports Google, Microsoft, and custom providers (JWKS or decode-only).
4
+ */
5
+ export interface DecodedToken {
6
+ sub: string;
7
+ email?: string;
8
+ name?: string;
9
+ iss?: string;
10
+ provider?: string;
11
+ }
12
+ export interface AuthGoogleConfig {
13
+ /** Optional: validate audience (client ID). */
14
+ clientId?: string;
15
+ }
16
+ export interface AuthMicrosoftConfig {
17
+ tenantId: string;
18
+ /** Optional: validate audience (client ID). */
19
+ clientId?: string;
20
+ }
21
+ export interface AuthCustomConfig {
22
+ /** JWKS URL for signature verification. */
23
+ jwksUrl?: string;
24
+ /** Expected issuer (iss claim). */
25
+ issuer?: string;
26
+ /** If true, only decode payload (no verification). Use for dev or trusted tokens. */
27
+ decodeOnly?: boolean;
28
+ }
29
+ export interface AuthOptions {
30
+ google?: AuthGoogleConfig;
31
+ microsoft?: AuthMicrosoftConfig;
32
+ custom?: AuthCustomConfig;
33
+ }
34
+ /**
35
+ * Decode (and optionally verify) an access token JWT.
36
+ * Returns normalized claims (sub, email, name, provider) or null on failure.
37
+ * If no auth options are provided or a provider uses decodeOnly, only decoding is performed (no signature verification).
38
+ */
39
+ export declare function decodeAccessToken(token: string, options?: AuthOptions): Promise<DecodedToken | null>;
40
+ //# sourceMappingURL=decode-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-token.d.ts","sourceRoot":"","sources":["../../src/auth/decode-token.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAwCD;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkD9B"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Decode and optionally verify OAuth/OpenID access tokens (JWT).
3
+ * Supports Google, Microsoft, and custom providers (JWKS or decode-only).
4
+ */
5
+ import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
6
+ const GOOGLE_ISSUER = "https://accounts.google.com";
7
+ const GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
8
+ function normalizePayload(payload) {
9
+ const sub = typeof payload.sub === "string" ? payload.sub : "";
10
+ const email = typeof payload.email === "string"
11
+ ? payload.email
12
+ : typeof payload.preferred_username === "string"
13
+ ? payload.preferred_username
14
+ : undefined;
15
+ const name = typeof payload.name === "string"
16
+ ? payload.name
17
+ : [payload.given_name, payload.family_name]
18
+ .filter((x) => typeof x === "string")
19
+ .join(" ")
20
+ .trim() || undefined;
21
+ const iss = typeof payload.iss === "string" ? payload.iss : undefined;
22
+ let provider;
23
+ if (iss) {
24
+ if (iss.includes("accounts.google.com"))
25
+ provider = "google";
26
+ else if (iss.includes("login.microsoftonline.com"))
27
+ provider = "microsoft";
28
+ else
29
+ provider = "custom";
30
+ }
31
+ return { sub, email, name, iss, provider };
32
+ }
33
+ function getGoogleJwksUrl() {
34
+ return new URL(GOOGLE_JWKS_URL);
35
+ }
36
+ function getMicrosoftJwksUrl(tenantId) {
37
+ return new URL(`https://login.microsoftonline.com/${tenantId}/discovery/v2.0/keys`);
38
+ }
39
+ /**
40
+ * Decode (and optionally verify) an access token JWT.
41
+ * Returns normalized claims (sub, email, name, provider) or null on failure.
42
+ * If no auth options are provided or a provider uses decodeOnly, only decoding is performed (no signature verification).
43
+ */
44
+ export async function decodeAccessToken(token, options) {
45
+ if (!token || typeof token !== "string")
46
+ return null;
47
+ try {
48
+ const decoded = decodeJwt(token);
49
+ const payload = decoded;
50
+ const iss = typeof payload.iss === "string" ? payload.iss : undefined;
51
+ // Determine which provider config applies and whether to verify
52
+ let verifyUrl = null;
53
+ let issuer;
54
+ let audience;
55
+ if (options?.google && iss?.includes("accounts.google.com")) {
56
+ verifyUrl = getGoogleJwksUrl();
57
+ issuer = GOOGLE_ISSUER;
58
+ audience = options.google.clientId;
59
+ }
60
+ else if (options?.microsoft &&
61
+ iss?.includes("login.microsoftonline.com")) {
62
+ const tenantId = options.microsoft.tenantId;
63
+ verifyUrl = getMicrosoftJwksUrl(tenantId);
64
+ issuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;
65
+ audience = options.microsoft.clientId;
66
+ }
67
+ else if (options?.custom) {
68
+ if (options.custom.decodeOnly) {
69
+ return normalizePayload(payload);
70
+ }
71
+ if (options.custom.jwksUrl) {
72
+ verifyUrl = new URL(options.custom.jwksUrl);
73
+ issuer = options.custom.issuer;
74
+ }
75
+ }
76
+ // No verification configured: decode only
77
+ if (!verifyUrl) {
78
+ return normalizePayload(payload);
79
+ }
80
+ const JWKS = createRemoteJWKSet(verifyUrl);
81
+ const verifyOptions = {};
82
+ if (issuer)
83
+ verifyOptions.issuer = issuer;
84
+ if (audience)
85
+ verifyOptions.audience = audience;
86
+ const { payload: verified } = await jwtVerify(token, JWKS, verifyOptions);
87
+ return normalizePayload(verified);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ //# sourceMappingURL=decode-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-token.js","sourceRoot":"","sources":["../../src/auth/decode-token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Auth module: decode and verify OAuth/OpenID access tokens.
3
+ */
4
+ export { decodeAccessToken, type DecodedToken, type AuthOptions, type AuthGoogleConfig, type AuthMicrosoftConfig, type AuthCustomConfig, } from "./decode-token.js";
5
+ export { createTokenAuth, type CreateTokenAuthOptions, } from "./token-auth.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,sBAAsB,GAC5B,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Auth module: decode and verify OAuth/OpenID access tokens.
3
+ */
4
+ export { decodeAccessToken, } from "./decode-token.js";
5
+ export { createTokenAuth, } from "./token-auth.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,iBAAiB,GAMlB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,GAEhB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Helper to use access token only at WebSocket connect (query or header).
3
+ * Returns an onAuth function that reads the token from the request and decodes it.
4
+ */
5
+ import type { IncomingMessage } from "node:http";
6
+ import type { UserInfo } from "../protocol.js";
7
+ import type { AuthOptions } from "./decode-token.js";
8
+ export interface CreateTokenAuthOptions {
9
+ /** Custom way to extract token from the upgrade request. Default: query access_token or Authorization Bearer. */
10
+ tokenFromRequest?: (req: IncomingMessage) => string | null;
11
+ }
12
+ /**
13
+ * Returns an onAuth function that reads the access token from the request (query or header),
14
+ * decodes it with decodeAccessToken, and returns UserInfo (userId, name, email, provider).
15
+ * Use this so the token is used only at connect; the connection is then recognized for its lifetime.
16
+ */
17
+ export declare function createTokenAuth(authOptions: AuthOptions, options?: CreateTokenAuthOptions): (request: IncomingMessage) => Promise<UserInfo | null>;
18
+ //# sourceMappingURL=token-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-auth.d.ts","sourceRoot":"","sources":["../../src/auth/token-auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkBrD,MAAM,WAAW,sBAAsB;IACrC,iHAAiH;IACjH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,GAAG,IAAI,CAAC;CAC5D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAexD"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Helper to use access token only at WebSocket connect (query or header).
3
+ * Returns an onAuth function that reads the token from the request and decodes it.
4
+ */
5
+ import { decodeAccessToken } from "./decode-token.js";
6
+ function defaultTokenFromRequest(req) {
7
+ const url = req.url ?? "";
8
+ try {
9
+ const u = new URL(url, "http://localhost");
10
+ const fromQuery = u.searchParams.get("access_token");
11
+ if (fromQuery)
12
+ return fromQuery;
13
+ }
14
+ catch {
15
+ // ignore URL parse errors
16
+ }
17
+ const auth = req.headers.authorization;
18
+ if (typeof auth === "string" && /^Bearer\s+/i.test(auth)) {
19
+ return auth.replace(/^Bearer\s+/i, "").trim() || null;
20
+ }
21
+ return null;
22
+ }
23
+ /**
24
+ * Returns an onAuth function that reads the access token from the request (query or header),
25
+ * decodes it with decodeAccessToken, and returns UserInfo (userId, name, email, provider).
26
+ * Use this so the token is used only at connect; the connection is then recognized for its lifetime.
27
+ */
28
+ export function createTokenAuth(authOptions, options) {
29
+ const tokenFromRequest = options?.tokenFromRequest ?? defaultTokenFromRequest;
30
+ return async (request) => {
31
+ const token = tokenFromRequest(request);
32
+ if (!token)
33
+ return null;
34
+ const decoded = await decodeAccessToken(token, authOptions);
35
+ if (!decoded)
36
+ return null;
37
+ return {
38
+ userId: decoded.sub,
39
+ name: decoded.name,
40
+ email: decoded.email,
41
+ provider: decoded.provider,
42
+ };
43
+ };
44
+ }
45
+ //# sourceMappingURL=token-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-auth.js","sourceRoot":"","sources":["../../src/auth/token-auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,SAAS,uBAAuB,CAAC,GAAoB;IACnD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACrD,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;IAC5B,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;IACxD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAOD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,WAAwB,EACxB,OAAgC;IAEhC,MAAM,gBAAgB,GAAG,OAAO,EAAE,gBAAgB,IAAI,uBAAuB,CAAC;IAE9E,OAAO,KAAK,EAAE,OAAwB,EAA4B,EAAE;QAClE,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAC5D,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1B,OAAO;YACL,MAAM,EAAE,OAAO,CAAC,GAAG;YACnB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Wraps a WebSocket: parse messages, throttle presence, dispatch to room.
3
+ */
4
+ import type { WebSocket } from "ws";
5
+ import type { RoomManager } from "./room-manager.js";
6
+ import type { AuthOptions } from "./auth/index.js";
7
+ export interface ConnectionOptions {
8
+ connectionId: string;
9
+ userId?: string;
10
+ userName?: string;
11
+ userEmail?: string;
12
+ provider?: string;
13
+ presenceThrottleMs: number;
14
+ roomManager: RoomManager;
15
+ auth?: AuthOptions;
16
+ }
17
+ export declare class Connection {
18
+ private readonly ws;
19
+ private readonly connectionId;
20
+ private userId;
21
+ private userName;
22
+ private userEmail;
23
+ private provider;
24
+ private readonly presenceThrottleMs;
25
+ private readonly roomManager;
26
+ private readonly auth;
27
+ private currentRoomId;
28
+ private lastPresenceUpdate;
29
+ private closed;
30
+ constructor(ws: WebSocket, options: ConnectionOptions);
31
+ private send;
32
+ private handleMessage;
33
+ private dispatch;
34
+ private handleClose;
35
+ }
36
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAcpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAmBnD,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAC/B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,MAAM,CAAS;gBAEX,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,iBAAiB;IAerD,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,aAAa;YA+BP,QAAQ;IAgFtB,OAAO,CAAC,WAAW;CAQpB"}