@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 +65 -2
- package/dist/index.js +223 -10
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +2 -1
- package/examples/game/app/components/Room.tsx +8 -4
- package/examples/game/party/game.room.ts +14 -1
- package/examples/game/party/server.ts +2 -2
- package/package.json +2 -2
- package/src/index.ts +2 -1
- package/src/server.ts +231 -10
- package/src/session.guard.ts +111 -0
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
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
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
|