Get started

Auth Service Documentation

The Auth Service handles login, user accounts, and permissions. It makes sure only the right people can access Quox and controls what each person is allowed to do.

What is Authentication?

Authentication is how systems verify you are who you say you are. It is the digital equivalent of showing your ID at the door.

In simple terms: When you log in with a username and password, you are authenticating. The Auth Service is Quox's bouncer - it checks IDs, remembers who is allowed in, and keeps track of what everyone is permitted to do.

Why Does Quox Need an Auth Service?

Without authentication, anyone could access your infrastructure through Quox. The Auth Service provides:

  • User accounts - Everyone gets their own login, so actions can be tracked to individuals
  • Organizations - Group users into teams or companies, each with their own private space
  • Permissions - Control who can do what (some people can just view, others can make changes)
  • Security features - Protect accounts with two-factor authentication and detect suspicious activity

Key Concepts Explained

TermWhat It Means
AuthenticationProving who you are (logging in)
AuthorizationDetermining what you are allowed to do once logged in
JWT (JSON Web Token)A secure "ticket" that proves you have logged in, used for each request
MFA (Multi-Factor Authentication)Extra security where you need your password AND a code from your phone
RBAC (Role-Based Access Control)Giving permissions based on roles like "Admin" or "Viewer"
OrganisationA group of users who share resources (like a company or team)

How login works (simplified)

  1. You enter your email and password
  2. The Auth Service checks if they match what is stored (passwords are encrypted, never stored in plain text)
  3. If correct, you receive a token (like a wristband at an event)
  4. Every time you do something in Quox, your token is checked to verify it is really you
  5. Tokens expire after a while for security, so you may need to log in again periodically

Understanding Roles

Different people need different levels of access. Quox uses roles to manage this:

RoleWhat They Can Do
ViewerLook at information, but cannot make changes
RunnerExecute workflows and run commands
BuilderCreate new agents, workflows, and tools
AdminManage users, change settings, view audit logs
OwnerFull control, including deleting the organisation

Multi-Factor Authentication (MFA)

MFA adds an extra layer of security. Even if someone steals your password, they cannot get in without your phone:

  1. Enable MFA in your account settings
  2. Scan a QR code with an authenticator app (like Google Authenticator or Authy)
  3. When logging in, enter your password AND the 6-digit code from the app
  4. Backup codes are provided in case you lose access to your phone
i

Tip for beginners: Always enable MFA for accounts that access important systems. It takes just a few seconds during login but makes your account much harder to hack. Think of it as adding a deadbolt to your door in addition to the regular lock.


Quox Auth Service Documentation

Version: 1.0.0 Status: Phase A Complete Location: /services/auth/


Overview

The Quox Auth Service provides authentication, authorization, and organization management for the Quox Enterprise Workspace. It is a standalone Node.js microservice using SQLite for storage, designed for self-hosted deployments.

Key Features

  • User Authentication - Email/password with Argon2id hashing
  • JWT Sessions - Short-lived access tokens + rotating refresh tokens
  • MFA Support - TOTP (authenticator apps) with backup codes
  • Organizations - Multi-tenant isolation with membership management
  • RBAC - Role-based access control with fine-grained capabilities
  • Invites - Email-based invitation system with expiring tokens
  • Audit Logging - Complete trail of authentication and authorization events
  • Rate Limiting - Protection against brute force and abuse

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      Quox Auth Service                        │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │   Routes    │  │ Middleware  │  │   Utils     │              │
│  ├─────────────┤  ├─────────────┤  ├─────────────┤              │
│  │ auth.js     │  │ authenticate│  │ crypto.js   │              │
│  │ orgs.js     │  │ authorize   │  │ policy.js   │              │
│  │ invites.js  │  │ rateLimit   │  │ audit.js    │              │
│  │ mfa.js      │  │             │  │             │              │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘              │
│         │                │                │                      │
│         └────────────────┼────────────────┘                      │
│                          │                                       │
│                   ┌──────▼──────┐                                │
│                   │   SQLite    │                                │
│                   │   Database  │                                │
│                   └─────────────┘                                │
└─────────────────────────────────────────────────────────────────┘
          Port 3101

Installation

Prerequisites

  • Node.js 20+
  • npm or yarn

