@agent-relay/dashboard-server 2.0.70 → 2.0.72

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 (59) hide show
  1. package/dist/server.js +39 -34
  2. package/dist/server.js.map +1 -1
  3. package/out/404.html +1 -1
  4. package/out/_next/static/chunks/{285-271fc707e03bb4c5.js → 285-52fb0aee5b6b90a6.js} +1 -1
  5. package/out/about.html +1 -1
  6. package/out/about.txt +1 -1
  7. package/out/app/onboarding.html +1 -1
  8. package/out/app/onboarding.txt +1 -1
  9. package/out/app.html +1 -1
  10. package/out/app.txt +2 -2
  11. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
  12. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  13. package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
  14. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  15. package/out/blog.html +1 -1
  16. package/out/blog.txt +1 -1
  17. package/out/careers.html +1 -1
  18. package/out/careers.txt +1 -1
  19. package/out/changelog.html +1 -1
  20. package/out/changelog.txt +1 -1
  21. package/out/cloud/link.html +1 -1
  22. package/out/cloud/link.txt +1 -1
  23. package/out/complete-profile.html +1 -1
  24. package/out/complete-profile.txt +1 -1
  25. package/out/connect-repos.html +1 -1
  26. package/out/connect-repos.txt +1 -1
  27. package/out/contact.html +1 -1
  28. package/out/contact.txt +1 -1
  29. package/out/docs.html +1 -1
  30. package/out/docs.txt +1 -1
  31. package/out/history.html +1 -1
  32. package/out/history.txt +1 -1
  33. package/out/index.html +1 -1
  34. package/out/index.txt +2 -2
  35. package/out/login.html +1 -1
  36. package/out/login.txt +1 -1
  37. package/out/metrics.html +1 -1
  38. package/out/metrics.txt +1 -1
  39. package/out/pricing.html +1 -1
  40. package/out/pricing.txt +1 -1
  41. package/out/privacy.html +1 -1
  42. package/out/privacy.txt +1 -1
  43. package/out/providers/setup/claude.html +1 -1
  44. package/out/providers/setup/claude.txt +1 -1
  45. package/out/providers/setup/codex.html +1 -1
  46. package/out/providers/setup/codex.txt +1 -1
  47. package/out/providers/setup/cursor.html +1 -1
  48. package/out/providers/setup/cursor.txt +1 -1
  49. package/out/providers.html +1 -1
  50. package/out/providers.txt +1 -1
  51. package/out/security.html +1 -1
  52. package/out/security.txt +1 -1
  53. package/out/signup.html +1 -1
  54. package/out/signup.txt +1 -1
  55. package/out/terms.html +1 -1
  56. package/out/terms.txt +1 -1
  57. package/package.json +1 -1
  58. /package/out/_next/static/{-BPTMieIVPN3UZGwSczoP → ZAMJuS0sABKOx3XCZHCpH}/_buildManifest.js +0 -0
  59. /package/out/_next/static/{-BPTMieIVPN3UZGwSczoP → ZAMJuS0sABKOx3XCZHCpH}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -989,6 +989,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
989
989
  };
990
990
  // Set up direct message handler to forward messages to presence WebSocket
991
991
  // This enables agents to send replies that appear in the dashboard UI
992
+ // Note: the relay daemon already persists messages authoritatively, so we
993
+ // only need to broadcast the real-time event here (no storage.saveMessage).
992
994
  client.onMessage = (from, payload, messageId) => {
993
995
  const body = typeof payload === 'object' && payload !== null && 'body' in payload
994
996
  ? payload.body
@@ -1000,28 +1002,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1000
1002
  // Determine entity type: user if they have presence state, agent otherwise
1001
1003
  const fromEntityType = senderPresence ? 'user' : 'agent';
1002
1004
  const timestamp = new Date().toISOString();
1003
- // Persist the message to storage so it survives page refresh
1004
- if (storage) {
1005
- storage.saveMessage({
1006
- id: messageId || `dm-${crypto.randomUUID()}`,
1007
- ts: Date.now(),
1008
- from,
1009
- to: senderName,
1010
- topic: undefined,
1011
- kind: 'message',
1012
- body,
1013
- data: {
1014
- fromAvatarUrl,
1015
- fromEntityType,
1016
- },
1017
- status: 'unread',
1018
- is_urgent: false,
1019
- is_broadcast: false,
1020
- }).catch((err) => {
1021
- console.error('[dashboard] Failed to persist direct message', err);
1022
- });
1023
- }
1024
- // Broadcast to presence WebSocket clients so cloud/dashboard can display the message
1005
+ // Broadcast real-time event so the dashboard UI updates immediately.
1006
+ // Pass id (= messageId) so the client has a stable identifier and
1007
+ // doesn't need to fabricate one from Date.now().
1025
1008
  broadcastDirectMessage({
1026
1009
  type: 'direct_message',
1027
1010
  targetUser: senderName,
@@ -1029,6 +1012,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1029
1012
  fromAvatarUrl,
1030
1013
  fromEntityType,
1031
1014
  body,
1015
+ id: messageId,
1032
1016
  messageId,
1033
1017
  timestamp,
1034
1018
  });
@@ -2650,7 +2634,6 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2650
2634
  };
