onelobby 0.5.0 → 0.6.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/README.md +2 -2
- package/blueprint.schema.json +8 -1
- package/dist/blueprint.d.ts +4 -0
- package/dist/blueprint.js +5 -0
- package/dist/blueprint.js.map +1 -1
- package/dist/codec.d.ts +23 -0
- package/dist/codec.js +28 -0
- package/dist/codec.js.map +1 -1
- package/dist/direct/browser-host.d.ts +46 -0
- package/dist/direct/browser-host.js +93 -0
- package/dist/direct/browser-host.js.map +1 -0
- package/dist/direct/hub-host.d.ts +34 -23
- package/dist/direct/hub-host.js +75 -59
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub-local.d.ts +22 -0
- package/dist/direct/hub-local.js +46 -0
- package/dist/direct/hub-local.js.map +1 -0
- package/dist/direct/hub.d.ts +37 -10
- package/dist/direct/hub.js +114 -23
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +36 -3
- package/dist/direct/index.js +146 -20
- package/dist/direct/index.js.map +1 -1
- package/dist/direct/metrics.d.ts +12 -0
- package/dist/direct/metrics.js +45 -0
- package/dist/direct/metrics.js.map +1 -0
- package/dist/direct/portmap.d.ts +9 -2
- package/dist/direct/portmap.js +7 -2
- package/dist/direct/portmap.js.map +1 -1
- package/dist/direct/sha1.d.ts +16 -0
- package/dist/direct/sha1.js +111 -0
- package/dist/direct/sha1.js.map +1 -0
- package/dist/direct-address.d.ts +2 -2
- package/dist/direct-address.js.map +1 -1
- package/dist/friendly.d.ts +23 -0
- package/dist/friendly.js +28 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/internals.d.ts +9 -0
- package/dist/internals.js +9 -0
- package/dist/internals.js.map +1 -1
- package/dist/join-code-kind.d.ts +10 -0
- package/dist/join-code-kind.js +10 -0
- package/dist/join-code-kind.js.map +1 -0
- package/dist/joincode.d.ts +10 -1
- package/dist/joincode.js +23 -3
- package/dist/joincode.js.map +1 -1
- package/dist/lobby.d.ts +65 -4
- package/dist/lobby.js +151 -12
- package/dist/lobby.js.map +1 -1
- package/dist/lockstep.d.ts +8 -0
- package/dist/lockstep.js +23 -2
- package/dist/lockstep.js.map +1 -1
- package/dist/nostr/crypto.d.ts +20 -0
- package/dist/nostr/crypto.js +78 -0
- package/dist/nostr/crypto.js.map +1 -0
- package/dist/nostr/host.d.ts +66 -0
- package/dist/nostr/host.js +196 -0
- package/dist/nostr/host.js.map +1 -0
- package/dist/nostr/limits.d.ts +31 -0
- package/dist/nostr/limits.js +36 -0
- package/dist/nostr/limits.js.map +1 -0
- package/dist/nostr/link.d.ts +70 -0
- package/dist/nostr/link.js +180 -0
- package/dist/nostr/link.js.map +1 -0
- package/dist/nostr/relays.d.ts +79 -0
- package/dist/nostr/relays.js +315 -0
- package/dist/nostr/relays.js.map +1 -0
- package/dist/nostr/room.d.ts +43 -0
- package/dist/nostr/room.js +104 -0
- package/dist/nostr/room.js.map +1 -0
- package/dist/nostr/secp256k1.d.ts +10 -0
- package/dist/nostr/secp256k1.js +171 -0
- package/dist/nostr/secp256k1.js.map +1 -0
- package/dist/nostr/socket.d.ts +48 -0
- package/dist/nostr/socket.js +141 -0
- package/dist/nostr/socket.js.map +1 -0
- package/dist/onelobby.bundle.js +146 -0
- package/dist/onelobby.bundle.js.map +7 -0
- package/dist/onelobby.bundle.mjs +11 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +31 -3
- package/dist/onelobby.global.js +2599 -46
- package/dist/onelobby.global.js.map +4 -4
- package/dist/onelobby.global.min.js +11 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +68 -14
- package/dist/onelobby.js.map +1 -1
- package/dist/pause.d.ts +85 -0
- package/dist/pause.js +160 -0
- package/dist/pause.js.map +1 -0
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js.map +1 -1
- package/dist/webrtc.d.ts +1 -0
- package/dist/webrtc.js +6 -1
- package/dist/webrtc.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-local.js","sourceRoot":"","sources":["../../src/direct/hub-local.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,OAAO,cAAc;IAUI;IAT7B,wFAAwF;IACxF,MAAM,CAAU,OAAO,GAAG,OAAO,CAAC;IAClC,UAAU,GAAG,CAAC,CAAC;IACf,MAAM,GAAyB,IAAI,CAAC;IACpC,SAAS,GAA4B,IAAI,CAAC;IAC1C,OAAO,GAA0B,IAAI,CAAC;IACtC,OAAO,GAA0B,IAAI,CAAC;IAC9B,OAAO,CAAiB;IAEhC,YAA6B,GAAiB;QAAjB,QAAG,GAAH,GAAG,CAAc;QAC5C,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CACjJ,IAAI,CAAC,MAAM,EAAE,CACd,CAAC;QACF,cAAc,CAAC,GAAG,EAAE;YAClB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,OAAO;YACT,CAAC;YACD,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,IAAY;QACf,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAClF,CAAC;IAED,KAAK;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QACvB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,OAAO,IAAI,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAEO,MAAM;QACZ,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC;YAAE,OAAO;QAClC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACpB,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC","sourcesContent":["import type { SocketLike } from \"../transport.js\";\nimport type { SignalingHub, Session } from \"./hub.js\";\n\n/**\n * A direct host's own signaling socket (C#'s `HubSignalingSocket`): sends go straight into the hub; the hub's\n * replies are delivered on a microtask, so callbacks never run inside the hub. No Node APIs, so a browser tab can\n * use it too (`kit.hostNostr`).\n */\nexport class LocalHubSocket implements SocketLike {\n /** Address the hub sees for the host itself (rate limits and kicks are per address). */\n static readonly ADDRESS = \"local\";\n readyState = 0;\n onopen: SocketLike[\"onopen\"] = null;\n onmessage: SocketLike[\"onmessage\"] = null;\n onclose: SocketLike[\"onclose\"] = null;\n onerror: SocketLike[\"onerror\"] = null;\n private session: Session | null;\n\n constructor(private readonly hub: SignalingHub) {\n this.session = hub.connect(LocalHubSocket.ADDRESS, (text) => queueMicrotask(() => this.readyState === 1 && this.onmessage?.({ data: text })), () =>\n this.finish(),\n );\n queueMicrotask(() => {\n if (!this.session) {\n this.finish();\n return;\n }\n this.readyState = 1;\n this.onopen?.();\n });\n }\n\n send(data: string): void {\n if (this.readyState === 1 && this.session) this.hub.receive(this.session, data);\n }\n\n close(): void {\n const s = this.session;\n const wasOpen = this.readyState === 1;\n this.finish();\n if (wasOpen && s) this.hub.disconnect(s);\n }\n\n private finish(): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n queueMicrotask(() => this.onclose?.());\n }\n}\n"]}
|
package/dist/direct/hub.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The signaling hub (protocol lk1) in TypeScript, for a Node process that hosts direct
|
|
3
|
-
* (docs/direct-ip.md s.2): a line-by-line port of `server/OneLobby.Hub/Hub.cs`, held to the same rules by
|
|
2
|
+
* The signaling hub (protocol lk1) in TypeScript, for a Node process or a browser tab that hosts direct lobbies
|
|
3
|
+
* (docs/design/direct-ip.md s.2): a line-by-line port of `server/OneLobby.Hub/Hub.cs`, held to the same rules by
|
|
4
4
|
* `conformance/server`. Transport-agnostic: feed it text frames with `receive`, call `tick` about once a second.
|
|
5
|
-
* Metrics
|
|
6
|
-
*
|
|
5
|
+
* Metrics (same names, labels and help text as C#, rendered by `metrics.render()`) are counted too, so every hub shape
|
|
6
|
+
* looks the same from outside; everything else (codes, list, resume, private lobbies, kicks, migration claims, TURN
|
|
7
|
+
* credentials, rate limits) behaves as in C#.
|
|
7
8
|
*/
|
|
9
|
+
import { Metrics } from "./metrics.js";
|
|
10
|
+
export { Metrics };
|
|
8
11
|
export declare const PROTO = "lk1";
|
|
9
12
|
/** The settings the hub reads; same names and defaults as C#'s `HubOptions`. */
|
|
10
13
|
export interface HubOptions {
|
|
@@ -26,6 +29,8 @@ export interface HubOptions {
|
|
|
26
29
|
createPerMin: number;
|
|
27
30
|
joinPerMin: number;
|
|
28
31
|
signalPerSec: number;
|
|
32
|
+
/** Open sockets per address (loopback included); `disableIpLimits` lifts it, `rateScale` scales it. */
|
|
33
|
+
socketsPerIp: number;
|
|
29
34
|
openLobbiesPerIp: number;
|
|
30
35
|
idleTimeoutMs: number;
|
|
31
36
|
resumeWindowMs: number;
|
|
@@ -39,8 +44,14 @@ export interface HubOptions {
|
|
|
39
44
|
issueCodes: boolean;
|
|
40
45
|
}
|
|
41
46
|
export declare function defaultHubOptions(): HubOptions;
|
|
42
|
-
/** Settings for a hub inside the host's game (docs/direct-ip.md s.2): no codes, no list, no ICE servers. */
|
|
47
|
+
/** Settings for a hub inside the host's game (docs/design/direct-ip.md s.2): no codes, no list, no ICE servers. */
|
|
43
48
|
export declare function directHubOptions(maxPlayers: number, password?: string | null): HubOptions;
|
|
49
|
+
/** A session's total `signal` budget is this many targets' worth of `signalPerSec` (spec/signaling.md rate limits). */
|
|
50
|
+
export declare const SIGNAL_FANOUT = 8;
|
|
51
|
+
/** Hub sessions bridged from Nostr have the address `nostr:<pubkey prefix>`. */
|
|
52
|
+
export declare const NOSTR_ADDR = "nostr:";
|
|
53
|
+
/** `signal` per second per (sender, Nostr target): relays reject high publish rates. */
|
|
54
|
+
export declare const NOSTR_SIGNAL_PER_SEC = 10;
|
|
44
55
|
/** Sliding-window counter: at most `limit` events per `windowMs`. */
|
|
45
56
|
export declare class RateWindow {
|
|
46
57
|
readonly limit: number;
|
|
@@ -60,8 +71,7 @@ export declare class Session {
|
|
|
60
71
|
readonly ip: string;
|
|
61
72
|
readonly connId: string;
|
|
62
73
|
readonly send: (text: string) => void;
|
|
63
|
-
readonly close: () => void;
|
|
64
|
-
readonly signalRate: RateWindow;
|
|
74
|
+
readonly close: (code?: number, reason?: string) => void;
|
|
65
75
|
closed: boolean;
|
|
66
76
|
lastSeen: number;
|
|
67
77
|
helloDone: boolean;
|
|
@@ -72,7 +82,13 @@ export declare class Session {
|
|
|
72
82
|
member: Member | null;
|
|
73
83
|
pendingIn: HubLobby | null;
|
|
74
84
|
pendingMember: Member | null;
|
|
75
|
-
constructor(id: number, ip: string, connId: string, send: (text: string) => void, close: () => void
|
|
85
|
+
constructor(id: number, ip: string, connId: string, send: (text: string) => void, close: (code?: number, reason?: string) => void);
|
|
86
|
+
/** Nostr joiners: the remote address of the host's WebRTC link to this session, once known (kick bans it too). */
|
|
87
|
+
linkIp: string | null;
|
|
88
|
+
/** One window per target member (a member belongs to one lobby, so a new lobby starts fresh). */
|
|
89
|
+
readonly signalRates: Map<Member, RateWindow>;
|
|
90
|
+
/** All signals of this session, so the per-target budget does not grow without bound with the lobby. */
|
|
91
|
+
signalTotal: RateWindow | null;
|
|
76
92
|
}
|
|
77
93
|
declare class Member {
|
|
78
94
|
peerId: number;
|
|
@@ -132,13 +148,20 @@ export declare class SignalingHub {
|
|
|
132
148
|
private createdOnce;
|
|
133
149
|
private members;
|
|
134
150
|
private readonly logSalt;
|
|
151
|
+
readonly metrics: Metrics;
|
|
135
152
|
constructor(options?: Partial<HubOptions>, deps?: HubDeps);
|
|
136
153
|
get lobbyCount(): number;
|
|
137
154
|
get socketCount(): number;
|
|
138
155
|
private get scale();
|
|
139
156
|
/** Registers a socket. Returns null (after sending server_full and closing) when over maxPeers. */
|
|
140
|
-
connect(ip: string, send: (text: string) => void, close: () => void): Session | null;
|
|
157
|
+
connect(ip: string, send: (text: string) => void, close: (code?: number, reason?: string) => void): Session | null;
|
|
141
158
|
disconnect(s: Session): void;
|
|
159
|
+
/**
|
|
160
|
+
* The host's WebRTC link to joiner `peerId` has the remote address `ip` (selected candidate pair; the host's own
|
|
161
|
+
* lobby reports it, a relayed or mDNS address must not be passed). Only Nostr sessions take it: their socket address
|
|
162
|
+
* is a pubkey the joiner chose. A joiner whose link address is banned is removed as kicked; a later kick bans it.
|
|
163
|
+
*/
|
|
164
|
+
reportLinkAddress(peerId: number, ip: string): void;
|
|
142
165
|
private closeSession;
|
|
143
166
|
/** Call about once a second: idle timeouts, resume-window expiry, code retirement. */
|
|
144
167
|
tick(): void;
|
|
@@ -153,6 +176,11 @@ export declare class SignalingHub {
|
|
|
153
176
|
private list;
|
|
154
177
|
private leave;
|
|
155
178
|
private signal;
|
|
179
|
+
/**
|
|
180
|
+
* spec/signaling.md rate limits: `signal` is counted per sender and target, so a host answering several joiners at once
|
|
181
|
+
* gets the budget for each, and per sender in total (SIGNAL_FANOUT targets' worth).
|
|
182
|
+
*/
|
|
183
|
+
private signalRate;
|
|
156
184
|
private requireHost;
|
|
157
185
|
private hostMeta;
|
|
158
186
|
private hostPhase;
|
|
@@ -178,4 +206,3 @@ export declare class SignalingHub {
|
|
|
178
206
|
private logEvent;
|
|
179
207
|
private hashIp;
|
|
180
208
|
}
|
|
181
|
-
export {};
|
package/dist/direct/hub.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { createHash, createHmac, randomBytes, randomInt } from "node:crypto";
|
|
3
1
|
/**
|
|
4
|
-
* The signaling hub (protocol lk1) in TypeScript, for a Node process that hosts direct
|
|
5
|
-
* (docs/direct-ip.md s.2): a line-by-line port of `server/OneLobby.Hub/Hub.cs`, held to the same rules by
|
|
2
|
+
* The signaling hub (protocol lk1) in TypeScript, for a Node process or a browser tab that hosts direct lobbies
|
|
3
|
+
* (docs/design/direct-ip.md s.2): a line-by-line port of `server/OneLobby.Hub/Hub.cs`, held to the same rules by
|
|
6
4
|
* `conformance/server`. Transport-agnostic: feed it text frames with `receive`, call `tick` about once a second.
|
|
7
|
-
* Metrics
|
|
8
|
-
*
|
|
5
|
+
* Metrics (same names, labels and help text as C#, rendered by `metrics.render()`) are counted too, so every hub shape
|
|
6
|
+
* looks the same from outside; everything else (codes, list, resume, private lobbies, kicks, migration claims, TURN
|
|
7
|
+
* credentials, rate limits) behaves as in C#.
|
|
9
8
|
*/
|
|
9
|
+
import { Metrics } from "./metrics.js";
|
|
10
|
+
import { randomBelow, randomHex, shortHash, turnCredential } from "./sha1.js";
|
|
11
|
+
export { Metrics };
|
|
10
12
|
export const PROTO = "lk1";
|
|
11
13
|
const NAME_MAX = 24;
|
|
12
14
|
const LOBBY_NAME_MAX = 48;
|
|
@@ -19,11 +21,11 @@ export function defaultHubOptions() {
|
|
|
19
21
|
return {
|
|
20
22
|
turnUrls: [], turnSecret: "", stunUrls: ["stun:stun.l.google.com:19302"], region: "", allowedGames: [], gameQuotas: {},
|
|
21
23
|
maxLobbies: 1000, maxPeers: 10000, hardMaxPlayers: 64, rateScale: 1, turnTtlSeconds: 3600, maxMessageBytes: 16 * 1024,
|
|
22
|
-
disableIpLimits: false, createPerMin: 10, joinPerMin: 60, signalPerSec: 20, openLobbiesPerIp: 8, idleTimeoutMs: 30_000,
|
|
24
|
+
disableIpLimits: false, createPerMin: 10, joinPerMin: 60, signalPerSec: 20, socketsPerIp: 32, openLobbiesPerIp: 8, idleTimeoutMs: 30_000,
|
|
23
25
|
resumeWindowMs: 60_000, codeReuseMs: 24 * 3600 * 1000, listLimitMax: 100, singleLobby: false, password: null, issueCodes: true,
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
|
-
/** Settings for a hub inside the host's game (docs/direct-ip.md s.2): no codes, no list, no ICE servers. */
|
|
28
|
+
/** Settings for a hub inside the host's game (docs/design/direct-ip.md s.2): no codes, no list, no ICE servers. */
|
|
27
29
|
export function directHubOptions(maxPlayers, password) {
|
|
28
30
|
const n = Math.min(250, Math.max(2, maxPlayers));
|
|
29
31
|
return {
|
|
@@ -32,6 +34,12 @@ export function directHubOptions(maxPlayers, password) {
|
|
|
32
34
|
maxLobbies: 1, hardMaxPlayers: n, maxPeers: n + 8,
|
|
33
35
|
};
|
|
34
36
|
}
|
|
37
|
+
/** A session's total `signal` budget is this many targets' worth of `signalPerSec` (spec/signaling.md rate limits). */
|
|
38
|
+
export const SIGNAL_FANOUT = 8;
|
|
39
|
+
/** Hub sessions bridged from Nostr have the address `nostr:<pubkey prefix>`. */
|
|
40
|
+
export const NOSTR_ADDR = "nostr:";
|
|
41
|
+
/** `signal` per second per (sender, Nostr target): relays reject high publish rates. */
|
|
42
|
+
export const NOSTR_SIGNAL_PER_SEC = 10;
|
|
35
43
|
/** Sliding-window counter: at most `limit` events per `windowMs`. */
|
|
36
44
|
export class RateWindow {
|
|
37
45
|
limit;
|
|
@@ -105,7 +113,6 @@ export class Session {
|
|
|
105
113
|
connId;
|
|
106
114
|
send;
|
|
107
115
|
close;
|
|
108
|
-
signalRate;
|
|
109
116
|
closed = false;
|
|
110
117
|
lastSeen = 0;
|
|
111
118
|
helloDone = false;
|
|
@@ -116,14 +123,19 @@ export class Session {
|
|
|
116
123
|
member = null;
|
|
117
124
|
pendingIn = null;
|
|
118
125
|
pendingMember = null;
|
|
119
|
-
constructor(id, ip, connId, send, close
|
|
126
|
+
constructor(id, ip, connId, send, close) {
|
|
120
127
|
this.id = id;
|
|
121
128
|
this.ip = ip;
|
|
122
129
|
this.connId = connId;
|
|
123
130
|
this.send = send;
|
|
124
131
|
this.close = close;
|
|
125
|
-
this.signalRate = signalRate;
|
|
126
132
|
}
|
|
133
|
+
/** Nostr joiners: the remote address of the host's WebRTC link to this session, once known (kick bans it too). */
|
|
134
|
+
linkIp = null;
|
|
135
|
+
/** One window per target member (a member belongs to one lobby, so a new lobby starts fresh). */
|
|
136
|
+
signalRates = new Map();
|
|
137
|
+
/** All signals of this session, so the per-target budget does not grow without bound with the lobby. */
|
|
138
|
+
signalTotal = null;
|
|
127
139
|
}
|
|
128
140
|
class Member {
|
|
129
141
|
peerId = 0;
|
|
@@ -171,7 +183,7 @@ const int = (o, k) => {
|
|
|
171
183
|
const bool = (o, k) => (typeof o[k] === "boolean" ? o[k] : null);
|
|
172
184
|
const has = (o, k) => o[k] !== undefined && o[k] !== null;
|
|
173
185
|
const field = (name) => ({ field: name });
|
|
174
|
-
const hex = (n) =>
|
|
186
|
+
const hex = (n) => randomHex(n);
|
|
175
187
|
const byCode = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
176
188
|
/** Not sanitised (spec/signaling.md): over the limits or non-string values are rejected. */
|
|
177
189
|
function readMeta(node) {
|
|
@@ -208,11 +220,22 @@ export class SignalingHub {
|
|
|
208
220
|
createdOnce = false;
|
|
209
221
|
members = 0;
|
|
210
222
|
logSalt = hex(8);
|
|
223
|
+
metrics = new Metrics();
|
|
211
224
|
constructor(options = {}, deps = {}) {
|
|
212
225
|
this.o = { ...defaultHubOptions(), ...options };
|
|
213
226
|
this.now = deps.now ?? (() => Date.now());
|
|
214
|
-
this.rand = deps.randomInt ?? ((n) =>
|
|
227
|
+
this.rand = deps.randomInt ?? ((n) => randomBelow(n));
|
|
215
228
|
this.log = deps.log ?? (() => { });
|
|
229
|
+
const m = this.metrics;
|
|
230
|
+
m.gauge("onelobby_lobbies", "Live lobbies.", () => this.lobbyCount);
|
|
231
|
+
m.gauge("onelobby_sockets", "Open signaling sockets.", () => this.socketCount);
|
|
232
|
+
m.gauge("onelobby_lobbies_retired", "Join codes held back from reuse.", () => this.retired.size);
|
|
233
|
+
m.describe("onelobby_messages_total", "Client messages received, by type.");
|
|
234
|
+
m.describe("onelobby_errors_total", "Error replies sent, by code.");
|
|
235
|
+
m.describe("onelobby_path_total", "ICE path reports from peers: direct vs relayed (DESIGN s.10).");
|
|
236
|
+
m.describe("onelobby_connections_total", "Sockets accepted since start.");
|
|
237
|
+
m.describe("onelobby_lobbies_created_total", "Lobbies created since start.");
|
|
238
|
+
m.describe("onelobby_rate_limit_hits_total", "Requests refused by a rate limit, by kind.");
|
|
216
239
|
}
|
|
217
240
|
get lobbyCount() {
|
|
218
241
|
return this.lobbies.size;
|
|
@@ -226,15 +249,22 @@ export class SignalingHub {
|
|
|
226
249
|
// ---------------------------------------------------------------- connection lifecycle
|
|
227
250
|
/** Registers a socket. Returns null (after sending server_full and closing) when over maxPeers. */
|
|
228
251
|
connect(ip, send, close) {
|
|
229
|
-
const s = new Session(++this.nextSession, ip, hex(6), send, close
|
|
252
|
+
const s = new Session(++this.nextSession, ip, hex(6), send, close);
|
|
230
253
|
s.lastSeen = this.now();
|
|
254
|
+
this.metrics.inc("onelobby_connections_total");
|
|
231
255
|
if (this.sessions.size >= this.o.maxPeers) {
|
|
232
256
|
this.error(s, "server_full", "server is at its peer limit");
|
|
233
257
|
s.close();
|
|
234
258
|
return null;
|
|
235
259
|
}
|
|
260
|
+
const st = this.ipOf(ip);
|
|
261
|
+
if (!this.o.disableIpLimits && st.sockets >= this.o.socketsPerIp * this.scale) {
|
|
262
|
+
this.rateLimited(s, "sockets", 5000, "max_sockets");
|
|
263
|
+
s.close(1008, "too_many_sockets");
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
236
266
|
this.sessions.set(s.id, s);
|
|
237
|
-
|
|
267
|
+
st.sockets++;
|
|
238
268
|
return s;
|
|
239
269
|
}
|
|
240
270
|
disconnect(s) {
|
|
@@ -259,10 +289,30 @@ export class SignalingHub {
|
|
|
259
289
|
s.lobby = null;
|
|
260
290
|
this.logEvent("detach", lobby, s);
|
|
261
291
|
this.broadcast(lobby, { t: "peer_left", peerId: m.peerId, reason: "timeout" });
|
|
292
|
+
this.metrics.inc("onelobby_peers_left_total", "reason", "timeout");
|
|
262
293
|
if (lobby.hostId === m.peerId)
|
|
263
294
|
this.promoteHost(lobby, m.peerId);
|
|
264
295
|
}
|
|
265
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* The host's WebRTC link to joiner `peerId` has the remote address `ip` (selected candidate pair; the host's own
|
|
299
|
+
* lobby reports it, a relayed or mDNS address must not be passed). Only Nostr sessions take it: their socket address
|
|
300
|
+
* is a pubkey the joiner chose. A joiner whose link address is banned is removed as kicked; a later kick bans it.
|
|
301
|
+
*/
|
|
302
|
+
reportLinkAddress(peerId, ip) {
|
|
303
|
+
for (const lobby of this.lobbies.values()) {
|
|
304
|
+
const mem = lobby.find(peerId);
|
|
305
|
+
const sess = mem?.session;
|
|
306
|
+
if (!mem || !sess || !sess.ip.startsWith(NOSTR_ADDR))
|
|
307
|
+
continue;
|
|
308
|
+
sess.linkIp = ip;
|
|
309
|
+
if (lobby.kicked.has(ip)) {
|
|
310
|
+
this.removeMember(lobby, mem, "kicked");
|
|
311
|
+
this.closeSession(sess);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
266
316
|
closeSession(s) {
|
|
267
317
|
s.close();
|
|
268
318
|
this.disconnect(s);
|
|
@@ -272,8 +322,10 @@ export class SignalingHub {
|
|
|
272
322
|
tick() {
|
|
273
323
|
const now = this.now();
|
|
274
324
|
for (const s of [...this.sessions.values()])
|
|
275
|
-
if (now - s.lastSeen > this.o.idleTimeoutMs)
|
|
325
|
+
if (now - s.lastSeen > this.o.idleTimeoutMs) {
|
|
326
|
+
this.metrics.inc("onelobby_idle_timeouts_total");
|
|
276
327
|
this.closeSession(s);
|
|
328
|
+
}
|
|
277
329
|
for (const lobby of [...this.lobbies.values()])
|
|
278
330
|
for (const m of [...lobby.members])
|
|
279
331
|
if (m.detachedAt > 0 && now - m.detachedAt >= this.o.resumeWindowMs)
|
|
@@ -308,6 +360,7 @@ export class SignalingHub {
|
|
|
308
360
|
this.error(s, "bad_request", "expected a JSON object with a string t", field("t"));
|
|
309
361
|
return;
|
|
310
362
|
}
|
|
363
|
+
this.metrics.inc("onelobby_messages_total", "type", KNOWN_TYPES.has(t) ? t : "unknown");
|
|
311
364
|
if (t === "hello")
|
|
312
365
|
return this.hello(s, msg);
|
|
313
366
|
if (!s.helloDone) {
|
|
@@ -399,6 +452,7 @@ export class SignalingHub {
|
|
|
399
452
|
s.lobby = rl;
|
|
400
453
|
s.member = resumed;
|
|
401
454
|
this.logEvent("resume", rl, s);
|
|
455
|
+
this.metrics.inc("onelobby_resumes_total");
|
|
402
456
|
for (const other of rl.members)
|
|
403
457
|
if (other !== resumed)
|
|
404
458
|
this.sendTo(other, { t: "peer_joined", peer: this.peerObj(resumed) });
|
|
@@ -412,7 +466,7 @@ export class SignalingHub {
|
|
|
412
466
|
if (this.o.turnUrls.length > 0 && this.o.turnSecret.length > 0) {
|
|
413
467
|
const expiry = Math.floor(this.now() / 1000) + this.o.turnTtlSeconds;
|
|
414
468
|
const username = `${expiry}:${s.connId}`;
|
|
415
|
-
const credential =
|
|
469
|
+
const credential = turnCredential(this.o.turnSecret, username);
|
|
416
470
|
ice.push({ urls: [...this.o.turnUrls], username, credential, ttl: this.o.turnTtlSeconds });
|
|
417
471
|
}
|
|
418
472
|
return ice;
|
|
@@ -482,6 +536,7 @@ export class SignalingHub {
|
|
|
482
536
|
this.createdOnce = true;
|
|
483
537
|
this.gameLobbies.set(s.gameId, (this.gameLobbies.get(s.gameId) ?? 0) + 1);
|
|
484
538
|
ip.open++;
|
|
539
|
+
this.metrics.inc("onelobby_lobbies_created_total");
|
|
485
540
|
const host = this.addMember(lobby, s, null);
|
|
486
541
|
lobby.hostId = host.peerId;
|
|
487
542
|
this.logEvent("create", lobby, s);
|
|
@@ -632,13 +687,20 @@ export class SignalingHub {
|
|
|
632
687
|
const me = s.member;
|
|
633
688
|
if (!lobby || !me)
|
|
634
689
|
return this.error(s, "bad_request", "not in a lobby");
|
|
635
|
-
const now = this.now();
|
|
636
|
-
if (!this.o.disableIpLimits && !s.signalRate.tryHit(now))
|
|
637
|
-
return this.rateLimited(s, "signal", s.signalRate.retryAfterMs(now), null);
|
|
638
690
|
const to = int(m, "to");
|
|
639
691
|
const target = to === null ? undefined : lobby.find(to);
|
|
640
692
|
if (!target || !target.session || target === me)
|
|
641
693
|
return this.error(s, "bad_request", "unknown signal target", field("to"));
|
|
694
|
+
// a direct hub has no migration: the only peer links are host <-> joiner, so a joiner cannot dial another joiner
|
|
695
|
+
if (this.o.singleLobby && me.peerId !== lobby.hostId && target.peerId !== lobby.hostId)
|
|
696
|
+
return this.error(s, "bad_request", "signals go to or from the host only", field("to"));
|
|
697
|
+
const now = this.now();
|
|
698
|
+
const rate = this.signalRate(s, target, now);
|
|
699
|
+
if (!rate.tryHit(now))
|
|
700
|
+
return this.rateLimited(s, "signal", rate.retryAfterMs(now), null);
|
|
701
|
+
const total = (s.signalTotal ??= new RateWindow(this.o.signalPerSec * SIGNAL_FANOUT * this.scale, 1000));
|
|
702
|
+
if (!total.tryHit(now))
|
|
703
|
+
return this.rateLimited(s, "signal", total.retryAfterMs(now), null);
|
|
642
704
|
if (!has(m, "sdp") && !has(m, "ice"))
|
|
643
705
|
return this.error(s, "bad_request", "signal needs sdp or ice", field("sdp"));
|
|
644
706
|
const o = { t: "signal", from: me.peerId };
|
|
@@ -647,6 +709,23 @@ export class SignalingHub {
|
|
|
647
709
|
if (has(m, "ice"))
|
|
648
710
|
o.ice = m.ice;
|
|
649
711
|
this.send(target.session, o);
|
|
712
|
+
this.metrics.inc("onelobby_signals_relayed_total");
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* spec/signaling.md rate limits: `signal` is counted per sender and target, so a host answering several joiners at once
|
|
716
|
+
* gets the budget for each, and per sender in total (SIGNAL_FANOUT targets' worth).
|
|
717
|
+
*/
|
|
718
|
+
signalRate(s, to, now) {
|
|
719
|
+
let w = s.signalRates.get(to);
|
|
720
|
+
if (!w) {
|
|
721
|
+
for (const [k, v] of s.signalRates)
|
|
722
|
+
if (v.isIdle(now))
|
|
723
|
+
s.signalRates.delete(k);
|
|
724
|
+
// relays refuse high publish rates, so a Nostr-bridged target gets half the budget
|
|
725
|
+
w = new RateWindow((to.session?.ip.startsWith(NOSTR_ADDR) ? NOSTR_SIGNAL_PER_SEC : this.o.signalPerSec) * this.scale, 1000);
|
|
726
|
+
s.signalRates.set(to, w);
|
|
727
|
+
}
|
|
728
|
+
return w;
|
|
650
729
|
}
|
|
651
730
|
requireHost(s) {
|
|
652
731
|
if (!s.lobby || !s.member) {
|
|
@@ -705,8 +784,13 @@ export class SignalingHub {
|
|
|
705
784
|
data.closeCode = code;
|
|
706
785
|
if (closeReason !== null)
|
|
707
786
|
data.closeReason = closeReason;
|
|
708
|
-
if (ban !== false)
|
|
787
|
+
if (ban !== false) {
|
|
709
788
|
lobby.kicked.add(target.ip);
|
|
789
|
+
// a Nostr joiner picks its own key, so also ban the address its WebRTC link came from, when the host saw it
|
|
790
|
+
const link = target.session?.linkIp;
|
|
791
|
+
if (link && target.ip.startsWith(NOSTR_ADDR))
|
|
792
|
+
lobby.kicked.add(link);
|
|
793
|
+
}
|
|
710
794
|
this.removeMember(lobby, target, ban === false ? "disconnected" : "kicked", Object.keys(data).length ? data : undefined);
|
|
711
795
|
}
|
|
712
796
|
approve(s, m) {
|
|
@@ -755,11 +839,14 @@ export class SignalingHub {
|
|
|
755
839
|
lobby.epoch = epoch;
|
|
756
840
|
lobby.pendingClaimPeer = lobby.pendingClaimEpoch = 0;
|
|
757
841
|
this.broadcast(lobby, { t: "host_changed", hostId: lobby.hostId, epoch: lobby.epoch });
|
|
842
|
+
this.metrics.inc("onelobby_host_changes_total");
|
|
758
843
|
this.logEvent("host_claim", lobby, s);
|
|
759
844
|
}
|
|
760
845
|
pathReport(s, m) {
|
|
761
846
|
const p = str(m, "path");
|
|
762
|
-
if (p
|
|
847
|
+
if (p === "relayed" || p === "direct")
|
|
848
|
+
this.metrics.inc("onelobby_path_total", "path", p);
|
|
849
|
+
else
|
|
763
850
|
this.error(s, "bad_request", "path must be relayed or direct", field("path"));
|
|
764
851
|
}
|
|
765
852
|
// ---------------------------------------------------------------- membership changes
|
|
@@ -779,6 +866,7 @@ export class SignalingHub {
|
|
|
779
866
|
}
|
|
780
867
|
if (wasConnected) {
|
|
781
868
|
this.broadcast(lobby, { t: "peer_left", peerId: mem.peerId, reason });
|
|
869
|
+
this.metrics.inc("onelobby_peers_left_total", "reason", reason);
|
|
782
870
|
if ((reason === "kicked" || reason === "disconnected") && ms)
|
|
783
871
|
this.error(ms, "kicked", reason === "kicked" ? "the host removed you from the lobby" : "the host disconnected you", kickData);
|
|
784
872
|
}
|
|
@@ -811,6 +899,7 @@ export class SignalingHub {
|
|
|
811
899
|
else
|
|
812
900
|
lobby.epoch++;
|
|
813
901
|
this.broadcast(lobby, { t: "host_changed", hostId: next.peerId, epoch: lobby.epoch });
|
|
902
|
+
this.metrics.inc("onelobby_host_changes_total");
|
|
814
903
|
const hs = next.session;
|
|
815
904
|
if (hs)
|
|
816
905
|
for (const pm of lobby.pending)
|
|
@@ -884,12 +973,14 @@ export class SignalingHub {
|
|
|
884
973
|
this.send(m.session, o);
|
|
885
974
|
}
|
|
886
975
|
error(s, code, msg, data) {
|
|
976
|
+
this.metrics.inc("onelobby_errors_total", "code", code);
|
|
887
977
|
const o = { t: "error", code, msg };
|
|
888
978
|
if (data !== undefined)
|
|
889
979
|
o.data = data;
|
|
890
980
|
s.send(JSON.stringify(o));
|
|
891
981
|
}
|
|
892
982
|
rateLimited(s, kind, retryAfterMs, reason) {
|
|
983
|
+
this.metrics.inc("onelobby_rate_limit_hits_total", "kind", kind);
|
|
893
984
|
this.log(`rate_limit kind=${kind} ip=${this.hashIp(s.ip)}`);
|
|
894
985
|
const data = { retryAfterMs };
|
|
895
986
|
if (reason !== null)
|
|
@@ -900,7 +991,7 @@ export class SignalingHub {
|
|
|
900
991
|
this.log(`lobby ${ev} code=${l.code} game=${l.gameId} players=${l.members.length} ip=${s ? this.hashIp(s.ip) : "-"}`);
|
|
901
992
|
}
|
|
902
993
|
hashIp(ip) {
|
|
903
|
-
return
|
|
994
|
+
return shortHash(this.logSalt + ip);
|
|
904
995
|
}
|
|
905
996
|
}
|
|
906
997
|
//# sourceMappingURL=hub.js.map
|