/** * API token middleware. * * Gates `/api/*`, Socket.IO and raw WebSocket upgrades behind the shared * API token for every caller that is NOT on loopback. Loopback callers * (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) pass without a token — that is * how local skills (`api_call` in `config/skills/_common/lib.sh`) and the * local dashboard work with zero setup. * * The token can be presented as `Authorization: Bearer `, * `X-Crewly-Token: `, a `crewly_token` cookie, or (WebSocket / * dashboard deep link only) a `?token=` query parameter. * * `X-Forwarded-For` is honoured ONLY when `CREWLY_TRUST_PROXY=1`, otherwise * any LAN caller could spoof a loopback origin. * * The existing `require-auth.middleware.ts` (HS256 JWT for chat-v2) is * untouched; this module only re-uses its verifier so that `/ws/chat` * clients holding a valid JWT keep working when `CREWLY_JWT_SECRET` is set. * * @module middleware/api-token */ import type { Request, Response, NextFunction } from 'express'; import type { IncomingMessage, Server as HttpServer } from 'http'; /** Minimal request shape shared by Express requests and raw upgrade requests. */ export interface AddressableRequest { headers: IncomingMessage['headers']; socket?: { remoteAddress?: string; } | null; url?: string; } /** * Whether `X-Forwarded-For` should be trusted for loopback classification. * * @returns True when `CREWLY_TRUST_PROXY` is `1` or `true` */ export declare function isProxyTrusted(): boolean; /** * Resolve the address a request originates from. * * @param req - Express request or raw upgrade request * @returns Remote address string, or empty string when unknown */ export declare function getClientAddress(req: AddressableRequest): string; /** * Whether an address is one of the loopback forms Node reports. * * @param address - Remote address * @returns True for `127.0.0.1`, `::1` and `::ffff:127.0.0.1` */ export declare function isLoopbackAddress(address: string): boolean; /** * Whether a request originates from loopback (honouring the proxy rule). * * @param req - Express request or raw upgrade request * @returns True when the caller is local */ export declare function isLoopbackRequest(req: AddressableRequest): boolean; /** * Extract the token a caller presented, if any. * * Order: `Authorization: Bearer`, `X-Crewly-Token`, `crewly_token` cookie, * then (when `allowQuery`) the `?token=` query parameter. * * @param req - Request carrying headers and url * @param allowQuery - Whether to consult the query string (WebSocket handshakes) * @returns Presented token or null */ export declare function extractPresentedToken(req: AddressableRequest, allowQuery: boolean): string | null; /** * The API token a caller presented in a header (`Authorization: Bearer` or * `X-Crewly-Token`) or, failing that, in the `crewly_token` cookie, and which * of the two it was. A cookie is sent by the browser on its own, so a * cookie-borne token alone does not authorise a write (#999). * * @param req - Request carrying headers * @returns Token and source, or null when none was presented */ export declare function extractPresentedTokenWithSource(req: AddressableRequest): { token: string; source: 'header' | 'cookie'; } | null; /** * Parse a `Cookie` header (exported for the caller-identity middleware). * * @param header - Raw cookie header * @returns Parsed cookies */ export declare function parseCookieHeader(header: string | undefined): Record; /** * Whether a request presented a valid API token. * * @param req - Request carrying headers and url * @param allowQuery - Whether to consult the query string * @returns True when the presented token matches */ export declare function hasValidApiToken(req: AddressableRequest, allowQuery: boolean): boolean; /** * Express middleware: loopback passes, everyone else needs the API token. * * Mounted in front of `/api` in `backend/src/index.ts`. `/health`, static * assets and the SPA shell are not under `/api` and stay open. * * @param req - Express request * @param res - Express response * @param next - Express next */ export declare function apiTokenMiddleware(req: Request, res: Response, next: NextFunction): void; /** * Whether `GET /health` is configured open to everyone (`CREWLY_PUBLIC_HEALTH`). * * @returns True when the env var is `1` or `true` */ export declare function isPublicHealthEnabled(): boolean; /** * Gate for the root `GET /health` (#825). * * - Loopback: passes straight to the handler, untouched (CLI, Docker * healthchecks, desktop, local dashboard). * - Non-loopback with a valid API token (header, Bearer or cookie): passes. * - Non-loopback without one: the standard 401 token challenge, same as * `/api`, and nothing about the install is disclosed. * - `CREWLY_PUBLIC_HEALTH=1`: everyone passes (public monitors behind a * reverse proxy). * * Why: crewly-mobile probes `/health` to choose its LAN transport. While * `/health` answered 200 to anyone, a phone without the token chose LAN and * then got 401 on every `/api` call, never falling back to the relay. A 401 * here makes that probe fail, so the app uses the relay, which works. * "Loopback" follows {@link getClientAddress}: the socket address, or the * first `X-Forwarded-For` hop only when `CREWLY_TRUST_PROXY` is set. * * @param req - Express request * @param res - Express response * @param next - Express next */ export declare function healthGateMiddleware(req: Request, res: Response, next: NextFunction): void; /** * Decide whether a raw HTTP request (WebSocket upgrade or Engine.IO * handshake) may proceed. * * Loopback passes. Otherwise the API token must be present (query `token` * accepted here because browsers cannot set headers on WebSocket * handshakes). `/ws/chat` additionally accepts a valid chat JWT when * `CREWLY_JWT_SECRET` is configured, since that gateway has its own auth. * * @param req - Incoming HTTP request * @returns True when the connection is allowed */ export declare function isUpgradeAllowed(req: IncomingMessage): boolean; /** * Engine.IO `allowRequest` hook for the Socket.IO server. Covers both the * polling handshake (which bypasses Express) and the WebSocket transport. * * @param req - Handshake request * @param callback - Engine.IO callback `(err, success)` */ export declare function socketIoAllowRequest(req: IncomingMessage, callback: (err: string | null | undefined, success: boolean) => void): void; /** * Install the WebSocket upgrade gate on an HTTP server. * * Wraps `httpServer.emit` (the same exclusive-handoff pattern the browser * bridge and chat gateway use) so an unauthorised `upgrade` is answered * with `401` and closed before ANY gateway sees it. Must be installed * AFTER every gateway has attached so this wrapper is the outermost one. * * @param httpServer - Server whose upgrades to gate */ export declare function installWebSocketGate(httpServer: HttpServer): void; //# sourceMappingURL=api-token.middleware.d.ts.map