nixamp 0.7.41 → 0.9.3

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 (48) hide show
  1. package/dist/catalogs.d.ts +100 -0
  2. package/dist/catalogs.js +0 -0
  3. package/dist/certs.d.ts +112 -0
  4. package/dist/certs.js +217 -0
  5. package/dist/channels.d.ts +17 -0
  6. package/dist/channels.js +45 -0
  7. package/dist/directory.d.ts +10 -0
  8. package/dist/directory.js +6 -0
  9. package/dist/dns.d.ts +63 -0
  10. package/dist/dns.js +171 -0
  11. package/dist/library.d.ts +36 -0
  12. package/dist/library.js +167 -0
  13. package/dist/main.js +92 -4
  14. package/dist/names.d.ts +66 -0
  15. package/dist/names.js +184 -0
  16. package/dist/naming.d.ts +57 -0
  17. package/dist/naming.js +150 -0
  18. package/dist/owner.js +4 -0
  19. package/dist/publish.d.ts +2 -0
  20. package/dist/publish.js +3 -0
  21. package/dist/server.d.ts +37 -0
  22. package/dist/server.js +418 -5
  23. package/dist/session.d.ts +8 -0
  24. package/dist/session.js +103 -0
  25. package/package.json +8 -2
  26. package/src/catalogs.ts +0 -0
  27. package/src/certs.ts +294 -0
  28. package/src/channels.ts +41 -0
  29. package/src/directory.ts +16 -0
  30. package/src/dns.ts +207 -0
  31. package/src/library.ts +175 -0
  32. package/src/main.ts +88 -4
  33. package/src/names.ts +239 -0
  34. package/src/naming.ts +197 -0
  35. package/src/owner.ts +3 -0
  36. package/src/publish.ts +5 -0
  37. package/src/server.ts +446 -5
  38. package/src/session.ts +117 -0
  39. package/web/dist/assets/{hls-3VKVEQE3-70uzupqn.js → hls-3VKVEQE3-C88rYdXy.js} +1 -1
  40. package/web/dist/assets/index-BB3VT0Ks.js +1 -0
  41. package/web/dist/assets/index-DrXbwjOa.css +1 -0
  42. package/web/dist/assets/{mpegts-DQqgM7pi.js → mpegts-BMDK3Ac9.js} +1 -1
  43. package/web/dist/assets/{mpegts-LO6RVLD6-CzrQKX7m.js → mpegts-LO6RVLD6-C06vXzyy.js} +1 -1
  44. package/web/dist/index.html +23 -2
  45. package/web/dist/install.sh +3 -1
  46. package/web/dist/sw.js +6 -6
  47. package/web/dist/assets/index-ComwKkzf.js +0 -1
  48. package/web/dist/assets/index-D3xGDAOd.css +0 -1