Setup

bash
cd /home/control/quox-dashboard/services/auth

# Install dependencies
npm install

# Copy environment template
cp .env.example .env

# Edit configuration
vim .env

# Start service
./start.sh
# or: npm start

Environment Variables

VariableDefaultDescription
AUTH_PORT3101Server port
AUTH_HOST0.0.0.0Bind address
AUTH_DB_PATH/var/lib/quox/auth.dbSQLite database path
JWT_SECRET(required)Secret for signing JWTs
JWT_ACCESS_EXPIRY15mAccess token lifetime
JWT_REFRESH_EXPIRY7dRefresh token lifetime
CORS_ORIGINSlocalhostComma-separated allowed origins
NODE_ENVdevelopmentEnvironment mode

Database Schema

Tables

TablePurpose
usersGlobal user accounts
user_emailsEmail addresses (multiple per user)
auth_identitiesAuthentication methods (password, OAuth)
sessionsActive sessions with refresh tokens
mfa_backup_codesTOTP backup codes
orgsOrganizations (tenants)
org_membershipsUser membership in organizations
teamsTeams within organizations
team_membershipsUser membership in teams
invitesPending invitations
service_accountsNon-human API identities
secretsEncrypted secret storage
integration_credentialsEncrypted API credentials per-org
instance_configGlobal instance configuration (encrypted values)
api_keysInstance-level API keys
audit_eventsAll security-relevant events
rate_limitsRequest rate tracking

Entity Relationships

User (1) ──────────┬──── (N) UserEmail
                   ├──── (N) AuthIdentity
                   ├──── (N) Session
                   ├──── (N) OrgMembership ──── (1) Org
                   └──── (N) TeamMembership ──── (1) Team ──── (1) Org

API Reference

Authentication

POST /api/auth/signup

Create a new user account.

Request:

json
{
  "email": "[email protected]",
  "password": "SecurePassword123",
  "display_name": "John Doe"
}

Response (201):

json
{
  "success": true,
  "message": "Account created. Please check your email to verify your account.",
  "user": {
    "id": "01KFG2V1W9H6BASQCR3XNZHAY1",
    "email": "[email protected]",
    "display_name": "John Doe",
    "status": "pending_verification"
  }
}

Validation:

  • Email must be valid format
  • Password minimum 12 characters with at least one letter and one number
  • Email must not already exist

POST /api/auth/login

Authenticate and receive tokens.

Request:

json
{
  "email": "[email protected]",
  "password": "SecurePassword123",
  "org_id": "01KFG..." // optional - set active org context
}

Response (200):

json
{
  "success": true,
  "user": {
    "id": "01KFG...",
    "email": "[email protected]",
    "display_name": "John Doe",
    "mfa_enabled": false
  },
  "org": {
    "id": "01KFG...",
    "name": "Acme Corp",
    "slug": "acme-corp",
    "role": "admin"
  },
  "mfa_required": false,
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900
}

Errors:

  • invalid_credentials - Wrong email or password
  • email_not_verified - Must verify email first
  • account_suspended - Account has been suspended

POST /api/auth/logout

Revoke current session.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "success": true,
  "message": "Logged out successfully"
}

POST /api/auth/refresh

Get new access token using refresh token.

Request:

json
{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

Response (200):

json
{
  "success": true,
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900
}

POST /api/auth/verify-email

Verify email address with token from email link.

Request:

json
{
  "token": "abc123..."
}

Response (200):

json
{
  "success": true,
  "message": "Email verified successfully"
}

POST /api/auth/forgot-password

Request password reset email.

Request:

json
{
  "email": "[email protected]"
}

Response (200):

json
{
  "success": true,
  "message": "If the email exists, a password reset link has been sent"
}

Note: Always returns success to prevent email enumeration.


POST /api/auth/reset-password

Complete password reset with token.

Request:

json
{
  "token": "abc123...",
  "password": "NewSecurePassword123"
}

Response (200):

json
{
  "success": true,
  "message": "Password reset successfully. Please log in with your new password."
}

GET /api/auth/me

Get current authenticated user info.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "user": {
    "id": "01KFG...",
    "email": "[email protected]",
    "display_name": "John Doe",
    "avatar_url": null,
    "status": "active",
    "mfa_enabled": false,
    "created_at": "2026-01-21 10:50:18"
  },
  "organizations": [
    {
      "id": "01KFG...",
      "name": "Acme Corp",
      "slug": "acme-corp",
      "role": "owner",
      "plan": "free"
    }
  ],
  "current_org": null
}

