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.
Files changed (99) hide show
  1. package/README.md +2 -2
  2. package/blueprint.schema.json +8 -1
  3. package/dist/blueprint.d.ts +4 -0
  4. package/dist/blueprint.js +5 -0
  5. package/dist/blueprint.js.map +1 -1
  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 +36 -3
  22. package/dist/direct/index.js +146 -20
  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 +23 -0
  36. package/dist/friendly.js +28 -0
  37. package/dist/friendly.js.map +1 -1
  38. package/dist/index.d.ts +8 -2
  39. package/dist/index.js +6 -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 +31 -3
  85. package/dist/onelobby.global.js +2599 -46
  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 +68 -14
  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 +1 -1
@@ -1,4 +1,8 @@
1
- import { encodeJoinCode } from "../joincode.js";
1
+ import { encodeJoinCode, encodeRoomCode } from "../joincode.js";
2
+ import { joinCodeKindName } from "../join-code-kind.js";
3
+ import { NostrHostBridge } from "../nostr/host.js";
4
+ import { HOST_FIRST_RELAY_MS } from "../nostr/limits.js";
5
+ import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_STUN } from "../nostr/relays.js";
2
6
  import { DEFAULT_DIRECT_PORT } from "../direct-address.js";
3
7
  import { DirectHubHost, PortInUseError, portFailureHint } from "./hub-host.js";
4
8
  import { directHubOptions } from "./hub.js";
@@ -8,8 +12,28 @@ export { DirectHubHost, LocalHubSocket, PortInUseError, originAllowed } from "./
8
12
  export { SignalingHub, Session, RateWindow, sanitize, defaultHubOptions, directHubOptions, PROTO } from "./hub.js";
9
13
  export { PortMapper, isPublicIPv4, lanIPv4, localAddresses } from "./portmap.js";
10
14
  export { webRtcLinkFactoryFor } from "../webrtc.js";
