import path from 'node:path' import cors from 'cors' import express from 'express' import helmet from 'helmet' import { FIELDLENS_SCHEMA_VERSION, isSyncRequest, type DeviceRegistration } from '@fieldlens/contracts' import { getAccessRequest, listAccessRequests, requestAccess, reviewAccessRequest } from './access.js' import { authenticate, canApproveAccess, requireAccess, requireAccessApprover } from './auth.js' import { config } from './config.js' import { loadDeviceContext } from './context.js' import { databaseReady, pool } from './db.js' import { log } from './log.js' import { FIELDLENS_MCP_OAUTH_SCOPES, handleMcpRequest } from './mcp.js' import { runMigrations } from './migrations.js' import { processSyncEvent } from './sync.js' const app = express() app.disable('x-powered-by') const keycloakOrigin = new URL(config.issuer).origin app.use(helmet({ crossOriginEmbedderPolicy: false, contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'blob:'], mediaSrc: ["'self'", 'blob:'], connectSrc: ["'self'", keycloakOrigin], workerSrc: ["'self'", 'blob:'], objectSrc: ["'none'"], baseUri: ["'self'"], frameAncestors: ["'none'"], }, }, })) app.use((_req, res, next) => { res.setHeader('Permissions-Policy', 'camera=(self), microphone=(), geolocation=()') res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin') next() }) const desktopOrigins = new Set([ 'tauri://localhost', 'http://tauri.localhost', 'https://tauri.localhost', ]) app.use(cors({ origin(origin, callback) { if (!origin || origin === config.webOrigin || desktopOrigins.has(origin)) callback(null, true) else callback(new Error('cors_origin_rejected')) }, credentials: false, })) app.use(express.json({ limit: '1mb' })) app.use((req, res, next) => { const started = performance.now() res.on('finish', () => log('info', 'http_request', { method: req.method, path: req.path, status: res.statusCode, durationMs: Math.round(performance.now() - started), })) next() }) app.get('/api/health/live', (_req, res) => { res.json({ status: 'live', version: process.env.FIELDLENS_VERSION ?? 'dev' }) }) function redirectDesktopDownload(res: express.Response, filename: string, baseUrl = config.desktopDownloadBaseUrl) { const base = baseUrl.replace(/\/$/, '') res.setHeader('Cache-Control', 'no-store') res.redirect(302, `${base}/${filename}`) } app.get('/downloads/fieldlens-print-windows-x64-setup.exe', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe') }) app.get('/downloads/fieldlens-print-linux-x64.deb', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-linux-x64.deb', config.linuxDesktopDownloadBaseUrl) }) app.get('/downloads/install.sh', (_req, res) => { redirectDesktopDownload(res, 'install.sh', config.linuxDesktopDownloadBaseUrl) }) app.get('/downloads/latest.json', (_req, res) => { redirectDesktopDownload(res, 'latest.json') }) app.get('/downloads/release.json', (_req, res) => { redirectDesktopDownload(res, 'release.json') }) app.get('/downloads/SHA256SUMS', (_req, res) => { redirectDesktopDownload(res, 'SHA256SUMS') }) app.get('/downloads/fieldlens-print-windows-x64-setup.exe.sig', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe.sig') }) app.get('/downloads/fieldlens-print-linux-x64.AppImage', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-linux-x64.AppImage') }) app.get('/downloads/fieldlens-print-linux-x64.AppImage.sig', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-linux-x64.AppImage.sig') }) // Backward-compatible aliases used by the first downloads page. app.get('/downloads/fieldlens-print/windows-x64', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe') }) app.get('/downloads/fieldlens-print/linux-x64.deb', (_req, res) => { redirectDesktopDownload(res, 'fieldlens-print-linux-x64.deb', config.linuxDesktopDownloadBaseUrl) }) app.get('/api/health/ready', async (_req, res) => { try { await databaseReady() res.json({ status: 'ready', database: 'ready', version: process.env.FIELDLENS_VERSION ?? 'dev' }) } catch { log('error', 'database_readiness_failed') res.status(503).json({ status: 'not-ready', database: 'unavailable' }) } }) const mcpResourceMetadataUrl = new URL('/.well-known/oauth-protected-resource', config.mcpResourceUrl).href function sendMcpProtectedResourceMetadata(res: express.Response) { res.setHeader('Cache-Control', 'public, max-age=300') res.json({ resource: config.mcpResourceUrl, authorization_servers: [config.issuer], scopes_supported: [...FIELDLENS_MCP_OAUTH_SCOPES], bearer_methods_supported: ['header'], }) } app.get('/.well-known/oauth-protected-resource', (_req, res) => { sendMcpProtectedResourceMetadata(res) }) app.get('/.well-known/oauth-protected-resource/api/mcp', (_req, res) => { sendMcpProtectedResourceMetadata(res) }) app.use('/api/mcp', (_req, res, next) => { res.setHeader( 'WWW-Authenticate', `Bearer resource_metadata="${mcpResourceMetadataUrl}", scope="${FIELDLENS_MCP_OAUTH_SCOPES.join(' ')}"`, ) res.setHeader('Cache-Control', 'no-store') next() }, authenticate, (_req, res, next) => { res.removeHeader('WWW-Authenticate') next() }, requireAccess('operator')) app.post('/api/mcp', handleMcpRequest) app.get('/api/mcp', (_req, res) => { res.status(405).end() }) app.delete('/api/mcp', (_req, res) => { res.status(405).end() }) app.use('/api/v1', authenticate) app.get('/api/v1/access/me', async (req, res) => { const request = await getAccessRequest(req.identity!.subject) const roles = req.identity!.roles.filter((role) => role.startsWith('fieldlens-')) res.json({ status: roles.length > 0 ? 'approved' : request?.status ?? 'none', role: roles[0] ?? request?.approvedRole ?? null, canApprove: canApproveAccess(req.identity), username: req.identity!.username ?? null, email: req.identity!.email ?? null, }) }) app.post('/api/v1/access/request', async (req, res) => { const request = await requestAccess(req.identity!) log('info', 'access_requested', { subject: req.identity!.subject, status: request.status }) res.status(request.status === 'approved' ? 200 : 202).json({ status: request.status, role: request.approvedRole, }) }) app.get('/api/v1/access/requests', requireAccessApprover, async (req, res) => { const status = req.query.status === 'approved' || req.query.status === 'rejected' ? req.query.status : 'pending' const requests = await listAccessRequests(status) res.json({ requests }) }) app.post('/api/v1/access/requests/:subject/approve', requireAccessApprover, async (req, res) => { const request = await reviewAccessRequest(String(req.params.subject), 'approved', req.identity!.subject) if (!request) { res.status(404).json({ error: 'access_request_not_found' }) return } log('info', 'access_approved', { subject: request.subject, reviewer: req.identity!.subject }) res.json({ status: request.status, role: request.approvedRole }) }) app.post('/api/v1/access/requests/:subject/reject', requireAccessApprover, async (req, res) => { const request = await reviewAccessRequest(String(req.params.subject), 'rejected', req.identity!.subject) if (!request) { res.status(404).json({ error: 'access_request_not_found' }) return } log('info', 'access_rejected', { subject: request.subject, reviewer: req.identity!.subject }) res.json({ status: request.status }) }) app.get('/api/v1/me', requireAccess('operator'), (req, res) => { res.json({ subject: req.identity!.subject, username: req.identity!.username, roles: req.identity!.roles.filter((role) => role.startsWith('fieldlens-')), clientId: req.identity!.clientId, }) }) app.get('/api/v1/stations', requireAccess('operator'), async (_req, res) => { const result = await pool.query(` SELECT s.id, s.site_id AS "siteId", s.name, s.station_type AS "stationType", s.active FROM stations s JOIN sites si ON si.id = s.site_id WHERE si.tenant_id = $1 AND s.active = true ORDER BY s.name `, [config.tenantId]) res.json({ stations: result.rows }) }) app.post('/api/v1/devices/register', requireAccess('operator'), async (req, res) => { const body = req.body as Partial if (typeof body.installationId !== 'string' || body.schemaVersion !== FIELDLENS_SCHEMA_VERSION) { res.status(400).json({ error: 'invalid_device_registration' }) return } const client = await pool.connect() try { const requestedStation = typeof body.stationId === 'string' ? body.stationId : null const station = await client.query<{ id: string siteId: string name: string stationType: string active: boolean }>(` SELECT s.id, s.site_id AS "siteId", s.name, s.station_type AS "stationType", s.active FROM stations s JOIN sites si ON si.id = s.site_id WHERE si.tenant_id = $1 AND s.active = true AND ($2::uuid IS NULL OR s.id = $2::uuid) ORDER BY CASE WHEN $2::uuid IS NOT NULL AND s.id = $2::uuid THEN 0 ELSE 1 END, s.created_at LIMIT 1 `, [config.tenantId, requestedStation]) if (!station.rows[0]) { res.status(409).json({ error: 'no_active_station' }) return } const deviceId = crypto.randomUUID() const result = await client.query<{ id: string }>(` INSERT INTO devices (id, station_id, installation_id, label, last_seen_at, app_version) VALUES ($1,$2,$3,$4,now(),$5) ON CONFLICT (installation_id) DO UPDATE SET station_id = EXCLUDED.station_id, label = COALESCE(EXCLUDED.label, devices.label), last_seen_at = now(), app_version = EXCLUDED.app_version RETURNING id `, [deviceId, station.rows[0].id, body.installationId, body.label ?? null, body.appVersion ?? null]) log('info', 'device_registered', { deviceId: result.rows[0]!.id, stationId: station.rows[0].id }) res.json({ id: result.rows[0]!.id, station: station.rows[0] }) } finally { client.release() } }) app.post('/api/v1/sync', requireAccess('operator'), async (req, res) => { if (!isSyncRequest(req.body)) { res.status(400).json({ error: 'invalid_sync_request' }) return } const client = await pool.connect() try { const context = await loadDeviceContext(client, req.body.deviceId) if (!context) { res.status(403).json({ error: 'device_not_registered_for_tenant' }) return } const acknowledgements = [] for (const event of req.body.events) { acknowledgements.push(await processSyncEvent(client, event, context, req.identity!)) } await client.query('UPDATE devices SET last_seen_at = now() WHERE id = $1', [context.deviceId]) const accepted = acknowledgements.filter((ack) => ack.status === 'accepted').length const duplicate = acknowledgements.filter((ack) => ack.status === 'duplicate').length const rejected = acknowledgements.filter((ack) => ack.status === 'rejected').length log('info', 'sync_batch', { deviceId: context.deviceId, size: acknowledgements.length, accepted, duplicate, rejected }) res.json({ schemaVersion: FIELDLENS_SCHEMA_VERSION, acknowledgements }) } finally { client.release() } }) app.get('/api/v1/print-captures', requireAccess('operator'), async (req, res) => { const requested = Number(req.query.limit ?? 50) const limit = Number.isFinite(requested) ? Math.max(1, Math.min(200, Math.trunc(requested))) : 50 const result = await pool.query(` SELECT id, device_id AS "deviceId", source, stream_format AS "streamFormat", byte_count AS "byteCount", barcode_count AS "barcodeCount", forwarded, failure_kind AS "failureKind", app_version AS "appVersion", captured_at AS "capturedAt", received_at AS "receivedAt" FROM print_capture_diagnostics WHERE tenant_id = $1 ORDER BY captured_at DESC LIMIT $2 `, [config.tenantId, limit]) res.json({ captures: result.rows }) }) app.get('/api/v1/scans', requireAccess('supervisor'), async (req, res) => { const limit = Math.max(1, Math.min(500, Number(req.query.limit ?? 100))) const result = await pool.query(` SELECT id, station_id AS "stationId", device_id AS "deviceId", raw_value AS "rawValue", format, duplicate, source, source_metadata AS "sourceMetadata", captured_at AS "capturedAt", received_at AS "receivedAt" FROM scans WHERE tenant_id = $1 ORDER BY captured_at DESC LIMIT $2 `, [config.tenantId, limit]) res.json({ scans: result.rows }) }) app.get('/api/v1/pallet-sessions/:id', requireAccess('supervisor'), async (req, res) => { const session = await pool.query(` SELECT ps.* FROM pallet_sessions ps WHERE ps.id = $1 AND ps.tenant_id = $2 `, [req.params.id, config.tenantId]) if (!session.rows[0]) { res.status(404).json({ error: 'not_found' }) return } const observations = await pool.query( 'SELECT * FROM pallet_observations WHERE pallet_session_id = $1 ORDER BY carton_index', [req.params.id], ) const exceptions = await pool.query( 'SELECT * FROM pallet_exceptions WHERE pallet_session_id = $1 ORDER BY created_at', [req.params.id], ) res.json({ session: session.rows[0], observations: observations.rows, exceptions: exceptions.rows }) }) app.get('/api/v1/reference/labels/:ean', requireAccess('operator'), async (req, res) => { const started = performance.now() try { const result = await pool.query<{ fields_json: Record }>(` SELECT fields_json FROM reference_label_specs WHERE tenant_id = $1 AND ean = $2 AND active = true ORDER BY updated_at DESC `, [config.tenantId, req.params.ean]) res.json({ rows: result.rows.map((row) => row.fields_json) }) log('info', 'reference_lookup', { ean: req.params.ean, rows: result.rowCount, durationMs: Math.round(performance.now() - started), }) } catch { log('error', 'reference_lookup_failed', { ean: req.params.ean, durationMs: Math.round(performance.now() - started), }) res.status(503).json({ error: 'reference_provider_unavailable' }) } }) app.get('/api/v1/ops/summary', requireAccess('supervisor'), async (_req, res) => { const [stations, devices, scans, sessions, exceptions] = await Promise.all([ pool.query('SELECT count(*)::int AS count FROM stations s JOIN sites si ON si.id=s.site_id WHERE si.tenant_id=$1 AND s.active=true', [config.tenantId]), pool.query('SELECT count(*)::int AS count, max(last_seen_at) AS last_seen FROM devices d JOIN stations s ON s.id=d.station_id JOIN sites si ON si.id=s.site_id WHERE si.tenant_id=$1', [config.tenantId]), pool.query("SELECT count(*)::int AS count FROM scans WHERE tenant_id=$1 AND captured_at > now() - interval '24 hours'", [config.tenantId]), pool.query("SELECT count(*)::int AS count FROM pallet_sessions WHERE tenant_id=$1 AND started_at > now() - interval '24 hours'", [config.tenantId]), pool.query('SELECT count(*)::int AS count FROM pallet_exceptions pe JOIN pallet_sessions ps ON ps.id=pe.pallet_session_id WHERE ps.tenant_id=$1', [config.tenantId]), ]) res.json({ stations: stations.rows[0], devices: devices.rows[0], scans24h: scans.rows[0], palletSessions24h: sessions.rows[0], conformityExceptions: exceptions.rows[0], }) }) if (config.staticDir) { const staticDir = path.resolve(config.staticDir) app.use(express.static(staticDir, { etag: true, maxAge: '1h', setHeaders(res, filename) { if (filename.endsWith('sw.js') || filename.includes('workbox-')) { res.setHeader('Cache-Control', 'no-cache') } }, })) app.use((req, res, next) => { if (req.method !== 'GET' || req.path.startsWith('/api/')) return next() res.setHeader('Cache-Control', 'no-cache') res.sendFile(path.join(staticDir, 'index.html')) }) } await runMigrations() const server = app.listen(config.port, '0.0.0.0', () => { log('info', 'server_started', { port: config.port, environment: config.environment, version: process.env.FIELDLENS_VERSION ?? 'dev', }) }) async function shutdown(signal: string) { log('info', 'shutdown_started', { signal }) server.close(async () => { await pool.end() process.exit(0) }) setTimeout(() => process.exit(1), 10_000).unref() } process.on('SIGTERM', () => void shutdown('SIGTERM')) process.on('SIGINT', () => void shutdown('SIGINT')) export { app }