package/dist/names.js ADDED
@@ -0,0 +1,184 @@
1
+ import { isReserved } from "./handles.js";
2
+ import { isIPv4, isIPv6 } from "./dns.js";
3
+ /** An error with the HTTP status it deserves, so a route can pass it straight on. */
4
+ export class NameError extends Error {
5
+ status;
6
+ constructor(status, message) {
7
+ super(message);
8
+ this.status = status;
9
+ }
10
+ }
11
+ /**
12
+ * Names nobody may give a server, over and above the handles nobody may take:
13
+ * the mail and discovery names a client resolves on its own, anything an
14
+ * underscore marks as a protocol record, and anything punycode-shaped.
15
+ */
16
+ const RESERVED_LABELS = new Set([
17
+ "www", "mail", "mx", "smtp", "imap", "pop", "ns1", "ns2", "ftp",
18
+ "_dmarc", "_acme-challenge", "autoconfig", "autodiscover",
19
+ ]);
20
+ /**
21
+ * The label a server may have: two to thirty of [a-z0-9-], not starting or
22
+ * ending with a hyphen, lowercased for the caller who typed it in capitals.
23
+ * "" when it is not one, so a route can say so in one sentence.
24
+ */
25
+ export function validLabel(value) {
26
+ if (typeof value !== "string")
27
+ return "";
28
+ const label = value.trim().toLowerCase();
29
+ if (!/^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$/.test(label))
30
+ return "";
31
+ if (label.startsWith("_") || label.startsWith("xn--"))
32
+ return "";
33
+ if (RESERVED_LABELS.has(label) || isReserved(label))
34
+ return "";
35
+ return label;
36
+ }
37
+ const TABLE = "dns_names";
38
+ const SCHEMA = `
39
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
40
+ account_id TEXT NOT NULL,
41
+ handle TEXT NOT NULL,
42
+ label TEXT NOT NULL,
43
+ a TEXT NOT NULL DEFAULT '',
44
+ aaaa TEXT NOT NULL DEFAULT '',
45
+ ttl INTEGER NOT NULL DEFAULT 600,
46
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
47
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
48
+ PRIMARY KEY (handle, label)
49
+ );
50
+ CREATE INDEX IF NOT EXISTS ${TABLE}_account ON ${TABLE} (account_id);
51
+ `;
52
+ export const MIN_TTL = 600;
53
+ export const MAX_TTL = 86_400;
54
+ /** Enough for a rack, not enough for a squatter. */
55
+ export const DEFAULT_PER_ACCOUNT = 20;
56
+ function when(value) {
57
+ if (value instanceof Date)
58
+ return value.getTime();
59
+ const parsed = new Date(String(value ?? 0)).getTime();
60
+ return Number.isFinite(parsed) ? parsed : 0;
61
+ }
62
+ export class Names {
63
+ db;
64
+ dns;
65
+ limits;
66
+ ready = null;
67
+ constructor(db, dns, limits = {}) {
68
+ this.db = db;
69
+ this.dns = dns;
70
+ this.limits = limits;
71
+ }
72
+ async ensure() {
73
+ this.ready ??= this.db.query(SCHEMA).then(() => undefined);
74
+ await this.ready;
75
+ }
76
+ host(handle, label) {
77
+ return `${label}.${handle}.${this.dns.zone}`;
78
+ }
79
+ record(handle, row) {
80
+ return {
81
+ label: row.label,
82
+ host: this.host(handle, row.label),
83
+ a: row.a ?? "",
84
+ aaaa: row.aaaa ?? "",
85
+ ttl: Number(row.ttl) || MIN_TTL,
86
+ createdAt: when(row.created_at),
87
+ updatedAt: when(row.updated_at),
88
+ };
89
+ }
90
+ async list(accountId, handle) {
91
+ await this.ensure();
92
+ const { rows } = await this.db.query(`SELECT account_id, handle, label, a, aaaa, ttl, created_at, updated_at
93
+ FROM ${TABLE} WHERE account_id = $1 AND handle = $2 ORDER BY created_at`, [accountId, handle]);
94
+ return rows.map((row) => this.record(handle, row));
95
+ }
96
+ /** The row for one name, whoever owns it. */
97
+ async find(handle, label) {
98
+ const { rows } = await this.db.query(`SELECT account_id, handle, label, a, aaaa, ttl, created_at, updated_at
99
+ FROM ${TABLE} WHERE handle = $1 AND label = $2`, [handle, label]);
100
+ return rows[0] ?? null;
101
+ }
102
+ /**
103
+ * Make or change a name. `null` for a family takes that record away,
104
+ * `undefined` leaves it as it was, so a server that has only ever had an
105
+ * IPv4 address can be given an IPv6 one without restating the first.
106
+ */
107
+ async set(accountId, handle, wanted, want) {
108
+ const label = validLabel(wanted);
109
+ if (label === "")
110
+ throw new NameError(400, "that is not a name a server can have");
111
+ if (want.a !== undefined && want.a !== null && !isIPv4(want.a)) {
112
+ throw new NameError(400, "that is not an IPv4 address");
113
+ }
114
+ if (want.aaaa !== undefined && want.aaaa !== null && !isIPv6(want.aaaa)) {
115
+ throw new NameError(400, "that is not an IPv6 address");
116
+ }
117
+ await this.ensure();
118
+ const existing = await this.find(handle, label);
119
+ if (existing !== null && existing.account_id !== accountId) {
120
+ throw new NameError(409, "that name belongs to somebody else");
121
+ }
122
+ if (existing === null) {
123
+ const { rows } = await this.db.query(`SELECT count(*)::int AS n FROM ${TABLE} WHERE account_id = $1`, [accountId]);
124
+ const have = Number(rows[0]?.n ?? 0);
125
+ const limit = this.limits.perAccount ?? DEFAULT_PER_ACCOUNT;
126
+ if (have >= limit)
127
+ throw new NameError(422, `an account may have ${limit} names, and this one has ${have}`);
128
+ }
129
+ // What the name will point at once this is done.
130
+ const a = want.a === undefined ? (existing?.a ?? "") : (want.a ?? "");
131
+ const aaaa = want.aaaa === undefined ? (existing?.aaaa ?? "") : (want.aaaa ?? "");
132
+ if (a === "" && aaaa === "")
133
+ throw new NameError(400, "give an address");
134
+ const ttl = Math.min(MAX_TTL, Math.max(MIN_TTL, Math.floor(want.ttl ?? existing?.ttl ?? MIN_TTL)));
135
+ // The zone first. A registrar that says no is the caller's problem to hear
136
+ // about now, not a row that claims a name the world cannot resolve.
137
+ const host = this.host(handle, label);
138
+ try {
139
+ if (a !== "")
140
+ await this.dns.set(host, "A", a, ttl);
141
+ else if (existing !== null && existing.a !== "")
142
+ await this.dns.remove(host, "A");
143
+ if (aaaa !== "")
144
+ await this.dns.set(host, "AAAA", aaaa, ttl);
145
+ else if (existing !== null && existing.aaaa !== "")
146
+ await this.dns.remove(host, "AAAA");
147
+ }
148
+ catch (error) {
149
+ throw new NameError(502, `the zone did not take that record: ${error.message}`);
150
+ }
151
+ const { rows } = await this.db.query(`INSERT INTO ${TABLE} (account_id, handle, label, a, aaaa, ttl)
152
+ VALUES ($1, $2, $3, $4, $5, $6)
153
+ ON CONFLICT (handle, label) DO UPDATE
154
+ SET a = EXCLUDED.a, aaaa = EXCLUDED.aaaa, ttl = EXCLUDED.ttl, updated_at = now()
155
+ RETURNING account_id, handle, label, a, aaaa, ttl, created_at, updated_at`, [accountId, handle, label, a, aaaa, ttl]);
156
+ const row = rows[0];
157
+ return this.record(handle, row ?? {
158
+ account_id: accountId, handle, label, a, aaaa, ttl,
159
+ created_at: existing?.created_at ?? new Date(), updated_at: new Date(),
160
+ });
161
+ }
162
+ /** Take a name away. False when it is not this account's to take. */
163
+ async remove(accountId, handle, wanted) {
164
+ const label = validLabel(wanted);
165
+ if (label === "")
166
+ return false;
167
+ await this.ensure();
168
+ const existing = await this.find(handle, label);
169
+ if (existing === null || existing.account_id !== accountId)
170
+ return false;
171
+ const host = this.host(handle, label);
172
+ try {
173
+ await this.dns.remove(host, "A");
174
+ await this.dns.remove(host, "AAAA");
175
+ }
176
+ catch (error) {
177
+ throw new NameError(502, `the zone did not let go of that record: ${error.message}`);
178
+ }
179
+ await this.db.query(`DELETE FROM ${TABLE} WHERE account_id = $1 AND handle = $2 AND label = $3`, [
180
+ accountId, handle, label,
181
+ ]);
182
+ return true;
183
+ }
184
+ }
@@ -0,0 +1,57 @@
1
+ export interface Named {
2
+ host: string;
3
+ a: string;
4
+ aaaa: string;
5
+ }
6
+ export interface CertFiles {
7
+ cert: string;
8
+ key: string;
9
+ expiresAt: number;
10
+ /** The name on the certificate, e.g. `*.chovy.nixamp.com`. */
11
+ host: string;
12
+ }
13
+ /**
14
+ * Claim, or refresh, this machine's name.
15
+ *
16
+ * "auto" for both families: the site records whichever addresses this request
17
+ * arrived from, which is the only honest answer to "what is my public
18
+ * address" from behind a router. Null when refused or unreachable, and the
19
+ * reason is said rather than thrown: a server without a name still serves.
20
+ */
21
+ export declare function claimName(site: string, token: string, label: string, say: (line: string) => void, fetcher?: typeof fetch): Promise<Named | null>;
22
+ /**
23
+ * The handle's certificate.
24
+ *
25
+ * The first one is issued while we wait: a wildcard by DNS challenge takes a
26
+ * couple of minutes, which is said once so the pause reads as work rather
27
+ * than a hang. After that it is a cached answer on the site's side.
28
+ */
29
+ export declare function fetchCert(site: string, token: string, opts: {
30
+ waitMs?: number;
31
+ everyMs?: number;
32
+ sleep?: (ms: number) => Promise<void>;
33
+ fetcher?: typeof fetch;
34
+ }, say: (line: string) => void): Promise<CertFiles | null>;
35
+ /**
36
+ * Keep a certificate beside the keys. Private, because the key is what lets
37
+ * anybody be this server.
38
+ */
39
+ export declare function writeCertFiles(stateDir: string, files: CertFiles): {
40
+ cert: string;
41
+ key: string;
42
+ };
43
+ /**
44
+ * The certificate from last time, if it is still good for more than a day.
45
+ * Anything closer to expiry is treated as absent so the next start fetches
46
+ * a fresh one rather than serving one that lapses overnight.
47
+ */
48
+ export declare function readCertFiles(stateDir: string, host: string): {
49
+ cert: string;
50
+ key: string;
51
+ expiresAt: number;
52
+ } | null;
53
+ /**
54
+ * What this machine calls itself in DNS: the name it was given, else the
55
+ * first label of its hostname, made safe for a subdomain.
56
+ */
57
+ export declare function labelFor(name: string, hostname: string): string;
package/dist/naming.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A name and a certificate for this machine, from nixamp.com.
3
+ *
4
+ * The DNS keys never leave nixamp.com: a server signed in to an account asks
5
+ * for `<label>.<handle>.nixamp.com` and the site makes the record for the
6
+ * address the request came from, A and AAAA both. The certificate is one
7
+ * wildcard per handle, issued and renewed by the site, handed to the account's
8
+ * own servers over the authenticated API and kept on disk here so a restart
9
+ * serves https at once rather than after a round trip.
10
+ */
11
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ /** A certificate this close to expiry is not worth serving: fetch a fresh one. */
14
+ const TOO_CLOSE_MS = 24 * 60 * 60 * 1000;
15
+ function bearer(token) {
16
+ return { authorization: `Bearer ${token}`, "content-type": "application/json" };
17
+ }
18
+ /**
19
+ * Claim, or refresh, this machine's name.
20
+ *
21
+ * "auto" for both families: the site records whichever addresses this request
22
+ * arrived from, which is the only honest answer to "what is my public
23
+ * address" from behind a router. Null when refused or unreachable, and the
24
+ * reason is said rather than thrown: a server without a name still serves.
25
+ */
26
+ export async function claimName(site, token, label, say, fetcher = fetch) {
27
+ try {
28
+ const answer = await fetcher(`${site}/api/v1/dns/${encodeURIComponent(label)}`, {
29
+ method: "PUT",
30
+ headers: bearer(token),
31
+ body: JSON.stringify({ a: "auto", aaaa: "auto" }),
32
+ });
33
+ const body = (await answer.json().catch(() => ({})));
34
+ if (!answer.ok || !body.name?.host) {
35
+ say(`nixamp: ${site} would not name this machine: ${body.error ?? `answered ${answer.status}`}`);
36
+ return null;
37
+ }
38
+ return { host: body.name.host, a: body.name.a ?? "", aaaa: body.name.aaaa ?? "" };
39
+ }
40
+ catch (error) {
41
+ say(`nixamp: could not reach ${site} to claim a name: ${error.message}`);
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * The handle's certificate.
47
+ *
48
+ * The first one is issued while we wait: a wildcard by DNS challenge takes a
49
+ * couple of minutes, which is said once so the pause reads as work rather
50
+ * than a hang. After that it is a cached answer on the site's side.
51
+ */
52
+ export async function fetchCert(site, token, opts, say) {
53
+ const waitMs = opts.waitMs ?? 240_000;
54
+ const everyMs = opts.everyMs ?? 10_000;
55
+ const sleep = opts.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
56
+ const fetcher = opts.fetcher ?? fetch;
57
+ let waited = 0;
58
+ let announced = false;
59
+ for (;;) {
60
+ let answer;
61
+ try {
62
+ answer = await fetcher(`${site}/api/v1/certs`, { headers: bearer(token) });
63
+ }
64
+ catch (error) {
65
+ say(`nixamp: could not reach ${site} for a certificate: ${error.message}`);
66
+ return null;
67
+ }
68
+ const body = (await answer.json().catch(() => ({})));
69
+ if (answer.ok && body.status === "ready" && body.cert && body.key && body.host) {
70
+ return { cert: body.cert, key: body.key, expiresAt: body.expiresAt ?? 0, host: body.host };
71
+ }
72
+ if (answer.status === 202 || body.status === "issuing") {
73
+ if (!announced) {
74
+ announced = true;
75
+ const host = body.host ?? "your handle";
76
+ say(`Getting a certificate for ${host}… the first one takes a couple of minutes.`);
77
+ }
78
+ if (waited >= waitMs) {
79
+ say(`nixamp: ${site} is still issuing the certificate; serving http until it is ready.`);
80
+ return null;
81
+ }
82
+ await sleep(everyMs);
83
+ waited += everyMs;
84
+ continue;
85
+ }
86
+ say(`nixamp: ${site} could not issue a certificate: ${body.error ?? `answered ${answer.status}`}`);
87
+ return null;
88
+ }
89
+ }
90
+ /** `*.chovy.nixamp.com` becomes `chovy.nixamp.com`, which is a filename. */
91
+ function fileStem(host) {
92
+ return host.replace(/^\*\./, "").replace(/[^A-Za-z0-9.-]/g, "_");
93
+ }
94
+ /**
95
+ * Keep a certificate beside the keys. Private, because the key is what lets
96
+ * anybody be this server.
97
+ */
98
+ export function writeCertFiles(stateDir, files) {
99
+ const dir = join(stateDir, "tls");
100
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
101
+ chmodSync(dir, 0o700);
102
+ const stem = join(dir, fileStem(files.host));
103
+ const cert = `${stem}.cert.pem`;
104
+ const key = `${stem}.key.pem`;
105
+ writeFileSync(cert, files.cert, { mode: 0o600 });
106
+ writeFileSync(key, files.key, { mode: 0o600 });
107
+ chmodSync(cert, 0o600);
108
+ chmodSync(key, 0o600);
109
+ writeFileSync(`${stem}.json`, JSON.stringify({ host: files.host, expiresAt: files.expiresAt }), { mode: 0o600 });
110
+ return { cert, key };
111
+ }
112
+ /**
113
+ * The certificate from last time, if it is still good for more than a day.
114
+ * Anything closer to expiry is treated as absent so the next start fetches
115
+ * a fresh one rather than serving one that lapses overnight.
116
+ */
117
+ export function readCertFiles(stateDir, host) {
118
+ const stem = join(stateDir, "tls", fileStem(host));
119
+ try {
120
+ const meta = JSON.parse(readFileSync(`${stem}.json`, "utf8"));
121
+ const expiresAt = typeof meta.expiresAt === "number" ? meta.expiresAt : 0;
122
+ if (expiresAt - Date.now() < TOO_CLOSE_MS)
123
+ return null;
124
+ const cert = readFileSync(`${stem}.cert.pem`, "utf8");
125
+ const key = readFileSync(`${stem}.key.pem`, "utf8");
126
+ if (!cert || !key)
127
+ return null;
128
+ return { cert, key, expiresAt };
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * What this machine calls itself in DNS: the name it was given, else the
136
+ * first label of its hostname, made safe for a subdomain.
137
+ */
138
+ export function labelFor(name, hostname) {
139
+ for (const candidate of [name, hostname.split(".")[0] ?? ""]) {
140
+ const label = candidate
141
+ .toLowerCase()
142
+ .replace(/[^a-z0-9-]+/g, "-")
143
+ .replace(/^-+|-+$/g, "")
144
+ .slice(0, 30)
145
+ .replace(/-+$/g, "");
146
+ if (label.length >= 2)
147
+ return label;
148
+ }
149
+ return "server";
150
+ }
package/dist/owner.js CHANGED
@@ -102,5 +102,9 @@ export function needsAdmin(path, method = "GET") {
102
102
  // Listening to a channel is not: that is what the share link is for.
103
103
  if (path.startsWith("/api/channels/") && method !== "GET")
104
104
  return true;
105
+ // Adding, refreshing or removing a catalog is administering; browsing one,
106
+ // and picking something in it to play, is what the link is for.
107
+ if (path.startsWith("/api/catalogs") && method !== "GET" && !path.endsWith("/play"))
108
+ return true;
105
109
  return false;
106
110
  }
package/dist/publish.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface PublishTarget {
21
21
  */
22
22
  tracks: () => number;
23
23
  nowPlaying: () => string;
24
+ /** The admin share link, kept by the directory for the owner alone. */
25
+ admin?: string;
24
26
  /** Whether the player is actually running, so a stopped server is not listed as live. */
25
27
  playing?: () => boolean;
26
28
  /** The live channels on this server, by name, for the listing to show. */
package/dist/publish.js CHANGED
@@ -58,6 +58,9 @@ export class Publisher {
58
58
  name: this.target.name,
59
59
  url: this.target.url,
60
60
  ...(this.target.audio ? { audio: this.target.audio } : {}),
61
+ // The link that drives this server. The directory keeps it for the
62
+ // account that owns the listing and shows it to nobody else.
63
+ ...(this.target.admin ? { admin: this.target.admin } : {}),
61
64
  tracks: this.target.tracks(),
62
65
  nowPlaying: this.target.nowPlaying(),
63
66
  ...(this.target.playing ? { playing: this.target.playing() } : {}),
package/dist/server.d.ts CHANGED
@@ -13,6 +13,10 @@ import { Directory } from "./directory.ts";
13
13
  import { PartyLine } from "./partyline.ts";
14
14
  import { Follows } from "./follows.ts";
15
15
  import { Favorites } from "./favorites.ts";
16
+ import { Catalogs } from "./catalogs.ts";
17
+ import { Names } from "./names.ts";
18
+ import { Certs } from "./certs.ts";
19
+ import { type Throttle } from "@profullstack/throttle";
16
20
  import { type Tools, type Track } from "./audio.ts";
17
21
  import { type Command, type RemoteTrack, type Snapshot } from "./protocol.ts";
18
22
  export declare const SERVE_BAND_COUNT = 24;
@@ -34,6 +38,8 @@ export interface ServeOptions {
34
38
  newKey: boolean;
35
39
  /** Start without the noise it makes when it wakes up. */
36
40
  noJingle: boolean;
41
+ /** Do not ask nixamp.com for a name and a certificate, even when signed in. */
42
+ noName: boolean;
37
43
  /**
38
44
  * Ask the local firewall to let the port through, and put it back on the way
39
45
  * out. Off by default because it changes the machine, not just this process.
@@ -337,6 +343,23 @@ export declare class PlayerEngine implements Engine {
337
343
  * looking at what is on wants "that album from the web", not every track in
338
344
  * it. An entry names where to start, so clicking it plays.
339
345
  */
346
+ /**
347
+ * A fetch-shaped Request for the throttle, built from the Node one.
348
+ *
349
+ * @profullstack/throttle is written against the web Request so it runs at an
350
+ * edge; this server is Node's http. Only what the throttle reads is carried
351
+ * across: method, URL and headers. The body is not, because metering is
352
+ * decided before anybody reads it.
353
+ */
354
+ export declare function requestFor(request: IncomingMessage, origin?: string): Request;
355
+ /** Write a refusal the throttle produced back through the Node response. */
356
+ export declare function answerWith(response: ServerResponse, refused: Response): Promise<void>;
357
+ /**
358
+ * How many channels a server will start on demand at once. Each is an ffmpeg,
359
+ * and a catalog has thousands of entries; this is what keeps a room full of
360
+ * curious people from becoming a room full of decoders.
361
+ */
362
+ export declare const MAX_ON_DEMAND = 4;
340
363
  /**
341
364
  * Probe a source and start carrying it as a channel of its own.
342
365
  *
@@ -414,6 +437,8 @@ export interface HandlerOptions {
414
437
  channels?: Channels;
415
438
  /** Write down the channels this server pulls, so a restart puts them back. */
416
439
  rememberChannels?: (list: RememberedChannel[]) => void;
440
+ /** The m3u catalogs this server keeps, browsable by group. */
441
+ catalogs?: Catalogs;
417
442
  /** Live audio going out to RTMP. */
418
443
  broadcaster?: Broadcaster;
419
444
  /** Where a broadcast should send, and what it should look like. */
@@ -511,6 +536,18 @@ export interface HandlerOptions {
511
536
  follows?: Follows;
512
537
  /** The servers an account hearted. nixamp.com only, like follows. */
513
538
  favorites?: Favorites;
539
+ /** Names under `<handle>.<zone>` for an account's servers. nixamp.com only. */
540
+ names?: Names;
541
+ /** One wildcard certificate per handle, issued and renewed here. nixamp.com only. */
542
+ certs?: Certs;
543
+ /** The zone the names live in, e.g. "nixamp.com". */
544
+ dnsZone?: string;
545
+ /**
546
+ * The rate limit over everything, from @profullstack/throttle. Fetch-shaped,
547
+ * so the handler builds a Request from the Node one and writes back the
548
+ * Response it is refused with.
549
+ */
550
+ throttle?: Throttle;
514
551
  /** The VAPID public key a browser needs before it can subscribe. */
515
552
  vapidPublicKey?: string;
516
553
  }