sideby 0.0.0-stage → 0.1.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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -2
  3. package/README.zh-CN.md +455 -0
  4. package/dist/build.json +1 -0
  5. package/dist/src/cli/args.d.ts +13 -0
  6. package/dist/src/cli/args.js +38 -0
  7. package/dist/src/cli/commands.d.ts +37 -0
  8. package/dist/src/cli/commands.js +487 -0
  9. package/dist/src/cli/format.d.ts +16 -0
  10. package/dist/src/cli/format.js +80 -0
  11. package/dist/src/cli/json-schemas.d.ts +2 -0
  12. package/dist/src/cli/json-schemas.js +224 -0
  13. package/dist/src/cli/main.d.ts +2 -0
  14. package/dist/src/cli/main.js +186 -0
  15. package/dist/src/cli/schemas.d.ts +1 -0
  16. package/dist/src/cli/schemas.js +16 -0
  17. package/dist/src/core/account-name.d.ts +2 -0
  18. package/dist/src/core/account-name.js +2 -0
  19. package/dist/src/core/accounts.d.ts +18 -0
  20. package/dist/src/core/accounts.js +116 -0
  21. package/dist/src/core/config.d.ts +60 -0
  22. package/dist/src/core/config.js +181 -0
  23. package/dist/src/core/create.d.ts +30 -0
  24. package/dist/src/core/create.js +126 -0
  25. package/dist/src/core/doctor.d.ts +41 -0
  26. package/dist/src/core/doctor.js +171 -0
  27. package/dist/src/core/errors.d.ts +12 -0
  28. package/dist/src/core/errors.js +14 -0
  29. package/dist/src/core/file-memo.d.ts +24 -0
  30. package/dist/src/core/file-memo.js +41 -0
  31. package/dist/src/core/fs-safe.d.ts +38 -0
  32. package/dist/src/core/fs-safe.js +205 -0
  33. package/dist/src/core/handoff.d.ts +41 -0
  34. package/dist/src/core/handoff.js +54 -0
  35. package/dist/src/core/identity.d.ts +23 -0
  36. package/dist/src/core/identity.js +79 -0
  37. package/dist/src/core/last-doctor.d.ts +36 -0
  38. package/dist/src/core/last-doctor.js +69 -0
  39. package/dist/src/core/launch.d.ts +35 -0
  40. package/dist/src/core/launch.js +108 -0
  41. package/dist/src/core/login-path.d.ts +23 -0
  42. package/dist/src/core/login-path.js +96 -0
  43. package/dist/src/core/paths.d.ts +13 -0
  44. package/dist/src/core/paths.js +32 -0
  45. package/dist/src/core/quota-levels.d.ts +19 -0
  46. package/dist/src/core/quota-levels.js +34 -0
  47. package/dist/src/core/secret-file.d.ts +30 -0
  48. package/dist/src/core/secret-file.js +116 -0
  49. package/dist/src/core/share-mode-names.d.ts +1 -0
  50. package/dist/src/core/share-mode-names.js +10 -0
  51. package/dist/src/core/share-modes.d.ts +24 -0
  52. package/dist/src/core/share-modes.js +405 -0
  53. package/dist/src/core/shell-init.d.ts +25 -0
  54. package/dist/src/core/shell-init.js +61 -0
  55. package/dist/src/core/skill-check.d.ts +11 -0
  56. package/dist/src/core/skill-check.js +57 -0
  57. package/dist/src/core/usage-days.d.ts +15 -0
  58. package/dist/src/core/usage-days.js +33 -0
  59. package/dist/src/core/version.d.ts +9 -0
  60. package/dist/src/core/version.js +46 -0
  61. package/dist/src/desktop/app.d.ts +60 -0
  62. package/dist/src/desktop/app.js +316 -0
  63. package/dist/src/desktop/icon.d.ts +16 -0
  64. package/dist/src/desktop/icon.js +174 -0
  65. package/dist/src/families/claude/index.d.ts +10 -0
  66. package/dist/src/families/claude/index.js +124 -0
  67. package/dist/src/families/claude/layout.d.ts +6 -0
  68. package/dist/src/families/claude/layout.js +5 -0
  69. package/dist/src/families/claude/quota-cache.d.ts +31 -0
  70. package/dist/src/families/claude/quota-cache.js +111 -0
  71. package/dist/src/families/claude/quota.d.ts +3 -0
  72. package/dist/src/families/claude/quota.js +53 -0
  73. package/dist/src/families/claude/usage.d.ts +22 -0
  74. package/dist/src/families/claude/usage.js +146 -0
  75. package/dist/src/families/codex/index.d.ts +5 -0
  76. package/dist/src/families/codex/index.js +67 -0
  77. package/dist/src/families/codex/rollout.d.ts +67 -0
  78. package/dist/src/families/codex/rollout.js +298 -0
  79. package/dist/src/families/grok/index.d.ts +8 -0
  80. package/dist/src/families/grok/index.js +78 -0
  81. package/dist/src/families/index.d.ts +3 -0
  82. package/dist/src/families/index.js +40 -0
  83. package/dist/src/families/logos.d.ts +21 -0
  84. package/dist/src/families/logos.js +26 -0
  85. package/dist/src/families/pi/index.d.ts +3 -0
  86. package/dist/src/families/pi/index.js +54 -0
  87. package/dist/src/families/shared/toml.d.ts +5 -0
  88. package/dist/src/families/shared/toml.js +16 -0
  89. package/dist/src/index.d.ts +5 -0
  90. package/dist/src/index.js +4 -0
  91. package/dist/src/panel/background.d.ts +62 -0
  92. package/dist/src/panel/background.js +274 -0
  93. package/dist/src/panel/handler.d.ts +93 -0
  94. package/dist/src/panel/handler.js +411 -0
  95. package/dist/src/panel/i18n.d.ts +287 -0
  96. package/dist/src/panel/i18n.js +570 -0
  97. package/dist/src/panel/page-logic.d.ts +154 -0
  98. package/dist/src/panel/page-logic.js +326 -0
  99. package/dist/src/panel/page-script.d.ts +2 -0
  100. package/dist/src/panel/page-script.js +1633 -0
  101. package/dist/src/panel/page-styles.d.ts +7 -0
  102. package/dist/src/panel/page-styles.js +514 -0
  103. package/dist/src/panel/page.d.ts +20 -0
  104. package/dist/src/panel/page.js +96 -0
  105. package/dist/src/panel/server.d.ts +32 -0
  106. package/dist/src/panel/server.js +133 -0
  107. package/dist/src/panel/theme.d.ts +116 -0
  108. package/dist/src/panel/theme.js +132 -0
  109. package/dist/src/plugins/account-script.d.ts +4 -0
  110. package/dist/src/plugins/account-script.js +66 -0
  111. package/dist/src/plugins/bus.d.ts +33 -0
  112. package/dist/src/plugins/bus.js +84 -0
  113. package/dist/src/plugins/loader.d.ts +58 -0
  114. package/dist/src/plugins/loader.js +250 -0
  115. package/dist/src/quota/claude-statusline.d.ts +28 -0
  116. package/dist/src/quota/claude-statusline.js +301 -0
  117. package/dist/src/quota/statusline-tap.d.ts +11 -0
  118. package/dist/src/quota/statusline-tap.js +104 -0
  119. package/dist/src/runtime.d.ts +149 -0
  120. package/dist/src/runtime.js +371 -0
  121. package/dist/src/types.d.ts +278 -0
  122. package/dist/src/types.js +3 -0
  123. package/dist/testing/demo.d.ts +8 -0
  124. package/dist/testing/demo.js +50 -0
  125. package/dist/testing/index.d.ts +36 -0
  126. package/dist/testing/index.js +117 -0
  127. package/package.json +66 -4
  128. package/schemas/alias.json +102 -0
  129. package/schemas/app-install.json +74 -0
  130. package/schemas/app-uninstall.json +54 -0
  131. package/schemas/config.json +166 -0
  132. package/schemas/doctor.json +229 -0
  133. package/schemas/error.json +25 -0
  134. package/schemas/list.json +177 -0
  135. package/schemas/new.json +181 -0
  136. package/schemas/next.json +127 -0
  137. package/schemas/plugins.json +74 -0
  138. package/schemas/quota-setup.json +60 -0
  139. package/schemas/quota-teardown.json +30 -0
  140. package/schemas/quota.json +203 -0
  141. package/skills/sideby/SKILL.md +52 -0
  142. package/skills/sideby/references/accounts.md +31 -0
  143. package/skills/sideby/references/diagnose.md +47 -0
  144. package/skills/sideby/references/handoff.md +41 -0
  145. package/skills/sideby/references/quota.md +14 -0