Organizations

POST /api/orgs

Create a new organization. Creator becomes owner.

Headers: Authorization: Bearer <access_token>

Request:

json
{
  "name": "Acme Corporation",
  "slug": "acme-corp" // optional - auto-generated if omitted
}

Response (201):

json
{
  "success": true,
  "organization": {
    "id": "01KFG...",
    "name": "Acme Corporation",
    "slug": "acme-corp",
    "plan": "free",
    "role": "owner"
  }
}

GET /api/orgs

List organizations the user belongs to.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "organizations": [
    {
      "id": "01KFG...",
      "name": "Acme Corporation",
      "slug": "acme-corp",
      "plan": "free",
      "role": "owner",
      "membership_status": "active"
    }
  ]
}

GET /api/orgs/:orgId

Get organization details.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "organization": {
    "id": "01KFG...",
    "name": "Acme Corporation",
    "slug": "acme-corp",
    "plan": "free",
    "logo_url": null,
    "mfa_policy": "optional",
    "created_at": "2026-01-21 10:51:52",
    "member_count": 5
  },
  "membership": {
    "role": "owner",
    "status": "active",
    "joined_at": "2026-01-21 10:51:52"
  }
}

GET /api/orgs/:orgId/members

List organization members.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "members": [
    {
      "id": "01KFG...",
      "email": "[email protected]",
      "display_name": "John Doe",
      "role": "owner",
      "status": "active",
      "joined_at": "2026-01-21 10:51:52"
    }
  ],
  "invites": [
    {
      "email": "[email protected]",
      "role": "builder",
      "status": "pending",
      "created_at": "2026-01-21 11:00:00",
      "expires_at": "2026-01-28 11:00:00"
    }
  ]
}

POST /api/orgs/:orgId/members

Invite a new member. Requires admin role.

Headers: Authorization: Bearer <access_token>

Request:

json
{
  "email": "[email protected]",
  "role": "builder"
}

Response (201):

json
{
  "success": true,
  "invite": {
    "id": "01KFG...",
    "email": "[email protected]",
    "role": "builder",
    "expires_at": "2026-01-28 11:00:00"
  }
}

MFA

POST /api/mfa/setup

Begin MFA setup. Returns secret and QR code.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "success": true,
  "secret": "JBSWY3DPEHPK3PXP",
  "qr_code": "data:image/png;base64,...",
  "otpauth_url": "otpauth://totp/Quox:[email protected]?secret=...",
  "message": "Scan the QR code with your authenticator app, then verify with a code"
}

POST /api/mfa/verify-setup

Complete MFA setup by verifying first code.

Headers: Authorization: Bearer <access_token>

Request:

json
{
  "code": "123456"
}

Response (200):

json
{
  "success": true,
  "message": "MFA enabled successfully",
  "backup_codes": [
    "ABCD-1234",
    "EFGH-5678",
    // ... 10 codes total
  ],
  "warning": "Save these backup codes in a safe place. They will not be shown again."
}

POST /api/mfa/verify

Verify MFA code during login to elevate session.

Headers: Authorization: Bearer <access_token>

Request:

json
{
  "code": "123456"
  // OR
  "backup_code": "ABCD-1234"
}

Response (200):

json
{
  "success": true,
  "message": "MFA verified",
  "mfa_verified": true,
  "backup_codes_remaining": 9
}

Invites

GET /api/invites/:token

Get invite details (public - no auth required).

Response (200):

json
{
  "invite": {
    "email": "[email protected]",
    "role": "builder",
    "organization": {
      "name": "Acme Corporation",
      "slug": "acme-corp"
    },
    "invited_by": {
      "display_name": "John Doe",
      "email": "[email protected]"
    },
    "expires_at": "2026-01-28 11:00:00"
  }
}

POST /api/invites/:token/accept

Accept an invite. Must be logged in as the invited email.

Headers: Authorization: Bearer <access_token>

Response (200):

