3da1694085
Add the office jukebox flow so Spotify can be controlled from the SOUNDCLAW skill, manual jukebox UI, and local browser auth bridge during development. Made-with: Cursor
188 lines
5.6 KiB
JavaScript
188 lines
5.6 KiB
JavaScript
const http = require("node:http");
|
|
const https = require("node:https");
|
|
const next = require("next");
|
|
|
|
const { createAccessGate } = require("./access-gate");
|
|
const { createGatewayProxy } = require("./gateway-proxy");
|
|
const { assertPublicHostAllowed, resolveHosts } = require("./network-policy");
|
|
const { loadUpstreamGatewaySettings } = require("./studio-settings");
|
|
|
|
const resolvePort = () => {
|
|
const raw = process.env.PORT?.trim() || "3000";
|
|
const port = Number(raw);
|
|
if (!Number.isFinite(port) || port <= 0) return 3000;
|
|
return port;
|
|
};
|
|
|
|
const resolvePathname = (url) => {
|
|
const raw = typeof url === "string" ? url : "";
|
|
const idx = raw.indexOf("?");
|
|
return (idx === -1 ? raw : raw.slice(0, idx)) || "/";
|
|
};
|
|
|
|
const CERT_DIR = require("node:path").join(__dirname, "..", ".certs");
|
|
const CERT_PATH = require("node:path").join(CERT_DIR, "localhost.crt");
|
|
const KEY_PATH = require("node:path").join(CERT_DIR, "localhost.key");
|
|
|
|
const generateHttpsCert = async () => {
|
|
const fs = require("node:fs");
|
|
|
|
// Re-use a saved cert so the browser only needs to trust it once.
|
|
if (fs.existsSync(CERT_PATH) && fs.existsSync(KEY_PATH)) {
|
|
return {
|
|
key: fs.readFileSync(KEY_PATH, "utf8"),
|
|
cert: fs.readFileSync(CERT_PATH, "utf8"),
|
|
};
|
|
}
|
|
|
|
const selfsigned = require("selfsigned");
|
|
const attrs = [{ name: "commonName", value: "localhost" }];
|
|
const pems = await selfsigned.generate(attrs, {
|
|
days: 825,
|
|
keySize: 2048,
|
|
algorithm: "sha256",
|
|
extensions: [
|
|
{
|
|
name: "subjectAltName",
|
|
altNames: [
|
|
{ type: 2, value: "localhost" },
|
|
{ type: 7, ip: "127.0.0.1" },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
fs.mkdirSync(CERT_DIR, { recursive: true });
|
|
fs.writeFileSync(CERT_PATH, pems.cert);
|
|
fs.writeFileSync(KEY_PATH, pems.private);
|
|
|
|
console.info(`\nCert saved to ${CERT_DIR}`);
|
|
console.info("To make browsers trust it (macOS), run:");
|
|
console.info(` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CERT_PATH}"\n`);
|
|
|
|
return { key: pems.private, cert: pems.cert };
|
|
};
|
|
|
|
async function main() {
|
|
const dev = process.argv.includes("--dev");
|
|
const useHttps = process.argv.includes("--https") || process.env.HTTPS === "true";
|
|
const hostnames = Array.from(new Set(resolveHosts(process.env)));
|
|
const hostname = hostnames[0] ?? "127.0.0.1";
|
|
const port = resolvePort();
|
|
for (const host of hostnames) {
|
|
assertPublicHostAllowed({
|
|
host,
|
|
studioAccessToken: process.env.STUDIO_ACCESS_TOKEN,
|
|
});
|
|
}
|
|
|
|
const app = next({
|
|
dev,
|
|
hostname,
|
|
port,
|
|
...(dev ? { webpack: true } : null),
|
|
});
|
|
const handle = app.getRequestHandler();
|
|
|
|
const accessGate = createAccessGate({
|
|
token: process.env.STUDIO_ACCESS_TOKEN,
|
|
});
|
|
|
|
const proxy = createGatewayProxy({
|
|
loadUpstreamSettings: async () => {
|
|
const settings = loadUpstreamGatewaySettings(process.env);
|
|
return { url: settings.url, token: settings.token };
|
|
},
|
|
allowWs: (req) => {
|
|
if (resolvePathname(req.url) !== "/api/gateway/ws") return false;
|
|
return true;
|
|
},
|
|
verifyClient: (info) => accessGate.allowUpgrade(info.req),
|
|
});
|
|
|
|
await app.prepare();
|
|
const handleUpgrade = app.getUpgradeHandler();
|
|
const handleServerUpgrade = (req, socket, head) => {
|
|
if (resolvePathname(req.url) === "/api/gateway/ws") {
|
|
proxy.handleUpgrade(req, socket, head);
|
|
return;
|
|
}
|
|
handleUpgrade(req, socket, head);
|
|
};
|
|
|
|
const httpsCert = useHttps ? await generateHttpsCert() : null;
|
|
|
|
const createServer = () =>
|
|
useHttps
|
|
? https.createServer(httpsCert, (req, res) => {
|
|
if (accessGate.handleHttp(req, res)) return;
|
|
handle(req, res);
|
|
})
|
|
: http.createServer((req, res) => {
|
|
if (accessGate.handleHttp(req, res)) return;
|
|
handle(req, res);
|
|
});
|
|
|
|
const servers = hostnames.map(() => createServer());
|
|
|
|
const attachUpgradeHandlers = (server) => {
|
|
server.on("upgrade", handleServerUpgrade);
|
|
server.on("newListener", (eventName, listener) => {
|
|
if (eventName !== "upgrade") return;
|
|
if (listener === handleServerUpgrade) return;
|
|
process.nextTick(() => {
|
|
server.removeListener("upgrade", listener);
|
|
});
|
|
});
|
|
};
|
|
|
|
for (const server of servers) {
|
|
attachUpgradeHandlers(server);
|
|
}
|
|
|
|
const listenOnHost = (server, host) =>
|
|
new Promise((resolve, reject) => {
|
|
const onError = (err) => {
|
|
server.off("error", onError);
|
|
reject(err);
|
|
};
|
|
server.once("error", onError);
|
|
server.listen(port, host, () => {
|
|
server.off("error", onError);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
const closeServer = (server) =>
|
|
new Promise((resolve) => {
|
|
if (!server.listening) return resolve();
|
|
server.close(() => resolve());
|
|
});
|
|
|
|
try {
|
|
await Promise.all(servers.map((server, index) => listenOnHost(server, hostnames[index])));
|
|
} catch (err) {
|
|
await Promise.all(servers.map((server) => closeServer(server)));
|
|
throw err;
|
|
}
|
|
|
|
const hostForBrowser = hostnames.some((value) => value === "127.0.0.1" || value === "::1")
|
|
? "localhost"
|
|
: hostname === "0.0.0.0" || hostname === "::"
|
|
? "localhost"
|
|
: hostname;
|
|
|
|
const protocol = useHttps ? "https" : "http";
|
|
const browserUrl = `${protocol}://${hostForBrowser}:${port}`;
|
|
console.info(`Open in browser: ${browserUrl}`);
|
|
if (useHttps) {
|
|
console.info("HTTPS mode: self-signed cert in use. You may need to accept a browser security warning once.");
|
|
console.info(`Spotify redirect URI: ${browserUrl}/office`);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exitCode = 1;
|
|
});
|