// The gate every replica route shares, and its error mapping. // // 404, not 403, when the feature is absent: the standalone server artifact is the // SAME one the production Dockerfile ships to multi-tenant deployments, where a // server-side replica of every user's mail would be badly wrong. Nothing should // learn the routes exist in a deployment that does not have the feature. import { NextResponse } from 'next/server'; import { logger } from '@/lib/logger'; import { isSqlcipherAvailable } from '@/lib/mail-index/binding'; import { hasKeyChannel, IndexKeyError } from '@/lib/mail-index/key'; import { getStoreDir } from '@/lib/mail-index/paths'; import { JmapIndexError } from '@/lib/mail-index/jmap'; import { IndexSessionError } from '@/lib/mail-index/reindex'; import { ReplicaUnavailableError } from './store'; import { ReplicaSyncError } from './errors'; /** Returns a response to send immediately, or `null` when the gate is open. */ export function gateReplicaRoute(): NextResponse | null { if (!getStoreDir()) return new NextResponse(null, { status: 404 }); if (!hasKeyChannel()) { return NextResponse.json( { error: 'The offline replica has no key channel in this process.', code: 'no-key-channel' }, { status: 503 }, ); } if (!isSqlcipherAvailable()) { // The native binding is an optionalDependency, so "not installed" is a normal // state on a platform with no prebuild - not an error to log loudly. return NextResponse.json( { error: 'Encrypted local storage is unavailable on this platform.', code: 'no-binding' }, { status: 503 }, ); } return null; } export function replicaErrorResponse(error: unknown, context: string): NextResponse { if (error instanceof IndexSessionError) { return NextResponse.json({ error: error.message }, { status: error.status }); } if (error instanceof JmapIndexError) { return NextResponse.json({ error: error.message }, { status: error.status }); } if (error instanceof IndexKeyError) { // `no-secure-storage` is the Linux-without-a-keyring refusal: a real, expected // outcome with a user-facing explanation, not a server fault. const status = error.code === 'no-secure-storage' ? 503 : 500; return NextResponse.json({ error: error.message, code: error.code }, { status }); } if (error instanceof ReplicaUnavailableError) { return NextResponse.json({ error: error.message, code: 'unavailable' }, { status: 503 }); } if (error instanceof ReplicaSyncError) { // A transport failure here means the BACKEND is unreachable, which for a sync // is an expected outcome rather than a server fault - 503 with the class, so // the renderer can retry rather than surface an error. const status = error.cls === 'Auth' ? 401 : error.cls === 'RateLimit' ? 429 : 503; return NextResponse.json({ error: error.message, code: error.cls }, { status }); } logger.error(`offline-replica: ${context} failed`, { error: error instanceof Error ? error.message : String(error), }); return NextResponse.json({ error: `${context} failed` }, { status: 500 }); } export const NO_STORE = { 'Cache-Control': 'no-store' } as const;