nixamp 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.d.ts CHANGED
@@ -22,6 +22,11 @@ export interface DaemonState {
22
22
  }[];
23
23
  /** The firewall standing between this port and the rest of the network. */
24
24
  firewall?: string | null;
25
+ /**
26
+ * The public address was asked of an outside service rather than found on an
27
+ * interface, so it names the router and not this port.
28
+ */
29
+ guessedPublic?: boolean;
25
30
  }
26
31
  /** XDG, with the usual fallback. One daemon per user, which is one too few for nobody. */
27
32
  export declare function stateDir(): string;
package/dist/daemon.js CHANGED
@@ -84,6 +84,9 @@ export function daemonLines(state) {
84
84
  for (const { label, url } of addresses)
85
85
  lines.push(` ${label.padEnd(width)} ${link(url)}`);
86
86
  lines.push(` ${"source".padEnd(width)} ${state.source}`);
87
+ if (state.guessedPublic) {
88
+ lines.push("", " That internet address is this machine's router, not this port.", ` Nothing outside reaches it until ${state.port} is forwarded here.`);
89
+ }
87
90
  if (state.firewall) {
88
91
  const { open } = portCommands(state.firewall, state.port);
89
92
  lines.push("", ` ${state.firewall} is running, so nothing else can reach port ${state.port} yet:`, ` sudo ${open.join(" ")}`, " or `nixamp daemon stop` and start again with --open-port.");
@@ -158,6 +161,7 @@ async function waitForAnnounce(log, timeoutMs) {
158
161
  source: String(parsed["source"]),
159
162
  ...(Array.isArray(urls) ? { urls: urls } : {}),
160
163
  ...(typeof parsed["firewall"] === "string" ? { firewall: parsed["firewall"] } : {}),
164
+ ...(parsed["guessedPublic"] === true ? { guessedPublic: true } : {}),
161
165
  };
162
166
  }
163
167
  }
package/dist/main.js CHANGED
@@ -70,6 +70,8 @@ Options for serve:
70
70
  --public-url URL the address this server is reachable at from outside,
71
71
  when that is a tunnel or a forwarded port rather than one of
72
72
  its own interfaces. Also NIXAMP_PUBLIC_URL
73
+ --no-lookup do not ask ipinfo.io what this machine's public address is
74
+ when nothing local looks public
73
75
  --ingest accept a live stream in at POST /api/ingest
74
76
  --rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
75
77
  --rtmp-streams N how many may publish at once (default 3, a port each)
