Frontend sends verifyId with host-key-accept so backend can correlate the
verification response. Backend tracks pre-ready connections in pendingClients
map, destroys on error/disconnect, and guards against calling verify() after
the connection has already timed out or errored (_destruct crash fix).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
63 tests across 8 spec files, all passing. Removes 2 stale stub files from
backend/test/ that were incompatible with the current async EncryptionService
and 3-argument AuthService constructor. New suite lives in src/ co-located
with source files per NestJS convention.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C-2: JWT moved from localStorage to httpOnly cookie (eliminates XSS token theft)
C-3: WebSocket auth via short-lived single-use tickets (JWT no longer in URLs)
H-1: JWT expiry reduced from 7 days to 4 hours
H-3: TOTP secrets encrypted at rest with vault EncryptionService (auto-migrates plaintext)
H-6: Rate limiting via @nestjs/throttler (60 req/min global, tighten on auth)
H-8: Constant-time login — Argon2id verify runs against dummy hash for non-existent users
H-9: Password hashing upgraded from bcrypt(10) to Argon2id (auto-upgrades on login)
H-10: Credential list API no longer returns encrypted blobs
H-16: Admin pages use Nuxt route middleware instead of client-side guard
Plus: auth bootstrap plugin, cookie-parser middleware, all frontend Authorization headers removed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- H-5: Redact keystroke data from WS message logs — log type/sessionId/bytes only
- H-4: Remove private key content/length/passphrase logging, replace with safe single line
- H-14: Remove username@hostname from password auth log, use hostId only
- M-1: Enforce session ownership in data/resize/disconnect handlers via clientSessions map
- C-5: Real host key verification flow — MITM protection blocks changed keys immediately,
new hosts ask user via host-key-verify WS message with 30s timeout, pending map resolves on
host-key-accept/host-key-reject response
- H-13: Shell PROMPT_COMMAND/precmd injection is now opt-in via options.enableCwdTracking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BREAKING CHANGE (forward-only): New credentials/keys encrypted with v2
(Argon2id-derived AES-256-GCM). Existing v1 records decrypt transparently.
- Argon2id params: 64 MiB memory, 3 iterations, 4 parallelism (OWASP)
- Per-record 16-byte salt stored in ciphertext format
- v2 format: v2:<salt>:<iv>:<authTag>:<ciphertext>
- Backwards compatible: v1 records still decrypt with raw key
- Admin endpoint POST /api/credentials/migrate-v2 upgrades all v1→v2
- Added docs/FUTURE-FEATURES.md with remaining spec gaps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full per-user data isolation across all tables:
- Migration adds userId FK to hosts, host_groups, credentials, ssh_keys,
connection_logs. Backfills existing data to admin@wraith.local.
- All services scope queries by userId from JWT (req.user.sub).
Users can only see/modify their own data. Cross-user access returns 403.
- Two roles: admin (full access + user management) and user (own data only).
- Admin endpoints: list/create/edit/delete users, reset password, reset TOTP.
Protected by AdminGuard. Admins cannot delete themselves or remove own role.
- JWT payload now includes role. Frontend auth store exposes isAdmin getter.
- Seed script fixed: checks for admin@wraith.local specifically (not any user).
Uses upsert, seeds with role=admin. Migration cleans up duplicate users.
- Connection logs now attributed to the connecting user via WS auth.
- Deleting a user CASCADEs to all their hosts, credentials, keys, and logs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Inject shell integration (PROMPT_COMMAND/precmd) on SSH connect that
emits OSC 7 escape sequences reporting the working directory on every
prompt. Supports bash and zsh.
- Frontend captures OSC 7 via xterm.js parser, updates session store CWD.
- SFTP sidebar watches session CWD and navigates when it changes.
- SFTP starts at ~/ (user home) instead of / on initial connect, resolved
via SFTP realpath('.') on the backend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ROOT CAUSE: guacd showed "User resolution is 0x0 at 0 DPI" and
immediately killed every RDP connection.
The Guacamole protocol requires five client capability instructions
(size, audio, video, image, timezone) BETWEEN receiving 'args' and
sending 'connect'. Our handshake skipped all five and jumped straight
to CONNECT. guacd never received the display dimensions, defaulted to
0x0, and terminated the connection.
Now sends the complete handshake:
select → (receive args) → size → audio → video → image → timezone → connect
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. TCP stream fragmentation — guacd→browser data pipe treated each TCP
chunk as a complete instruction. TCP is a stream protocol; instructions
WILL be split across chunks (especially display/image data). Added
instruction buffer that accumulates data and only forwards complete
instructions (terminated by ';').
2. Missing client.onerror — when guacd fails the RDP connection (NLA,
auth, TLS), it sends a Guacamole error instruction. No handler was
registered, so errors were silently swallowed. User saw blank canvas
with no feedback. Now surfaces errors via console and gateway callback.
3. Missing client.onstatechange — no connection state tracking. Added
state transition logging for diagnostics.
Also improved CONNECT handshake logging to surface connection parameters
(host, port, user, domain, security mode) without exposing passwords.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
guacd was dying silently with no error instruction sent back.
Enable -L debug -f for verbose FreeRDP diagnostics and log
first 5 guacd→browser instructions plus connection parameters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Echo VERSION_X_Y_Z args back to guacd in CONNECT handshake
- Set guacd to network_mode: host so it can reach RDP targets on
NetBird/Tailscale overlay networks (100.64.x.x)
- App container uses host.docker.internal to reach guacd on host
- Add diagnostic logging for guacd→browser instruction relay
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uses ssh2 utils.parseKey() to check if the key decrypts and
parses correctly, logs the key type and public key fingerprint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs key format, length, auth method selection, and ssh2 debug
output for auth/key events to diagnose why key auth is rejected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a credential's sshKeyId points to a deleted/missing SSH key row,
the connection attempt silently had zero auth methods. Now throws a
clear error explaining the SSH key is missing. Also catches the case
where a credential has neither password nor SSH key configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When SSH timed out, the onClose callback referenced `const sessionId`
before connect() resolved, causing a Temporal Dead Zone ReferenceError
that killed the process. Changed to `let` with try/catch so connection
failures send an error message to the client instead of crashing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Process-level uncaughtException/unhandledRejection handlers plus
try/catch around upgrade and connection handlers. This will log
whatever is crashing the server on browser WebSocket connections
before the process dies, instead of silently restarting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WsAdapter registered its upgrade handler first and destroyed sockets
for non-matching paths. Now we remove all existing upgrade listeners,
install ours first, and forward non-terminal/sftp upgrades to the
original WsAdapter handlers for RDP.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The NestJS WsAdapter silently swallowed WebSocket connections through
NPM despite 101 responses in the access log. Replaced with manual
ws.Server instances using noServer mode and explicit HTTP upgrade
event handling. Gateways are now plain @Injectable services, not
@WebSocketGateway decorators.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NPM forwards /api/* correctly but silently drops WebSocket upgrades on
/ws/* despite toggle being enabled and custom nginx config. Moving
gateways to /api/ws/terminal and /api/ws/sftp so they ride the same
proxy rules that already work for REST endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handleConnection never fires despite browser getting open event.
Adding server-level upgrade listener to see if upgrades reach NestJS.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With the NestJS ws adapter, the JWT token URL is on the HTTP upgrade
request (second arg to handleConnection), not on the WebSocket client
object. client.url was undefined, new URL(undefined) threw, catch
returned null, and every connection got 4001 Unauthorized.
Fix: Pass the IncomingMessage req to validateClient and prefer req.url.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dialogs: bypassed component-based dialogs entirely — inlined modals
directly in index.vue with inline style fallbacks for z-index/colors.
If button clicks work, we see the modal. Period.
Profile 500: created UpdateProfileDto with class-validator decorators
so ValidationPipe processes it correctly. Added error logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Home/Profile links to nav bar alongside Vault/Settings/Logout
- Profile page: change email, display name, password
- TOTP 2FA: setup with QR code, verify, disable with password
- Login flow: two-step TOTP challenge when 2FA is enabled
- Backend: new endpoints PUT /profile, POST /totp/setup|verify|disable
- Migration: add totp_secret and totp_enabled columns to users
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>