15
+ export { NostrHostBridge } from "../nostr/host.js";
11
16
  /**
12
- * Hosts a direct-IP lobby (docs/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port
17
+ * The one startup line for a direct host, character for character as C#'s `DirectLobby.FormatHostLog`, e.g.
18
+ * `OneLobby: direct host listening on TCP+UDP :47811 (47810 busy); code 0A00-0BSE-P3S (SameNetwork); port mapping off`.
19
+ */
20
+ export function formatHostLog(requested, bound, code, kind, mapping,
21
+ /** With Nostr on: the IP code and how many relays are connected ("4/5", or "" for none reachable). */
22
+ nostr) {
23
+ let line = `OneLobby: direct host listening on TCP+UDP :${bound}`;
24
+ if (bound !== requested)
25
+ line += ` (${bound - requested === 1 ? requested : `${requested}-${bound - 1}`} busy)`;
26
+ const name = joinCodeKindName;
27
+ line += code ? `; code ${code} (${name(kind)})` : "; no join code (no IPv4 address)";
28
+ if (nostr)
29
+ line += nostr.directCode ? `; ip code ${nostr.directCode} (${name(nostr.directCodeKind)})` : "; no ip code (no IPv4 address)";
30
+ line += `; port mapping ${mapping || "off"}`;
31
+ if (nostr)
32
+ line += nostr.relays ? `; nostr ${nostr.relays} relays` : "; nostr: no relay reachable";
33
+ return line;
34
+ }
35
+ /**
36
+ * Hosts a direct-IP lobby (docs/design/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port
13
37
  * (unless `openPort: false`), connects `kit` to the hub in memory and creates the lobby. Resolves with the lobby;
14
38
  * show `lobby.joinCode` with `lobby.joinCodeKind` ("internet" or "same_network"). Rejects with `port_in_use`.
15
39
  * When the lobby ends (leave, or the process exits) the listener closes and the router mapping is removed; joiners
@@ -25,6 +49,7 @@ export async function hostDirect(kit, opts = {}) {
25
49
  ? {
26
50
  port: bp.hosting.directPort,
27
51
  openPort: bp.hosting.openPort,
52
+ nostr: bp.hosting.nostr ? { relays: [...bp.hosting.nostrRelays] } : false,
28
53
  allowedOrigins: [...bp.hosting.allowedOrigins],
29
54
  maxPlayers: bp.lobby.maxPlayers,
30
55
  lateJoin: bp.lobby.lateJoin,
@@ -37,12 +62,15 @@ export async function hostDirect(kit, opts = {}) {
37
62
  // busy (another program holds it): try the next fallbackPorts; the join code carries the port
38
63
  const last = port + Math.max(0, o.fallbackPorts ?? (o.port === undefined ? DEFAULT_FALLBACK_PORTS : 0));
39
64
  const maxPlayers = o.maxPlayers ?? 4;
65
+ const nostrOn = o.nostr !== false;
66
+ const nostrCfg = typeof o.nostr === "object" ? o.nostr : {};
67
+ const stunUrls = nostrOn ? (nostrCfg.stun ?? [...DEFAULT_NOSTR_STUN]) : [];
40
68
  let hub;
41
69
  let anyBusy = false;
42
70
  let anyReserved = false;
43
71
  for (let p = port; !hub; p++) {
44
72
  try {
45
- hub = await DirectHubHost.listen(p, directHubOptions(maxPlayers, o.password));
73
+ hub = await DirectHubHost.listen(p, { ...directHubOptions(maxPlayers, o.password), stunUrls }, o.bindHost ? { host: o.bindHost } : undefined);
46
74
  }
47
75
  catch (e) {
48
76
  if (!(e instanceof PortInUseError))
@@ -59,14 +87,20 @@ export async function hostDirect(kit, opts = {}) {
59
87
  }
60
88
  hub.http = o.http ?? null;
61
89
  hub.allowedOrigins = o.allowedOrigins ?? [];
90
+ const manual = (o.publicAddress ?? process.env.ONELOBBY_PUBLIC_IP ?? "").trim() || null;
91
+ if (manual && !isIPv4(manual)) {
92
+ await hub.close();
93
+ throw new OneLobbyError("bad_address", `publicAddress must be an IPv4 address like 203.0.113.7, got "${manual}"`);
94
+ }
62
95
  // The router request runs alongside lobby creation; it takes up to a few seconds when nothing answers.
63
- const mapping = o.openPort === false
96
+ const mapping = o.openPort === false || (manual && opts.openPort !== true)
64
97
  ? Promise.resolve(null)
65
98
  : hub.openPort({ description: `OneLobby ${kit.options.gameId}`, ...o.portMapping }).catch((e) => ({
66
99
  mapped: false, method: "none", externalIPv4: null, localIPv4: null, detail: e.message,
67
100
  }));
68
101
  const wrapLinks = (inner) => ({
69
- create: (lo) => inner.create({ ...lo, iceServers: [], portRange: { begin: hub.port, end: hub.port } }),
102
+ // Nostr joiners have no address to dial: the host needs STUN too (the UDP port pin stays)
103
+ create: (lo) => inner.create({ ...lo, iceServers: nostrOn ? lo.iceServers : [], portRange: { begin: hub.port, end: hub.port } }),
70
104
  });
71
105
  let lobby;
72
106
  try {
@@ -79,6 +113,7 @@ export async function hostDirect(kit, opts = {}) {
79
113
  await hub.close();
80
114
  throw e;
81
115
  }
116
+ lobby.linkAddressSink = (peerId, ip) => hub.hub.reportLinkAddress(peerId, ip);
82
117
  lobby.on("sessionEnded", () => void hub.close());
83
118
  // leave() resolves once the router mapping is deleted, so a host that exits right after it does not leave the port
84
119
  // forwarded until the lease runs out (2 h, or never on routers that only grant permanent leases)
@@ -91,23 +126,114 @@ export async function hostDirect(kit, opts = {}) {
91
126
  await hub.close();
92
127
  }
93
128
  };
94
- const map = await mapping;
95
- lobby.directAddresses = localAddresses(hub.port);
96
- lobby.portMapping = map ? map.detail : "off";
97
- let codeIp = null;
98
- if (map?.mapped && isPublicIPv4(map.externalIPv4)) {
99
- codeIp = map.externalIPv4;
100
- lobby.joinCode = encodeJoinCode(codeIp, hub.port);
101
- lobby.joinCodeKind = "internet";
129
+ // Nostr: the room code is ready at once; relays connect while the router answers
130
+ let relaysNote = "";
131
+ let nostrUp = false;
132
+ let nostrCode = "";
133
+ let onRelays = null;
134
+ if (nostrOn) {
135
+ const env = kit.nostrHostEnv();
136
+ nostrCode = encodeRoomCode();
137
+ try {
138
+ const bridge = await NostrHostBridge.start(hub.hub, {
139
+ code: nostrCode,
140
+ relays: nostrCfg.relays && nostrCfg.relays.length > 0 ? nostrCfg.relays : [...DEFAULT_NOSTR_RELAYS],
141
+ connect: env.connect,
142
+ clock: env.clock,
143
+ wall: env.wall,
144
+ maxPlayers,
145
+ onChange: () => onRelays?.(),
146
+ });
147
+ hub.nostr = bridge;
148
+ lobby.on("sessionEnded", () => bridge.close());
149
+ nostrUp = await settleRelays(bridge);
150
+ relaysNote = nostrUp ? `${bridge.relaysConnected}/${bridge.relaysTotal}` : "";
151
+ }
152
+ catch {
153
+ nostrCode = ""; // no WebSocket in this runtime, say: the host keeps hosting by IP
154
+ }
102
155
  }
103
- else {
104
- codeIp = map?.localIPv4 ?? (await lanIPv4());
105
- if (codeIp) {
106
- lobby.joinCode = encodeJoinCode(codeIp, hub.port);
107
- lobby.joinCodeKind = "same_network";
156
+ const map = await mapping;
157
+ const direct = Object.assign(lobby, { directHub: hub, joinUrl: null });
158
+ direct.directAddresses = localAddresses(hub.port);
159
+ const log = o.log === false ? null : (o.log ?? console.log);
160
+ // Sets joinCode / joinCodeKind / directCode / joinUrl / portMapping / directHostLog and what GET /joininfo answers.
161
+ let lastIp = [null, "same_network", "off"];
162
+ const applyCode = (ip, kind, mappingNote) => {
163
+ lastIp = [ip, kind, mappingNote];
164
+ direct.portMapping = mappingNote;
165
+ direct.directCode = ip ? encodeJoinCode(ip, hub.port) : "";
166
+ direct.directCodeKind = ip ? kind : "none";
167
+ if (nostrOn && nostrCode && nostrUp) {
168
+ direct.joinCode = nostrCode;
169
+ direct.joinCodeKind = "nostr";
170
+ }
171
+ else {
172
+ // Nostr off, or no relay reachable (yet): the IP code is the code; it turns Nostr when a relay connects
173
+ direct.joinCode = direct.directCode;
174
+ direct.joinCodeKind = direct.directCodeKind;
108
175
  }
176
+ direct.joinUrl = o.http && ip ? `http://${ip}:${hub.port}/` : null;
177
+ direct.directHostLog = formatHostLog(port, hub.port, direct.joinCode, direct.joinCodeKind, direct.portMapping, nostrOn ? { directCode: direct.directCode, directCodeKind: direct.directCodeKind, relays: relaysNote } : undefined);
178
+ hub.joinInfo = { joinCode: direct.joinCode, joinCodeKind: direct.joinCodeKind, directCode: direct.directCode, directCodeKind: direct.directCodeKind, joinUrl: direct.joinUrl, port: hub.port };
179
+ log?.(direct.directHostLog);
180
+ };
181
+ if (manual)
182
+ applyCode(manual, "internet", map?.mapped ? map.detail : o.publicAddress ? "manual" : "manual (ONELOBBY_PUBLIC_IP)");
183
+ else if (map?.mapped && isPublicIPv4(map.externalIPv4))
184
+ applyCode(map.externalIPv4, "internet", map.detail);
185
+ else {
186
+ applyCode(map?.localIPv4 ?? (await lanIPv4()), "same_network", map ? map.detail : "off");
187
+ if (map && !map.mapped && hub.portMapper)
188
+ retryMapping(hub.portMapper, o.portMapping?.retryMs ?? 30000, (r) => {
189
+ if (!isPublicIPv4(r.externalIPv4))
190
+ return;
191
+ applyCode(r.externalIPv4, "internet", r.detail);
192
+ direct.notifyJoinCodeChanged();
193
+ });
109
194
  }
110
- const joinUrl = o.http && codeIp ? `http://${codeIp}:${hub.port}/` : null;
111
- return Object.assign(lobby, { directHub: hub, joinUrl });
195
+ // a relay answering after the 10 s wait: the Nostr code becomes the joinCode
196
+ onRelays = () => {
197
+ const b = hub.nostr;
198
+ if (!b || b.relaysConnected === 0 || nostrUp)
199
+ return;
200
+ nostrUp = true;
201
+ relaysNote = `${b.relaysConnected}/${b.relaysTotal}`;
202
+ applyCode(...lastIp);
203
+ direct.notifyJoinCodeChanged();
204
+ };
205
+ return direct;
206
+ }
207
+ const MAX_RETRY_MS = 5 * 60_000;
208
+ /** Asks the router again after `firstMs`, doubling up to 5 minutes, until a mapping lands or the host quits. */
209
+ function retryMapping(mapper, firstMs, onMapped) {
210
+ if (!(firstMs > 0))
211
+ return;
212
+ let wait = firstMs;
213
+ const round = () => {
214
+ const t = setTimeout(async () => {
215
+ if (mapper.closed)
216
+ return;
217
+ const r = await mapper.open();
218
+ if (mapper.closed)
219
+ return;
220
+ if (r.mapped)
221
+ onMapped(r);
222
+ else {
223
+ wait = Math.min(wait * 2, MAX_RETRY_MS);
224
+ round();
225
+ }
226
+ }, wait);
227
+ t.unref?.();
228
+ };
229
+ round();
230
+ }
231
+ /** Waits for the relays to connect: true once at least one has (then up to 1.5 s more for the rest), false after 10 s. */
232
+ function settleRelays(bridge) {
233
+ return bridge.settle(HOST_FIRST_RELAY_MS);
234
+ }
235
+ function isIPv4(s) {
236
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
237
+ return !!m && m.slice(1).every((x) => Number(x) <= 255);
112
238
  }
