{"version":3,"file":"adapter.mjs","names":[],"sources":["../src/adapter.ts"],"sourcesContent":["import type {\n  CookieOptions,\n  CsrfAdapter,\n  CsrfRequest,\n  CsrfResponse,\n  RequiredCsrfConfig,\n} from '@csrf-armor/core';\nimport type express from 'express';\n\n/**\n * Express.js adapter for CSRF protection.\n *\n * This adapter implements the CsrfAdapter interface to provide CSRF protection\n * for Express.js applications. It handles request/response transformation,\n * token extraction from various sources, and cookie/header management.\n *\n * @example\n * ```typescript\n * import { CsrfProtection } from '@csrf-armor/core';\n * import { ExpressAdapter } from '@csrf-armor/express';\n *\n * const csrf = new CsrfProtection(new ExpressAdapter(), {\n *   secret: 'your-secret-key',\n *   strategy: 'signed-double-submit'\n * });\n * ```\n */\nexport class ExpressAdapter\n  implements CsrfAdapter<express.Request, express.Response>\n{\n  /**\n   * Extracts CSRF-relevant data from an Express.js request.\n   *\n   * Converts an Express Request object into a standardized CsrfRequest format\n   * that can be processed by the core CSRF protection logic. Handles header\n   * normalization, cookie extraction, and body access.\n   *\n   * @param req - Express.js request object\n   * @returns Normalized CSRF request object\n   *\n   * @example\n   * ```typescript\n   * const adapter = new ExpressAdapter();\n   * const csrfRequest = adapter.extractRequest(req);\n   * // csrfRequest contains normalized headers, cookies, and body\n   * ```\n   */\n  extractRequest(req: express.Request): CsrfRequest {\n    // Create a Map for headers with proper type handling\n    const headers = new Map<string, string>();\n\n    // Safely process headers, handling string, string[], and undefined values\n    if (req.headers) {\n      Object.entries(req.headers).forEach(([key, value]) => {\n        if (Array.isArray(value)) {\n          // Join array values into a single string\n          headers.set(key.toLowerCase(), value.join(', '));\n        } else if (value !== undefined) {\n          // Set string values directly\n          headers.set(key.toLowerCase(), value);\n        }\n        // Skip undefined values\n      });\n    }\n\n    return {\n      method: req.method,\n      url: req.url,\n      headers,\n      cookies: new Map(\n        Object.entries(req.cookies ?? {}).map(([key, value]) => [key.toLowerCase(), String(value)])\n      ),\n      body: req.body,\n    };\n  }\n\n  /**\n   * Sets a cookie on the Express.js response with CSRF protection options.\n   *\n   * Converts CSRF armor cookie options to Express.js cookie format,\n   * including proper maxAge conversion (seconds to milliseconds).\n   *\n   * @param res - Express.js response object\n   * @param name - Cookie name\n   * @param value - Cookie value\n   * @param options - CSRF cookie options\n   *\n   * @private\n   */\n  private setCookie(\n    res: express.Response,\n    name: string,\n    value: string,\n    options?: CookieOptions\n  ): void {\n    res.cookie(name, value, {\n      secure: options?.secure,\n      httpOnly: options?.httpOnly,\n      sameSite: options?.sameSite,\n      path: options?.path,\n      domain: options?.domain,\n      maxAge: options?.maxAge ? options.maxAge * 1000 : undefined, // Express expects milliseconds\n    });\n  }\n\n  /**\n   * Sets HTTP headers on the Express.js response.\n   *\n   * Handles both Map and object-based header formats from the CSRF response.\n   *\n   * @param res - Express.js response object\n   * @param headers - Headers to set (Map or object format)\n   *\n   * @private\n   */\n  private setHeaders(\n    res: express.Response,\n    headers: CsrfResponse['headers']\n  ): void {\n    const entries =\n      headers instanceof Map ? headers : Object.entries(headers || {});\n    for (const [key, value] of entries) {\n      res.setHeader(key, value);\n    }\n  }\n\n  /**\n   * Applies CSRF protection data to an Express.js response.\n   *\n   * Sets headers and cookies on the Express response based on the CSRF\n   * protection results. This includes CSRF tokens, strategy hints, and\n   * security cookies.\n   *\n   * @param res - Express.js response object to modify\n   * @param csrfResponse - CSRF response data containing headers and cookies\n   * @returns The modified Express.js response object\n   *\n   * @example\n   * ```typescript\n   * const adapter = new ExpressAdapter();\n   * const modifiedResponse = adapter.applyResponse(res, {\n   *   headers: new Map([['x-csrf-token', 'abc123']]),\n   *   cookies: new Map([['csrf-token', { value: 'def456', options: { httpOnly: true } }]])\n   * });\n   * ```\n   */\n  applyResponse(\n    res: express.Response,\n    csrfResponse: CsrfResponse\n  ): express.Response {\n    this.setHeaders(res, csrfResponse.headers);\n\n    if (csrfResponse.cookies) {\n      const entries =\n        csrfResponse.cookies instanceof Map\n          ? Array.from(csrfResponse.cookies.entries())\n          : Object.entries(csrfResponse.cookies);\n\n      for (const [name, { value, options }] of entries) {\n        this.setCookie(res, name, value, options);\n      }\n    }\n\n    return res;\n  }\n\n  /**\n   * Extracts CSRF token from various locations in an Express.js request.\n   *\n   * Attempts to find the CSRF token in the following order:\n   * 1. HTTP headers (e.g., X-CSRF-Token)\n   * 2. URL query parameters\n   * 3. Request body (form data or JSON)\n   *\n   * This method handles relative URLs safely by providing a base URL for parsing.\n   *\n   * @param request - Normalized CSRF request object\n   * @param config - CSRF configuration containing token field names\n   * @returns The extracted token string, or undefined if not found\n   *\n   * @example\n   * ```typescript\n   * const adapter = new ExpressAdapter();\n   * const token = await adapter.getTokenFromRequest(csrfRequest, {\n   *   token: { headerName: 'X-CSRF-Token', fieldName: 'csrf_token' }\n   * });\n   * // Returns token from header, query param, or body\n   * ```\n   */\n  async getTokenFromRequest(\n    request: CsrfRequest,\n    config: RequiredCsrfConfig\n  ): Promise<string | undefined> {\n    const headers =\n      request.headers instanceof Map\n        ? request.headers\n        : new Map(Object.entries(request.headers));\n\n    // Try header first (most common for APIs)\n    const headerValue = headers.get(config.token.headerName.toLowerCase());\n    if (headerValue) return headerValue;\n\n    // Try cookie\n    const cookies =\n      request.cookies instanceof Map\n        ? request.cookies\n        : new Map(\n            Object.entries(request.cookies || {}).map(([key, value]) => [key.toLowerCase(), String(value)])\n          );\n    const cookieValue = cookies.get(config.cookie.name.toLowerCase());\n    if (cookieValue) return cookieValue;\n\n    // Try query parameter\n    if (request.url) {\n      try {\n        // Express request.url is a relative path, so we need to create a full URL\n        const url = new URL(request.url, 'http://localhost');\n        const queryValue = url.searchParams.get(config.token.fieldName);\n        if (queryValue) return queryValue;\n      } catch {\n        // If URL parsing fails, skip query parameter extraction\n      }\n    }\n\n    // Try form body\n    if (request.body && typeof request.body === 'object') {\n      const body = request.body as Record<string, unknown>;\n      const formValue = body[config.token.fieldName];\n      if (typeof formValue === 'string') return formValue;\n    }\n\n    return undefined;\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2BA,IAAa,iBAAb,MAEA;;;;;;;;;;;;;;;;;;CAkBE,eAAe,KAAmC;EAEhD,MAAM,0BAAU,IAAI,KAAqB;AAGzC,MAAI,IAAI,QACN,QAAO,QAAQ,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,WAAW;AACpD,OAAI,MAAM,QAAQ,MAAM,CAEtB,SAAQ,IAAI,IAAI,aAAa,EAAE,MAAM,KAAK,KAAK,CAAC;YACvC,UAAU,OAEnB,SAAQ,IAAI,IAAI,aAAa,EAAE,MAAM;IAGvC;AAGJ,SAAO;GACL,QAAQ,IAAI;GACZ,KAAK,IAAI;GACT;GACA,SAAS,IAAI,IACX,OAAO,QAAQ,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,IAAI,aAAa,EAAE,OAAO,MAAM,CAAC,CAAC,CAC5F;GACD,MAAM,IAAI;GACX;;;;;;;;;;;;;;;CAgBH,AAAQ,UACN,KACA,MACA,OACA,SACM;AACN,MAAI,OAAO,MAAM,OAAO;GACtB,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,UAAU,SAAS;GACnB,MAAM,SAAS;GACf,QAAQ,SAAS;GACjB,QAAQ,SAAS,SAAS,QAAQ,SAAS,MAAO;GACnD,CAAC;;;;;;;;;;;;CAaJ,AAAQ,WACN,KACA,SACM;EACN,MAAM,UACJ,mBAAmB,MAAM,UAAU,OAAO,QAAQ,WAAW,EAAE,CAAC;AAClE,OAAK,MAAM,CAAC,KAAK,UAAU,QACzB,KAAI,UAAU,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;CAwB7B,cACE,KACA,cACkB;AAClB,OAAK,WAAW,KAAK,aAAa,QAAQ;AAE1C,MAAI,aAAa,SAAS;GACxB,MAAM,UACJ,aAAa,mBAAmB,MAC5B,MAAM,KAAK,aAAa,QAAQ,SAAS,CAAC,GAC1C,OAAO,QAAQ,aAAa,QAAQ;AAE1C,QAAK,MAAM,CAAC,MAAM,EAAE,OAAO,cAAc,QACvC,MAAK,UAAU,KAAK,MAAM,OAAO,QAAQ;;AAI7C,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;CA0BT,MAAM,oBACJ,SACA,QAC6B;EAO7B,MAAM,eALJ,QAAQ,mBAAmB,MACvB,QAAQ,UACR,IAAI,IAAI,OAAO,QAAQ,QAAQ,QAAQ,CAAC,EAGlB,IAAI,OAAO,MAAM,WAAW,aAAa,CAAC;AACtE,MAAI,YAAa,QAAO;EASxB,MAAM,eALJ,QAAQ,mBAAmB,MACvB,QAAQ,UACR,IAAI,IACF,OAAO,QAAQ,QAAQ,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,IAAI,aAAa,EAAE,OAAO,MAAM,CAAC,CAAC,CAChG,EACqB,IAAI,OAAO,OAAO,KAAK,aAAa,CAAC;AACjE,MAAI,YAAa,QAAO;AAGxB,MAAI,QAAQ,IACV,KAAI;GAGF,MAAM,aADM,IAAI,IAAI,QAAQ,KAAK,mBAAmB,CAC7B,aAAa,IAAI,OAAO,MAAM,UAAU;AAC/D,OAAI,WAAY,QAAO;UACjB;AAMV,MAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAS,UAAU;GAEpD,MAAM,YADO,QAAQ,KACE,OAAO,MAAM;AACpC,OAAI,OAAO,cAAc,SAAU,QAAO"}