claude-relay 2.4.2 → 2.5.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 (75) hide show
  1. package/bin/cli.js +1 -2350
  2. package/package.json +7 -42
  3. package/LICENSE +0 -21
  4. package/README.md +0 -281
  5. package/lib/cli-sessions.js +0 -270
  6. package/lib/config.js +0 -222
  7. package/lib/daemon.js +0 -423
  8. package/lib/ipc.js +0 -112
  9. package/lib/pages.js +0 -714
  10. package/lib/project.js +0 -1224
  11. package/lib/public/app.js +0 -2157
  12. package/lib/public/apple-touch-icon.png +0 -0
  13. package/lib/public/css/base.css +0 -145
  14. package/lib/public/css/diff.css +0 -128
  15. package/lib/public/css/filebrowser.css +0 -1076
  16. package/lib/public/css/highlight.css +0 -144
  17. package/lib/public/css/input.css +0 -512
  18. package/lib/public/css/menus.css +0 -683
  19. package/lib/public/css/messages.css +0 -1159
  20. package/lib/public/css/overlays.css +0 -731
  21. package/lib/public/css/rewind.css +0 -529
  22. package/lib/public/css/sidebar.css +0 -794
  23. package/lib/public/favicon.svg +0 -26
  24. package/lib/public/icon-192.png +0 -0
  25. package/lib/public/icon-512.png +0 -0
  26. package/lib/public/icon-mono.svg +0 -19
  27. package/lib/public/index.html +0 -460
  28. package/lib/public/manifest.json +0 -27
  29. package/lib/public/modules/diff.js +0 -398
  30. package/lib/public/modules/events.js +0 -21
  31. package/lib/public/modules/filebrowser.js +0 -1375
  32. package/lib/public/modules/fileicons.js +0 -172
  33. package/lib/public/modules/icons.js +0 -54
  34. package/lib/public/modules/input.js +0 -578
  35. package/lib/public/modules/markdown.js +0 -149
  36. package/lib/public/modules/notifications.js +0 -643
  37. package/lib/public/modules/qrcode.js +0 -70
  38. package/lib/public/modules/rewind.js +0 -334
  39. package/lib/public/modules/sidebar.js +0 -628
  40. package/lib/public/modules/state.js +0 -3
  41. package/lib/public/modules/terminal.js +0 -658
  42. package/lib/public/modules/theme.js +0 -622
  43. package/lib/public/modules/tools.js +0 -1410
  44. package/lib/public/modules/utils.js +0 -56
  45. package/lib/public/style.css +0 -10
  46. package/lib/public/sw.js +0 -75
  47. package/lib/push.js +0 -125
  48. package/lib/sdk-bridge.js +0 -771
  49. package/lib/server.js +0 -577
  50. package/lib/sessions.js +0 -402
  51. package/lib/terminal-manager.js +0 -187
  52. package/lib/terminal.js +0 -24
  53. package/lib/themes/ayu-light.json +0 -9
  54. package/lib/themes/catppuccin-latte.json +0 -9
  55. package/lib/themes/catppuccin-mocha.json +0 -9
  56. package/lib/themes/claude-light.json +0 -9
  57. package/lib/themes/claude.json +0 -9
  58. package/lib/themes/dracula.json +0 -9
  59. package/lib/themes/everforest-light.json +0 -9
  60. package/lib/themes/everforest.json +0 -9
  61. package/lib/themes/github-light.json +0 -9
  62. package/lib/themes/gruvbox-dark.json +0 -9
  63. package/lib/themes/gruvbox-light.json +0 -9
  64. package/lib/themes/monokai.json +0 -9
  65. package/lib/themes/nord-light.json +0 -9
  66. package/lib/themes/nord.json +0 -9
  67. package/lib/themes/one-dark.json +0 -9
  68. package/lib/themes/one-light.json +0 -9
  69. package/lib/themes/rose-pine-dawn.json +0 -9
  70. package/lib/themes/rose-pine.json +0 -9
  71. package/lib/themes/solarized-dark.json +0 -9
  72. package/lib/themes/solarized-light.json +0 -9
  73. package/lib/themes/tokyo-night-light.json +0 -9
  74. package/lib/themes/tokyo-night.json +0 -9
  75. package/lib/updater.js +0 -96