113
239
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/direct/index.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\n/**\n * `onelobby/direct` (Node only): host a direct-IP lobby from a Node process, with no server of anyone's\n * (docs/direct-ip.md). The process runs the signaling hub, listens on TCP `port`, binds its peer links to UDP `port`,\n * asks the router to forward both (UPnP, then NAT-PMP) and hands out a join code. Joiners use `kit.joinDirect(code)`\n * from `onelobby` (Node or browser) or `JoinDirectAsync` in C#; the wire protocol is the same, so they mix.\n *\n * WebRTC in Node needs an RTCPeerConnection: `npm install node-datachannel`, then\n * `advanced: { linkFactory: webRtcLinkFactoryFor(RTCPeerConnection) }` with\n * `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. Its libdatachannel honours the port pin.\n */\nimport type { HostOptions, OneLobby } from \"../onelobby.js\";\nimport type { Lobby, RateLimit } from \"../lobby.js\";\nimport type { LinkFactory } from \"../transport.js\";\nimport { encodeJoinCode } from \"../joincode.js\";\nimport { DEFAULT_DIRECT_PORT } from \"../direct-address.js\";\nimport { DirectHubHost, PortInUseError, portFailureHint, type HttpHandler } from \"./hub-host.js\";\nimport { directHubOptions } from \"./hub.js\";\nimport { isPublicIPv4, lanIPv4, localAddresses, type PortMapResult, type PortMapperOptions } from \"./portmap.js\";\nimport { OneLobbyError } from \"../signaling.js\";\n\nexport { DirectHubHost, LocalHubSocket, PortInUseError, originAllowed } from \"./hub-host.js\";\nexport type { HttpHandler, HttpReply } from \"./hub-host.js\";\nexport { SignalingHub, Session, RateWindow, sanitize, defaultHubOptions, directHubOptions, PROTO } from \"./hub.js\";\nexport type { HubOptions, HubDeps } from \"./hub.js\";\nexport { PortMapper, isPublicIPv4, lanIPv4, localAddresses } from \"./portmap.js\";\nexport type { PortMapResult, PortMapperOptions, PortMapMethod } from \"./portmap.js\";\nexport { webRtcLinkFactoryFor } from \"../webrtc.js\";\n\nexport interface DirectHostOptions {\n /**\n * TCP and UDP port to listen on (default 47810). When it is busy (another program holds it) the next free one of the\n * following `fallbackPorts` is used; the join code carries the port, so friends never type it.\n */\n port?: number;\n /** How many ports after `port` to try when it is busy; default 9 with `port` left out, else 0. */\n fallbackPorts?: number;\n /** Optional; joiners must send the same one or get `bad_password`. */\n password?: string;\n maxPlayers?: number;\n /** Lobby name; default \"<displayName>'s game\". */\n name?: string;\n lateJoin?: boolean;\n meta?: Record<string, string>;\n /** Ask the router to forward the port so the code works from the internet (s.2a). The host-menu toggle; default on. */\n openPort?: boolean;\n /** Test seam and knobs for the router request (description defaults to \"OneLobby <gameId>\"). */\n portMapping?: PortMapperOptions;\n /**\n * Serves plain-HTTP `GET`s on the same port, typically a join page for browsers (recipe s.4b). The page joins with\n * `kit.joinDirect(location.host)`; the lobby's `joinUrl` is then the link to share.\n */\n http?: HttpHandler;\n /**\n * Browser `Origin` values allowed to open a signaling socket (exact match, e.g. \"https://play.example\"; \"*\" allows\n * every origin). Native clients send no Origin and are always allowed, as are pages served from the host's own address\n * (`http`) and localhost pages, so this is only for a web game hosted somewhere else that joins players to this host.\n */\n allowedOrigins?: string[];\n /** Per-joiner cap on incoming raw messages, e.g. `{ messages: 60, perMs: 1000 }`; the excess is dropped. */\n rateLimit?: RateLimit;\n}\n\n/** A direct host lobby: the normal Lobby, plus the hub behind it. Leaving or ending the lobby stops the hub. */\nexport type DirectLobby = Lobby & {\n directHub: DirectHubHost;\n /** With the `http` option: `http://<the code's address>:<port>/`, the link a browser opens to join. Else null. */\n joinUrl: string | null;\n};\n\n/**\n * Hosts a direct-IP lobby (docs/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port\n * (unless `openPort: false`), connects `kit` to the hub in memory and creates the lobby. Resolves with the lobby;\n * show `lobby.joinCode` with `lobby.joinCodeKind` (\"internet\" or \"same_network\"). Rejects with `port_in_use`.\n * When the lobby ends (leave, or the process exits) the listener closes and the router mapping is removed; joiners\n * get `sessionEnded(\"host_left\")`.\n */\n/** With `port` left out, how many ports after the default `hostDirect` tries when it is busy. */\nexport const DEFAULT_FALLBACK_PORTS = 9;\n\nexport async function hostDirect(kit: OneLobby, opts: DirectHostOptions = {}): Promise<DirectLobby> {\n const bp = kit.blueprint;\n const defined = Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined)) as DirectHostOptions;\n // a blueprint supplies hosting.* and lobby.* (explicit options win)\n const o: DirectHostOptions = bp\n ? {\n port: bp.hosting.directPort,\n openPort: bp.hosting.openPort,\n allowedOrigins: [...bp.hosting.allowedOrigins],\n maxPlayers: bp.lobby.maxPlayers,\n lateJoin: bp.lobby.lateJoin,\n meta: { ...bp.lobby.meta },\n ...(bp.session.rateLimit ? { rateLimit: { ...bp.session.rateLimit } } : {}),\n ...defined,\n }\n : defined;\n const port = o.port ?? DEFAULT_DIRECT_PORT;\n // busy (another program holds it): try the next fallbackPorts; the join code carries the port\n const last = port + Math.max(0, o.fallbackPorts ?? (o.port === undefined ? DEFAULT_FALLBACK_PORTS : 0));\n const maxPlayers = o.maxPlayers ?? 4;\n let hub: DirectHubHost | undefined;\n let anyBusy = false;\n let anyReserved = false;\n for (let p = port; !hub; p++) {\n try {\n hub = await DirectHubHost.listen(p, directHubOptions(maxPlayers, o.password));\n } catch (e) {\n if (!(e instanceof PortInUseError)) throw e;\n if (e.reserved) anyReserved = true;\n else anyBusy = true;\n if (p < last) continue;\n const range = last > port ? `ports ${port}-${last}` : `port ${port}`;\n throw new OneLobbyError(\"port_in_use\", `cannot listen on ${range}: ${portFailureHint(anyBusy, anyReserved)}`);\n }\n }\n hub.http = o.http ?? null;\n hub.allowedOrigins = o.allowedOrigins ?? [];\n // The router request runs alongside lobby creation; it takes up to a few seconds when nothing answers.\n const mapping: Promise<PortMapResult | null> =\n o.openPort === false\n ? Promise.resolve(null)\n : hub.openPort({ description: `OneLobby ${kit.options.gameId}`, ...o.portMapping }).catch((e: Error) => ({\n mapped: false, method: \"none\" as const, externalIPv4: null, localIPv4: null, detail: e.message,\n }));\n const wrapLinks = (inner: LinkFactory): LinkFactory => ({\n create: (lo) => inner.create({ ...lo, iceServers: [], portRange: { begin: hub.port, end: hub.port } }),\n });\n let lobby: Lobby;\n try {\n const name = o.name ?? `${kit.options.displayName || \"Player\"}'s game`;\n const h: HostOptions = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta, rateLimit: o.rateLimit };\n lobby = await kit.hostDirectWith(h, { socketFactory: () => hub.connectLocal(), wrapLinks });\n } catch (e) {\n await mapping;\n await hub.close();\n throw e;\n }\n lobby.on(\"sessionEnded\", () => void hub.close());\n // leave() resolves once the router mapping is deleted, so a host that exits right after it does not leave the port\n // forwarded until the lease runs out (2 h, or never on routers that only grant permanent leases)\n const leave = lobby.leave.bind(lobby);\n lobby.leave = async () => {\n try {\n await leave();\n } finally {\n await hub.close();\n }\n };\n const map = await mapping;\n lobby.directAddresses = localAddresses(hub.port);\n lobby.portMapping = map ? map.detail : \"off\";\n let codeIp: string | null = null;\n if (map?.mapped && isPublicIPv4(map.externalIPv4)) {\n codeIp = map.externalIPv4!;\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"internet\";\n } else {\n codeIp = map?.localIPv4 ?? (await lanIPv4());\n if (codeIp) {\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"same_network\";\n }\n }\n const joinUrl = o.http && codeIp ? `http://${codeIp}:${hub.port}/` : null;\n return Object.assign(lobby, { directHub: hub, joinUrl });\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/direct/index.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\n/**\n * `onelobby/direct` (Node only): host a direct-IP lobby from a Node process, with no server of anyone's\n * (docs/design/direct-ip.md). The process runs the signaling hub, listens on TCP `port`, binds its peer links to UDP `port`,\n * asks the router to forward both (UPnP, then NAT-PMP) and hands out a join code. Joiners use `kit.joinDirect(code)`\n * from `onelobby` (Node or browser) or `JoinDirectAsync` in C#; the wire protocol is the same, so they mix.\n *\n * WebRTC in Node needs an RTCPeerConnection: `npm install node-datachannel`, then\n * `advanced: { linkFactory: webRtcLinkFactoryFor(RTCPeerConnection) }` with\n * `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. Its libdatachannel honours the port pin.\n */\nimport type { HostOptions, OneLobby } from \"../onelobby.js\";\nimport type { Lobby, RateLimit } from \"../lobby.js\";\nimport type { LinkFactory } from \"../transport.js\";\nimport { encodeJoinCode, encodeRoomCode } from \"../joincode.js\";\nimport { joinCodeKindName, type IpCodeKind, type JoinCodeKind } from \"../join-code-kind.js\";\nimport { NostrHostBridge } from \"../nostr/host.js\";\nimport { HOST_FIRST_RELAY_MS } from \"../nostr/limits.js\";\nimport { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_STUN } from \"../nostr/relays.js\";\nimport { DEFAULT_DIRECT_PORT } from \"../direct-address.js\";\nimport { DirectHubHost, PortInUseError, portFailureHint, type HttpHandler } from \"./hub-host.js\";\nimport { directHubOptions } from \"./hub.js\";\nimport { isPublicIPv4, lanIPv4, localAddresses, type PortMapper, type PortMapResult, type PortMapperOptions } from \"./portmap.js\";\nimport { OneLobbyError } from \"../signaling.js\";\n\nexport { DirectHubHost, LocalHubSocket, PortInUseError, originAllowed } from \"./hub-host.js\";\nexport type { HttpHandler, HttpReply, DirectJoinInfo } from \"./hub-host.js\";\nexport { SignalingHub, Session, RateWindow, sanitize, defaultHubOptions, directHubOptions, PROTO } from \"./hub.js\";\nexport type { HubOptions, HubDeps } from \"./hub.js\";\nexport { PortMapper, isPublicIPv4, lanIPv4, localAddresses } from \"./portmap.js\";\nexport type { PortMapResult, PortMapperOptions, PortMapMethod } from \"./portmap.js\";\nexport { webRtcLinkFactoryFor } from \"../webrtc.js\";\nexport { NostrHostBridge } from \"../nostr/host.js\";\n\nexport interface DirectHostOptions {\n /**\n * TCP and UDP port to listen on (default 47810). When it is busy (another program holds it) the next free one of the\n * following `fallbackPorts` is used; the join code carries the port, so friends never type it.\n */\n port?: number;\n /** Test seam: listen on this one local address (e.g. \"127.0.0.1\") instead of every interface. Default: every interface. */\n bindHost?: string;\n /** How many ports after `port` to try when it is busy; default 9 with `port` left out, else 0. */\n fallbackPorts?: number;\n /** Optional; joiners must send the same one or get `bad_password`. */\n password?: string;\n maxPlayers?: number;\n /** Lobby name; default \"<displayName>'s game\". */\n name?: string;\n lateJoin?: boolean;\n meta?: Record<string, string>;\n /** Ask the router to forward the port so the code works from the internet (s.2a). The host-menu toggle; default on. */\n openPort?: boolean;\n /**\n * Nostr signaling (spec/nostr-signaling.md), default on: `lobby.joinCode` is a version 1 code that joiners reach through\n * public Nostr relays, so it works with no port forwarded; the IP code moves to `lobby.directCode`. `false`: IP codes\n * only (`joinCode === directCode`). An object picks the relays (default: the blueprint's `hosting.nostrRelays`, else the\n * library list) and the STUN servers (default Google's and Cloudflare's public ones).\n */\n nostr?: boolean | { relays?: string[]; stun?: string[] };\n /**\n * This machine's public IPv4 address when the player forwarded the port by hand (no UPnP): the join code and\n * `joinUrl` carry it and `joinCodeKind` is \"internet\". Default: the `ONELOBBY_PUBLIC_IP` environment variable (then `portMapping` reads \"manual (ONELOBBY_PUBLIC_IP)\"). With it\n * set the router is not asked unless `openPort: true` is passed.\n */\n publicAddress?: string;\n /** Test seam and knobs for the router request (description defaults to \"OneLobby <gameId>\"). */\n portMapping?: PortMapperOptions;\n /**\n * Serves plain-HTTP `GET`s on the same port, typically a join page for browsers (recipe s.4b). The page joins with\n * `kit.joinDirect(location.host)`; the lobby's `joinUrl` is then the link to share.\n */\n http?: HttpHandler;\n /**\n * Browser `Origin` values allowed to open a signaling socket (exact match, e.g. \"https://play.example\"; \"*\" allows\n * every origin). Native clients send no Origin and are always allowed, as are pages served from the host's own address\n * (`http`) and localhost pages, so this is only for a web game hosted somewhere else that joins players to this host.\n */\n allowedOrigins?: string[];\n /** Per-joiner cap on incoming raw messages, e.g. `{ messages: 60, perMs: 1000 }`; the excess is dropped. */\n rateLimit?: RateLimit;\n /** Receives the startup line (`lobby.directHostLog`) once the host is up. Default `console.log`; `false` prints nothing. */\n log?: ((line: string) => void) | false;\n}\n\n/**\n * The one startup line for a direct host, character for character as C#'s `DirectLobby.FormatHostLog`, e.g.\n * `OneLobby: direct host listening on TCP+UDP :47811 (47810 busy); code 0A00-0BSE-P3S (SameNetwork); port mapping off`.\n */\nexport function formatHostLog(\n requested: number,\n bound: number,\n code: string,\n kind: JoinCodeKind,\n mapping: string,\n /** With Nostr on: the IP code and how many relays are connected (\"4/5\", or \"\" for none reachable). */\n nostr?: { directCode: string; directCodeKind: IpCodeKind; relays: string },\n): string {\n let line = `OneLobby: direct host listening on TCP+UDP :${bound}`;\n if (bound !== requested) line += ` (${bound - requested === 1 ? requested : `${requested}-${bound - 1}`} busy)`;\n const name = joinCodeKindName;\n line += code ? `; code ${code} (${name(kind)})` : \"; no join code (no IPv4 address)\";\n if (nostr) line += nostr.directCode ? `; ip code ${nostr.directCode} (${name(nostr.directCodeKind)})` : \"; no ip code (no IPv4 address)\";\n line += `; port mapping ${mapping || \"off\"}`;\n if (nostr) line += nostr.relays ? `; nostr ${nostr.relays} relays` : \"; nostr: no relay reachable\";\n return line;\n}\n\n/** A direct host lobby: the normal Lobby, plus the hub behind it. Leaving or ending the lobby stops the hub. */\nexport type DirectLobby = Lobby & {\n directHub: DirectHubHost;\n /** With the `http` option: `http://<the code's address>:<port>/`, the link a browser opens to join. Else null. */\n joinUrl: string | null;\n};\n\n/**\n * Hosts a direct-IP lobby (docs/design/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port\n * (unless `openPort: false`), connects `kit` to the hub in memory and creates the lobby. Resolves with the lobby;\n * show `lobby.joinCode` with `lobby.joinCodeKind` (\"internet\" or \"same_network\"). Rejects with `port_in_use`.\n * When the lobby ends (leave, or the process exits) the listener closes and the router mapping is removed; joiners\n * get `sessionEnded(\"host_left\")`.\n */\n/** With `port` left out, how many ports after the default `hostDirect` tries when it is busy. */\nexport const DEFAULT_FALLBACK_PORTS = 9;\n\nexport async function hostDirect(kit: OneLobby, opts: DirectHostOptions = {}): Promise<DirectLobby> {\n const bp = kit.blueprint;\n const defined = Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined)) as DirectHostOptions;\n // a blueprint supplies hosting.* and lobby.* (explicit options win)\n const o: DirectHostOptions = bp\n ? {\n port: bp.hosting.directPort,\n openPort: bp.hosting.openPort,\n nostr: bp.hosting.nostr ? { relays: [...bp.hosting.nostrRelays] } : false,\n allowedOrigins: [...bp.hosting.allowedOrigins],\n maxPlayers: bp.lobby.maxPlayers,\n lateJoin: bp.lobby.lateJoin,\n meta: { ...bp.lobby.meta },\n ...(bp.session.rateLimit ? { rateLimit: { ...bp.session.rateLimit } } : {}),\n ...defined,\n }\n : defined;\n const port = o.port ?? DEFAULT_DIRECT_PORT;\n // busy (another program holds it): try the next fallbackPorts; the join code carries the port\n const last = port + Math.max(0, o.fallbackPorts ?? (o.port === undefined ? DEFAULT_FALLBACK_PORTS : 0));\n const maxPlayers = o.maxPlayers ?? 4;\n const nostrOn = o.nostr !== false;\n const nostrCfg = typeof o.nostr === \"object\" ? o.nostr : {};\n const stunUrls = nostrOn ? (nostrCfg.stun ?? [...DEFAULT_NOSTR_STUN]) : [];\n let hub: DirectHubHost | undefined;\n let anyBusy = false;\n let anyReserved = false;\n for (let p = port; !hub; p++) {\n try {\n hub = await DirectHubHost.listen(p, { ...directHubOptions(maxPlayers, o.password), stunUrls }, o.bindHost ? { host: o.bindHost } : undefined);\n } catch (e) {\n if (!(e instanceof PortInUseError)) throw e;\n if (e.reserved) anyReserved = true;\n else anyBusy = true;\n if (p < last) continue;\n const range = last > port ? `ports ${port}-${last}` : `port ${port}`;\n throw new OneLobbyError(\"port_in_use\", `cannot listen on ${range}: ${portFailureHint(anyBusy, anyReserved)}`);\n }\n }\n hub.http = o.http ?? null;\n hub.allowedOrigins = o.allowedOrigins ?? [];\n const manual = (o.publicAddress ?? process.env.ONELOBBY_PUBLIC_IP ?? \"\").trim() || null;\n if (manual && !isIPv4(manual)) {\n await hub.close();\n throw new OneLobbyError(\"bad_address\", `publicAddress must be an IPv4 address like 203.0.113.7, got \"${manual}\"`);\n }\n // The router request runs alongside lobby creation; it takes up to a few seconds when nothing answers.\n const mapping: Promise<PortMapResult | null> =\n o.openPort === false || (manual && opts.openPort !== true)\n ? Promise.resolve(null)\n : hub.openPort({ description: `OneLobby ${kit.options.gameId}`, ...o.portMapping }).catch((e: Error) => ({\n mapped: false, method: \"none\" as const, externalIPv4: null, localIPv4: null, detail: e.message,\n }));\n const wrapLinks = (inner: LinkFactory): LinkFactory => ({\n // Nostr joiners have no address to dial: the host needs STUN too (the UDP port pin stays)\n create: (lo) => inner.create({ ...lo, iceServers: nostrOn ? lo.iceServers : [], portRange: { begin: hub.port, end: hub.port } }),\n });\n let lobby: Lobby;\n try {\n const name = o.name ?? `${kit.options.displayName || \"Player\"}'s game`;\n const h: HostOptions = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta, rateLimit: o.rateLimit };\n lobby = await kit.hostDirectWith(h, { socketFactory: () => hub.connectLocal(), wrapLinks });\n } catch (e) {\n await mapping;\n await hub.close();\n throw e;\n }\n lobby.linkAddressSink = (peerId, ip) => hub.hub.reportLinkAddress(peerId, ip);\n lobby.on(\"sessionEnded\", () => void hub.close());\n // leave() resolves once the router mapping is deleted, so a host that exits right after it does not leave the port\n // forwarded until the lease runs out (2 h, or never on routers that only grant permanent leases)\n const leave = lobby.leave.bind(lobby);\n lobby.leave = async () => {\n try {\n await leave();\n } finally {\n await hub.close();\n }\n };\n // Nostr: the room code is ready at once; relays connect while the router answers\n let relaysNote = \"\";\n let nostrUp = false;\n let nostrCode = \"\";\n let onRelays: (() => void) | null = null;\n if (nostrOn) {\n const env = kit.nostrHostEnv();\n nostrCode = encodeRoomCode();\n try {\n const bridge = await NostrHostBridge.start(hub.hub, {\n code: nostrCode,\n relays: nostrCfg.relays && nostrCfg.relays.length > 0 ? nostrCfg.relays : [...DEFAULT_NOSTR_RELAYS],\n connect: env.connect,\n clock: env.clock,\n wall: env.wall,\n maxPlayers,\n onChange: () => onRelays?.(),\n });\n hub.nostr = bridge;\n lobby.on(\"sessionEnded\", () => bridge.close());\n nostrUp = await settleRelays(bridge);\n relaysNote = nostrUp ? `${bridge.relaysConnected}/${bridge.relaysTotal}` : \"\";\n } catch {\n nostrCode = \"\"; // no WebSocket in this runtime, say: the host keeps hosting by IP\n }\n }\n const map = await mapping;\n const direct = Object.assign(lobby, { directHub: hub, joinUrl: null as string | null });\n direct.directAddresses = localAddresses(hub.port);\n const log = o.log === false ? null : (o.log ?? console.log);\n // Sets joinCode / joinCodeKind / directCode / joinUrl / portMapping / directHostLog and what GET /joininfo answers.\n let lastIp: [string | null, Exclude<IpCodeKind, \"none\">, string] = [null, \"same_network\", \"off\"];\n const applyCode = (ip: string | null, kind: Exclude<IpCodeKind, \"none\">, mappingNote: string) => {\n lastIp = [ip, kind, mappingNote];\n direct.portMapping = mappingNote;\n direct.directCode = ip ? encodeJoinCode(ip, hub.port) : \"\";\n direct.directCodeKind = ip ? kind : \"none\";\n if (nostrOn && nostrCode && nostrUp) {\n direct.joinCode = nostrCode;\n direct.joinCodeKind = \"nostr\";\n } else {\n // Nostr off, or no relay reachable (yet): the IP code is the code; it turns Nostr when a relay connects\n direct.joinCode = direct.directCode;\n direct.joinCodeKind = direct.directCodeKind;\n }\n direct.joinUrl = o.http && ip ? `http://${ip}:${hub.port}/` : null;\n direct.directHostLog = formatHostLog(port, hub.port, direct.joinCode, direct.joinCodeKind, direct.portMapping, nostrOn ? { directCode: direct.directCode, directCodeKind: direct.directCodeKind, relays: relaysNote } : undefined);\n hub.joinInfo = { joinCode: direct.joinCode, joinCodeKind: direct.joinCodeKind, directCode: direct.directCode, directCodeKind: direct.directCodeKind, joinUrl: direct.joinUrl, port: hub.port };\n log?.(direct.directHostLog);\n };\n if (manual) applyCode(manual, \"internet\", map?.mapped ? map.detail : o.publicAddress ? \"manual\" : \"manual (ONELOBBY_PUBLIC_IP)\");\n else if (map?.mapped && isPublicIPv4(map.externalIPv4)) applyCode(map.externalIPv4, \"internet\", map.detail);\n else {\n applyCode(map?.localIPv4 ?? (await lanIPv4()), \"same_network\", map ? map.detail : \"off\");\n if (map && !map.mapped && hub.portMapper) retryMapping(hub.portMapper, o.portMapping?.retryMs ?? 30000, (r) => {\n if (!isPublicIPv4(r.externalIPv4)) return;\n applyCode(r.externalIPv4, \"internet\", r.detail);\n direct.notifyJoinCodeChanged();\n });\n }\n // a relay answering after the 10 s wait: the Nostr code becomes the joinCode\n onRelays = () => {\n const b = hub.nostr;\n if (!b || b.relaysConnected === 0 || nostrUp) return;\n nostrUp = true;\n relaysNote = `${b.relaysConnected}/${b.relaysTotal}`;\n applyCode(...lastIp);\n direct.notifyJoinCodeChanged();\n };\n return direct;\n}\n\nconst MAX_RETRY_MS = 5 * 60_000;\n\n/** Asks the router again after `firstMs`, doubling up to 5 minutes, until a mapping lands or the host quits. */\nfunction retryMapping(mapper: PortMapper, firstMs: number, onMapped: (r: PortMapResult) => void): void {\n if (!(firstMs > 0)) return;\n let wait = firstMs;\n const round = () => {\n const t = setTimeout(async () => {\n if (mapper.closed) return;\n const r = await mapper.open();\n if (mapper.closed) return;\n if (r.mapped) onMapped(r);\n else {\n wait = Math.min(wait * 2, MAX_RETRY_MS);\n round();\n }\n }, wait);\n t.unref?.();\n };\n round();\n}\n\n/** Waits for the relays to connect: true once at least one has (then up to 1.5 s more for the rest), false after 10 s. */\nfunction settleRelays(bridge: NostrHostBridge): Promise<boolean> {\n return bridge.settle(HOST_FIRST_RELAY_MS);\n}\n\nfunction isIPv4(s: string): boolean {\n const m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(s);\n return !!m && m.slice(1).every((x) => Number(x) <= 255);\n}\n"]}
@@ -0,0 +1,12 @@
1
+ /** Tiny Prometheus text exposition: labelled counters plus gauges read at scrape time (C#'s `Metrics` in `server/OneLobby.Hub`). */
2
+ export declare class Metrics {
3
+ private readonly counters;
4
+ private readonly gauges;
5
+ private readonly help;
6
+ describe(name: string, help: string): void;
7
+ gauge(name: string, help: string, read: () => number): void;
8
+ /** Counter name plus optional single label, e.g. inc("onelobby_path_total", "path", "relayed"). */
9
+ inc(name: string, label?: string, value?: string, by?: number): void;
10
+ get(name: string, label?: string, value?: string): number;
11
+ render(): string;
12
+ }
@@ -0,0 +1,45 @@
1
+ /** Tiny Prometheus text exposition: labelled counters plus gauges read at scrape time (C#'s `Metrics` in `server/OneLobby.Hub`). */
2
+ export class Metrics {
3
+ counters = new Map();
4
+ gauges = new Map();
5
+ help = new Map();
6
+ describe(name, help) {
7
+ this.help.set(name, help);
8
+ }
9
+ gauge(name, help, read) {
10
+ this.gauges.set(name, { help, read });
11
+ }
12
+ /** Counter name plus optional single label, e.g. inc("onelobby_path_total", "path", "relayed"). */
13
+ inc(name, label, value, by = 1) {
14
+ const key = keyOf(name, label, value);
15
+ this.counters.set(key, (this.counters.get(key) ?? 0) + by);
16
+ }
17
+ get(name, label, value) {
18
+ return this.counters.get(keyOf(name, label, value)) ?? 0;
19
+ }
20
+ render() {
21
+ let out = "";
22
+ for (const name of [...this.gauges.keys()].sort(ordinal)) {
23
+ const g = this.gauges.get(name);
24
+ out += `# HELP ${name} ${g.help}\n# TYPE ${name} gauge\n${name} ${g.read()}\n`;
25
+ }
26
+ let lastBase = null;
27
+ for (const key of [...this.counters.keys()].sort(ordinal)) {
28
+ const br = key.indexOf("{");
29
+ const base = br < 0 ? key : key.slice(0, br);
30
+ if (base !== lastBase) {
31
+ const h = this.help.get(base);
32
+ if (h !== undefined)
33
+ out += `# HELP ${base} ${h}\n`;
34
+ out += `# TYPE ${base} counter\n`;
35
+ lastBase = base;
36
+ }
37
+ out += `${key} ${this.counters.get(key)}\n`;
38
+ }
39
+ return out;
40
+ }
41
+ }
42
+ const ordinal = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
43
+ const escape = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
44
+ const keyOf = (name, label, value) => (label === undefined ? name : `${name}{${label}="${escape(value ?? "")}"}`);
45
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/direct/metrics.ts"],"names":[],"mappings":"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","sourcesContent":["/** Tiny Prometheus text exposition: labelled counters plus gauges read at scrape time (C#'s `Metrics` in `server/OneLobby.Hub`). */\nexport class Metrics {\n private readonly counters = new Map<string, number>();\n private readonly gauges = new Map<string, { help: string; read: () => number }>();\n private readonly help = new Map<string, string>();\n\n describe(name: string, help: string): void {\n this.help.set(name, help);\n }\n\n gauge(name: string, help: string, read: () => number): void {\n this.gauges.set(name, { help, read });\n }\n\n /** Counter name plus optional single label, e.g. inc(\"onelobby_path_total\", \"path\", \"relayed\"). */\n inc(name: string, label?: string, value?: string, by = 1): void {\n const key = keyOf(name, label, value);\n this.counters.set(key, (this.counters.get(key) ?? 0) + by);\n }\n\n get(name: string, label?: string, value?: string): number {\n return this.counters.get(keyOf(name, label, value)) ?? 0;\n }\n\n render(): string {\n let out = \"\";\n for (const name of [...this.gauges.keys()].sort(ordinal)) {\n const g = this.gauges.get(name)!;\n out += `# HELP ${name} ${g.help}\\n# TYPE ${name} gauge\\n${name} ${g.read()}\\n`;\n }\n let lastBase: string | null = null;\n for (const key of [...this.counters.keys()].sort(ordinal)) {\n const br = key.indexOf(\"{\");\n const base = br < 0 ? key : key.slice(0, br);\n if (base !== lastBase) {\n const h = this.help.get(base);\n if (h !== undefined) out += `# HELP ${base} ${h}\\n`;\n out += `# TYPE ${base} counter\\n`;\n lastBase = base;\n }\n out += `${key} ${this.counters.get(key)}\\n`;\n }\n return out;\n }\n}\n\nconst ordinal = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);\nconst escape = (s: string): string => s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/\\n/g, \"\\\\n\");\nconst keyOf = (name: string, label?: string, value?: string): string => (label === undefined ? name : `${name}{${label}=\"${escape(value ?? \"\")}\"}`);\n"]}
@@ -29,6 +29,11 @@ export interface PortMapperOptions {
29
29
  description?: string;
30
30
  tryUpnp?: boolean;
31
31
  tryNatPmp?: boolean;
32
+ /**
33
+ * `hostDirect` only: when nothing was mapped, ask the router again after this many ms, doubling up to 5 minutes, until
34
+ * it answers or the host quits (a router still booting after a power cut, UPnP switched back on). Default 30000; 0 never.
35
+ */
36
+ retryMs?: number;
32
37
  /**
33
38
  * Optional log hook; left out (default) stays quiet. Gets one line when the host quits and its mappings are deleted:
34
39
  * `direct host mapping deleted port=47810 protocols=TCP,UDP mapping=UPnP`.
@@ -36,7 +41,7 @@ export interface PortMapperOptions {
36
41
  log?: (line: string) => void;
37
42
  }
38
43
  /**
39
- * Opens TCP and UDP `port` on the home router for a direct-IP host (docs/direct-ip.md s.2a): UPnP IGD (SSDP, then
44
+ * Opens TCP and UDP `port` on the home router for a direct-IP host (docs/design/direct-ip.md s.2a): UPnP IGD (SSDP, then
40
45
  * GetExternalIPAddress + AddPortMapping), then NAT-PMP (RFC 6886). Node twin of C#'s `PortMapper`, same order and rules:
41
46
  * same external and internal port or nothing (a join code carries one port), leases renew at half-life, {@link close}
42
47
  * deletes the mappings. Windows Firewall drops SSDP replies on a network marked Public; the result is then unmapped.
@@ -58,7 +63,9 @@ export declare class PortMapper {
58
63
  */
59
64
  private readonly maybeMapped;
60
65
  constructor(port: number, options?: PortMapperOptions);
61
- /** Finds the router and maps TCP + UDP `port`. Never rejects for network trouble: see `detail`. */
66
+ /** True once {@link close} ran: no more requests go out. */
67
+ get closed(): boolean;
68
+ /** Finds the router and maps TCP + UDP `port`. Never rejects for network trouble: see `detail`. May run again after an unmapped result. */
62
69
  open(): Promise<PortMapResult>;
63
70
  private openBoth;
64
71
  private done;
@@ -21,7 +21,7 @@ class UpnpError extends Error {
21
21
  }
22
22
  }
