nixamp 0.78.6 → 0.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/backtoschool/dist/assets/{ar-BBh9pRWn.js → ar-BYDVUbTZ.js} +1 -1
- package/backtoschool/dist/assets/{cs-DYDz-hZb.js → cs-DsCx7BAy.js} +1 -1
- package/backtoschool/dist/assets/{da-DL0tbrTr.js → da-5tLM7Wal.js} +1 -1
- package/backtoschool/dist/assets/{de-POU_MZN1.js → de-2Q8k6IKm.js} +1 -1
- package/backtoschool/dist/assets/{es-VLEhXfMp.js → es-CVwFkQ0E.js} +1 -1
- package/backtoschool/dist/assets/{fi-B__GWwbQ.js → fi-D7KuvUvL.js} +1 -1
- package/backtoschool/dist/assets/{fr-DDtDAMct.js → fr-CkWg-nDv.js} +1 -1
- package/backtoschool/dist/assets/{hi-C6MybeLD.js → hi-DayNvhO4.js} +1 -1
- package/backtoschool/dist/assets/{hls-OR5FY6YP-CL3tmveA.js → hls-OR5FY6YP-D2hwLr-P.js} +1 -1
- package/backtoschool/dist/assets/{hu-CgiQxTUY.js → hu-BqS46UyM.js} +1 -1
- package/backtoschool/dist/assets/{id-pzntv5iA.js → id-DTiKw_BS.js} +1 -1
- package/backtoschool/dist/assets/{index-DeCDKrPz.js → index-EM-0SwBp.js} +2 -2
- package/backtoschool/dist/assets/{it-dKw8vCrE.js → it-B60apeGa.js} +1 -1
- package/backtoschool/dist/assets/{mpegts-B6-Xba87.js → mpegts-C4v5WI5W.js} +1 -1
- package/backtoschool/dist/assets/{mpegts-Y2FARLFE-MgjbiVoD.js → mpegts-Y2FARLFE-D4OTyGyp.js} +1 -1
- package/backtoschool/dist/assets/{nl-DZ0o7967.js → nl-DctlIuWS.js} +1 -1
- package/backtoschool/dist/assets/{ru-s6C6TpM6.js → ru-CO8g0gfi.js} +1 -1
- package/backtoschool/dist/assets/{sv-BHj0BZlD.js → sv-CoFMz-Qg.js} +1 -1
- package/backtoschool/dist/assets/{uk-BH7Y4zb1.js → uk-C3_GprSP.js} +1 -1
- package/backtoschool/dist/assets/{vi-nf4HfTQk.js → vi-B0RDG4iN.js} +1 -1
- package/backtoschool/dist/assets/{zh-CT2P8jxm.js → zh--yO6izcR.js} +1 -1
- package/backtoschool/dist/index.html +1 -1
- package/dist/ai-keys.d.ts +1 -1
- package/dist/ai-keys.js +1 -1
- package/dist/ai-label.d.ts +38 -0
- package/dist/ai-label.js +237 -5
- package/dist/ai-text.d.ts +42 -1
- package/dist/ai-text.js +69 -7
- package/dist/event-writer.d.ts +10 -1
- package/dist/event-writer.js +27 -8
- package/dist/label-routes.js +9 -0
- package/dist/library-mcp.d.ts +6 -6
- package/dist/music-hub.d.ts +11 -0
- package/dist/music-hub.js +33 -2
- package/dist/nixamp-link-api.d.ts +5 -1
- package/dist/nixamp-link-api.js +12 -2
- package/dist/nixamp-link-types.d.ts +2 -0
- package/dist/nixamp-link.d.ts +22 -9
- package/dist/nixamp-link.js +75 -58
- package/dist/oauth.d.ts +1 -0
- package/dist/oauth.js +8 -1
- package/dist/server.js +18 -9
- package/dist/station-cli.js +24 -0
- package/dist/station-mcp.d.ts +6 -6
- package/dist/station-usage.d.ts +1 -1
- package/dist/station-usage.js +4 -1
- package/dist/universal-footer.d.ts +2 -0
- package/dist/universal-footer.js +39 -0
- package/package.json +7 -5
- package/src/ai-keys.ts +1 -1
- package/src/ai-label.ts +240 -5
- package/src/ai-text.ts +84 -8
- package/src/event-writer.ts +36 -11
- package/src/label-routes.ts +9 -0
- package/src/music-hub.ts +38 -2
- package/src/nixamp-link-api.ts +15 -3
- package/src/nixamp-link-types.ts +2 -0
- package/src/nixamp-link.ts +77 -61
- package/src/oauth.ts +9 -1
- package/src/server.ts +18 -9
- package/src/station-cli.ts +23 -0
- package/src/station-usage.ts +4 -1
- package/src/universal-footer.ts +41 -0
- package/web/dist/assets/{ar-7tVGBWgq.js → ar-C5qVRZh9.js} +1 -1
- package/web/dist/assets/{chunk-55GBNX64-BDcCEJUP.js → chunk-NW3WDGEQ-CnyjTRUh.js} +1 -1
- package/web/dist/assets/{cs-hTmkOWFS.js → cs-fajZsJrs.js} +1 -1
- package/web/dist/assets/{da-218F21ho.js → da-fycV1k1N.js} +1 -1
- package/web/dist/assets/{de-DesLnw7U.js → de-QUzTQ_9X.js} +1 -1
- package/web/dist/assets/{es-Bc9fW4qY.js → es-CQu2leAJ.js} +1 -1
- package/web/dist/assets/{fi-Df_rWRQA.js → fi-AgKEsukC.js} +1 -1
- package/web/dist/assets/{fr-D8xzxUiQ.js → fr-CRbHnW4y.js} +1 -1
- package/web/dist/assets/{hi-Ddh5AA1z.js → hi-BuU7xy9L.js} +1 -1
- package/web/dist/assets/{hls-OR5FY6YP-_WOdANsC.js → hls-OR5FY6YP-BxZwqFB9.js} +1 -1
- package/web/dist/assets/{hu-ChrJNlJD.js → hu-C40Z-HcJ.js} +1 -1
- package/web/dist/assets/{id-dNB_py4J.js → id-2b7626UL.js} +1 -1
- package/web/dist/assets/{it-DyHyXE3N.js → it-7sr3nJuS.js} +1 -1
- package/web/dist/assets/main-zuI3sOLC.js +33 -0
- package/web/dist/assets/{mpegts-CL9RC5Ls.js → mpegts-DPRtdBXL.js} +1 -1
- package/web/dist/assets/{mpegts-Y2FARLFE-C91SD8Pl.js → mpegts-Y2FARLFE-BY7parIQ.js} +1 -1
- package/web/dist/assets/{nl-DlwRNqNC.js → nl-BdDGgfzj.js} +1 -1
- package/web/dist/assets/{radio-BnQ78a-O.js → radio-CeRhNjpU.js} +1 -1
- package/web/dist/assets/{ru--N-8v6QS.js → ru-BMGI8Oa4.js} +1 -1
- package/web/dist/assets/{station-C_6L4CDU.js → station-D5tW9jq4.js} +1 -1
- package/web/dist/assets/station-D6KfgFXf.css +1 -0
- package/web/dist/assets/{sv-DbR0DO05.js → sv-BXASdl7c.js} +1 -1
- package/web/dist/assets/{uk-Bi3fEoz1.js → uk-urnhuqtG.js} +1 -1
- package/web/dist/assets/{vi-DqrcyLEz.js → vi-BhLPAKuD.js} +1 -1
- package/web/dist/assets/{vod-CbSC5NFA.js → vod-Dt73bt8P.js} +1 -1
- package/web/dist/assets/{watch-D-ZRxfJ_.js → watch-GxgHBQcI.js} +1 -1
- package/web/dist/assets/{zh-_4uBqFzc.js → zh-BaOCIB9u.js} +1 -1
- package/web/dist/index.html +58 -12
- package/web/dist/music.html +1 -1
- package/web/dist/radio.html +5 -5
- package/web/dist/station.html +4 -4
- package/web/dist/sw.js +30 -30
- package/web/dist/vod.html +4 -4
- package/web/dist/watch.html +4 -4
- package/dist/pit-footer.d.ts +0 -3
- package/dist/pit-footer.js +0 -40
- package/src/pit-footer.ts +0 -40
- package/web/dist/assets/main-D9XA9svM.js +0 -26
- package/web/dist/assets/station-CxJvFwzV.css +0 -1
- /package/web/dist/assets/{chunk-55GBNX64-DR1XNWCJ.css → chunk-NW3WDGEQ-DR1XNWCJ.css} +0 -0
package/dist/library-mcp.d.ts
CHANGED
|
@@ -9,10 +9,8 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
9
9
|
name: string;
|
|
10
10
|
description: string;
|
|
11
11
|
inputSchema: {
|
|
12
|
-
required?: undefined;
|
|
13
12
|
type: string;
|
|
14
13
|
properties: {
|
|
15
|
-
limit?: undefined;
|
|
16
14
|
server: {
|
|
17
15
|
type: "string";
|
|
18
16
|
description: string;
|
|
@@ -25,6 +23,7 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
25
23
|
offset?: undefined;
|
|
26
24
|
kind?: undefined;
|
|
27
25
|
q?: undefined;
|
|
26
|
+
limit?: undefined;
|
|
28
27
|
name?: undefined;
|
|
29
28
|
id?: undefined;
|
|
30
29
|
items?: undefined;
|
|
@@ -32,12 +31,12 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
32
31
|
source?: undefined;
|
|
33
32
|
force?: undefined;
|
|
34
33
|
};
|
|
34
|
+
required?: undefined;
|
|
35
35
|
};
|
|
36
36
|
} | {
|
|
37
37
|
name: string;
|
|
38
38
|
description: string;
|
|
39
39
|
inputSchema: {
|
|
40
|
-
required?: undefined;
|
|
41
40
|
type: string;
|
|
42
41
|
properties: {
|
|
43
42
|
server: {
|
|
@@ -77,6 +76,7 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
77
76
|
source?: undefined;
|
|
78
77
|
force?: undefined;
|
|
79
78
|
};
|
|
79
|
+
required?: undefined;
|
|
80
80
|
};
|
|
81
81
|
} | {
|
|
82
82
|
name: string;
|
|
@@ -119,7 +119,6 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
119
119
|
inputSchema: {
|
|
120
120
|
type: string;
|
|
121
121
|
properties: {
|
|
122
|
-
limit?: undefined;
|
|
123
122
|
artist?: undefined;
|
|
124
123
|
album?: undefined;
|
|
125
124
|
genre?: undefined;
|
|
@@ -128,6 +127,7 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
128
127
|
offset?: undefined;
|
|
129
128
|
kind?: undefined;
|
|
130
129
|
q?: undefined;
|
|
130
|
+
limit?: undefined;
|
|
131
131
|
server: {
|
|
132
132
|
type: "string";
|
|
133
133
|
description: string;
|
|
@@ -154,7 +154,6 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
154
154
|
inputSchema: {
|
|
155
155
|
type: string;
|
|
156
156
|
properties: {
|
|
157
|
-
limit?: undefined;
|
|
158
157
|
artist?: undefined;
|
|
159
158
|
album?: undefined;
|
|
160
159
|
genre?: undefined;
|
|
@@ -163,6 +162,7 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
163
162
|
offset?: undefined;
|
|
164
163
|
kind?: undefined;
|
|
165
164
|
q?: undefined;
|
|
165
|
+
limit?: undefined;
|
|
166
166
|
name?: undefined;
|
|
167
167
|
server: {
|
|
168
168
|
type: "string";
|
|
@@ -192,7 +192,6 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
192
192
|
required?: undefined;
|
|
193
193
|
type: string;
|
|
194
194
|
properties: {
|
|
195
|
-
limit?: undefined;
|
|
196
195
|
artist?: undefined;
|
|
197
196
|
album?: undefined;
|
|
198
197
|
genre?: undefined;
|
|
@@ -201,6 +200,7 @@ export declare const LIBRARY_TOOLS: ({
|
|
|
201
200
|
offset?: undefined;
|
|
202
201
|
kind?: undefined;
|
|
203
202
|
q?: undefined;
|
|
203
|
+
limit?: undefined;
|
|
204
204
|
name?: undefined;
|
|
205
205
|
id?: undefined;
|
|
206
206
|
items?: undefined;
|
package/dist/music-hub.d.ts
CHANGED
|
@@ -61,7 +61,14 @@ export interface MusicHubOptions {
|
|
|
61
61
|
peers: () => string[];
|
|
62
62
|
fetcher?: typeof fetch;
|
|
63
63
|
now?: () => number;
|
|
64
|
+
/**
|
|
65
|
+
* Label servers that moved, old https origin to new (a box gone, its label adopted
|
|
66
|
+
* on another): /music addresses under the old origin's id go to the new one's.
|
|
67
|
+
*/
|
|
68
|
+
moved?: Record<string, string>;
|
|
64
69
|
}
|
|
70
|
+
/** NIXAMP_MUSIC_MOVED: "https://old:4321>https://new:4321", several separated by commas. */
|
|
71
|
+
export declare function parseMoved(text: string | undefined): Record<string, string>;
|
|
65
72
|
export declare class MusicHub {
|
|
66
73
|
private readonly options;
|
|
67
74
|
private cache;
|
|
@@ -70,6 +77,10 @@ export declare class MusicHub {
|
|
|
70
77
|
private now;
|
|
71
78
|
/** The https origin behind an opaque id, among the servers known now. */
|
|
72
79
|
resolve(id: string): string;
|
|
80
|
+
/** The servers to gather, less any that moved (the same label would be listed twice). */
|
|
81
|
+
private peers;
|
|
82
|
+
/** The id a moved server goes by now, or "" when this id never moved. */
|
|
83
|
+
movedTo(id: string): string;
|
|
73
84
|
/** The catalogue, gathered at most once a minute (a slow or gone server is left out, not waited on). */
|
|
74
85
|
catalogue(): Promise<HubCatalogue>;
|
|
75
86
|
private read;
|
package/dist/music-hub.js
CHANGED
|
@@ -23,6 +23,16 @@ export function artistFollowId(name) {
|
|
|
23
23
|
const slug = name.split(/\s*;\s*/)[0].normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "artist";
|
|
24
24
|
return `artist:${slug}`;
|
|
25
25
|
}
|
|
26
|
+
/** NIXAMP_MUSIC_MOVED: "https://old:4321>https://new:4321", several separated by commas. */
|
|
27
|
+
export function parseMoved(text) {
|
|
28
|
+
const moved = {};
|
|
29
|
+
for (const pair of (text ?? "").split(",")) {
|
|
30
|
+
const [from, to] = pair.split(">").map((one) => one.trim().replace(/\/+$/, ""));
|
|
31
|
+
if (from && to && /^https?:\/\//.test(from) && /^https?:\/\//.test(to) && from !== to)
|
|
32
|
+
moved[from] = to;
|
|
33
|
+
}
|
|
34
|
+
return moved;
|
|
35
|
+
}
|
|
26
36
|
export class MusicHub {
|
|
27
37
|
options;
|
|
28
38
|
cache = null;
|
|
@@ -38,10 +48,22 @@ export class MusicHub {
|
|
|
38
48
|
if (!/^[a-f0-9]{10}$/.test(id))
|
|
39
49
|
return "";
|
|
40
50
|
if (!this.bases.has(id))
|
|
41
|
-
for (const base of this.
|
|
51
|
+
for (const base of this.peers())
|
|
42
52
|
this.bases.set(serverId(base), base);
|
|
43
53
|
return this.bases.get(id) ?? "";
|
|
44
54
|
}
|
|
55
|
+
/** The servers to gather, less any that moved (the same label would be listed twice). */
|
|
56
|
+
peers() {
|
|
57
|
+
const moved = this.options.moved ?? {};
|
|
58
|
+
return this.options.peers().filter((base) => !moved[base.replace(/\/+$/, "")]);
|
|
59
|
+
}
|
|
60
|
+
/** The id a moved server goes by now, or "" when this id never moved. */
|
|
61
|
+
movedTo(id) {
|
|
62
|
+
for (const [from, to] of Object.entries(this.options.moved ?? {}))
|
|
63
|
+
if (serverId(from) === id)
|
|
64
|
+
return serverId(to);
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
45
67
|
/** The catalogue, gathered at most once a minute (a slow or gone server is left out, not waited on). */
|
|
46
68
|
catalogue() {
|
|
47
69
|
if (this.cache && this.now() - this.cache.at < 60_000)
|
|
@@ -64,7 +86,7 @@ export class MusicHub {
|
|
|
64
86
|
}
|
|
65
87
|
}
|
|
66
88
|
async gather() {
|
|
67
|
-
const peers = [...new Set(this.
|
|
89
|
+
const peers = [...new Set(this.peers())];
|
|
68
90
|
const holders = new Set();
|
|
69
91
|
const genres = new Map();
|
|
70
92
|
const stations = [];
|
|
@@ -181,6 +203,15 @@ export async function handleMusicRoutes(request, response, path, ctx) {
|
|
|
181
203
|
ctx.json(response, 405, { error: "not like that" });
|
|
182
204
|
return true;
|
|
183
205
|
}
|
|
206
|
+
// A label that moved to another server: its old addresses (shared links, search results) go to the new ones.
|
|
207
|
+
const old = /^(\/music\/(?:artist|album|member)\/|\/api\/v1\/music\/(?:file|member)\/)([a-f0-9]{10})(\/.*)$/.exec(path);
|
|
208
|
+
const now = old ? ctx.hub.movedTo?.(old[2]) ?? "" : "";
|
|
209
|
+
if (old && now) {
|
|
210
|
+
const query = (request.url ?? "").includes("?") ? (request.url ?? "").slice((request.url ?? "").indexOf("?")) : "";
|
|
211
|
+
response.writeHead(301, { location: `${old[1]}${now}${old[3]}${query}`, "cache-control": "public, max-age=86400" });
|
|
212
|
+
response.end();
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
184
215
|
if (path === "/api/v1/music") {
|
|
185
216
|
ctx.json(response, 200, await ctx.hub.catalogue());
|
|
186
217
|
return true;
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* All five want the site's own session: the person, signed in here. What
|
|
12
12
|
* they hold on nixamp's side is a client token, and it never leaves the
|
|
13
|
-
* server -- the page only ever learns a handle and a list of streams.
|
|
13
|
+
* server -- the page only ever learns a handle and a list of streams. The
|
|
14
|
+
* token is kept in memory only, for its hour; never in the database.
|
|
14
15
|
*/
|
|
15
16
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
17
|
import { type Accounts } from "./accounts.ts";
|
|
@@ -32,6 +33,9 @@ export interface NixampLinkApiOptions {
|
|
|
32
33
|
fetcher?: typeof fetch;
|
|
33
34
|
}
|
|
34
35
|
export declare function nixampLinkPath(path: string): boolean;
|
|
36
|
+
/** nixamp.com refused the token held in memory. */
|
|
37
|
+
export declare class StaleToken extends Error {
|
|
38
|
+
}
|
|
35
39
|
/**
|
|
36
40
|
* Every server the nixamp account remembers, asked what it is doing.
|
|
37
41
|
*
|
package/dist/nixamp-link-api.js
CHANGED
|
@@ -39,6 +39,9 @@ function landing(home, outcome, reason = "") {
|
|
|
39
39
|
url.searchParams.set("reason", reason.slice(0, 200));
|
|
40
40
|
return `${url.pathname}${url.search}${url.hash || "#settings"}`;
|
|
41
41
|
}
|
|
42
|
+
/** nixamp.com refused the token held in memory. */
|
|
43
|
+
export class StaleToken extends Error {
|
|
44
|
+
}
|
|
42
45
|
/**
|
|
43
46
|
* Every server the nixamp account remembers, asked what it is doing.
|
|
44
47
|
*
|
|
@@ -54,8 +57,10 @@ export async function streamsFor(token, issuer, pageSite, fetcher) {
|
|
|
54
57
|
headers: { authorization: `Bearer ${token}`, accept: "application/json" },
|
|
55
58
|
signal: AbortSignal.timeout(8000),
|
|
56
59
|
});
|
|
60
|
+
if (answer.status === 401)
|
|
61
|
+
throw new StaleToken("nixamp.com no longer accepts this connection; refresh from nixamp");
|
|
57
62
|
if (!answer.ok)
|
|
58
|
-
throw new Error(
|
|
63
|
+
throw new Error(`nixamp.com answered ${answer.status}`);
|
|
59
64
|
const body = (await answer.json().catch(() => ({})));
|
|
60
65
|
servers = Array.isArray(body.servers) ? body.servers : [];
|
|
61
66
|
const page = pageSite.replace(/\/+$/, "");
|
|
@@ -177,13 +182,18 @@ export async function handleNixampLinkApi(request, response, url, options) {
|
|
|
177
182
|
}
|
|
178
183
|
const token = await options.links.accessToken(account.id);
|
|
179
184
|
if (token === "") {
|
|
180
|
-
|
|
185
|
+
const { connected } = await options.links.of(account.id);
|
|
186
|
+
json(response, 409, connected
|
|
187
|
+
? { error: "refresh from nixamp to see your streams", connected: true, refresh: true }
|
|
188
|
+
: { error: "connect your nixamp account first", connected: false });
|
|
181
189
|
return true;
|
|
182
190
|
}
|
|
183
191
|
try {
|
|
184
192
|
json(response, 200, { servers: await streamsFor(token, options.issuer, options.issuer, options.fetcher ?? fetch) });
|
|
185
193
|
}
|
|
186
194
|
catch (error) {
|
|
195
|
+
if (error instanceof StaleToken)
|
|
196
|
+
options.links.drop(account.id);
|
|
187
197
|
json(response, 502, { error: error.message });
|
|
188
198
|
}
|
|
189
199
|
return true;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
/** The connection as the account holder sees it. Never the tokens. */
|
|
6
6
|
export interface LinkView {
|
|
7
7
|
connected: boolean;
|
|
8
|
+
/** A token is in memory right now, so the streams can be read without asking nixamp.com again. */
|
|
9
|
+
ready: boolean;
|
|
8
10
|
handle: string;
|
|
9
11
|
nixampUserId: string;
|
|
10
12
|
scope: string;
|
package/dist/nixamp-link.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Queryable } from "./follows.ts";
|
|
2
2
|
import type { LinkView } from "./nixamp-link-types.ts";
|
|
3
3
|
export type { LinkView } from "./nixamp-link-types.ts";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What the school asks nixamp for: who you are and your servers. Not
|
|
6
|
+
* offline_access: a refresh token would have to be kept somewhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LINK_SCOPE = "profile streams";
|
|
6
9
|
export interface TokenGrant {
|
|
7
10
|
access_token: string;
|
|
8
11
|
refresh_token?: string;
|
|
@@ -32,10 +35,19 @@ export declare class NixampLinks {
|
|
|
32
35
|
/** How nixamp.com and the servers it lists are reached, for what the tokens are used on. */
|
|
33
36
|
readonly fetcher: typeof fetch;
|
|
34
37
|
private ready;
|
|
38
|
+
/** Access tokens, in memory only, until they end. Never in the database. */
|
|
39
|
+
private readonly live;
|
|
35
40
|
constructor(db: Queryable, exchange: OAuthExchange, clientId: string, now?: () => number,
|
|
36
41
|
/** How nixamp.com and the servers it lists are reached, for what the tokens are used on. */
|
|
37
42
|
fetcher?: typeof fetch);
|
|
38
43
|
private ensure;
|
|
44
|
+
/**
|
|
45
|
+
* Before 0.79 the tokens were kept in this table in plain text. Hand each
|
|
46
|
+
* refresh token back to nixamp.com (so no grant is left standing there),
|
|
47
|
+
* then drop the columns. The identity link stays; the person clicks
|
|
48
|
+
* "Refresh from nixamp" the next time they go live.
|
|
49
|
+
*/
|
|
50
|
+
private forgetLegacyTokens;
|
|
39
51
|
/**
|
|
40
52
|
* The first leg: where to send the browser, and the two secrets the
|
|
41
53
|
* callback must bring back. The state is the CSRF check, the verifier is
|
|
@@ -46,21 +58,22 @@ export declare class NixampLinks {
|
|
|
46
58
|
state: string;
|
|
47
59
|
verifier: string;
|
|
48
60
|
};
|
|
49
|
-
/** The second leg: the code becomes
|
|
61
|
+
/** The second leg: the code becomes a token, the token says whose; who is kept, the token only in memory. */
|
|
50
62
|
finish(userId: string, leg: {
|
|
51
63
|
code: string;
|
|
52
64
|
verifier: string;
|
|
53
65
|
redirectUri: string;
|
|
54
66
|
}): Promise<LinkView>;
|
|
55
|
-
private expiry;
|
|
56
67
|
of(userId: string): Promise<LinkView>;
|
|
68
|
+
private held;
|
|
57
69
|
/**
|
|
58
|
-
* A token good for a call right now,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* the row is dropped so the settings page says so too.
|
|
70
|
+
* A token good for a call right now, or "" when there is none in memory:
|
|
71
|
+
* not connected, expired, or the server restarted since. The page then
|
|
72
|
+
* offers "Refresh from nixamp".
|
|
62
73
|
*/
|
|
63
74
|
accessToken(userId: string): Promise<string>;
|
|
64
|
-
/**
|
|
75
|
+
/** nixamp.com refused the token: forget it, so the page asks again. */
|
|
76
|
+
drop(userId: string): void;
|
|
77
|
+
/** Withdraw the token on nixamp.com and forget the connection here. True when there was one. */
|
|
65
78
|
disconnect(userId: string): Promise<boolean>;
|
|
66
79
|
}
|
package/dist/nixamp-link.js
CHANGED
|
@@ -9,11 +9,17 @@
|
|
|
9
9
|
* broadcast backend a host may plug in, and plugging it in is a choice.
|
|
10
10
|
*
|
|
11
11
|
* So the school is an OAuth 2.1 client of nixamp.com, the way bittorrented
|
|
12
|
-
* is: PKCE
|
|
13
|
-
* the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* is: PKCE and a consent page on nixamp.com. What the connection buys is the
|
|
13
|
+
* list of the servers they run on nixamp and what is live on them, so "go
|
|
14
|
+
* live" is a pick from a list rather than a link pasted from a terminal.
|
|
15
|
+
*
|
|
16
|
+
* No token is kept at rest (fleet rule, 2026-10-28: nothing a user gave us
|
|
17
|
+
* may be readable by root on the box). The row here is the identity link
|
|
18
|
+
* only: which nixamp account, as which handle. The access token lives in
|
|
19
|
+
* this process's memory until it expires (an hour) and is never written to
|
|
20
|
+
* the database; no refresh token is asked for. When it is gone (expired, or
|
|
21
|
+
* the server restarted) the person, who is present to go live, clicks
|
|
22
|
+
* "Refresh from nixamp" and says yes once more on nixamp.com.
|
|
17
23
|
*
|
|
18
24
|
* The client half of OAuth is behind an interface, because the server on the
|
|
19
25
|
* other end is this same program in production and a stub in a test.
|
|
@@ -27,18 +33,18 @@ const SCHEMA = `
|
|
|
27
33
|
nixamp_user_id TEXT NOT NULL,
|
|
28
34
|
handle TEXT NOT NULL DEFAULT '',
|
|
29
35
|
scope TEXT NOT NULL DEFAULT '',
|
|
30
|
-
access_token TEXT NOT NULL DEFAULT '',
|
|
31
|
-
access_expires_at TIMESTAMPTZ,
|
|
32
|
-
refresh_token TEXT NOT NULL DEFAULT '',
|
|
33
36
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
34
37
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
35
38
|
);
|
|
36
39
|
`;
|
|
37
|
-
/**
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* What the school asks nixamp for: who you are and your servers. Not
|
|
42
|
+
* offline_access: a refresh token would have to be kept somewhere.
|
|
43
|
+
*/
|
|
44
|
+
export const LINK_SCOPE = "profile streams";
|
|
39
45
|
/** How long before an access token's end it is treated as spent. */
|
|
40
46
|
const EARLY_MS = 60_000;
|
|
41
|
-
export const NOT_CONNECTED = { connected: false, handle: "", nixampUserId: "", scope: "", since: null };
|
|
47
|
+
export const NOT_CONNECTED = { connected: false, ready: false, handle: "", nixampUserId: "", scope: "", since: null };
|
|
42
48
|
/** The real thing: nixamp.com over HTTP. */
|
|
43
49
|
export function nixampExchange(issuer, clientId, fetcher = fetch) {
|
|
44
50
|
const base = issuer.replace(/\/+$/, "");
|
|
@@ -109,11 +115,12 @@ function asTime(value) {
|
|
|
109
115
|
}
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
112
|
-
function view(row) {
|
|
118
|
+
function view(row, ready) {
|
|
113
119
|
if (!row)
|
|
114
120
|
return { ...NOT_CONNECTED };
|
|
115
121
|
return {
|
|
116
122
|
connected: true,
|
|
123
|
+
ready,
|
|
117
124
|
handle: String(row["handle"] ?? ""),
|
|
118
125
|
nixampUserId: String(row["nixamp_user_id"] ?? ""),
|
|
119
126
|
scope: String(row["scope"] ?? ""),
|
|
@@ -127,6 +134,8 @@ export class NixampLinks {
|
|
|
127
134
|
now;
|
|
128
135
|
fetcher;
|
|
129
136
|
ready = null;
|
|
137
|
+
/** Access tokens, in memory only, until they end. Never in the database. */
|
|
138
|
+
live = new Map();
|
|
130
139
|
constructor(db, exchange, clientId, now = () => Date.now(),
|
|
131
140
|
/** How nixamp.com and the servers it lists are reached, for what the tokens are used on. */
|
|
132
141
|
fetcher = fetch) {
|
|
@@ -137,9 +146,28 @@ export class NixampLinks {
|
|
|
137
146
|
this.fetcher = fetcher;
|
|
138
147
|
}
|
|
139
148
|
async ensure() {
|
|
140
|
-
this.ready ??= this.db.query(SCHEMA).then(() =>
|
|
149
|
+
this.ready ??= this.db.query(SCHEMA).then(() => this.forgetLegacyTokens());
|
|
141
150
|
await this.ready;
|
|
142
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Before 0.79 the tokens were kept in this table in plain text. Hand each
|
|
154
|
+
* refresh token back to nixamp.com (so no grant is left standing there),
|
|
155
|
+
* then drop the columns. The identity link stays; the person clicks
|
|
156
|
+
* "Refresh from nixamp" the next time they go live.
|
|
157
|
+
*/
|
|
158
|
+
async forgetLegacyTokens() {
|
|
159
|
+
const { rows: columns } = await this.db.query(`SELECT column_name FROM information_schema.columns
|
|
160
|
+
WHERE table_schema = current_schema() AND table_name = '${TABLE}' AND column_name IN ('access_token', 'refresh_token', 'access_expires_at')`);
|
|
161
|
+
if (columns.length === 0)
|
|
162
|
+
return;
|
|
163
|
+
const names = new Set(columns.map((row) => String(row["column_name"])));
|
|
164
|
+
if (names.has("refresh_token")) {
|
|
165
|
+
const { rows } = await this.db.query(`SELECT refresh_token FROM ${TABLE} WHERE refresh_token <> ''`);
|
|
166
|
+
for (const row of rows)
|
|
167
|
+
await this.exchange.revoke(String(row["refresh_token"] ?? "")).catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
await this.db.query(`ALTER TABLE ${TABLE} DROP COLUMN IF EXISTS access_token, DROP COLUMN IF EXISTS refresh_token, DROP COLUMN IF EXISTS access_expires_at`);
|
|
170
|
+
}
|
|
143
171
|
/**
|
|
144
172
|
* The first leg: where to send the browser, and the two secrets the
|
|
145
173
|
* callback must bring back. The state is the CSRF check, the verifier is
|
|
@@ -159,7 +187,7 @@ export class NixampLinks {
|
|
|
159
187
|
});
|
|
160
188
|
return { url, state, verifier };
|
|
161
189
|
}
|
|
162
|
-
/** The second leg: the code becomes
|
|
190
|
+
/** The second leg: the code becomes a token, the token says whose; who is kept, the token only in memory. */
|
|
163
191
|
async finish(userId, leg) {
|
|
164
192
|
const grant = await this.exchange.token(new URLSearchParams({
|
|
165
193
|
grant_type: "authorization_code",
|
|
@@ -167,65 +195,54 @@ export class NixampLinks {
|
|
|
167
195
|
code_verifier: leg.verifier,
|
|
168
196
|
redirect_uri: leg.redirectUri,
|
|
169
197
|
}));
|
|
198
|
+
// Asked for none, but a refresh token sent anyway is handed straight back.
|
|
199
|
+
if (grant.refresh_token)
|
|
200
|
+
await this.exchange.revoke(grant.refresh_token).catch(() => undefined);
|
|
170
201
|
const who = await this.exchange.userinfo(grant.access_token);
|
|
171
202
|
await this.ensure();
|
|
172
|
-
await this.db.query(`INSERT INTO ${TABLE} (user_id, nixamp_user_id, handle, scope
|
|
173
|
-
VALUES ($1, $2, $3, $4
|
|
203
|
+
await this.db.query(`INSERT INTO ${TABLE} (user_id, nixamp_user_id, handle, scope)
|
|
204
|
+
VALUES ($1, $2, $3, $4)
|
|
174
205
|
ON CONFLICT (user_id) DO UPDATE SET nixamp_user_id = EXCLUDED.nixamp_user_id, handle = EXCLUDED.handle,
|
|
175
|
-
scope = EXCLUDED.scope,
|
|
176
|
-
refresh_token = EXCLUDED.refresh_token, updated_at = NOW()`, [userId, who.sub, who.handle ?? "", grant.scope ?? LINK_SCOPE, grant.access_token, this.expiry(grant), grant.refresh_token ?? ""]);
|
|
177
|
-
return this.of(userId);
|
|
178
|
-
}
|
|
179
|
-
expiry(grant) {
|
|
206
|
+
scope = EXCLUDED.scope, updated_at = NOW()`, [userId, who.sub, who.handle ?? "", grant.scope ?? LINK_SCOPE]);
|
|
180
207
|
const seconds = typeof grant.expires_in === "number" && grant.expires_in > 0 ? grant.expires_in : 3600;
|
|
181
|
-
|
|
208
|
+
this.live.set(userId, { token: grant.access_token, until: this.now() + seconds * 1000 });
|
|
209
|
+
return this.of(userId);
|
|
182
210
|
}
|
|
183
211
|
async of(userId) {
|
|
184
212
|
await this.ensure();
|
|
185
213
|
const { rows } = await this.db.query(`SELECT nixamp_user_id, handle, scope, created_at FROM ${TABLE} WHERE user_id = $1`, [userId]);
|
|
186
|
-
return view(rows[0]);
|
|
214
|
+
return view(rows[0], rows[0] !== undefined && this.held(userId) !== "");
|
|
215
|
+
}
|
|
216
|
+
held(userId) {
|
|
217
|
+
const kept = this.live.get(userId);
|
|
218
|
+
if (!kept)
|
|
219
|
+
return "";
|
|
220
|
+
if (kept.until - this.now() > EARLY_MS)
|
|
221
|
+
return kept.token;
|
|
222
|
+
this.live.delete(userId);
|
|
223
|
+
return "";
|
|
187
224
|
}
|
|
188
225
|
/**
|
|
189
|
-
* A token good for a call right now,
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* the row is dropped so the settings page says so too.
|
|
226
|
+
* A token good for a call right now, or "" when there is none in memory:
|
|
227
|
+
* not connected, expired, or the server restarted since. The page then
|
|
228
|
+
* offers "Refresh from nixamp".
|
|
193
229
|
*/
|
|
194
230
|
async accessToken(userId) {
|
|
195
231
|
await this.ensure();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const until = asTime(row["access_expires_at"]);
|
|
202
|
-
if (access !== "" && until !== null && until - this.now() > EARLY_MS)
|
|
203
|
-
return access;
|
|
204
|
-
const refresh = String(row["refresh_token"] ?? "");
|
|
205
|
-
if (refresh === "")
|
|
206
|
-
return "";
|
|
207
|
-
let grant;
|
|
208
|
-
try {
|
|
209
|
-
grant = await this.exchange.token(new URLSearchParams({ grant_type: "refresh_token", refresh_token: refresh }));
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
await this.db.query(`DELETE FROM ${TABLE} WHERE user_id = $1`, [userId]);
|
|
213
|
-
return "";
|
|
214
|
-
}
|
|
215
|
-
await this.db.query(`UPDATE ${TABLE} SET access_token = $2, access_expires_at = $3, refresh_token = $4, updated_at = NOW() WHERE user_id = $1`, [userId, grant.access_token, this.expiry(grant), grant.refresh_token ?? refresh]);
|
|
216
|
-
return grant.access_token;
|
|
232
|
+
return this.held(userId);
|
|
233
|
+
}
|
|
234
|
+
/** nixamp.com refused the token: forget it, so the page asks again. */
|
|
235
|
+
drop(userId) {
|
|
236
|
+
this.live.delete(userId);
|
|
217
237
|
}
|
|
218
|
-
/** Withdraw the
|
|
238
|
+
/** Withdraw the token on nixamp.com and forget the connection here. True when there was one. */
|
|
219
239
|
async disconnect(userId) {
|
|
220
240
|
await this.ensure();
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
await this.exchange.revoke(refresh !== "" ? refresh : access);
|
|
228
|
-
await this.db.query(`DELETE FROM ${TABLE} WHERE user_id = $1`, [userId]);
|
|
229
|
-
return true;
|
|
241
|
+
const token = this.held(userId);
|
|
242
|
+
this.live.delete(userId);
|
|
243
|
+
if (token !== "")
|
|
244
|
+
await this.exchange.revoke(token).catch(() => undefined);
|
|
245
|
+
const { rows } = await this.db.query(`DELETE FROM ${TABLE} WHERE user_id = $1 RETURNING user_id`, [userId]);
|
|
246
|
+
return rows.length > 0;
|
|
230
247
|
}
|
|
231
248
|
}
|
package/dist/oauth.d.ts
CHANGED
package/dist/oauth.js
CHANGED
|
@@ -41,7 +41,14 @@ export function coinpayProvider(clientId, clientSecret, base = "https://coinpayp
|
|
|
41
41
|
// Never verified, whatever the answer says: CoinPay sends no mail to the
|
|
42
42
|
// address it reports, so anybody can register a CoinPay account under
|
|
43
43
|
// someone else's address. Its `sub` is the whole identity.
|
|
44
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
provider: "coinpay",
|
|
46
|
+
subject,
|
|
47
|
+
email,
|
|
48
|
+
verified: false,
|
|
49
|
+
// Only when CoinPay names one: an absent field stays absent, not `undefined`.
|
|
50
|
+
...(typeof who["openprofile_url"] === "string" ? { openprofileUrl: who["openprofile_url"] } : {}),
|
|
51
|
+
};
|
|
45
52
|
},
|
|
46
53
|
};
|
|
47
54
|
}
|
package/dist/server.js
CHANGED
|
@@ -37,7 +37,7 @@ import { anonymousHandle, Handles } from "./handles.js";
|
|
|
37
37
|
import { OpenDirs } from "./opendirs.js";
|
|
38
38
|
import { Servers } from "./servers.js";
|
|
39
39
|
import { AccountProfiles, hostCard, PHOTO_LIMIT } from "./profiles.js";
|
|
40
|
-
import { createProfileHandlers, saveSignupProfile, validateProfile } from "@profullstack/profile";
|
|
40
|
+
import { createProfileHandlers, saveOpenProfileContact, saveSignupProfile, validateProfile } from "@profullstack/profile";
|
|
41
41
|
import { SIGNUP_PROFILE_FIELDS, signupProfiles } from "./signup-profiles.js";
|
|
42
42
|
import { NixampLinks, nixampExchange } from "./nixamp-link.js";
|
|
43
43
|
import { handleNixampLinkApi, nixampLinkPath } from "./nixamp-link-api.js";
|
|
@@ -67,7 +67,7 @@ import { handleAiRoutes, isPublicSharePath } from "./ai-routes.js";
|
|
|
67
67
|
import { AiLabel, VeoUnavailable } from "./ai-label.js";
|
|
68
68
|
import { buyLink, handleLabelRoutes, isPublicLabelPath } from "./label-routes.js";
|
|
69
69
|
import { GrantVerifier, MusicStore, PRICES } from "./music-store.js";
|
|
70
|
-
import { handleMusicRoutes, isMusicPath, MusicHub, musicPageFor, serverId } from "./music-hub.js";
|
|
70
|
+
import { handleMusicRoutes, isMusicPath, MusicHub, musicPageFor, parseMoved, serverId } from "./music-hub.js";
|
|
71
71
|
import { fileLedger, pgLedger, VeoBudgetError, VeoStudio } from "./veo-studio.js";
|
|
72
72
|
import { KnownServers } from "./known-servers.js";
|
|
73
73
|
import { handleStoreRoutes, isStorePath } from "./store-routes.js";
|
|
@@ -83,7 +83,7 @@ import { aiKey } from "./ai-keys.js";
|
|
|
83
83
|
import { STATION_APP_CSP, STATION_PAGE, STATION_PAGE_CSP, STATION_PAGE_HTML, WATCH_APP_CSP } from "./station-page.js";
|
|
84
84
|
import { handleRoomProxy, handleRooms, isRoomPath, ROOM_PAGE, roomAddress, RoomProbe, roomEntries, roomSlugOf, roomSlugs, RoomStore, watchBase } from "./guide-rooms.js";
|
|
85
85
|
import { handleVods, VodProbe, vodEntries, VodStore, vodView } from "./guide-vods.js";
|
|
86
|
-
import {
|
|
86
|
+
import { withUniversalFooter } from "./universal-footer.js";
|
|
87
87
|
import { handleFollowPage } from "./follow-page.js";
|
|
88
88
|
import { asRadioPage, buildCatalogue, isRadioPath, NAV_STYLE, RADIO_ALBUM, RADIO_ARTIST, RADIO_CSP, RADIO_STATION, RADIO_SUB, RADIO_BUY, radioBuyLink, radioBuyQuery, radioNav, radioPeers, radioRewriter, radioStations, renderAlbum, stationImage, renderArtist, renderDirectory, renderMissing, rewriteResponse, } from "./radio-pages.js";
|
|
89
89
|
import { adCsp, guideAds, guideCsp, withPageAd, guideHosts, guidePeers, peerBase, PeerGuides, renderGuide } from "./station-guide.js";
|
|
@@ -112,6 +112,7 @@ import { Durable } from "./durable.js";
|
|
|
112
112
|
import { notifyAll, resendEmail, webPush } from "./notify.js";
|
|
113
113
|
import { vapidKeysFromEnv, vapidPublicKeyResponse } from "@profullstack/notifications/server";
|
|
114
114
|
import { inviteSubject, inviteText, isEmail, isPhone, watchLink } from "./invite.js";
|
|
115
|
+
import { c0mputeKey } from "./ai-text.js";
|
|
115
116
|
import { EventWriter } from "./event-writer.js";
|
|
116
117
|
import { handleLiveApi } from "./live-api.js";
|
|
117
118
|
import { LiveEvents, eventStructuredData, isTicketed } from "./live-events.js";
|
|
@@ -4847,6 +4848,14 @@ export function createHandler(engine, options) {
|
|
|
4847
4848
|
html(response, 401, signInFailedPage(result.error || "Could not sign in."));
|
|
4848
4849
|
return;
|
|
4849
4850
|
}
|
|
4851
|
+
if (identity.provider === "coinpay" && identity.openprofileUrl && options.signupProfiles) {
|
|
4852
|
+
try {
|
|
4853
|
+
await saveOpenProfileContact(options.signupProfiles, String(result.account.id), identity.openprofileUrl);
|
|
4854
|
+
}
|
|
4855
|
+
catch (e) {
|
|
4856
|
+
console.warn("[coinpay] OpenProfile import skipped:", e instanceof Error ? e.message : e);
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4850
4859
|
if (pending.userCode) {
|
|
4851
4860
|
// This round trip was approving a terminal. The browser is finished;
|
|
4852
4861
|
// the session belongs to whatever is polling.
|
|
@@ -6667,7 +6676,7 @@ export function createHandler(engine, options) {
|
|
|
6667
6676
|
// somewhere usable, but it says 404 so it is never indexed.
|
|
6668
6677
|
const shell = readIfPossible(join(web, "index.html"));
|
|
6669
6678
|
if (shell !== null) {
|
|
6670
|
-
html(response, 404, requestHost === "nixamp.com" ?
|
|
6679
|
+
html(response, 404, requestHost === "nixamp.com" ? withUniversalFooter(shell) : shell);
|
|
6671
6680
|
return;
|
|
6672
6681
|
}
|
|
6673
6682
|
}
|
|
@@ -6707,17 +6716,17 @@ export function createHandler(engine, options) {
|
|
|
6707
6716
|
}
|
|
6708
6717
|
const shell = subject ? readIfPossible(file) : null;
|
|
6709
6718
|
if (subject && shell !== null) {
|
|
6710
|
-
html(response, 200, requestHost === "nixamp.com" ?
|
|
6719
|
+
html(response, 200, requestHost === "nixamp.com" ? withUniversalFooter(joinDocument(shell, subject, eventSite ?? "https://nixamp.com")) : joinDocument(shell, subject, eventSite ?? "https://nixamp.com"));
|
|
6711
6720
|
return;
|
|
6712
6721
|
}
|
|
6713
6722
|
}
|
|
6714
6723
|
// nixamp.com's own pages (the app shell, the radio console) carry the
|
|
6715
|
-
//
|
|
6724
|
+
// shared Profullstack footer, appended on the way out. A local install
|
|
6716
6725
|
// serving the same files on its own host is left alone.
|
|
6717
6726
|
if (requestHost === "nixamp.com" && file.endsWith(".html") && (request.method === "GET" || request.method === "HEAD")) {
|
|
6718
6727
|
const shell = readIfPossible(file);
|
|
6719
6728
|
if (shell !== null) {
|
|
6720
|
-
const page =
|
|
6729
|
+
const page = withUniversalFooter(shell);
|
|
6721
6730
|
response.writeHead(200, {
|
|
6722
6731
|
...CORS,
|
|
6723
6732
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -7748,7 +7757,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
7748
7757
|
return [...new Set([...guidePeers(), ...listed, ...(knownServers?.list().map((one) => one.base) ?? [])])];
|
|
7749
7758
|
};
|
|
7750
7759
|
// nixamp.com/music: every server's label in one catalogue (music-hub.ts).
|
|
7751
|
-
const musicHub = options.directory ? new MusicHub({ peers: musicServers }) : undefined;
|
|
7760
|
+
const musicHub = options.directory ? new MusicHub({ peers: musicServers, moved: parseMoved(process.env["NIXAMP_MUSIC_MOVED"]) }) : undefined;
|
|
7752
7761
|
const musicStore = supportPayments && supportPayments.enabled && pool && jwtSecret ? new MusicStore({
|
|
7753
7762
|
db: pool,
|
|
7754
7763
|
support: supportPayments,
|
|
@@ -8440,7 +8449,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
8440
8449
|
...(favorites ? { favorites } : {}),
|
|
8441
8450
|
...(settingsSync ? { settingsSync } : {}),
|
|
8442
8451
|
...(events ? { events, eventWriter: new EventWriter({
|
|
8443
|
-
openaiKey: process.env["OPENAI_API_KEY"], anthropicKey: process.env["ANTHROPIC_API_KEY"],
|
|
8452
|
+
c0mputeKey: c0mputeKey(), openaiKey: process.env["OPENAI_API_KEY"], anthropicKey: process.env["ANTHROPIC_API_KEY"],
|
|
8444
8453
|
openaiModel: process.env["NIXAMP_WRITER_OPENAI_MODEL"], claudeModel: process.env["NIXAMP_WRITER_CLAUDE_MODEL"],
|
|
8445
8454
|
}) } : {}),
|
|
8446
8455
|
...(events && follows ? { onEventUpdated: (updated, previous) => {
|