Authentication

UI = MCP — single surface

Every protected endpoint accepts either authentication scheme. Whatever the LDM web app does, an autonomous agent (Claude Desktop, Cursor, custom MCP client, n8n, server-to-server integration) can do via the same URL — just swap the JWT cookie for a Bearer token. There is no parallel /v1/* mirror to keep in sync.

Internally a single HybridAuthGuard resolves either method to a uniform req.user + req.tenantId so controller code is identical for both callers. Per-method @RequireScope(...) enforces fine-grained scope checks for Bearer keys; UI sessions get the scopes their role grants.

Bearer token (agents / MCP / SDK)

Tokens are issued by POST /v1/signup and look like ldm_pk_<random>. Send them in the Authorization header on every request; scopes are checked against the key.

Authorization: Bearer ldm_pk_a1b2c3d4...

Use Bearer for: AI agents (Claude Desktop, Cursor), MCP servers, scheduled scripts, server-to-server integrations. The agent-card at /.well-known/agent-card.json advertises this scheme so discovery-aware clients can find the auth requirements without prior config.

Cookie / JWT session (web UI)

Visit https://app.live-direct-marketing.online/login and authenticate with email + password. The browser receives an HttpOnly cookie carrying a short-lived JWT; subsequent /api/* requests use it automatically (no manual header).

# After /login completes, the UI calls the API like this — same endpoints,
# auth comes from the session cookie set by /login.
curl -b cookies.txt https://api.live-direct-marketing.online/api/companies

Use Cookie/JWT for: the LDM web app itself, manual exploration in a browser, local dev. Roles OWNER and ADMIN carry all scopes; other roles get scopes per their assignment in the workspace.

Generic 401 (anti-enumeration)

A missing header, malformed scheme, unknown key, or revoked key — all four failure modes return the same response. Distinct error messages would let attackers enumerate valid key prefixes or formats.

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "statusCode": 401,
  "message": "Invalid credentials"
}

If you receive this response, verify (in order): (1) the Authorization header is sent at all, (2) the value starts with Bearer (with the trailing space), (3) the key was copied verbatim from /v1/signup response, (4) the key has not been revoked.

Server-side storage

Plaintext keys are returned by /v1/signup exactly once. The server stores only a SHA-256 hash with a 7-character prefix for fast lookup. The plaintext is never logged.