json
{
  "success": true,
  "message": "Invite accepted",
  "organization": {
    "id": "01KFG...",
    "name": "Acme Corporation",
    "slug": "acme-corp",
    "role": "builder"
  }
}

RBAC Model

Roles

RoleLevelDescription
owner5Full control, can delete org, transfer ownership
admin4Manage members, roles, policies, audit
builder3Create assets (assistants, agents, tools, workflows)
runner2Execute workflows, view runs
viewer1Read-only access

Custom Role Overlays (Enterprise)

Enterprise organisations can extend built-in roles with custom capability bundles. A membership keeps its built-in base role (owner / admin / …) and may additionally carry a custom_role_id that grants extra capabilities on top. Capabilities are unioned at request time — there is no way for a custom role to remove a built-in capability, only to add.

PUT /api/orgs/:orgId/members/:userId/custom-role
    { "custom_role_id": "01KF..." | null }   # null = clear overlay

Capability grants may be org-wide or scope-bound:

  • scope_id = NULL — the capability applies anywhere in the org.
  • scope_id = <scope ULID> — the capability only applies for requests that explicitly target that scope (e.g. a team).

Cross-org protection: a custom role that belongs to a different org is silently ignored by the resolver. A custom role assigned to a membership must live in the same org (enforced on PUT).

Capabilities

Roles map to fine-grained capabilities:

javascript
// Organization management
'org.manage_members'      // Invite, remove, change roles
'org.manage_roles'        // Configure role permissions
'org.manage_sso'          // Configure SSO/SAML
'org.view_audit'          // View audit logs
'org.delete'              // Delete organization

// Team management
'team.create'             // Create teams
'team.manage'             // Manage team settings
'team.delete'             // Delete teams

// Directory assets
'directory.create_assistant'
'directory.create_agent'
'directory.create_tool'
'directory.create_workflow'
'directory.publish'       // Publish org-wide

// Runtime
'runtime.run_workflow'    // Execute workflows
'runtime.cancel_run'      // Cancel running workflows
'runtime.view_runs'       // View execution history

// HITL
'hitl.approve'            // Approve/deny requests

// Secrets
'secrets.manage'          // Create/update/delete secrets
'secrets.view'            // View secret metadata

// Service accounts
'service_accounts.manage' // Manage API keys

Policy Engine

The policy engine provides a single authorization check:

javascript
import { can } from './utils/policy.js';

const result = can(
  // Principal (who)
  { type: 'user', id: '01KFG...', orgRole: 'builder' },

  // Action (what)
  'directory.create_workflow',

  // Resource (on what)
  { type: 'workflow', orgId: '01KFG...' },

  // Context
  { mfaVerified: true }
);

// result: { allowed: true, reason: 'allowed' }
// or:     { allowed: false, reason: 'role_builder_lacks_capability_org.manage_members' }

Enterprise Enforcement (ET-ENFORCE)

Stream ET-ENFORCE took every governance feature that was stored-but-not-enforced and wired it into the request path. Each control below runs at request time, on the credential mint paths, AND on the deprovision side.

Session policy

session_policies table holds per-org idle_timeout_sec (default 30 min) and max_session_sec (default 12 h). Access tokens issued at login / refresh / switch-org / SSO callback all carry session_id. The authenticate middleware calls enforceSessionPolicy() on every request:

  • session row missing or revoked → 401 session_expired
  • idle window exceeded → revoke session, 401 session_expired idle_timeout
  • max-session window exceeded → revoke, 401 session_expired max_session
  • otherwise → stamp last_active_at

The refresh endpoint runs the same check, so a captured refresh token cannot mint new access tokens past the policy windows.

GET    /api/orgs/:orgId/security/session-policy   (Enterprise admin)
PUT    /api/orgs/:orgId/security/session-policy   { idleTimeoutSec, maxSessionSec, rememberMeEnabled }

IP allowlist

ip_allowlist table holds per-org IPv4 CIDR rules. Empty list = allow-all (opt-in). Enforced at:

  • POST /api/auth/login — refuses 403 ip_not_allowed before issuing credentials
  • every authenticated request — authenticate() re-checks against the JWT's org_id
  • POST /api/auth/refresh — captured token can't be refreshed from outside the network
  • POST /api/auth/switch-org — pivoting into a stricter-allowlist org is gated
  • SSO callback (server.js issueSession) — same check before cookie issuance
  • POST /api/invites/:token/signup-and-accept — guests can't bypass via invite link

