@signe/room 2.9.0 → 2.9.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/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@signe/room",
3
- "version": "2.9.0",
4
- "description": "",
3
+ "version": "2.9.2",
4
+ "description": "PartyKit room primitives with synchronized state, sessions, guards, and HTTP handlers.",
5
5
  "main": "./dist/index.js",
6
- "keywords": [],
6
+ "keywords": [
7
+ "partykit",
8
+ "realtime",
9
+ "websocket",
10
+ "cloudflare",
11
+ "typescript"
12
+ ],
7
13
  "author": "Samuel Ronce",
8
14
  "type": "module",
9
15
  "exports": {
@@ -17,7 +23,7 @@
17
23
  "dset": "^3.1.3",
18
24
  "partysocket": "^1.0.1",
19
25
  "zod": "^3.23.8",
20
- "@signe/sync": "2.9.0"
26
+ "@signe/sync": "2.9.2"
21
27
  },
22
28
  "publishConfig": {
23
29
  "access": "public"
package/readme.md CHANGED
@@ -123,7 +123,7 @@ The `@Request` decorator allows you to handle HTTP requests with specific routes
123
123
 
124
124
  ```ts
125
125
  import { z } from "zod";
126
- import { Room, Request, RequestGuard, ServerResponse } from "@signe/room";
126
+ import { Guard, Room, Request, ServerResponse } from "@signe/room";
127
127
 
128
128
  @Room({
129
129
  path: "api"
@@ -169,11 +169,12 @@ class ApiRoom {
169
169
  }
170
170
  ```
171
171
 
172
- Request handler methods receive these parameters:
173
- 1. `req`: The original Party.Request object
174
- 2. `body`: The validated request body (if validation schema was provided)
175
- 3. `params`: An object containing any path parameters
176
- 4. `room`: The Party.Room instance
172
+ Request handler methods receive:
173
+
174
+ 1. `req`: the original `Party.Request`, extended with `req.params` and
175
+ `req.data` when a validation schema is provided.
176
+ 2. `res`: a `ServerResponse` helper for JSON, text, redirects, and common
177
+ error responses.
177
178
 
178
179
  You can return:
179
180
  - A Response object for complete control
package/src/jwt.ts CHANGED
@@ -132,9 +132,7 @@ export class JWTAuth {
132
132
  };
133
133
 
134
134
  // Encode header and payload
135
- // @ts-expect-error - TS doesn't have a built-in TextEncoder
136
135
  const encodedHeader: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
137
- // @ts-expect-error - TS doesn't have a built-in TextEncoder
138
136
  const encodedPayload: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
139
137
 
140
138
  // Create signature base
@@ -174,9 +172,7 @@ export class JWTAuth {
174
172
 
175
173
  // Decode header and payload
176
174
  try {
177
- // @ts-expect-error - TS doesn't have a built-in TextDecoder
178
175
  const header: JWTHeader = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
179
- // @ts-expect-error - TS doesn't have a built-in TextDecoder
180
176
  const payload: JWTPayload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
181
177
 
182
178
  // Check algorithm
@@ -214,4 +210,4 @@ export class JWTAuth {
214
210
  throw new Error('Token verification failed: Unknown error');
215
211
  }
216
212
  }
217
- }
213
+ }
package/src/server.ts CHANGED
@@ -615,7 +615,10 @@ export class Server implements Party.Server {
615
615
  return;
616
616
  }
617
617
 
618
- const sessionExpiryTime = subRoom.constructor.sessionExpiryTime;
618
+ const sessionExpiryTime =
619
+ subRoom.sessionExpiryTime ??
620
+ subRoom.constructor.sessionExpiryTime ??
621
+ 5 * 60 * 1000;
619
622
  await this.garbageCollector({ sessionExpiryTime });
620
623
 
621
624
  // Check room guards