@@ -0,0 +1,411 @@
1
+ // HTTP handler for the local Panel. Mountable under any basePath; standalone and embedded use the same rules.
2
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { resolveRef } from '../core/accounts.js';
4
+ import { aliasArgs, aliasRefs } from '../core/config.js';
5
+ import { UserError } from '../core/errors.js';
6
+ import { planHandoff } from '../core/handoff.js';
7
+ import { buildId, packageVersion } from '../core/version.js';
8
+ import { renderPage } from './page.js';
9
+ import { resolveTheme } from './theme.js';
10
+ export const PANEL_VERSION = packageVersion();
11
+ export const MAX_BODY_BYTES = 64 * 1024;
12
+ /** Write requests allowed to wait behind the running one; more get 409. */
13
+ export const MAX_QUEUED_WRITES = 4;
14
+ const PANEL_LANGS = ['auto', 'en', 'zh'];
15
+ function resolveLang(lang) {
16
+ if (lang === undefined)
17
+ return 'auto';
18
+ const found = PANEL_LANGS.find((l) => l === lang);
19
+ if (!found)
20
+ throw new TypeError('panel lang must be "auto", "en" or "zh"');
21
+ return found;
22
+ }
23
+ /** Counts of failing and warning Findings per checked Account, from the Doctor history. */
24
+ export function healthByAccount(history) {
25
+ const out = new Map();
26
+ for (const [ref, entry] of Object.entries(history?.accounts ?? {}))
27
+ out.set(ref, {
28
+ at: entry.at,
29
+ fail: entry.findings.filter((f) => f.level === 'fail').length,
30
+ warn: entry.findings.filter((f) => f.level === 'warn').length,
31
+ });
32
+ return out;
33
+ }
34
+ /** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
35
+ export function aliasesByAccount(aliases, accounts) {
36
+ const out = new Map();
37
+ for (const [alias, target] of Object.entries(aliases ?? {})) {
38
+ let ref;
39
+ try {
40
+ ref = resolveRef(target, accounts).ref;
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ out.set(ref, [...(out.get(ref) ?? []), alias].sort());
46
+ }
47
+ return out;
48
+ }
49
+ /** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
50
+ export function handoffByFamily(rt, accounts, now) {
51
+ const out = {};
52
+ for (const [id, f] of rt.families) {
53
+ const list = accounts.filter((a) => a.family === id);
54
+ if (!f.readQuota || list.filter((a) => a.kind === 'subscription').length < 2)
55
+ continue;
56
+ const plan = planHandoff(id, list.map((a) => ({
57
+ ref: a.ref,
58
+ name: a.name,
59
+ isMain: a.isMain,
60
+ kind: a.kind,
61
+ login: a.login ?? 'unknown',
62
+ quota: a.quota ?? { status: 'unavailable', reason: 'unrecognized' },
63
+ })), now);
64
+ if (plan.pick && plan.hasQuota)
65
+ out[id] = plan.pick;
66
+ }
67
+ return out;
68
+ }
69
+ class HttpError extends Error {
70
+ status;
71
+ /** Machine-readable reason the page acts on, such as `bad-token`. */
72
+ code;
73
+ constructor(status, message, code) {
74
+ super(message);
75
+ this.status = status;
76
+ if (code)
77
+ this.code = code;
78
+ }
79
+ }
80
+ function normalizeBase(p) {
81
+ if (!p || p === '/')
82
+ return '';
83
+ const withSlash = p.startsWith('/') ? p : `/${p}`;
84
+ return withSlash.replace(/\/+$/, '');
85
+ }
86
+ function sameSecret(given, expected) {
87
+ const a = Buffer.from(given);
88
+ const b = Buffer.from(expected);
89
+ return a.length === b.length && timingSafeEqual(a, b);
90
+ }
91
+ function send(res, status, body, extra = {}) {
92
+ const data = JSON.stringify(body);
93
+ res.writeHead(status, {
94
+ 'content-type': 'application/json; charset=utf-8',
95
+ 'cache-control': 'no-store',
96
+ 'x-content-type-options': 'nosniff',
97
+ ...extra,
98
+ });
99
+ res.end(data);
100
+ }
101
+ function readBody(req) {
102
+ const declared = Number(req.headers['content-length']);
103
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES)
104
+ return Promise.reject(new HttpError(413, `request body over ${MAX_BODY_BYTES} bytes`));
105
+ return new Promise((resolve, reject) => {
106
+ const chunks = [];
107
+ let size = 0;
108
+ let failed = false;
109
+ req.on('data', (c) => {
110
+ if (failed)
111
+ return;
112
+ size += c.length;
113
+ if (size > MAX_BODY_BYTES) {
114
+ failed = true;
115
+ reject(new HttpError(413, `request body over ${MAX_BODY_BYTES} bytes`));
116
+ return;
117
+ }
118
+ chunks.push(c);
119
+ });
120
+ req.on('error', (err) => {
121
+ if (!failed)
122
+ reject(err);
123
+ });
124
+ req.on('end', () => {
125
+ if (failed)
126
+ return;
127
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
128
+ if (!raw)
129
+ return resolve({});
130
+ try {
131
+ const parsed = JSON.parse(raw);
132
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
133
+ return reject(new HttpError(400, 'request body must be a JSON object'));
134
+ resolve(parsed);
135
+ }
136
+ catch {
137
+ reject(new HttpError(400, 'request body is not valid JSON'));
138
+ }
139
+ });
140
+ });
141
+ }
142
+ function optString(body, key) {
143
+ const v = body[key];
144
+ if (v === undefined || v === null || v === '')
145
+ return undefined;
146
+ if (typeof v !== 'string')
147
+ throw new HttpError(400, `"${key}" must be a string`);
148
+ return v;
149
+ }
150
+ function optBool(body, key) {
151
+ const v = body[key];
152
+ if (v === undefined || v === null)
153
+ return false;
154
+ if (typeof v !== 'boolean')
155
+ throw new HttpError(400, `"${key}" must be true or false`);
156
+ return v;
157
+ }
158
+ async function accountState(rt, account, families) {
159
+ const errors = [];
160
+ const guard = async (p) => {
161
+ try {
162
+ return await p;
163
+ }
164
+ catch (err) {
165
+ errors.push(err.message);
166
+ return undefined;
167
+ }
168
+ };
169
+ const [status, quota, usage] = await Promise.all([
170
+ guard(rt.status(account)),
171
+ guard(rt.quota(account)),
172
+ guard(rt.usage(account)),
173
+ ]);
174
+ const fam = families.find((f) => f.id === account.family);
175
+ const base = status ?? {
176
+ ...account,
177
+ familyTitle: fam?.title ?? account.family,
178
+ hostInstalled: fam?.installed ?? false,
179
+ login: 'unknown',
180
+ };
181
+ errors.push(...(status?.problems ?? []));
182
+ return {
183
+ ...base,
184
+ ...(quota ? { quota } : {}),
185
+ ...(usage ? { usage } : {}),
186
+ ...(errors.length ? { error: errors.join('; ') } : {}),
187
+ };
188
+ }
189
+ async function buildState(rt, opts) {
190
+ const ctx = rt.readContext();
191
+ const [families, accounts, history, quotaSetups] = await Promise.all([
192
+ rt.familyInfo(),
193
+ rt.accounts(),
194
+ rt.doctorHistory(),
195
+ rt.quotaSetups(),
196
+ ]);
197
+ const health = healthByAccount(history);
198
+ const aliases = aliasesByAccount(aliasRefs(rt.config.aliases), accounts);
199
+ const list = await Promise.all(accounts.map(async (a) => {
200
+ const st = await accountState(rt, a, families);
201
+ const h = health.get(a.ref);
202
+ const names = aliases.get(a.ref);
203
+ const withArgs = (names ?? [])
204
+ .map((n) => [n, aliasArgs(rt.config.aliases?.[n])])
205
+ .filter(([, args]) => args.length);
206
+ return {
207
+ ...st,
208
+ ...(h ? { health: h } : {}),
209
+ ...(names ? { aliases: names } : {}),
210
+ ...(withArgs.length ? { aliasArgs: Object.fromEntries(withArgs) } : {}),
211
+ };
212
+ }));
213
+ const handoff = handoffByFamily(rt, list, ctx.now);
214
+ return {
215
+ schemaVersion: 1,
216
+ version: opts.version,
217
+ build: opts.build,
218
+ readOnly: opts.readOnly,
219
+ generatedAt: ctx.now.toISOString(),
220
+ families: families.map((f) => {
221
+ const logo = rt.families.get(f.id)?.logo;
222
+ return logo ? { ...f, logo } : f;
223
+ }),
224
+ accounts: list,
225
+ pluginErrors: rt.pluginErrors.map((e) => ({ where: e.where, message: e.message })),
226
+ quotaSetups,
227
+ ...(history ? { history } : {}),
228
+ ...(rt.config.aliases ? { configAliases: aliasRefs(rt.config.aliases) } : {}),
229
+ ...(Object.keys(handoff).length ? { handoff } : {}),
230
+ };
231
+ }
232
+ /** Runs write requests one at a time; a few may wait, the rest are told to retry. */
233
+ function writeQueue() {
234
+ let tail = Promise.resolve();
235
+ let inFlight = 0;
236
+ return (fn) => {
237
+ if (inFlight > MAX_QUEUED_WRITES)
238
+ return Promise.reject(new HttpError(409, 'another change is still running; try again in a moment'));
239
+ inFlight++;
240
+ const run = tail.then(fn);
241
+ tail = run.catch(() => undefined);
242
+ return run.finally(() => {
243
+ inFlight--;
244
+ });
245
+ };
246
+ }
247
+ export function createPanelHandler(opts) {
248
+ const basePath = normalizeBase(opts.basePath);
249
+ const allowed = new Set(opts.allowedHosts.map((h) => h.toLowerCase()));
250
+ const readOnly = Boolean(opts.readOnly);
251
+ const version = opts.version ?? PANEL_VERSION;
252
+ const build = buildId();
253
+ const theme = resolveTheme(opts.theme);
254
+ const lang = resolveLang(opts.lang);
255
+ const token = randomBytes(32).toString('base64url');
256
+ const serial = writeQueue();
257
+ const getRuntime = typeof opts.runtime === 'function' ? opts.runtime : async () => opts.runtime;
258
+ const routes = {
259
+ '/api/doctor': {
260
+ // Only the record of the check is written, and a read-only Panel skips that (persist: false).
261
+ writes: () => false,
262
+ async run(body) {
263
+ const target = optString(body, 'target');
264
+ const rt = await getRuntime();
265
+ return rt.doctorWithHistory({ persist: !readOnly, ...(target ? { target } : {}) });
266
+ },
267
+ },
268
+ '/api/fix': {
269
+ async run(body) {
270
+ const target = optString(body, 'target');
271
+ const force = optBool(body, 'force');
272
+ const rt = await getRuntime();
273
+ return rt.doctorWithHistory({ fix: true, force, ...(target ? { target } : {}) });
274
+ },
275
+ },
276
+ '/api/accounts': {
277
+ async run(body) {
278
+ const family = optString(body, 'family');
279
+ const name = optString(body, 'name');
280
+ if (!family || !name)
281
+ throw new HttpError(400, 'pick a family and enter a name');
282
+ const api = optBool(body, 'api');
283
+ const alias = optString(body, 'alias');
284
+ const rt = await getRuntime();
285
+ return rt.createAccount(family, name, { api, ...(alias ? { alias } : {}) });
286
+ },
287
+ },
288
+ '/api/quota/setup': {
289
+ writes: (body) => body.confirm === true,
290
+ async run(body) {
291
+ const family = optString(body, 'family');
292
+ const confirm = optBool(body, 'confirm');
293
+ if (!family)
294
+ throw new HttpError(400, '"family" is required');
295
+ const rt = await getRuntime();
296
+ const setup = rt.quotaSetup(family);
297
+ const plan = confirm ? await setup.apply() : await setup.plan();
298
+ return { family, summary: setup.summary, plan };
299
+ },
300
+ },
301
+ // Undo for the setup above; the Runtime turns a teardown that throws into `{ ok: false, message }`.
302
+ '/api/quota/teardown': {
303
+ async run(body) {
304
+ const family = optString(body, 'family');
305
+ if (!family)
306
+ throw new HttpError(400, '"family" is required');
307
+ const rt = await getRuntime();
308
+ return { family, result: await rt.quotaSetup(family).teardown() };
309
+ },
310
+ },
311
+ };
312
+ const handle = async (req, res) => {
313
+ let pathname;
314
+ try {
315
+ pathname = new URL(req.url ?? '/', 'http://x').pathname;
316
+ }
317
+ catch {
318
+ return false;
319
+ }
320
+ if (basePath && pathname !== basePath && !pathname.startsWith(`${basePath}/`))
321
+ return false;
322
+ const sub = pathname.slice(basePath.length) || '/';
323
+ try {
324
+ const host = (req.headers.host ?? '').toLowerCase();
325
+ if (!allowed.has(host))
326
+ throw new HttpError(403, 'host not allowed');
327
+ const method = req.method ?? 'GET';
328
+ if (method === 'GET' || method === 'HEAD') {
329
+ if (sub === '/' || sub === '/index.html') {
330
+ const nonce = randomBytes(16).toString('base64');
331
+ res.writeHead(200, {
332
+ 'content-type': 'text/html; charset=utf-8',
333
+ 'cache-control': 'no-store',
334
+ 'x-content-type-options': 'nosniff',
335
+ 'referrer-policy': 'no-referrer',
336
+ 'content-security-policy': [
337
+ "default-src 'none'",
338
+ `script-src 'nonce-${nonce}'`,
339
+ `style-src 'nonce-${nonce}'`,
340
+ "connect-src 'self'",
341
+ "img-src 'self' data:",
342
+ "base-uri 'none'",
343
+ "form-action 'none'",
344
+ "frame-ancestors 'self'",
345
+ ].join('; '),
346
+ });
347
+ res.end(method === 'HEAD'
348
+ ? undefined
349
+ : renderPage({ basePath, token, nonce, version, build, readOnly, theme, lang }));
350
+ return true;
351
+ }
352
+ if (sub === '/api/health') {
353
+ send(res, 200, { app: 'sideby', version, build });
354
+ return true;
355
+ }
356
+ // The page fetches a fresh token here after the server restarted (a new process, a new token).
357
+ // It reveals nothing a same-origin read of the page does not; a cross-site page cannot read it.
358
+ if (sub === '/api/session') {
359
+ if (req.headers['sec-fetch-site'] === 'cross-site')
360
+ throw new HttpError(403, 'origin not allowed');
361
+ send(res, 200, { token });
362
+ return true;
363
+ }
364
+ if (sub === '/api/state') {
365
+ send(res, 200, await buildState(await getRuntime(), { version, build, readOnly }));
366
+ return true;
367
+ }
368
+ if (Object.hasOwn(routes, sub))
369
+ throw new HttpError(405, 'use POST');
370
+ throw new HttpError(404, 'not found');
371
+ }
372
+ if (method !== 'POST')
373
+ throw new HttpError(405, 'method not allowed');
374
+ const route = Object.hasOwn(routes, sub) ? routes[sub] : undefined;
375
+ if (!route)
376
+ throw new HttpError(404, 'not found');
377
+ const origin = req.headers.origin;
378
+ if (!origin || origin.toLowerCase() !== `http://${host}`)
379
+ throw new HttpError(403, 'origin not allowed');
380
+ const given = req.headers['x-sideby-token'];
381
+ if (typeof given !== 'string' || !sameSecret(given, token))
382
+ throw new HttpError(403, 'missing or wrong x-sideby-token; reload the page', 'bad-token');
383
+ const body = (await readBody(req));
384
+ if (readOnly && (route.writes?.(body) ?? true))
385
+ throw new HttpError(403, 'this panel is read-only');
386
+ // Reads that record history (doctor) also run in this queue, so overlapping checks merge in order.
387
+ send(res, 200, await serial(() => route.run(body)));
388
+ return true;
389
+ }
390
+ catch (err) {
391
+ if (res.headersSent) {
392
+ res.end();
393
+ return true;
394
+ }
395
+ if (err instanceof HttpError) {
396
+ const body = err.code ? { error: err.message, code: err.code } : { error: err.message };
397
+ send(res, err.status, body, err.status === 413 ? { connection: 'close' } : {});
398
+ if (err.status === 413)
399
+ req.resume();
400
+ }
401
+ else if (err instanceof UserError) {
402
+ send(res, 400, err.code ? { error: err.message, code: err.code } : { error: err.message });
403
+ }
404
+ else {
405
+ send(res, 500, { error: err?.message || 'internal error' });
406
+ }
407
+ return true;
408
+ }
409
+ };
410
+ return Object.assign(handle, { token });
411
+ }
@@ -0,0 +1,287 @@
1
+ export type PanelLang = 'en' | 'zh';
2
+ declare const en: {
3
+ 'top.tagline': string;
4
+ 'top.refresh': string;
5
+ 'top.loading': string;
6
+ 'top.refreshing': string;
7
+ 'top.updated': string;
8
+ 'top.updatedTitle': string;
9
+ 'top.language': string;
10
+ 'foot.text': string;
11
+ 'acct.title': string;
12
+ 'theme.system': string;
13
+ 'theme.light': string;
14
+ 'theme.dark': string;
15
+ 'err.noAnswer': string;
16
+ 'err.session': string;
17
+ 'err.expired': string;
18
+ 'err.server': string;
19
+ 'conn.lost': string;
20
+ 'load.failed': string;
21
+ 'load.failedShort': string;
22
+ 'btn.copy': string;
23
+ 'btn.copyText': string;
24
+ 'btn.retry': string;
25
+ 'btn.close': string;
26
+ 'btn.cancel': string;
27
+ 'btn.done': string;
28
+ 'time.justNow': string;
29
+ 'time.minAgo': string;
30
+ 'time.hAgo': string;
31
+ 'time.dAgo': string;
32
+ 'time.in': string;
33
+ 'toast.dismiss': string;
34
+ 'toast.copied': string;
35
+ 'toast.copyFailed': string;
36
+ 'toast.reconnected': string;
37
+ 'toast.refreshed': string;
38
+ 'toast.updated': string;
39
+ 'toast.refreshFailed': string;
40
+ 'toast.checkOk': string;
41
+ 'toast.checkFound': string;
42
+ 'toast.checkFailed': string;
43
+ 'toast.show': string;
44
+ 'toast.created': string;
45
+ 'toast.createdProblems': string;
46
+ 'toast.quotaOn': string;
47
+ 'toast.quotaOff': string;
48
+ 'toast.undo': string;
49
+ 'toast.undoFailed': string;
50
+ 'toast.undoMissing': string;
51
+ 'tip.label': string;
52
+ 'tip.api.term': string;
53
+ 'tip.api.body': string;
54
+ 'tip.shared.term': string;
55
+ 'tip.shared.body': string;
56
+ 'tip.drift.term': string;
57
+ 'tip.drift.body': string;
58
+ 'tip.quotaOff.term': string;
59
+ 'tip.quotaOff.body': string;
60
+ 'tip.quotaOff.cli': string;
61
+ 'tip.age.term': string;
62
+ 'tip.age.body': string;
63
+ 'stat.accounts': string;
64
+ 'stat.families.one': string;
65
+ 'stat.families.other': string;
66
+ 'stat.api.one': string;
67
+ 'stat.api.other': string;
68
+ 'stat.attention': string;
69
+ 'stat.attentionOn': string;
70
+ 'stat.attentionOff': string;
71
+ 'stat.btnLabel': string;
72
+ 'stat.allClear': string;
73
+ 'stat.tokens': string;
74
+ 'stat.sessions.one': string;
75
+ 'stat.sessions.other': string;
76
+ 'stat.nextReset': string;
77
+ 'stat.noQuota': string;
78
+ 'need.error.one': string;
79
+ 'need.error.other': string;
80
+ 'need.health-fail.one': string;
81
+ 'need.health-fail.other': string;
82
+ 'need.signed-out.one': string;
83
+ 'need.signed-out.other': string;
84
+ 'need.not-installed.one': string;
85
+ 'need.not-installed.other': string;
86
+ 'need.quota-high.one': string;
87
+ 'need.quota-high.other': string;
88
+ 'need.health-warn.one': string;
89
+ 'need.health-warn.other': string;
90
+ 'tb.search': string;
91
+ 'tb.searchPh': string;
92
+ 'tb.family': string;
93
+ 'tb.allFamilies': string;
94
+ 'tb.sort': string;
95
+ 'sort.pressure': string;
96
+ 'sort.name': string;
97
+ 'sort.recent': string;
98
+ 'tb.quota': string;
99
+ 'tb.used': string;
100
+ 'tb.left': string;
101
+ 'tb.view': string;
102
+ 'tb.list': string;
103
+ 'tb.cards': string;
104
+ 'tb.hideEmails': string;
105
+ 'tb.showEmails': string;
106
+ 'tb.segLabel': string;
107
+ 'tb.new': string;
108
+ 'count.accounts.one': string;
109
+ 'count.accounts.other': string;
110
+ 'count.filtered': string;
111
+ 'filter.none': string;
112
+ 'filter.noneQuery': string;
113
+ 'filter.noneBody': string;
114
+ 'filter.clear': string;
115
+ 'group.expand': string;
116
+ 'group.collapse': string;
117
+ 'group.toggle': string;
118
+ 'group.attention.one': string;
119
+ 'group.attention.other': string;
120
+ 'acct.alias': string;
121
+ 'acct.aliasArgs': string;
122
+ 'acct.api': string;
123
+ 'acct.model': string;
124
+ 'acct.signedOut': string;
125
+ 'acct.signInHint': string;
126
+ 'acct.signedOutLabel': string;
127
+ 'acct.next': string;
128
+ 'acct.nextHint': string;
129
+ 'acct.nextLabel': string;
130
+ 'acct.notInstalled': string;
131
+ 'acct.readError': string;
132
+ 'acct.installHost': string;
133
+ 'acct.installIt': string;
134
+ 'quota.pctUsed': string;
135
+ 'quota.pctLeft': string;
136
+ 'quota.meterUsed': string;
137
+ 'quota.meterLeft': string;
138
+ 'quota.resetsAt': string;
139
+ 'quota.resetPassed': string;
140
+ 'quota.resetNoData': string;
141
+ 'quota.dataFrom': string;
142
+ 'quota.asOf': string;
143
+ 'quota.noWindows': string;
144
+ 'quota.off': string;
145
+ 'quota.turnOnFamily': string;
146
+ 'quota.turnOnFamilyNote': string;
147
+ 'quota.turnOnFamilyLabel': string;
148
+ 'quota.blocked': string;
149
+ 'quota.api': string;
150
+ 'quota.noSession': string;
151
+ 'quota.noSessionHint': string;
152
+ 'quota.noSource': string;
153
+ 'quota.unrecognized': string;
154
+ 'quota.unavailable': string;
155
+ 'quota.unitUsed': string;
156
+ 'quota.unitLeft': string;
157
+ 'quota.resetShort': string;
158
+ 'quota.missing': string;
159
+ 'quota.waiting': string;
160
+ 'col.account': string;
161
+ 'col.used': string;
162
+ 'col.left': string;
163
+ 'col.tokens': string;
164
+ 'col.last': string;
165
+ 'col.health': string;
166
+ 'usage.tokens': string;
167
+ 'usage.cache': string;
168
+ 'usage.sessions.one': string;
169
+ 'usage.sessions.other': string;
170
+ 'usage.detail': string;
171
+ 'usage.none': string;
172
+ 'usage.noSource': string;
173
+ 'usage.unreadable': string;
174
+ 'usage.sparkLabel': string;
175
+ 'last.title': string;
176
+ 'last.unknown': string;
177
+ 'health.title': string;
178
+ 'health.intro': string;
179
+ 'health.lead': string;
180
+ 'health.sharedTerm': string;
181
+ 'health.driftTerm': string;
182
+ 'health.more.one': string;
183
+ 'health.more.other': string;
184
+ 'health.run': string;
185
+ 'health.running': string;
186
+ 'health.fixing': string;
187
+ 'health.checking': string;
188
+ 'health.waiting': string;
189
+ 'health.last': string;
190
+ 'health.never': string;
191
+ 'health.error': string;
192
+ 'health.notChecked': string;
193
+ 'health.ok': string;
194
+ 'health.issues.one': string;
195
+ 'health.issues.other': string;
196
+ 'health.warnings.one': string;
197
+ 'health.warnings.other': string;
198
+ 'health.filterHint': string;
199
+ 'health.chipLabel': string;
200
+ 'health.only': string;
201
+ 'health.filtered': string;
202
+ 'health.checkThis': string;
203
+ 'health.showAll': string;
204
+ 'health.autoFix': string;
205
+ 'health.from': string;
206
+ 'health.allGood': string;
207
+ 'health.refGood': string;
208
+ 'health.allGoodSub': string;
209
+ 'health.refNever': string;
210
+ 'fix.all': string;
211
+ 'fix.hintAction': string;
212
+ 'fix.off': string;
213
+ 'fix.whyNoCheck': string;
214
+ 'fix.whyManual': string;
215
+ 'fix.whyNothing': string;
216
+ 'fix.result': string;
217
+ 'fix.nothing': string;
218
+ 'menu.title': string;
219
+ 'menu.label': string;
220
+ 'menu.copyRun': string;
221
+ 'menu.copyAlias': string;
222
+ 'menu.copyLogin': string;
223
+ 'menu.copyDir': string;
224
+ 'menu.copyEmail': string;
225
+ 'menu.check': string;
226
+ 'menu.like': string;
227
+ 'nf.title': string;
228
+ 'nf.intro': string;
229
+ 'nf.tool': string;
230
+ 'nf.accounts.one': string;
231
+ 'nf.accounts.other': string;
232
+ 'nf.runOnce': string;
233
+ 'nf.notInstalled': string;
234
+ 'nf.name': string;
235
+ 'nf.namePh': string;
236
+ 'nf.api': string;
237
+ 'nf.apiHint': string;
238
+ 'nf.dir': string;
239
+ 'nf.launch': string;
240
+ 'nf.create': string;
241
+ 'nf.suggested': string;
242
+ 'nf.refusedApi': string;
243
+ 'nf.creating': string;
244
+ 'nf.mainTaken': string;
245
+ 'nf.badName': string;
246
+ 'nf.exists': string;
247
+ 'nf.enterName': string;
248
+ 'nf.noReady': string;
249
+ 'nf.created': string;
250
+ 'nf.createdProblems': string;
251
+ 'nf.next': string;
252
+ 'nf.fillIn': string;
253
+ 'nf.another': string;
254
+ 'nf.alias': string;
255
+ 'nf.optional': string;
256
+ 'nf.aliasPh': string;
257
+ 'nf.aliasHint': string;
258
+ 'nf.aliasSuggested': string;
259
+ 'nf.aliasBad': string;
260
+ 'nf.aliasReserved': string;
261
+ 'nf.aliasTaken': string;
262
+ 'nf.or': string;
263
+ 'nf.aliasNewShell': string;
264
+ 'nf.aliasNotAdded': string;
265
+ 'nf.shellFileUpdated': string;
266
+ 'nf.shellFileSame': string;
267
+ 'qs.title': string;
268
+ 'qs.preparing': string;
269
+ 'qs.changes': string;
270
+ 'qs.undoHint': string;
271
+ 'qs.apply': string;
272
+ 'qs.applying': string;
273
+ 'qs.alreadyOn': string;
274
+ 'empty.title': string;
275
+ 'empty.body': string;
276
+ 'empty.runOnce': string;
277
+ 'empty.install': string;
278
+ 'empty.noFamilies': string;
279
+ 'empty.create': string;
280
+ 'banner.readOnly': string;
281
+ 'banner.plugins.one': string;
282
+ 'banner.plugins.other': string;
283
+ 'banner.pluginsHint': string;
284
+ };
285
+ export type MessageKey = keyof typeof en;
286
+ export declare const MESSAGES: Record<PanelLang, Record<string, string>>;
287
+ export {};