IPv6 callers receive a distinct ipv6_not_supported_by_allowlist reason so the UI can explain it. Trust-proxy is set to 1; req.ip reflects the upstream proxy's X-Forwarded-For.

GET    /api/orgs/:orgId/security/ip-allowlist
POST   /api/orgs/:orgId/security/ip-allowlist     { cidr, label? }
DELETE /api/orgs/:orgId/security/ip-allowlist/:entryId

MFA-required mode

orgs.security_flags_json.mfa.mode ∈ { disabled, optional, required }. At login, enforceMfaGate() blocks with 403 mfa_enrollment_required when mode is required and the user has no MFA enrolled. At switch-org, the gate is forced (mfaVerified=false, phase=request) so a user who MFA'd in a low-security org cannot pivot into a required-MFA org without re-challenging. Root-admin break-glass exemption is preserved.

GET    /api/orgs/:orgId/security/mfa
PUT    /api/orgs/:orgId/security/mfa              { mode: 'disabled' | 'optional' | 'required' }

Custom role overlays

Enterprise orgs can layer org-defined capability bundles on top of built-in roles via custom_roles and custom_role_capabilities. Each membership may carry an optional org_memberships.custom_role_id (NULL → no overlay). The resolver unions built-in caps with the overlay's grants. Scope-bound grants only apply when the requested capability check matches that scope.

Privilege-escalation guard at BOTH grant-time and assign-time: an admin cannot confer a capability they do not hold themselves. Free-form (non-enum) capability strings are exempt from the guard since they have no built-in hierarchy. Self-target is refused; targets with role strictly-greater-than the assigner are refused.

GET    /api/orgs/:orgId/custom-roles
POST   /api/orgs/:orgId/custom-roles              { slug, name, description? }
DELETE /api/orgs/:orgId/custom-roles/:roleId
POST   /api/orgs/:orgId/custom-roles/:roleId/capabilities    { capability, scope_id? }
DELETE /api/orgs/:orgId/custom-roles/:roleId/capabilities    ?capability=... &scope_id=...
PUT    /api/orgs/:orgId/members/:userId/custom-role          { custom_role_id: ULID | null }

Team-scoped secrets

secrets.scope_id is a foreign key into scopes. For team-scoped secrets, create / reveal / rotate / delete all require an active team_memberships row on the team behind that scope — ORG_MANAGE_SETTINGS alone is not sufficient. Org-wide secrets (NULL scope_id) keep the existing capability-only gate. Cross-org and non-team scope IDs fall to 403.

SCIM 2.0 deprovisioning

DELETE /scim/v2/Users/:id flips org_memberships.status='removed' AND revokes all the user's session rows scoped to the deprovisioning org (and any org_id IS NULL general-context sessions). Refresh-token flow fails immediately. The authenticate middleware rejects pre-existing access tokens with 403 membership_revoked. SCIM cannot modify the org owner's identity (email, display_name, external_id, role, status) under any circumstance — owner lifecycle goes through the dashboard transfer- ownership flow only.

Verified-domain auto-join

verified_domains.verified_at is consulted at signup. New users whose email domain is owned by a verified org receive a viewer membership automatically (audit org.member.auto_joined with via=verified_domain). Public consumer / disposable domains (gmail.com, outlook.com, mailinator.com, etc.) are blocklisted from verification.

Guest signup-and-accept

POST /api/invites/:token/signup-and-accept (anonymous, rate-limited) creates the user + password identity + verified email + org membership

  • capability grant in one IMMEDIATE transaction. Email is locked to invite.email so the token cannot create an account under a different identity. Returns access + refresh tokens identical in shape to /login. IP allowlist enforced. State-oracle hardened: missing / expired / consumed token responses converge to 410 invite_not_usable.

Lifecycle event log

team_events is an append-only log of

team.created / archived / member.invited / member.joined / member.left / member.role_changed
. Every membership lifecycle change emits one event. Webhook subscribers (webhooks + webhook_deliveries) and Matrix integrations tail this log via listTeamEventsSince(cursor). Single invites, bulk invites, auditor invites, and guest signup all emit consistent joined / invited events.

