security(relay): minor hardening follow-ups from PR review

- Uniform auth responses remove the session-enumeration oracle: unknown
  session and wrong password now return an identical 401 at the API layer and
  an identical WS handshake/close; desktop-status and the 503 "not connected"
  signal are only exposed after successful auth.
- Session-count cap re-checked after bcrypt.hash so concurrent creates can't
  overshoot maxSessions.
- Relay web client reconnect backoff resets only after a successful auth
  response (an accept-then-close server no longer defeats the backoff).
- idGenerator comment corrected to match the rejection-sampling; drop unused
  recordFailure return value.
- DEPLOY.md: document ALLOWED_ORIGINS for reverse-proxy deployments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:13:33 -07:00
parent 74bd3c2e98
commit b5e5bed7dd
7 changed files with 78 additions and 41 deletions
+16 -1
View File
@@ -10,6 +10,13 @@ class MacroPadApp {
this.desktopConnected = false;
this.wsAuthenticated = false;
// Reconnect backoff. Reset only after a successful AUTH (not on
// socket open): a server that accepts the socket and then closes it
// would otherwise reset the backoff on every open and defeat it.
this.baseReconnectDelay = 3000;
this.maxReconnectDelay = 30000;
this.reconnectDelay = this.baseReconnectDelay;
// Get session ID from URL
const pathMatch = window.location.pathname.match(/^\/([a-zA-Z0-9]+)/);
this.sessionId = pathMatch ? pathMatch[1] : null;
@@ -117,7 +124,12 @@ class MacroPadApp {
this.ws.onclose = () => {
this.wsAuthenticated = false;
this.updateConnectionStatus(false);
setTimeout(() => this.setupWebSocket(), 3000);
// Reconnect using the current backoff, then grow it (capped) for
// the next attempt. The backoff is only reset on a successful
// auth response, so accept-then-close cycles keep backing off.
const delay = this.reconnectDelay;
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
setTimeout(() => this.setupWebSocket(), delay);
};
this.ws.onerror = () => this.updateConnectionStatus(false);
@@ -133,6 +145,9 @@ class MacroPadApp {
case 'auth_response':
if (data.success) {
this.wsAuthenticated = true;
// Reset the reconnect backoff only after a confirmed,
// successful auth (not merely on socket open).
this.reconnectDelay = this.baseReconnectDelay;
this.updateConnectionStatus(this.desktopConnected);
} else {
this.handleAuthError();