23
23
  /**
24
- * Opens TCP and UDP `port` on the home router for a direct-IP host (docs/direct-ip.md s.2a): UPnP IGD (SSDP, then
24
+ * Opens TCP and UDP `port` on the home router for a direct-IP host (docs/design/direct-ip.md s.2a): UPnP IGD (SSDP, then
25
25
  * GetExternalIPAddress + AddPortMapping), then NAT-PMP (RFC 6886). Node twin of C#'s `PortMapper`, same order and rules:
26
26
  * same external and internal port or nothing (a join code carries one port), leases renew at half-life, {@link close}
27
27
  * deletes the mappings. Windows Firewall drops SSDP replies on a network marked Public; the result is then unmapped.
@@ -54,10 +54,15 @@ export class PortMapper {
54
54
  description: "OneLobby",
55
55
  tryUpnp: true,
56
56
  tryNatPmp: true,
57
+ retryMs: 30000,
57
58
  ...options,
58
59
  };
59
60
  }
60
- /** Finds the router and maps TCP + UDP `port`. Never rejects for network trouble: see `detail`. */
61
+ /** True once {@link close} ran: no more requests go out. */
62
+ get closed() {
63
+ return this.stopped;
64
+ }
65
+ /** Finds the router and maps TCP + UDP `port`. Never rejects for network trouble: see `detail`. May run again after an unmapped result. */
61
66
  open() {
62
67
  return (this.opening = this.openBoth());
63
68
  }