@jgengine/ws 0.8.0 → 0.9.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/CHANGELOG.md +16 -0
- package/dist/createWsBackend.d.ts +4 -0
- package/dist/createWsBackend.js +167 -27
- package/dist/host.d.ts +5 -0
- package/dist/host.js +32 -3
- package/dist/hostRouter.d.ts +7 -4
- package/dist/hostRouter.js +100 -29
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/peer.js +5 -1
- package/dist/protocol.d.ts +5 -0
- package/dist/protocol.js +29 -5
- package/dist/readsHandler.d.ts +3 -0
- package/dist/readsHandler.js +21 -2
- package/llms.txt +247 -681
- package/package.json +6 -2
package/dist/hostRouter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { decidePoseSync } from "@jgengine/core/multiplayer/presenceModel";
|
|
1
|
+
import { decidePoseSync, spawnPresenceState } from "@jgengine/core/multiplayer/presenceModel";
|
|
2
2
|
import { createPositionHistory } from "@jgengine/core/multiplayer/lagCompensation";
|
|
3
3
|
import { createChatRateLimiter, DEFAULT_CHAT_BODY_LENGTH, DEFAULT_CHAT_HISTORY_LIMIT, DEFAULT_CHAT_RATE_LIMIT, } from "@jgengine/core/game/chat";
|
|
4
|
-
import { decodeWsClientMessage, encodeWsMessage, subscriptionKey, } from "./protocol.js";
|
|
4
|
+
import { decodeWsClientMessage, encodeWsMessage, inspectWsDecodeFailure, subscriptionKey, } from "./protocol.js";
|
|
5
5
|
export const DEFAULT_POSE_RULES = {
|
|
6
6
|
maxSpeed: 12,
|
|
7
7
|
maxVerticalOffset: 3,
|
|
@@ -24,8 +24,12 @@ export function createHostRouter(options) {
|
|
|
24
24
|
const host = options.host;
|
|
25
25
|
const now = options.now ?? Date.now;
|
|
26
26
|
const poseRules = options.poseRules ?? DEFAULT_POSE_RULES;
|
|
27
|
-
const
|
|
27
|
+
const allowAnonymous = options.allowAnonymous === true;
|
|
28
|
+
const singleSession = options.singleSession !== false;
|
|
29
|
+
const authenticate = options.authenticate ??
|
|
30
|
+
(allowAnonymous ? ({ userId }) => userId : null);
|
|
28
31
|
const connections = new Set();
|
|
32
|
+
const sessionsByUserId = new Map();
|
|
29
33
|
const presence = new Map();
|
|
30
34
|
const positionHistoryMs = options.positionHistoryMs ?? 1_000;
|
|
31
35
|
const histories = new Map();
|
|
@@ -135,12 +139,22 @@ export function createHostRouter(options) {
|
|
|
135
139
|
broadcastVoice(serverId, key.slice(prefix.length));
|
|
136
140
|
}
|
|
137
141
|
};
|
|
138
|
-
const
|
|
142
|
+
const requireMembership = async (connection, id, serverId) => {
|
|
139
143
|
const userId = connection.userId;
|
|
140
144
|
if (userId === null) {
|
|
141
|
-
replyError(connection,
|
|
142
|
-
return;
|
|
145
|
+
replyError(connection, id, "Not authenticated");
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (!(await host.isMember({ userId, serverId }))) {
|
|
149
|
+
replyError(connection, id, "Not a member of this server");
|
|
150
|
+
return null;
|
|
143
151
|
}
|
|
152
|
+
return userId;
|
|
153
|
+
};
|
|
154
|
+
const handleChatSend = async (connection, message) => {
|
|
155
|
+
const userId = await requireMembership(connection, message.id, message.serverId);
|
|
156
|
+
if (userId === null)
|
|
157
|
+
return;
|
|
144
158
|
const body = message.body.trim();
|
|
145
159
|
if (body.length === 0) {
|
|
146
160
|
replyError(connection, message.id, "empty message");
|
|
@@ -234,28 +248,19 @@ export function createHostRouter(options) {
|
|
|
234
248
|
}
|
|
235
249
|
};
|
|
236
250
|
const unsubscribeHost = host.subscribe(onHostEvent);
|
|
237
|
-
const handlePose = (connection, serverId, pose) => {
|
|
251
|
+
const handlePose = async (connection, serverId, pose) => {
|
|
238
252
|
if (connection.userId === null)
|
|
239
253
|
return;
|
|
254
|
+
if (!(await host.isMember({ userId: connection.userId, serverId })))
|
|
255
|
+
return;
|
|
240
256
|
const rows = presence.get(serverId) ?? new Map();
|
|
241
257
|
presence.set(serverId, rows);
|
|
242
258
|
const timestamp = now();
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
rows.set(connection.userId, {
|
|
246
|
-
position: { x: pose.x, y: pose.y, z: pose.z },
|
|
247
|
-
rotationY: pose.rotationY,
|
|
248
|
-
rotationPitch: pose.rotationPitch,
|
|
249
|
-
lastSeenAtMs: timestamp,
|
|
250
|
-
appearance: pose.appearance,
|
|
251
|
-
});
|
|
252
|
-
historyFor(serverId).record(connection.userId, timestamp, { x: pose.x, y: pose.y, z: pose.z });
|
|
253
|
-
broadcastPresence(serverId);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
259
|
+
const hadRow = rows.has(connection.userId);
|
|
260
|
+
const current = rows.get(connection.userId) ?? spawnPresenceState(undefined, timestamp, poseRules);
|
|
256
261
|
const decision = decidePoseSync(current, { position: { x: pose.x, y: pose.y, z: pose.z }, rotationY: pose.rotationY, rotationPitch: pose.rotationPitch }, poseRules, timestamp);
|
|
257
262
|
const appearanceChanged = pose.appearance !== undefined && !appearanceEqual(pose.appearance, current.appearance);
|
|
258
|
-
if (decision.changed || decision.refreshKeepAlive || appearanceChanged) {
|
|
263
|
+
if (decision.changed || decision.refreshKeepAlive || appearanceChanged || !hadRow) {
|
|
259
264
|
rows.set(connection.userId, {
|
|
260
265
|
position: decision.position,
|
|
261
266
|
rotationY: decision.rotationY,
|
|
@@ -265,8 +270,9 @@ export function createHostRouter(options) {
|
|
|
265
270
|
});
|
|
266
271
|
historyFor(serverId).record(connection.userId, timestamp, decision.position);
|
|
267
272
|
}
|
|
268
|
-
if (decision.changed || appearanceChanged)
|
|
273
|
+
if (decision.changed || appearanceChanged || !hadRow) {
|
|
269
274
|
broadcastPresence(serverId);
|
|
275
|
+
}
|
|
270
276
|
};
|
|
271
277
|
const dropPresence = (connection) => {
|
|
272
278
|
if (connection.userId === null)
|
|
@@ -289,18 +295,36 @@ export function createHostRouter(options) {
|
|
|
289
295
|
};
|
|
290
296
|
const handleMessage = async (connection, message) => {
|
|
291
297
|
if (message.t === "hello") {
|
|
298
|
+
if (connection.userId !== null) {
|
|
299
|
+
replyError(connection, message.id, "Already authenticated");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (authenticate === null) {
|
|
303
|
+
replyError(connection, message.id, "Not authenticated");
|
|
304
|
+
connection.transport.close();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
292
307
|
const userId = await authenticate({ userId: message.userId, token: message.token });
|
|
293
308
|
if (userId === null) {
|
|
294
309
|
replyError(connection, message.id, "Not authenticated");
|
|
295
310
|
connection.transport.close();
|
|
296
311
|
return;
|
|
297
312
|
}
|
|
313
|
+
if (singleSession) {
|
|
314
|
+
const previous = sessionsByUserId.get(userId);
|
|
315
|
+
if (previous !== undefined && previous !== connection) {
|
|
316
|
+
sessionsByUserId.delete(userId);
|
|
317
|
+
previous.userId = null;
|
|
318
|
+
previous.transport.close();
|
|
319
|
+
}
|
|
320
|
+
sessionsByUserId.set(userId, connection);
|
|
321
|
+
}
|
|
298
322
|
connection.userId = userId;
|
|
299
323
|
reply(connection, message.id, { userId });
|
|
300
324
|
return;
|
|
301
325
|
}
|
|
302
326
|
if (message.t === "pose") {
|
|
303
|
-
handlePose(connection, message.serverId, message.pose);
|
|
327
|
+
await handlePose(connection, message.serverId, message.pose);
|
|
304
328
|
return;
|
|
305
329
|
}
|
|
306
330
|
if (connection.userId === null) {
|
|
@@ -317,6 +341,7 @@ export function createHostRouter(options) {
|
|
|
317
341
|
serverId: message.serverId,
|
|
318
342
|
attributes: message.attributes,
|
|
319
343
|
});
|
|
344
|
+
connection.joinedServers.add(result.serverId);
|
|
320
345
|
reply(connection, message.id, result);
|
|
321
346
|
return;
|
|
322
347
|
}
|
|
@@ -326,6 +351,8 @@ export function createHostRouter(options) {
|
|
|
326
351
|
gameId: message.gameId,
|
|
327
352
|
code: message.code,
|
|
328
353
|
});
|
|
354
|
+
if (result !== null)
|
|
355
|
+
connection.joinedServers.add(result.serverId);
|
|
329
356
|
reply(connection, message.id, result);
|
|
330
357
|
return;
|
|
331
358
|
}
|
|
@@ -340,6 +367,7 @@ export function createHostRouter(options) {
|
|
|
340
367
|
}
|
|
341
368
|
case "leave": {
|
|
342
369
|
await host.leaveServer({ userId, serverId: message.serverId });
|
|
370
|
+
connection.joinedServers.delete(message.serverId);
|
|
343
371
|
dropPresenceForServer(userId, message.serverId);
|
|
344
372
|
dropVoiceForServer(userId, message.serverId);
|
|
345
373
|
reply(connection, message.id, null);
|
|
@@ -366,10 +394,12 @@ export function createHostRouter(options) {
|
|
|
366
394
|
return;
|
|
367
395
|
}
|
|
368
396
|
case "chatSend": {
|
|
369
|
-
handleChatSend(connection, message);
|
|
397
|
+
await handleChatSend(connection, message);
|
|
370
398
|
return;
|
|
371
399
|
}
|
|
372
400
|
case "voiceJoin": {
|
|
401
|
+
if ((await requireMembership(connection, message.id, message.serverId)) === null)
|
|
402
|
+
return;
|
|
373
403
|
const participant = { userId };
|
|
374
404
|
if (message.streamId !== undefined)
|
|
375
405
|
participant.streamId = message.streamId;
|
|
@@ -379,6 +409,8 @@ export function createHostRouter(options) {
|
|
|
379
409
|
return;
|
|
380
410
|
}
|
|
381
411
|
case "voiceLeave": {
|
|
412
|
+
if ((await requireMembership(connection, message.id, message.serverId)) === null)
|
|
413
|
+
return;
|
|
382
414
|
const roster = voiceRoster(message.serverId, message.channelId);
|
|
383
415
|
const removed = roster.delete(userId);
|
|
384
416
|
reply(connection, message.id, null);
|
|
@@ -387,6 +419,8 @@ export function createHostRouter(options) {
|
|
|
387
419
|
return;
|
|
388
420
|
}
|
|
389
421
|
case "voicePublish": {
|
|
422
|
+
if ((await requireMembership(connection, message.id, message.serverId)) === null)
|
|
423
|
+
return;
|
|
390
424
|
const participant = voiceRoster(message.serverId, message.channelId).get(userId);
|
|
391
425
|
if (participant === undefined) {
|
|
392
426
|
replyError(connection, message.id, "not in this voice channel");
|
|
@@ -398,6 +432,13 @@ export function createHostRouter(options) {
|
|
|
398
432
|
return;
|
|
399
433
|
}
|
|
400
434
|
case "subscribe": {
|
|
435
|
+
if ((message.channel === "chat" ||
|
|
436
|
+
message.channel === "voice" ||
|
|
437
|
+
message.channel === "presence") &&
|
|
438
|
+
!(await host.isMember({ userId, serverId: message.serverId }))) {
|
|
439
|
+
replyError(connection, message.id, "Not a member of this server");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
401
442
|
connection.subscriptions.add(subscriptionKey(message.channel, message.serverId, message.action));
|
|
402
443
|
reply(connection, message.id, null);
|
|
403
444
|
await pushSubscription(connection, message.channel, message.serverId, message.action);
|
|
@@ -414,19 +455,49 @@ export function createHostRouter(options) {
|
|
|
414
455
|
replyError(connection, message.id, error instanceof Error ? error.message : "Internal error");
|
|
415
456
|
}
|
|
416
457
|
};
|
|
458
|
+
const leaveJoinedServers = (connection) => {
|
|
459
|
+
if (connection.userId === null)
|
|
460
|
+
return;
|
|
461
|
+
const userId = connection.userId;
|
|
462
|
+
const servers = [...connection.joinedServers];
|
|
463
|
+
connection.joinedServers.clear();
|
|
464
|
+
for (const serverId of servers) {
|
|
465
|
+
void host.leaveServer({ userId, serverId }).catch(() => undefined);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
417
468
|
return {
|
|
418
469
|
connect: (transport) => {
|
|
419
|
-
const connection = {
|
|
470
|
+
const connection = {
|
|
471
|
+
transport,
|
|
472
|
+
userId: null,
|
|
473
|
+
subscriptions: new Set(),
|
|
474
|
+
joinedServers: new Set(),
|
|
475
|
+
queue: Promise.resolve(),
|
|
476
|
+
};
|
|
420
477
|
connections.add(connection);
|
|
421
478
|
return {
|
|
422
479
|
handleRaw: (raw) => {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
480
|
+
connection.queue = connection.queue
|
|
481
|
+
.then(async () => {
|
|
482
|
+
const text = typeof raw === "string" ? raw : String(raw);
|
|
483
|
+
const message = decodeWsClientMessage(text);
|
|
484
|
+
if (message === null) {
|
|
485
|
+
const failure = inspectWsDecodeFailure(text);
|
|
486
|
+
if (failure.id !== undefined) {
|
|
487
|
+
replyError(connection, failure.id, failure.reason);
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
await handleMessage(connection, message);
|
|
492
|
+
})
|
|
493
|
+
.catch(() => undefined);
|
|
427
494
|
},
|
|
428
495
|
close: () => {
|
|
496
|
+
if (connection.userId !== null && sessionsByUserId.get(connection.userId) === connection) {
|
|
497
|
+
sessionsByUserId.delete(connection.userId);
|
|
498
|
+
}
|
|
429
499
|
connections.delete(connection);
|
|
500
|
+
leaveJoinedServers(connection);
|
|
430
501
|
dropPresence(connection);
|
|
431
502
|
dropVoice(connection);
|
|
432
503
|
},
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./createWsBackend.js";
|
|
2
|
+
export * from "./host.js";
|
|
3
|
+
export * from "./hostRouter.js";
|
|
4
|
+
export * from "./httpReads.js";
|
|
5
|
+
export * from "./peer.js";
|
|
6
|
+
export * from "./pipe.js";
|
|
7
|
+
export * from "./protocol.js";
|
|
8
|
+
export * from "./readsHandler.js";
|
|
9
|
+
export * from "./socketIoPipe.js";
|
|
10
|
+
export * from "./voiceChannel.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./createWsBackend.js";
|
|
2
|
+
export * from "./host.js";
|
|
3
|
+
export * from "./hostRouter.js";
|
|
4
|
+
export * from "./httpReads.js";
|
|
5
|
+
export * from "./peer.js";
|
|
6
|
+
export * from "./pipe.js";
|
|
7
|
+
export * from "./protocol.js";
|
|
8
|
+
export * from "./readsHandler.js";
|
|
9
|
+
export * from "./socketIoPipe.js";
|
|
10
|
+
export * from "./voiceChannel.js";
|
package/dist/peer.js
CHANGED
|
@@ -93,7 +93,11 @@ export function createPeerHost(options) {
|
|
|
93
93
|
});
|
|
94
94
|
if (ownsHost)
|
|
95
95
|
host.start();
|
|
96
|
-
const router = createHostRouter({
|
|
96
|
+
const router = createHostRouter({
|
|
97
|
+
allowAnonymous: true,
|
|
98
|
+
...options.router,
|
|
99
|
+
host,
|
|
100
|
+
});
|
|
97
101
|
const backend = createWsBackend({
|
|
98
102
|
userId: options.userId,
|
|
99
103
|
token: options.token,
|
package/dist/protocol.d.ts
CHANGED
|
@@ -173,6 +173,11 @@ export type WsRunCommandResult = TransportRunCommandResult;
|
|
|
173
173
|
export type WsBrowseResult = SessionListing[];
|
|
174
174
|
export type WsJoinByCodeResult = JoinServerResult | null;
|
|
175
175
|
export declare function encodeWsMessage(message: WsClientMessage | WsServerMessage): string;
|
|
176
|
+
export type WsDecodeFailure = {
|
|
177
|
+
reason: string;
|
|
178
|
+
id?: number;
|
|
179
|
+
};
|
|
180
|
+
export declare function inspectWsDecodeFailure(raw: unknown): WsDecodeFailure;
|
|
176
181
|
export declare function decodeWsClientMessage(raw: unknown): WsClientMessage | null;
|
|
177
182
|
export declare function decodeWsServerMessage(raw: unknown): WsServerMessage | null;
|
|
178
183
|
export declare function subscriptionKey(channel: WsChannel, serverId: string, action?: string): string;
|
package/dist/protocol.js
CHANGED
|
@@ -35,15 +35,39 @@ function isAppearance(value) {
|
|
|
35
35
|
return (isRecord(value) &&
|
|
36
36
|
Object.values(value).every((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean"));
|
|
37
37
|
}
|
|
38
|
+
function isFiniteNumber(value) {
|
|
39
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
40
|
+
}
|
|
38
41
|
function isPose(value) {
|
|
39
42
|
return (isRecord(value) &&
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
isFiniteNumber(value.x) &&
|
|
44
|
+
isFiniteNumber(value.y) &&
|
|
45
|
+
isFiniteNumber(value.z) &&
|
|
46
|
+
isFiniteNumber(value.rotationY) &&
|
|
47
|
+
isFiniteNumber(value.rotationPitch) &&
|
|
45
48
|
(value.appearance === undefined || isAppearance(value.appearance)));
|
|
46
49
|
}
|
|
50
|
+
export function inspectWsDecodeFailure(raw) {
|
|
51
|
+
if (typeof raw !== "string")
|
|
52
|
+
return { reason: "Invalid message framing" };
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { reason: "Invalid JSON" };
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(parsed))
|
|
61
|
+
return { reason: "Invalid message" };
|
|
62
|
+
const id = typeof parsed.id === "number" ? parsed.id : undefined;
|
|
63
|
+
if (parsed.v !== WS_PROTOCOL_VERSION) {
|
|
64
|
+
return { reason: "Protocol version mismatch", id };
|
|
65
|
+
}
|
|
66
|
+
if (typeof parsed.t !== "string") {
|
|
67
|
+
return { reason: "Invalid message type", id };
|
|
68
|
+
}
|
|
69
|
+
return { reason: "Malformed message", id };
|
|
70
|
+
}
|
|
47
71
|
export function decodeWsClientMessage(raw) {
|
|
48
72
|
const message = parseVersioned(raw);
|
|
49
73
|
if (message === null)
|
package/dist/readsHandler.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HostPersistence, ServerListing } from "@jgengine/core/runtime/hostPersistence";
|
|
2
2
|
export type ReadsPersistence = Pick<HostPersistence, "listServers" | "loadProfile" | "getLeaderboardTop" | "getLeaderboardProfile">;
|
|
3
3
|
export type ReadsHandler = (request: Request) => Promise<Response>;
|
|
4
|
+
export type ReadsAuthenticate = (request: Request) => Promise<string | null> | string | null;
|
|
4
5
|
export type ReadsHandlerOptions = {
|
|
5
6
|
persistence: ReadsPersistence | (() => Promise<ReadsPersistence>);
|
|
6
7
|
basePath?: string;
|
|
@@ -8,5 +9,7 @@ export type ReadsHandlerOptions = {
|
|
|
8
9
|
gameId: string;
|
|
9
10
|
limit?: number;
|
|
10
11
|
}) => Promise<ServerListing[]>;
|
|
12
|
+
authenticate?: ReadsAuthenticate;
|
|
13
|
+
allowPublicProfiles?: boolean;
|
|
11
14
|
};
|
|
12
15
|
export declare function createReadsHandler(options: ReadsHandlerOptions): ReadsHandler;
|
package/dist/readsHandler.js
CHANGED
|
@@ -7,6 +7,7 @@ function json(body, status = 200) {
|
|
|
7
7
|
}
|
|
8
8
|
export function createReadsHandler(options) {
|
|
9
9
|
const basePath = (options.basePath ?? "/api").replace(/\/$/, "");
|
|
10
|
+
const allowPublicProfiles = options.allowPublicProfiles === true;
|
|
10
11
|
let persistence = null;
|
|
11
12
|
const getPersistence = () => {
|
|
12
13
|
persistence ??=
|
|
@@ -21,9 +22,18 @@ export function createReadsHandler(options) {
|
|
|
21
22
|
if (request.method !== "GET") {
|
|
22
23
|
return json({ error: "method not allowed" }, 405);
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
let segments;
|
|
26
|
+
try {
|
|
27
|
+
segments = url.pathname.slice(basePath.length).split("/").filter(Boolean).map(decodeURIComponent);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return json({ error: "malformed path" }, 400);
|
|
31
|
+
}
|
|
25
32
|
const gameId = url.searchParams.get("gameId") ?? "";
|
|
26
33
|
const limitParam = url.searchParams.get("limit");
|
|
34
|
+
if (limitParam !== null && !Number.isFinite(Number(limitParam))) {
|
|
35
|
+
return json({ error: "limit must be a number" }, 400);
|
|
36
|
+
}
|
|
27
37
|
const limit = limitParam === null ? undefined : Number(limitParam);
|
|
28
38
|
const [head, tail] = segments;
|
|
29
39
|
if (head === "servers" && tail === undefined) {
|
|
@@ -31,7 +41,7 @@ export function createReadsHandler(options) {
|
|
|
31
41
|
return json(await options.listOpenServers({ gameId, limit }));
|
|
32
42
|
}
|
|
33
43
|
const records = await (await getPersistence()).listServers(gameId);
|
|
34
|
-
return json(toOpenServerListings(records.map(toServerListing), limit));
|
|
44
|
+
return json(toOpenServerListings(records.map((record) => toServerListing(record)), limit));
|
|
35
45
|
}
|
|
36
46
|
if (head === "leaderboard" && tail !== undefined && segments.length === 2) {
|
|
37
47
|
const scope = url.searchParams.get("scope") ?? "global";
|
|
@@ -50,6 +60,15 @@ export function createReadsHandler(options) {
|
|
|
50
60
|
return json(await (await getPersistence()).getLeaderboardProfile({ gameId, userId: tail }));
|
|
51
61
|
}
|
|
52
62
|
if (head === "profile" && tail !== undefined && segments.length === 2) {
|
|
63
|
+
if (!allowPublicProfiles) {
|
|
64
|
+
if (options.authenticate === undefined) {
|
|
65
|
+
return json({ error: "unauthorized" }, 401);
|
|
66
|
+
}
|
|
67
|
+
const actorUserId = await options.authenticate(request);
|
|
68
|
+
if (actorUserId === null || actorUserId !== tail) {
|
|
69
|
+
return json({ error: "unauthorized" }, 401);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
53
72
|
const profile = await (await getPersistence()).loadProfile({ userId: tail, gameId });
|
|
54
73
|
return json(profile === null
|
|
55
74
|
? null
|