@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.
- package/README.md +366 -0
- package/dist/auth/decode-token.d.ts +40 -0
- package/dist/auth/decode-token.d.ts.map +1 -0
- package/dist/auth/decode-token.js +93 -0
- package/dist/auth/decode-token.js.map +1 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token-auth.d.ts +18 -0
- package/dist/auth/token-auth.d.ts.map +1 -0
- package/dist/auth/token-auth.js +45 -0
- package/dist/auth/token-auth.js.map +1 -0
- package/dist/connection.d.ts +36 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +170 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +135 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +17 -0
- package/dist/protocol.js.map +1 -0
- package/dist/room-manager.d.ts +19 -0
- package/dist/room-manager.d.ts.map +1 -0
- package/dist/room-manager.js +34 -0
- package/dist/room-manager.js.map +1 -0
- package/dist/room.d.ts +41 -0
- package/dist/room.d.ts.map +1 -0
- package/dist/room.js +150 -0
- package/dist/room.js.map +1 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +105 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/chat-storage.d.ts +19 -0
- package/dist/storage/chat-storage.d.ts.map +1 -0
- package/dist/storage/chat-storage.js +6 -0
- package/dist/storage/chat-storage.js.map +1 -0
- package/dist/storage/in-memory.d.ts +11 -0
- package/dist/storage/in-memory.d.ts.map +1 -0
- package/dist/storage/in-memory.js +35 -0
- package/dist/storage/in-memory.js.map +1 -0
- package/dist/storage/mysql.d.ts +19 -0
- package/dist/storage/mysql.d.ts.map +1 -0
- package/dist/storage/mysql.js +70 -0
- package/dist/storage/mysql.js.map +1 -0
- package/dist/storage/postgres.d.ts +21 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +70 -0
- package/dist/storage/postgres.js.map +1 -0
- package/dist/storage/sqlite.d.ts +15 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +71 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/package.json +51 -0
- package/src/auth/decode-token.test.ts +119 -0
- package/src/auth/decode-token.ts +138 -0
- package/src/auth/index.ts +16 -0
- package/src/auth/token-auth.test.ts +95 -0
- package/src/auth/token-auth.ts +55 -0
- package/src/connection.test.ts +339 -0
- package/src/connection.ts +204 -0
- package/src/index.ts +80 -0
- package/src/protocol.test.ts +29 -0
- package/src/protocol.ts +137 -0
- package/src/room-manager.ts +45 -0
- package/src/room.test.ts +175 -0
- package/src/room.ts +207 -0
- package/src/server.test.ts +223 -0
- package/src/server.ts +153 -0
- package/src/storage/chat-storage.ts +23 -0
- package/src/storage/db-types.d.ts +43 -0
- package/src/storage/in-memory.test.ts +96 -0
- package/src/storage/in-memory.ts +52 -0
- package/src/storage/mysql.ts +117 -0
- package/src/storage/postgres.ts +117 -0
- package/src/storage/sqlite.ts +120 -0
- package/tsconfig.json +11 -0
- 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 @@
|
|
|
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"}
|