redroid-mrnumber/service/config.ts
Natalie eb84d431f3 feat(service): inbound trigger service — POST /api/screening/requests + durable queue + worker
Stand up the HTTP surface Prospector calls (this app had no listening port). Bun
service: bearer-auth (MRNUMBER_SERVICE_TOKEN, constant-time) POST /api/screening/requests
{phone, ref} → 202 {accepted, id}, enqueued in a durable SQLite queue; a single serial
worker (one Android box) drains by invoking mr_lookup.py and records the people-service
signal. GET /api/screening/requests/:id returns the row; GET /health is open. Crash-safe
(requeues stale in-flight rows on restart). 7 bun tests + typecheck; smoke-tested end to
end (auth 401, enqueue 202, drain→verdict, invalid-phone 400).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:57:54 -04:00

47 lines
1.7 KiB
TypeScript

/**
* Trigger-service config. All env-resolved; the service token falls back to the
* 0600 plum secret file. No secrets are baked in.
*/
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
function fileOrEnv(envVar: string, file: string): string {
if (process.env[envVar]) return process.env[envVar] as string;
try {
return readFileSync(file, 'utf8').trim();
} catch {
return '';
}
}
export interface ServiceConfig {
readonly port: number;
/** Bearer token Prospector must present to POST /api/screening/requests. */
readonly serviceToken: string;
/** Absolute path to client/mr_lookup.py. */
readonly lookupScript: string;
readonly python: string;
/** SQLite file backing the durable queue. */
readonly dbPath: string;
/** Worker poll interval (ms) when the queue is empty. */
readonly pollIntervalMs: number;
/** Per-lookup timeout (ms) — a lookup sleeps ~9s for paid content + vision. */
readonly lookupTimeoutMs: number;
}
export function loadConfig(): ServiceConfig {
const root = join(import.meta.dir, '..');
return {
port: Number(process.env['MRNUMBER_PORT'] ?? 8787),
serviceToken: fileOrEnv(
'MRNUMBER_SERVICE_TOKEN',
join(homedir(), '.config/cocotte-secrets/mr-number.service-token'),
),
lookupScript: process.env['MR_NUMBER_LOOKUP_SCRIPT'] ?? join(root, 'client', 'mr_lookup.py'),
python: process.env['MR_NUMBER_PYTHON'] ?? '/opt/homebrew/bin/python3',
dbPath: process.env['MRNUMBER_QUEUE_DB'] ?? join(root, 'service', 'queue.sqlite'),
pollIntervalMs: Number(process.env['MRNUMBER_POLL_MS'] ?? 2000),
lookupTimeoutMs: Number(process.env['MRNUMBER_LOOKUP_TIMEOUT_MS'] ?? 180_000),
};
}