package/lib/server.js DELETED
@@ -1,577 +0,0 @@
1
- var http = require("http");
2
- var crypto = require("crypto");
3
- var fs = require("fs");
4
- var path = require("path");
5
- var { WebSocketServer } = require("ws");
6
- var { pinPageHtml, setupPageHtml, dashboardPageHtml } = require("./pages");
7
- var { createProjectContext } = require("./project");
8
-
9
- var { CONFIG_DIR } = require("./config");
10
-
11
- var publicDir = path.join(__dirname, "public");
12
- var bundledThemesDir = path.join(__dirname, "themes");
13
- var userThemesDir = path.join(CONFIG_DIR, "themes");
14
-
15
- var MIME_TYPES = {
16
- ".html": "text/html",
17
- ".css": "text/css",
18
- ".js": "application/javascript",
19
- ".json": "application/json",
20
- ".png": "image/png",
21
- ".jpg": "image/jpeg",
22
- ".jpeg": "image/jpeg",
23
- ".gif": "image/gif",
24
- ".webp": "image/webp",
25
- ".bmp": "image/bmp",
26
- ".svg": "image/svg+xml",
27
- ".ico": "image/x-icon",
28
- };
29
-
30
- function generateAuthToken(pin) {
31
- return crypto.createHash("sha256").update("claude-relay:" + pin).digest("hex");
32
- }
33
-
34
- function parseCookies(req) {
35
- var cookies = {};
36
- var header = req.headers.cookie || "";
37
- header.split(";").forEach(function (part) {
38
- var pair = part.trim().split("=");
39
- if (pair.length === 2) cookies[pair[0]] = pair[1];
40
- });
41
- return cookies;
42
- }
43
-
44
- function isAuthed(req, authToken) {
45
- if (!authToken) return true;
46
- var cookies = parseCookies(req);
47
- return cookies["relay_auth"] === authToken;
48
- }
49
-
50
- // --- PIN rate limiting ---
51
- var pinAttempts = {}; // ip → { count, lastAttempt }
52
- var PIN_MAX_ATTEMPTS = 5;
53
- var PIN_LOCKOUT_MS = 15 * 60 * 1000; // 15 minutes
54
-
55
- function checkPinRateLimit(ip) {
56
- var entry = pinAttempts[ip];
57
- if (!entry) return null;
58
- if (entry.count >= PIN_MAX_ATTEMPTS) {
59
- var elapsed = Date.now() - entry.lastAttempt;
60
- if (elapsed < PIN_LOCKOUT_MS) {
61
- return Math.ceil((PIN_LOCKOUT_MS - elapsed) / 1000);
62
- }
63
- delete pinAttempts[ip];
64
- }
65
- return null;
66
- }
67
-
68
- function recordPinFailure(ip) {
69
- if (!pinAttempts[ip]) pinAttempts[ip] = { count: 0, lastAttempt: 0 };
70
- pinAttempts[ip].count++;
71
- pinAttempts[ip].lastAttempt = Date.now();
72
- }
73
-
74
- function clearPinFailures(ip) {
75
- delete pinAttempts[ip];
76
- }
77
-
78
- function serveStatic(urlPath, res) {
79
- if (urlPath === "/") urlPath = "/index.html";
80
-
81
- var filePath = path.join(publicDir, urlPath);
82
-
83
- if (!filePath.startsWith(publicDir)) {
84
- res.writeHead(403);
85
- res.end("Forbidden");
86
- return true;
87
- }
88
-
89
- try {
90
- var content = fs.readFileSync(filePath);
91
- var ext = path.extname(filePath);
92
- var mime = MIME_TYPES[ext] || "application/octet-stream";
93
- res.writeHead(200, { "Content-Type": mime + "; charset=utf-8", "Cache-Control": "no-cache" });
94
- res.end(content);
95
- return true;
96
- } catch (e) {
97
- return false;
98
- }
99
- }
100
-
101
- /**
102
- * Extract slug from URL path: /p/{slug}/... → slug
103
- * Returns null if path doesn't match /p/{slug}
104
- */
105
- function extractSlug(urlPath) {
106
- var match = urlPath.match(/^\/p\/([a-z0-9_-]+)(\/|$)/);
107
- return match ? match[1] : null;
108
- }
109
-
110
- /**
111
- * Strip the /p/{slug} prefix from URL path
112
- */
113
- function stripPrefix(urlPath, slug) {
114
- var prefix = "/p/" + slug;
115
- var rest = urlPath.substring(prefix.length);
116
- return rest || "/";
117
- }
118
-
119
- /**
120
- * Create a multi-project server.
121
- * opts: { tlsOptions, caPath, pinHash, port, debug, dangerouslySkipPermissions }
122
- */
123
- function createServer(opts) {
124
- var tlsOptions = opts.tlsOptions || null;
125
- var caPath = opts.caPath || null;
126
- var pinHash = opts.pinHash || null;
127
- var portNum = opts.port || 2633;
128
- var debug = opts.debug || false;
129
- var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
130
- var lanHost = opts.lanHost || null;
131
- var onAddProject = opts.onAddProject || null;
132
- var onRemoveProject = opts.onRemoveProject || null;
133
-
134
- var authToken = pinHash || null;
135
- var realVersion = require("../package.json").version;
136
- var currentVersion = debug ? "0.0.9" : realVersion;
137
-
138
- var caContent = caPath ? (function () { try { return fs.readFileSync(caPath); } catch (e) { return null; } })() : null;
139
- var pinPage = pinPageHtml();
140
-
141
- // --- Project registry ---
142
- var projects = new Map(); // slug → projectContext
143
-
144
- // --- Push module (global) ---
145
- var pushModule = null;
146
- try {
147
- var { initPush } = require("./push");
148
- pushModule = initPush();
149
- } catch (e) {}
150
-
151
- // --- HTTP handler ---
152
- var appHandler = function (req, res) {
153
- var fullUrl = req.url.split("?")[0];
154
-
155
- // Global auth endpoint
156
- if (req.method === "POST" && req.url === "/auth") {
157
- var ip = req.socket.remoteAddress || "";
158
- var remaining = checkPinRateLimit(ip);
159
- if (remaining !== null) {
160
- res.writeHead(429, { "Content-Type": "application/json" });
161
- res.end(JSON.stringify({ ok: false, locked: true, retryAfter: remaining }));
162
- return;
163
- }
164
- var body = "";
165
- req.on("data", function (chunk) { body += chunk; });
166
- req.on("end", function () {
167
- try {
168
- var data = JSON.parse(body);
169
- if (authToken && generateAuthToken(data.pin) === authToken) {
170
- clearPinFailures(ip);
171
- res.writeHead(200, {
172
- "Set-Cookie": "relay_auth=" + authToken + "; Path=/; HttpOnly; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""),
173
- "Content-Type": "application/json",
174
- });
175
- res.end('{"ok":true}');
176
- } else {
177
- recordPinFailure(ip);
178
- var attemptsLeft = PIN_MAX_ATTEMPTS - (pinAttempts[ip] ? pinAttempts[ip].count : 0);
179
- res.writeHead(401, { "Content-Type": "application/json" });
180
- res.end(JSON.stringify({ ok: false, attemptsLeft: Math.max(attemptsLeft, 0) }));
181
- }
182
- } catch (e) {
183
- res.writeHead(400);
184
- res.end("Bad request");
185
- }
186
- });
187
- return;
188
- }
189
-
190
- // CA certificate download
191
- if (req.url === "/ca/download" && req.method === "GET" && caContent) {
192
- res.writeHead(200, {
193
- "Content-Type": "application/x-pem-file",
194
- "Content-Disposition": 'attachment; filename="claude-relay-ca.pem"',
195
- });
196
- res.end(caContent);
197
- return;
198
- }
199
-
200
- // CORS preflight for cross-origin requests (HTTP onboarding → HTTPS)
201
- if (req.method === "OPTIONS") {
202
- res.writeHead(204, {
203
- "Access-Control-Allow-Origin": "*",
204
- "Access-Control-Allow-Methods": "GET, OPTIONS",
205
- "Access-Control-Allow-Headers": "Content-Type",
206
- "Access-Control-Max-Age": "86400",
207
- });
208
- res.end();
209
- return;
210
- }
211
-
212
- // Setup page
213
- if (fullUrl === "/setup" && req.method === "GET") {
214
- var host = req.headers.host || "localhost";
215
- var hostname = host.split(":")[0];
216
- var protocol = tlsOptions ? "https" : "http";
217
- var setupUrl = protocol + "://" + hostname + ":" + portNum;
218
- var lanMode = /[?&]mode=lan/.test(req.url);
219
- res.writeHead(200, {
220
- "Content-Type": "text/html; charset=utf-8",
221
- "Access-Control-Allow-Origin": "*",
222
- });
223
- res.end(setupPageHtml(setupUrl, setupUrl, !!caContent, lanMode));
224
- return;
225
- }
226
-
227
- // Global push endpoints (used by setup page)
228
- if (req.method === "GET" && fullUrl === "/api/vapid-public-key" && pushModule) {
229
- res.writeHead(200, { "Content-Type": "application/json" });
230
- res.end(JSON.stringify({ publicKey: pushModule.publicKey }));
231
- return;
232
- }
233
-
234
- if (req.method === "POST" && fullUrl === "/api/push-subscribe" && pushModule) {
235
- var body = "";
236
- req.on("data", function (chunk) { body += chunk; });
237
- req.on("end", function () {
238
- try {
239
- var parsed = JSON.parse(body);
240
- var sub = parsed.subscription || parsed;
241
- pushModule.addSubscription(sub, parsed.replaceEndpoint);
242
- res.writeHead(200, { "Content-Type": "application/json" });
243
- res.end('{"ok":true}');
244
- } catch (e) {
245
- res.writeHead(400);
246
- res.end("Bad request");
247
- }
248
- });
249
- return;
250
- }
251
-
252
- // Theme list: bundled (lib/themes/) + user (~/.claude-relay/themes/)
253
- if (req.method === "GET" && fullUrl === "/api/themes") {
254
- var bundled = {};
255
- var custom = {};
256
- // Read bundled themes
257
- try {
258
- var bFiles = fs.readdirSync(bundledThemesDir);
259
- for (var i = 0; i < bFiles.length; i++) {
260
- if (!bFiles[i].endsWith(".json")) continue;
261
- try {
262
- var raw = fs.readFileSync(path.join(bundledThemesDir, bFiles[i]), "utf8");
263
- var id = bFiles[i].replace(/\.json$/, "");
264
- bundled[id] = JSON.parse(raw);
265
- } catch (e) {}
266
- }
267
- } catch (e) {}
268
- // Read user themes (override bundled if same id)
269
- try {
270
- var uFiles = fs.readdirSync(userThemesDir);
271
- for (var j = 0; j < uFiles.length; j++) {
272
- if (!uFiles[j].endsWith(".json")) continue;
273
- try {
274
- var uRaw = fs.readFileSync(path.join(userThemesDir, uFiles[j]), "utf8");
275
- var uid = uFiles[j].replace(/\.json$/, "");
276
- custom[uid] = JSON.parse(uRaw);
277
- } catch (e) {}
278
- }
279
- } catch (e) {}
280
- res.writeHead(200, { "Content-Type": "application/json" });
281
- res.end(JSON.stringify({ bundled: bundled, custom: custom }));
282
- return;
283
- }
284
-
285
- // Root path — dashboard or redirect
286
- if (fullUrl === "/" && req.method === "GET") {
287
- if (!isAuthed(req, authToken)) {
288
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
289
- res.end(pinPage);
290
- return;
291
- }
292
- var hasGoneParam = req.url.indexOf("gone=") !== -1;
293
- if (projects.size === 1 && !hasGoneParam) {
294
- var slug = projects.keys().next().value;
295
- res.writeHead(302, { "Location": "/p/" + slug + "/" });
296
- res.end();
297
- return;
298
- }
299
- var statusList = [];
300
- projects.forEach(function (ctx) { statusList.push(ctx.getStatus()); });
301
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
302
- res.end(dashboardPageHtml(statusList, currentVersion));
303
- return;
304
- }
305
-
306
- // Global info endpoint (auth required)
307
- if (req.method === "GET" && req.url === "/info") {
308
- if (!isAuthed(req, authToken)) {
309
- res.writeHead(401, {
310
- "Content-Type": "application/json",
311
- "Access-Control-Allow-Origin": "*",
312
- });
313
- res.end('{"error":"unauthorized"}');
314
- return;
315
- }
316
- var projectList = [];
317
- projects.forEach(function (ctx, slug) {
318
- projectList.push({ slug: slug, project: ctx.project });
319
- });
320
- res.end(JSON.stringify({ projects: projectList, version: currentVersion }));
321
- return;
322
- }
323
-
324
- // Static files at root (favicon, manifest, icons, sw.js, etc.)
325
- if (fullUrl.lastIndexOf("/") === 0 && !fullUrl.includes("..")) {
326
- if (serveStatic(fullUrl, res)) return;
327
- }
328
-
329
- // Project-scoped routes: /p/{slug}/...
330
- var slug = extractSlug(req.url.split("?")[0]);
331
- if (!slug) {
332
- // Not a project route and not handled above
333
- res.writeHead(404);
334
- res.end("Not found");
335
- return;
336
- }
337
-
338
- var ctx = projects.get(slug);
339
- if (!ctx) {
340
- res.writeHead(302, { "Location": "/?gone=" + encodeURIComponent(slug) });
341
- res.end();
342
- return;
343
- }
344
-
345
- // Redirect /p/{slug} → /p/{slug}/ (trailing slash required for relative paths)
346
- if (fullUrl === "/p/" + slug) {
347
- res.writeHead(301, { "Location": "/p/" + slug + "/" });
348
- res.end();
349
- return;
350
- }
351
-
352
- // Auth check for project routes
353
- if (!isAuthed(req, authToken)) {
354
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
355
- res.end(pinPage);
356
- return;
357
- }
358
-
359
- // Strip prefix for project-scoped handling
360
- var projectUrl = stripPrefix(req.url.split("?")[0], slug);
361
- // Re-attach query string for API routes
362
- var qsIdx = req.url.indexOf("?");
363
- var projectUrlWithQS = qsIdx >= 0 ? projectUrl + req.url.substring(qsIdx) : projectUrl;
364
-
365
- // Try project HTTP handler first (APIs)
366
- var origUrl = req.url;
367
- req.url = projectUrlWithQS;
368
- var handled = ctx.handleHTTP(req, res, projectUrlWithQS);
369
- req.url = origUrl;
370
- if (handled) return;
371
-
372
- // Static files (same assets for all projects)
373
- if (req.method === "GET") {
374
- if (serveStatic(projectUrl, res)) return;
375
- }
376
-
377
- res.writeHead(404);
378
- res.end("Not found");
379
- };
380
-
381
- // --- Server setup ---
382
- var server;
383
- if (tlsOptions) {
384
- server = require("https").createServer(tlsOptions, appHandler);
385
- } else {
386
- server = http.createServer(appHandler);
387
- }
388
-
389
- // --- HTTP onboarding server (only when TLS is active) ---
390
- var onboardingServer = null;
391
- if (tlsOptions) {
392
- onboardingServer = http.createServer(function (req, res) {
393
- var url = req.url.split("?")[0];
394
-
395
- // CA certificate download
396
- if (url === "/ca/download" && req.method === "GET" && caContent) {
397
- res.writeHead(200, {
398
- "Content-Type": "application/x-pem-file",
399
- "Content-Disposition": 'attachment; filename="claude-relay-ca.pem"',
400
- });
401
- res.end(caContent);
402
- return;
403
- }
404
-
405
- // Setup page
406
- if (url === "/setup" && req.method === "GET") {
407
- var host = req.headers.host || "localhost";
408
- var hostname = host.split(":")[0];
409
- var httpsSetupUrl = "https://" + hostname + ":" + portNum;
410
- var httpSetupUrl = "http://" + hostname + ":" + (portNum + 1);
411
- var lanMode = /[?&]mode=lan/.test(req.url);
412
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
413
- res.end(setupPageHtml(httpsSetupUrl, httpSetupUrl, !!caContent, lanMode));
414
- return;
415
- }
416
-
417
- // /info — CORS-enabled, used by setup page to verify HTTPS
418
- if (url === "/info" && req.method === "GET") {
419
- res.writeHead(200, {
420
- "Content-Type": "application/json",
421
- "Access-Control-Allow-Origin": "*",
422
- });
423
- res.end(JSON.stringify({ version: currentVersion }));
424
- return;
425
- }
426
-
427
- // Static files at root (favicon, manifest, icons, etc.)
428
- if (url.lastIndexOf("/") === 0 && !url.includes("..")) {
429
- if (serveStatic(url, res)) return;
430
- }
431
-
432
- // Everything else → redirect to HTTPS setup
433
- var hostname = (req.headers.host || "localhost").split(":")[0];
434
- res.writeHead(302, { "Location": "https://" + hostname + ":" + portNum + "/setup" });
435
- res.end();
436
- });
437
- }
438
-
439
- // --- WebSocket ---
440
- var wss = new WebSocketServer({ noServer: true });
441
-
442
- server.on("upgrade", function (req, socket, head) {
443
- // Origin validation (CSRF prevention)
444
- var origin = req.headers.origin;
445
- if (origin) {
446
- try {
447
- var originUrl = new URL(origin);
448
- var originPort = String(originUrl.port || (originUrl.protocol === "https:" ? "443" : "80"));
449
- // Extract port from Host header for reverse proxy support.
450
- // Use URL parser to correctly handle IPv6 addresses (e.g. [::1])
451
- // and infer default port from origin protocol (not backend tlsOptions)
452
- // so TLS-terminating proxies on :443 with HTTP backends work.
453
- var hostPort;
454
- try {
455
- var hostUrl = new URL(originUrl.protocol + "//" + (req.headers.host || ""));
456
- hostPort = String(hostUrl.port || (originUrl.protocol === "https:" ? "443" : "80"));
457
- } catch (e2) {
458
- hostPort = String(portNum);
459
- }
460
- if (originPort !== String(portNum) && originPort !== hostPort) {
461
- socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
462
- socket.destroy();
463
- return;
464
- }
465
- } catch (e) {
466
- socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
467
- socket.destroy();
468
- return;
469
- }
470
- }
471
-
472
- if (!isAuthed(req, authToken)) {
473
- socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
474
- socket.destroy();
475
- return;
476
- }
477
-
478
- // Extract slug from WS URL: /p/{slug}/ws
479
- var wsSlug = extractSlug(req.url);
480
- if (!wsSlug) {
481
- socket.destroy();
482
- return;
483
- }
484
-
485
- var ctx = projects.get(wsSlug);
486
- if (!ctx) {
487
- socket.destroy();
488
- return;
489
- }
490
-
491
- wss.handleUpgrade(req, socket, head, function (ws) {
492
- ctx.handleConnection(ws);
493
- });
494
- });
495
-
496
- // --- Project management ---
497
- function addProject(cwd, slug, title) {
498
- if (projects.has(slug)) return false;
499
- var ctx = createProjectContext({
500
- cwd: cwd,
501
- slug: slug,
502
- title: title || null,
503
- pushModule: pushModule,
504
- debug: debug,
505
- dangerouslySkipPermissions: dangerouslySkipPermissions,
506
- currentVersion: currentVersion,
507
- lanHost: lanHost,
508
- getProjectCount: function () { return projects.size; },
509
- getProjectList: function () {
510
- var list = [];
511
- projects.forEach(function (ctx) { list.push(ctx.getStatus()); });
512
- return list;
513
- },
514
- onAddProject: onAddProject,
515
- onRemoveProject: onRemoveProject,
516
- });
517
- projects.set(slug, ctx);
518
- ctx.warmup();
519
- return true;
520
- }
521
-
522
- function removeProject(slug) {
523
- var ctx = projects.get(slug);
524
- if (!ctx) return false;
525
- ctx.destroy();
526
- projects.delete(slug);
527
- return true;
528
- }
529
-
530
- function getProjects() {
531
- var list = [];
532
- projects.forEach(function (ctx) {
533
- list.push(ctx.getStatus());
534
- });
535
- return list;
536
- }
537
-
538
- function setProjectTitle(slug, title) {
539
- var ctx = projects.get(slug);
540
- if (!ctx) return false;
541
- ctx.setTitle(title);
542
- return true;
543
- }
544
-
545
- function setAuthToken(hash) {
546
- authToken = hash;
547
- }
548
-
549
- function broadcastAll(msg) {
550
- projects.forEach(function (ctx) {
551
- ctx.send(msg);
552
- });
553
- }
554
-
555
- function destroyAll() {
556
- projects.forEach(function (ctx, slug) {
557
- console.log("[server] Destroying project:", slug);
558
- ctx.destroy();
559
- });
560
- projects.clear();
561
- }
562
-
563
- return {
564
- server: server,
565
- onboardingServer: onboardingServer,
566
- isTLS: !!tlsOptions,
567
- addProject: addProject,
568
- removeProject: removeProject,
569
- getProjects: getProjects,
570
- setProjectTitle: setProjectTitle,
571
- setAuthToken: setAuthToken,
572
- broadcastAll: broadcastAll,
573
- destroyAll: destroyAll,
574
- };
575
- }
576
-
577
- module.exports = { createServer: createServer, generateAuthToken: generateAuthToken };