package/dist/server.d.ts CHANGED
@@ -56,6 +56,12 @@ export interface ServeOptions {
56
56
  * listing is skipped and the printed links only work inside the house.
57
57
  */
58
58
  publicUrl: string;
59
+ /**
60
+ * Ask an outside service what this machine's public address is, when no
61
+ * interface holds one and none was given. Behind NAT that is the only way to
62
+ * learn it, and it is one short request at startup.
63
+ */
64
+ lookup: boolean;
59
65
  /**
60
66
  * Charge for listening once the stream is busy. Off unless asked for, and
61
67
  * useless without somewhere to pay: see NIXAMP_PAY_TO.
package/dist/server.js CHANGED
@@ -34,7 +34,7 @@ import { notifyAll, resendEmail, webPush } from "./notify.js";
34
34
  import { confirm, DEFAULT_DIRECTORY, Publisher } from "./publish.js";
35
35
  import { applyRemoteConfig, createPaywall, FREE_LISTENERS, paywallFromEnv, } from "./paywall.js";
36
36
  import { isRemote, playsInBrowser } from "./sources.js";
37
- import { allowedForListening, elevate, firewallInUse, keyCookie, keyFrom, newKey, portCommands, reachableAddresses, scopeOf, shareLink, audioLink, } from "./share.js";
37
+ import { allowedForListening, elevate, firewallInUse, keyCookie, keyFrom, lookupPublicIp, newKey, portCommands, reachableAddresses, scopeOf, shareLink, audioLink, } from "./share.js";
38
38
  import { extname, join, normalize, resolve, sep } from "node:path";
39
39
  import { fileURLToPath } from "node:url";
40
40
  import { detectTools, peaks, RATE, Stream, toMono, } from "./audio.js";
@@ -67,6 +67,7 @@ export function parseServeArgs(argv) {
67
67
  publish: "ask",
68
68
  name: "",
69
69
  publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
70
+ lookup: true,
70
71
  x402: false,
71
72
  owner: "",
72
73
  ingest: false,
@@ -127,6 +128,9 @@ export function parseServeArgs(argv) {
127
128
  }
128
129
  options.publicUrl = given.replace(/\/+$/, "");
129
130
  }
131
+ else if (arg === "--no-lookup") {
132
+ options.lookup = false;
133
+ }
130
134
  else if (arg === "--name") {
131
135
  options.name = value();
132
136
  }
@@ -2087,7 +2091,18 @@ export async function serve(argv, version = "0.1.0") {
2087
2091
  };
2088
2092
  // The link, not the address. Without the key the address is a 401, so
2089
2093
  // printing a bare host:port would be printing something that does not work.
2090
- const addresses = reachableAddresses(options.host, port, options.publicUrl);
2094
+ //
2095
+ // Behind NAT no interface holds the public address, so if nobody said what it
2096
+ // is and nothing local looks public, ask. What comes back is a fact about the
2097
+ // router and not about this port -- the port still has to be forwarded -- so
2098
+ // it is marked as a guess and everything that prints it says so.
2099
+ const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
2100
+ const guessedPublic = options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
2101
+ ? await lookupPublicIp()
2102
+ : "";
2103
+ const addresses = guessedPublic
2104
+ ? reachableAddresses(options.host, port, `http://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`)
2105
+ : localAddresses;
2091
2106
  // Listening on every interface proves the socket is open here and nothing
2092
2107
  // about the path between here and the phone.
2093
2108
  const listening = options.host === "0.0.0.0" || options.host === "::";
@@ -2108,6 +2123,7 @@ export async function serve(argv, version = "0.1.0") {
2108
2123
  source: root,
2109
2124
  urls: addresses,
2110
2125
  firewall,
2126
+ guessedPublic: guessedPublic !== "",
2111
2127
  }));
2112
2128
  }
2113
2129
  // The other half of "names now, tags later". It runs while the banner is
@@ -2128,6 +2144,11 @@ export async function serve(argv, version = "0.1.0") {
2128
2144
  for (const { label, url } of addresses) {
2129
2145
  console.log(` ${label.padEnd(width)} ${shareLink(url, key)}`);
2130
2146
  }
2147
+ if (guessedPublic) {
2148
+ console.log("");
2149
+ console.log(` That internet address is this machine's router, not this port.`);
2150
+ console.log(` Nothing outside reaches it until ${port} is forwarded here.`);
2151
+ }
2131
2152
  console.log("");
2132
2153
  if (key === null) {
2133
2154
  console.log(" No key: anyone who can reach this port can drive it and hear it.");
package/dist/share.d.ts CHANGED
@@ -25,6 +25,20 @@ export declare function keyFrom(request: IncomingMessage, url: URL): string | nu
25
25
  export declare function keyCookie(key: string): string;
26
26
  /** Where an address actually goes, which is not always where you would like. */
27
27
  export declare function classify(address: string): "private" | "cgnat" | "public";
28
+ /** Looks like an address, rather than an error page or a rate-limit notice. */
29
+ export declare function isIpAddress(value: string): boolean;
30
+ /**
31
+ * The address the rest of the world sees this machine as, by asking.
32
+ *
33
+ * Behind NAT no interface holds it, so there is nobody to ask but somebody
34
+ * outside. It is a claim about the router, not about this port: an address
35
+ * discovered this way is only reachable once the router forwards the port to
36
+ * this machine, which is why what prints it says so.
37
+ *
38
+ * Empty string for every failure. A player must not spend its startup waiting
39
+ * on somebody else's web service.
40
+ */
41
+ export declare function lookupPublicIp(send?: typeof fetch, timeoutMs?: number): Promise<string>;
28
42
  /**
29
43
  * The addresses another device could actually reach this machine on, nearest
30
44
  * first. On a server the public one is the point: it is the address a phone
package/dist/share.js CHANGED
@@ -74,6 +74,41 @@ export function classify(address) {
74
74
  return "cgnat";
75
75
  return "public";
76
76
  }
77
+ /** Looks like an address, rather than an error page or a rate-limit notice. */
78
+ export function isIpAddress(value) {
79
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) {
80
+ return value.split(".").every((part) => Number(part) <= 255);
81
+ }
82
+ // Enough of v6 to reject prose: hex groups and colons, nothing else. This is
83
+ // not a validator, it is a guard against printing an error page as an address.
84
+ return value.includes(":") && /^[0-9a-f:]+$/i.test(value);
85
+ }
86
+ /**
87
+ * The address the rest of the world sees this machine as, by asking.
88
+ *
89
+ * Behind NAT no interface holds it, so there is nobody to ask but somebody
90
+ * outside. It is a claim about the router, not about this port: an address
91
+ * discovered this way is only reachable once the router forwards the port to
92
+ * this machine, which is why what prints it says so.
93
+ *
94
+ * Empty string for every failure. A player must not spend its startup waiting
95
+ * on somebody else's web service.
96
+ */
97
+ export async function lookupPublicIp(send = fetch, timeoutMs = 2500) {
98
+ try {
99
+ const answer = await send("https://ipinfo.io/ip", {
100
+ signal: AbortSignal.timeout(timeoutMs),
101
+ headers: { accept: "text/plain", "user-agent": "nixamp" },
102
+ });
103
+ if (!answer.ok)
104
+ return "";
105
+ const found = (await answer.text()).trim();
106
+ return isIpAddress(found) ? found : "";
107
+ }
108
+ catch {
109
+ return "";
110
+ }
111
+ }
77
112
  /**
78
113
  * The addresses another device could actually reach this machine on, nearest
79
114
  * first. On a server the public one is the point: it is the address a phone
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/daemon.ts CHANGED
@@ -33,6 +33,11 @@ export interface DaemonState {
33
33
  urls?: { label: string; url: string }[];
34
34
  /** The firewall standing between this port and the rest of the network. */
