/** * @fileoverview Auth bridge: API keys / sessions → Verae JWT context. * @module services/authService */ import { veraeClient } from '../clients/veraeClient.js'; import { getTenantByApiKey, getTenant } from '../store/tenants.js'; import { issueSessionToken, parseSessionToken, isApiKey } from '../lib/tokens.js'; import { AppError } from '../errors.js'; import { createDebugger } from '../debug/logger.js'; const log = createDebugger('auth'); /** * Login with Verae username/password and issue middleware session token. * * @param {object} params * @param {string} params.username * @param {string} params.password * @param {object} [params.tenant] * @returns {Promise<{ accessToken: string, expiresAt: string, tenant: object, user: object }>} */ export async function loginWithCredentials({ username, password, tenant }) { log.debug('loginWithCredentials', { username, tenantId: tenant?.id }); const verae = await veraeClient.login({ username, password }); if (tenant?.veraeUsername && tenant.veraeUsername !== username) { throw new AppError('Credentials do not match tenant account', { status: 403, code: 'FORBIDDEN', }); } const tenantId = tenant?.id ?? `user:${verae.user.username}`; const accessToken = issueSessionToken({ tenantId, veraeToken: verae.token, expiresAt: verae.expiresAt, }); return { accessToken, expiresAt: verae.expiresAt, tenant: tenant ? { id: tenant.id, name: tenant.name, plan: tenant.plan } : { id: tenantId, name: verae.user.username, plan: 'free' }, user: verae.user, }; } /** * Resolve API key to tenant and login with stored Verae credentials. * @param {string} apiKey * @returns {Promise>>} */ export async function loginWithApiKey(apiKey) { const tenant = getTenantByApiKey(apiKey); if (!tenant) { log.debug('invalid api key'); throw new AppError('Invalid API key', { status: 401, code: 'UNAUTHORIZED' }); } if (!tenant.veraeUsername || !tenant.veraePassword) { throw new AppError('Tenant is missing Verae credentials', { status: 500, code: 'TENANT_MISCONFIGURED', }); } log.debug('loginWithApiKey', { tenantId: tenant.id, plan: tenant.plan }); return loginWithCredentials({ username: tenant.veraeUsername, password: tenant.veraePassword, tenant, }); } /** * Resolve bearer credential into request auth context. * * @param {string|null} rawToken * @returns {Promise<{ tenantId: string, tenant: object, veraeToken: string, authMethod: string }>} */ export async function resolveAuthContext(rawToken) { if (!rawToken) { throw new AppError('Missing authorization token', { status: 401, code: 'UNAUTHORIZED' }); } if (isApiKey(rawToken)) { const session = await loginWithApiKey(rawToken); const parsed = parseSessionToken(session.accessToken); const tenant = getTenant(session.tenant.id) ?? session.tenant; return { tenantId: session.tenant.id, tenant: { id: session.tenant.id, name: session.tenant.name, plan: session.tenant.plan }, veraeToken: parsed.veraeToken, authMethod: 'api_key', fullTenant: tenant, }; } const parsed = parseSessionToken(rawToken); if (!parsed?.veraeToken) { throw new AppError('Invalid or expired session token', { status: 401, code: 'UNAUTHORIZED' }); } if (parsed.expiresAt && Date.parse(parsed.expiresAt) < Date.now()) { throw new AppError('Session token expired', { status: 401, code: 'TOKEN_EXPIRED' }); } const tenant = getTenant(parsed.tenantId); log.debug('session auth', { tenantId: parsed.tenantId }); return { tenantId: parsed.tenantId, tenant: tenant ? { id: tenant.id, name: tenant.name, plan: tenant.plan } : { id: parsed.tenantId, plan: 'free' }, veraeToken: parsed.veraeToken, authMethod: 'session', fullTenant: tenant, }; } /** * Validate auth and optionally ping Verae /auth/validate. * @param {string} rawToken * @returns {Promise} */ export async function validateSession(rawToken) { const context = await resolveAuthContext(rawToken); const validation = await veraeClient.validate(context.veraeToken); return { valid: true, tenantId: context.tenantId, plan: context.tenant.plan, authMethod: context.authMethod, user: validation, }; }