@@ -1289,9 +1292,7 @@ export class Server implements Party.Server {
1289
1292
 
1290
1293
  const url = new URL(req.url);
1291
1294
  const method = req.method;
1292
- let pathname = url.pathname;
1293
-
1294
- pathname = '/' + pathname.split('/').slice(4).join('/');
1295
+ const pathname = this.normalizeRequestPath(url.pathname);
1295
1296
 
1296
1297
  // Check each registered handler
1297
1298
  for (const [routeKey, handler] of requestHandlers.entries()) {
@@ -1325,16 +1326,18 @@ export class Server implements Party.Server {
1325
1326
  if (handler.bodyValidation && ['POST', 'PUT', 'PATCH'].includes(method)) {
1326
1327
  try {
1327
1328
  const contentType = req.headers.get('content-type') || '';
1328
- if (contentType.includes('application/json')) {
1329
- const body = await req.json();
1330
- const validation = handler.bodyValidation.safeParse(body);
1331
- if (!validation.success) {
1332
- return res.badRequest("Invalid request body", {
1333
- details: validation.error
1334
- });
1335
- }
1336
- bodyData = validation.data;
1329
+ if (!contentType.includes('application/json')) {
1330
+ return res.badRequest("Content-Type must be application/json");
1331
+ }
1332
+
1333
+ const body = await req.json();
1334
+ const validation = handler.bodyValidation.safeParse(body);
1335
+ if (!validation.success) {
1336
+ return res.badRequest("Invalid request body", {
1337
+ details: validation.error
1338
+ });
1337
1339
  }
1340
+ bodyData = validation.data;
1338
1341
  } catch (error) {
1339
1342
  return res.badRequest("Failed to parse request body");
1340
1343
  }
@@ -1372,14 +1375,7 @@ export class Server implements Party.Server {
1372
1375
  * @returns {boolean} True if the paths match
1373
1376
  */
1374
1377
  private pathMatches(requestPath: string, handlerPath: string): boolean {
1375
- // Convert handler path pattern to regex
1376
- // Replace :param with named capture groups
1377
- const pathRegexString = handlerPath
1378
- .replace(/\//g, '\\/') // Escape slashes
1379
- .replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
1380
-
1381
- const pathRegex = new RegExp(`^${pathRegexString}`);
1382
- return pathRegex.test(requestPath);
1378
+ return this.pathPatternToRegex(handlerPath).test(requestPath);
1383
1379
  }
1384
1380
 
1385
1381
  /**
@@ -1401,12 +1397,7 @@ export class Server implements Party.Server {
1401
1397
  }
1402
1398
  });
1403
1399
 
1404
- // Extract parameter values from request path
1405
- const pathRegexString = handlerPath
1406
- .replace(/\//g, '\\/') // Escape slashes
1407
- .replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
1408
-
1409
- const pathRegex = new RegExp(`^${pathRegexString}`);
1400
+ const pathRegex = this.pathPatternToRegex(handlerPath);
1410
1401
  const matches = requestPath.match(pathRegex);
1411
1402
 
1412
1403
  if (matches && matches.length > 1) {
@@ -1419,6 +1410,28 @@ export class Server implements Party.Server {
1419
1410
  return params;
1420
1411
  }
1421
1412
 
1413
+ private normalizeRequestPath(pathname: string): string {
1414
+ const parts = pathname.split('/').filter(Boolean);
1415
+ if (parts[0] === 'parties' && parts.length >= 3) {
1416
+ const routePath = parts.slice(3).join('/');
1417
+ return routePath ? `/${routePath}` : '/';
1418
+ }
1419
+
1420
+ return pathname || '/';
1421
+ }
1422
+
1423
+ private pathPatternToRegex(handlerPath: string): RegExp {
1424
+ const segments = handlerPath.split('/').map(segment => {
1425
+ if (segment.startsWith(':')) {
1426
+ return '([^/]+)';
1427
+ }
1428
+
1429
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1430
+ });
1431
+
1432
+ return new RegExp(`^${segments.join('/')}$`);
1433
+ }
1434
+
1422
1435
  /**
1423
1436
  * @method handleShardRequest
1424
1437
  * @private
package/src/world.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { signal } from "@signe/reactive";
2
- import { Room, Action, Guard, Request } from "./decorators";
2
+ import { Room, Guard, Request } from "./decorators";
3
3
  import { sync, id, persist } from "@signe/sync";
4
4
  import { z } from "zod";
5
5
  import * as Party from "./types/party";
6
6
  import { guardManageWorld } from "./world.guard";
7
- import { response } from "./utils";
8
7
  import { RoomInterceptorPacket, RoomOnJoin } from "./interfaces";
9
8
  import { ServerResponse } from "./request/response";
10
9
 
@@ -24,14 +23,8 @@ const RoomConfigSchema = z.object({
24
23
  maxShards: z.number().int().positive().optional(),
25
24
  });
26
25
 
27
- const RegisterShardSchema = z.object({
28
- shardId: z.string(),
29
- roomId: z.string(),
30
- url: z.string().url(),
31
- maxConnections: z.number().int().positive(),
32
- });
33
-
34
26
  const UpdateShardStatsSchema = z.object({
27
+ shardId: z.string(),
35
28
  connections: z.number().int().min(0),
36
29
  status: z.enum(['active', 'maintenance', 'draining']).optional(),
37
30
  });
@@ -139,8 +132,15 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
139
132
  method: 'POST',
140
133
  })
141
134
  @Guard([guardManageWorld])
142
- async registerRoom(req: Party.Request) {
143
- const roomConfig: z.infer<typeof RoomConfigSchema> = await req.json();
135
+ async registerRoom(req: Party.Request, res?: ServerResponse) {
136
+ const roomConfigResult = RoomConfigSchema.safeParse(await req.json());
137
+ if (!roomConfigResult.success) {
138
+ return res?.badRequest("Invalid room configuration", {
139
+ details: roomConfigResult.error
140
+ });
141
+ }
142
+
143
+ const roomConfig = roomConfigResult.data;
144
144
  const roomId = roomConfig.name;
145
145
 
146
146
  if (!this.rooms()[roomId]) {
@@ -178,7 +178,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
178
178
  })
179
179
  @Guard([guardManageWorld])
180
180
  async updateShardStats(req: Party.Request, res: ServerResponse) {
181
- const body: { shardId: string; connections: number; status: ShardStatus } = await req.json();
181
+ const bodyResult = UpdateShardStatsSchema.safeParse(await req.json());
182
+ if (!bodyResult.success) {
183
+ return res.badRequest("Invalid shard statistics", {
184
+ details: bodyResult.error
185
+ });
186
+ }
187
+
188
+ const body = bodyResult.data;
182
189
  const { shardId, connections, status } = body;
183
190
  const shard = this.shards()[shardId];
184
191
 
@@ -199,7 +206,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
199
206
  })
200
207
  @Guard([guardManageWorld])
201
208
  async scaleRoom(req: Party.Request, res: ServerResponse) {
202
- const data: z.infer<typeof ScaleRoomSchema> = await req.json();
209
+ const dataResult = ScaleRoomSchema.safeParse(await req.json());
210
+ if (!dataResult.success) {
211
+ return res.badRequest("Invalid scale request", {
212
+ details: dataResult.error
213
+ });
214
+ }
215
+
216
+ const data = dataResult.data;
203
217
  const { targetShardCount, shardTemplate, roomId } = data;
204
218
 
205
219
  // Validate room exists
@@ -235,11 +249,6 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
235
249
  })
236
250
  .slice(0, previousShardCount - targetShardCount);
237
251
 
238
- // Remove the selected shards
239
- const shardsToKeep = roomShards.filter(
240
- shard => !shardsToRemove.some(s => s.id === shard.id)
241
- );
242
-
243
252
  // Update shards
244
253
  for (const shard of shardsToRemove) {
245
254
  delete this.shards()[shard.id];
@@ -448,4 +457,4 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
448
457
  this.shards()[shardId] = newShard;
449
458
  return newShard;
450
459
  }
451
- }
460
+ }