accelr8 3.8.2 → 4.0.0-next.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 +298 -64
- package/dist/chunks/accounts-CCKXHSQQ.js +54 -0
- package/dist/chunks/{agentcli-ZI2O7ID7.js → agentcli-HL6V26E7.js} +6 -3
- package/dist/chunks/{app-SYLCS3QX.js → app-JMUXGMWK.js} +158 -21
- package/dist/chunks/authclient-AUBBFWCI.js +34 -0
- package/dist/chunks/authpage-X2AKCMVH.js +21 -0
- package/dist/chunks/chunk-3OTVRBTJ.js +265 -0
- package/dist/chunks/chunk-5QXGOLQ3.js +101 -0
- package/dist/chunks/chunk-62O2F73J.js +13 -0
- package/dist/chunks/chunk-CAQJPW54.js +211 -0
- package/dist/chunks/chunk-CUUNLVID.js +124 -0
- package/dist/chunks/chunk-EQPM4FIJ.js +419 -0
- package/dist/chunks/chunk-FVC2UPMV.js +306 -0
- package/dist/chunks/{chunk-3JNUWI22.js → chunk-NRXIFJGR.js} +3 -155
- package/dist/chunks/chunk-RZ4OBBLX.js +129 -0
- package/dist/chunks/chunk-UMPU6SAD.js +860 -0
- package/dist/chunks/{client-BKQLCKVT.js → client-UCMRJVMA.js} +1621 -437
- package/dist/chunks/livekit-client-bundle-HLYFRJB2.js +15 -0
- package/dist/chunks/livekit-terminal-22Q2NFB5.js +535 -0
- package/dist/chunks/livekit-window-XVOCBQOM.js +626 -0
- package/dist/chunks/server-A4T45QZ4.js +2707 -0
- package/dist/chunks/{update-UEPNGB6Y.js → update-RGDNBI4T.js} +25 -7
- package/dist/cli.js +132 -10
- package/package.json +10 -7
- package/dist/chunks/chunk-7JSMON7M.js +0 -64
- package/dist/chunks/chunk-HY6ULYGH.js +0 -559
- package/dist/chunks/server-IRLRECZ7.js +0 -1024
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
detectVoiceTools,
|
|
4
3
|
tzCity,
|
|
5
4
|
tzToCoords
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CUUNLVID.js";
|
|
6
|
+
import {
|
|
7
|
+
detectVoiceTools
|
|
8
|
+
} from "./chunk-UMPU6SAD.js";
|
|
7
9
|
import {
|
|
8
10
|
DEFAULT_SERVER,
|
|
9
11
|
VERSION
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-5QXGOLQ3.js";
|
|
13
|
+
import {
|
|
14
|
+
accelr8Dir
|
|
15
|
+
} from "./chunk-62O2F73J.js";
|
|
11
16
|
|
|
12
17
|
// src/radio/app.ts
|
|
13
18
|
import tk from "terminal-kit";
|
|
14
19
|
import fs from "node:fs";
|
|
15
|
-
import os from "node:os";
|
|
16
20
|
import path from "node:path";
|
|
17
21
|
import { WebSocket } from "ws";
|
|
18
22
|
|
|
@@ -5348,12 +5352,18 @@ function renderOverlay(ctx, opts) {
|
|
|
5348
5352
|
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
5349
5353
|
var SR = 44100;
|
|
5350
5354
|
var CH = 2;
|
|
5351
|
-
function nextAudioAction(sched, serverNowMs, mem, driftBudgetMs = 1500) {
|
|
5355
|
+
function nextAudioAction(sched, serverNowMs, mem, driftBudgetMs = 1500, stallMs = 4e3) {
|
|
5352
5356
|
const pos = nowPlaying(sched, serverNowMs);
|
|
5353
5357
|
if (!pos || !pos.item.url) return { kind: "none" };
|
|
5354
|
-
|
|
5358
|
+
const play = (reason) => ({ kind: "play", track: pos.item, offsetMs: pos.offsetMs, reason });
|
|
5359
|
+
if (pos.item.id !== mem.playingTrackId) return play(mem.playingTrackId ? "track" : "start");
|
|
5360
|
+
if (!mem.procAlive) return play("dead");
|
|
5361
|
+
if (mem.lastDataAtServerMs !== void 0) {
|
|
5362
|
+
const quietFor = serverNowMs - (mem.lastDataAtServerMs || mem.spawnedAtServerMs);
|
|
5363
|
+
if (quietFor > (mem.lastDataAtServerMs ? stallMs : stallMs * 2)) return play("stall");
|
|
5364
|
+
}
|
|
5355
5365
|
const implied = mem.spawnedOffsetMs + (serverNowMs - mem.spawnedAtServerMs);
|
|
5356
|
-
if (Math.abs(implied - pos.offsetMs) > driftBudgetMs) return
|
|
5366
|
+
if (Math.abs(implied - pos.offsetMs) > driftBudgetMs) return play("drift");
|
|
5357
5367
|
return { kind: "none" };
|
|
5358
5368
|
}
|
|
5359
5369
|
function applyGain(buf, fromGain, target, rampPerSample = 1 / 1400) {
|
|
@@ -5370,6 +5380,7 @@ function applyGain(buf, fromGain, target, rampPerSample = 1 / 1400) {
|
|
|
5370
5380
|
}
|
|
5371
5381
|
function decoderArgs(url, offsetMs) {
|
|
5372
5382
|
const args = ["-hide_banner", "-loglevel", "error", "-re"];
|
|
5383
|
+
if (/^https?:/i.test(url)) args.push("-rw_timeout", "8000000");
|
|
5373
5384
|
if (offsetMs > 0) args.push("-ss", (offsetMs / 1e3).toFixed(3));
|
|
5374
5385
|
args.push("-i", url, "-f", "s16le", "-ar", String(SR), "-ac", String(CH), "-");
|
|
5375
5386
|
return args;
|
|
@@ -5408,6 +5419,39 @@ function sinkArgs(ffplayPath) {
|
|
|
5408
5419
|
"-"
|
|
5409
5420
|
];
|
|
5410
5421
|
}
|
|
5422
|
+
function coreAudioSinkArgs(ffmpegPath) {
|
|
5423
|
+
return [
|
|
5424
|
+
"-hide_banner",
|
|
5425
|
+
"-loglevel",
|
|
5426
|
+
"error",
|
|
5427
|
+
"-f",
|
|
5428
|
+
"s16le",
|
|
5429
|
+
"-ar",
|
|
5430
|
+
String(SR),
|
|
5431
|
+
...ffplayStereoFlag(ffmpegPath),
|
|
5432
|
+
"-i",
|
|
5433
|
+
"-",
|
|
5434
|
+
"-f",
|
|
5435
|
+
"audiotoolbox",
|
|
5436
|
+
"-"
|
|
5437
|
+
];
|
|
5438
|
+
}
|
|
5439
|
+
var coreAudioCache = null;
|
|
5440
|
+
function hasCoreAudioSink(ffmpegPath) {
|
|
5441
|
+
if (coreAudioCache !== null) return coreAudioCache;
|
|
5442
|
+
coreAudioCache = false;
|
|
5443
|
+
if (process.platform !== "darwin") return false;
|
|
5444
|
+
try {
|
|
5445
|
+
const out = spawnSync(ffmpegPath, ["-hide_banner", "-devices"], { encoding: "utf8", timeout: 3e3 });
|
|
5446
|
+
coreAudioCache = /\baudiotoolbox\b/.test((out.stdout || "") + (out.stderr || ""));
|
|
5447
|
+
} catch {
|
|
5448
|
+
}
|
|
5449
|
+
return coreAudioCache;
|
|
5450
|
+
}
|
|
5451
|
+
function pickSink(ffmpegPath, ffplayPath, pref = "auto") {
|
|
5452
|
+
const core = pref === "coreaudio" || pref === "auto" && hasCoreAudioSink(ffmpegPath);
|
|
5453
|
+
return core ? { kind: "coreaudio", bin: ffmpegPath, args: coreAudioSinkArgs(ffmpegPath) } : { kind: "ffplay", bin: ffplayPath, args: sinkArgs(ffplayPath) };
|
|
5454
|
+
}
|
|
5411
5455
|
var SyncedAudio = class {
|
|
5412
5456
|
sink = null;
|
|
5413
5457
|
decoder = null;
|
|
@@ -5419,12 +5463,50 @@ var SyncedAudio = class {
|
|
|
5419
5463
|
target = 0.7;
|
|
5420
5464
|
// desired volume (0..1)
|
|
5421
5465
|
muted = false;
|
|
5466
|
+
sinkKind = "";
|
|
5467
|
+
lastFeedAt = 0;
|
|
5468
|
+
// wall ms of the last write to the sink (music or silence)
|
|
5469
|
+
written = 0;
|
|
5470
|
+
// bytes into the current sink, to keep whole s16 stereo frames
|
|
5471
|
+
keepalive = null;
|
|
5422
5472
|
start(opts) {
|
|
5423
5473
|
this.stop();
|
|
5424
5474
|
this.opts = opts;
|
|
5425
5475
|
this.startSink();
|
|
5426
5476
|
this.tick();
|
|
5427
5477
|
this.timer = setInterval(() => this.tick(), 1e3);
|
|
5478
|
+
this.keepalive = setInterval(() => {
|
|
5479
|
+
const quiet = Date.now() - this.lastFeedAt;
|
|
5480
|
+
if (quiet > 60) this.feed(Buffer.alloc(Math.floor(Math.min(quiet, 250) * SR / 1e3) * CH * 2));
|
|
5481
|
+
}, 40);
|
|
5482
|
+
}
|
|
5483
|
+
/** Every write to the sink goes through here. Pads to a whole 4-byte frame
|
|
5484
|
+
* first, so a decoder killed mid-frame can't shift the stream into noise. */
|
|
5485
|
+
feed(buf) {
|
|
5486
|
+
const sink = this.sink;
|
|
5487
|
+
if (!sink || !sink.stdin || sink.stdin.destroyed) return true;
|
|
5488
|
+
const pad = (4 - this.written % 4) % 4;
|
|
5489
|
+
if (pad && buf.length) {
|
|
5490
|
+
sink.stdin.write(Buffer.alloc(pad));
|
|
5491
|
+
this.written += pad;
|
|
5492
|
+
}
|
|
5493
|
+
this.written += buf.length;
|
|
5494
|
+
this.lastFeedAt = Date.now();
|
|
5495
|
+
return sink.stdin.write(buf);
|
|
5496
|
+
}
|
|
5497
|
+
/** Open a fresh sink, so it binds to whatever output is current NOW (the TV
|
|
5498
|
+
* while screen sharing, the speakers after). The old one is dropped first
|
|
5499
|
+
* so its exit handler doesn't treat this as a crash. */
|
|
5500
|
+
restartSink() {
|
|
5501
|
+
const old = this.sink;
|
|
5502
|
+
this.sink = null;
|
|
5503
|
+
this.startSink();
|
|
5504
|
+
if (old) {
|
|
5505
|
+
try {
|
|
5506
|
+
old.kill("SIGKILL");
|
|
5507
|
+
} catch {
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5428
5510
|
}
|
|
5429
5511
|
setSchedule(sched) {
|
|
5430
5512
|
if (this.opts) this.opts.schedule = sched;
|
|
@@ -5453,8 +5535,14 @@ var SyncedAudio = class {
|
|
|
5453
5535
|
}
|
|
5454
5536
|
startSink() {
|
|
5455
5537
|
if (!this.opts) return;
|
|
5456
|
-
const
|
|
5538
|
+
const env = process.env.ACCELR8_RADIO_SINK;
|
|
5539
|
+
const pref = env === "ffplay" || env === "coreaudio" ? env : this.opts.sink ?? "auto";
|
|
5540
|
+
const pick4 = pickSink(this.opts.ffmpegPath, this.opts.ffplayPath, pref);
|
|
5541
|
+
this.opts.onSink?.(pick4.kind);
|
|
5542
|
+
const sink = spawn2(pick4.bin, pick4.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
5457
5543
|
this.sink = sink;
|
|
5544
|
+
this.sinkKind = pick4.kind;
|
|
5545
|
+
this.written = 0;
|
|
5458
5546
|
sink.stdin?.on("error", () => {
|
|
5459
5547
|
});
|
|
5460
5548
|
sink.stdin?.on("drain", () => this.decoder?.stdout?.resume());
|
|
@@ -5476,22 +5564,24 @@ var SyncedAudio = class {
|
|
|
5476
5564
|
if (!this.opts) return;
|
|
5477
5565
|
if (!this.sink) this.startSink();
|
|
5478
5566
|
const action = nextAudioAction(this.opts.schedule, this.opts.serverNow(), this.mem);
|
|
5479
|
-
if (action.kind === "play")
|
|
5567
|
+
if (action.kind === "play") {
|
|
5568
|
+
if (action.reason !== "track") this.opts.onRespawn?.(action.reason, action.track, action.offsetMs);
|
|
5569
|
+
if (action.reason === "track" && this.sinkKind === "coreaudio") this.restartSink();
|
|
5570
|
+
this.spawnDecoder(action.track, action.offsetMs);
|
|
5571
|
+
}
|
|
5480
5572
|
}
|
|
5481
5573
|
spawnDecoder(track, offsetMs) {
|
|
5482
5574
|
if (!this.opts) return;
|
|
5483
5575
|
this.killDecoder();
|
|
5484
5576
|
const dec = spawn2(this.opts.ffmpegPath, decoderArgs(track.url, offsetMs), { stdio: ["ignore", "pipe", "ignore"] });
|
|
5485
5577
|
this.decoder = dec;
|
|
5486
|
-
this.mem = { playingTrackId: track.id, spawnedAtServerMs: this.opts.serverNow(), spawnedOffsetMs: offsetMs, procAlive: true };
|
|
5578
|
+
this.mem = { playingTrackId: track.id, spawnedAtServerMs: this.opts.serverNow(), spawnedOffsetMs: offsetMs, procAlive: true, lastDataAtServerMs: 0 };
|
|
5487
5579
|
this.opts.onTrack?.(track);
|
|
5488
5580
|
dec.stdout?.on("data", (chunk) => {
|
|
5489
5581
|
if (this.decoder !== dec) return;
|
|
5582
|
+
if (this.opts) this.mem.lastDataAtServerMs = this.opts.serverNow();
|
|
5490
5583
|
this.gain = applyGain(chunk, this.gain, this.effTarget());
|
|
5491
|
-
|
|
5492
|
-
if (!sink || !sink.stdin || sink.stdin.destroyed) return;
|
|
5493
|
-
const ok = sink.stdin.write(chunk);
|
|
5494
|
-
if (ok === false) dec.stdout?.pause();
|
|
5584
|
+
if (this.feed(chunk) === false) dec.stdout?.pause();
|
|
5495
5585
|
});
|
|
5496
5586
|
dec.on("error", () => {
|
|
5497
5587
|
if (this.decoder === dec) this.mem.procAlive = false;
|
|
@@ -5514,6 +5604,10 @@ var SyncedAudio = class {
|
|
|
5514
5604
|
clearInterval(this.timer);
|
|
5515
5605
|
this.timer = null;
|
|
5516
5606
|
}
|
|
5607
|
+
if (this.keepalive) {
|
|
5608
|
+
clearInterval(this.keepalive);
|
|
5609
|
+
this.keepalive = null;
|
|
5610
|
+
}
|
|
5517
5611
|
this.killDecoder();
|
|
5518
5612
|
if (this.sink) {
|
|
5519
5613
|
try {
|
|
@@ -5529,7 +5623,7 @@ var SyncedAudio = class {
|
|
|
5529
5623
|
// src/radio/app.ts
|
|
5530
5624
|
function rlog(msg) {
|
|
5531
5625
|
try {
|
|
5532
|
-
const dir =
|
|
5626
|
+
const dir = accelr8Dir();
|
|
5533
5627
|
fs.mkdirSync(dir, { recursive: true });
|
|
5534
5628
|
fs.appendFileSync(path.join(dir, "radio.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
5535
5629
|
`);
|
|
@@ -5550,6 +5644,7 @@ var frameH = 0;
|
|
|
5550
5644
|
var renderTimer = null;
|
|
5551
5645
|
var running = false;
|
|
5552
5646
|
var startedAt = Date.now();
|
|
5647
|
+
var VISUAL_EPOCH = Date.UTC(2026, 0, 1);
|
|
5553
5648
|
var skewMs = 0;
|
|
5554
5649
|
var serverNow = () => Date.now() + skewMs;
|
|
5555
5650
|
var W = () => Math.max(20, term.width || process.stdout.columns || 100);
|
|
@@ -5595,10 +5690,29 @@ function connectStation(opts) {
|
|
|
5595
5690
|
return;
|
|
5596
5691
|
}
|
|
5597
5692
|
stationWs = ws;
|
|
5693
|
+
let lastHeard = Date.now();
|
|
5694
|
+
const beat = setInterval(() => {
|
|
5695
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
5696
|
+
if (Date.now() - lastHeard > 25e3) {
|
|
5697
|
+
rlog("station socket silent 25s, reconnecting");
|
|
5698
|
+
ws.terminate();
|
|
5699
|
+
return;
|
|
5700
|
+
}
|
|
5701
|
+
try {
|
|
5702
|
+
ws.ping();
|
|
5703
|
+
} catch {
|
|
5704
|
+
}
|
|
5705
|
+
}, 1e4);
|
|
5706
|
+
ws.on("pong", () => {
|
|
5707
|
+
lastHeard = Date.now();
|
|
5708
|
+
});
|
|
5598
5709
|
ws.on("open", () => {
|
|
5710
|
+
lastHeard = Date.now();
|
|
5599
5711
|
ws.send(JSON.stringify({ t: "hello", id: me.id, name: me.name, tz: me.tz, color: me.color }));
|
|
5712
|
+
if (youVoted && skipTrackId) ws.send(JSON.stringify({ t: "voteSkip", trackId: skipTrackId }));
|
|
5600
5713
|
});
|
|
5601
5714
|
ws.on("message", (raw) => {
|
|
5715
|
+
lastHeard = Date.now();
|
|
5602
5716
|
let m;
|
|
5603
5717
|
try {
|
|
5604
5718
|
m = JSON.parse(raw.toString());
|
|
@@ -5608,6 +5722,7 @@ function connectStation(opts) {
|
|
|
5608
5722
|
if (m && m.t === "radioState") applyStation(m);
|
|
5609
5723
|
});
|
|
5610
5724
|
ws.on("close", () => {
|
|
5725
|
+
clearInterval(beat);
|
|
5611
5726
|
if (stationWs === ws) {
|
|
5612
5727
|
state = { ...state, connected: false };
|
|
5613
5728
|
scheduleReconnect(opts);
|
|
@@ -5615,7 +5730,7 @@ function connectStation(opts) {
|
|
|
5615
5730
|
});
|
|
5616
5731
|
ws.on("error", () => {
|
|
5617
5732
|
try {
|
|
5618
|
-
ws.
|
|
5733
|
+
ws.terminate();
|
|
5619
5734
|
} catch {
|
|
5620
5735
|
}
|
|
5621
5736
|
});
|
|
@@ -5638,8 +5753,14 @@ function applyStation(m) {
|
|
|
5638
5753
|
skipVotes = m.skip.votes ?? 0;
|
|
5639
5754
|
skipNeeded = m.skip.needed ?? 1;
|
|
5640
5755
|
if (m.skip.trackId && m.skip.trackId !== skipTrackId) {
|
|
5756
|
+
if (youVoted && skipTrackId) {
|
|
5757
|
+
skipNote = "skipped \u2713";
|
|
5758
|
+
skipNoteUntil = Date.now() + 4e3;
|
|
5759
|
+
}
|
|
5641
5760
|
skipTrackId = m.skip.trackId;
|
|
5642
5761
|
youVoted = false;
|
|
5762
|
+
} else if (Array.isArray(m.skip.voters) && Date.now() - lastToggleAt > 2e3) {
|
|
5763
|
+
youVoted = m.skip.voters.includes(me.id);
|
|
5643
5764
|
}
|
|
5644
5765
|
}
|
|
5645
5766
|
}
|
|
@@ -5668,7 +5789,14 @@ function startBackground(opts) {
|
|
|
5668
5789
|
}
|
|
5669
5790
|
if (tools.ffmpegPath && tools.ffplayPath) {
|
|
5670
5791
|
audio = new SyncedAudio();
|
|
5671
|
-
audio.start({
|
|
5792
|
+
audio.start({
|
|
5793
|
+
ffplayPath: tools.ffplayPath,
|
|
5794
|
+
ffmpegPath: tools.ffmpegPath,
|
|
5795
|
+
schedule: state.schedule,
|
|
5796
|
+
serverNow,
|
|
5797
|
+
onRespawn: (reason, track, offsetMs) => rlog(`audio respawn: ${reason} \xB7 ${track.id} @ ${(offsetMs / 1e3).toFixed(1)}s`),
|
|
5798
|
+
onSink: (kind) => rlog(`audio sink: ${kind}`)
|
|
5799
|
+
});
|
|
5672
5800
|
}
|
|
5673
5801
|
}
|
|
5674
5802
|
function makeScreen() {
|
|
@@ -5694,9 +5822,18 @@ var skipVotes = 0;
|
|
|
5694
5822
|
var skipNeeded = 1;
|
|
5695
5823
|
var youVoted = false;
|
|
5696
5824
|
var skipTrackId = "";
|
|
5825
|
+
var skipNote = "";
|
|
5826
|
+
var skipNoteUntil = 0;
|
|
5827
|
+
var lastToggleAt = 0;
|
|
5697
5828
|
function toggleSkip() {
|
|
5829
|
+
if (!stationWs || stationWs.readyState !== WebSocket.OPEN) {
|
|
5830
|
+
skipNote = "offline, retrying";
|
|
5831
|
+
skipNoteUntil = Date.now() + 3e3;
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5698
5834
|
youVoted = !youVoted;
|
|
5699
|
-
|
|
5835
|
+
lastToggleAt = Date.now();
|
|
5836
|
+
stationWs.send(JSON.stringify({ t: youVoted ? "voteSkip" : "unvoteSkip", trackId: skipTrackId || void 0 }));
|
|
5700
5837
|
}
|
|
5701
5838
|
function drawControls() {
|
|
5702
5839
|
const w = screen.width, y = 1;
|
|
@@ -5708,7 +5845,7 @@ function drawControls() {
|
|
|
5708
5845
|
const vol = audio ? audio.isMuted ? "MUTED" : `vol ${audio.volume}%` : "no audio";
|
|
5709
5846
|
put2(1, `\u266A ${vol} [m] mute [+/-] volume [q] quit`);
|
|
5710
5847
|
const met = skipVotes >= skipNeeded && skipVotes > 0;
|
|
5711
|
-
const label = youVoted ? "[s] voted \u2713" : "[s] vote skip";
|
|
5848
|
+
const label = Date.now() < skipNoteUntil ? `[s] ${skipNote}` : youVoted ? "[s] voted \u2713" : "[s] vote skip";
|
|
5712
5849
|
const right = `${label} skip ${skipVotes}/${skipNeeded} `;
|
|
5713
5850
|
put2(w - right.length, right, met ? C_GOOD : 231, youVoted);
|
|
5714
5851
|
}
|
|
@@ -5743,7 +5880,7 @@ var renderErrs = 0;
|
|
|
5743
5880
|
function renderFrame() {
|
|
5744
5881
|
if (!running) return;
|
|
5745
5882
|
try {
|
|
5746
|
-
const tMs =
|
|
5883
|
+
const tMs = serverNow() - VISUAL_EPOCH;
|
|
5747
5884
|
if (visuals) {
|
|
5748
5885
|
lastFrame = visuals.frame(tMs, screen.width, screen.height);
|
|
5749
5886
|
frameW = screen.width;
|
|
@@ -5810,7 +5947,7 @@ function runRadio(opts) {
|
|
|
5810
5947
|
const detail = err instanceof Error ? err.stack || err.message : String(err);
|
|
5811
5948
|
rlog(`CRASH: ${detail}`);
|
|
5812
5949
|
teardown();
|
|
5813
|
-
console.error("\naccelr8 radio crashed:\n" + detail + "\n\nlog: " + path.join(
|
|
5950
|
+
console.error("\naccelr8 radio crashed:\n" + detail + "\n\nlog: " + path.join(accelr8Dir(), "radio.log") + "\n(press any key to close)");
|
|
5814
5951
|
try {
|
|
5815
5952
|
process.stdin.setRawMode?.(true);
|
|
5816
5953
|
process.stdin.resume();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AuthNetworkError,
|
|
4
|
+
approveSignIn,
|
|
5
|
+
approveUrl,
|
|
6
|
+
deviceLogin,
|
|
7
|
+
ensureSignedIn,
|
|
8
|
+
getAccessToken,
|
|
9
|
+
httpBase,
|
|
10
|
+
legacyProofs,
|
|
11
|
+
logout,
|
|
12
|
+
olderServerMessage,
|
|
13
|
+
pendingSignIn,
|
|
14
|
+
refreshCredentials,
|
|
15
|
+
whoami
|
|
16
|
+
} from "./chunk-3OTVRBTJ.js";
|
|
17
|
+
import "./chunk-FVC2UPMV.js";
|
|
18
|
+
import "./chunk-5QXGOLQ3.js";
|
|
19
|
+
import "./chunk-62O2F73J.js";
|
|
20
|
+
export {
|
|
21
|
+
AuthNetworkError,
|
|
22
|
+
approveSignIn,
|
|
23
|
+
approveUrl,
|
|
24
|
+
deviceLogin,
|
|
25
|
+
ensureSignedIn,
|
|
26
|
+
getAccessToken,
|
|
27
|
+
httpBase,
|
|
28
|
+
legacyProofs,
|
|
29
|
+
logout,
|
|
30
|
+
olderServerMessage,
|
|
31
|
+
pendingSignIn,
|
|
32
|
+
refreshCredentials,
|
|
33
|
+
whoami
|
|
34
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DIFFERENT_NETWORK,
|
|
4
|
+
ago,
|
|
5
|
+
confirmPage,
|
|
6
|
+
deniedPage,
|
|
7
|
+
donePage,
|
|
8
|
+
enterCodePage,
|
|
9
|
+
esc,
|
|
10
|
+
platformName
|
|
11
|
+
} from "./chunk-RZ4OBBLX.js";
|
|
12
|
+
export {
|
|
13
|
+
DIFFERENT_NETWORK,
|
|
14
|
+
ago,
|
|
15
|
+
confirmPage,
|
|
16
|
+
deniedPage,
|
|
17
|
+
donePage,
|
|
18
|
+
enterCodePage,
|
|
19
|
+
esc,
|
|
20
|
+
platformName
|
|
21
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AuthLockTimeout,
|
|
4
|
+
credentialsKey,
|
|
5
|
+
deleteCredentials,
|
|
6
|
+
loadCredentials,
|
|
7
|
+
loadLegacyIdentity,
|
|
8
|
+
loadTokens,
|
|
9
|
+
saveCredentials,
|
|
10
|
+
withAuthLock
|
|
11
|
+
} from "./chunk-FVC2UPMV.js";
|
|
12
|
+
import {
|
|
13
|
+
PORT,
|
|
14
|
+
VERSION
|
|
15
|
+
} from "./chunk-5QXGOLQ3.js";
|
|
16
|
+
|
|
17
|
+
// src/authclient.ts
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
var sha256hex = (s) => createHash("sha256").update(s).digest("hex");
|
|
21
|
+
function legacyProofs(serverUrl, baseDir) {
|
|
22
|
+
const server = credentialsKey(serverUrl);
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const [key, token] of Object.entries(loadTokens(baseDir))) {
|
|
25
|
+
const i = key.indexOf(":");
|
|
26
|
+
if (i <= 0 || !token) continue;
|
|
27
|
+
out.push({ projectId: key.slice(0, i), handle: key.slice(i + 1), proof: sha256hex(sha256hex(token) + "\n" + server) });
|
|
28
|
+
if (out.length >= 20) break;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
var REFRESH_EARLY_MS = 6e4;
|
|
33
|
+
function httpBase(serverUrl) {
|
|
34
|
+
const key = credentialsKey(serverUrl);
|
|
35
|
+
return key.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
36
|
+
}
|
|
37
|
+
function approveUrl(serverUrl) {
|
|
38
|
+
return httpBase(serverUrl) + "/approve";
|
|
39
|
+
}
|
|
40
|
+
var AuthNetworkError = class extends Error {
|
|
41
|
+
};
|
|
42
|
+
async function call(url, init = {}) {
|
|
43
|
+
let res;
|
|
44
|
+
try {
|
|
45
|
+
res = await fetch(url, {
|
|
46
|
+
method: init.method ?? (init.body === void 0 ? "GET" : "POST"),
|
|
47
|
+
headers: {
|
|
48
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" },
|
|
49
|
+
...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
|
|
50
|
+
},
|
|
51
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
|
|
52
|
+
signal: AbortSignal.timeout(15e3)
|
|
53
|
+
});
|
|
54
|
+
} catch (e) {
|
|
55
|
+
throw new AuthNetworkError(`could not reach ${new URL(url).origin}: ${e instanceof Error ? e.message : e}`);
|
|
56
|
+
}
|
|
57
|
+
let data = null;
|
|
58
|
+
try {
|
|
59
|
+
data = await res.json();
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
return { status: res.status, data };
|
|
63
|
+
}
|
|
64
|
+
function toCredentials(serverUrl, r, now = Date.now()) {
|
|
65
|
+
return {
|
|
66
|
+
server: credentialsKey(serverUrl),
|
|
67
|
+
user: { id: r.user.id, name: r.user.name, color: r.user.color },
|
|
68
|
+
sessionId: r.sessionId,
|
|
69
|
+
accessToken: r.accessToken,
|
|
70
|
+
accessExpiresAt: now + r.accessExpiresIn * 1e3,
|
|
71
|
+
refreshToken: r.refreshToken,
|
|
72
|
+
refreshExpiresAt: now + r.refreshExpiresIn * 1e3,
|
|
73
|
+
signedInAt: now
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
var isTokens = (d) => !!d && typeof d.accessToken === "string" && typeof d.refreshToken === "string" && !!d.user;
|
|
77
|
+
function defaultName() {
|
|
78
|
+
try {
|
|
79
|
+
return os.userInfo().username;
|
|
80
|
+
} catch {
|
|
81
|
+
return process.env.USER || "guest";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var OLDER_SERVER = /* @__PURE__ */ new Set([404, 405, 426]);
|
|
85
|
+
function olderServerMessage(serverUrl) {
|
|
86
|
+
return [
|
|
87
|
+
`${credentialsKey(serverUrl)} runs an older accelr8 with no sign-in yet, so this version can't use it.`,
|
|
88
|
+
"To try this version, start your own server in another terminal:",
|
|
89
|
+
` accelr8 serve --host 127.0.0.1`,
|
|
90
|
+
"then open accelr8 on it:",
|
|
91
|
+
` accelr8 --server ws://127.0.0.1:${PORT}`
|
|
92
|
+
].join("\n ");
|
|
93
|
+
}
|
|
94
|
+
async function deviceLogin(serverUrl, opts = {}) {
|
|
95
|
+
const print = opts.print ?? ((l) => console.log(l));
|
|
96
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
97
|
+
const base = httpBase(serverUrl);
|
|
98
|
+
const legacy = loadLegacyIdentity(opts.baseDir);
|
|
99
|
+
const startRes = await call(`${base}/auth/device`, {
|
|
100
|
+
body: {
|
|
101
|
+
machine: (opts.machine ?? os.hostname()).slice(0, 64),
|
|
102
|
+
platform: process.platform,
|
|
103
|
+
name: opts.name || legacy.name || defaultName(),
|
|
104
|
+
legacyId: legacy.id,
|
|
105
|
+
legacyName: legacy.name,
|
|
106
|
+
// old ids were public: the old projects come along only with proof
|
|
107
|
+
legacyProof: legacy.id ? legacyProofs(serverUrl, opts.baseDir) : void 0,
|
|
108
|
+
version: VERSION
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (startRes.status !== 200 || typeof startRes.data?.deviceCode !== "string") {
|
|
112
|
+
if (!startRes.data?.message && OLDER_SERVER.has(startRes.status)) throw new Error(olderServerMessage(serverUrl));
|
|
113
|
+
throw new Error(startRes.data?.message || `the server refused to start a sign-in (HTTP ${startRes.status})`);
|
|
114
|
+
}
|
|
115
|
+
const start = startRes.data;
|
|
116
|
+
print("");
|
|
117
|
+
print(` To sign in, open this page in your browser:`);
|
|
118
|
+
print(` ${approveUrl(serverUrl)}`);
|
|
119
|
+
print(` and type this code:`);
|
|
120
|
+
print(` ${start.userCode}`);
|
|
121
|
+
print("");
|
|
122
|
+
print(` Waiting for you to approve it. The code expires in ${Math.round(start.expiresIn / 60)} minutes.`);
|
|
123
|
+
await opts.onCode?.(start);
|
|
124
|
+
let interval = Math.max(1, start.interval) * 1e3;
|
|
125
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
126
|
+
for (; ; ) {
|
|
127
|
+
const r = await call(`${base}/auth/device/token`, { body: { deviceCode: start.deviceCode } });
|
|
128
|
+
if (r.status === 200 && isTokens(r.data)) {
|
|
129
|
+
const creds = toCredentials(serverUrl, r.data);
|
|
130
|
+
saveCredentials(creds, opts.baseDir);
|
|
131
|
+
return creds;
|
|
132
|
+
}
|
|
133
|
+
if (r.status === 200 && r.data?.pending) {
|
|
134
|
+
} else if (r.data?.error === "slow_down") interval += 5e3;
|
|
135
|
+
else throw new Error(r.data?.message || `sign-in failed (HTTP ${r.status})`);
|
|
136
|
+
if (Date.now() + interval > deadline) throw new Error("The code expired. Run accelr8 login again for a new one.");
|
|
137
|
+
await sleep(interval);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function refreshCredentials(serverUrl, opts = {}) {
|
|
141
|
+
const before = loadCredentials(serverUrl, opts.baseDir);
|
|
142
|
+
try {
|
|
143
|
+
return await withAuthLock(async () => {
|
|
144
|
+
const creds = loadCredentials(serverUrl, opts.baseDir);
|
|
145
|
+
if (!creds) return null;
|
|
146
|
+
if (!opts.force && creds.accessExpiresAt - Date.now() > REFRESH_EARLY_MS) return creds;
|
|
147
|
+
if (opts.force && before && creds.refreshToken !== before.refreshToken) return creds;
|
|
148
|
+
const r = await call(`${httpBase(serverUrl)}/auth/refresh`, { body: { refreshToken: creds.refreshToken } });
|
|
149
|
+
if (r.status === 200 && isTokens(r.data)) {
|
|
150
|
+
const next = { ...toCredentials(serverUrl, r.data), signedInAt: creds.signedInAt };
|
|
151
|
+
saveCredentials(next, opts.baseDir);
|
|
152
|
+
return next;
|
|
153
|
+
}
|
|
154
|
+
if (r.data?.error === "invalid_grant") {
|
|
155
|
+
deleteCredentials(serverUrl, opts.baseDir, (saved) => saved.refreshToken === creds.refreshToken);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
throw new AuthNetworkError(`refresh failed (HTTP ${r.status})`);
|
|
159
|
+
}, opts.baseDir, opts.lock);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
if (!(e instanceof AuthLockTimeout)) throw e;
|
|
162
|
+
const now = loadCredentials(serverUrl, opts.baseDir);
|
|
163
|
+
if (now && (!before || now.refreshToken !== before.refreshToken)) return now;
|
|
164
|
+
throw new AuthNetworkError("another accelr8 process is refreshing this sign-in; trying again shortly");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function getAccessToken(serverUrl, opts = {}) {
|
|
168
|
+
const creds = loadCredentials(serverUrl, opts.baseDir);
|
|
169
|
+
if (!creds) return null;
|
|
170
|
+
if (!opts.force && creds.accessExpiresAt - Date.now() > REFRESH_EARLY_MS) return { token: creds.accessToken, creds };
|
|
171
|
+
try {
|
|
172
|
+
const next = await refreshCredentials(serverUrl, opts);
|
|
173
|
+
return next ? { token: next.accessToken, creds: next } : null;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (e instanceof AuthNetworkError) return { token: creds.accessToken, creds };
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function logout(serverUrl, opts = {}) {
|
|
180
|
+
const creds = loadCredentials(serverUrl, opts.baseDir);
|
|
181
|
+
if (!creds) return { wasSignedIn: false, revoked: null };
|
|
182
|
+
let revoked = null;
|
|
183
|
+
try {
|
|
184
|
+
const r = await call(`${httpBase(serverUrl)}/auth/logout`, { body: { refreshToken: creds.refreshToken, all: opts.all === true }, bearer: creds.accessToken });
|
|
185
|
+
if (r.status === 200) revoked = Number(r.data?.revoked ?? 0);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
deleteCredentials(serverUrl, opts.baseDir);
|
|
189
|
+
return { wasSignedIn: true, user: creds.user, revoked };
|
|
190
|
+
}
|
|
191
|
+
async function whoami(serverUrl, opts = {}) {
|
|
192
|
+
const local = loadCredentials(serverUrl, opts.baseDir);
|
|
193
|
+
if (!local) return null;
|
|
194
|
+
let tok;
|
|
195
|
+
try {
|
|
196
|
+
tok = await getAccessToken(serverUrl, opts);
|
|
197
|
+
} catch {
|
|
198
|
+
tok = { token: local.accessToken, creds: local };
|
|
199
|
+
}
|
|
200
|
+
if (!tok) return null;
|
|
201
|
+
try {
|
|
202
|
+
let r = await call(`${httpBase(serverUrl)}/auth/me`, { bearer: tok.token });
|
|
203
|
+
if (r.status === 401) {
|
|
204
|
+
const again = await getAccessToken(serverUrl, { ...opts, force: true });
|
|
205
|
+
if (!again) return null;
|
|
206
|
+
tok = again;
|
|
207
|
+
r = await call(`${httpBase(serverUrl)}/auth/me`, { bearer: tok.token });
|
|
208
|
+
}
|
|
209
|
+
if (r.status === 200 && r.data?.user) {
|
|
210
|
+
const user = r.data.user;
|
|
211
|
+
if (user.name !== tok.creds.user.name || user.color !== tok.creds.user.color) {
|
|
212
|
+
saveCredentials({ ...tok.creds, user: { id: user.id, name: user.name, color: user.color } }, opts.baseDir);
|
|
213
|
+
}
|
|
214
|
+
return { server: tok.creds.server, user, machine: r.data.session?.machine, since: r.data.session?.createdAt, verified: true };
|
|
215
|
+
}
|
|
216
|
+
return { server: tok.creds.server, user: tok.creds.user, since: tok.creds.signedInAt, verified: false, note: `server answered HTTP ${r.status}` };
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { server: tok.creds.server, user: tok.creds.user, since: tok.creds.signedInAt, verified: false, note: e instanceof Error ? e.message : String(e) };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function signedInCall(serverUrl, route, body, baseDir) {
|
|
222
|
+
const tok = await getAccessToken(serverUrl, { baseDir });
|
|
223
|
+
if (!tok) throw new Error(`This machine is not signed in to ${credentialsKey(serverUrl)}. Run accelr8 login first.`);
|
|
224
|
+
return call(`${httpBase(serverUrl)}${route}`, { body, bearer: tok.token });
|
|
225
|
+
}
|
|
226
|
+
async function pendingSignIn(serverUrl, userCode, opts = {}) {
|
|
227
|
+
const r = await signedInCall(serverUrl, "/auth/device/pending", { userCode }, opts.baseDir);
|
|
228
|
+
if (r.status !== 200 || typeof r.data?.machine !== "string") throw new Error(r.data?.message || `the server refused (HTTP ${r.status})`);
|
|
229
|
+
return r.data;
|
|
230
|
+
}
|
|
231
|
+
async function approveSignIn(serverUrl, userCode, opts = {}) {
|
|
232
|
+
const r = await signedInCall(serverUrl, "/auth/device/approve", { userCode }, opts.baseDir);
|
|
233
|
+
if (r.status !== 200 || !r.data?.user) throw new Error(r.data?.message || `the server refused (HTTP ${r.status})`);
|
|
234
|
+
return r.data.user;
|
|
235
|
+
}
|
|
236
|
+
async function ensureSignedIn(serverUrl, opts = {}) {
|
|
237
|
+
const have = loadCredentials(serverUrl, opts.baseDir);
|
|
238
|
+
if (have) return have;
|
|
239
|
+
const print = opts.print ?? ((l) => console.log(l));
|
|
240
|
+
print(`You are not signed in to ${credentialsKey(serverUrl)} yet.`);
|
|
241
|
+
try {
|
|
242
|
+
const creds = await deviceLogin(serverUrl, opts);
|
|
243
|
+
print(` Signed in as ${creds.user.name}. Opening accelr8...`);
|
|
244
|
+
return creds;
|
|
245
|
+
} catch (e) {
|
|
246
|
+
print(` ${e instanceof Error ? e.message : e}`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export {
|
|
252
|
+
legacyProofs,
|
|
253
|
+
httpBase,
|
|
254
|
+
approveUrl,
|
|
255
|
+
AuthNetworkError,
|
|
256
|
+
olderServerMessage,
|
|
257
|
+
deviceLogin,
|
|
258
|
+
refreshCredentials,
|
|
259
|
+
getAccessToken,
|
|
260
|
+
logout,
|
|
261
|
+
whoami,
|
|
262
|
+
pendingSignIn,
|
|
263
|
+
approveSignIn,
|
|
264
|
+
ensureSignedIn
|
|
265
|
+
};
|