accelr8 3.8.1 → 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-7YGYMFSZ.js → app-JMUXGMWK.js} +272 -75
- 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-3RLQY3ZB.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-HY6ULYGH.js +0 -559
- package/dist/chunks/chunk-UTWXIX5N.js +0 -64
- package/dist/chunks/server-V56IHFUH.js +0 -1024
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/protocol.ts
|
|
4
|
+
var VERSION = true ? "4.0.0-next.1" : "4.0.0-next.1";
|
|
5
|
+
var PROTO = 4;
|
|
6
|
+
var PORT = 8787;
|
|
7
|
+
var USER_COLORS = [39, 213, 220, 46, 208, 141, 51, 203, 118, 165];
|
|
8
|
+
function colorForId(id) {
|
|
9
|
+
let h = 0;
|
|
10
|
+
for (const ch of id) h = h * 31 + ch.charCodeAt(0) >>> 0;
|
|
11
|
+
return USER_COLORS[h % USER_COLORS.length];
|
|
12
|
+
}
|
|
13
|
+
var DEFAULT_SERVER = "wss://accelr8-canvas.patonchain.workers.dev";
|
|
14
|
+
var CANVAS_W = 100;
|
|
15
|
+
var CANVAS_H = 30;
|
|
16
|
+
var LIMITS = {
|
|
17
|
+
chatMaxCp: 4e3,
|
|
18
|
+
// code points per chat message
|
|
19
|
+
chatMaxJson: 8192,
|
|
20
|
+
// serialized bytes per chat message
|
|
21
|
+
chatPerWindow: 10,
|
|
22
|
+
// messages per 10s per session
|
|
23
|
+
chatTail: 50,
|
|
24
|
+
// messages delivered in `joined`
|
|
25
|
+
chatPage: 50,
|
|
26
|
+
// default history page (max 100)
|
|
27
|
+
chatSyncMaxGap: 200,
|
|
28
|
+
// bigger gap -> full resync
|
|
29
|
+
chatRetain: 2e3,
|
|
30
|
+
// messages kept per project
|
|
31
|
+
draftMinIntervalMs: 100,
|
|
32
|
+
// server drops faster arrivals
|
|
33
|
+
draftMaxCp: 4e3,
|
|
34
|
+
// Frame budgets, per connection, per 10s window. cursor and paint have their
|
|
35
|
+
// own budgets because they are continuous by nature (the client sends cursor
|
|
36
|
+
// at 25/s, and one paint per cell while dragging): over budget they are
|
|
37
|
+
// DROPPED, never a disconnect. So do the executor frames an owner's terminal
|
|
38
|
+
// streams back (agentAccept/Delta/Done/Error: each reply sends up to 10
|
|
39
|
+
// deltas a second, and one terminal may run several replies at once): over
|
|
40
|
+
// budget a delta is dropped, and the frames that start or end a reply always
|
|
41
|
+
// pass. Everything else shares framesPerWindow, and a connection over
|
|
42
|
+
// hardFramesPerWindow in total is closed as a flood.
|
|
43
|
+
framesPerWindow: 200,
|
|
44
|
+
// other frames per 10s -> disconnect
|
|
45
|
+
cursorPerWindow: 400,
|
|
46
|
+
// cursor frames per 10s (client sends <= 250) -> drop extras
|
|
47
|
+
paintPerWindow: 1500,
|
|
48
|
+
// paint frames per 10s (fast drags burst) -> drop extras
|
|
49
|
+
execPerWindow: 1e3,
|
|
50
|
+
// executor frames per 10s (10 replies at once) -> drop extra deltas
|
|
51
|
+
hardFramesPerWindow: 4e3,
|
|
52
|
+
// all non-voice frames per 10s -> disconnect
|
|
53
|
+
invokesPerProjectMin: 20,
|
|
54
|
+
triggersPerUserMin: 6,
|
|
55
|
+
// messages that @mention an agent, per user per minute
|
|
56
|
+
// Wrong join codes. A code is the only way into a project, so guesses are
|
|
57
|
+
// limited three ways: per account, per network (an IPv4 address or an IPv6
|
|
58
|
+
// /64; a network that keeps hitting it is locked out for 10, 20, 40...
|
|
59
|
+
// minutes, up to 6 hours), and server-wide. Right codes are never counted.
|
|
60
|
+
joinGuessesPerMin: 5,
|
|
61
|
+
// per signed-in user (was per connection)
|
|
62
|
+
joinGuessesPerNet: 10,
|
|
63
|
+
// per network per 10 minutes
|
|
64
|
+
joinGuessesServerPerMin: 60,
|
|
65
|
+
// all wrong codes on the server, per minute
|
|
66
|
+
joinCodeLength: 8,
|
|
67
|
+
// 32 symbols, uniform: 40 bits
|
|
68
|
+
renamesPerHour: 10,
|
|
69
|
+
revealsPerMin: 10,
|
|
70
|
+
secretsPerProject: 50,
|
|
71
|
+
secretValueMax: 8192,
|
|
72
|
+
agentQueueCap: 5,
|
|
73
|
+
agentMaxDepth: 3,
|
|
74
|
+
agentBudgetPerTrigger: 5,
|
|
75
|
+
agentRepeatPerChain: 2,
|
|
76
|
+
voiceFramesPerSec: 50,
|
|
77
|
+
// ~25/s real, 2x headroom; exempt from framesPerWindow
|
|
78
|
+
voiceFrameMaxB64: 4096
|
|
79
|
+
// a 40ms s16le mono chunk is ~1.7KB base64; cap slack
|
|
80
|
+
};
|
|
81
|
+
var DEFAULT_ACCESS = { mode: "owner", users: [] };
|
|
82
|
+
function encode(msg) {
|
|
83
|
+
return JSON.stringify(msg);
|
|
84
|
+
}
|
|
85
|
+
function decode(data) {
|
|
86
|
+
return JSON.parse(data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
VERSION,
|
|
91
|
+
PROTO,
|
|
92
|
+
PORT,
|
|
93
|
+
colorForId,
|
|
94
|
+
DEFAULT_SERVER,
|
|
95
|
+
CANVAS_W,
|
|
96
|
+
CANVAS_H,
|
|
97
|
+
LIMITS,
|
|
98
|
+
DEFAULT_ACCESS,
|
|
99
|
+
encode,
|
|
100
|
+
decode
|
|
101
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/home.ts
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
function accelr8Dir() {
|
|
7
|
+
const override = process.env.ACCELR8_HOME;
|
|
8
|
+
return override && override.trim() ? path.resolve(override) : path.join(os.homedir(), ".accelr8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
accelr8Dir
|
|
13
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/livekit-token.ts
|
|
4
|
+
function isVoiceUrl(url) {
|
|
5
|
+
try {
|
|
6
|
+
const u = new URL(url);
|
|
7
|
+
return ["ws:", "wss:", "http:", "https:"].includes(u.protocol) && !!u.hostname && !u.username && !u.password;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function parseVoiceIdentity(identity) {
|
|
13
|
+
const m = /^sess:([A-Za-z0-9_.@-]{1,128})$/.exec(identity);
|
|
14
|
+
return m ? { sid: m[1] } : null;
|
|
15
|
+
}
|
|
16
|
+
function sanitizeVoiceName(v, maxCp = 32) {
|
|
17
|
+
let out = "";
|
|
18
|
+
let n = 0;
|
|
19
|
+
for (const ch of String(v ?? "")) {
|
|
20
|
+
if (n >= maxCp) break;
|
|
21
|
+
const cp = ch.codePointAt(0);
|
|
22
|
+
if (cp < 32 || cp >= 127 && cp <= 159) continue;
|
|
23
|
+
if (cp >= 8203 && cp <= 8207 || cp >= 8234 && cp <= 8238 || cp >= 8294 && cp <= 8297 || cp === 65279) continue;
|
|
24
|
+
out += ch;
|
|
25
|
+
n++;
|
|
26
|
+
}
|
|
27
|
+
return out.trim() || "guest";
|
|
28
|
+
}
|
|
29
|
+
function decodeVoiceMetadata(raw) {
|
|
30
|
+
if (typeof raw !== "string" || !raw || raw.length > 2048) return null;
|
|
31
|
+
try {
|
|
32
|
+
const o = JSON.parse(raw);
|
|
33
|
+
if (!o || typeof o !== "object" || o.v !== 1) return null;
|
|
34
|
+
return {
|
|
35
|
+
v: 1,
|
|
36
|
+
sid: typeof o.sid === "string" ? o.sid.slice(0, 128) : "",
|
|
37
|
+
userId: typeof o.userId === "string" ? o.userId.slice(0, 64) : "",
|
|
38
|
+
name: sanitizeVoiceName(o.name),
|
|
39
|
+
color: Number.isInteger(o.color) ? o.color : 7,
|
|
40
|
+
client: o.client === "web" ? "web" : "tui"
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var enc = new TextEncoder();
|
|
47
|
+
|
|
48
|
+
// src/livekit-core.ts
|
|
49
|
+
function initialVoiceState(room = "", identity = "") {
|
|
50
|
+
return {
|
|
51
|
+
connection: "connecting",
|
|
52
|
+
room,
|
|
53
|
+
identity,
|
|
54
|
+
muted: true,
|
|
55
|
+
participants: [],
|
|
56
|
+
inputDeviceId: null,
|
|
57
|
+
playbackBlocked: false,
|
|
58
|
+
needsClick: false,
|
|
59
|
+
error: null,
|
|
60
|
+
endReason: null
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var CONNECTIONS = ["connecting", "connected", "reconnecting", "disconnected"];
|
|
64
|
+
var REASONS = ["left", "replaced", "removed", "room_closed", "network", "error", "window_closed"];
|
|
65
|
+
function normalizeVoiceState(raw) {
|
|
66
|
+
if (!raw || typeof raw !== "object") return {};
|
|
67
|
+
const r = raw;
|
|
68
|
+
const out = {};
|
|
69
|
+
if (typeof r.connection === "string" && CONNECTIONS.includes(r.connection)) out.connection = r.connection;
|
|
70
|
+
if (typeof r.room === "string") out.room = r.room.slice(0, 256);
|
|
71
|
+
if (typeof r.identity === "string") out.identity = r.identity.slice(0, 256);
|
|
72
|
+
if (typeof r.muted === "boolean") out.muted = r.muted;
|
|
73
|
+
if (typeof r.playbackBlocked === "boolean") out.playbackBlocked = r.playbackBlocked;
|
|
74
|
+
if (typeof r.needsClick === "boolean") out.needsClick = r.needsClick;
|
|
75
|
+
if (r.inputDeviceId === null || typeof r.inputDeviceId === "string") out.inputDeviceId = r.inputDeviceId === null ? null : r.inputDeviceId.slice(0, 256);
|
|
76
|
+
if (r.error === null || typeof r.error === "string") out.error = r.error === null ? null : sanitizeText(r.error, 200);
|
|
77
|
+
if (r.endReason === null || typeof r.endReason === "string" && REASONS.includes(r.endReason)) out.endReason = r.endReason ?? null;
|
|
78
|
+
if (Array.isArray(r.participants)) {
|
|
79
|
+
out.participants = r.participants.slice(0, 100).flatMap((p) => {
|
|
80
|
+
if (!p || typeof p !== "object") return [];
|
|
81
|
+
const q = p;
|
|
82
|
+
if (typeof q.identity !== "string" || !q.identity) return [];
|
|
83
|
+
return [{
|
|
84
|
+
identity: q.identity.slice(0, 256),
|
|
85
|
+
sid: typeof q.sid === "string" ? q.sid.slice(0, 128) : null,
|
|
86
|
+
userId: typeof q.userId === "string" ? q.userId.slice(0, 64) : null,
|
|
87
|
+
name: sanitizeVoiceName(q.name),
|
|
88
|
+
color: Number.isInteger(q.color) ? q.color : null,
|
|
89
|
+
client: q.client === "web" ? "web" : q.client === "tui" ? "tui" : null,
|
|
90
|
+
isLocal: q.isLocal === true,
|
|
91
|
+
muted: q.muted !== false,
|
|
92
|
+
speaking: q.speaking === true
|
|
93
|
+
}];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function sanitizeText(s, max) {
|
|
99
|
+
let out = "";
|
|
100
|
+
for (const ch of s) {
|
|
101
|
+
const cp = ch.codePointAt(0);
|
|
102
|
+
if (cp < 32 || cp >= 127 && cp <= 159 || cp >= 8234 && cp <= 8238 || cp >= 8294 && cp <= 8297) continue;
|
|
103
|
+
out += ch;
|
|
104
|
+
if (out.length >= max) break;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function sortParticipants(list) {
|
|
109
|
+
return [...list].sort((a, b) => a.isLocal !== b.isLocal ? a.isLocal ? -1 : 1 : a.name.localeCompare(b.name) || a.identity.localeCompare(b.identity));
|
|
110
|
+
}
|
|
111
|
+
var VoiceStore = class {
|
|
112
|
+
state;
|
|
113
|
+
listeners = /* @__PURE__ */ new Set();
|
|
114
|
+
constructor(initial) {
|
|
115
|
+
this.state = initial;
|
|
116
|
+
}
|
|
117
|
+
snapshot() {
|
|
118
|
+
return this.state;
|
|
119
|
+
}
|
|
120
|
+
subscribe(fn) {
|
|
121
|
+
this.listeners.add(fn);
|
|
122
|
+
return () => {
|
|
123
|
+
this.listeners.delete(fn);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
update(patch) {
|
|
127
|
+
const prev = this.state;
|
|
128
|
+
if (prev.connection === "disconnected" && prev.endReason) return;
|
|
129
|
+
const next = { ...prev, ...patch };
|
|
130
|
+
if (patch.participants) next.participants = sortParticipants(patch.participants);
|
|
131
|
+
if (prev.endReason && !next.endReason) next.endReason = prev.endReason;
|
|
132
|
+
if (next.connection === "disconnected") {
|
|
133
|
+
next.participants = [];
|
|
134
|
+
next.muted = true;
|
|
135
|
+
next.needsClick = false;
|
|
136
|
+
}
|
|
137
|
+
if (JSON.stringify(prev) === JSON.stringify(next)) return;
|
|
138
|
+
this.state = next;
|
|
139
|
+
for (const fn of [...this.listeners]) {
|
|
140
|
+
try {
|
|
141
|
+
fn(next);
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function mapLiveKitDisconnect(code, wasConnected = false) {
|
|
148
|
+
if (code === void 0 && wasConnected) return "network";
|
|
149
|
+
switch (code) {
|
|
150
|
+
case 1:
|
|
151
|
+
return "left";
|
|
152
|
+
// CLIENT_INITIATED
|
|
153
|
+
case 2:
|
|
154
|
+
return "replaced";
|
|
155
|
+
// DUPLICATE_IDENTITY
|
|
156
|
+
case 4:
|
|
157
|
+
return "removed";
|
|
158
|
+
// PARTICIPANT_REMOVED
|
|
159
|
+
case 5:
|
|
160
|
+
case 10:
|
|
161
|
+
return "room_closed";
|
|
162
|
+
case 3:
|
|
163
|
+
case 7:
|
|
164
|
+
case 9:
|
|
165
|
+
case 14:
|
|
166
|
+
case 15:
|
|
167
|
+
return "network";
|
|
168
|
+
default:
|
|
169
|
+
return "error";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
var RTC_NODE = "@livekit/rtc-node";
|
|
173
|
+
var RTC_NODE_VERSION = "1.2.1";
|
|
174
|
+
function rtcNodeMissingHint() {
|
|
175
|
+
return `in-terminal voice needs ${RTC_NODE}@${RTC_NODE_VERSION} \xB7 npm i -g ${RTC_NODE}@${RTC_NODE_VERSION} \xB7 or /voice for the window`;
|
|
176
|
+
}
|
|
177
|
+
function describeEnd(reason) {
|
|
178
|
+
switch (reason) {
|
|
179
|
+
case "left":
|
|
180
|
+
return "voice off";
|
|
181
|
+
case "replaced":
|
|
182
|
+
return "voice moved to another window";
|
|
183
|
+
case "removed":
|
|
184
|
+
return "you were removed from voice";
|
|
185
|
+
case "room_closed":
|
|
186
|
+
return "the voice room closed";
|
|
187
|
+
case "network":
|
|
188
|
+
return "voice lost its connection";
|
|
189
|
+
case "window_closed":
|
|
190
|
+
return "voice off \xB7 the voice window closed";
|
|
191
|
+
case "error":
|
|
192
|
+
return "voice stopped because of an error";
|
|
193
|
+
default:
|
|
194
|
+
return "voice off";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export {
|
|
199
|
+
isVoiceUrl,
|
|
200
|
+
parseVoiceIdentity,
|
|
201
|
+
sanitizeVoiceName,
|
|
202
|
+
decodeVoiceMetadata,
|
|
203
|
+
initialVoiceState,
|
|
204
|
+
normalizeVoiceState,
|
|
205
|
+
VoiceStore,
|
|
206
|
+
mapLiveKitDisconnect,
|
|
207
|
+
RTC_NODE,
|
|
208
|
+
RTC_NODE_VERSION,
|
|
209
|
+
rtcNodeMissingHint,
|
|
210
|
+
describeEnd
|
|
211
|
+
};
|