onelobby 0.4.1 → 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.
Files changed (99) hide show
  1. package/README.md +2 -2
  2. package/blueprint.schema.json +244 -0
  3. package/dist/blueprint.d.ts +170 -0
  4. package/dist/blueprint.js +286 -0
  5. package/dist/blueprint.js.map +1 -0
  6. package/dist/codec.d.ts +23 -0
  7. package/dist/codec.js +28 -0
  8. package/dist/codec.js.map +1 -1
  9. package/dist/direct/browser-host.d.ts +46 -0
  10. package/dist/direct/browser-host.js +93 -0
  11. package/dist/direct/browser-host.js.map +1 -0
  12. package/dist/direct/hub-host.d.ts +34 -23
  13. package/dist/direct/hub-host.js +75 -59
  14. package/dist/direct/hub-host.js.map +1 -1
  15. package/dist/direct/hub-local.d.ts +22 -0
  16. package/dist/direct/hub-local.js +46 -0
  17. package/dist/direct/hub-local.js.map +1 -0
  18. package/dist/direct/hub.d.ts +37 -10
  19. package/dist/direct/hub.js +114 -23
  20. package/dist/direct/hub.js.map +1 -1
  21. package/dist/direct/index.d.ts +37 -4
  22. package/dist/direct/index.js +162 -21
  23. package/dist/direct/index.js.map +1 -1
  24. package/dist/direct/metrics.d.ts +12 -0
  25. package/dist/direct/metrics.js +45 -0
  26. package/dist/direct/metrics.js.map +1 -0
  27. package/dist/direct/portmap.d.ts +9 -2
  28. package/dist/direct/portmap.js +7 -2
  29. package/dist/direct/portmap.js.map +1 -1
  30. package/dist/direct/sha1.d.ts +16 -0
  31. package/dist/direct/sha1.js +111 -0
  32. package/dist/direct/sha1.js.map +1 -0
  33. package/dist/direct-address.d.ts +2 -2
  34. package/dist/direct-address.js.map +1 -1
  35. package/dist/friendly.d.ts +28 -0
  36. package/dist/friendly.js +40 -0
  37. package/dist/friendly.js.map +1 -1
  38. package/dist/index.d.ts +11 -3
  39. package/dist/index.js +8 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/internals.d.ts +9 -0
  42. package/dist/internals.js +9 -0
  43. package/dist/internals.js.map +1 -1
  44. package/dist/join-code-kind.d.ts +10 -0
  45. package/dist/join-code-kind.js +10 -0
  46. package/dist/join-code-kind.js.map +1 -0
  47. package/dist/joincode.d.ts +10 -1
  48. package/dist/joincode.js +23 -3
  49. package/dist/joincode.js.map +1 -1
  50. package/dist/lobby.d.ts +65 -4
  51. package/dist/lobby.js +151 -12
  52. package/dist/lobby.js.map +1 -1
  53. package/dist/lockstep.d.ts +8 -0
  54. package/dist/lockstep.js +23 -2
  55. package/dist/lockstep.js.map +1 -1
  56. package/dist/nostr/crypto.d.ts +20 -0
  57. package/dist/nostr/crypto.js +78 -0
  58. package/dist/nostr/crypto.js.map +1 -0
  59. package/dist/nostr/host.d.ts +66 -0
  60. package/dist/nostr/host.js +196 -0
  61. package/dist/nostr/host.js.map +1 -0
  62. package/dist/nostr/limits.d.ts +31 -0
  63. package/dist/nostr/limits.js +36 -0
  64. package/dist/nostr/limits.js.map +1 -0
  65. package/dist/nostr/link.d.ts +70 -0
  66. package/dist/nostr/link.js +180 -0
  67. package/dist/nostr/link.js.map +1 -0
  68. package/dist/nostr/relays.d.ts +79 -0
  69. package/dist/nostr/relays.js +315 -0
  70. package/dist/nostr/relays.js.map +1 -0
  71. package/dist/nostr/room.d.ts +43 -0
  72. package/dist/nostr/room.js +104 -0
  73. package/dist/nostr/room.js.map +1 -0
  74. package/dist/nostr/secp256k1.d.ts +10 -0
  75. package/dist/nostr/secp256k1.js +171 -0
  76. package/dist/nostr/secp256k1.js.map +1 -0
  77. package/dist/nostr/socket.d.ts +48 -0
  78. package/dist/nostr/socket.js +141 -0
  79. package/dist/nostr/socket.js.map +1 -0
  80. package/dist/onelobby.bundle.js +146 -0
  81. package/dist/onelobby.bundle.js.map +7 -0
  82. package/dist/onelobby.bundle.mjs +11 -5
  83. package/dist/onelobby.bundle.mjs.map +4 -4
  84. package/dist/onelobby.d.ts +54 -4
  85. package/dist/onelobby.global.js +2932 -49
  86. package/dist/onelobby.global.js.map +4 -4
  87. package/dist/onelobby.global.min.js +11 -5
  88. package/dist/onelobby.global.min.js.map +4 -4
  89. package/dist/onelobby.js +158 -15
  90. package/dist/onelobby.js.map +1 -1
  91. package/dist/pause.d.ts +85 -0
  92. package/dist/pause.js +160 -0
  93. package/dist/pause.js.map +1 -0
  94. package/dist/transport.d.ts +3 -1
  95. package/dist/transport.js.map +1 -1
  96. package/dist/webrtc.d.ts +1 -0
  97. package/dist/webrtc.js +6 -1
  98. package/dist/webrtc.js.map +1 -1
  99. package/package.json +4 -2
