{"version":3,"file":"csrf.mjs","names":[],"sources":["../../src/hono/csrf.ts"],"sourcesContent":["import { timingSafeEqual } from 'node:crypto';\nimport type { MiddlewareHandler } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport { METHOD_NAME_ALL } from 'hono/router';\nimport { RegExpRouter } from 'hono/router/reg-exp-router';\nimport { SmartRouter } from 'hono/router/smart-router';\nimport { TrieRouter } from 'hono/router/trie-router';\nimport { Status } from '../error/status';\n\ntype HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';\n\nexport type CSRFIgnoreRule = string | { path: string; methods?: [HTTPMethod, ...HTTPMethod[]] };\n\nexport interface CSRFConfig {\n  /**\n   * Cookie name for CSRF token\n   * @default 'XSRF-TOKEN'\n   */\n  cookieName?: string;\n\n  /**\n   * Header name for CSRF token\n   * @default 'X-XSRF-TOKEN'\n   */\n  headerName?: string;\n\n  /**\n   * Ignore rules for specific paths and methods\n   * @example\n   * [\n   *   { path: '/api/webhook/*', methods: ['POST'] },\n   *   { path: '/auth/apple/callback' }, // ignores all methods\n   * ]\n   */\n  ignores?: CSRFIgnoreRule[];\n\n  /**\n   * Skip CSRF check for these methods\n   * @default ['GET', 'HEAD', 'OPTIONS']\n   */\n  safeMethods?: HTTPMethod[];\n\n  /**\n   * Origin allowed to bypass CSRF check\n   * @default undefined\n   */\n  origin?: string[];\n\n  /**\n   * Sec-Fetch-Site allowed to bypass CSRF check\n   * @default undefined\n   */\n  secFetchSite?: Array<'same-origin' | 'same-site' | 'none' | 'cross-origin'>;\n\n  /**\n   * Custom error message\n   * @default 'CSRF token validation failed'\n   */\n  errorMessage?: string;\n}\n\n/** use timing safe compare to prevent timing attack */\nfunction safeCompare(a: string, b: string): boolean {\n  if (typeof a !== 'string' || typeof b !== 'string') return false;\n  if (a.length === 0 || b.length === 0) return false;\n\n  const bufferA = Buffer.from(a, 'utf-8');\n  const bufferB = Buffer.from(b, 'utf-8');\n  if (bufferA.length !== bufferB.length) return false;\n  return timingSafeEqual(bufferA, bufferB);\n}\n\n/**\n * Create CSRF protection middleware\n *\n * @example\n * ```ts\n * import { Hono } from 'hono';\n * import { csrf } from '@shware/http/hono';\n *\n * const app = new Hono();\n *\n * // basic usage\n * app.use(csrf());\n *\n * // with configuration\n * app.use(csrf({\n *   cookieName: 'csrf-token',\n *   headerName: 'X-CSRF-Token',\n *   ignores: [\n *     { path: '/api/webhook/*', methods: ['POST'] },\n *     { path: '/auth/apple/callback' },\n *   ]\n * }));\n * ```\n */\nexport function csrf(config: CSRFConfig = {}): MiddlewareHandler {\n  const cookieName = config.cookieName ?? 'XSRF-TOKEN';\n  const headerName = config.headerName ?? 'X-XSRF-TOKEN';\n  const safeMethods = new Set(config.safeMethods ?? ['GET', 'HEAD', 'OPTIONS']);\n  const errorMessage = config.errorMessage ?? 'CSRF token validation failed';\n\n  // initialize router for matching ignore rules\n  const router = new SmartRouter<boolean>({\n    routers: [new RegExpRouter(), new TrieRouter()],\n  });\n\n  // register ignore rules\n  if (config.ignores) {\n    for (const rule of config.ignores) {\n      if (typeof rule === 'string') {\n        router.add(METHOD_NAME_ALL, rule, true);\n      } else if (rule.methods && rule.methods.length > 0) {\n        for (const method of rule.methods) {\n          router.add(method, rule.path, true);\n        }\n      } else {\n        // if no methods are specified, ignore all methods\n        router.add(METHOD_NAME_ALL, rule.path, true);\n      }\n    }\n  }\n\n  // return middleware\n  return async (c, next) => {\n    const method = c.req.method;\n    const path = c.req.path;\n\n    // check if the request should be ignored\n    // 1. ignore safe methods\n    if (safeMethods.has(method)) {\n      await next();\n      return;\n    }\n\n    // 2. ignore configured origin\n    if (config.origin?.includes(c.req.header('origin') ?? '')) {\n      await next();\n      return;\n    }\n\n    // 3. ignore configured secFetchSite\n    if (config.secFetchSite?.includes((c.req.header('sec-fetch-site') ?? '') as never)) {\n      await next();\n      return;\n    }\n\n    // 4. ignore configured ignore rules\n    const [matched] = router.match(method, path);\n    if (matched.length > 0) {\n      await next();\n      return;\n    }\n\n    const cookieToken = getCookie(c, cookieName);\n    const headerToken = c.req.header(headerName);\n\n    if (!cookieToken || !headerToken) {\n      throw Status.permissionDenied(errorMessage).error();\n    }\n\n    if (!safeCompare(cookieToken, headerToken)) {\n      throw Status.permissionDenied(errorMessage).error();\n    }\n\n    await next();\n  };\n}\n"],"mappings":";;;;;;;;;AA8DA,SAAS,YAAY,GAAW,GAAoB;CAClD,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU,OAAO;CAC3D,IAAI,EAAE,WAAW,KAAK,EAAE,WAAW,GAAG,OAAO;CAE7C,MAAM,UAAU,OAAO,KAAK,GAAG,OAAO;CACtC,MAAM,UAAU,OAAO,KAAK,GAAG,OAAO;CACtC,IAAI,QAAQ,WAAW,QAAQ,QAAQ,OAAO;CAC9C,OAAO,gBAAgB,SAAS,OAAO;AACzC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,KAAK,SAAqB,CAAC,GAAsB;CAC/D,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,cAAc,IAAI,IAAI,OAAO,eAAe;EAAC;EAAO;EAAQ;CAAS,CAAC;CAC5E,MAAM,eAAe,OAAO,gBAAgB;CAG5C,MAAM,SAAS,IAAI,YAAqB,EACtC,SAAS,CAAC,IAAI,aAAa,GAAG,IAAI,WAAW,CAAC,EAChD,CAAC;CAGD,IAAI,OAAO,SACT,KAAK,MAAM,QAAQ,OAAO,SACxB,IAAI,OAAO,SAAS,UAClB,OAAO,IAAI,iBAAiB,MAAM,IAAI;MACjC,IAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GAC/C,KAAK,MAAM,UAAU,KAAK,SACxB,OAAO,IAAI,QAAQ,KAAK,MAAM,IAAI;MAIpC,OAAO,IAAI,iBAAiB,KAAK,MAAM,IAAI;CAMjD,OAAO,OAAO,GAAG,SAAS;EACxB,MAAM,SAAS,EAAE,IAAI;EACrB,MAAM,OAAO,EAAE,IAAI;EAInB,IAAI,YAAY,IAAI,MAAM,GAAG;GAC3B,MAAM,KAAK;GACX;EACF;EAGA,IAAI,OAAO,QAAQ,SAAS,EAAE,IAAI,OAAO,QAAQ,KAAK,EAAE,GAAG;GACzD,MAAM,KAAK;GACX;EACF;EAGA,IAAI,OAAO,cAAc,SAAU,EAAE,IAAI,OAAO,gBAAgB,KAAK,EAAY,GAAG;GAClF,MAAM,KAAK;GACX;EACF;EAGA,MAAM,CAAC,WAAW,OAAO,MAAM,QAAQ,IAAI;EAC3C,IAAI,QAAQ,SAAS,GAAG;GACtB,MAAM,KAAK;GACX;EACF;EAEA,MAAM,cAAc,UAAU,GAAG,UAAU;EAC3C,MAAM,cAAc,EAAE,IAAI,OAAO,UAAU;EAE3C,IAAI,CAAC,eAAe,CAAC,aACnB,MAAM,OAAO,iBAAiB,YAAY,CAAC,CAAC,MAAM;EAGpD,IAAI,CAAC,YAAY,aAAa,WAAW,GACvC,MAAM,OAAO,iBAAiB,YAAY,CAAC,CAAC,MAAM;EAGpD,MAAM,KAAK;CACb;AACF"}