Deprovisioned-user login block

If the login user has zero active memberships AND at least one terminal membership (removed / suspended / left), login refuses with 403 deprovisioned. Brand-new users with zero membership rows pass through unaffected.

Governance claims matrix

Every claim lives in docs/governance-claims.json and renders to docs/GOVERNANCE_CLAIMS_MATRIX.md. Each claim names: UI location, backend path, evidence source, status (real / partial / implied / empty-state), and the test that exercises it.


Rate Limiting

EndpointLimitWindowNotes
/api/auth/signup515 minPer IP
/api/auth/login515 minPer IP, only failed attempts
/api/auth/forgot-password31 hourPer email
/api/mfa/verify*55 minPer user
/api/orgs (POST)524 hoursPer user
/api/orgs/*/members (POST)201 hourPer user
All other API routes1001 minPer user/IP

Audit Events

All security-relevant actions are logged to audit_events table:

Event Types

# Authentication
auth.signup, auth.login, auth.logout, auth.login_failed
auth.password_reset_request, auth.password_reset_complete
auth.email_verify, auth.mfa_enable, auth.mfa_disable, auth.mfa_verify
auth.session_revoke, auth.session_revoke_all

# Organization
org.create, org.update, org.delete

# Membership
org.member.invite, org.member.accept, org.member.role_change
org.member.suspend, org.member.remove, org.member.leave

# Service Accounts
service_account.create, service_account.revoke, service_account.use

# Secrets
secret.create, secret.access, secret.update, secret.delete

# Integration Credentials
integration.create, integration.update, integration.delete
integration.test, integration.access, integration.set_default

# Instance Configuration
config.update, config.api_key.set, config.api_key.clear
config.endpoint.update, config.smtp.update, config.encryption.rotate

DEFCON Levels for Events

LevelEvents
GREENNormal operations: auth.login, auth.signup, secret.access
BLUECredential changes: config.api_key.set, integration.create
AMBERDeletions/security: config.api_key.clear, integration.delete, config.encryption.rotate
REDFailures: auth.login_failed (repeated)

Audit Record Structure

json
{
  "id": "01KFG...",
  "org_id": "01KFG...",
  "actor_type": "user",
  "actor_id": "01KFG...",
  "action": "org.member.invite",
  "target_type": "user",
  "target_id": "[email protected]",
  "scope_id": null,
  "status": "success",
  "ip_address": "192.168.1.100",
  "user_agent": "Mozilla/5.0...",
  "metadata_json": "{\"role\":\"builder\"}",
  "created_at": "2026-01-21 11:00:00"
}

Security Considerations

Password Security

  • Argon2id hashing with 64MB memory cost, 3 iterations
  • Minimum 12 characters with letter + number requirement
  • All sessions revoked on password change

Token Security

  • Access tokens: 15 minute expiry, JWT signed with HS256
  • Refresh tokens: 7 day expiry, rotated on use
  • Stored as httpOnly secure cookies in production

MFA

  • TOTP with 30-second windows (1 step tolerance for clock drift)
  • 10 backup codes generated on setup
  • MFA required for sensitive operations (SSO config, secrets)

Tenant Isolation

  • All queries include org_id filter
  • Cross-org access blocked at middleware level
  • Audit logs include org context

Credential Encryption

  • All API keys and secrets encrypted with AES-256-GCM
  • Master encryption key stored in MASTER_ENCRYPTION_KEY environment variable
  • Per-value random IV and authentication tag
  • Format: iv:authTag:ciphertext (base64 encoded)
  • Legacy hex format auto-detected for backwards compatibility

Key Rotation

Encryption keys can be rotated without data loss:

bash
# Generate new key
openssl rand -base64 32

# Rotate via admin API (requires superadmin)
curl -X POST /api/admin/rotate-encryption-key \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"newKey": "NEW_KEY_BASE64", "confirmRotation": true}'

Key rotation:

  • Re-encrypts all integration_credentials
  • Re-encrypts all encrypted instance_config values
  • Re-encrypts all api_keys
  • Wrapped in database transaction (atomic)
  • Logs config.encryption.rotate audit event

Important: After rotation, update MASTER_ENCRYPTION_KEY before restarting.


Files Reference

services/auth/
├── package.json           # Dependencies
├── server.js              # Express app, route mounting
├── start.sh               # Startup script
├── .env                   # Environment configuration
├── .env.example           # Template
│
├── db/
│   ├── schema.sql         # Database schema (13 tables, 25+ indexes)
│   ├── init.js            # Schema initialization script
│   └── index.js           # Connection, queries, transactions
│
├── routes/
│   ├── auth.js            # Authentication endpoints
│   ├── orgs.js            # Organization management
│   ├── invites.js         # Invite accept/decline
│   └── mfa.js             # MFA setup/verify
│
├── middleware/
│   ├── authenticate.js    # JWT/API key verification
│   ├── authorize.js       # RBAC capability checks
│   └── rateLimit.js       # SQLite-backed rate limiting
│
└── utils/
    ├── crypto.js          # Hashing, JWT, token generation
    ├── policy.js          # RBAC engine
    └── audit.js           # Audit logging utilities

Troubleshooting

"rate_limited" error

bash
# Clear rate limits (development only)
cd services/auth
node -e "import Database from 'better-sqlite3'; const db = new Database(process.env.AUTH_DB_PATH || '/home/control/.quox/auth.db'); db.exec('DELETE FROM rate_limits'); db.close();"

"email_not_verified" on login

bash
# Manually verify user (development only)
node -e "import Database from 'better-sqlite3'; const db = new Database('/home/control/.quox/auth.db'); db.exec(\"UPDATE users SET status='active' WHERE primary_email='[email protected]'\"); db.exec(\"UPDATE user_emails SET verified_at=datetime('now') WHERE email='[email protected]'\"); db.close();"

JWT secret issues

Ensure JWT_SECRET is set in .env. For production, generate a strong secret:

bash
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"

Phase B: Organization Hierarchy (COMPLETE)

Routes Added

Route FilePurposeEndpoints
divisions.jsDivision management8 endpoints
departments.jsDepartment management8 endpoints
teams.jsTeam management9 endpoints

Scope System

Every org unit creates an associated scope for permission inheritance:

Org (depth=0) → Division (depth=1) → Department (depth=2) → Team (depth=3)

Teams API

POST /api/orgs/:orgId/teams

Create a team within an organization.

GET /api/orgs/:orgId/teams

List all teams (with optional ?includeStats=true).

GET /api/orgs/:orgId/teams/:teamId

Get team details with members and scope.

PATCH /api/orgs/:orgId/teams/:teamId

Update team name/description.

DELETE /api/orgs/:orgId/teams/:teamId

Delete team (org admin only).

POST /api/orgs/:orgId/teams/:teamId/members

Add member to team.

PATCH /api/orgs/:orgId/teams/:teamId/members/:userId

Update member role (admin, member, viewer).

DELETE /api/orgs/:orgId/teams/:teamId/members/:userId

Remove member from team.

GET /api/orgs/:orgId/my-teams

Get current user's teams with their roles.


Phase C: Human-in-the-Loop (COMPLETE)

Routes Added

Route FilePurposeEndpoints
inbox.jsInbox and items17 endpoints

Inbox API

Inbox CRUD

  • POST /api/orgs/:orgId/inboxes - Create inbox
  • GET /api/orgs/:orgId/inboxes - List inboxes
  • GET /api/orgs/:orgId/inboxes/:inboxId - Get inbox details
  • PATCH /api/orgs/:orgId/inboxes/:inboxId - Update inbox
  • DELETE /api/orgs/:orgId/inboxes/:inboxId - Delete inbox

Inbox Items

  • POST /api/orgs/:orgId/inbox-items - Create item
  • GET /api/orgs/:orgId/inbox-items - List items (with scope filtering)
  • GET /api/orgs/:orgId/inbox-items/:itemId - Get item details
  • PATCH /api/orgs/:orgId/inbox-items/:itemId - Update item

Routing Rules

  • POST /api/orgs/:orgId/routing-rules - Create rule
  • GET /api/orgs/:orgId/routing-rules - List rules
  • PATCH /api/orgs/:orgId/routing-rules/:ruleId - Update rule
  • DELETE /api/orgs/:orgId/routing-rules/:ruleId - Delete rule

Item Assignment

  • POST /api/orgs/:orgId/inbox-items/:itemId/assign - Assign item
  • DELETE /api/orgs/:orgId/inbox-items/:itemId/assign/:assigneeId - Unassign

Stats

  • GET /api/orgs/:orgId/inbox-stats - Get inbox statistics

Item Types

TypePurpose
approvalBinary approve/reject decision
questionFree-form response needed
taskWork to be completed
alertInformational notification
reviewContent review request
escalationEscalated from another item

Item Statuses

StatusDescription
pendingAwaiting action
in_progressBeing worked on
approvedApproved (for approvals)
rejectedRejected (for approvals)
resolvedCompleted
escalatedMoved to higher authority
expiredTimeout reached

Phase D: Integrations Vault (COMPLETE)

Overview

The Integrations Vault provides secure, encrypted storage for API credentials:

  • AES-256-GCM encryption with per-value IV
  • Per-org isolation - credentials scoped to organization
  • Health monitoring - test connection status
  • Audit logging - all access tracked

Routes Added

Route FilePurposeEndpoints
integrations.jsCredential management8 endpoints

Integrations API

Credential CRUD

  • POST /api/orgs/:orgId/integrations - Create credential
  • GET /api/orgs/:orgId/integrations - List credentials
  • GET /api/orgs/:orgId/integrations/:id - Get credential details
  • PUT /api/orgs/:orgId/integrations/:id - Update credential
  • DELETE /api/orgs/:orgId/integrations/:id - Delete credential

Testing & Defaults

  • POST /api/orgs/:orgId/integrations/:id/test - Test connection
  • POST /api/orgs/:orgId/integrations/:id/default - Set as default

Supported Integration Types

CategoryTypes
AI Servicesanthropic, openai, gemini, perplexity, elevenlabs
Messagingtelegram, slack, discord
DevOpsgithub, gitlab, docker, vercel, netlify
Cloudaws, gcp, azure, digitalocean, cloudflare
E-Signaturesdocusign, adobesign, pandadoc
CustomAny REST API with custom authentication

Vault UI Features

The dashboard Vault UI includes:

  • Search - Filter by label, type, or description
  • Status filters - Healthy, Failed, Untested
  • Health indicators - Color-coded status dots
  • Bulk actions - Multi-select and bulk delete
  • Test All - Batch test all credentials
  • Usage stats - Last used timestamps

Files Reference (Updated)

services/auth/
├── routes/
│   ├── auth.js            # Authentication endpoints
│   ├── orgs.js            # Organization management
│   ├── divisions.js       # Division management (Phase B)
│   ├── departments.js     # Department management (Phase B)
│   ├── teams.js           # Team management (Phase B) - 619 lines
│   ├── inbox.js           # HITL inbox system (Phase C) - 17 endpoints
│   ├── integrations.js    # Integration credentials (Phase D) - 8 endpoints
│   ├── setup.js           # Instance configuration
│   ├── admin.js           # Admin operations (key rotation, stats)
│   ├── invites.js         # Invite accept/decline
│   └── mfa.js             # MFA setup/verify
│
├── lib/
│   └── capabilities.js    # RBAC capability definitions
│
├── utils/
│   ├── encryption.js      # AES-256-GCM encryption, key rotation
│   ├── audit.js           # Audit logging with 50+ event types
│   ├── crypto.js          # Hashing, JWT, token generation
│   └── policy.js          # RBAC engine
│
└── db/
    ├── schema.sql         # Full schema including scopes, inboxes
    └── integrations.js    # Integration credential queries

Current Status

PhaseStatusRoutesTables
A: Auth✅ Completeauth.js, orgs.js, mfa.jsusers, sessions, orgs, org_memberships
B: Org Model✅ Completedivisions.js, departments.js, teams.jsdivisions, departments, teams, scopes
C: HITL✅ Completeinbox.jsinboxes, inbox_items, routing_rules
D: Integrations✅ Completeintegrations.js, setup.js, admin.jsintegration_credentials, instance_config, api_keys
E: Workflows🔄 Planned-workflows, workflow_steps

Documentation last updated: 2026-01-31