2651
2635
  // Helper to broadcast direct messages to all connected clients
2652
2636
  // This enables agent replies to appear in the dashboard UI
2653
- // Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
2654
2637
  const broadcastDirectMessage = (message) => {
2655
2638
  // Push into buffer and wrap with sequence ID for replay support
2656
2639
  const rawPayload = JSON.stringify(message);
@@ -2664,15 +2647,20 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2664
2647
  client.send(payload);
2665
2648
  }
2666
2649
  });
2667
- // Also broadcast to presence WebSocket clients (cloud mode)
2668
- const presenceClients = Array.from(wssPresence.clients).filter(c => c.readyState === WebSocket.OPEN);
2669
- if (presenceClients.length > 0) {
2670
- debug(`[dashboard] Broadcasting direct_message to ${presenceClients.length} presence clients`);
2671
- wssPresence.clients.forEach((client) => {
2672
- if (client.readyState === WebSocket.OPEN) {
2673
- client.send(payload);
2674
- }
2675
- });
2650
+ // Only broadcast to presence WS if UserBridge does NOT have a session for
2651
+ // the target user. When UserBridge is active it already delivers the DM
2652
+ // directly to the user's WebSocket(s), so broadcasting here would duplicate.
2653
+ const targetHandledByBridge = userBridge?.isUserRegistered(message.targetUser) ?? false;
2654
+ if (!targetHandledByBridge) {
2655
+ const presenceClients = Array.from(wssPresence.clients).filter(c => c.readyState === WebSocket.OPEN);
2656
+ if (presenceClients.length > 0) {
2657
+ debug(`[dashboard] Broadcasting direct_message to ${presenceClients.length} presence clients (no bridge session)`);
2658
+ wssPresence.clients.forEach((client) => {
2659
+ if (client.readyState === WebSocket.OPEN) {
2660
+ client.send(payload);
2661
+ }
2662
+ });
2663
+ }
2676
2664
  }
2677
2665
  };
2678
2666
  // Helper to get online users list (without ws references)
@@ -4827,10 +4815,22 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4827
4815
  const repoName = fullName.split('/').pop();
4828
4816
  const workspaceDir = process.env.WORKSPACE_DIR || path.dirname(projectRoot || dataDir);
4829
4817
  const targetDir = path.join(workspaceDir, repoName);
4830
- // Idempotent: skip if already cloned
4831
- if (fs.existsSync(targetDir)) {
4818
+ // Prevent path traversal (e.g., repoName = ".." or ".")
4819
+ const resolvedTarget = path.resolve(targetDir);
4820
+ const resolvedWorkspace = path.resolve(workspaceDir);
4821
+ if (!resolvedTarget.startsWith(resolvedWorkspace + path.sep)) {
4822
+ return res.status(400).json({ success: false, error: 'Invalid path' });
4823
+ }
4824
+ // Idempotent: skip if already cloned (check for .git to avoid false positives
4825
+ // from empty directories left behind by previous failed clone attempts)
4826
+ if (fs.existsSync(path.join(targetDir, '.git'))) {
4832
4827
  return res.json({ success: true, message: 'Already cloned', path: targetDir });
4833
4828
  }
4829
+ // Clean up stale directory from a previous failed clone (no .git = incomplete)
4830
+ if (fs.existsSync(targetDir)) {
4831
+ console.log(`[api/repos/clone] Removing stale directory ${targetDir} (no .git found)`);
4832
+ fs.rmSync(targetDir, { recursive: true, force: true });
4833
+ }
4834
4834
  // Use plain HTTPS URL - git credential helper handles authentication.
4835
4835
  // The credential helper (git-credential-relay) fetches per-repo tokens from
4836
4836
  // the cloud API, which correctly resolves installation tokens for private repos.
@@ -4854,6 +4854,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4854
4854
  catch (err) {
4855
4855
  const safeMessage = (err.message || 'Clone failed');
4856
4856
  console.error('[api/repos/clone] Clone failed:', safeMessage);
4857
+ // Clean up failed clone directory so future attempts aren't blocked
4858
+ try {
4859
+ fs.rmSync(targetDir, { recursive: true, force: true });
4860
+ }
4861
+ catch { /* ignore */ }
4857
4862
  res.status(500).json({ success: false, error: safeMessage });
4858
4863
  }
4859
4864
  });