// Generated by scripts/sync-shared.mjs from shared/host/pair-access.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs". /** * Pairing trust fence shared by plugins that expose host routes: loopback * (the desktop) always passes; a live paired-device cookie is an additional * allow path when remote-web-ui is loaded. The consuming plugin never * depends on that plugin — without the service the fence stays * loopback-only. * * Per-package wrappers (access.ts) call this with their own name so each * plugin keeps a self-describing export; the security decision lives only * here. */ import type { IncomingMessage } from 'node:http' import { isLoopbackRequest } from './loopback.ts' /** Structural pairing lookup (no package dependency on remote-web-ui). */ interface PairingAccess { isPairedDevice(request: IncomingMessage): boolean } /** * Structural host-context shape: shared sources carry no @deepseek-ai * dependency (the shared package must typecheck standalone), so the fence * reads only the two members it needs; cordis Context satisfies this. * ctx.get is optional on the test harness; production Context always has it. */ interface LookupCtx { get?(name: string, strict?: boolean): unknown remoteWebUiPairing?: PairingAccess } /** * Whether this request may enter the plugin's host routes. * @param ctx - host context; may expose remoteWebUiPairing. * @param request - the incoming HTTP request. * @returns true for loopback, or a live paired-device cookie. */ export function isPairedOrLoopbackAllowed(ctx: LookupCtx, request: IncomingMessage): boolean { if (isLoopbackRequest(request)) return true return lookupPairing(ctx)?.isPairedDevice(request) === true } /** * The pairing service, or undefined when this composition does not expose it. * * Two lookups exist because the fence is shared with test harnesses that hand * over a bare object: a real cordis context resolves services through `get`, a * harness may carry the property directly. `get(name, false)` is the * non-strict form, so an unloaded service answers undefined rather than * throwing. Reading an un-injected service as a PROPERTY throws instead * (`cannot get property "remoteWebUiPairing" without inject`); a trust fence * must answer "not paired" in that case, never leak the error into the route * handler, where the host web server turns it into an empty-body 400 (#1942). */ function lookupPairing(ctx: LookupCtx): PairingAccess | undefined { if (typeof ctx.get === 'function') { const viaGet = ctx.get('remoteWebUiPairing', false) return isPairingAccess(viaGet) ? viaGet : undefined } try { const viaProperty = ctx.remoteWebUiPairing return isPairingAccess(viaProperty) ? viaProperty : undefined } catch { return undefined } } function isPairingAccess(value: unknown): value is PairingAccess { return value !== undefined && value !== null && typeof (value as PairingAccess).isPairedDevice === 'function' }