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
| Term | What It Means |
|---|---|
| Authentication | Proving who you are (logging in) |
| Authorization | Determining 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" |
| Organisation | A group of users who share resources (like a company or team) |
How login works (simplified)
- You enter your email and password
- The Auth Service checks if they match what is stored (passwords are encrypted, never stored in plain text)
- If correct, you receive a token (like a wristband at an event)
- Every time you do something in Quox, your token is checked to verify it is really you
- 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:
| Role | What They Can Do |
|---|---|
| Viewer | Look at information, but cannot make changes |
| Runner | Execute workflows and run commands |
| Builder | Create new agents, workflows, and tools |
| Admin | Manage users, change settings, view audit logs |
| Owner | Full 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:
- Enable MFA in your account settings
- Scan a QR code with an authenticator app (like Google Authenticator or Authy)
- When logging in, enter your password AND the 6-digit code from the app
- Backup codes are provided in case you lose access to your phone
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
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
| Variable | Default | Description |
|---|---|---|
AUTH_PORT | 3101 | Server port |
AUTH_HOST | 0.0.0.0 | Bind address |
AUTH_DB_PATH | /var/lib/quox/auth.db | SQLite database path |
JWT_SECRET | (required) | Secret for signing JWTs |
JWT_ACCESS_EXPIRY | 15m | Access token lifetime |
JWT_REFRESH_EXPIRY | 7d | Refresh token lifetime |
CORS_ORIGINS | localhost | Comma-separated allowed origins |
NODE_ENV | development | Environment mode |
Database Schema
Tables
| Table | Purpose |
|---|---|
users | Global user accounts |
user_emails | Email addresses (multiple per user) |
auth_identities | Authentication methods (password, OAuth) |
sessions | Active sessions with refresh tokens |
mfa_backup_codes | TOTP backup codes |
orgs | Organizations (tenants) |
org_memberships | User membership in organizations |
teams | Teams within organizations |
team_memberships | User membership in teams |
invites | Pending invitations |
service_accounts | Non-human API identities |
secrets | Encrypted secret storage |
integration_credentials | Encrypted API credentials per-org |
instance_config | Global instance configuration (encrypted values) |
api_keys | Instance-level API keys |
audit_events | All security-relevant events |
rate_limits | Request 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:
{
"email": "[email protected]",
"password": "SecurePassword123",
"display_name": "John Doe"
}
Response (201):
{
"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:
{
"email": "[email protected]",
"password": "SecurePassword123",
"org_id": "01KFG..." // optional - set active org context
}
Response (200):
{
"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 passwordemail_not_verified- Must verify email firstaccount_suspended- Account has been suspended
POST /api/auth/logout
Revoke current session.
Headers: Authorization: Bearer <access_token>
Response (200):
{
"success": true,
"message": "Logged out successfully"
}
POST /api/auth/refresh
Get new access token using refresh token.
Request:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
Response (200):
{
"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:
{
"token": "abc123..."
}
Response (200):
{
"success": true,
"message": "Email verified successfully"
}
POST /api/auth/forgot-password
Request password reset email.
Request:
{
"email": "[email protected]"
}
Response (200):
{
"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:
{
"token": "abc123...",
"password": "NewSecurePassword123"
}
Response (200):
{
"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):
{
"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:
{
"name": "Acme Corporation",
"slug": "acme-corp" // optional - auto-generated if omitted
}
Response (201):
{
"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):
{
"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):
{
"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):
{
"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:
{
"email": "[email protected]",
"role": "builder"
}
Response (201):
{
"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):
{
"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:
{
"code": "123456"
}
Response (200):
{
"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:
{
"code": "123456"
// OR
"backup_code": "ABCD-1234"
}
Response (200):
{
"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):
{
"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):
{
"success": true,
"message": "Invite accepted",
"organization": {
"id": "01KFG...",
"name": "Acme Corporation",
"slug": "acme-corp",
"role": "builder"
}
}
RBAC Model
Roles
| Role | Level | Description |
|---|---|---|
owner | 5 | Full control, can delete org, transfer ownership |
admin | 4 | Manage members, roles, policies, audit |
builder | 3 | Create assets (assistants, agents, tools, workflows) |
runner | 2 | Execute workflows, view runs |
viewer | 1 | Read-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:
// 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:
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 403ip_not_allowedbefore issuing credentials- every authenticated request —
authenticate()re-checks against the JWT'sorg_id POST /api/auth/refresh— captured token can't be refreshed from outside the networkPOST /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.emailso 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 to410 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_changedwebhooks + 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
| Endpoint | Limit | Window | Notes |
|---|---|---|---|
/api/auth/signup | 5 | 15 min | Per IP |
/api/auth/login | 5 | 15 min | Per IP, only failed attempts |
/api/auth/forgot-password | 3 | 1 hour | Per email |
/api/mfa/verify* | 5 | 5 min | Per user |
/api/orgs (POST) | 5 | 24 hours | Per user |
/api/orgs/*/members (POST) | 20 | 1 hour | Per user |
| All other API routes | 100 | 1 min | Per 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
| Level | Events |
|---|---|
| GREEN | Normal operations: auth.login, auth.signup, secret.access |
| BLUE | Credential changes: config.api_key.set, integration.create |
| AMBER | Deletions/security: config.api_key.clear, integration.delete, config.encryption.rotate |
| RED | Failures: auth.login_failed (repeated) |
Audit Record Structure
{
"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_idfilter - 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_KEYenvironment 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:
# 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_configvalues - Re-encrypts all
api_keys - Wrapped in database transaction (atomic)
- Logs
config.encryption.rotateaudit 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
# 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
# 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:
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"
Phase B: Organization Hierarchy (COMPLETE)
Routes Added
| Route File | Purpose | Endpoints |
|---|---|---|
divisions.js | Division management | 8 endpoints |
departments.js | Department management | 8 endpoints |
teams.js | Team management | 9 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 File | Purpose | Endpoints |
|---|---|---|
inbox.js | Inbox and items | 17 endpoints |
Inbox API
Inbox CRUD
POST /api/orgs/:orgId/inboxes- Create inboxGET /api/orgs/:orgId/inboxes- List inboxesGET /api/orgs/:orgId/inboxes/:inboxId- Get inbox detailsPATCH /api/orgs/:orgId/inboxes/:inboxId- Update inboxDELETE /api/orgs/:orgId/inboxes/:inboxId- Delete inbox
Inbox Items
POST /api/orgs/:orgId/inbox-items- Create itemGET /api/orgs/:orgId/inbox-items- List items (with scope filtering)GET /api/orgs/:orgId/inbox-items/:itemId- Get item detailsPATCH /api/orgs/:orgId/inbox-items/:itemId- Update item
Routing Rules
POST /api/orgs/:orgId/routing-rules- Create ruleGET /api/orgs/:orgId/routing-rules- List rulesPATCH /api/orgs/:orgId/routing-rules/:ruleId- Update ruleDELETE /api/orgs/:orgId/routing-rules/:ruleId- Delete rule
Item Assignment
POST /api/orgs/:orgId/inbox-items/:itemId/assign- Assign itemDELETE /api/orgs/:orgId/inbox-items/:itemId/assign/:assigneeId- Unassign
Stats
GET /api/orgs/:orgId/inbox-stats- Get inbox statistics
Item Types
| Type | Purpose |
|---|---|
approval | Binary approve/reject decision |
question | Free-form response needed |
task | Work to be completed |
alert | Informational notification |
review | Content review request |
escalation | Escalated from another item |
Item Statuses
| Status | Description |
|---|---|
pending | Awaiting action |
in_progress | Being worked on |
approved | Approved (for approvals) |
rejected | Rejected (for approvals) |
resolved | Completed |
escalated | Moved to higher authority |
expired | Timeout 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 File | Purpose | Endpoints |
|---|---|---|
integrations.js | Credential management | 8 endpoints |
Integrations API
Credential CRUD
POST /api/orgs/:orgId/integrations- Create credentialGET /api/orgs/:orgId/integrations- List credentialsGET /api/orgs/:orgId/integrations/:id- Get credential detailsPUT /api/orgs/:orgId/integrations/:id- Update credentialDELETE /api/orgs/:orgId/integrations/:id- Delete credential
Testing & Defaults
POST /api/orgs/:orgId/integrations/:id/test- Test connectionPOST /api/orgs/:orgId/integrations/:id/default- Set as default
Supported Integration Types
| Category | Types |
|---|---|
| AI Services | anthropic, openai, gemini, perplexity, elevenlabs |
| Messaging | telegram, slack, discord |
| DevOps | github, gitlab, docker, vercel, netlify |
| Cloud | aws, gcp, azure, digitalocean, cloudflare |
| E-Signatures | docusign, adobesign, pandadoc |
| Custom | Any 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
| Phase | Status | Routes | Tables |
|---|---|---|---|
| A: Auth | ✅ Complete | auth.js, orgs.js, mfa.js | users, sessions, orgs, org_memberships |
| B: Org Model | ✅ Complete | divisions.js, departments.js, teams.js | divisions, departments, teams, scopes |
| C: HITL | ✅ Complete | inbox.js | inboxes, inbox_items, routing_rules |
| D: Integrations | ✅ Complete | integrations.js, setup.js, admin.js | integration_credentials, instance_config, api_keys |
| E: Workflows | 🔄 Planned | - | workflows, workflow_steps |
Documentation last updated: 2026-01-31