import { upsertPushDevice, getPushDevices, deletePushDevice } from 'officerdb'; // Device registration, reached through the platform's authenticated proxy. // // The caller comes from X-Officer-User, injected by createSidecarProxy and trusted because this server // binds loopback only. Nothing else can reach it. // // Every route here is scoped to that id, including the DELETE — which takes a token from the URL, and a // token is the address of a device rather than a secret. const PLATFORMS = new Set(['ios', 'android']); const ENVIRONMENTS = new Set(['production', 'sandbox']); type RegisterBody = { token?: string; platform?: string; environment?: string; bundleId?: string; appSlug?: string; }; function ownerId(req: Request): number | null { const id = Number(req.headers.get('X-Officer-User')); return Number.isFinite(id) && id > 0 ? id : null; } export async function handleDeviceRoute(req: Request, url: URL): Promise { const userId = ownerId(req); if (userId === null) return Response.json({ error: 'missing X-Officer-User' }, { status: 401 }); if (url.pathname === '/_officer/devices' && req.method === 'GET') { const devices = await getPushDevices(userId, url.searchParams.get('appSlug') ?? undefined); // Never echo tokens back in full: they are the address of a device, and this response goes to a // client. Enough to identify a row, not enough to push to it. return Response.json({ devices: devices.map((d) => ({ id: d.id, platform: d.platform, environment: d.environment, bundleId: d.bundleId, appSlug: d.appSlug, tokenSuffix: d.token.slice(-8), lastSeenAt: d.lastSeenAt, })), }); } if (url.pathname === '/_officer/devices' && req.method === 'POST') { let body: RegisterBody; try { body = (await req.json()) as RegisterBody; } catch { return Response.json({ error: 'invalid json' }, { status: 400 }); } const { token, platform, bundleId, appSlug } = body; const environment = body.environment ?? 'production'; if (!token || !platform || !bundleId || !appSlug) { return Response.json({ error: 'token, platform, bundleId and appSlug are required' }, { status: 400 }); } if (!PLATFORMS.has(platform)) { return Response.json({ error: "platform must be 'ios' or 'android'" }, { status: 400 }); } // Rejected rather than defaulted: an iOS token from a debug build fails against production APNs with // a silent BadDeviceToken, so a wrong value here is a device that never receives anything and never // reports why. if (!ENVIRONMENTS.has(environment)) { return Response.json({ error: "environment must be 'production' or 'sandbox'" }, { status: 400 }); } const device = await upsertPushDevice({ userId, token, platform, environment, bundleId, appSlug }); return Response.json({ ok: true, id: device.id }); } const match = url.pathname.match(/^\/_officer\/devices\/(.+)$/); if (match && req.method === 'DELETE') { await deletePushDevice(decodeURIComponent(match[1]!), userId); return Response.json({ ok: true }); } return null; }