@signe/room 2.3.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -453,7 +453,7 @@ declare class Server implements Server$1 {
453
453
  * @method onClose
454
454
  * @async
455
455
  * @param {Party.Connection} conn - The connection object of the disconnecting user.
456
- * @description Handles user disconnection, removing them from the room and triggering the onLeave event.
456
+ * @description Handles user disconnection, removing them from the room and triggering the onLeave event..
457
457
  * @returns {Promise<void>}
458
458
  *
459
459
  * @example
@@ -475,6 +475,16 @@ declare class Server implements Server$1 {
475
475
  * @returns {Promise<Response>} The response to return to the client
476
476
  */
477
477
  onRequest(req: Request$1): Promise<Response>;
478
+ /**
479
+ * @method handleSessionRestore
480
+ * @private
481
+ * @async
482
+ * @param {Party.Request} req - The HTTP request for session restore
483
+ * @param {ServerResponse} res - The response object
484
+ * @description Handles session restoration from transfer data, creates session from privateId
485
+ * @returns {Promise<Response>} The response to return to the client
486
+ */
487
+ private handleSessionRestore;
478
488
  /**
479
489
  * @method handleDirectRequest
480
490
  * @private
@@ -839,4 +849,57 @@ declare class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
839
849
  private createShard;
840
850
  }
841
851
 
842
- export { Action, ClientIo, Guard, MockConnection, Request, type RequestOptions, Room, RoomGuard, type RoomInterceptorPacket, type RoomMethods, type RoomOnJoin, type RoomOnLeave, type RoomOptions, Server, ServerIo, ServerResponse, Shard, type ShardOptions, WorldRoom, request, testRoom };
852
+ /**
853
+ * @description Factory function that creates a session guard with access to room storage
854
+ * @param {Party.Storage} storage - The room storage instance
855
+ * @returns {Function} - The guard function
856
+ *
857
+ * @example
858
+ * ```typescript
859
+ * import { createRequireSessionGuard } from "./session.guard";
860
+ *
861
+ * export class GameRoom {
862
+ * constructor(private room: Party.Room) {}
863
+ *
864
+ * @Action("sendMessage")
865
+ * @Guard([createRequireSessionGuard(this.room.storage)])
866
+ * async sendMessage(user: User, message: string, conn: Party.Connection) {
867
+ * // This action will only execute if the user has a valid session
868
+ * this.$broadcast({ type: "message", user, message });
869
+ * }
870
+ * }
871
+ * ```
872
+ */
873
+ declare function createRequireSessionGuard(storage: Storage$1): (sender: Connection, value: any) => Promise<boolean>;
874
+ /**
875
+ * @description Guard function that verifies if a user session exists (for room and request guards)
876
+ * @param {Party.Connection} sender - The connection object of the sender
877
+ * @param {any} value - The value/payload sent with the action or request
878
+ * @param {Party.Room} room - The room instance
879
+ * @returns {Promise<boolean>} - Returns true if session exists, false otherwise
880
+ *
881
+ * @example
882
+ * ```typescript
883
+ * import { requireSession } from "./session.guard";
884
+ *
885
+ * // For room guards
886
+ * @Room({
887
+ * path: "game-{id}",
888
+ * guards: [requireSession]
889
+ * })
890
+ * export class GameRoom {
891
+ * // Room implementation
892
+ * }
893
+ *
894
+ * // For request guards
895
+ * @Request({ path: '/api/data', method: 'GET' })
896
+ * @Guard([requireSession])
897
+ * async getData(req: Party.Request, res: ServerResponse) {
898
+ * // This request will only execute if the user has a valid session
899
+ * return res.success({ data: "protected data" });
900
+ * }
901
+ * ```
902
+ */
903
+ declare const requireSession: (sender: Connection, value: any, room: Room$1) => Promise<boolean>;
904
+
905
+ export { Action, ClientIo, Guard, MockConnection, Request, type RequestOptions, Room, RoomGuard, type RoomInterceptorPacket, type RoomMethods, type RoomOnJoin, type RoomOnLeave, type RoomOptions, Server, ServerIo, ServerResponse, Shard, type ShardOptions, WorldRoom, createRequireSessionGuard, request, requireSession, testRoom };
package/dist/index.js CHANGED
@@ -615,6 +615,101 @@ var Server = class {
615
615
  instance.$broadcast = (obj) => {
616
616
  return this.broadcast(obj, instance);
617
617
  };
618
+ instance.$sessionTransfer = async (userOrPublicId, targetRoomId) => {
619
+ let user;
620
+ let publicId = null;
621
+ const signal2 = this.getUsersProperty(instance);
622
+ if (!signal2) {
623
+ console.error("[sessionTransfer] `users` property not defined in the room.");
624
+ return null;
625
+ }
626
+ if (typeof userOrPublicId === "string") {
627
+ publicId = userOrPublicId;
628
+ user = signal2()[publicId];
629
+ if (!user) {
630
+ console.error(`[sessionTransfer] User with publicId ${publicId} not found.`);
631
+ return null;
632
+ }
633
+ } else {
634
+ user = userOrPublicId;
635
+ const users = signal2();
636
+ for (const [id2, u] of Object.entries(users)) {
637
+ if (u === user) {
638
+ publicId = id2;
639
+ break;
640
+ }
641
+ }
642
+ if (!publicId && user && typeof user === "object") {
643
+ for (const [id2, u] of Object.entries(users)) {
644
+ if (u && typeof u === "object") {
645
+ if (u.constructor === user.constructor) {
646
+ publicId = id2;
647
+ break;
648
+ }
649
+ }
650
+ }
651
+ }
652
+ if (!publicId) {
653
+ console.error("[sessionTransfer] User not found in users collection.", {
654
+ userType: user?.constructor?.name,
655
+ userKeys: user ? Object.keys(user) : "null",
656
+ usersCount: Object.keys(users).length,
657
+ userIds: Object.keys(users)
658
+ });
659
+ return null;
660
+ }
661
+ }
662
+ const sessions = await this.room.storage.list();
663
+ let userSession = null;
664
+ let privateId = null;
665
+ for (const [key, session] of sessions) {
666
+ if (key.startsWith("session:") && session.publicId === publicId) {
667
+ userSession = session;
668
+ privateId = key.replace("session:", "");
669
+ break;
670
+ }
671
+ }
672
+ if (!userSession || !privateId) {
673
+ console.error(`[sessionTransfer] Session for publicId ${publicId} not found.`);
674
+ return null;
675
+ }
676
+ const usersPropName = this.getUsersPropName(instance);
677
+ if (!usersPropName) {
678
+ console.error("[sessionTransfer] `users` property not defined in the room.");
679
+ return null;
680
+ }
681
+ const userSnapshot = createStatesSnapshot(user);
682
+ const transferData = {
683
+ privateId,
684
+ userSnapshot,
685
+ sessionState: userSession.state,
686
+ publicId
687
+ };
688
+ try {
689
+ const targetRoomParty = this.room.context.parties.main.get(targetRoomId);
690
+ const response2 = await targetRoomParty.fetch("/session-transfer", {
691
+ method: "POST",
692
+ body: JSON.stringify(transferData),
693
+ headers: {
694
+ "Content-Type": "application/json"
695
+ }
696
+ });
697
+ if (!response2.ok) {
698
+ throw new Error(`Transfer request failed: ${await response2.text()}`);
699
+ }
700
+ const { transferToken } = await response2.json();
701
+ await this.deleteSession(privateId);
702
+ await this.room.storage.delete(`${usersPropName}.${publicId}`);
703
+ const currentUsers = signal2();
704
+ if (currentUsers[publicId]) {
705
+ delete currentUsers[publicId];
706
+ }
707
+ return transferToken;
708
+ } catch (error) {
709
+ console.error(`[sessionTransfer] Failed to transfer session to room ${targetRoomId}:`, error);
710
+ return null;
711
+ }
712
+ };
618
713
  const syncCb = /* @__PURE__ */ __name((values) => {
619
714
  if (options.getMemoryAll) {
620
715
  buildObject(values, instance.$memoryAll);
@@ -805,29 +900,46 @@ var Server = class {
805
900
  });
806
901
  const roomGuards = subRoom.constructor["_roomGuards"] || [];
807
902
  for (const guard of roomGuards) {
808
- const isAuthorized = await guard(conn, ctx);
903
+ const isAuthorized = await guard(conn, ctx, this.room);
809
904
  if (!isAuthorized) {
810
905
  conn.close();
811
906
  return;
812
907
  }
813
908
  }
909
+ let transferToken = null;
910
+ if (ctx.request?.url) {
911
+ const url = new URL(ctx.request.url);
912
+ transferToken = url.searchParams.get("transferToken");
913
+ }
914
+ let transferData = null;
915
+ if (transferToken) {
916
+ transferData = await this.room.storage.get(`transfer:${transferToken}`);
917
+ if (transferData) {
918
+ await this.room.storage.delete(`transfer:${transferToken}`);
919
+ }
920
+ }
814
921
  const existingSession = await this.getSession(conn.id);
815
- const publicId = existingSession?.publicId || generateShortUUID2();
922
+ const publicId = existingSession?.publicId || transferData?.publicId || generateShortUUID2();
816
923
  let user = null;
817
924
  const signal2 = this.getUsersProperty(subRoom);
818
925
  const usersPropName = this.getUsersPropName(subRoom);
819
926
  if (signal2) {
820
927
  const { classType } = signal2.options;
821
928
  if (!existingSession?.publicId) {
822
- user = isClass(classType) ? new classType() : classType(conn, ctx);
823
- signal2()[publicId] = user;
824
- const snapshot = createStatesSnapshot(user);
825
- this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
929
+ if (transferData?.restored && signal2()[publicId]) {
930
+ user = signal2()[publicId];
931
+ } else {
932
+ user = isClass(classType) ? new classType() : classType(conn, ctx);
933
+ signal2()[publicId] = user;
934
+ const snapshot = createStatesSnapshot(user);
935
+ this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
936
+ }
826
937
  } else {
827
938
  user = signal2()[existingSession.publicId];
828
939
  }
829
940
  if (!existingSession) {
830
- await this.saveSession(conn.id, {
941
+ const sessionPrivateId = transferData?.privateId || conn.id;
942
+ await this.saveSession(sessionPrivateId, {
831
943
  publicId
832
944
  });
833
945
  } else {
@@ -1115,7 +1227,7 @@ var Server = class {
1115
1227
  * @method onClose
1116
1228
  * @async
1117
1229
  * @param {Party.Connection} conn - The connection object of the disconnecting user.
1118
- * @description Handles user disconnection, removing them from the room and triggering the onLeave event.
1230
+ * @description Handles user disconnection, removing them from the room and triggering the onLeave event..
1119
1231
  * @returns {Promise<void>}
1120
1232
  *
1121
1233
  * @example
@@ -1181,6 +1293,58 @@ var Server = class {
1181
1293
  return this.handleDirectRequest(req, res);
1182
1294
  }
1183
1295
  /**
1296
+ * @method handleSessionRestore
1297
+ * @private
1298
+ * @async
1299
+ * @param {Party.Request} req - The HTTP request for session restore
1300
+ * @param {ServerResponse} res - The response object
1301
+ * @description Handles session restoration from transfer data, creates session from privateId
1302
+ * @returns {Promise<Response>} The response to return to the client
1303
+ */
1304
+ async handleSessionRestore(req, res) {
1305
+ try {
1306
+ const transferData = await req.json();
1307
+ const { privateId, userSnapshot, sessionState, publicId } = transferData;
1308
+ if (!privateId || !publicId) {
1309
+ return res.badRequest("Missing privateId or publicId in transfer data");
1310
+ }
1311
+ const subRoom = await this.getSubRoom();
1312
+ if (!subRoom) {
1313
+ return res.serverError("Room not available");
1314
+ }
1315
+ await this.saveSession(privateId, {
1316
+ publicId,
1317
+ state: sessionState,
1318
+ created: Date.now(),
1319
+ connected: false
1320
+ // Will be set to true when user connects
1321
+ });
1322
+ if (userSnapshot) {
1323
+ const signal2 = this.getUsersProperty(subRoom);
1324
+ const usersPropName = this.getUsersPropName(subRoom);
1325
+ if (signal2 && usersPropName) {
1326
+ const { classType } = signal2.options;
1327
+ const user = isClass(classType) ? new classType() : classType();
1328
+ load(user, userSnapshot, true);
1329
+ signal2()[publicId] = user;
1330
+ await this.room.storage.put(`${usersPropName}.${publicId}`, userSnapshot);
1331
+ }
1332
+ }
1333
+ const transferToken = generateShortUUID2();
1334
+ await this.room.storage.put(`transfer:${transferToken}`, {
1335
+ privateId,
1336
+ publicId,
1337
+ restored: true
1338
+ });
1339
+ return res.success({
1340
+ transferToken
1341
+ });
1342
+ } catch (error) {
1343
+ console.error("Error restoring session:", error);
1344
+ return res.serverError("Failed to restore session");
1345
+ }
1346
+ }
1347
+ /**
1184
1348
  * @method handleDirectRequest
1185
1349
  * @private
1186
1350
  * @async
@@ -1193,6 +1357,10 @@ var Server = class {
1193
1357
  if (!subRoom) {
1194
1358
  return res.notFound();
1195
1359
  }
1360
+ const url = new URL(req.url);
1361
+ if (url.pathname.endsWith("/session-transfer") && req.method === "POST") {
1362
+ return this.handleSessionRestore(req, res);
1363
+ }
1196
1364
  const response2 = await this.tryMatchRequestHandler(req, res, subRoom);
1197
1365
  if (response2) {
1198
1366
  return response2;
@@ -1291,7 +1459,7 @@ var Server = class {
1291
1459
  */
1292
1460
  pathMatches(requestPath, handlerPath) {
1293
1461
  const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1294
- const pathRegex = new RegExp(`^${pathRegexString}$`);
1462
+ const pathRegex = new RegExp(`^${pathRegexString}`);
1295
1463
  return pathRegex.test(requestPath);
1296
1464
  }
1297
1465
  /**
@@ -1311,7 +1479,7 @@ var Server = class {
1311
1479
  }
1312
1480
  });
1313
1481
  const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1314
- const pathRegex = new RegExp(`^${pathRegexString}$`);
1482
+ const pathRegex = new RegExp(`^${pathRegexString}`);
1315
1483
  const matches = requestPath.match(pathRegex);
1316
1484
  if (matches && matches.length > 1) {
1317
1485
  for (let i = 0; i < paramNames.length; i++) {
@@ -2433,6 +2601,49 @@ WorldRoom = _ts_decorate([
2433
2601
  typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2434
2602
  ])
2435
2603
  ], WorldRoom);
2604
+
2605
+ // src/session.guard.ts
2606
+ function createRequireSessionGuard(storage) {
2607
+ return async (sender, value) => {
2608
+ if (!sender || !sender.id) {
2609
+ return false;
2610
+ }
2611
+ try {
2612
+ const session = await storage.get(`session:${sender.id}`);
2613
+ if (!session) {
2614
+ return false;
2615
+ }
2616
+ const typedSession = session;
2617
+ if (!typedSession.publicId) {
2618
+ return false;
2619
+ }
2620
+ return true;
2621
+ } catch (error) {
2622
+ console.error("Error checking session in requireSession guard:", error);
2623
+ return false;
2624
+ }
2625
+ };
2626
+ }
2627
+ __name(createRequireSessionGuard, "createRequireSessionGuard");
2628
+ var requireSession = /* @__PURE__ */ __name(async (sender, value, room) => {
2629
+ if (!sender || !sender.id) {
2630
+ return false;
2631
+ }
2632
+ try {
2633
+ const session = await room.storage.get(`session:${sender.id}`);
2634
+ if (!session) {
2635
+ return false;
2636
+ }
2637
+ const typedSession = session;
2638
+ if (!typedSession.publicId) {
2639
+ return false;
2640
+ }
2641
+ return true;
2642
+ } catch (error) {
2643
+ console.error("Error checking session in requireSession guard:", error);
2644
+ return false;
2645
+ }
2646
+ }, "requireSession");
2436
2647
  export {
2437
2648
  Action,
2438
2649
  ClientIo,
@@ -2446,7 +2657,9 @@ export {
2446
2657
  ServerResponse,
2447
2658
  Shard,
2448
2659
  WorldRoom,
2660
+ createRequireSessionGuard,
2449
2661
  request,
2662
+ requireSession,
2450
2663
  testRoom
2451
2664
  };
2452
2665
  //# sourceMappingURL=index.js.map