ccdeck 3.28.1 → 3.29.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.
- package/README.md +4 -0
- package/THIRD_PARTY_NOTICES.md +148 -4
- package/dist/web/assets/{BrowserWatchModal-CIla-_hm.js → BrowserWatchModal-CxfWHmjb.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-CSgh5kjS.js → UsageHistoryModal-D-XRttte.js} +1 -1
- package/dist/web/assets/hls-BFpXDghs.js +42 -0
- package/dist/web/assets/index-DrbRdXDD.js +298 -0
- package/dist/web/assets/index-GTZ73XfY.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/release-notes.json +26 -0
- package/src/server/account-health.mjs +38 -0
- package/src/server/claude-accounts.mjs +84 -26
- package/src/server/cswap-admin.mjs +136 -8
- package/src/server/deck-prefs.mjs +2 -0
- package/src/server/fm-station.mjs +186 -0
- package/src/server/index.mjs +204 -20
- package/src/server/lan-engine.mjs +244 -38
- package/src/server/lan-socket.mjs +101 -15
- package/src/server/lan-sync.mjs +86 -9
- package/dist/web/assets/index-CsHTYwoD.js +0 -297
- package/dist/web/assets/index-DbwzUUcd.css +0 -1
package/src/server/index.mjs
CHANGED
|
@@ -432,6 +432,67 @@ function notifyTrays(title, body, { chime = null, who = null } = {}) {
|
|
|
432
432
|
return true;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
// The desktop app's updater lives in Electron, while the window is a page
|
|
436
|
+
// served by this deck. Keep only the small piece of state the page needs. A
|
|
437
|
+
// report is accepted only with the deck token; a browser may request an
|
|
438
|
+
// install, but Electron verifies the exact ready version again before acting.
|
|
439
|
+
const DESKTOP_UPDATE_STATUSES = new Set(["idle", "checking", "current", "downloading", "ready", "error"]);
|
|
440
|
+
let desktopUpdateState = { status: "idle", version: null };
|
|
441
|
+
|
|
442
|
+
function cleanDesktopUpdate(value) {
|
|
443
|
+
if (!value || typeof value !== "object" || !DESKTOP_UPDATE_STATUSES.has(value.status)) return null;
|
|
444
|
+
const version = typeof value.version === "string" && value.version.trim()
|
|
445
|
+
? value.version.trim().slice(0, 80)
|
|
446
|
+
: null;
|
|
447
|
+
if (value.status === "ready" && !version) return null;
|
|
448
|
+
return { status: value.status, version };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function broadcastDesktopUpdate() {
|
|
452
|
+
const frame = `event: desktop-update\ndata: ${JSON.stringify(desktopUpdateState)}\n\n`;
|
|
453
|
+
for (const client of sseClients) {
|
|
454
|
+
if (!trayClients.has(client)) writeSse(client, frame);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function handleDesktopUpdateRead(_req, res) {
|
|
459
|
+
send(res, 200, desktopUpdateState);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function handleDesktopUpdateReport(req, res) {
|
|
463
|
+
// Same-origin pages pass the generic mutation gate, but only the native app
|
|
464
|
+
// may claim what its updater has verified.
|
|
465
|
+
if (!presentsDeckToken(req.headers ?? {})) {
|
|
466
|
+
return send(res, 401, { ok: false, reason: "app_token_required" });
|
|
467
|
+
}
|
|
468
|
+
const body = await readBody(req, res).catch(() => null);
|
|
469
|
+
let value = null;
|
|
470
|
+
try { value = cleanDesktopUpdate(JSON.parse(body ?? "")); } catch { /* bad JSON */ }
|
|
471
|
+
if (!value) return send(res, 400, { ok: false, reason: "bad_update_state" });
|
|
472
|
+
desktopUpdateState = value;
|
|
473
|
+
broadcastDesktopUpdate();
|
|
474
|
+
send(res, 200, { ok: true });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// The window's two messages about that update, relayed to the app as frames
|
|
478
|
+
// on its tray stream: apply it (`desktop-update-restart`), or it has been shown
|
|
479
|
+
// (`desktop-update-seen`, so the app's own ready notice stands down, #1182).
|
|
480
|
+
// Both behind the same gates as /api/restart, and neither decides anything:
|
|
481
|
+
// this refuses only what cannot be current, and Electron checks the exact
|
|
482
|
+
// ready version again before acting on either.
|
|
483
|
+
async function handleDesktopUpdateRequest(req, res, event) {
|
|
484
|
+
const body = await readBody(req, res).catch(() => null);
|
|
485
|
+
let version = "";
|
|
486
|
+
try { version = String(JSON.parse(body ?? "")?.version ?? "").trim(); } catch { /* bad JSON */ }
|
|
487
|
+
if (!version || desktopUpdateState.status !== "ready" || desktopUpdateState.version !== version) {
|
|
488
|
+
return send(res, 409, { ok: false, reason: "update_not_ready" });
|
|
489
|
+
}
|
|
490
|
+
if (trayClients.size === 0) return send(res, 409, { ok: false, reason: "app_disconnected" });
|
|
491
|
+
const frame = `event: ${event}\ndata: ${JSON.stringify({ version })}\n\n`;
|
|
492
|
+
for (const client of trayClients) writeSse(client, frame);
|
|
493
|
+
send(res, 202, { ok: true });
|
|
494
|
+
}
|
|
495
|
+
|
|
435
496
|
let persistPath = null; // absolute path to events.jsonl, or null
|
|
436
497
|
|
|
437
498
|
// ─── Which deck records which session ─────────────────────────────────────
|
|
@@ -3889,6 +3950,31 @@ async function handlePresence(req, res) {
|
|
|
3889
3950
|
* Tailscale is here at all before anybody can turn discovery on. */
|
|
3890
3951
|
const tailnet = createTailnet();
|
|
3891
3952
|
|
|
3953
|
+
// Whether every import is followed by checkImports, which only asks on a Mac.
|
|
3954
|
+
// When it does, the import skips its own detached collection — see importAccount.
|
|
3955
|
+
const CHECKS_IMPORTS = process.platform === "darwin";
|
|
3956
|
+
|
|
3957
|
+
const ACTIVE_VERDICT_FIRST_RETRY_MS = 60_000;
|
|
3958
|
+
const ACTIVE_VERDICT_MAX_RETRY_MS = 30 * 60_000;
|
|
3959
|
+
const LIVE_LOGIN_TIMEOUT_MS = 5_000;
|
|
3960
|
+
let activeVerdictRetryMs = ACTIVE_VERDICT_FIRST_RETRY_MS;
|
|
3961
|
+
let activeVerdictNextAt = 0;
|
|
3962
|
+
// A claude-swap that never reports a verdict for the active slot would
|
|
3963
|
+
// otherwise cost a usage collection every minute for as long as LAN is on.
|
|
3964
|
+
function refreshActiveVerdict() {
|
|
3965
|
+
if (Date.now() < activeVerdictNextAt) return;
|
|
3966
|
+
activeVerdictNextAt = Date.now() + activeVerdictRetryMs;
|
|
3967
|
+
activeVerdictRetryMs = Math.min(activeVerdictRetryMs * 2, ACTIVE_VERDICT_MAX_RETRY_MS);
|
|
3968
|
+
void import("./claude-accounts.mjs")
|
|
3969
|
+
.then(({ verdictsNow, invalidateClaudeAccountsCache }) =>
|
|
3970
|
+
verdictsNow().then(got => { if (got) invalidateClaudeAccountsCache(); }))
|
|
3971
|
+
.catch(() => {});
|
|
3972
|
+
}
|
|
3973
|
+
function activeVerdictArrived() {
|
|
3974
|
+
activeVerdictRetryMs = ACTIVE_VERDICT_FIRST_RETRY_MS;
|
|
3975
|
+
activeVerdictNextAt = 0;
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3892
3978
|
const lanEngine = createEngine({
|
|
3893
3979
|
tailnet,
|
|
3894
3980
|
// Who holds the discovery port when it is taken, so the panel can say.
|
|
@@ -3901,17 +3987,63 @@ const lanEngine = createEngine({
|
|
|
3901
3987
|
about: aboutThisDeck({ version: RUNNING_VERSION }),
|
|
3902
3988
|
readAccounts: async () => {
|
|
3903
3989
|
const { fetchClaudeAccounts } = await import("./claude-accounts.mjs");
|
|
3904
|
-
|
|
3990
|
+
const got = await fetchClaudeAccounts();
|
|
3991
|
+
// A login this Mac's Keychain will not open from the deck's session. The
|
|
3992
|
+
// engine refuses to export it with a fixed code rather than spawning an
|
|
3993
|
+
// export that can only fail. Mac only: claude-swap's `keychain_unavailable`
|
|
3994
|
+
// elsewhere is an unreadable .enc file, and the sentence the peer prints
|
|
3995
|
+
// talks about a Keychain.
|
|
3996
|
+
if (!got || !Array.isArray(got.accounts)) return got;
|
|
3997
|
+
// The active slot is withheld from peers until it has a fresh verdict — see
|
|
3998
|
+
// cachedExportReadable — so ask for one now rather than waiting on the
|
|
3999
|
+
// collector's own schedule.
|
|
4000
|
+
if (got.accounts.some(a => a.active === true && a.collector == null)) refreshActiveVerdict();
|
|
4001
|
+
else activeVerdictArrived();
|
|
4002
|
+
const { markUnreadable } = await import("./cswap-admin.mjs");
|
|
4003
|
+
return { ...got, accounts: markUnreadable(got.accounts) };
|
|
4004
|
+
},
|
|
4005
|
+
// Bounded well inside a round, since a want waits on it.
|
|
4006
|
+
liveLogin: async () => {
|
|
4007
|
+
const { currentIdentity } = await import("./cswap-admin.mjs");
|
|
4008
|
+
const late = new Promise(resolve => setTimeout(resolve, LIVE_LOGIN_TIMEOUT_MS, null).unref?.());
|
|
4009
|
+
return Promise.race([currentIdentity().catch(() => null), late]);
|
|
3905
4010
|
},
|
|
3906
|
-
exportAccount: async num => {
|
|
3907
|
-
const {
|
|
3908
|
-
const
|
|
3909
|
-
|
|
4011
|
+
exportAccount: async (num, expectedKey) => {
|
|
4012
|
+
const { accountKey } = await import("./lan-sync.mjs");
|
|
4013
|
+
const { shareAccounts, unwrapShare } = await import("./cswap-admin.mjs");
|
|
4014
|
+
// Do not collect live verdicts here. A LAN want has a ten-second round
|
|
4015
|
+
// budget, while a verdict collection may wait up to ninety seconds (or
|
|
4016
|
+
// queue behind another one). The want handler already re-reads this deck's
|
|
4017
|
+
// cached account state immediately before export and refuses known
|
|
4018
|
+
// unreadable/dead copies. The share itself is therefore the only bounded
|
|
4019
|
+
// operation left on the hot path.
|
|
4020
|
+
//
|
|
4021
|
+
// Without the explanation, which would outlast the asking peer's patience;
|
|
4022
|
+
// see shareAccounts.
|
|
4023
|
+
const out = await shareAccounts([String(num)], { explain: false });
|
|
4024
|
+
// The blob or nothing. WHY it failed is never carried out of here: the
|
|
4025
|
+
// engine decides what a peer is told from this deck's state — see
|
|
4026
|
+
// `readable` in readAccounts below.
|
|
4027
|
+
if (!out?.ok) return null;
|
|
4028
|
+
const opened = unwrapShare(out.blob);
|
|
4029
|
+
if (!opened.ok) return null;
|
|
4030
|
+
let accounts;
|
|
4031
|
+
try { accounts = JSON.parse(opened.payload)?.accounts; } catch { return null; }
|
|
4032
|
+
if (!Array.isArray(accounts) || accounts.length !== 1) return null;
|
|
4033
|
+
// Slot numbers are local. Verify the payload identity after export so a
|
|
4034
|
+
// moved/reused slot can never satisfy a want for another account.
|
|
4035
|
+
return accountKey(accounts[0]?.email, accounts[0]?.organizationUuid) === expectedKey ? out.blob : null;
|
|
4036
|
+
},
|
|
4037
|
+
// The logins a round just imported, checked once after it — see checkImports.
|
|
4038
|
+
checkArrivals: async steps => {
|
|
4039
|
+
const { checkImports } = await import("./cswap-admin.mjs");
|
|
4040
|
+
return checkImports(steps.map(step => {
|
|
4041
|
+
const [email, org] = String(step?.key ?? "").split("@@");
|
|
4042
|
+
return { email, org: org ?? "" };
|
|
4043
|
+
}));
|
|
3910
4044
|
},
|
|
3911
4045
|
importAccount: async (blob, step) => {
|
|
3912
|
-
|
|
3913
|
-
// the forced import, which fillEmptySlot now owns along with it.
|
|
3914
|
-
const { importAccount, fillEmptySlot } = await import("./cswap-admin.mjs");
|
|
4046
|
+
const { importAccount, fillEmptySlot, landed } = await import("./cswap-admin.mjs");
|
|
3915
4047
|
const [want, wantOrg] = String(step?.key ?? "").split("@@");
|
|
3916
4048
|
// NO `force`, ever, and this is the line where that promise is kept. A
|
|
3917
4049
|
// plain import adds an account that is missing and replaces exactly one
|
|
@@ -3932,22 +4064,27 @@ const lanEngine = createEngine({
|
|
|
3932
4064
|
// `force === true && narrowing` — so the promise above survives word for
|
|
3933
4065
|
// word, and a bundle that does not carry what was asked for is refused
|
|
3934
4066
|
// rather than unpacked.
|
|
3935
|
-
const out = await importAccount(blob, { only: { email: want, org: wantOrg ?? "" } });
|
|
4067
|
+
const out = await importAccount(blob, { only: { email: want, org: wantOrg ?? "" }, collect: !CHECKS_IMPORTS });
|
|
3936
4068
|
if (!out?.ok) return { ok: false, why: out?.reason ?? "import refused" };
|
|
3937
4069
|
// A SKIP IS NOT A HEAL, and reading `ok` alone said it was. `cswap import`
|
|
3938
|
-
// exits ZERO when it declines an account it already holds
|
|
3939
|
-
// own comment says so and reports it as `added: false` — so a round that
|
|
4070
|
+
// exits ZERO when it declines an account it already holds, so a round that
|
|
3940
4071
|
// changed nothing was counted as a successful repair and the panel said the
|
|
3941
4072
|
// account had been fixed. Whoever read that then waited for numbers that
|
|
3942
4073
|
// were never going to move.
|
|
3943
4074
|
//
|
|
4075
|
+
// `landed`, NOT `added`: `added` counts new slots only, and a plain import
|
|
4076
|
+
// that replaces a quarantined slot is `healed` — the one repair this path
|
|
4077
|
+
// exists for. Reading `added` reported every such heal as a failure and
|
|
4078
|
+
// then sent it on to fillEmptySlot, which declined it. A slot that stayed
|
|
4079
|
+
// `present` is the decline.
|
|
4080
|
+
//
|
|
3944
4081
|
// The decline is narrow and documented on claude-swap's side: a plain
|
|
3945
4082
|
// import replaces a slot only when its usage row is quarantined as
|
|
3946
4083
|
// refresh-token-dead, and is "never triggered by the live store's
|
|
3947
4084
|
// `no credentials` state". So an account whose login expired heals over the
|
|
3948
4085
|
// network, and one that has NO stored login does not — which is a true
|
|
3949
4086
|
// sentence the panel can now print instead of a false one.
|
|
3950
|
-
if (out.
|
|
4087
|
+
if (landed(out.results)) return { ok: true };
|
|
3951
4088
|
|
|
3952
4089
|
// THE DECLINE, AND WHY IT MATTERS WHICH ONE IT IS.
|
|
3953
4090
|
//
|
|
@@ -3986,7 +4123,7 @@ const lanEngine = createEngine({
|
|
|
3986
4123
|
// verdict taken before any of it began. fillEmptySlot re-reads the verdict
|
|
3987
4124
|
// as its first statement INSIDE the lock, so the promise holds by
|
|
3988
4125
|
// construction rather than by how long the queue happened to be.
|
|
3989
|
-
return fillEmptySlot(blob, { email: want, org: wantOrg ?? "" });
|
|
4126
|
+
return fillEmptySlot(blob, { email: want, org: wantOrg ?? "", collect: !CHECKS_IMPORTS });
|
|
3990
4127
|
},
|
|
3991
4128
|
// The deck's own long-term key, kept so a restart is the same deck rather
|
|
3992
4129
|
// than a stranger to everybody who has paired with it. Written once, on the
|
|
@@ -4117,6 +4254,7 @@ export function lanApplyFields(prefs, { load = false, env = process.env } = {})
|
|
|
4117
4254
|
shared: Array.isArray(lan.shared) ? lan.shared : [],
|
|
4118
4255
|
autoAsk: lan.autoAsk !== false,
|
|
4119
4256
|
autoAccept: lan.autoAccept !== false,
|
|
4257
|
+
pairingMode: lan.pairingMode === "invite" ? "invite" : "automatic",
|
|
4120
4258
|
// Whether paired decks are told which shared account this one is on.
|
|
4121
4259
|
shareActive: lan.shareActive !== false,
|
|
4122
4260
|
// Discovery over Tailscale, off unless somebody turned it on, and the two
|
|
@@ -5875,6 +6013,31 @@ async function handleClaudeFm(req, res) {
|
|
|
5875
6013
|
send(res, 200, answer);
|
|
5876
6014
|
}
|
|
5877
6015
|
|
|
6016
|
+
/**
|
|
6017
|
+
* A custom station's YouTube link, resolved to the channel and video the
|
|
6018
|
+
* embed plays (#1208). fm-station.mjs rebuilds the request from the parsed
|
|
6019
|
+
* link rather than fetching it as given, and caches the answer.
|
|
6020
|
+
*
|
|
6021
|
+
* AGENTS_DECK_NO_MUSIC is the promise that this deck never contacts YouTube,
|
|
6022
|
+
* and a station somebody added is not an exception to it: the answer is the
|
|
6023
|
+
* same plain no /api/claude-fm gives, and the canvas draws nothing for it.
|
|
6024
|
+
*/
|
|
6025
|
+
async function handleFmStation(req, res) {
|
|
6026
|
+
if (process.env.AGENTS_DECK_NO_MUSIC === "1") {
|
|
6027
|
+
return send(res, 200, { ok: false, off: true });
|
|
6028
|
+
}
|
|
6029
|
+
const url = new URL(req.url, "http://localhost");
|
|
6030
|
+
const stationUrl = url.searchParams.get("url") ?? "";
|
|
6031
|
+
const { parseYouTubeStationUrl, resolveYouTubeStation } = await import(
|
|
6032
|
+
pathToFileURL(join(PKG_ROOT, "src/server/fm-station.mjs")).href
|
|
6033
|
+
);
|
|
6034
|
+
if (!parseYouTubeStationUrl(stationUrl)) {
|
|
6035
|
+
return send(res, 400, { ok: false, error: "unsupported_url" });
|
|
6036
|
+
}
|
|
6037
|
+
const answer = await resolveYouTubeStation(stationUrl);
|
|
6038
|
+
send(res, answer.ok ? 200 : 404, answer);
|
|
6039
|
+
}
|
|
6040
|
+
|
|
5878
6041
|
async function handleLofiGirl(req, res) {
|
|
5879
6042
|
if (process.env.AGENTS_DECK_NO_MUSIC === "1") {
|
|
5880
6043
|
return send(res, 200, { ok: true, live: false, off: true });
|
|
@@ -6154,14 +6317,14 @@ function getProjectRollup() {
|
|
|
6154
6317
|
* The "Projects" report for one account: how many tokens it spent per project
|
|
6155
6318
|
* over a window. The server tallies only tokens (per model); the web side
|
|
6156
6319
|
* prices them with its own table, so cost never lives in two places. `num` is
|
|
6157
|
-
* the account's slot, resolved to its `(email, org)` key here; `days` is
|
|
6158
|
-
* or 0 for all tracked.
|
|
6320
|
+
* the account's slot, resolved to its `(email, org)` key here; `days` is 1, 7,
|
|
6321
|
+
* 30, or 0 for all tracked.
|
|
6159
6322
|
*/
|
|
6160
6323
|
async function handleAccountProjects(req, res) {
|
|
6161
6324
|
const url = new URL(req.url, "http://localhost");
|
|
6162
6325
|
const num = Number(url.searchParams.get("num"));
|
|
6163
6326
|
const d = Number(url.searchParams.get("days"));
|
|
6164
|
-
const days = d === 30 ? 30 : d === 0 ? 0 : 7;
|
|
6327
|
+
const days = d === 1 ? 1 : d === 30 ? 30 : d === 0 ? 0 : 7;
|
|
6165
6328
|
if (!Number.isInteger(num) || num <= 0) return send(res, 400, { ok: false, reason: "bad_account" });
|
|
6166
6329
|
const { identityForSlot } = await import(pathToFileURL(join(PKG_ROOT, "src/server/swap-log.mjs")).href);
|
|
6167
6330
|
const id = await identityForSlot(num);
|
|
@@ -6211,10 +6374,17 @@ async function handleClaudeAccountAdmin(req, res) {
|
|
|
6211
6374
|
// `only` names one account inside the pasted bundle, and is the only way
|
|
6212
6375
|
// `force` is honoured at all - see importAccount for why the pair is
|
|
6213
6376
|
// required rather than the flag alone.
|
|
6214
|
-
case "import":
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6377
|
+
case "import": {
|
|
6378
|
+
const out = await admin.importAccount(parsed.blob, {
|
|
6379
|
+
force: parsed.force === true,
|
|
6380
|
+
only: parsed.only ?? null,
|
|
6381
|
+
collect: !CHECKS_IMPORTS,
|
|
6382
|
+
});
|
|
6383
|
+
// Checked the way a LAN round's arrivals are (#1244): `cswap import`
|
|
6384
|
+
// exiting 0 on a Mac is not proof this process can read what it wrote.
|
|
6385
|
+
result = out?.ok ? { ...out, results: await admin.checkImportResults(out.results) } : out;
|
|
6386
|
+
break;
|
|
6387
|
+
}
|
|
6218
6388
|
case "remove": result = await admin.removeAccount(parsed.account); break;
|
|
6219
6389
|
// #721. Re-captures the active slot's credentials in place; see
|
|
6220
6390
|
// recaptureActive for why this is not a login and cannot become one.
|
|
@@ -6270,6 +6440,7 @@ function cswapAutoModule() {
|
|
|
6270
6440
|
const PINNED_MODULES = [
|
|
6271
6441
|
"self-update.mjs",
|
|
6272
6442
|
"claude-accounts.mjs",
|
|
6443
|
+
"account-health.mjs",
|
|
6273
6444
|
"cswap-admin.mjs",
|
|
6274
6445
|
"cswap-auto.mjs",
|
|
6275
6446
|
"quota.mjs",
|
|
@@ -6279,6 +6450,7 @@ const PINNED_MODULES = [
|
|
|
6279
6450
|
"browser-watch.mjs",
|
|
6280
6451
|
"browser-watch-store.mjs",
|
|
6281
6452
|
"claude-fm.mjs",
|
|
6453
|
+
"fm-station.mjs",
|
|
6282
6454
|
"lofi-girl.mjs",
|
|
6283
6455
|
"live-radio-mix.mjs",
|
|
6284
6456
|
"best-of-nostalgia.mjs",
|
|
@@ -7244,6 +7416,13 @@ const GUARDED_READS = new Set([
|
|
|
7244
7416
|
// Per-account, per-project token spend — the user's own work, the same class
|
|
7245
7417
|
// of secret as the accounts list it hangs off.
|
|
7246
7418
|
"/api/account-projects",
|
|
7419
|
+
// Not a secret, and here for the other reason a read can be dangerous: it is
|
|
7420
|
+
// the one route where the caller names what the deck goes and fetches
|
|
7421
|
+
// (#1208). fm-station.mjs holds that to YouTube, but a page on another site
|
|
7422
|
+
// still had a way to make this process download pages on demand, and the
|
|
7423
|
+
// only caller that needs it is the deck's own canvas, which sends
|
|
7424
|
+
// Sec-Fetch-Site: same-origin on every fetch.
|
|
7425
|
+
"/api/fm-station",
|
|
7247
7426
|
]);
|
|
7248
7427
|
|
|
7249
7428
|
function isAuthorizedMutation(req) {
|
|
@@ -7554,6 +7733,10 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
7554
7733
|
if (req.method === "GET" && url.pathname === "/api/hook-challenge") return handleHookChallenge(req, res, url);
|
|
7555
7734
|
if (req.method === "GET" && url.pathname === "/events") return handleSse(req, res);
|
|
7556
7735
|
if (req.method === "GET" && url.pathname === "/api/version") return guard(handleVersion(req, res), res);
|
|
7736
|
+
if (req.method === "GET" && url.pathname === "/api/desktop-update") return handleDesktopUpdateRead(req, res);
|
|
7737
|
+
if (req.method === "POST" && url.pathname === "/api/desktop-update") return guard(handleDesktopUpdateReport(req, res), res);
|
|
7738
|
+
if (req.method === "POST" && url.pathname === "/api/desktop-update/restart") return guard(handleDesktopUpdateRequest(req, res, "desktop-update-restart"), res);
|
|
7739
|
+
if (req.method === "POST" && url.pathname === "/api/desktop-update/seen") return guard(handleDesktopUpdateRequest(req, res, "desktop-update-seen"), res);
|
|
7557
7740
|
if (req.method === "POST" && url.pathname === "/api/upgrade") return guard(handleUpgrade(req, res), res);
|
|
7558
7741
|
if (req.method === "POST" && url.pathname === "/api/restart") return guard(handleRestart(req, res), res);
|
|
7559
7742
|
if (req.method === "POST" && url.pathname === "/api/presence") return guard(handlePresence(req, res), res);
|
|
@@ -7613,6 +7796,7 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
7613
7796
|
if (req.method === "GET" && url.pathname === "/api/cswap-auto") return guard(handleCswapAuto(req, res), res);
|
|
7614
7797
|
if (req.method === "POST" && url.pathname === "/api/cswap-auto") return guard(handleCswapAutoAction(req, res), res);
|
|
7615
7798
|
if (req.method === "GET" && url.pathname === "/api/claude-fm") return guard(handleClaudeFm(req, res), res);
|
|
7799
|
+
if (req.method === "GET" && url.pathname === "/api/fm-station") return guard(handleFmStation(req, res), res);
|
|
7616
7800
|
if (req.method === "GET" && url.pathname === "/api/lofi-girl") return guard(handleLofiGirl(req, res), res);
|
|
7617
7801
|
if (req.method === "GET" && url.pathname === "/api/live-radio-mix") return guard(handleLiveRadioMix(req, res), res);
|
|
7618
7802
|
if (req.method === "GET" && url.pathname === "/api/best-of-nostalgia") return guard(handleBestOfNostalgia(req, res), res);
|