package/dist/pause.js ADDED
@@ -0,0 +1,160 @@
1
+ import { PAUSE_FLAG_PAUSER_LEFT, PAUSE_REFUSE } from "./codec.js";
2
+ import { Emitter } from "./emitter.js";
3
+ import { OneLobbyError } from "./signaling.js";
4
+ /** The least time between two accepted pause requests from one peer. */
5
+ export const PAUSE_MIN_REQUEST_GAP_MS = 500;
6
+ const NONE = Object.freeze({ paused: false, byPeer: 0, pauserLeft: false });
7
+ function refusalName(code) {
8
+ return code === PAUSE_REFUSE.notInGame ? "not_in_game" : code === PAUSE_REFUSE.rateLimited ? "rate_limited" : code === PAUSE_REFUSE.notAllowed ? "not_allowed" : "unknown";
9
+ }
10
+ /**
11
+ * Shared co-op pause (docs/recipes/pause.md): any player asks the host to pause or resume the match for everyone.
12
+ * The host validates (phase, rate, policy), keeps the state and broadcasts it over the reliable channel. OneLobby moves
13
+ * the state and, in lockstep, stops the host's frames; scaling time, menus and banners stay in the game. It is outside
14
+ * the sim: never put it in a lockstep command or hash.
15
+ */
16
+ export class Pause extends Emitter {
17
+ hub;
18
+ deps;
19
+ /** Set by a blueprint (`session.pause`); the default is "anyone". */
20
+ mode = "anyone";
21
+ /**
22
+ * Host: replaces the default policy. Called with (requesting peer, true to pause / false to resume); return true to
23
+ * allow. Default: anyone pauses, the pauser or the host resumes (host only in "host" mode). Phase and rate run first.
24
+ */
25
+ policy = null;
26
+ current = NONE;
27
+ lastAccepted = new Map();
28
+ constructor(hub, deps) {
29
+ super();
30
+ this.hub = hub;
31
+ this.deps = deps;
32
+ }
33
+ /** The current shared state (the same on every peer once the host's broadcast has arrived). */
34
+ get state() {
35
+ return this.current;
36
+ }
37
+ /**
38
+ * True when the local peer may resume now under the default policy: the pauser or the host (in "host" mode, the
39
+ * host). A custom `policy` lives on the host, so on a joiner this is a hint.
40
+ */
41
+ get canResume() {
42
+ return this.current.paused && this.defaultAllows(this.hub.localPeerId, false);
43
+ }
44
+ /**
45
+ * Ask to pause (true) or resume (false) the match for everyone. Throws `pause_disabled` when the blueprint says
46
+ * `session.pause` is "off" (the default; set it to "anyone" or "host" in onelobby.json). Works only while the lobby
47
+ * is in game (phase "in_game", after `start()`): before that the host refuses with `not_in_game`. The result arrives
48
+ * as `changed` or `refused`; asking for the state that already holds does nothing.
49
+ */
50
+ request(paused) {
51
+ if (this.mode === "off")
52
+ throw new OneLobbyError("pause_disabled", "pause is off in onelobby.json (session.pause is \"off\"); set session.pause to \"anyone\" or \"host\" to turn it on");
53
+ if (this.hub.isHost)
54
+ this.hostHandle(this.hub.localPeerId, paused);
55
+ else
56
+ this.hub.sendToHost({ type: "pause_request", paused: paused ? 1 : 0 });
57
+ }
58
+ /**
59
+ * Host only: unpause at once, skipping the policy and the rate limit. Call it before a restart or game over so the
60
+ * next round does not start frozen. Does nothing when not paused.
61
+ */
62
+ clear() {
63
+ if (!this.hub.isHost)
64
+ throw new Error("only the host can clear the pause");
65
+ if (!this.current.paused)
66
+ return;
67
+ this.set({ paused: false, byPeer: this.hub.localPeerId, pauserLeft: false }, true);
68
+ }
69
+ // ---- internals (called by Lobby) ------------------------------------------------------------------------------
70
+ handle(from, msg) {
71
+ switch (msg.type) {
72
+ case "pause_request":
73
+ if (this.hub.isHost && this.hub.connectedPeers().includes(from))
74
+ this.hostHandle(from, msg.paused !== 0);
75
+ break;
76
+ case "pause_state":
77
+ if (!this.hub.isHost && from === this.hub.hostPeerId) {
78
+ this.set({ paused: msg.paused !== 0, byPeer: msg.byPeer, pauserLeft: (msg.flags & PAUSE_FLAG_PAUSER_LEFT) !== 0 }, false);
79
+ }
80
+ break;
81
+ case "pause_refuse":
82
+ if (!this.hub.isHost && from === this.hub.hostPeerId)
83
+ this.emit("refused", refusalName(msg.reason));
84
+ break;
85
+ }
86
+ }
87
+ /** Host: a late joiner's link is up; it gets the state when there is one to tell. */
88
+ sendState(peerId) {
89
+ if (this.current.paused)
90
+ this.hub.sendTo(peerId, this.stateMessage(), 0);
91
+ }
92
+ /** A peer is gone: the host takes over the pause its leaver held. */
93
+ onPeerLeft(peerId) {
94
+ this.lastAccepted.delete(peerId);
95
+ const s = this.current;
96
+ if (this.hub.isHost && s.paused && !s.pauserLeft && s.byPeer === peerId) {
97
+ this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);
98
+ }
99
+ }
100
+ /** This peer just became the host (migration): it keeps the state as is, and takes over a pause whose holder is gone. */
101
+ onBecameHost() {
102
+ this.deps.hold(this.current.paused);
103
+ const s = this.current;
104
+ if (s.paused && !s.pauserLeft && !this.deps.hasPeer(s.byPeer)) {
105
+ this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);
106
+ }
107
+ }
108
+ /** The lobby went back to the lobby phase: nothing stays paused. */
109
+ onLobbyPhase() {
110
+ if (!this.current.paused)
111
+ return;
112
+ this.set({ paused: false, byPeer: this.hub.hostPeerId, pauserLeft: false }, this.hub.isHost);
113
+ }
114
+ // ---- host path --------------------------------------------------------------------------------------------------
115
+ defaultAllows(peer, wantPaused) {
116
+ if (this.mode === "host")
117
+ return peer === this.hub.hostPeerId;
118
+ return wantPaused || peer === this.current.byPeer || peer === this.hub.hostPeerId;
119
+ }
120
+ hostHandle(from, want) {
121
+ if (this.mode === "off")
122
+ return this.refuse(from, want, PAUSE_REFUSE.notAllowed);
123
+ if (this.deps.phase() !== "in_game")
124
+ return this.refuse(from, want, PAUSE_REFUSE.notInGame);
125
+ if (want === this.current.paused)
126
+ return;
127
+ const now = this.hub.clock.now();
128
+ const last = this.lastAccepted.get(from);
129
+ if (last !== undefined && now - last < PAUSE_MIN_REQUEST_GAP_MS)
130
+ return this.refuse(from, want, PAUSE_REFUSE.rateLimited);
131
+ const ok = this.policy ? this.policy(from, want) : this.defaultAllows(from, want);
132
+ if (!ok)
133
+ return this.refuse(from, want, PAUSE_REFUSE.notAllowed);
134
+ this.lastAccepted.set(from, now);
135
+ this.set({ paused: want, byPeer: from, pauserLeft: false }, true);
136
+ }
137
+ refuse(to, want, reason) {
138
+ if (to === this.hub.localPeerId)
139
+ this.emit("refused", refusalName(reason));
140
+ else
141
+ this.hub.sendTo(to, { type: "pause_refuse", paused: want ? 1 : 0, reason }, 0);
142
+ }
143
+ stateMessage() {
144
+ const s = this.current;
145
+ return { type: "pause_state", paused: s.paused ? 1 : 0, byPeer: s.byPeer, flags: s.pauserLeft ? PAUSE_FLAG_PAUSER_LEFT : 0 };
146
+ }
147
+ set(next, broadcast) {
148
+ const prev = this.current;
149
+ this.current = Object.freeze(next);
150
+ this.deps.hold(next.paused);
151
+ if (broadcast) {
152
+ const msg = this.stateMessage();
153
+ for (const p of this.hub.connectedPeers())
154
+ this.hub.sendTo(p, msg, 0);
155
+ }
156
+ if (prev.paused !== next.paused || prev.byPeer !== next.byPeer || prev.pauserLeft !== next.pauserLeft)
157
+ this.emit("changed", next);
158
+ }
159
+ }
160
+ //# sourceMappingURL=pause.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pause.js","sourceRoot":"","sources":["../src/pause.ts"],"names":[],"mappings":"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","sourcesContent":["import { PAUSE_FLAG_PAUSER_LEFT, PAUSE_REFUSE, type Message } from \"./codec.js\";\nimport { Emitter } from \"./emitter.js\";\nimport type { Hub } from \"./hub.js\";\nimport { OneLobbyError } from \"./signaling.js\";\n\n/** The least time between two accepted pause requests from one peer. */\nexport const PAUSE_MIN_REQUEST_GAP_MS = 500;\n\n/**\n * The shared pause: `paused` is on while someone holds the match; `byPeer` is the peer that paused (while paused) or\n * that last resumed it; `pauserLeft` says the pauser left and the host now holds the pause (`byPeer` is then the host).\n */\nexport interface PauseState {\n readonly paused: boolean;\n readonly byPeer: number;\n readonly pauserLeft: boolean;\n}\n\n/** Why a request was refused: `not_in_game`, `rate_limited` or `not_allowed` (the `ErrorCodes` constants). */\nexport type PauseRefusal = \"not_in_game\" | \"rate_limited\" | \"not_allowed\" | \"unknown\";\n\nexport type PauseEvents = {\n /** The state changed (paused, resumed, pauser left, cleared). Fires on every peer. */\n changed: [PauseState];\n /** A request of this peer was refused; `code` is a `PauseRefusal`. */\n refused: [code: PauseRefusal];\n};\n\n/** `session.pause` of a blueprint: \"off\" makes `request` throw `pause_disabled`, \"host\" makes the default policy host-only. */\nexport type PauseMode = \"off\" | \"host\" | \"anyone\";\n\nexport interface PauseDeps {\n /** The lobby's phase now (\"lobby\" | \"in_game\"). */\n phase: () => string;\n /** Lockstep: stop (true) or resume (false) the host's frame emission. */\n hold: (held: boolean) => void;\n /** Whether `peerId` is still in the roster. */\n hasPeer: (peerId: number) => boolean;\n}\n\nconst NONE: PauseState = Object.freeze({ paused: false, byPeer: 0, pauserLeft: false });\n\nfunction refusalName(code: number): PauseRefusal {\n return code === PAUSE_REFUSE.notInGame ? \"not_in_game\" : code === PAUSE_REFUSE.rateLimited ? \"rate_limited\" : code === PAUSE_REFUSE.notAllowed ? \"not_allowed\" : \"unknown\";\n}\n\n/**\n * Shared co-op pause (docs/recipes/pause.md): any player asks the host to pause or resume the match for everyone.\n * The host validates (phase, rate, policy), keeps the state and broadcasts it over the reliable channel. OneLobby moves\n * the state and, in lockstep, stops the host's frames; scaling time, menus and banners stay in the game. It is outside\n * the sim: never put it in a lockstep command or hash.\n */\nexport class Pause extends Emitter<PauseEvents> {\n /** Set by a blueprint (`session.pause`); the default is \"anyone\". */\n mode: PauseMode = \"anyone\";\n /**\n * Host: replaces the default policy. Called with (requesting peer, true to pause / false to resume); return true to\n * allow. Default: anyone pauses, the pauser or the host resumes (host only in \"host\" mode). Phase and rate run first.\n */\n policy: ((peerId: number, paused: boolean) => boolean) | null = null;\n\n private current: PauseState = NONE;\n private lastAccepted = new Map<number, number>();\n\n constructor(private hub: Hub, private deps: PauseDeps) {\n super();\n }\n\n /** The current shared state (the same on every peer once the host's broadcast has arrived). */\n get state(): PauseState {\n return this.current;\n }\n\n /**\n * True when the local peer may resume now under the default policy: the pauser or the host (in \"host\" mode, the\n * host). A custom `policy` lives on the host, so on a joiner this is a hint.\n */\n get canResume(): boolean {\n return this.current.paused && this.defaultAllows(this.hub.localPeerId, false);\n }\n\n /**\n * Ask to pause (true) or resume (false) the match for everyone. Throws `pause_disabled` when the blueprint says\n * `session.pause` is \"off\" (the default; set it to \"anyone\" or \"host\" in onelobby.json). Works only while the lobby\n * is in game (phase \"in_game\", after `start()`): before that the host refuses with `not_in_game`. The result arrives\n * as `changed` or `refused`; asking for the state that already holds does nothing.\n */\n request(paused: boolean): void {\n if (this.mode === \"off\") throw new OneLobbyError(\"pause_disabled\", \"pause is off in onelobby.json (session.pause is \\\"off\\\"); set session.pause to \\\"anyone\\\" or \\\"host\\\" to turn it on\");\n if (this.hub.isHost) this.hostHandle(this.hub.localPeerId, paused);\n else this.hub.sendToHost({ type: \"pause_request\", paused: paused ? 1 : 0 });\n }\n\n /**\n * Host only: unpause at once, skipping the policy and the rate limit. Call it before a restart or game over so the\n * next round does not start frozen. Does nothing when not paused.\n */\n clear(): void {\n if (!this.hub.isHost) throw new Error(\"only the host can clear the pause\");\n if (!this.current.paused) return;\n this.set({ paused: false, byPeer: this.hub.localPeerId, pauserLeft: false }, true);\n }\n\n // ---- internals (called by Lobby) ------------------------------------------------------------------------------\n\n handle(from: number, msg: Message): void {\n switch (msg.type) {\n case \"pause_request\":\n if (this.hub.isHost && this.hub.connectedPeers().includes(from)) this.hostHandle(from, msg.paused !== 0);\n break;\n case \"pause_state\":\n if (!this.hub.isHost && from === this.hub.hostPeerId) {\n this.set({ paused: msg.paused !== 0, byPeer: msg.byPeer, pauserLeft: (msg.flags & PAUSE_FLAG_PAUSER_LEFT) !== 0 }, false);\n }\n break;\n case \"pause_refuse\":\n if (!this.hub.isHost && from === this.hub.hostPeerId) this.emit(\"refused\", refusalName(msg.reason));\n break;\n }\n }\n\n /** Host: a late joiner's link is up; it gets the state when there is one to tell. */\n sendState(peerId: number): void {\n if (this.current.paused) this.hub.sendTo(peerId, this.stateMessage(), 0);\n }\n\n /** A peer is gone: the host takes over the pause its leaver held. */\n onPeerLeft(peerId: number): void {\n this.lastAccepted.delete(peerId);\n const s = this.current;\n if (this.hub.isHost && s.paused && !s.pauserLeft && s.byPeer === peerId) {\n this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);\n }\n }\n\n /** This peer just became the host (migration): it keeps the state as is, and takes over a pause whose holder is gone. */\n onBecameHost(): void {\n this.deps.hold(this.current.paused);\n const s = this.current;\n if (s.paused && !s.pauserLeft && !this.deps.hasPeer(s.byPeer)) {\n this.set({ paused: true, byPeer: this.hub.localPeerId, pauserLeft: true }, true);\n }\n }\n\n /** The lobby went back to the lobby phase: nothing stays paused. */\n onLobbyPhase(): void {\n if (!this.current.paused) return;\n this.set({ paused: false, byPeer: this.hub.hostPeerId, pauserLeft: false }, this.hub.isHost);\n }\n\n // ---- host path --------------------------------------------------------------------------------------------------\n\n private defaultAllows(peer: number, wantPaused: boolean): boolean {\n if (this.mode === \"host\") return peer === this.hub.hostPeerId;\n return wantPaused || peer === this.current.byPeer || peer === this.hub.hostPeerId;\n }\n\n private hostHandle(from: number, want: boolean): void {\n if (this.mode === \"off\") return this.refuse(from, want, PAUSE_REFUSE.notAllowed);\n if (this.deps.phase() !== \"in_game\") return this.refuse(from, want, PAUSE_REFUSE.notInGame);\n if (want === this.current.paused) return;\n const now = this.hub.clock.now();\n const last = this.lastAccepted.get(from);\n if (last !== undefined && now - last < PAUSE_MIN_REQUEST_GAP_MS) return this.refuse(from, want, PAUSE_REFUSE.rateLimited);\n const ok = this.policy ? this.policy(from, want) : this.defaultAllows(from, want);\n if (!ok) return this.refuse(from, want, PAUSE_REFUSE.notAllowed);\n this.lastAccepted.set(from, now);\n this.set({ paused: want, byPeer: from, pauserLeft: false }, true);\n }\n\n private refuse(to: number, want: boolean, reason: number): void {\n if (to === this.hub.localPeerId) this.emit(\"refused\", refusalName(reason));\n else this.hub.sendTo(to, { type: \"pause_refuse\", paused: want ? 1 : 0, reason }, 0);\n }\n\n private stateMessage(): Message {\n const s = this.current;\n return { type: \"pause_state\", paused: s.paused ? 1 : 0, byPeer: s.byPeer, flags: s.pauserLeft ? PAUSE_FLAG_PAUSER_LEFT : 0 };\n }\n\n private set(next: PauseState, broadcast: boolean): void {\n const prev = this.current;\n this.current = Object.freeze(next);\n this.deps.hold(next.paused);\n if (broadcast) {\n const msg = this.stateMessage();\n for (const p of this.hub.connectedPeers()) this.hub.sendTo(p, msg, 0);\n }\n if (prev.paused !== next.paused || prev.byPeer !== next.byPeer || prev.pauserLeft !== next.pauserLeft) this.emit(\"changed\", next);\n }\n}\n"]}
@@ -36,6 +36,8 @@ export interface PeerLink {
36
36
  onMessage: ((channel: ChannelName, bytes: Uint8Array) => void) | null;
37
37
  /** Fired once after ICE completes: whether the selected candidate pair uses a TURN relay (reported to the server). */
38
38
  onPath?: ((path: "direct" | "relayed") => void) | null;
39
+ /** Fired when the selected candidate pair's remote address is known and is neither relayed nor an mDNS name. */
40
+ onRemoteAddress?: ((ip: string) => void) | null;
39
41
  /** The offerer calls this to start negotiation. The answerer just feeds handleSignal. */
40
42
  startOffer(): void;
41
43
  handleSignal(s: SignalPayload): void | Promise<void>;
@@ -47,7 +49,7 @@ export interface LinkOptions {
47
49
  iceServers: IceServer[];
48
50
  relayOnly: boolean;
49
51
  /**
50
- * Direct-IP host (docs/direct-ip.md s.2): bind ICE to these local UDP ports. One port (begin = end) also turns on
52
+ * Direct-IP host (docs/design/direct-ip.md s.2): bind ICE to these local UDP ports. One port (begin = end) also turns on
51
53
  * ICE UDP mux so every peer shares it. Honoured by libdatachannel-based RTCPeerConnection (node-datachannel);
52
54
  * browsers ignore it.
53
55
  */
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,CAAC,MAAM,WAAW,GAAgC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AA6E3F,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,GAAG,CAA0B,CAAC","sourcesContent":["/**\n * Transport seams. The protocol logic never touches RTCPeerConnection or WebSocket directly, so the whole\n * lobby/lockstep/chat/voice stack runs under vitest against `MemoryNetwork` / `MemoryServer` (see testing.ts).\n */\n\nexport type ChannelName = \"reliable\" | \"fast\" | \"voice\";\n\nexport const CHANNEL_IDS: Record<ChannelName, number> = { reliable: 0, fast: 1, voice: 2 };\n\nexport interface IceServer {\n urls: string | string[];\n username?: string;\n credential?: string;\n ttl?: number;\n}\n\nexport interface SdpInit {\n type: \"offer\" | \"answer\";\n sdp: string;\n}\nexport interface IceInit {\n candidate: string;\n sdpMid?: string | null;\n sdpMLineIndex?: number | null;\n}\n\nexport interface SignalPayload {\n sdp?: SdpInit;\n ice?: IceInit;\n}\n\n/** One peer connection with the three negotiated channels (s.8). */\nexport interface PeerLink {\n readonly remoteId: number;\n readonly isOpen: boolean;\n /** Fired for each local SDP/ICE blob that must reach the remote through signaling. */\n onSignal: ((s: SignalPayload) => void) | null;\n /** Fired once, when all three channels are open. */\n onOpen: (() => void) | null;\n /** Fired once when the link dies (after open or while connecting). */\n onClose: ((reason: string) => void) | null;\n onMessage: ((channel: ChannelName, bytes: Uint8Array) => void) | null;\n /** Fired once after ICE completes: whether the selected candidate pair uses a TURN relay (reported to the server). */\n onPath?: ((path: \"direct\" | \"relayed\") => void) | null;\n /** The offerer calls this to start negotiation. The answerer just feeds handleSignal. */\n startOffer(): void;\n handleSignal(s: SignalPayload): void | Promise<void>;\n send(channel: ChannelName, bytes: Uint8Array): void;\n close(): void;\n}\n\nexport interface LinkOptions {\n remoteId: number;\n iceServers: IceServer[];\n relayOnly: boolean;\n /**\n * Direct-IP host (docs/direct-ip.md s.2): bind ICE to these local UDP ports. One port (begin = end) also turns on\n * ICE UDP mux so every peer shares it. Honoured by libdatachannel-based RTCPeerConnection (node-datachannel);\n * browsers ignore it.\n */\n portRange?: { begin: number; end: number };\n /**\n * Direct-IP joiner: remote ICE candidate lines (`candidate:1 1 UDP ... typ host`) added after the remote\n * description, for the address the player typed or pasted as a code.\n */\n extraRemoteCandidates?: string[];\n}\n\nexport interface LinkFactory {\n create(opts: LinkOptions): PeerLink;\n}\n\n/** Subset of the WebSocket API the signaling client needs. */\nexport interface SocketLike {\n readonly readyState: number;\n onopen: ((ev?: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n onclose: ((ev?: unknown) => void) | null;\n onerror: ((ev?: unknown) => void) | null;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n}\nexport type SocketFactory = (url: string) => SocketLike;\n\nexport const defaultSocketFactory: SocketFactory = (url) => new WebSocket(url) as unknown as SocketLike;\n"]}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,CAAC,MAAM,WAAW,GAAgC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AA+E3F,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,GAAG,CAA0B,CAAC","sourcesContent":["/**\n * Transport seams. The protocol logic never touches RTCPeerConnection or WebSocket directly, so the whole\n * lobby/lockstep/chat/voice stack runs under vitest against `MemoryNetwork` / `MemoryServer` (see testing.ts).\n */\n\nexport type ChannelName = \"reliable\" | \"fast\" | \"voice\";\n\nexport const CHANNEL_IDS: Record<ChannelName, number> = { reliable: 0, fast: 1, voice: 2 };\n\nexport interface IceServer {\n urls: string | string[];\n username?: string;\n credential?: string;\n ttl?: number;\n}\n\nexport interface SdpInit {\n type: \"offer\" | \"answer\";\n sdp: string;\n}\nexport interface IceInit {\n candidate: string;\n sdpMid?: string | null;\n sdpMLineIndex?: number | null;\n}\n\nexport interface SignalPayload {\n sdp?: SdpInit;\n ice?: IceInit;\n}\n\n/** One peer connection with the three negotiated channels (s.8). */\nexport interface PeerLink {\n readonly remoteId: number;\n readonly isOpen: boolean;\n /** Fired for each local SDP/ICE blob that must reach the remote through signaling. */\n onSignal: ((s: SignalPayload) => void) | null;\n /** Fired once, when all three channels are open. */\n onOpen: (() => void) | null;\n /** Fired once when the link dies (after open or while connecting). */\n onClose: ((reason: string) => void) | null;\n onMessage: ((channel: ChannelName, bytes: Uint8Array) => void) | null;\n /** Fired once after ICE completes: whether the selected candidate pair uses a TURN relay (reported to the server). */\n onPath?: ((path: \"direct\" | \"relayed\") => void) | null;\n /** Fired when the selected candidate pair's remote address is known and is neither relayed nor an mDNS name. */\n onRemoteAddress?: ((ip: string) => void) | null;\n /** The offerer calls this to start negotiation. The answerer just feeds handleSignal. */\n startOffer(): void;\n handleSignal(s: SignalPayload): void | Promise<void>;\n send(channel: ChannelName, bytes: Uint8Array): void;\n close(): void;\n}\n\nexport interface LinkOptions {\n remoteId: number;\n iceServers: IceServer[];\n relayOnly: boolean;\n /**\n * Direct-IP host (docs/design/direct-ip.md s.2): bind ICE to these local UDP ports. One port (begin = end) also turns on\n * ICE UDP mux so every peer shares it. Honoured by libdatachannel-based RTCPeerConnection (node-datachannel);\n * browsers ignore it.\n */\n portRange?: { begin: number; end: number };\n /**\n * Direct-IP joiner: remote ICE candidate lines (`candidate:1 1 UDP ... typ host`) added after the remote\n * description, for the address the player typed or pasted as a code.\n */\n extraRemoteCandidates?: string[];\n}\n\nexport interface LinkFactory {\n create(opts: LinkOptions): PeerLink;\n}\n\n/** Subset of the WebSocket API the signaling client needs. */\nexport interface SocketLike {\n readonly readyState: number;\n onopen: ((ev?: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n onclose: ((ev?: unknown) => void) | null;\n onerror: ((ev?: unknown) => void) | null;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n}\nexport type SocketFactory = (url: string) => SocketLike;\n\nexport const defaultSocketFactory: SocketFactory = (url) => new WebSocket(url) as unknown as SocketLike;\n"]}
package/dist/webrtc.d.ts CHANGED
@@ -8,6 +8,7 @@ export declare class WebRtcLink implements PeerLink {
8
8
  onClose: PeerLink["onClose"];
9
9
  onMessage: PeerLink["onMessage"];
10
10
  onPath: PeerLink["onPath"];
11
+ onRemoteAddress: PeerLink["onRemoteAddress"];
11
12
  private pc;
12
13
  private channels;
13
14
  private pendingIce;
package/dist/webrtc.js CHANGED
@@ -13,6 +13,7 @@ export class WebRtcLink {
13
13
  onClose = null;
14
14
  onMessage = null;
15
15
  onPath = null;
16
+ onRemoteAddress = null;
16
17
  pc;
17
18
  channels = {};
18
19
  pendingIce = [];
@@ -114,7 +115,7 @@ export class WebRtcLink {
114
115
  for (const c of this.pendingIce.splice(0))
115
116
  await this.pc.addIceCandidate(c);
116
117
  if (this.extra.length) {
117
- // docs/direct-ip.md s.2: the address the joiner typed is the host's reachable candidate
118
+ // docs/design/direct-ip.md s.2: the address the joiner typed is the host's reachable candidate
118
119
  const mid = /^a=mid:(\S+)/m.exec(s.sdp.sdp)?.[1] ?? "0";
119
120
  for (const candidate of this.extra.splice(0))
120
121
  await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });
@@ -157,6 +158,10 @@ export class WebRtcLink {
157
158
  const l = pair && byId.get(pair.localCandidateId);
158
159
  const r = pair && byId.get(pair.remoteCandidateId);
159
160
  if (l && r) {
161
+ // the remote address (not a relay's, not an mDNS name) lets a Nostr host ban the IP behind a kicked key
162
+ const addr = String(r.address ?? r.ip ?? "");
163
+ if (r.candidateType !== "relay" && l.candidateType !== "relay" && addr && !addr.endsWith(".local"))
164
+ this.onRemoteAddress?.(addr.replace(/^::ffff:/i, ""));
160
165
  this.onPath?.(l.candidateType === "relay" || r.candidateType === "relay" ? "relayed" : "direct");
161
166
  return;
162
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"webrtc.js","sourceRoot":"","sources":["../src/webrtc.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CHANNEL_IDS,\n type ChannelName,\n type LinkFactory,\n type LinkOptions,\n type PeerLink,\n type SignalPayload,\n} from \"./transport.js\";\n\nconst CHANNEL_INIT: Record<ChannelName, RTCDataChannelInit> = {\n reliable: { negotiated: true, id: CHANNEL_IDS.reliable, ordered: true },\n fast: { negotiated: true, id: CHANNEL_IDS.fast, ordered: false, maxRetransmits: 0 },\n voice: { negotiated: true, id: CHANNEL_IDS.voice, ordered: false, maxRetransmits: 0 },\n};\n\n/** Browser (or any WebRTC-capable runtime) PeerLink over RTCPeerConnection with three negotiated channels. */\nexport class WebRtcLink implements PeerLink {\n readonly remoteId: number;\n isOpen = false;\n onSignal: PeerLink[\"onSignal\"] = null;\n onOpen: PeerLink[\"onOpen\"] = null;\n onClose: PeerLink[\"onClose\"] = null;\n onMessage: PeerLink[\"onMessage\"] = null;\n onPath: PeerLink[\"onPath\"] = null;\n\n private pc: RTCPeerConnection;\n private channels = {} as Record<ChannelName, RTCDataChannel>;\n private pendingIce: RTCIceCandidateInit[] = [];\n /**\n * Set once setRemoteDescription resolves. Not `pc.remoteDescription`: node-datachannel's polyfill returns an object\n * there before any is set, and its addIceCandidate throws until one is.\n */\n private haveRemote = false;\n private closed = false;\n private opened = 0;\n private early: [ChannelName, Uint8Array][] = [];\n private extra: string[];\n\n /** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */\n constructor(opts: LinkOptions, rtc: typeof RTCPeerConnection = globalThis.RTCPeerConnection) {\n this.remoteId = opts.remoteId;\n this.extra = opts.extraRemoteCandidates ?? [];\n const config: RTCConfiguration & Record<string, unknown> = {\n iceServers: opts.iceServers as RTCIceServer[],\n iceTransportPolicy: opts.relayOnly ? \"relay\" : \"all\",\n };\n if (opts.portRange) {\n // libdatachannel names (node-datachannel passes its config through); one pinned port = every peer on it\n config.portRangeBegin = opts.portRange.begin;\n config.portRangeEnd = opts.portRange.end;\n if (opts.portRange.begin === opts.portRange.end) config.enableIceUdpMux = true;\n }\n this.pc = new rtc(config);\n this.pc.onicecandidate = (e) => {\n if (e.candidate) this.onSignal?.({ ice: e.candidate.toJSON() as { candidate: string } });\n };\n this.pc.onconnectionstatechange = () => {\n const s = this.pc.connectionState;\n if (s === \"failed\" || s === \"closed\" || s === \"disconnected\") {\n // \"disconnected\" can recover; give ICE a moment before declaring the link dead.\n if (s === \"disconnected\") setTimeout(() => this.pc.connectionState === \"disconnected\" && this.fail(\"disconnected\"), 4000);\n else this.fail(s);\n }\n };\n }\n\n /**\n * The offerer creates the channels just before its offer, the answerer once the remote offer is set. Creating them\n * any earlier makes libdatachannel (node-datachannel) start an offer of its own on the answering side, which then\n * collides with the real one (both ICE agents controlling, the link never opens).\n */\n private openChannels(): void {\n if (this.channels.reliable) return;\n for (const name of [\"reliable\", \"fast\", \"voice\"] as ChannelName[]) {\n const ch = this.pc.createDataChannel(name, CHANNEL_INIT[name]);\n ch.binaryType = \"arraybuffer\";\n ch.onopen = () => {\n if (++this.opened === 3 && !this.closed) {\n this.isOpen = true;\n this.onOpen?.();\n for (const [n, data] of this.early.splice(0)) if (!this.closed) this.onMessage?.(n, data);\n void this.reportPath();\n }\n };\n ch.onclose = () => this.fail(`channel ${name} closed`);\n ch.onmessage = (e) => {\n const d = e.data as unknown;\n const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d as never);\n // The far side can open first and speak at once; hold its data until this side is open too, so whoever\n // reads it can already answer (otherwise the host queues or drops its replies to a link it thinks is down).\n if (!this.isOpen) {\n if (this.early.length < 1000) this.early.push([name, data]);\n return;\n }\n this.onMessage?.(name, data);\n };\n this.channels[name] = ch;\n }\n }\n\n startOffer(): void {\n void (async () => {\n try {\n this.openChannels();\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n this.onSignal?.({ sdp: { type: \"offer\", sdp: offer.sdp ?? \"\" } });\n } catch (e) {\n this.fail(`offer failed: ${String(e)}`);\n }\n })();\n }\n\n async handleSignal(s: SignalPayload): Promise<void> {\n try {\n if (s.sdp) {\n await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });\n this.haveRemote = true;\n for (const c of this.pendingIce.splice(0)) await this.pc.addIceCandidate(c);\n if (this.extra.length) {\n // docs/direct-ip.md s.2: the address the joiner typed is the host's reachable candidate\n const mid = /^a=mid:(\\S+)/m.exec(s.sdp.sdp)?.[1] ?? \"0\";\n for (const candidate of this.extra.splice(0)) await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });\n }\n if (s.sdp.type === \"offer\") {\n this.openChannels();\n const answer = await this.pc.createAnswer();\n await this.pc.setLocalDescription(answer);\n this.onSignal?.({ sdp: { type: \"answer\", sdp: answer.sdp ?? \"\" } });\n }\n }\n if (s.ice) {\n // a candidate can come before the description it belongs to (the far side's native callbacks are unordered)\n if (this.haveRemote) await this.pc.addIceCandidate(s.ice as RTCIceCandidateInit);\n else this.pendingIce.push(s.ice as RTCIceCandidateInit);\n }\n } catch (e) {\n this.fail(`signal failed: ${String(e)}`);\n }\n }\n\n /** Selected candidate pair has a relay candidate -> \"relayed\", else \"direct\". Retries while stats settle. */\n private async reportPath(attempt = 0): Promise<void> {\n if (this.closed || attempt > 10) return;\n try {\n const stats = await this.pc.getStats();\n const byId = new Map<string, any>();\n stats.forEach((v) => byId.set(v.id, v));\n let pair: any;\n stats.forEach((v) => {\n if (v.type === \"transport\" && v.selectedCandidatePairId) pair = byId.get(v.selectedCandidatePairId);\n });\n if (!pair) stats.forEach((v) => { if (v.type === \"candidate-pair\" && (v.selected || (v.nominated && v.state === \"succeeded\"))) pair = v; });\n const l = pair && byId.get(pair.localCandidateId);\n const r = pair && byId.get(pair.remoteCandidateId);\n if (l && r) {\n this.onPath?.(l.candidateType === \"relay\" || r.candidateType === \"relay\" ? \"relayed\" : \"direct\");\n return;\n }\n } catch {\n /* stats unavailable: retry */\n }\n setTimeout(() => void this.reportPath(attempt + 1), 300);\n }\n\n send(channel: ChannelName, bytes: Uint8Array): void {\n const ch = this.channels[channel];\n if (ch && ch.readyState === \"open\") ch.send(bytes as unknown as ArrayBuffer);\n }\n\n close(): void {\n this.fail(\"closed locally\");\n }\n\n private fail(reason: string): void {\n if (this.closed) return;\n this.closed = true;\n const wasOpen = this.isOpen;\n this.isOpen = false;\n try {\n this.pc.close();\n } catch {\n /* ignore */\n }\n void wasOpen;\n this.onClose?.(reason);\n }\n}\n\nexport const webRtcLinkFactory: LinkFactory = { create: (opts) => new WebRtcLink(opts) };\n\n/** A link factory over a given RTCPeerConnection class, e.g. `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. */\nexport function webRtcLinkFactoryFor(rtc: typeof RTCPeerConnection): LinkFactory {\n return { create: (opts) => new WebRtcLink(opts, rtc) };\n}\n"]}
1
+ {"version":3,"file":"webrtc.js","sourceRoot":"","sources":["../src/webrtc.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CHANNEL_IDS,\n type ChannelName,\n type LinkFactory,\n type LinkOptions,\n type PeerLink,\n type SignalPayload,\n} from \"./transport.js\";\n\nconst CHANNEL_INIT: Record<ChannelName, RTCDataChannelInit> = {\n reliable: { negotiated: true, id: CHANNEL_IDS.reliable, ordered: true },\n fast: { negotiated: true, id: CHANNEL_IDS.fast, ordered: false, maxRetransmits: 0 },\n voice: { negotiated: true, id: CHANNEL_IDS.voice, ordered: false, maxRetransmits: 0 },\n};\n\n/** Browser (or any WebRTC-capable runtime) PeerLink over RTCPeerConnection with three negotiated channels. */\nexport class WebRtcLink implements PeerLink {\n readonly remoteId: number;\n isOpen = false;\n onSignal: PeerLink[\"onSignal\"] = null;\n onOpen: PeerLink[\"onOpen\"] = null;\n onClose: PeerLink[\"onClose\"] = null;\n onMessage: PeerLink[\"onMessage\"] = null;\n onPath: PeerLink[\"onPath\"] = null;\n onRemoteAddress: PeerLink[\"onRemoteAddress\"] = null;\n\n private pc: RTCPeerConnection;\n private channels = {} as Record<ChannelName, RTCDataChannel>;\n private pendingIce: RTCIceCandidateInit[] = [];\n /**\n * Set once setRemoteDescription resolves. Not `pc.remoteDescription`: node-datachannel's polyfill returns an object\n * there before any is set, and its addIceCandidate throws until one is.\n */\n private haveRemote = false;\n private closed = false;\n private opened = 0;\n private early: [ChannelName, Uint8Array][] = [];\n private extra: string[];\n\n /** `rtc`: the RTCPeerConnection class to use (default the global one; in Node, node-datachannel's polyfill). */\n constructor(opts: LinkOptions, rtc: typeof RTCPeerConnection = globalThis.RTCPeerConnection) {\n this.remoteId = opts.remoteId;\n this.extra = opts.extraRemoteCandidates ?? [];\n const config: RTCConfiguration & Record<string, unknown> = {\n iceServers: opts.iceServers as RTCIceServer[],\n iceTransportPolicy: opts.relayOnly ? \"relay\" : \"all\",\n };\n if (opts.portRange) {\n // libdatachannel names (node-datachannel passes its config through); one pinned port = every peer on it\n config.portRangeBegin = opts.portRange.begin;\n config.portRangeEnd = opts.portRange.end;\n if (opts.portRange.begin === opts.portRange.end) config.enableIceUdpMux = true;\n }\n this.pc = new rtc(config);\n this.pc.onicecandidate = (e) => {\n if (e.candidate) this.onSignal?.({ ice: e.candidate.toJSON() as { candidate: string } });\n };\n this.pc.onconnectionstatechange = () => {\n const s = this.pc.connectionState;\n if (s === \"failed\" || s === \"closed\" || s === \"disconnected\") {\n // \"disconnected\" can recover; give ICE a moment before declaring the link dead.\n if (s === \"disconnected\") setTimeout(() => this.pc.connectionState === \"disconnected\" && this.fail(\"disconnected\"), 4000);\n else this.fail(s);\n }\n };\n }\n\n /**\n * The offerer creates the channels just before its offer, the answerer once the remote offer is set. Creating them\n * any earlier makes libdatachannel (node-datachannel) start an offer of its own on the answering side, which then\n * collides with the real one (both ICE agents controlling, the link never opens).\n */\n private openChannels(): void {\n if (this.channels.reliable) return;\n for (const name of [\"reliable\", \"fast\", \"voice\"] as ChannelName[]) {\n const ch = this.pc.createDataChannel(name, CHANNEL_INIT[name]);\n ch.binaryType = \"arraybuffer\";\n ch.onopen = () => {\n if (++this.opened === 3 && !this.closed) {\n this.isOpen = true;\n this.onOpen?.();\n for (const [n, data] of this.early.splice(0)) if (!this.closed) this.onMessage?.(n, data);\n void this.reportPath();\n }\n };\n ch.onclose = () => this.fail(`channel ${name} closed`);\n ch.onmessage = (e) => {\n const d = e.data as unknown;\n const data = d instanceof ArrayBuffer ? new Uint8Array(d) : ArrayBuffer.isView(d) ? new Uint8Array(d.buffer, d.byteOffset, d.byteLength) : new Uint8Array(d as never);\n // The far side can open first and speak at once; hold its data until this side is open too, so whoever\n // reads it can already answer (otherwise the host queues or drops its replies to a link it thinks is down).\n if (!this.isOpen) {\n if (this.early.length < 1000) this.early.push([name, data]);\n return;\n }\n this.onMessage?.(name, data);\n };\n this.channels[name] = ch;\n }\n }\n\n startOffer(): void {\n void (async () => {\n try {\n this.openChannels();\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n this.onSignal?.({ sdp: { type: \"offer\", sdp: offer.sdp ?? \"\" } });\n } catch (e) {\n this.fail(`offer failed: ${String(e)}`);\n }\n })();\n }\n\n async handleSignal(s: SignalPayload): Promise<void> {\n try {\n if (s.sdp) {\n await this.pc.setRemoteDescription({ type: s.sdp.type, sdp: s.sdp.sdp });\n this.haveRemote = true;\n for (const c of this.pendingIce.splice(0)) await this.pc.addIceCandidate(c);\n if (this.extra.length) {\n // docs/design/direct-ip.md s.2: the address the joiner typed is the host's reachable candidate\n const mid = /^a=mid:(\\S+)/m.exec(s.sdp.sdp)?.[1] ?? \"0\";\n for (const candidate of this.extra.splice(0)) await this.pc.addIceCandidate({ candidate, sdpMid: mid, sdpMLineIndex: 0 });\n }\n if (s.sdp.type === \"offer\") {\n this.openChannels();\n const answer = await this.pc.createAnswer();\n await this.pc.setLocalDescription(answer);\n this.onSignal?.({ sdp: { type: \"answer\", sdp: answer.sdp ?? \"\" } });\n }\n }\n if (s.ice) {\n // a candidate can come before the description it belongs to (the far side's native callbacks are unordered)\n if (this.haveRemote) await this.pc.addIceCandidate(s.ice as RTCIceCandidateInit);\n else this.pendingIce.push(s.ice as RTCIceCandidateInit);\n }\n } catch (e) {\n this.fail(`signal failed: ${String(e)}`);\n }\n }\n\n /** Selected candidate pair has a relay candidate -> \"relayed\", else \"direct\". Retries while stats settle. */\n private async reportPath(attempt = 0): Promise<void> {\n if (this.closed || attempt > 10) return;\n try {\n const stats = await this.pc.getStats();\n const byId = new Map<string, any>();\n stats.forEach((v) => byId.set(v.id, v));\n let pair: any;\n stats.forEach((v) => {\n if (v.type === \"transport\" && v.selectedCandidatePairId) pair = byId.get(v.selectedCandidatePairId);\n });\n if (!pair) stats.forEach((v) => { if (v.type === \"candidate-pair\" && (v.selected || (v.nominated && v.state === \"succeeded\"))) pair = v; });\n const l = pair && byId.get(pair.localCandidateId);\n const r = pair && byId.get(pair.remoteCandidateId);\n if (l && r) {\n // the remote address (not a relay's, not an mDNS name) lets a Nostr host ban the IP behind a kicked key\n const addr = String(r.address ?? r.ip ?? \"\");\n if (r.candidateType !== \"relay\" && l.candidateType !== \"relay\" && addr && !addr.endsWith(\".local\")) this.onRemoteAddress?.(addr.replace(/^::ffff:/i, \"\"));\n this.onPath?.(l.candidateType === \"relay\" || r.candidateType === \"relay\" ? \"relayed\" : \"direct\");\n return;\n }\n } catch {\n /* stats unavailable: retry */\n }\n setTimeout(() => void this.reportPath(attempt + 1), 300);\n }\n\n send(channel: ChannelName, bytes: Uint8Array): void {\n const ch = this.channels[channel];\n if (ch && ch.readyState === \"open\") ch.send(bytes as unknown as ArrayBuffer);\n }\n\n close(): void {\n this.fail(\"closed locally\");\n }\n\n private fail(reason: string): void {\n if (this.closed) return;\n this.closed = true;\n const wasOpen = this.isOpen;\n this.isOpen = false;\n try {\n this.pc.close();\n } catch {\n /* ignore */\n }\n void wasOpen;\n this.onClose?.(reason);\n }\n}\n\nexport const webRtcLinkFactory: LinkFactory = { create: (opts) => new WebRtcLink(opts) };\n\n/** A link factory over a given RTCPeerConnection class, e.g. `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. */\nexport function webRtcLinkFactoryFor(rtc: typeof RTCPeerConnection): LinkFactory {\n return { create: (opts) => new WebRtcLink(opts, rtc) };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onelobby",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Multiplayer for browser games: host/join lobbies by short code, peer-to-peer WebRTC, lockstep sync, text and voice chat. Zero runtime dependencies; works from a plain <script> tag.",
5
5
  "keywords": [
6
6
  "multiplayer",
@@ -63,10 +63,12 @@
63
63
  "types": "./dist/global.d.ts",
64
64
  "default": "./dist/onelobby.bundle.mjs"
65
65
  },
66
+ "./blueprint.schema.json": "./blueprint.schema.json",
66
67
  "./package.json": "./package.json"
67
68
  },
68
69
  "files": [
69
70
  "dist",
71
+ "blueprint.schema.json",
70
72
  "README.md",
71
73
  "LICENSE"
72
74
  ],
@@ -83,7 +85,7 @@
83
85
  "access": "public"
84
86
  },
85
87
  "scripts": {
86
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/bundle.mjs",
88
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/bundle.mjs && node scripts/copy-schema.mjs",
87
89
  "typecheck": "tsc -p tsconfig.json --noEmit",
88
90
  "test": "vitest run",
89
91
  "prepublishOnly": "npm run typecheck && npm run build && npm test && node scripts/prepublish-check.mjs"