35
35
  firewall?: string | null;
36
+ /**
37
+ * The public address was asked of an outside service rather than found on an
38
+ * interface, so it names the router and not this port.
39
+ */
40
+ guessedPublic?: boolean;
36
41
  }
37
42
 
38
43
  /** XDG, with the usual fallback. One daemon per user, which is one too few for nobody. */
@@ -115,6 +120,13 @@ export function daemonLines(state: DaemonState): string[] {
115
120
  for (const { label, url } of addresses) lines.push(` ${label.padEnd(width)} ${link(url)}`);
116
121
  lines.push(` ${"source".padEnd(width)} ${state.source}`);
117
122
 
123
+ if (state.guessedPublic) {
124
+ lines.push(
125
+ "",
126
+ " That internet address is this machine's router, not this port.",
127
+ ` Nothing outside reaches it until ${state.port} is forwarded here.`,
128
+ );
129
+ }
118
130
  if (state.firewall) {
119
131
  const { open } = portCommands(state.firewall as Firewall, state.port);
120
132
  lines.push(
@@ -209,6 +221,7 @@ async function waitForAnnounce(
209
221
  source: String(parsed["source"]),
210
222
  ...(Array.isArray(urls) ? { urls: urls as { label: string; url: string }[] } : {}),
211
223
  ...(typeof parsed["firewall"] === "string" ? { firewall: parsed["firewall"] } : {}),
224
+ ...(parsed["guessedPublic"] === true ? { guessedPublic: true } : {}),
212
225
  };
213
226
  }
214
227
  } catch {
package/src/main.ts CHANGED
@@ -94,6 +94,8 @@ Options for serve:
94
94
  --public-url URL the address this server is reachable at from outside,
95
95
  when that is a tunnel or a forwarded port rather than one of
96
96
  its own interfaces. Also NIXAMP_PUBLIC_URL
97
+ --no-lookup do not ask ipinfo.io what this machine's public address is
98
+ when nothing local looks public
97
99
  --ingest accept a live stream in at POST /api/ingest
98
100
  --rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
99
101
  --rtmp-streams N how many may publish at once (default 3, a port each)
package/src/server.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  keyCookie,
62
62
  keyFrom,
63
63
  keysMatch,
64
+ lookupPublicIp,
64
65
  newKey,
65
66
  portCommands,
66
67
  reachableAddresses,
@@ -128,6 +129,12 @@ export interface ServeOptions {
128
129
  * listing is skipped and the printed links only work inside the house.
129
130
  */
130
131
  publicUrl: string;
132
+ /**
133
+ * Ask an outside service what this machine's public address is, when no
134
+ * interface holds one and none was given. Behind NAT that is the only way to
135
+ * learn it, and it is one short request at startup.
136
+ */
137
+ lookup: boolean;
131
138
  /**
132
139
  * Charge for listening once the stream is busy. Off unless asked for, and
133
140
  * useless without somewhere to pay: see NIXAMP_PAY_TO.
@@ -178,6 +185,7 @@ export function parseServeArgs(argv: string[]): ServeOptions {
178
185
  publish: "ask",
179
186
  name: "",
180
187
  publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
188
+ lookup: true,
181
189
  x402: false,
182
190
  owner: "",
183
191
  ingest: false,
@@ -226,6 +234,8 @@ export function parseServeArgs(argv: string[]): ServeOptions {
226
234
  throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
227
235
  }
228
236
  options.publicUrl = given.replace(/\/+$/, "");
237
+ } else if (arg === "--no-lookup") {
238
+ options.lookup = false;
229
239
  } else if (arg === "--name") {
230
240
  options.name = value();
231
241
  } else if (arg === "--owner") {
@@ -2392,7 +2402,19 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
2392
2402
 
2393
2403
  // The link, not the address. Without the key the address is a 401, so
2394
2404
  // printing a bare host:port would be printing something that does not work.
2395
- const addresses = reachableAddresses(options.host, port, options.publicUrl);
2405
+ //
2406
+ // Behind NAT no interface holds the public address, so if nobody said what it
2407
+ // is and nothing local looks public, ask. What comes back is a fact about the
2408
+ // router and not about this port -- the port still has to be forwarded -- so
2409
+ // it is marked as a guess and everything that prints it says so.
2410
+ const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
2411
+ const guessedPublic =
2412
+ options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
2413
+ ? await lookupPublicIp()
2414
+ : "";
2415
+ const addresses = guessedPublic
2416
+ ? reachableAddresses(options.host, port, `http://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`)
2417
+ : localAddresses;
2396
2418
 
2397
2419
  // Listening on every interface proves the socket is open here and nothing
2398
2420
  // about the path between here and the phone.
@@ -2416,6 +2438,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
2416
2438
  source: root,
2417
2439
  urls: addresses,
2418
2440
  firewall,
2441
+ guessedPublic: guessedPublic !== "",
2419
2442
  }),
2420
2443
  );
2421
2444
  }
@@ -2441,6 +2464,12 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
2441
2464
  console.log(` ${label.padEnd(width)} ${shareLink(url, key)}`);
2442
2465
  }
2443
2466
 
2467
+ if (guessedPublic) {
2468
+ console.log("");
2469
+ console.log(` That internet address is this machine's router, not this port.`);
2470
+ console.log(` Nothing outside reaches it until ${port} is forwarded here.`);
2471
+ }
2472
+
2444
2473
  console.log("");
2445
2474
  if (key === null) {
2446
2475
  console.log(" No key: anyone who can reach this port can drive it and hear it.");
package/src/share.ts CHANGED
@@ -87,6 +87,41 @@ export function classify(address: string): "private" | "cgnat" | "public" {
87
87
  return "public";
88
88
  }
89
89
 
90
+ /** Looks like an address, rather than an error page or a rate-limit notice. */
91
+ export function isIpAddress(value: string): boolean {
92
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(value)) {
93
+ return value.split(".").every((part) => Number(part) <= 255);
94
+ }
95
+ // Enough of v6 to reject prose: hex groups and colons, nothing else. This is
96
+ // not a validator, it is a guard against printing an error page as an address.
97
+ return value.includes(":") && /^[0-9a-f:]+$/i.test(value);
98
+ }
99
+
100
+ /**
101
+ * The address the rest of the world sees this machine as, by asking.
102
+ *
103
+ * Behind NAT no interface holds it, so there is nobody to ask but somebody
104
+ * outside. It is a claim about the router, not about this port: an address
105
+ * discovered this way is only reachable once the router forwards the port to
106
+ * this machine, which is why what prints it says so.
107
+ *
108
+ * Empty string for every failure. A player must not spend its startup waiting
109
+ * on somebody else's web service.
110
+ */
111
+ export async function lookupPublicIp(send: typeof fetch = fetch, timeoutMs = 2500): Promise<string> {
112
+ try {
113
+ const answer = await send("https://ipinfo.io/ip", {
114
+ signal: AbortSignal.timeout(timeoutMs),
115
+ headers: { accept: "text/plain", "user-agent": "nixamp" },
116
+ });
117
+ if (!answer.ok) return "";
118
+ const found = (await answer.text()).trim();
119
+ return isIpAddress(found) ? found : "";
120
+ } catch {
121
+ return "";
122
+ }
123
+ }
124
+
90
125
  /**
91
126
  * The addresses another device could actually reach this machine on, nearest
92
127
  * first. On a server the public one is the point: it is the address a phone
package/web/dist/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1788937268228";
2
+ const CACHE = "nixamp-1788937565929";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/apple-touch-icon.png",