[{"data":1,"prerenderedAt":10268},["ShallowReactive",2],{"navLinks":3,"sidebar_docs_navigation_\u002Fblog":64,"navigation":76,"navLinks_footer":790,"blog_data":803},{"id":4,"extension":5,"links":6,"meta":61,"stem":62,"__hash__":63},"navigationMenu\u002Fnavigation.json","json",[7,52,57],{"nested":8,"label":9,"icon":10,"to":11,"children":12},true,"Docs","i-lucide-book-open","\u002Fdocs\u002Fgetting-started",[13,19,26,32,39,45],{"label":14,"icon":15,"to":11,"description":16,"github":17,"badge":18},"Getting Started","i-lucide-rocket","An introduction to help you understand the core components.","https:\u002F\u002Fgithub.com\u002FSergo706\u002Fdocshub","Start Here",{"label":20,"icon":21,"to":22,"description":23,"github":24,"badge":25},"Auth H3 Client","i-lucide-key-round","\u002Fdocs\u002Fauth-h3client","Seamlessly enforce OAuth 2.0 authentication and session management integrated directly as the client of the IAM module.","https:\u002F\u002Fgithub.com\u002FSergo706\u002Fauth-h3client","Core",{"label":27,"icon":28,"to":29,"description":30,"github":31,"badge":25},"IAM","i-lucide-shield-check","\u002Fdocs\u002Fiam","Identity and Access Management featuring granular roles, permissions, and security policies.","https:\u002F\u002Fgithub.com\u002FSergo706\u002Fauth",{"label":33,"icon":34,"to":35,"description":36,"github":37,"badge":38},"Bot Detection","i-lucide-cpu","\u002Fdocs\u002Fbot-detection","Advanced behavioral analysis and request fingerprinting to stop malicious automated traffic.","https:\u002F\u002Fgithub.com\u002FSergo706\u002Fbot-detector","Security",{"label":40,"icon":41,"to":42,"description":43,"github":44,"badge":38},"Shield Base","i-lucide-database-zap","\u002Fdocs\u002Fshield-base","CLI and programmatic toolkit for compiling offline-ready IP intelligence databases from BGP, GeoIP, Tor, FireHOL, and other public threat feeds.","https:\u002F\u002Fgithub.com\u002FSergo706\u002Fshield-base-cli",{"label":46,"icon":47,"to":48,"description":49,"github":50,"badge":51},"Utils","i-lucide-wrench","\u002Fdocs\u002Futils","A standard library of highly optimized helpers for formatting, validation, and core logic.","https:\u002F\u002Fgithub.com\u002FSergo706\u002Futils","Library",{"nested":53,"label":54,"icon":55,"to":56},false,"Blog","i-lucide-pen-line","\u002Fblog",{"nested":53,"label":58,"icon":59,"to":60},"Website","lucide:app-window-mac","https:\u002F\u002Friavzon.com",{},"navigation","gkaQ0xRGxSLrLyM3kttLe0oBwkrR1EBjlepF8LSbwF8",[65],{"title":54,"path":56,"stem":66,"children":67,"page":53},"blog",[68,72],{"title":69,"path":70,"stem":71},"IAM API Tokens with Auth H3 Client: Secure M2M Access in Nuxt and Nitro","\u002Fblog\u002Fiam-api-tokens-auth-h3client","blog\u002Fiam-api-tokens-auth-h3client",{"title":73,"path":74,"stem":75},"Layered Bot Defense: How Shield Base, Bot Detector, and the IAM Canary Cookie Work Together","\u002Fblog\u002Flayered-bot-defense","blog\u002Flayered-bot-defense",[77],{"title":9,"path":78,"stem":79,"children":80,"page":53},"\u002Fdocs","docs",[81,229,347,352,530,597],{"title":20,"path":22,"stem":82,"children":83},"docs\u002Fauth-h3client\u002Findex",[84,85,94,131,157,179,182,203,207],{"title":20,"path":22,"stem":82},{"title":14,"path":86,"stem":87,"children":88},"\u002Fdocs\u002Fauth-h3client\u002Fgetting-started","docs\u002Fauth-h3client\u002F00.getting-started\u002Findex",[89,90],{"title":14,"path":86,"stem":87},{"title":91,"path":92,"stem":93},"Nuxt Module","\u002Fdocs\u002Fauth-h3client\u002Fgetting-started\u002Fnuxt","docs\u002Fauth-h3client\u002F00.getting-started\u002F00.nuxt",{"title":95,"path":96,"stem":97,"children":98},"Essentials","\u002Fdocs\u002Fauth-h3client\u002Fessentials","docs\u002Fauth-h3client\u002F01.essentials\u002Findex",[99,100,104,108,112,116,120,123,127],{"title":95,"path":96,"stem":97},{"title":101,"path":102,"stem":103},"Session Management","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fsession","docs\u002Fauth-h3client\u002F01.essentials\u002F00.session",{"title":105,"path":106,"stem":107},"Route Protection","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Froute-protection","docs\u002Fauth-h3client\u002F01.essentials\u002F01.route-protection",{"title":109,"path":110,"stem":111},"CSRF Protection","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fcsrf","docs\u002Fauth-h3client\u002F01.essentials\u002F02.csrf",{"title":113,"path":114,"stem":115},"Auth Flows","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fauth-flows","docs\u002Fauth-h3client\u002F01.essentials\u002F03.auth-flows",{"title":117,"path":118,"stem":119},"OAuth and OIDC","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Foauth","docs\u002Fauth-h3client\u002F01.essentials\u002F04.oauth",{"title":33,"path":121,"stem":122},"\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fbot-detection","docs\u002Fauth-h3client\u002F01.essentials\u002F05.bot-detection",{"title":124,"path":125,"stem":126},"Cookies","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fcookies","docs\u002Fauth-h3client\u002F01.essentials\u002F06.cookies",{"title":128,"path":129,"stem":130},"Logging","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Flogging","docs\u002Fauth-h3client\u002F01.essentials\u002F07.logging",{"title":132,"path":133,"stem":134,"children":135},"MFA","\u002Fdocs\u002Fauth-h3client\u002Fmfa","docs\u002Fauth-h3client\u002F02.mfa\u002Findex",[136,137,141,145,149,153],{"title":132,"path":133,"stem":134},{"title":138,"path":139,"stem":140},"Built-in MFA","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fbuilt-in-flow","docs\u002Fauth-h3client\u002F02.mfa\u002F01.built-in-flow",{"title":142,"path":143,"stem":144},"Password Reset","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fpassword-reset","docs\u002Fauth-h3client\u002F02.mfa\u002F02.password-reset",{"title":146,"path":147,"stem":148},"Email Change","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Femail-change","docs\u002Fauth-h3client\u002F02.mfa\u002F03.email-change",{"title":150,"path":151,"stem":152},"Custom MFA Flow","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fcustom-flow","docs\u002Fauth-h3client\u002F02.mfa\u002F04.custom-flow",{"title":154,"path":155,"stem":156},"Client-Side MFA","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fclient-side","docs\u002Fauth-h3client\u002F02.mfa\u002F05.client-side",{"title":158,"path":159,"stem":160,"children":161},"Client-side","\u002Fdocs\u002Fauth-h3client\u002Fclient","docs\u002Fauth-h3client\u002F03.client\u002Findex",[162,163,167,171,175],{"title":158,"path":159,"stem":160},{"title":164,"path":165,"stem":166},"useAuthData","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fuse-auth-data","docs\u002Fauth-h3client\u002F03.client\u002F00.use-auth-data",{"title":168,"path":169,"stem":170},"useMagicLink","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fuse-magic-link","docs\u002Fauth-h3client\u002F03.client\u002F01.use-magic-link",{"title":172,"path":173,"stem":174},"executeRequest","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fexecute-request","docs\u002Fauth-h3client\u002F03.client\u002F02.execute-request",{"title":176,"path":177,"stem":178},"getCsrfToken","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fget-csrf-token","docs\u002Fauth-h3client\u002F03.client\u002F03.get-csrf-token",{"title":38,"path":180,"stem":181},"\u002Fdocs\u002Fauth-h3client\u002Fsecurity","docs\u002Fauth-h3client\u002F04.security",{"title":183,"path":184,"stem":185,"children":186,"page":53},"Guides","\u002Fdocs\u002Fauth-h3client\u002Fguides","docs\u002Fauth-h3client\u002F05.guides",[187,191,195,199],{"title":188,"path":189,"stem":190},"H3 and Nitro Setup","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fh3-nitro","docs\u002Fauth-h3client\u002F05.guides\u002F00.h3-nitro",{"title":192,"path":193,"stem":194},"HMAC Inter-service Auth","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fhmac","docs\u002Fauth-h3client\u002F05.guides\u002Fhmac",{"title":196,"path":197,"stem":198},"Image Upload","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fimage-upload","docs\u002Fauth-h3client\u002F05.guides\u002Fimage-upload",{"title":200,"path":201,"stem":202},"mTLS Configuration","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fmtls","docs\u002Fauth-h3client\u002F05.guides\u002Fmtls",{"title":204,"path":205,"stem":206},"Configuration","\u002Fdocs\u002Fauth-h3client\u002Fconfiguration","docs\u002Fauth-h3client\u002F06.configuration",{"title":208,"path":209,"stem":210,"children":211},"API Reference","\u002Fdocs\u002Fauth-h3client\u002Fapi","docs\u002Fauth-h3client\u002F07.api\u002Findex",[212,213,217,221,225],{"title":208,"path":209,"stem":210},{"title":214,"path":215,"stem":216},"Routes Reference","\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fcontrollers","docs\u002Fauth-h3client\u002F07.api\u002F00.controllers",{"title":218,"path":219,"stem":220},"Middleware Reference","\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fmiddleware","docs\u002Fauth-h3client\u002F07.api\u002F01.middleware",{"title":222,"path":223,"stem":224},"Client-side Reference","\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fcomposables","docs\u002Fauth-h3client\u002F07.api\u002F02.composables",{"title":226,"path":227,"stem":228},"Utilities","\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Futilities","docs\u002Fauth-h3client\u002F07.api\u002F03.utilities",{"title":230,"path":35,"stem":231,"children":232},"Bot Detector","docs\u002Fbot-detection\u002Findex",[233,234,237,241,245,264,338,341,344],{"title":230,"path":35,"stem":231},{"title":14,"path":235,"stem":236},"\u002Fdocs\u002Fbot-detection\u002Fgetting-started","docs\u002Fbot-detection\u002F00.getting-started",{"title":238,"path":239,"stem":240},"CLI","\u002Fdocs\u002Fbot-detection\u002Fcli","docs\u002Fbot-detection\u002F01.cli",{"title":242,"path":243,"stem":244},"Data Sources","\u002Fdocs\u002Fbot-detection\u002Fdata-sources","docs\u002Fbot-detection\u002F02.data-sources",{"title":183,"path":246,"stem":247,"children":248,"page":53},"\u002Fdocs\u002Fbot-detection\u002Fguides","docs\u002Fbot-detection\u002F03.guides",[249,253,257,260],{"title":250,"path":251,"stem":252},"Custom Checkers","\u002Fdocs\u002Fbot-detection\u002Fguides\u002Fcustom","docs\u002Fbot-detection\u002F03.guides\u002FCUSTOM",{"title":254,"path":255,"stem":256},"Scheduling Database Generation","\u002Fdocs\u002Fbot-detection\u002Fguides\u002Fgenerate","docs\u002Fbot-detection\u002F03.guides\u002FGENERATE",{"title":128,"path":258,"stem":259},"\u002Fdocs\u002Fbot-detection\u002Fguides\u002Flogging","docs\u002Fbot-detection\u002F03.guides\u002FLOGGING",{"title":261,"path":262,"stem":263},"Score Modes and Reputation Healing","\u002Fdocs\u002Fbot-detection\u002Fguides\u002Fscore","docs\u002Fbot-detection\u002F03.guides\u002FSCORE",{"title":265,"path":266,"stem":267,"children":268},"Checkers","\u002Fdocs\u002Fbot-detection\u002Fcheckers","docs\u002Fbot-detection\u002F04.checkers\u002Findex",[269,270,274,278,282,286,290,294,298,302,306,310,314,318,322,326,330,334],{"title":265,"path":266,"stem":267},{"title":271,"path":272,"stem":273},"IP Validation","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fip-validation","docs\u002Fbot-detection\u002F04.checkers\u002F01.ip-validation",{"title":275,"path":276,"stem":277},"Good \u002F Bad Bot Verification","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fgood-bots","docs\u002Fbot-detection\u002F04.checkers\u002F02.good-bots",{"title":279,"path":280,"stem":281},"Browser & Device Fingerprint","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fbrowser-device","docs\u002Fbot-detection\u002F04.checkers\u002F03.browser-device",{"title":283,"path":284,"stem":285},"Locale Map","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Flocale-map","docs\u002Fbot-detection\u002F04.checkers\u002F04.locale-map",{"title":287,"path":288,"stem":289},"Known Threats","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fknown-threats","docs\u002Fbot-detection\u002F04.checkers\u002F05.known-threats",{"title":291,"path":292,"stem":293},"ASN Classification","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fasn-classification","docs\u002Fbot-detection\u002F04.checkers\u002F06.asn-classification",{"title":295,"path":296,"stem":297},"Tor Analysis","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Ftor-analysis","docs\u002Fbot-detection\u002F04.checkers\u002F07.tor-analysis",{"title":299,"path":300,"stem":301},"Timezone Consistency","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Ftimezone-consistency","docs\u002Fbot-detection\u002F04.checkers\u002F08.timezone-consistency",{"title":303,"path":304,"stem":305},"Honeypot","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fhoneypot","docs\u002Fbot-detection\u002F04.checkers\u002F09.honeypot",{"title":307,"path":308,"stem":309},"Known Bad IPs","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fknown-bad-ips","docs\u002Fbot-detection\u002F04.checkers\u002F10.known-bad-ips",{"title":311,"path":312,"stem":313},"Behavior Rate","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fbehavior-rate","docs\u002Fbot-detection\u002F04.checkers\u002F11.behavior-rate",{"title":315,"path":316,"stem":317},"Proxy \u002F ISP \u002F Cookie","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fproxy-isp-cookies","docs\u002Fbot-detection\u002F04.checkers\u002F12.proxy-isp-cookies",{"title":319,"path":320,"stem":321},"Session Coherence","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fsession-coherence","docs\u002Fbot-detection\u002F04.checkers\u002F13.session-coherence",{"title":323,"path":324,"stem":325},"Velocity Fingerprint","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fvelocity-fingerprint","docs\u002Fbot-detection\u002F04.checkers\u002F14.velocity-fingerprint",{"title":327,"path":328,"stem":329},"UA & Header Analysis","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fua-header","docs\u002Fbot-detection\u002F04.checkers\u002F15.ua-header",{"title":331,"path":332,"stem":333},"Geolocation","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fgeolocation","docs\u002Fbot-detection\u002F04.checkers\u002F16.geolocation",{"title":335,"path":336,"stem":337},"Known Bad User-Agents","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fknown-bad-ua","docs\u002Fbot-detection\u002F04.checkers\u002F17.known-bad-ua",{"title":38,"path":339,"stem":340},"\u002Fdocs\u002Fbot-detection\u002Fsecurity","docs\u002Fbot-detection\u002F04.security",{"title":208,"path":342,"stem":343},"\u002Fdocs\u002Fbot-detection\u002Fapi","docs\u002Fbot-detection\u002F05.api",{"title":204,"path":345,"stem":346},"\u002Fdocs\u002Fbot-detection\u002Fconfiguration","docs\u002Fbot-detection\u002F06.configuration",{"title":348,"path":11,"stem":349,"children":350},"Introduction","docs\u002Fgetting-started\u002Findex",[351],{"title":348,"path":11,"stem":349},{"title":27,"path":29,"stem":353,"children":354},"docs\u002Fiam\u002Findex",[355,356,359,494,497,513,516],{"title":27,"path":29,"stem":353},{"title":14,"path":357,"stem":358},"\u002Fdocs\u002Fiam\u002Fgetting-started","docs\u002Fiam\u002F00.getting-started",{"title":95,"path":360,"stem":361,"children":362},"\u002Fdocs\u002Fiam\u002Fessentials","docs\u002Fiam\u002F01.essentials\u002Findex",[363,364,368,372,376,380,384,388,392,396,400,404,407,411,415,419,423,426,430,434,437,441,444],{"title":95,"path":360,"stem":361},{"title":365,"path":366,"stem":367},"Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Ftokens","docs\u002Fiam\u002F01.essentials\u002F00.tokens",{"title":369,"path":370,"stem":371},"Access Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Faccess-tokens","docs\u002Fiam\u002F01.essentials\u002F01.access-tokens",{"title":373,"path":374,"stem":375},"Refresh Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Frefresh-tokens","docs\u002Fiam\u002F01.essentials\u002F02.refresh-tokens",{"title":377,"path":378,"stem":379},"Anomaly Detection","\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies","docs\u002Fiam\u002F01.essentials\u002F03.anomalies",{"title":381,"path":382,"stem":383},"Signup","\u002Fdocs\u002Fiam\u002Fessentials\u002Fsignup","docs\u002Fiam\u002F01.essentials\u002F04.signup",{"title":385,"path":386,"stem":387},"Login","\u002Fdocs\u002Fiam\u002Fessentials\u002Flogin","docs\u002Fiam\u002F01.essentials\u002F05.login",{"title":389,"path":390,"stem":391},"Logout","\u002Fdocs\u002Fiam\u002Fessentials\u002Flogout","docs\u002Fiam\u002F01.essentials\u002F06.logout",{"title":393,"path":394,"stem":395},"OAuth","\u002Fdocs\u002Fiam\u002Fessentials\u002Foauth","docs\u002Fiam\u002F01.essentials\u002F07.oauth",{"title":397,"path":398,"stem":399},"Magic Links","\u002Fdocs\u002Fiam\u002Fessentials\u002Fmagic-links","docs\u002Fiam\u002F01.essentials\u002F08.magic-links",{"title":401,"path":402,"stem":403},"Emails","\u002Fdocs\u002Fiam\u002Fessentials\u002Femails","docs\u002Fiam\u002F01.essentials\u002F09.emails",{"title":132,"path":405,"stem":406},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fmfa","docs\u002Fiam\u002F01.essentials\u002F10.mfa",{"title":408,"path":409,"stem":410},"Fingerprinting","\u002Fdocs\u002Fiam\u002Fessentials\u002Ffingerprinting","docs\u002Fiam\u002F01.essentials\u002F11.fingerprinting",{"title":412,"path":413,"stem":414},"Backend for Frontend","\u002Fdocs\u002Fiam\u002Fessentials\u002Fbff","docs\u002Fiam\u002F01.essentials\u002F12.bff",{"title":416,"path":417,"stem":418},"HMAC Authentication","\u002Fdocs\u002Fiam\u002Fessentials\u002Fhmac","docs\u002Fiam\u002F01.essentials\u002F13.hmac",{"title":420,"path":421,"stem":422},"XSS Protection","\u002Fdocs\u002Fiam\u002Fessentials\u002Fxss","docs\u002Fiam\u002F01.essentials\u002F14.xss",{"title":128,"path":424,"stem":425},"\u002Fdocs\u002Fiam\u002Fessentials\u002Flogging","docs\u002Fiam\u002F01.essentials\u002F15.logging",{"title":427,"path":428,"stem":429},"Rate Limiting","\u002Fdocs\u002Fiam\u002Fessentials\u002Frate-limiting","docs\u002Fiam\u002F01.essentials\u002F16.rate-limiting",{"title":431,"path":432,"stem":433},"Database","\u002Fdocs\u002Fiam\u002Fessentials\u002Fdatabase","docs\u002Fiam\u002F01.essentials\u002F17.database",{"title":124,"path":435,"stem":436},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fcookies","docs\u002Fiam\u002F01.essentials\u002F18.cookies",{"title":438,"path":439,"stem":440},"Service Startup","\u002Fdocs\u002Fiam\u002Fessentials\u002Fservice","docs\u002Fiam\u002F01.essentials\u002F19.service",{"title":142,"path":442,"stem":443},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fpassword-reset","docs\u002Fiam\u002F01.essentials\u002F20.password-reset",{"title":445,"path":446,"stem":447,"children":448},"API Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi","docs\u002Fiam\u002F01.essentials\u002F21.api\u002Findex",[449,450,454,458,488,491],{"title":445,"path":446,"stem":447},{"title":451,"path":452,"stem":453},"Creating Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fcreation","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F00.creation",{"title":455,"path":456,"stem":457},"Verifying Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fverification","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F01.verification",{"title":459,"path":460,"stem":461,"children":462},"Manage Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002Findex",[463,464,468,472,476,480,484],{"title":459,"path":460,"stem":461},{"title":465,"path":466,"stem":467},"Privileges","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Fprivilege","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F00.privilege",{"title":469,"path":470,"stem":471},"Revocation","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Frevocation","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F01.revocation",{"title":473,"path":474,"stem":475},"Rotation","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Frotation","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F02.rotation",{"title":477,"path":478,"stem":479},"IP Restriction","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Fip-updates","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F03.ip-updates",{"title":481,"path":482,"stem":483},"Metadata","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Fmetadata","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F04.metadata",{"title":485,"path":486,"stem":487},"Token Listing","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Flist","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F05.list",{"title":427,"path":489,"stem":490},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Frate-limiting","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F03.rate-limiting",{"title":38,"path":492,"stem":493},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fsecurity","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F04.security",{"title":38,"path":495,"stem":496},"\u002Fdocs\u002Fiam\u002Fsecurity","docs\u002Fiam\u002F02.security",{"title":183,"path":498,"stem":499,"children":500,"page":53},"\u002Fdocs\u002Fiam\u002Fguides","docs\u002Fiam\u002F03.guides",[501,505,509],{"title":502,"path":503,"stem":504},"Deployment","\u002Fdocs\u002Fiam\u002Fguides\u002Fdeployment","docs\u002Fiam\u002F03.guides\u002Fdeployment",{"title":506,"path":507,"stem":508},"Operation Scripts","\u002Fdocs\u002Fiam\u002Fguides\u002Foperation-scripts","docs\u002Fiam\u002F03.guides\u002Foperation-scripts",{"title":510,"path":511,"stem":512},"Role-Based Access Control","\u002Fdocs\u002Fiam\u002Fguides\u002Frbac","docs\u002Fiam\u002F03.guides\u002Frbac",{"title":204,"path":514,"stem":515},"\u002Fdocs\u002Fiam\u002Fconfiguration","docs\u002Fiam\u002F04.configuration",{"title":517,"path":518,"stem":519,"children":520,"page":53},"Api","\u002Fdocs\u002Fiam\u002Fapi","docs\u002Fiam\u002F05.API",[521,524,527],{"title":208,"path":522,"stem":523},"\u002Fdocs\u002Fiam\u002Fapi\u002Fapi","docs\u002Fiam\u002F05.API\u002F00.api",{"title":218,"path":525,"stem":526},"\u002Fdocs\u002Fiam\u002Fapi\u002Fmiddlewares","docs\u002Fiam\u002F05.API\u002F02.middlewares",{"title":214,"path":528,"stem":529},"\u002Fdocs\u002Fiam\u002Fapi\u002Froutes","docs\u002Fiam\u002F05.API\u002F03.routes",{"title":40,"path":42,"stem":531,"children":532},"docs\u002Fshield-base\u002Findex",[533,534,537,541,582,586,590,594],{"title":40,"path":42,"stem":531},{"title":14,"path":535,"stem":536},"\u002Fdocs\u002Fshield-base\u002Fgetting-started","docs\u002Fshield-base\u002F00.getting-started",{"title":538,"path":539,"stem":540},"CLI Reference","\u002Fdocs\u002Fshield-base\u002Fcli","docs\u002Fshield-base\u002F01.cli",{"title":242,"path":542,"stem":543,"children":544},"\u002Fdocs\u002Fshield-base\u002Fdata-sources","docs\u002Fshield-base\u002F02.data-sources\u002Findex",[545,546,550,554,558,562,566,570,574,578],{"title":242,"path":542,"stem":543},{"title":547,"path":548,"stem":549},"BGP \u002F ASN","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fbgp","docs\u002Fshield-base\u002F02.data-sources\u002Fbgp",{"title":551,"path":552,"stem":553},"City Geolocation","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fcity","docs\u002Fshield-base\u002F02.data-sources\u002Fcity",{"title":555,"path":556,"stem":557},"Country Geolocation","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fcountry","docs\u002Fshield-base\u002F02.data-sources\u002Fcountry",{"title":559,"path":560,"stem":561},"Verified Crawlers","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fcrawlers","docs\u002Fshield-base\u002F02.data-sources\u002Fcrawlers",{"title":563,"path":564,"stem":565},"Disposable Emails","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Femail","docs\u002Fshield-base\u002F02.data-sources\u002Femail",{"title":567,"path":568,"stem":569},"FireHOL Threat Intelligence","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Ffirehol","docs\u002Fshield-base\u002F02.data-sources\u002Ffirehol",{"title":571,"path":572,"stem":573},"Proxy Detection","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fproxy","docs\u002Fshield-base\u002F02.data-sources\u002Fproxy",{"title":575,"path":576,"stem":577},"Tor Nodes","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Ftor","docs\u002Fshield-base\u002F02.data-sources\u002Ftor",{"title":579,"path":580,"stem":581},"Suspicious User-Agents","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fuseragent","docs\u002Fshield-base\u002F02.data-sources\u002Fuseragent",{"title":583,"path":584,"stem":585},"Programmatic Usage","\u002Fdocs\u002Fshield-base\u002Fusage","docs\u002Fshield-base\u002F03.usage",{"title":587,"path":588,"stem":589},"Custom Data Sources","\u002Fdocs\u002Fshield-base\u002Fcustom-data-sources","docs\u002Fshield-base\u002F04.custom-data-sources",{"title":591,"path":592,"stem":593},"TypeScript Types","\u002Fdocs\u002Fshield-base\u002Ftypes","docs\u002Fshield-base\u002F05.types",{"title":208,"path":595,"stem":596},"\u002Fdocs\u002Fshield-base\u002Fapi","docs\u002Fshield-base\u002F06.api",{"title":226,"path":48,"stem":598,"children":599},"docs\u002Futils\u002Findex",[600,601,618,651,748],{"title":226,"path":48,"stem":598},{"title":602,"path":603,"stem":604,"children":605,"page":53},"Eslint","\u002Fdocs\u002Futils\u002Feslint","docs\u002Futils\u002Feslint",[606,610,614],{"title":607,"path":608,"stem":609},"React Config","\u002Fdocs\u002Futils\u002Feslint\u002Freact","docs\u002Futils\u002Feslint\u002Freact",{"title":611,"path":612,"stem":613},"TypeScript Config","\u002Fdocs\u002Futils\u002Feslint\u002Ftypescript","docs\u002Futils\u002Feslint\u002Ftypescript",{"title":615,"path":616,"stem":617},"Vue Config","\u002Fdocs\u002Futils\u002Feslint\u002Fvue","docs\u002Futils\u002Feslint\u002Fvue",{"title":619,"path":620,"stem":621,"children":622,"page":53},"Server","\u002Fdocs\u002Futils\u002Fserver","docs\u002Futils\u002Fserver",[623,627,631,635,639,643,647],{"title":624,"path":625,"stem":626},"Encryption","\u002Fdocs\u002Futils\u002Fserver\u002Fencryption","docs\u002Futils\u002Fserver\u002Fencryption",{"title":628,"path":629,"stem":630},"Path Resolver","\u002Fdocs\u002Futils\u002Fserver\u002Fpathresolver","docs\u002Futils\u002Fserver\u002FpathResolver",{"title":632,"path":633,"stem":634},"File Replacements","\u002Fdocs\u002Futils\u002Fserver\u002Freplace","docs\u002Futils\u002Fserver\u002Freplace",{"title":636,"path":637,"stem":638},"run","\u002Fdocs\u002Futils\u002Fserver\u002Frun","docs\u002Futils\u002Fserver\u002Frun",{"title":640,"path":641,"stem":642},"scheduleTask","\u002Fdocs\u002Futils\u002Fserver\u002Fscheduletask","docs\u002Futils\u002Fserver\u002FscheduleTask",{"title":644,"path":645,"stem":646},"spawnRun","\u002Fdocs\u002Futils\u002Fserver\u002Fspawnrun","docs\u002Futils\u002Fserver\u002FspawnRun",{"title":648,"path":649,"stem":650},"uploadCsv","\u002Fdocs\u002Futils\u002Fserver\u002Fuploadcsv","docs\u002Futils\u002Fserver\u002FuploadCsv",{"title":652,"path":653,"stem":654,"children":655,"page":53},"Shared","\u002Fdocs\u002Futils\u002Fshared","docs\u002Futils\u002Fshared",[656,660,664,668,672,676,680,684,688,692,696,700,704,708,712,716,720,724,728,732,736,740,744],{"title":657,"path":658,"stem":659},"BatchQueue","\u002Fdocs\u002Futils\u002Fshared\u002Fbatchqueue","docs\u002Futils\u002Fshared\u002FbatchQueue",{"title":661,"path":662,"stem":663},"capitalize","\u002Fdocs\u002Futils\u002Fshared\u002Fcapitalize","docs\u002Futils\u002Fshared\u002Fcapitalize",{"title":665,"path":666,"stem":667},"chunkProcess","\u002Fdocs\u002Futils\u002Fshared\u002Fchunkprocess","docs\u002Futils\u002Fshared\u002FchunkProcess",{"title":669,"path":670,"stem":671},"cleanObject","\u002Fdocs\u002Futils\u002Fshared\u002Fcleanobject","docs\u002Futils\u002Fshared\u002FcleanObject",{"title":673,"path":674,"stem":675},"createConfigManager","\u002Fdocs\u002Futils\u002Fshared\u002Fconfigurationdefiner","docs\u002Futils\u002Fshared\u002FconfigurationDefiner",{"title":677,"path":678,"stem":679},"debounce","\u002Fdocs\u002Futils\u002Fshared\u002Fdebounce","docs\u002Futils\u002Fshared\u002Fdebounce",{"title":681,"path":682,"stem":683},"ensureArray","\u002Fdocs\u002Futils\u002Fshared\u002Fensurearray","docs\u002Futils\u002Fshared\u002FensureArray",{"title":685,"path":686,"stem":687},"fetchWithRetry","\u002Fdocs\u002Futils\u002Fshared\u002Ffetchwithretry","docs\u002Futils\u002Fshared\u002FfetchWithRetry",{"title":689,"path":690,"stem":691},"filterEmptyValues","\u002Fdocs\u002Futils\u002Fshared\u002Ffilteremptyvalues","docs\u002Futils\u002Fshared\u002FfilterEmptyValues",{"title":693,"path":694,"stem":695},"findStringsInObject","\u002Fdocs\u002Futils\u002Fshared\u002Ffindobjectvalues","docs\u002Futils\u002Fshared\u002FfindObjectValues",{"title":697,"path":698,"stem":699},"fisherYatesShuffle","\u002Fdocs\u002Futils\u002Fshared\u002Ffisheryatesshuffle","docs\u002Futils\u002Fshared\u002FfisherYatesShuffle",{"title":701,"path":702,"stem":703},"getRandomImage","\u002Fdocs\u002Futils\u002Fshared\u002Fgetrandomimage","docs\u002Futils\u002Fshared\u002FgetRandomImage",{"title":705,"path":706,"stem":707},"isObjectHasValues","\u002Fdocs\u002Futils\u002Fshared\u002Fisobjecthasvalues","docs\u002Futils\u002Fshared\u002FisObjectHasValues",{"title":709,"path":710,"stem":711},"isAsyncOrPromise","\u002Fdocs\u002Futils\u002Fshared\u002Fispromise","docs\u002Futils\u002Fshared\u002FisPromise",{"title":713,"path":714,"stem":715},"MiniCache","\u002Fdocs\u002Futils\u002Fshared\u002Fminicache","docs\u002Futils\u002Fshared\u002FminiCache",{"title":717,"path":718,"stem":719},"parseCookies","\u002Fdocs\u002Futils\u002Fshared\u002Fparserawcookies","docs\u002Futils\u002Fshared\u002FparseRawCookies",{"title":721,"path":722,"stem":723},"safeAction","\u002Fdocs\u002Futils\u002Fshared\u002Fpromiselocker","docs\u002Futils\u002Fshared\u002FpromiseLocker",{"title":725,"path":726,"stem":727},"Random","\u002Fdocs\u002Futils\u002Fshared\u002Frandom","docs\u002Futils\u002Fshared\u002Frandom",{"title":729,"path":730,"stem":731},"range","\u002Fdocs\u002Futils\u002Fshared\u002Frange","docs\u002Futils\u002Fshared\u002Frange",{"title":733,"path":734,"stem":735},"rateLimiters","\u002Fdocs\u002Futils\u002Fshared\u002Fratelimiters","docs\u002Futils\u002Fshared\u002FrateLimiters",{"title":737,"path":738,"stem":739},"safeObjectMerge","\u002Fdocs\u002Futils\u002Fshared\u002Fsafemerge","docs\u002Futils\u002Fshared\u002FsafeMerge",{"title":741,"path":742,"stem":743},"textTruncation","\u002Fdocs\u002Futils\u002Fshared\u002Ftexttruncation","docs\u002Futils\u002Fshared\u002FtextTruncation",{"title":745,"path":746,"stem":747},"validateZodSchema","\u002Fdocs\u002Futils\u002Fshared\u002Fvalidatezodschema","docs\u002Futils\u002Fshared\u002FvalidateZodSchema",{"title":749,"path":750,"stem":751,"children":752},"Utility Types","\u002Fdocs\u002Futils\u002Ftypes","docs\u002Futils\u002Ftypes\u002Findex",[753,754,758,762,766,770,774,778,782,786],{"title":749,"path":750,"stem":751},{"title":755,"path":756,"stem":757},"Brand","\u002Fdocs\u002Futils\u002Ftypes\u002Fbrand","docs\u002Futils\u002Ftypes\u002FBrand",{"title":759,"path":760,"stem":761},"DeepPartial","\u002Fdocs\u002Futils\u002Ftypes\u002Fdeeppartial","docs\u002Futils\u002Ftypes\u002FDeepPartial",{"title":763,"path":764,"stem":765},"Merge","\u002Fdocs\u002Futils\u002Ftypes\u002Fmerge","docs\u002Futils\u002Ftypes\u002FMerge",{"title":767,"path":768,"stem":769},"NonNullable","\u002Fdocs\u002Futils\u002Ftypes\u002Fnonnullable","docs\u002Futils\u002Ftypes\u002FNonNullable",{"title":771,"path":772,"stem":773},"Prettify","\u002Fdocs\u002Futils\u002Ftypes\u002Fprettify","docs\u002Futils\u002Ftypes\u002FPrettify",{"title":775,"path":776,"stem":777},"PromiseType","\u002Fdocs\u002Futils\u002Ftypes\u002Fpromisetype","docs\u002Futils\u002Ftypes\u002FPromiseType",{"title":779,"path":780,"stem":781},"RequireKeys","\u002Fdocs\u002Futils\u002Ftypes\u002Frequirekeys","docs\u002Futils\u002Ftypes\u002FRequireKeys",{"title":783,"path":784,"stem":785},"StandardResponse","\u002Fdocs\u002Futils\u002Ftypes\u002Fstandardresponse","docs\u002Futils\u002Ftypes\u002FStandardResponse",{"title":787,"path":788,"stem":789},"ValueOf","\u002Fdocs\u002Futils\u002Ftypes\u002Fvalueof","docs\u002Futils\u002Ftypes\u002FValueOf",{"id":4,"extension":5,"links":791,"meta":802,"stem":62,"__hash__":63},[792,800,801],{"nested":8,"label":9,"icon":10,"to":11,"children":793},[794,795,796,797,798,799],{"label":14,"icon":15,"to":11,"description":16,"github":17,"badge":18},{"label":20,"icon":21,"to":22,"description":23,"github":24,"badge":25},{"label":27,"icon":28,"to":29,"description":30,"github":31,"badge":25},{"label":33,"icon":34,"to":35,"description":36,"github":37,"badge":38},{"label":40,"icon":41,"to":42,"description":43,"github":44,"badge":38},{"label":46,"icon":47,"to":48,"description":49,"github":50,"badge":51},{"nested":53,"label":54,"icon":55,"to":56},{"nested":53,"label":58,"icon":59,"to":60},{},{"landing":804,"posts":820},{"id":805,"title":806,"body":807,"description":814,"extension":815,"meta":816,"navigation":8,"path":56,"seo":817,"stem":818,"__hash__":819},"blog_landing\u002Fblog\u002Findex.md","Insights & Ecosystem Updates",{"type":808,"value":809,"toc":810},"minimark",[],{"title":811,"searchDepth":812,"depth":812,"links":813},"",2,[],"Deep dives into security, authentication, and the latest developments in the ecosystem.","md",{},{"title":806,"description":814},"blog\u002Findex","9-Jq3eGMsVMEsFYD_S_D41AlY8sPyPuSYgdZ5OgqIws",[821,3813,4939],{"id":822,"title":69,"author":823,"authorGithub":824,"authorGithubUserName":825,"authorImg":826,"body":827,"date":3802,"description":3803,"extension":815,"featured":53,"icon":3804,"image":3805,"meta":3806,"navigation":8,"path":70,"rawbody":3807,"readingTime":3808,"seo":3809,"stem":71,"tags":3810,"__hash__":3812},"blog\u002Fblog\u002Fiam-api-tokens-auth-h3client.md","Sergio","https:\u002F\u002Fgithub.com\u002FSergo706","Sergo706","https:\u002F\u002Fgithub.com\u002FSergo706.png",{"type":808,"value":828,"toc":3789},[829,833,836,851,854,859,862,1026,1054,1075,1077,1081,1088,1098,1105,1126,1129,1131,1135,1145,1164,1167,1170,1222,1228,1460,1462,1466,1479,1482,1485,1666,1669,1685,1687,1691,1704,1710,1724,1726,1730,1740,1743,1835,1838,1841,2111,2114,2594,2607,2609,2615,2631,2637,2882,2885,2917,2920,2922,2926,2938,2944,3326,3338,3344,3638,3649,3652,3731,3733,3737,3743,3754,3757,3759,3763,3766,3772,3778,3781,3783,3785],[830,831,832],"p",{},"Machine-to-machine authentication looks simple until you need to answer real\nquestions. How do you scope access, revoke a leaked key, rotate credentials,\nkeep raw secrets out of the database, and still give users a clean dashboard\nfor managing their integrations?",[830,834,835],{},"The IAM service solves that problem with a dedicated API token subsystem.\nPublic verification and authenticated management are two distinct surfaces with\ndifferent security requirements. The database never stores a raw secret. Only\nthe SHA-256 digest of each key is persisted. Privilege labels and optional IP restrictions\nlive at the row level, and the full model maps cleanly into Auth H3 Client for\nNuxt, Nitro, and plain H3 applications.",[837,838,839],"important",{},[830,840,841,842,846,847,850],{},"This article assumes you already have a running IAM service and a Nuxt or Nitro\napp configured with ",[843,844,845],"code",{},"@riavzon\u002Fauth-h3client",". If your app exposes\nmachine-to-machine routes protected with ",[843,848,849],{},"defineAuthenticatePublicApi",", do not\nplace those routes behind the bundled Nuxt auth middleware. That middleware is\nfor browser session flows, while API-key routes need a path bypass.",[852,853],"hr",{},[855,856,858],"h2",{"id":857},"what-the-subsystem-exposes","What the subsystem exposes",[830,860,861],{},"The API token subsystem has two distinct surfaces. Public verification is the\nmachine-to-machine entry point. Management routes are session-authenticated\nbrowser flows for creating, listing, rotating, revoking, and updating tokens.",[863,864,865,884],"table",{},[866,867,868],"thead",{},[869,870,871,875,878,881],"tr",{},[872,873,874],"th",{},"Surface",[872,876,877],{},"Method",[872,879,880],{},"Route",[872,882,883],{},"Purpose",[885,886,887,909,927,944,960,976,992,1009],"tbody",{},[869,888,889,893,898,903],{},[890,891,892],"td",{},"Public verification",[890,894,895],{},[843,896,897],{},"GET",[890,899,900],{},[843,901,902],{},"\u002Fapi\u002Fpublic\u002Fverify",[890,904,905,906],{},"Verify a raw API token from ",[843,907,908],{},"X-API-KEY",[869,910,911,914,919,924],{},[890,912,913],{},"Token creation",[890,915,916],{},[843,917,918],{},"POST",[890,920,921],{},[843,922,923],{},"\u002Fapi\u002Fmanage\u002Fnew-token",[890,925,926],{},"Create a new API token",[869,928,929,932,936,941],{},[890,930,931],{},"Token inventory",[890,933,934],{},[843,935,897],{},[890,937,938],{},[843,939,940],{},"\u002Fapi\u002Fmanage\u002Flist-metadata",[890,942,943],{},"List the current user's valid tokens",[869,945,946,948,952,957],{},[890,947,469],{},[890,949,950],{},[843,951,918],{},[890,953,954],{},[843,955,956],{},"\u002Fapi\u002Fmanage\u002Frevoke",[890,958,959],{},"Invalidate a token",[869,961,962,964,968,973],{},[890,963,481],{},[890,965,966],{},[843,967,918],{},[890,969,970],{},[843,971,972],{},"\u002Fapi\u002Fmanage\u002Fmetadata",[890,974,975],{},"Return details for one token",[869,977,978,980,984,989],{},[890,979,473],{},[890,981,982],{},[843,983,918],{},[890,985,986],{},[843,987,988],{},"\u002Fapi\u002Fmanage\u002Frotate",[890,990,991],{},"Replace a token with a fresh raw key",[869,993,994,997,1001,1006],{},[890,995,996],{},"IP updates",[890,998,999],{},[843,1000,918],{},[890,1002,1003],{},[843,1004,1005],{},"\u002Fapi\u002Fmanage\u002Fip-restriction-update",[890,1007,1008],{},"Change the stored IP allowlist",[869,1010,1011,1014,1018,1023],{},[890,1012,1013],{},"Privilege updates",[890,1015,1016],{},[843,1017,918],{},[890,1019,1020],{},[843,1021,1022],{},"\u002Fapi\u002Fmanage\u002Fprivilege-update",[890,1024,1025],{},"Change the stored privilege label",[830,1027,1028,1029,1032,1033,1036,1037,1032,1040,1043,1044,1047,1048,1050,1051,1053],{},"Every token is scoped with one privilege label: ",[843,1030,1031],{},"custom",", ",[843,1034,1035],{},"demo",",\n",[843,1038,1039],{},"restricted",[843,1041,1042],{},"protected",", or ",[843,1045,1046],{},"full",". The verification route checks the exact\nlabel you request, not a hierarchy. If your route requires ",[843,1049,1035],{},", the token\nmust carry ",[843,1052,1035],{},".",[830,1055,1056,1057,1059,1060,1062,1063,1065,1066,1068,1069,1071,1072,1074],{},"The five levels have no built-in ordering. ",[843,1058,1035],{}," does not imply access to\n",[843,1061,1039],{}," routes, and ",[843,1064,1039],{}," does not include ",[843,1067,1042],{},". Each token\ncarries exactly one label and that label is matched literally. ",[843,1070,1031],{}," is a\ncatch-all for use cases that do not map to the four named levels. You decide\nwhat ",[843,1073,1031],{}," means in your own application.",[852,1076],{},[855,1078,1080],{"id":1079},"how-a-token-is-shaped-and-stored","How a token is shaped and stored",[830,1082,1083,1084,1087],{},"The raw token format is simple on the surface. Each key is created as\n",[843,1085,1086],{},"prefix_random_checksum",", where the checksum is the first eight hexadecimal\ncharacters of a SHA-256 digest of the random portion.",[1089,1090,1096],"pre",{"className":1091,"code":1093,"filename":1094,"language":1095,"meta":811},[1092],"language-text","rpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80\n","API token format","text",[843,1097,1093],{"__ignoreMap":811},[830,1099,1100,1101,1104],{},"That checksum lets the IAM service reject malformed keys quickly before doing a\ndatabase lookup. The raw key is only returned once, at creation or rotation\ntime. After that, the database stores only the SHA-256 digest in the\n",[843,1102,1103],{},"api_tokens.api_token"," column.",[830,1106,1107,1108,1111,1112,1032,1115,1118,1119,1122,1123,1125],{},"The subsystem also creates a separate ",[843,1109,1110],{},"public_identifier",". This value is not a\ncredential. It exists so management actions can target the correct row without\nrelying on the raw token after it has been issued. In the direct IAM API, most\nmanagement actions require ",[843,1113,1114],{},"tokenId",[843,1116,1117],{},"publicIdentifier",", and ",[843,1120,1121],{},"name"," together.\nAuth H3 Client deliberately hides ",[843,1124,1117],{}," from browser code and\nresolves it on the server.",[830,1127,1128],{},"The stored row carries operational metadata too. The IAM service records the\ntoken owner, privilege label, creation time, expiration time, last-use time,\nusage count, validity flag, and optional IP restriction list. That gives you a\nreal credential inventory rather than an opaque secret store.",[852,1130],{},[855,1132,1134],{"id":1133},"how-verification-works","How verification works",[830,1136,1137,1138,1141,1142,1144],{},"The public verification route is ",[843,1139,1140],{},"GET \u002Fapi\u002Fpublic\u002Fverify",". It reads the raw\ntoken from the ",[843,1143,908],{}," header, reads the required privilege from the query\nstring, and validates the request IP so IP-restricted tokens can be enforced.",[830,1146,1147,1148,1151,1152,1155,1156,1159,1160,1163],{},"Internally, verification follows a strict sequence. The IAM service validates\nthe checksum, hashes the raw key, looks up the hashed row where ",[843,1149,1150],{},"valid = 1",",\nchecks the exact ",[843,1153,1154],{},"privilege_type",", applies expiration rules, enforces any\nstored IP restrictions, and updates ",[843,1157,1158],{},"usage_count"," and ",[843,1161,1162],{},"last_used"," for\nsuccessful requests.",[830,1165,1166],{},"Failed verification attempts are throttled aggressively. Any request with a\nmissing key, a malformed privilege value, an unresolvable IP address, or an\ninvalid token feeds directly into the IAM verification limiters. The limiter\ncounts against the caller's IP, so repeated failures eventually trigger a\npermanent ban at the gateway level. Successful requests can also be\nrate-limited if you enable consumption limiting on the IAM side.",[830,1168,1169],{},"Call the route directly like this:",[1089,1171,1176],{"className":1172,"code":1173,"filename":1174,"language":1175,"meta":811,"style":811},"language-bash shiki shiki-themes light-plus light-plus dracula","curl \\\n  -H \"X-API-KEY: rpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80\" \\\n  \"http:\u002F\u002Flocalhost:10000\u002Fapi\u002Fpublic\u002Fverify?privilege=demo\"\n","Terminal","bash",[843,1177,1178,1191,1210],{"__ignoreMap":811},[1179,1180,1183,1187],"span",{"class":1181,"line":1182},"line",1,[1179,1184,1186],{"class":1185},"sHOzp","curl",[1179,1188,1190],{"class":1189},"st6lo"," \\\n",[1179,1192,1193,1197,1201,1205,1208],{"class":1181,"line":812},[1179,1194,1196],{"class":1195},"sjR7W","  -H",[1179,1198,1200],{"class":1199},"sFkSl"," \"",[1179,1202,1204],{"class":1203},"sFB1V","X-API-KEY: rpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80",[1179,1206,1207],{"class":1199},"\"",[1179,1209,1190],{"class":1189},[1179,1211,1213,1216,1219],{"class":1181,"line":1212},3,[1179,1214,1215],{"class":1199},"  \"",[1179,1217,1218],{"class":1203},"http:\u002F\u002Flocalhost:10000\u002Fapi\u002Fpublic\u002Fverify?privilege=demo",[1179,1220,1221],{"class":1199},"\"\n",[830,1223,1224,1225,1053],{},"On success, the IAM service returns a compact metadata object. This is the same\nshape Auth H3 Client later exposes on ",[843,1226,1227],{},"event.context.apiVerification",[1089,1229,1233],{"className":1230,"code":1231,"filename":1232,"language":5,"meta":811,"style":811},"language-json shiki shiki-themes light-plus light-plus dracula","{\n  \"ok\": true,\n  \"date\": \"2026-05-01T10:00:00.000Z\",\n  \"data\": {\n    \"name\": \"report-worker\",\n    \"tokenId\": 12,\n    \"userId\": 42,\n    \"createdAt\": \"2026-05-01T09:00:00.000Z\",\n    \"expiresAt\": \"2026-06-01T09:00:00.000Z\",\n    \"lastUsed\": \"2026-05-01T10:00:00.000Z\",\n    \"usageCount\": 8,\n    \"providedPrivilege\": \"demo\"\n  }\n}\n","Verification response",[843,1234,1235,1241,1261,1281,1296,1317,1334,1351,1372,1393,1413,1430,1448,1454],{"__ignoreMap":811},[1179,1236,1237],{"class":1181,"line":1182},[1179,1238,1240],{"class":1239},"sDd4n","{\n",[1179,1242,1243,1246,1250,1252,1256,1259],{"class":1181,"line":812},[1179,1244,1215],{"class":1245},"saJyd",[1179,1247,1249],{"class":1248},"s_W10","ok",[1179,1251,1207],{"class":1245},[1179,1253,1255],{"class":1254},"saOXh",":",[1179,1257,1258],{"class":1195}," true",[1179,1260,1036],{"class":1239},[1179,1262,1263,1265,1268,1270,1272,1274,1277,1279],{"class":1181,"line":1212},[1179,1264,1215],{"class":1245},[1179,1266,1267],{"class":1248},"date",[1179,1269,1207],{"class":1245},[1179,1271,1255],{"class":1254},[1179,1273,1200],{"class":1199},[1179,1275,1276],{"class":1203},"2026-05-01T10:00:00.000Z",[1179,1278,1207],{"class":1199},[1179,1280,1036],{"class":1239},[1179,1282,1284,1286,1289,1291,1293],{"class":1181,"line":1283},4,[1179,1285,1215],{"class":1245},[1179,1287,1288],{"class":1248},"data",[1179,1290,1207],{"class":1245},[1179,1292,1255],{"class":1254},[1179,1294,1295],{"class":1239}," {\n",[1179,1297,1299,1302,1304,1306,1308,1310,1313,1315],{"class":1181,"line":1298},5,[1179,1300,1301],{"class":1245},"    \"",[1179,1303,1121],{"class":1248},[1179,1305,1207],{"class":1245},[1179,1307,1255],{"class":1254},[1179,1309,1200],{"class":1199},[1179,1311,1312],{"class":1203},"report-worker",[1179,1314,1207],{"class":1199},[1179,1316,1036],{"class":1239},[1179,1318,1320,1322,1324,1326,1328,1332],{"class":1181,"line":1319},6,[1179,1321,1301],{"class":1245},[1179,1323,1114],{"class":1248},[1179,1325,1207],{"class":1245},[1179,1327,1255],{"class":1254},[1179,1329,1331],{"class":1330},"spgvN"," 12",[1179,1333,1036],{"class":1239},[1179,1335,1337,1339,1342,1344,1346,1349],{"class":1181,"line":1336},7,[1179,1338,1301],{"class":1245},[1179,1340,1341],{"class":1248},"userId",[1179,1343,1207],{"class":1245},[1179,1345,1255],{"class":1254},[1179,1347,1348],{"class":1330}," 42",[1179,1350,1036],{"class":1239},[1179,1352,1354,1356,1359,1361,1363,1365,1368,1370],{"class":1181,"line":1353},8,[1179,1355,1301],{"class":1245},[1179,1357,1358],{"class":1248},"createdAt",[1179,1360,1207],{"class":1245},[1179,1362,1255],{"class":1254},[1179,1364,1200],{"class":1199},[1179,1366,1367],{"class":1203},"2026-05-01T09:00:00.000Z",[1179,1369,1207],{"class":1199},[1179,1371,1036],{"class":1239},[1179,1373,1375,1377,1380,1382,1384,1386,1389,1391],{"class":1181,"line":1374},9,[1179,1376,1301],{"class":1245},[1179,1378,1379],{"class":1248},"expiresAt",[1179,1381,1207],{"class":1245},[1179,1383,1255],{"class":1254},[1179,1385,1200],{"class":1199},[1179,1387,1388],{"class":1203},"2026-06-01T09:00:00.000Z",[1179,1390,1207],{"class":1199},[1179,1392,1036],{"class":1239},[1179,1394,1396,1398,1401,1403,1405,1407,1409,1411],{"class":1181,"line":1395},10,[1179,1397,1301],{"class":1245},[1179,1399,1400],{"class":1248},"lastUsed",[1179,1402,1207],{"class":1245},[1179,1404,1255],{"class":1254},[1179,1406,1200],{"class":1199},[1179,1408,1276],{"class":1203},[1179,1410,1207],{"class":1199},[1179,1412,1036],{"class":1239},[1179,1414,1416,1418,1421,1423,1425,1428],{"class":1181,"line":1415},11,[1179,1417,1301],{"class":1245},[1179,1419,1420],{"class":1248},"usageCount",[1179,1422,1207],{"class":1245},[1179,1424,1255],{"class":1254},[1179,1426,1427],{"class":1330}," 8",[1179,1429,1036],{"class":1239},[1179,1431,1433,1435,1438,1440,1442,1444,1446],{"class":1181,"line":1432},12,[1179,1434,1301],{"class":1245},[1179,1436,1437],{"class":1248},"providedPrivilege",[1179,1439,1207],{"class":1245},[1179,1441,1255],{"class":1254},[1179,1443,1200],{"class":1199},[1179,1445,1035],{"class":1203},[1179,1447,1221],{"class":1199},[1179,1449,1451],{"class":1181,"line":1450},13,[1179,1452,1453],{"class":1239},"  }\n",[1179,1455,1457],{"class":1181,"line":1456},14,[1179,1458,1459],{"class":1239},"}\n",[852,1461],{},[855,1463,1465],{"id":1464},"how-management-works","How management works",[830,1467,1468,1469,1032,1472,1475,1476,1478],{},"Management routes are intentionally more demanding than public verification.\nThey sit behind ",[843,1470,1471],{},"requireAccessToken",[843,1473,1474],{},"requireRefreshToken",", fingerprint\ncollection, active MFA checks, JWT protection, and for ",[843,1477,918],{}," routes a JSON\ncontent-type check plus a 1 KB body limit.",[830,1480,1481],{},"That split is the right model for a real product. Verification is for services\ncalling your APIs. Management is for logged-in users who are creating and\nchanging credentials inside a dashboard.",[830,1483,1484],{},"Here is the direct IAM management map:",[863,1486,1487,1502],{},[866,1488,1489],{},[869,1490,1491,1494,1496,1499],{},[872,1492,1493],{},"Action",[872,1495,877],{},[872,1497,1498],{},"Input",[872,1500,1501],{},"Result",[885,1503,1504,1534,1551,1573,1595,1617,1641],{},[869,1505,1506,1511,1515,1531],{},[890,1507,1508],{},[843,1509,1510],{},"new-token",[890,1512,1513],{},[843,1514,918],{},[890,1516,1517,1032,1519,1032,1522,1032,1525,1032,1528],{},[843,1518,1121],{},[843,1520,1521],{},"prefix",[843,1523,1524],{},"expires?",[843,1526,1527],{},"ipv4?",[843,1529,1530],{},"privilege",[890,1532,1533],{},"New raw token and public identifier",[869,1535,1536,1541,1545,1548],{},[890,1537,1538],{},[843,1539,1540],{},"list-metadata",[890,1542,1543],{},[843,1544,897],{},[890,1546,1547],{},"None",[890,1549,1550],{},"All valid tokens for the authenticated user",[869,1552,1553,1558,1562,1570],{},[890,1554,1555],{},[843,1556,1557],{},"revoke",[890,1559,1560],{},[843,1561,918],{},[890,1563,1564,1032,1566,1032,1568],{},[843,1565,1114],{},[843,1567,1117],{},[843,1569,1121],{},[890,1571,1572],{},"Invalidates the token",[869,1574,1575,1580,1584,1592],{},[890,1576,1577],{},[843,1578,1579],{},"metadata",[890,1581,1582],{},[843,1583,918],{},[890,1585,1586,1032,1588,1032,1590],{},[843,1587,1114],{},[843,1589,1117],{},[843,1591,1121],{},[890,1593,1594],{},"Returns one token plus total counts",[869,1596,1597,1602,1606,1614],{},[890,1598,1599],{},[843,1600,1601],{},"rotate",[890,1603,1604],{},[843,1605,918],{},[890,1607,1608,1032,1610,1032,1612],{},[843,1609,1114],{},[843,1611,1117],{},[843,1613,1121],{},[890,1615,1616],{},"Returns a replacement raw token",[869,1618,1619,1624,1628,1638],{},[890,1620,1621],{},[843,1622,1623],{},"ip-restriction-update",[890,1625,1626],{},[843,1627,918],{},[890,1629,1630,1032,1632,1032,1634,1032,1636],{},[843,1631,1114],{},[843,1633,1117],{},[843,1635,1121],{},[843,1637,1527],{},[890,1639,1640],{},"Replaces the stored IP allowlist",[869,1642,1643,1648,1652,1663],{},[890,1644,1645],{},[843,1646,1647],{},"privilege-update",[890,1649,1650],{},[843,1651,918],{},[890,1653,1654,1032,1656,1032,1658,1032,1660],{},[843,1655,1114],{},[843,1657,1117],{},[843,1659,1121],{},[843,1661,1662],{},"newPrivilege",[890,1664,1665],{},"Replaces the stored privilege label",[830,1667,1668],{},"Rotation deserves special attention. The IAM service revokes the current token\nand creates a fresh raw token in one management flow. That means the caller can\nroll credentials forward without deleting the integration entirely. Creation and\nrotation are the only two moments when the raw secret leaves the server.",[1670,1671,1672],"note",{},[830,1673,1674,1675,1159,1678,1681,1682,1684],{},"The direct IAM create route returns both ",[843,1676,1677],{},"rawApiKey",[843,1679,1680],{},"rawPublicId",". Auth H3\nClient strips ",[843,1683,1680],{}," before exposing the creation result to your Nuxt\nhandler. That keeps management identity in the server layer instead of the\nbrowser.",[852,1686],{},[855,1688,1690],{"id":1689},"why-auth-h3-client-fits-this-subsystem-well","Why Auth H3 Client fits this subsystem well",[830,1692,1693,1694,1696,1697,1700,1701,1053],{},"Auth H3 Client does more than proxy requests. It gives the IAM token model the\nright shape for H3 and Nuxt applications. Public machine-to-machine routes use\n",[843,1695,849],{},". Authenticated dashboard routes use\n",[843,1698,1699],{},"defineApiManagementHandler",". Token inventory reads can use\n",[843,1702,1703],{},"getApiListsController",[830,1705,1706,1707,1709],{},"That split matters because the browser and service callers have different\nsecurity needs. Browser management routes need session auth, CSRF protection,\nand token identity mapping. Machine-to-machine verification needs a single\n",[843,1708,908],{}," header and a clean path to the IAM verification endpoint.",[830,1711,1712,1713,1715,1716,1159,1718,1720,1721,1723],{},"The wrappers also hide low-level IAM details from your application code. Your\nNuxt route only deals with ",[843,1714,1114],{}," for existing-token actions, while the\nserver wrapper resolves ",[843,1717,1117],{},[843,1719,1121],{}," through IAM\n",[843,1722,940],{}," before making the final management request.",[852,1725],{},[855,1727,1729],{"id":1728},"integrating-with-the-nuxt-module","Integrating with the Nuxt module",[830,1731,1732,1733,1736,1737,1739],{},"If your application only uses browser auth flows, the Nuxt module can run with\n",[843,1734,1735],{},"enableMiddleware: true"," and register the built-in middleware for every\nrequest. Mixed apps need a different setup. If you protect custom APIs with\n",[843,1738,849],{},", disable the bundled middleware and add your own\npath-aware middleware so browser auth routes still get bot detection and CSRF,\nwhile machine-to-machine routes bypass that chain.",[830,1741,1742],{},"Start by registering the module and disabling the bundled middleware.",[1089,1744,1749],{"className":1745,"code":1746,"filename":1747,"language":1748,"meta":811,"style":811},"language-ts shiki shiki-themes light-plus light-plus dracula","export default defineNuxtConfig({\n  modules: ['auth-h3client\u002Fmodule'],\n  authH3Client: {\n    enableMiddleware: false,\n    authStatusUrl: '\u002Fapi\u002Fauth\u002Fusers\u002FauthStatus'\n  }\n})\n","nuxt.config.ts","ts",[843,1750,1751,1766,1789,1798,1810,1826,1830],{"__ignoreMap":811},[1179,1752,1753,1757,1760,1763],{"class":1181,"line":1182},[1179,1754,1756],{"class":1755},"sZ328","export",[1179,1758,1759],{"class":1755}," default",[1179,1761,1762],{"class":1185}," defineNuxtConfig",[1179,1764,1765],{"class":1239},"({\n",[1179,1767,1768,1772,1775,1778,1781,1784,1786],{"class":1181,"line":812},[1179,1769,1771],{"class":1770},"sjsA6","  modules",[1179,1773,1255],{"class":1774},"s34zl",[1179,1776,1777],{"class":1239}," [",[1179,1779,1780],{"class":1199},"'",[1179,1782,1783],{"class":1203},"auth-h3client\u002Fmodule",[1179,1785,1780],{"class":1199},[1179,1787,1788],{"class":1239},"],\n",[1179,1790,1791,1794,1796],{"class":1181,"line":1212},[1179,1792,1793],{"class":1770},"  authH3Client",[1179,1795,1255],{"class":1774},[1179,1797,1295],{"class":1239},[1179,1799,1800,1803,1805,1808],{"class":1181,"line":1283},[1179,1801,1802],{"class":1770},"    enableMiddleware",[1179,1804,1255],{"class":1774},[1179,1806,1807],{"class":1195}," false",[1179,1809,1036],{"class":1239},[1179,1811,1812,1815,1817,1820,1823],{"class":1181,"line":1298},[1179,1813,1814],{"class":1770},"    authStatusUrl",[1179,1816,1255],{"class":1774},[1179,1818,1819],{"class":1199}," '",[1179,1821,1822],{"class":1203},"\u002Fapi\u002Fauth\u002Fusers\u002FauthStatus",[1179,1824,1825],{"class":1199},"'\n",[1179,1827,1828],{"class":1181,"line":1319},[1179,1829,1453],{"class":1239},[1179,1831,1832],{"class":1181,"line":1336},[1179,1833,1834],{"class":1239},"})\n",[830,1836,1837],{},"The module still gives you server auto-imports and client composables in this\nmode. It does not auto-register the bundled middleware, but it still registers\nthe auth status route and, when configured, the optional API token list route.",[830,1839,1840],{},"Configure the gateway in a Nitro plugin:",[1089,1842,1845],{"className":1745,"code":1843,"filename":1844,"language":1748,"meta":811,"style":811},"import { defineNitroPlugin } from 'nitropack\u002Fruntime'\nimport { useStorage } from 'nitropack\u002Fruntime\u002Fstorage'\nimport { configDefaults } from 'auth-h3client\u002Fserver\u002Ftemplates'\nimport { defineAuthConfiguration } from 'auth-h3client\u002Fv1'\n\nexport default defineNitroPlugin((nitroApp) => {\n  defineAuthConfiguration(nitroApp, {\n    ...configDefaults,\n    onSuccessRedirect: '\u002Fdashboard',\n    enableFireWallBans: false,\n    uStorage: {\n      storage: useStorage('cache'),\n      cacheOptions: {\n        successTtl: 60 * 60 * 24 * 30,\n        rateLimitTtl: 10\n      }\n    }\n  })\n})\n","server\u002Fplugins\u002Fauth.ts",[843,1846,1847,1871,1891,1911,1931,1936,1961,1974,1983,1999,2010,2019,2041,2050,2077,2088,2094,2100,2106],{"__ignoreMap":811},[1179,1848,1849,1852,1855,1858,1861,1864,1866,1869],{"class":1181,"line":1182},[1179,1850,1851],{"class":1755},"import",[1179,1853,1854],{"class":1239}," { ",[1179,1856,1857],{"class":1770},"defineNitroPlugin",[1179,1859,1860],{"class":1239}," } ",[1179,1862,1863],{"class":1755},"from",[1179,1865,1819],{"class":1199},[1179,1867,1868],{"class":1203},"nitropack\u002Fruntime",[1179,1870,1825],{"class":1199},[1179,1872,1873,1875,1877,1880,1882,1884,1886,1889],{"class":1181,"line":812},[1179,1874,1851],{"class":1755},[1179,1876,1854],{"class":1239},[1179,1878,1879],{"class":1770},"useStorage",[1179,1881,1860],{"class":1239},[1179,1883,1863],{"class":1755},[1179,1885,1819],{"class":1199},[1179,1887,1888],{"class":1203},"nitropack\u002Fruntime\u002Fstorage",[1179,1890,1825],{"class":1199},[1179,1892,1893,1895,1897,1900,1902,1904,1906,1909],{"class":1181,"line":1212},[1179,1894,1851],{"class":1755},[1179,1896,1854],{"class":1239},[1179,1898,1899],{"class":1770},"configDefaults",[1179,1901,1860],{"class":1239},[1179,1903,1863],{"class":1755},[1179,1905,1819],{"class":1199},[1179,1907,1908],{"class":1203},"auth-h3client\u002Fserver\u002Ftemplates",[1179,1910,1825],{"class":1199},[1179,1912,1913,1915,1917,1920,1922,1924,1926,1929],{"class":1181,"line":1283},[1179,1914,1851],{"class":1755},[1179,1916,1854],{"class":1239},[1179,1918,1919],{"class":1770},"defineAuthConfiguration",[1179,1921,1860],{"class":1239},[1179,1923,1863],{"class":1755},[1179,1925,1819],{"class":1199},[1179,1927,1928],{"class":1203},"auth-h3client\u002Fv1",[1179,1930,1825],{"class":1199},[1179,1932,1933],{"class":1181,"line":1298},[1179,1934,1935],{"emptyLinePlaceholder":8},"\n",[1179,1937,1938,1940,1942,1945,1948,1952,1955,1959],{"class":1181,"line":1319},[1179,1939,1756],{"class":1755},[1179,1941,1759],{"class":1755},[1179,1943,1944],{"class":1185}," defineNitroPlugin",[1179,1946,1947],{"class":1239},"((",[1179,1949,1951],{"class":1950},"sygFZ","nitroApp",[1179,1953,1954],{"class":1239},") ",[1179,1956,1958],{"class":1957},"sl46w","=>",[1179,1960,1295],{"class":1239},[1179,1962,1963,1966,1969,1971],{"class":1181,"line":1336},[1179,1964,1965],{"class":1185},"  defineAuthConfiguration",[1179,1967,1968],{"class":1239},"(",[1179,1970,1951],{"class":1770},[1179,1972,1973],{"class":1239},", {\n",[1179,1975,1976,1979,1981],{"class":1181,"line":1353},[1179,1977,1978],{"class":1254},"    ...",[1179,1980,1899],{"class":1770},[1179,1982,1036],{"class":1239},[1179,1984,1985,1988,1990,1992,1995,1997],{"class":1181,"line":1374},[1179,1986,1987],{"class":1770},"    onSuccessRedirect",[1179,1989,1255],{"class":1774},[1179,1991,1819],{"class":1199},[1179,1993,1994],{"class":1203},"\u002Fdashboard",[1179,1996,1780],{"class":1199},[1179,1998,1036],{"class":1239},[1179,2000,2001,2004,2006,2008],{"class":1181,"line":1395},[1179,2002,2003],{"class":1770},"    enableFireWallBans",[1179,2005,1255],{"class":1774},[1179,2007,1807],{"class":1195},[1179,2009,1036],{"class":1239},[1179,2011,2012,2015,2017],{"class":1181,"line":1415},[1179,2013,2014],{"class":1770},"    uStorage",[1179,2016,1255],{"class":1774},[1179,2018,1295],{"class":1239},[1179,2020,2021,2024,2026,2029,2031,2033,2036,2038],{"class":1181,"line":1432},[1179,2022,2023],{"class":1770},"      storage",[1179,2025,1255],{"class":1774},[1179,2027,2028],{"class":1185}," useStorage",[1179,2030,1968],{"class":1239},[1179,2032,1780],{"class":1199},[1179,2034,2035],{"class":1203},"cache",[1179,2037,1780],{"class":1199},[1179,2039,2040],{"class":1239},"),\n",[1179,2042,2043,2046,2048],{"class":1181,"line":1450},[1179,2044,2045],{"class":1770},"      cacheOptions",[1179,2047,1255],{"class":1774},[1179,2049,1295],{"class":1239},[1179,2051,2052,2055,2057,2060,2063,2065,2067,2070,2072,2075],{"class":1181,"line":1456},[1179,2053,2054],{"class":1770},"        successTtl",[1179,2056,1255],{"class":1774},[1179,2058,2059],{"class":1330}," 60",[1179,2061,2062],{"class":1254}," *",[1179,2064,2059],{"class":1330},[1179,2066,2062],{"class":1254},[1179,2068,2069],{"class":1330}," 24",[1179,2071,2062],{"class":1254},[1179,2073,2074],{"class":1330}," 30",[1179,2076,1036],{"class":1239},[1179,2078,2080,2083,2085],{"class":1181,"line":2079},15,[1179,2081,2082],{"class":1770},"        rateLimitTtl",[1179,2084,1255],{"class":1774},[1179,2086,2087],{"class":1330}," 10\n",[1179,2089,2091],{"class":1181,"line":2090},16,[1179,2092,2093],{"class":1239},"      }\n",[1179,2095,2097],{"class":1181,"line":2096},17,[1179,2098,2099],{"class":1239},"    }\n",[1179,2101,2103],{"class":1181,"line":2102},18,[1179,2104,2105],{"class":1239},"  })\n",[1179,2107,2109],{"class":1181,"line":2108},19,[1179,2110,1834],{"class":1239},[830,2112,2113],{},"Next, register a browser middleware that mirrors the packaged middleware but\nskips your machine-to-machine prefix.",[1089,2115,2118],{"className":1745,"code":2116,"filename":2117,"language":1748,"meta":811,"style":811},"import {\n  defineEventHandler,\n  getHeader,\n  getRequestURL,\n  isMethod,\n  sendNoContent,\n} from 'auth-h3client\u002Fv1'\nimport {\n  botDetectorMiddleware,\n  generateCsrfCookie,\n  isIPValid,\n} from 'auth-h3client\u002Fv1'\n\nexport default defineEventHandler(async (event) => {\n  const { pathname } = getRequestURL(event)\n\n  if (\n    isMethod(event, 'HEAD') ||\n    pathname === '\u002Fapi\u002Fhealth' ||\n    pathname.startsWith('\u002Fapi\u002F_mdc') ||\n    pathname.startsWith('\u002F_nuxt') ||\n    pathname.startsWith('\u002Fapi\u002Fpublic\u002F')\n  ) {\n    if (isMethod(event, 'HEAD') || pathname === '\u002Fapi\u002Fhealth') {\n      sendNoContent(event)\n    }\n\n    return\n  }\n\n  const forwardedFor = getHeader(event, 'x-forwarded-for')\n  if (forwardedFor === '127.0.0.1' || forwardedFor === '::1') {\n    return\n  }\n\n  isIPValid(event)\n  await botDetectorMiddleware(event)\n  generateCsrfCookie(event)\n})\n","server\u002Fmiddleware\u002Fauth-browser.ts",[843,2119,2120,2126,2133,2140,2147,2154,2161,2174,2180,2187,2194,2201,2213,2217,2243,2269,2273,2281,2304,2322,2345,2367,2387,2393,2435,2447,2452,2457,2463,2468,2473,2502,2537,2542,2547,2552,2563,2578,2589],{"__ignoreMap":811},[1179,2121,2122,2124],{"class":1181,"line":1182},[1179,2123,1851],{"class":1755},[1179,2125,1295],{"class":1239},[1179,2127,2128,2131],{"class":1181,"line":812},[1179,2129,2130],{"class":1770},"  defineEventHandler",[1179,2132,1036],{"class":1239},[1179,2134,2135,2138],{"class":1181,"line":1212},[1179,2136,2137],{"class":1770},"  getHeader",[1179,2139,1036],{"class":1239},[1179,2141,2142,2145],{"class":1181,"line":1283},[1179,2143,2144],{"class":1770},"  getRequestURL",[1179,2146,1036],{"class":1239},[1179,2148,2149,2152],{"class":1181,"line":1298},[1179,2150,2151],{"class":1770},"  isMethod",[1179,2153,1036],{"class":1239},[1179,2155,2156,2159],{"class":1181,"line":1319},[1179,2157,2158],{"class":1770},"  sendNoContent",[1179,2160,1036],{"class":1239},[1179,2162,2163,2166,2168,2170,2172],{"class":1181,"line":1336},[1179,2164,2165],{"class":1239},"} ",[1179,2167,1863],{"class":1755},[1179,2169,1819],{"class":1199},[1179,2171,1928],{"class":1203},[1179,2173,1825],{"class":1199},[1179,2175,2176,2178],{"class":1181,"line":1353},[1179,2177,1851],{"class":1755},[1179,2179,1295],{"class":1239},[1179,2181,2182,2185],{"class":1181,"line":1374},[1179,2183,2184],{"class":1770},"  botDetectorMiddleware",[1179,2186,1036],{"class":1239},[1179,2188,2189,2192],{"class":1181,"line":1395},[1179,2190,2191],{"class":1770},"  generateCsrfCookie",[1179,2193,1036],{"class":1239},[1179,2195,2196,2199],{"class":1181,"line":1415},[1179,2197,2198],{"class":1770},"  isIPValid",[1179,2200,1036],{"class":1239},[1179,2202,2203,2205,2207,2209,2211],{"class":1181,"line":1432},[1179,2204,2165],{"class":1239},[1179,2206,1863],{"class":1755},[1179,2208,1819],{"class":1199},[1179,2210,1928],{"class":1203},[1179,2212,1825],{"class":1199},[1179,2214,2215],{"class":1181,"line":1450},[1179,2216,1935],{"emptyLinePlaceholder":8},[1179,2218,2219,2221,2223,2226,2228,2231,2234,2237,2239,2241],{"class":1181,"line":1456},[1179,2220,1756],{"class":1755},[1179,2222,1759],{"class":1755},[1179,2224,2225],{"class":1185}," defineEventHandler",[1179,2227,1968],{"class":1239},[1179,2229,2230],{"class":1957},"async",[1179,2232,2233],{"class":1239}," (",[1179,2235,2236],{"class":1950},"event",[1179,2238,1954],{"class":1239},[1179,2240,1958],{"class":1957},[1179,2242,1295],{"class":1239},[1179,2244,2245,2248,2250,2254,2256,2259,2262,2264,2266],{"class":1181,"line":2079},[1179,2246,2247],{"class":1957},"  const",[1179,2249,1854],{"class":1239},[1179,2251,2253],{"class":2252},"s3JHE","pathname",[1179,2255,1860],{"class":1239},[1179,2257,2258],{"class":1254},"=",[1179,2260,2261],{"class":1185}," getRequestURL",[1179,2263,1968],{"class":1239},[1179,2265,2236],{"class":1770},[1179,2267,2268],{"class":1239},")\n",[1179,2270,2271],{"class":1181,"line":2090},[1179,2272,1935],{"emptyLinePlaceholder":8},[1179,2274,2275,2278],{"class":1181,"line":2096},[1179,2276,2277],{"class":1755},"  if",[1179,2279,2280],{"class":1239}," (\n",[1179,2282,2283,2286,2288,2290,2292,2294,2297,2299,2301],{"class":1181,"line":2102},[1179,2284,2285],{"class":1185},"    isMethod",[1179,2287,1968],{"class":1239},[1179,2289,2236],{"class":1770},[1179,2291,1032],{"class":1239},[1179,2293,1780],{"class":1199},[1179,2295,2296],{"class":1203},"HEAD",[1179,2298,1780],{"class":1199},[1179,2300,1954],{"class":1239},[1179,2302,2303],{"class":1254},"||\n",[1179,2305,2306,2309,2312,2314,2317,2319],{"class":1181,"line":2108},[1179,2307,2308],{"class":1770},"    pathname",[1179,2310,2311],{"class":1254}," ===",[1179,2313,1819],{"class":1199},[1179,2315,2316],{"class":1203},"\u002Fapi\u002Fhealth",[1179,2318,1780],{"class":1199},[1179,2320,2321],{"class":1254}," ||\n",[1179,2323,2325,2327,2329,2332,2334,2336,2339,2341,2343],{"class":1181,"line":2324},20,[1179,2326,2308],{"class":1770},[1179,2328,1053],{"class":1239},[1179,2330,2331],{"class":1185},"startsWith",[1179,2333,1968],{"class":1239},[1179,2335,1780],{"class":1199},[1179,2337,2338],{"class":1203},"\u002Fapi\u002F_mdc",[1179,2340,1780],{"class":1199},[1179,2342,1954],{"class":1239},[1179,2344,2303],{"class":1254},[1179,2346,2348,2350,2352,2354,2356,2358,2361,2363,2365],{"class":1181,"line":2347},21,[1179,2349,2308],{"class":1770},[1179,2351,1053],{"class":1239},[1179,2353,2331],{"class":1185},[1179,2355,1968],{"class":1239},[1179,2357,1780],{"class":1199},[1179,2359,2360],{"class":1203},"\u002F_nuxt",[1179,2362,1780],{"class":1199},[1179,2364,1954],{"class":1239},[1179,2366,2303],{"class":1254},[1179,2368,2370,2372,2374,2376,2378,2380,2383,2385],{"class":1181,"line":2369},22,[1179,2371,2308],{"class":1770},[1179,2373,1053],{"class":1239},[1179,2375,2331],{"class":1185},[1179,2377,1968],{"class":1239},[1179,2379,1780],{"class":1199},[1179,2381,2382],{"class":1203},"\u002Fapi\u002Fpublic\u002F",[1179,2384,1780],{"class":1199},[1179,2386,2268],{"class":1239},[1179,2388,2390],{"class":1181,"line":2389},23,[1179,2391,2392],{"class":1239},"  ) {\n",[1179,2394,2396,2399,2401,2404,2406,2408,2410,2412,2414,2416,2418,2421,2424,2426,2428,2430,2432],{"class":1181,"line":2395},24,[1179,2397,2398],{"class":1755},"    if",[1179,2400,2233],{"class":1239},[1179,2402,2403],{"class":1185},"isMethod",[1179,2405,1968],{"class":1239},[1179,2407,2236],{"class":1770},[1179,2409,1032],{"class":1239},[1179,2411,1780],{"class":1199},[1179,2413,2296],{"class":1203},[1179,2415,1780],{"class":1199},[1179,2417,1954],{"class":1239},[1179,2419,2420],{"class":1254},"||",[1179,2422,2423],{"class":1770}," pathname",[1179,2425,2311],{"class":1254},[1179,2427,1819],{"class":1199},[1179,2429,2316],{"class":1203},[1179,2431,1780],{"class":1199},[1179,2433,2434],{"class":1239},") {\n",[1179,2436,2438,2441,2443,2445],{"class":1181,"line":2437},25,[1179,2439,2440],{"class":1185},"      sendNoContent",[1179,2442,1968],{"class":1239},[1179,2444,2236],{"class":1770},[1179,2446,2268],{"class":1239},[1179,2448,2450],{"class":1181,"line":2449},26,[1179,2451,2099],{"class":1239},[1179,2453,2455],{"class":1181,"line":2454},27,[1179,2456,1935],{"emptyLinePlaceholder":8},[1179,2458,2460],{"class":1181,"line":2459},28,[1179,2461,2462],{"class":1755},"    return\n",[1179,2464,2466],{"class":1181,"line":2465},29,[1179,2467,1453],{"class":1239},[1179,2469,2471],{"class":1181,"line":2470},30,[1179,2472,1935],{"emptyLinePlaceholder":8},[1179,2474,2476,2478,2481,2484,2487,2489,2491,2493,2495,2498,2500],{"class":1181,"line":2475},31,[1179,2477,2247],{"class":1957},[1179,2479,2480],{"class":2252}," forwardedFor",[1179,2482,2483],{"class":1254}," =",[1179,2485,2486],{"class":1185}," getHeader",[1179,2488,1968],{"class":1239},[1179,2490,2236],{"class":1770},[1179,2492,1032],{"class":1239},[1179,2494,1780],{"class":1199},[1179,2496,2497],{"class":1203},"x-forwarded-for",[1179,2499,1780],{"class":1199},[1179,2501,2268],{"class":1239},[1179,2503,2505,2507,2509,2512,2514,2516,2519,2521,2524,2526,2528,2530,2533,2535],{"class":1181,"line":2504},32,[1179,2506,2277],{"class":1755},[1179,2508,2233],{"class":1239},[1179,2510,2511],{"class":1770},"forwardedFor",[1179,2513,2311],{"class":1254},[1179,2515,1819],{"class":1199},[1179,2517,2518],{"class":1203},"127.0.0.1",[1179,2520,1780],{"class":1199},[1179,2522,2523],{"class":1254}," ||",[1179,2525,2480],{"class":1770},[1179,2527,2311],{"class":1254},[1179,2529,1819],{"class":1199},[1179,2531,2532],{"class":1203},"::1",[1179,2534,1780],{"class":1199},[1179,2536,2434],{"class":1239},[1179,2538,2540],{"class":1181,"line":2539},33,[1179,2541,2462],{"class":1755},[1179,2543,2545],{"class":1181,"line":2544},34,[1179,2546,1453],{"class":1239},[1179,2548,2550],{"class":1181,"line":2549},35,[1179,2551,1935],{"emptyLinePlaceholder":8},[1179,2553,2555,2557,2559,2561],{"class":1181,"line":2554},36,[1179,2556,2198],{"class":1185},[1179,2558,1968],{"class":1239},[1179,2560,2236],{"class":1770},[1179,2562,2268],{"class":1239},[1179,2564,2566,2569,2572,2574,2576],{"class":1181,"line":2565},37,[1179,2567,2568],{"class":1755},"  await",[1179,2570,2571],{"class":1185}," botDetectorMiddleware",[1179,2573,1968],{"class":1239},[1179,2575,2236],{"class":1770},[1179,2577,2268],{"class":1239},[1179,2579,2581,2583,2585,2587],{"class":1181,"line":2580},38,[1179,2582,2191],{"class":1185},[1179,2584,1968],{"class":1239},[1179,2586,2236],{"class":1770},[1179,2588,2268],{"class":1239},[1179,2590,2592],{"class":1181,"line":2591},39,[1179,2593,1834],{"class":1239},[2595,2596,2597],"tip",{},[830,2598,2599,2600,2602,2603,2606],{},"If your app does not expose machine-to-machine API-key routes, keep\n",[843,2601,1735],{}," and optionally use ",[843,2604,2605],{},"registerApiRoute"," on the module.\nThat is the simplest browser-only setup.",[852,2608],{},[855,2610,2612,2613],{"id":2611},"protecting-a-custom-api-with-x-api-key","Protecting a custom API with ",[843,2614,908],{},[830,2616,2617,2618,2620,2621,2623,2624,2627,2628,2630],{},"Once the gateway is configured, protecting a custom API route is small and\npredictable. ",[843,2619,849],{}," reads the incoming ",[843,2622,908],{},",\ncalls IAM ",[843,2625,2626],{},"\u002Fapi\u002Fpublic\u002Fverify?privilege=...",", and places the verification\nresult on ",[843,2629,1227],{}," before your handler runs.",[830,2632,2633,2634,2636],{},"Here is a custom reports endpoint that grants access to tokens with the exact\n",[843,2635,1035],{}," privilege label:",[1089,2638,2641],{"className":1745,"code":2639,"filename":2640,"language":1748,"meta":811,"style":811},"import { defineAuthenticatePublicApi } from 'auth-h3client\u002Fv1'\n\nexport default defineAuthenticatePublicApi(async (event) => {\n  const token = event.context.apiVerification\n\n  return {\n    ok: true,\n    consumer: token.name,\n    tokenId: token.tokenId,\n    userId: token.userId,\n    privilege: token.providedPrivilege,\n    report: {\n      generatedAt: new Date().toISOString(),\n      items: ['orders', 'revenue', 'retention']\n    }\n  }\n}, 'demo')\n","server\u002Fapi\u002Fpublic\u002Freports.get.ts",[843,2642,2643,2661,2665,2688,2710,2714,2721,2732,2747,2762,2777,2792,2801,2824,2861,2865,2869],{"__ignoreMap":811},[1179,2644,2645,2647,2649,2651,2653,2655,2657,2659],{"class":1181,"line":1182},[1179,2646,1851],{"class":1755},[1179,2648,1854],{"class":1239},[1179,2650,849],{"class":1770},[1179,2652,1860],{"class":1239},[1179,2654,1863],{"class":1755},[1179,2656,1819],{"class":1199},[1179,2658,1928],{"class":1203},[1179,2660,1825],{"class":1199},[1179,2662,2663],{"class":1181,"line":812},[1179,2664,1935],{"emptyLinePlaceholder":8},[1179,2666,2667,2669,2671,2674,2676,2678,2680,2682,2684,2686],{"class":1181,"line":1212},[1179,2668,1756],{"class":1755},[1179,2670,1759],{"class":1755},[1179,2672,2673],{"class":1185}," defineAuthenticatePublicApi",[1179,2675,1968],{"class":1239},[1179,2677,2230],{"class":1957},[1179,2679,2233],{"class":1239},[1179,2681,2236],{"class":1950},[1179,2683,1954],{"class":1239},[1179,2685,1958],{"class":1957},[1179,2687,1295],{"class":1239},[1179,2689,2690,2692,2695,2697,2700,2702,2705,2707],{"class":1181,"line":1283},[1179,2691,2247],{"class":1957},[1179,2693,2694],{"class":2252}," token",[1179,2696,2483],{"class":1254},[1179,2698,2699],{"class":1770}," event",[1179,2701,1053],{"class":1239},[1179,2703,2704],{"class":1770},"context",[1179,2706,1053],{"class":1239},[1179,2708,2709],{"class":1770},"apiVerification\n",[1179,2711,2712],{"class":1181,"line":1298},[1179,2713,1935],{"emptyLinePlaceholder":8},[1179,2715,2716,2719],{"class":1181,"line":1319},[1179,2717,2718],{"class":1755},"  return",[1179,2720,1295],{"class":1239},[1179,2722,2723,2726,2728,2730],{"class":1181,"line":1336},[1179,2724,2725],{"class":1770},"    ok",[1179,2727,1255],{"class":1774},[1179,2729,1258],{"class":1195},[1179,2731,1036],{"class":1239},[1179,2733,2734,2737,2739,2741,2743,2745],{"class":1181,"line":1353},[1179,2735,2736],{"class":1770},"    consumer",[1179,2738,1255],{"class":1774},[1179,2740,2694],{"class":1770},[1179,2742,1053],{"class":1239},[1179,2744,1121],{"class":1770},[1179,2746,1036],{"class":1239},[1179,2748,2749,2752,2754,2756,2758,2760],{"class":1181,"line":1374},[1179,2750,2751],{"class":1770},"    tokenId",[1179,2753,1255],{"class":1774},[1179,2755,2694],{"class":1770},[1179,2757,1053],{"class":1239},[1179,2759,1114],{"class":1770},[1179,2761,1036],{"class":1239},[1179,2763,2764,2767,2769,2771,2773,2775],{"class":1181,"line":1395},[1179,2765,2766],{"class":1770},"    userId",[1179,2768,1255],{"class":1774},[1179,2770,2694],{"class":1770},[1179,2772,1053],{"class":1239},[1179,2774,1341],{"class":1770},[1179,2776,1036],{"class":1239},[1179,2778,2779,2782,2784,2786,2788,2790],{"class":1181,"line":1415},[1179,2780,2781],{"class":1770},"    privilege",[1179,2783,1255],{"class":1774},[1179,2785,2694],{"class":1770},[1179,2787,1053],{"class":1239},[1179,2789,1437],{"class":1770},[1179,2791,1036],{"class":1239},[1179,2793,2794,2797,2799],{"class":1181,"line":1432},[1179,2795,2796],{"class":1770},"    report",[1179,2798,1255],{"class":1774},[1179,2800,1295],{"class":1239},[1179,2802,2803,2806,2808,2812,2815,2818,2821],{"class":1181,"line":1450},[1179,2804,2805],{"class":1770},"      generatedAt",[1179,2807,1255],{"class":1774},[1179,2809,2811],{"class":2810},"sakC6"," new",[1179,2813,2814],{"class":1185}," Date",[1179,2816,2817],{"class":1239},"().",[1179,2819,2820],{"class":1185},"toISOString",[1179,2822,2823],{"class":1239},"(),\n",[1179,2825,2826,2829,2831,2833,2835,2838,2840,2842,2844,2847,2849,2851,2853,2856,2858],{"class":1181,"line":1456},[1179,2827,2828],{"class":1770},"      items",[1179,2830,1255],{"class":1774},[1179,2832,1777],{"class":1239},[1179,2834,1780],{"class":1199},[1179,2836,2837],{"class":1203},"orders",[1179,2839,1780],{"class":1199},[1179,2841,1032],{"class":1239},[1179,2843,1780],{"class":1199},[1179,2845,2846],{"class":1203},"revenue",[1179,2848,1780],{"class":1199},[1179,2850,1032],{"class":1239},[1179,2852,1780],{"class":1199},[1179,2854,2855],{"class":1203},"retention",[1179,2857,1780],{"class":1199},[1179,2859,2860],{"class":1239},"]\n",[1179,2862,2863],{"class":1181,"line":2079},[1179,2864,2099],{"class":1239},[1179,2866,2867],{"class":1181,"line":2090},[1179,2868,1453],{"class":1239},[1179,2870,2871,2874,2876,2878,2880],{"class":1181,"line":2096},[1179,2872,2873],{"class":1239},"}, ",[1179,2875,1780],{"class":1199},[1179,2877,1035],{"class":1203},[1179,2879,1780],{"class":1199},[1179,2881,2268],{"class":1239},[830,2883,2884],{},"Call the route from another service like this:",[1089,2886,2888],{"className":1172,"code":2887,"filename":1174,"language":1175,"meta":811,"style":811},"curl \\\n  -H \"X-API-KEY: rpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80\" \\\n  \"http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fpublic\u002Freports\"\n",[843,2889,2890,2896,2908],{"__ignoreMap":811},[1179,2891,2892,2894],{"class":1181,"line":1182},[1179,2893,1186],{"class":1185},[1179,2895,1190],{"class":1189},[1179,2897,2898,2900,2902,2904,2906],{"class":1181,"line":812},[1179,2899,1196],{"class":1195},[1179,2901,1200],{"class":1199},[1179,2903,1204],{"class":1203},[1179,2905,1207],{"class":1199},[1179,2907,1190],{"class":1189},[1179,2909,2910,2912,2915],{"class":1181,"line":1212},[1179,2911,1215],{"class":1199},[1179,2913,2914],{"class":1203},"http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fpublic\u002Freports",[1179,2916,1221],{"class":1199},[830,2918,2919],{},"This pattern is the cleanest way to grant access to a custom API. Your Nuxt\nserver owns the route contract and decides what the response shape looks like.\nThe IAM service owns credential validity. It checks the privilege label,\nenforces IP restrictions, updates the usage record, and applies abuse controls\nbefore your handler is ever reached.",[852,2921],{},[855,2923,2925],{"id":2924},"let-logged-in-users-create-and-rotate-tokens","Let logged-in users create and rotate tokens",[830,2927,2928,2929,2931,2932,2934,2935,2937],{},"The other half of the story is dashboard management. That is where\n",[843,2930,1699],{}," fits. The wrapper enforces session authentication\nand CSRF verification before it processes any action. It requires a ",[843,2933,918],{},"\nmethod and applies a 2 KB JSON body limit. Token identity resolution happens\non the server, so the browser only needs to submit a ",[843,2936,1114],{}," for any\nexisting-token action.",[830,2939,2940,2941,1255],{},"Create one route file and branch on ",[843,2942,2943],{},"event.context.params.action",[1089,2945,2948],{"className":1745,"code":2946,"filename":2947,"language":1748,"meta":811,"style":811},"import { defineApiManagementHandler } from 'auth-h3client\u002Fv1'\n\nexport default defineApiManagementHandler(async (event) => {\n  const action = event.context.params?.action\n\n  if (action === 'new-token') {\n    return { ok: true, data: event.context.newApiToken }\n  }\n\n  if (action === 'metadata') {\n    return { ok: true, data: event.context.extensiveMetadata }\n  }\n\n  if (action === 'rotate') {\n    return { ok: true, data: event.context.rotate }\n  }\n\n  if (action === 'revoke') {\n    return { ok: true, data: event.context.revoke }\n  }\n\n  return {\n    ok: true,\n    data: event.context.ipRestrictionUpdate ?? event.context.privilegeUpdate\n  }\n}, 'demo', 'protected')\n","server\u002Fapi\u002Fauth\u002Fapi-tokens\u002F[action].post.ts",[843,2949,2950,2968,2972,2995,3021,3025,3044,3077,3081,3085,3103,3134,3138,3142,3160,3190,3194,3198,3216,3246,3250,3254,3260,3270,3302,3306],{"__ignoreMap":811},[1179,2951,2952,2954,2956,2958,2960,2962,2964,2966],{"class":1181,"line":1182},[1179,2953,1851],{"class":1755},[1179,2955,1854],{"class":1239},[1179,2957,1699],{"class":1770},[1179,2959,1860],{"class":1239},[1179,2961,1863],{"class":1755},[1179,2963,1819],{"class":1199},[1179,2965,1928],{"class":1203},[1179,2967,1825],{"class":1199},[1179,2969,2970],{"class":1181,"line":812},[1179,2971,1935],{"emptyLinePlaceholder":8},[1179,2973,2974,2976,2978,2981,2983,2985,2987,2989,2991,2993],{"class":1181,"line":1212},[1179,2975,1756],{"class":1755},[1179,2977,1759],{"class":1755},[1179,2979,2980],{"class":1185}," defineApiManagementHandler",[1179,2982,1968],{"class":1239},[1179,2984,2230],{"class":1957},[1179,2986,2233],{"class":1239},[1179,2988,2236],{"class":1950},[1179,2990,1954],{"class":1239},[1179,2992,1958],{"class":1957},[1179,2994,1295],{"class":1239},[1179,2996,2997,2999,3002,3004,3006,3008,3010,3012,3015,3018],{"class":1181,"line":1283},[1179,2998,2247],{"class":1957},[1179,3000,3001],{"class":2252}," action",[1179,3003,2483],{"class":1254},[1179,3005,2699],{"class":1770},[1179,3007,1053],{"class":1239},[1179,3009,2704],{"class":1770},[1179,3011,1053],{"class":1239},[1179,3013,3014],{"class":1770},"params",[1179,3016,3017],{"class":1239},"?.",[1179,3019,3020],{"class":1770},"action\n",[1179,3022,3023],{"class":1181,"line":1298},[1179,3024,1935],{"emptyLinePlaceholder":8},[1179,3026,3027,3029,3031,3034,3036,3038,3040,3042],{"class":1181,"line":1319},[1179,3028,2277],{"class":1755},[1179,3030,2233],{"class":1239},[1179,3032,3033],{"class":1770},"action",[1179,3035,2311],{"class":1254},[1179,3037,1819],{"class":1199},[1179,3039,1510],{"class":1203},[1179,3041,1780],{"class":1199},[1179,3043,2434],{"class":1239},[1179,3045,3046,3049,3051,3053,3055,3057,3059,3061,3063,3065,3067,3069,3071,3074],{"class":1181,"line":1336},[1179,3047,3048],{"class":1755},"    return",[1179,3050,1854],{"class":1239},[1179,3052,1249],{"class":1770},[1179,3054,1255],{"class":1774},[1179,3056,1258],{"class":1195},[1179,3058,1032],{"class":1239},[1179,3060,1288],{"class":1770},[1179,3062,1255],{"class":1774},[1179,3064,2699],{"class":1770},[1179,3066,1053],{"class":1239},[1179,3068,2704],{"class":1770},[1179,3070,1053],{"class":1239},[1179,3072,3073],{"class":1770},"newApiToken",[1179,3075,3076],{"class":1239}," }\n",[1179,3078,3079],{"class":1181,"line":1353},[1179,3080,1453],{"class":1239},[1179,3082,3083],{"class":1181,"line":1374},[1179,3084,1935],{"emptyLinePlaceholder":8},[1179,3086,3087,3089,3091,3093,3095,3097,3099,3101],{"class":1181,"line":1395},[1179,3088,2277],{"class":1755},[1179,3090,2233],{"class":1239},[1179,3092,3033],{"class":1770},[1179,3094,2311],{"class":1254},[1179,3096,1819],{"class":1199},[1179,3098,1579],{"class":1203},[1179,3100,1780],{"class":1199},[1179,3102,2434],{"class":1239},[1179,3104,3105,3107,3109,3111,3113,3115,3117,3119,3121,3123,3125,3127,3129,3132],{"class":1181,"line":1415},[1179,3106,3048],{"class":1755},[1179,3108,1854],{"class":1239},[1179,3110,1249],{"class":1770},[1179,3112,1255],{"class":1774},[1179,3114,1258],{"class":1195},[1179,3116,1032],{"class":1239},[1179,3118,1288],{"class":1770},[1179,3120,1255],{"class":1774},[1179,3122,2699],{"class":1770},[1179,3124,1053],{"class":1239},[1179,3126,2704],{"class":1770},[1179,3128,1053],{"class":1239},[1179,3130,3131],{"class":1770},"extensiveMetadata",[1179,3133,3076],{"class":1239},[1179,3135,3136],{"class":1181,"line":1432},[1179,3137,1453],{"class":1239},[1179,3139,3140],{"class":1181,"line":1450},[1179,3141,1935],{"emptyLinePlaceholder":8},[1179,3143,3144,3146,3148,3150,3152,3154,3156,3158],{"class":1181,"line":1456},[1179,3145,2277],{"class":1755},[1179,3147,2233],{"class":1239},[1179,3149,3033],{"class":1770},[1179,3151,2311],{"class":1254},[1179,3153,1819],{"class":1199},[1179,3155,1601],{"class":1203},[1179,3157,1780],{"class":1199},[1179,3159,2434],{"class":1239},[1179,3161,3162,3164,3166,3168,3170,3172,3174,3176,3178,3180,3182,3184,3186,3188],{"class":1181,"line":2079},[1179,3163,3048],{"class":1755},[1179,3165,1854],{"class":1239},[1179,3167,1249],{"class":1770},[1179,3169,1255],{"class":1774},[1179,3171,1258],{"class":1195},[1179,3173,1032],{"class":1239},[1179,3175,1288],{"class":1770},[1179,3177,1255],{"class":1774},[1179,3179,2699],{"class":1770},[1179,3181,1053],{"class":1239},[1179,3183,2704],{"class":1770},[1179,3185,1053],{"class":1239},[1179,3187,1601],{"class":1770},[1179,3189,3076],{"class":1239},[1179,3191,3192],{"class":1181,"line":2090},[1179,3193,1453],{"class":1239},[1179,3195,3196],{"class":1181,"line":2096},[1179,3197,1935],{"emptyLinePlaceholder":8},[1179,3199,3200,3202,3204,3206,3208,3210,3212,3214],{"class":1181,"line":2102},[1179,3201,2277],{"class":1755},[1179,3203,2233],{"class":1239},[1179,3205,3033],{"class":1770},[1179,3207,2311],{"class":1254},[1179,3209,1819],{"class":1199},[1179,3211,1557],{"class":1203},[1179,3213,1780],{"class":1199},[1179,3215,2434],{"class":1239},[1179,3217,3218,3220,3222,3224,3226,3228,3230,3232,3234,3236,3238,3240,3242,3244],{"class":1181,"line":2108},[1179,3219,3048],{"class":1755},[1179,3221,1854],{"class":1239},[1179,3223,1249],{"class":1770},[1179,3225,1255],{"class":1774},[1179,3227,1258],{"class":1195},[1179,3229,1032],{"class":1239},[1179,3231,1288],{"class":1770},[1179,3233,1255],{"class":1774},[1179,3235,2699],{"class":1770},[1179,3237,1053],{"class":1239},[1179,3239,2704],{"class":1770},[1179,3241,1053],{"class":1239},[1179,3243,1557],{"class":1770},[1179,3245,3076],{"class":1239},[1179,3247,3248],{"class":1181,"line":2324},[1179,3249,1453],{"class":1239},[1179,3251,3252],{"class":1181,"line":2347},[1179,3253,1935],{"emptyLinePlaceholder":8},[1179,3255,3256,3258],{"class":1181,"line":2369},[1179,3257,2718],{"class":1755},[1179,3259,1295],{"class":1239},[1179,3261,3262,3264,3266,3268],{"class":1181,"line":2389},[1179,3263,2725],{"class":1770},[1179,3265,1255],{"class":1774},[1179,3267,1258],{"class":1195},[1179,3269,1036],{"class":1239},[1179,3271,3272,3275,3277,3279,3281,3283,3285,3288,3291,3293,3295,3297,3299],{"class":1181,"line":2395},[1179,3273,3274],{"class":1770},"    data",[1179,3276,1255],{"class":1774},[1179,3278,2699],{"class":1770},[1179,3280,1053],{"class":1239},[1179,3282,2704],{"class":1770},[1179,3284,1053],{"class":1239},[1179,3286,3287],{"class":1770},"ipRestrictionUpdate",[1179,3289,3290],{"class":1254}," ??",[1179,3292,2699],{"class":1770},[1179,3294,1053],{"class":1239},[1179,3296,2704],{"class":1770},[1179,3298,1053],{"class":1239},[1179,3300,3301],{"class":1770},"privilegeUpdate\n",[1179,3303,3304],{"class":1181,"line":2437},[1179,3305,1453],{"class":1239},[1179,3307,3308,3310,3312,3314,3316,3318,3320,3322,3324],{"class":1181,"line":2449},[1179,3309,2873],{"class":1239},[1179,3311,1780],{"class":1199},[1179,3313,1035],{"class":1203},[1179,3315,1780],{"class":1199},[1179,3317,1032],{"class":1239},[1179,3319,1780],{"class":1199},[1179,3321,1042],{"class":1203},[1179,3323,1780],{"class":1199},[1179,3325,2268],{"class":1239},[830,3327,3328,3329,3331,3332,3334,3335,3337],{},"In this example, newly created tokens are always issued with the ",[843,3330,1035],{},"\nprivilege. The optional third argument allows ",[843,3333,1647],{}," to move a\ntoken to ",[843,3336,1042],{},". If you omit that third argument, the wrapper rejects the\nprivilege update action.",[830,3339,3340,3341,3343],{},"From the browser, call that route with ",[843,3342,172],{},". The helper injects the\nCSRF header on the client and forwards cookies correctly during SSR.",[1089,3345,3348],{"className":1745,"code":3346,"filename":3347,"language":1748,"meta":811,"style":811},"import { executeRequest } from 'auth-h3client\u002Fclient'\n\nexport async function createDemoToken() {\n  return await executeRequest\u003C{\n    rawApiKey?: string\n    expiresAt?: string | null\n  }>('\u002Fapi\u002Fauth\u002Fapi-tokens\u002Fnew-token', 'POST', {\n    name: 'report-worker',\n    prefix: 'rpt',\n    expires: 1000 * 60 * 60 * 24 * 30,\n    ipv4: ['203.0.113.10']\n  })\n}\n\nexport async function rotateToken(tokenId: number) {\n  return await executeRequest\u003C{\n    msg: string\n    newRawToken?: string\n    newExpiry?: string | null\n  }>('\u002Fapi\u002Fauth\u002Fapi-tokens\u002Frotate', 'POST', {\n    tokenId\n  })\n}\n","app\u002Fcomposables\u002FuseApiTokens.ts",[843,3349,3350,3369,3373,3389,3402,3414,3430,3452,3467,3483,3511,3529,3533,3537,3541,3563,3573,3582,3591,3604,3625,3630,3634],{"__ignoreMap":811},[1179,3351,3352,3354,3356,3358,3360,3362,3364,3367],{"class":1181,"line":1182},[1179,3353,1851],{"class":1755},[1179,3355,1854],{"class":1239},[1179,3357,172],{"class":1770},[1179,3359,1860],{"class":1239},[1179,3361,1863],{"class":1755},[1179,3363,1819],{"class":1199},[1179,3365,3366],{"class":1203},"auth-h3client\u002Fclient",[1179,3368,1825],{"class":1199},[1179,3370,3371],{"class":1181,"line":812},[1179,3372,1935],{"emptyLinePlaceholder":8},[1179,3374,3375,3377,3380,3383,3386],{"class":1181,"line":1212},[1179,3376,1756],{"class":1755},[1179,3378,3379],{"class":1957}," async",[1179,3381,3382],{"class":1957}," function",[1179,3384,3385],{"class":1185}," createDemoToken",[1179,3387,3388],{"class":1239},"() {\n",[1179,3390,3391,3393,3396,3399],{"class":1181,"line":1283},[1179,3392,2718],{"class":1755},[1179,3394,3395],{"class":1755}," await",[1179,3397,3398],{"class":1185}," executeRequest",[1179,3400,3401],{"class":1239},"\u003C{\n",[1179,3403,3404,3407,3410],{"class":1181,"line":1298},[1179,3405,3406],{"class":1770},"    rawApiKey",[1179,3408,3409],{"class":1254},"?:",[1179,3411,3413],{"class":3412},"sFs1U"," string\n",[1179,3415,3416,3419,3421,3424,3427],{"class":1181,"line":1319},[1179,3417,3418],{"class":1770},"    expiresAt",[1179,3420,3409],{"class":1254},[1179,3422,3423],{"class":3412}," string",[1179,3425,3426],{"class":1254}," |",[1179,3428,3429],{"class":3412}," null\n",[1179,3431,3432,3435,3437,3440,3442,3444,3446,3448,3450],{"class":1181,"line":1336},[1179,3433,3434],{"class":1239},"  }>(",[1179,3436,1780],{"class":1199},[1179,3438,3439],{"class":1203},"\u002Fapi\u002Fauth\u002Fapi-tokens\u002Fnew-token",[1179,3441,1780],{"class":1199},[1179,3443,1032],{"class":1239},[1179,3445,1780],{"class":1199},[1179,3447,918],{"class":1203},[1179,3449,1780],{"class":1199},[1179,3451,1973],{"class":1239},[1179,3453,3454,3457,3459,3461,3463,3465],{"class":1181,"line":1353},[1179,3455,3456],{"class":1770},"    name",[1179,3458,1255],{"class":1774},[1179,3460,1819],{"class":1199},[1179,3462,1312],{"class":1203},[1179,3464,1780],{"class":1199},[1179,3466,1036],{"class":1239},[1179,3468,3469,3472,3474,3476,3479,3481],{"class":1181,"line":1374},[1179,3470,3471],{"class":1770},"    prefix",[1179,3473,1255],{"class":1774},[1179,3475,1819],{"class":1199},[1179,3477,3478],{"class":1203},"rpt",[1179,3480,1780],{"class":1199},[1179,3482,1036],{"class":1239},[1179,3484,3485,3488,3490,3493,3495,3497,3499,3501,3503,3505,3507,3509],{"class":1181,"line":1395},[1179,3486,3487],{"class":1770},"    expires",[1179,3489,1255],{"class":1774},[1179,3491,3492],{"class":1330}," 1000",[1179,3494,2062],{"class":1254},[1179,3496,2059],{"class":1330},[1179,3498,2062],{"class":1254},[1179,3500,2059],{"class":1330},[1179,3502,2062],{"class":1254},[1179,3504,2069],{"class":1330},[1179,3506,2062],{"class":1254},[1179,3508,2074],{"class":1330},[1179,3510,1036],{"class":1239},[1179,3512,3513,3516,3518,3520,3522,3525,3527],{"class":1181,"line":1415},[1179,3514,3515],{"class":1770},"    ipv4",[1179,3517,1255],{"class":1774},[1179,3519,1777],{"class":1239},[1179,3521,1780],{"class":1199},[1179,3523,3524],{"class":1203},"203.0.113.10",[1179,3526,1780],{"class":1199},[1179,3528,2860],{"class":1239},[1179,3530,3531],{"class":1181,"line":1432},[1179,3532,2105],{"class":1239},[1179,3534,3535],{"class":1181,"line":1450},[1179,3536,1459],{"class":1239},[1179,3538,3539],{"class":1181,"line":1456},[1179,3540,1935],{"emptyLinePlaceholder":8},[1179,3542,3543,3545,3547,3549,3552,3554,3556,3558,3561],{"class":1181,"line":2079},[1179,3544,1756],{"class":1755},[1179,3546,3379],{"class":1957},[1179,3548,3382],{"class":1957},[1179,3550,3551],{"class":1185}," rotateToken",[1179,3553,1968],{"class":1239},[1179,3555,1114],{"class":1950},[1179,3557,1255],{"class":1254},[1179,3559,3560],{"class":3412}," number",[1179,3562,2434],{"class":1239},[1179,3564,3565,3567,3569,3571],{"class":1181,"line":2090},[1179,3566,2718],{"class":1755},[1179,3568,3395],{"class":1755},[1179,3570,3398],{"class":1185},[1179,3572,3401],{"class":1239},[1179,3574,3575,3578,3580],{"class":1181,"line":2096},[1179,3576,3577],{"class":1770},"    msg",[1179,3579,1255],{"class":1254},[1179,3581,3413],{"class":3412},[1179,3583,3584,3587,3589],{"class":1181,"line":2102},[1179,3585,3586],{"class":1770},"    newRawToken",[1179,3588,3409],{"class":1254},[1179,3590,3413],{"class":3412},[1179,3592,3593,3596,3598,3600,3602],{"class":1181,"line":2108},[1179,3594,3595],{"class":1770},"    newExpiry",[1179,3597,3409],{"class":1254},[1179,3599,3423],{"class":3412},[1179,3601,3426],{"class":1254},[1179,3603,3429],{"class":3412},[1179,3605,3606,3608,3610,3613,3615,3617,3619,3621,3623],{"class":1181,"line":2324},[1179,3607,3434],{"class":1239},[1179,3609,1780],{"class":1199},[1179,3611,3612],{"class":1203},"\u002Fapi\u002Fauth\u002Fapi-tokens\u002Frotate",[1179,3614,1780],{"class":1199},[1179,3616,1032],{"class":1239},[1179,3618,1780],{"class":1199},[1179,3620,918],{"class":1203},[1179,3622,1780],{"class":1199},[1179,3624,1973],{"class":1239},[1179,3626,3627],{"class":1181,"line":2347},[1179,3628,3629],{"class":1770},"    tokenId\n",[1179,3631,3632],{"class":1181,"line":2369},[1179,3633,2105],{"class":1239},[1179,3635,3636],{"class":1181,"line":2389},[1179,3637,1459],{"class":1239},[830,3639,3640,3641,3643,3644,1159,3646,3648],{},"Notice what the browser does not send for rotate. It only sends ",[843,3642,1114],{},". The\nwrapper fetches the authenticated token inventory, resolves the matching\n",[843,3645,1117],{},[843,3647,1121],{},", and only then calls the IAM management endpoint.\nThat is one of the main reasons to use Auth H3 Client instead of calling the\nIAM management API directly from the browser.",[830,3650,3651],{},"The wrapper also gives you typed action results on the event context:",[863,3653,3654,3663],{},[866,3655,3656],{},[869,3657,3658,3660],{},[872,3659,1493],{},[872,3661,3662],{},"Event context field",[885,3664,3665,3676,3687,3698,3709,3720],{},[869,3666,3667,3671],{},[890,3668,3669],{},[843,3670,1510],{},[890,3672,3673],{},[843,3674,3675],{},"event.context.newApiToken",[869,3677,3678,3682],{},[890,3679,3680],{},[843,3681,1579],{},[890,3683,3684],{},[843,3685,3686],{},"event.context.extensiveMetadata",[869,3688,3689,3693],{},[890,3690,3691],{},[843,3692,1601],{},[890,3694,3695],{},[843,3696,3697],{},"event.context.rotate",[869,3699,3700,3704],{},[890,3701,3702],{},[843,3703,1557],{},[890,3705,3706],{},[843,3707,3708],{},"event.context.revoke",[869,3710,3711,3715],{},[890,3712,3713],{},[843,3714,1623],{},[890,3716,3717],{},[843,3718,3719],{},"event.context.ipRestrictionUpdate",[869,3721,3722,3726],{},[890,3723,3724],{},[843,3725,1647],{},[890,3727,3728],{},[843,3729,3730],{},"event.context.privilegeUpdate",[852,3732],{},[855,3734,3736],{"id":3735},"a-complete-flow-for-granting-access-to-a-custom-api","A complete flow for granting access to a custom API",[830,3738,3739,3740,3742],{},"With the pieces above, the end-to-end flow is straightforward. A logged-in user\ncreates a token from your dashboard. Your app stores only the raw token shown\nonce to the user. The external service then calls your Nuxt route with\n",[843,3741,908],{},", and the gateway verifies it against the IAM service before your\nhandler runs.",[830,3744,3745,3747,3748,3750,3751,3753],{},[843,3746,1703],{}," gives the browser a clean inventory view for active\ntokens. It proxies to IAM ",[843,3749,940],{}," and strips\n",[843,3752,1110],{}," before the response reaches the browser. That means the\nfrontend can render names, creation dates, expiry times, usage counts, and IP\nrestrictions without learning the internal management identifier.",[830,3755,3756],{},"This split gives you three strong properties at the same time. The custom API\nstays simple. The IAM service remains the only place that decides whether a key\nis valid. The browser never needs to hold server-side token identity data for\nmanagement actions.",[852,3758],{},[855,3760,3762],{"id":3761},"summary","Summary",[830,3764,3765],{},"Each\nkey is validated by checksum before a database lookup happens. The database\nholds only hashed values and never the raw secret. Privilege matching is exact,\nand IP restrictions are enforced at the verification layer before your handler\nsees the request. Every successful call updates the token's usage record, giving\nyou a real credential inventory rather than an opaque secret store. Rotation and\nrevocation are first-class operations built into the same management surface\nthat creates tokens.",[830,3767,3768,3769,3771],{},"Auth H3 Client is the layer that makes that subsystem practical in Nuxt and\nNitro. Machine-to-machine verification gets a dedicated wrapper that calls IAM\nand places results on the event context. Browser management routes get a\nseparate wrapper that handles session auth and CSRF checks, then resolves token\nidentity on the server so the browser never holds internal IAM references. The\ninventory controller strips ",[843,3770,1110],{}," from list responses so the\nfrontend can render names, dates, expiry times, and usage counts without\nlearning the server-side management identifier.",[830,3773,3774,3775,3777],{},"If you are building a mixed app with both browser auth and API-key protected\ncustom APIs, the right pattern is clear. Keep browser middleware on browser\nroutes. Bypass that middleware for ",[843,3776,849],{}," routes. Let\nthe IAM service own credential validity and token lifecycle decisions.",[3779,3780],"read-more",{"to":446},[3779,3782],{"to":92},[3779,3784],{"to":219},[3786,3787,3788],"style",{},"html pre.shiki code .sHOzp, html code.shiki .sHOzp{--shiki-light:#795E26;--shiki-default:#795E26;--shiki-dark:#50FA7B}html pre.shiki code .st6lo, html code.shiki .st6lo{--shiki-light:#EE0000;--shiki-default:#EE0000;--shiki-dark:#FF79C6}html pre.shiki code .sjR7W, html code.shiki .sjR7W{--shiki-light:#0000FF;--shiki-default:#0000FF;--shiki-dark:#BD93F9}html pre.shiki code .sFkSl, html code.shiki .sFkSl{--shiki-light:#A31515;--shiki-default:#A31515;--shiki-dark:#E9F284}html pre.shiki code .sFB1V, html code.shiki .sFB1V{--shiki-light:#A31515;--shiki-default:#A31515;--shiki-dark:#F1FA8C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sDd4n, html code.shiki .sDd4n{--shiki-light:#000000;--shiki-default:#000000;--shiki-dark:#F8F8F2}html pre.shiki code .saJyd, html code.shiki .saJyd{--shiki-light:#0451A5;--shiki-default:#0451A5;--shiki-dark:#8BE9FE}html pre.shiki code .s_W10, html code.shiki .s_W10{--shiki-light:#0451A5;--shiki-default:#0451A5;--shiki-dark:#8BE9FD}html pre.shiki code .saOXh, html code.shiki .saOXh{--shiki-light:#000000;--shiki-default:#000000;--shiki-dark:#FF79C6}html pre.shiki code .spgvN, html code.shiki .spgvN{--shiki-light:#098658;--shiki-default:#098658;--shiki-dark:#BD93F9}html pre.shiki code .sZ328, html code.shiki .sZ328{--shiki-light:#AF00DB;--shiki-default:#AF00DB;--shiki-dark:#FF79C6}html pre.shiki code .sjsA6, html code.shiki .sjsA6{--shiki-light:#001080;--shiki-default:#001080;--shiki-dark:#F8F8F2}html pre.shiki code .s34zl, html code.shiki .s34zl{--shiki-light:#001080;--shiki-default:#001080;--shiki-dark:#FF79C6}html pre.shiki code .sygFZ, html code.shiki .sygFZ{--shiki-light:#001080;--shiki-light-font-style:inherit;--shiki-default:#001080;--shiki-default-font-style:inherit;--shiki-dark:#FFB86C;--shiki-dark-font-style:italic}html pre.shiki code .sl46w, html code.shiki .sl46w{--shiki-light:#0000FF;--shiki-default:#0000FF;--shiki-dark:#FF79C6}html pre.shiki code .s3JHE, html code.shiki .s3JHE{--shiki-light:#0070C1;--shiki-default:#0070C1;--shiki-dark:#F8F8F2}html pre.shiki code .sakC6, html code.shiki .sakC6{--shiki-light:#0000FF;--shiki-light-font-weight:inherit;--shiki-default:#0000FF;--shiki-default-font-weight:inherit;--shiki-dark:#FF79C6;--shiki-dark-font-weight:bold}html pre.shiki code .sFs1U, html code.shiki .sFs1U{--shiki-light:#267F99;--shiki-light-font-style:inherit;--shiki-default:#267F99;--shiki-default-font-style:inherit;--shiki-dark:#8BE9FD;--shiki-dark-font-style:italic}",{"title":811,"searchDepth":812,"depth":812,"links":3790},[3791,3792,3793,3794,3795,3796,3797,3799,3800,3801],{"id":857,"depth":812,"text":858},{"id":1079,"depth":812,"text":1080},{"id":1133,"depth":812,"text":1134},{"id":1464,"depth":812,"text":1465},{"id":1689,"depth":812,"text":1690},{"id":1728,"depth":812,"text":1729},{"id":2611,"depth":812,"text":3798},"Protecting a custom API with X-API-KEY",{"id":2924,"depth":812,"text":2925},{"id":3735,"depth":812,"text":3736},{"id":3761,"depth":812,"text":3762},"2026-05-01","A detailed guide to the IAM API token subsystem, from verification and management to protecting custom APIs with Auth H3 Client.",null,"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1200&q=80",{},"---\ntitle: \"IAM API Tokens with Auth H3 Client: Secure M2M Access in Nuxt and Nitro\"\ndescription: \"A detailed guide to the IAM API token subsystem, from verification and management to protecting custom APIs with Auth H3 Client.\"\ntags:\n  - Security\n  - API Tokens\n  - Nuxt\nimage: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1200&q=80\"\nauthor: \"Sergio\"\nauthorImg: \"https:\u002F\u002Fgithub.com\u002FSergo706.png\"\nauthorGithub: \"https:\u002F\u002Fgithub.com\u002FSergo706\"\nauthorGithubUserName: \"Sergo706\"\nfeatured: false\ndate: 2026-05-01T10:00:00.000Z\nreadingTime: \"14 min read\"\n---\n\nMachine-to-machine authentication looks simple until you need to answer real\nquestions. How do you scope access, revoke a leaked key, rotate credentials,\nkeep raw secrets out of the database, and still give users a clean dashboard\nfor managing their integrations?\n\nThe IAM service solves that problem with a dedicated API token subsystem.\nPublic verification and authenticated management are two distinct surfaces with\ndifferent security requirements. The database never stores a raw secret. Only\nthe SHA-256 digest of each key is persisted. Privilege labels and optional IP restrictions\nlive at the row level, and the full model maps cleanly into Auth H3 Client for\nNuxt, Nitro, and plain H3 applications.\n\n::important\nThis article assumes you already have a running IAM service and a Nuxt or Nitro\napp configured with `@riavzon\u002Fauth-h3client`. If your app exposes\nmachine-to-machine routes protected with `defineAuthenticatePublicApi`, do not\nplace those routes behind the bundled Nuxt auth middleware. That middleware is\nfor browser session flows, while API-key routes need a path bypass.\n::\n\n---\n\n## What the subsystem exposes\n\nThe API token subsystem has two distinct surfaces. Public verification is the\nmachine-to-machine entry point. Management routes are session-authenticated\nbrowser flows for creating, listing, rotating, revoking, and updating tokens.\n\n| Surface | Method | Route | Purpose |\n|---|---|---|---|\n| Public verification | `GET` | `\u002Fapi\u002Fpublic\u002Fverify` | Verify a raw API token from `X-API-KEY` |\n| Token creation | `POST` | `\u002Fapi\u002Fmanage\u002Fnew-token` | Create a new API token |\n| Token inventory | `GET` | `\u002Fapi\u002Fmanage\u002Flist-metadata` | List the current user's valid tokens |\n| Revocation | `POST` | `\u002Fapi\u002Fmanage\u002Frevoke` | Invalidate a token |\n| Metadata | `POST` | `\u002Fapi\u002Fmanage\u002Fmetadata` | Return details for one token |\n| Rotation | `POST` | `\u002Fapi\u002Fmanage\u002Frotate` | Replace a token with a fresh raw key |\n| IP updates | `POST` | `\u002Fapi\u002Fmanage\u002Fip-restriction-update` | Change the stored IP allowlist |\n| Privilege updates | `POST` | `\u002Fapi\u002Fmanage\u002Fprivilege-update` | Change the stored privilege label |\n\nEvery token is scoped with one privilege label: `custom`, `demo`,\n`restricted`, `protected`, or `full`. The verification route checks the exact\nlabel you request, not a hierarchy. If your route requires `demo`, the token\nmust carry `demo`.\n\nThe five levels have no built-in ordering. `demo` does not imply access to\n`restricted` routes, and `restricted` does not include `protected`. Each token\ncarries exactly one label and that label is matched literally. `custom` is a\ncatch-all for use cases that do not map to the four named levels. You decide\nwhat `custom` means in your own application.\n\n---\n\n## How a token is shaped and stored\n\nThe raw token format is simple on the surface. Each key is created as\n`prefix_random_checksum`, where the checksum is the first eight hexadecimal\ncharacters of a SHA-256 digest of the random portion.\n\n```text [API token format]\nrpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80\n```\n\nThat checksum lets the IAM service reject malformed keys quickly before doing a\ndatabase lookup. The raw key is only returned once, at creation or rotation\ntime. After that, the database stores only the SHA-256 digest in the\n`api_tokens.api_token` column.\n\nThe subsystem also creates a separate `public_identifier`. This value is not a\ncredential. It exists so management actions can target the correct row without\nrelying on the raw token after it has been issued. In the direct IAM API, most\nmanagement actions require `tokenId`, `publicIdentifier`, and `name` together.\nAuth H3 Client deliberately hides `publicIdentifier` from browser code and\nresolves it on the server.\n\nThe stored row carries operational metadata too. The IAM service records the\ntoken owner, privilege label, creation time, expiration time, last-use time,\nusage count, validity flag, and optional IP restriction list. That gives you a\nreal credential inventory rather than an opaque secret store.\n\n---\n\n## How verification works\n\nThe public verification route is `GET \u002Fapi\u002Fpublic\u002Fverify`. It reads the raw\ntoken from the `X-API-KEY` header, reads the required privilege from the query\nstring, and validates the request IP so IP-restricted tokens can be enforced.\n\nInternally, verification follows a strict sequence. The IAM service validates\nthe checksum, hashes the raw key, looks up the hashed row where `valid = 1`,\nchecks the exact `privilege_type`, applies expiration rules, enforces any\nstored IP restrictions, and updates `usage_count` and `last_used` for\nsuccessful requests.\n\nFailed verification attempts are throttled aggressively. Any request with a\nmissing key, a malformed privilege value, an unresolvable IP address, or an\ninvalid token feeds directly into the IAM verification limiters. The limiter\ncounts against the caller's IP, so repeated failures eventually trigger a\npermanent ban at the gateway level. Successful requests can also be\nrate-limited if you enable consumption limiting on the IAM side.\n\nCall the route directly like this:\n\n```bash [Terminal]\ncurl \\\n  -H \"X-API-KEY: rpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80\" \\\n  \"http:\u002F\u002Flocalhost:10000\u002Fapi\u002Fpublic\u002Fverify?privilege=demo\"\n```\n\nOn success, the IAM service returns a compact metadata object. This is the same\nshape Auth H3 Client later exposes on `event.context.apiVerification`.\n\n```json [Verification response]\n{\n  \"ok\": true,\n  \"date\": \"2026-05-01T10:00:00.000Z\",\n  \"data\": {\n    \"name\": \"report-worker\",\n    \"tokenId\": 12,\n    \"userId\": 42,\n    \"createdAt\": \"2026-05-01T09:00:00.000Z\",\n    \"expiresAt\": \"2026-06-01T09:00:00.000Z\",\n    \"lastUsed\": \"2026-05-01T10:00:00.000Z\",\n    \"usageCount\": 8,\n    \"providedPrivilege\": \"demo\"\n  }\n}\n```\n\n---\n\n## How management works\n\nManagement routes are intentionally more demanding than public verification.\nThey sit behind `requireAccessToken`, `requireRefreshToken`, fingerprint\ncollection, active MFA checks, JWT protection, and for `POST` routes a JSON\ncontent-type check plus a 1 KB body limit.\n\nThat split is the right model for a real product. Verification is for services\ncalling your APIs. Management is for logged-in users who are creating and\nchanging credentials inside a dashboard.\n\nHere is the direct IAM management map:\n\n| Action | Method | Input | Result |\n|---|---|---|---|\n| `new-token` | `POST` | `name`, `prefix`, `expires?`, `ipv4?`, `privilege` | New raw token and public identifier |\n| `list-metadata` | `GET` | None | All valid tokens for the authenticated user |\n| `revoke` | `POST` | `tokenId`, `publicIdentifier`, `name` | Invalidates the token |\n| `metadata` | `POST` | `tokenId`, `publicIdentifier`, `name` | Returns one token plus total counts |\n| `rotate` | `POST` | `tokenId`, `publicIdentifier`, `name` | Returns a replacement raw token |\n| `ip-restriction-update` | `POST` | `tokenId`, `publicIdentifier`, `name`, `ipv4?` | Replaces the stored IP allowlist |\n| `privilege-update` | `POST` | `tokenId`, `publicIdentifier`, `name`, `newPrivilege` | Replaces the stored privilege label |\n\nRotation deserves special attention. The IAM service revokes the current token\nand creates a fresh raw token in one management flow. That means the caller can\nroll credentials forward without deleting the integration entirely. Creation and\nrotation are the only two moments when the raw secret leaves the server.\n\n::note\nThe direct IAM create route returns both `rawApiKey` and `rawPublicId`. Auth H3\nClient strips `rawPublicId` before exposing the creation result to your Nuxt\nhandler. That keeps management identity in the server layer instead of the\nbrowser.\n::\n\n---\n\n## Why Auth H3 Client fits this subsystem well\n\nAuth H3 Client does more than proxy requests. It gives the IAM token model the\nright shape for H3 and Nuxt applications. Public machine-to-machine routes use\n`defineAuthenticatePublicApi`. Authenticated dashboard routes use\n`defineApiManagementHandler`. Token inventory reads can use\n`getApiListsController`.\n\nThat split matters because the browser and service callers have different\nsecurity needs. Browser management routes need session auth, CSRF protection,\nand token identity mapping. Machine-to-machine verification needs a single\n`X-API-KEY` header and a clean path to the IAM verification endpoint.\n\nThe wrappers also hide low-level IAM details from your application code. Your\nNuxt route only deals with `tokenId` for existing-token actions, while the\nserver wrapper resolves `publicIdentifier` and `name` through IAM\n`\u002Fapi\u002Fmanage\u002Flist-metadata` before making the final management request.\n\n---\n\n## Integrating with the Nuxt module\n\nIf your application only uses browser auth flows, the Nuxt module can run with\n`enableMiddleware: true` and register the built-in middleware for every\nrequest. Mixed apps need a different setup. If you protect custom APIs with\n`defineAuthenticatePublicApi`, disable the bundled middleware and add your own\npath-aware middleware so browser auth routes still get bot detection and CSRF,\nwhile machine-to-machine routes bypass that chain.\n\nStart by registering the module and disabling the bundled middleware.\n\n```ts [nuxt.config.ts]\nexport default defineNuxtConfig({\n  modules: ['auth-h3client\u002Fmodule'],\n  authH3Client: {\n    enableMiddleware: false,\n    authStatusUrl: '\u002Fapi\u002Fauth\u002Fusers\u002FauthStatus'\n  }\n})\n```\n\nThe module still gives you server auto-imports and client composables in this\nmode. It does not auto-register the bundled middleware, but it still registers\nthe auth status route and, when configured, the optional API token list route.\n\nConfigure the gateway in a Nitro plugin:\n\n```ts [server\u002Fplugins\u002Fauth.ts]\nimport { defineNitroPlugin } from 'nitropack\u002Fruntime'\nimport { useStorage } from 'nitropack\u002Fruntime\u002Fstorage'\nimport { configDefaults } from 'auth-h3client\u002Fserver\u002Ftemplates'\nimport { defineAuthConfiguration } from 'auth-h3client\u002Fv1'\n\nexport default defineNitroPlugin((nitroApp) => {\n  defineAuthConfiguration(nitroApp, {\n    ...configDefaults,\n    onSuccessRedirect: '\u002Fdashboard',\n    enableFireWallBans: false,\n    uStorage: {\n      storage: useStorage('cache'),\n      cacheOptions: {\n        successTtl: 60 * 60 * 24 * 30,\n        rateLimitTtl: 10\n      }\n    }\n  })\n})\n```\n\nNext, register a browser middleware that mirrors the packaged middleware but\nskips your machine-to-machine prefix.\n\n```ts [server\u002Fmiddleware\u002Fauth-browser.ts]\nimport {\n  defineEventHandler,\n  getHeader,\n  getRequestURL,\n  isMethod,\n  sendNoContent,\n} from 'auth-h3client\u002Fv1'\nimport {\n  botDetectorMiddleware,\n  generateCsrfCookie,\n  isIPValid,\n} from 'auth-h3client\u002Fv1'\n\nexport default defineEventHandler(async (event) => {\n  const { pathname } = getRequestURL(event)\n\n  if (\n    isMethod(event, 'HEAD') ||\n    pathname === '\u002Fapi\u002Fhealth' ||\n    pathname.startsWith('\u002Fapi\u002F_mdc') ||\n    pathname.startsWith('\u002F_nuxt') ||\n    pathname.startsWith('\u002Fapi\u002Fpublic\u002F')\n  ) {\n    if (isMethod(event, 'HEAD') || pathname === '\u002Fapi\u002Fhealth') {\n      sendNoContent(event)\n    }\n\n    return\n  }\n\n  const forwardedFor = getHeader(event, 'x-forwarded-for')\n  if (forwardedFor === '127.0.0.1' || forwardedFor === '::1') {\n    return\n  }\n\n  isIPValid(event)\n  await botDetectorMiddleware(event)\n  generateCsrfCookie(event)\n})\n```\n\n::tip\nIf your app does not expose machine-to-machine API-key routes, keep\n`enableMiddleware: true` and optionally use `registerApiRoute` on the module.\nThat is the simplest browser-only setup.\n::\n\n---\n\n## Protecting a custom API with `X-API-KEY`\n\nOnce the gateway is configured, protecting a custom API route is small and\npredictable. `defineAuthenticatePublicApi` reads the incoming `X-API-KEY`,\ncalls IAM `\u002Fapi\u002Fpublic\u002Fverify?privilege=...`, and places the verification\nresult on `event.context.apiVerification` before your handler runs.\n\nHere is a custom reports endpoint that grants access to tokens with the exact\n`demo` privilege label:\n\n```ts [server\u002Fapi\u002Fpublic\u002Freports.get.ts]\nimport { defineAuthenticatePublicApi } from 'auth-h3client\u002Fv1'\n\nexport default defineAuthenticatePublicApi(async (event) => {\n  const token = event.context.apiVerification\n\n  return {\n    ok: true,\n    consumer: token.name,\n    tokenId: token.tokenId,\n    userId: token.userId,\n    privilege: token.providedPrivilege,\n    report: {\n      generatedAt: new Date().toISOString(),\n      items: ['orders', 'revenue', 'retention']\n    }\n  }\n}, 'demo')\n```\n\nCall the route from another service like this:\n\n```bash [Terminal]\ncurl \\\n  -H \"X-API-KEY: rpt_d2f460c847aca70d00766922991aa073210fc107de5b251669f9b94ffa9d30e7122549a9b2d94be78a0b801629036a5f0aea8d82a12cd565044c39aa6608a36a_af609e80\" \\\n  \"http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fpublic\u002Freports\"\n```\n\nThis pattern is the cleanest way to grant access to a custom API. Your Nuxt\nserver owns the route contract and decides what the response shape looks like.\nThe IAM service owns credential validity. It checks the privilege label,\nenforces IP restrictions, updates the usage record, and applies abuse controls\nbefore your handler is ever reached.\n\n---\n\n## Let logged-in users create and rotate tokens\n\nThe other half of the story is dashboard management. That is where\n`defineApiManagementHandler` fits. The wrapper enforces session authentication\nand CSRF verification before it processes any action. It requires a `POST`\nmethod and applies a 2 KB JSON body limit. Token identity resolution happens\non the server, so the browser only needs to submit a `tokenId` for any\nexisting-token action.\n\nCreate one route file and branch on `event.context.params.action`:\n\n```ts [server\u002Fapi\u002Fauth\u002Fapi-tokens\u002F[action].post.ts]\nimport { defineApiManagementHandler } from 'auth-h3client\u002Fv1'\n\nexport default defineApiManagementHandler(async (event) => {\n  const action = event.context.params?.action\n\n  if (action === 'new-token') {\n    return { ok: true, data: event.context.newApiToken }\n  }\n\n  if (action === 'metadata') {\n    return { ok: true, data: event.context.extensiveMetadata }\n  }\n\n  if (action === 'rotate') {\n    return { ok: true, data: event.context.rotate }\n  }\n\n  if (action === 'revoke') {\n    return { ok: true, data: event.context.revoke }\n  }\n\n  return {\n    ok: true,\n    data: event.context.ipRestrictionUpdate ?? event.context.privilegeUpdate\n  }\n}, 'demo', 'protected')\n```\n\nIn this example, newly created tokens are always issued with the `demo`\nprivilege. The optional third argument allows `privilege-update` to move a\ntoken to `protected`. If you omit that third argument, the wrapper rejects the\nprivilege update action.\n\nFrom the browser, call that route with `executeRequest`. The helper injects the\nCSRF header on the client and forwards cookies correctly during SSR.\n\n```ts [app\u002Fcomposables\u002FuseApiTokens.ts]\nimport { executeRequest } from 'auth-h3client\u002Fclient'\n\nexport async function createDemoToken() {\n  return await executeRequest\u003C{\n    rawApiKey?: string\n    expiresAt?: string | null\n  }>('\u002Fapi\u002Fauth\u002Fapi-tokens\u002Fnew-token', 'POST', {\n    name: 'report-worker',\n    prefix: 'rpt',\n    expires: 1000 * 60 * 60 * 24 * 30,\n    ipv4: ['203.0.113.10']\n  })\n}\n\nexport async function rotateToken(tokenId: number) {\n  return await executeRequest\u003C{\n    msg: string\n    newRawToken?: string\n    newExpiry?: string | null\n  }>('\u002Fapi\u002Fauth\u002Fapi-tokens\u002Frotate', 'POST', {\n    tokenId\n  })\n}\n```\n\nNotice what the browser does not send for rotate. It only sends `tokenId`. The\nwrapper fetches the authenticated token inventory, resolves the matching\n`publicIdentifier` and `name`, and only then calls the IAM management endpoint.\nThat is one of the main reasons to use Auth H3 Client instead of calling the\nIAM management API directly from the browser.\n\nThe wrapper also gives you typed action results on the event context:\n\n| Action | Event context field |\n|---|---|\n| `new-token` | `event.context.newApiToken` |\n| `metadata` | `event.context.extensiveMetadata` |\n| `rotate` | `event.context.rotate` |\n| `revoke` | `event.context.revoke` |\n| `ip-restriction-update` | `event.context.ipRestrictionUpdate` |\n| `privilege-update` | `event.context.privilegeUpdate` |\n\n---\n\n## A complete flow for granting access to a custom API\n\nWith the pieces above, the end-to-end flow is straightforward. A logged-in user\ncreates a token from your dashboard. Your app stores only the raw token shown\nonce to the user. The external service then calls your Nuxt route with\n`X-API-KEY`, and the gateway verifies it against the IAM service before your\nhandler runs.\n\n`getApiListsController` gives the browser a clean inventory view for active\ntokens. It proxies to IAM `\u002Fapi\u002Fmanage\u002Flist-metadata` and strips\n`public_identifier` before the response reaches the browser. That means the\nfrontend can render names, creation dates, expiry times, usage counts, and IP\nrestrictions without learning the internal management identifier.\n\nThis split gives you three strong properties at the same time. The custom API\nstays simple. The IAM service remains the only place that decides whether a key\nis valid. The browser never needs to hold server-side token identity data for\nmanagement actions.\n\n---\n\n## Summary\n\nEach\nkey is validated by checksum before a database lookup happens. The database\nholds only hashed values and never the raw secret. Privilege matching is exact,\nand IP restrictions are enforced at the verification layer before your handler\nsees the request. Every successful call updates the token's usage record, giving\nyou a real credential inventory rather than an opaque secret store. Rotation and\nrevocation are first-class operations built into the same management surface\nthat creates tokens.\n\nAuth H3 Client is the layer that makes that subsystem practical in Nuxt and\nNitro. Machine-to-machine verification gets a dedicated wrapper that calls IAM\nand places results on the event context. Browser management routes get a\nseparate wrapper that handles session auth and CSRF checks, then resolves token\nidentity on the server so the browser never holds internal IAM references. The\ninventory controller strips `public_identifier` from list responses so the\nfrontend can render names, dates, expiry times, and usage counts without\nlearning the server-side management identifier.\n\nIf you are building a mixed app with both browser auth and API-key protected\ncustom APIs, the right pattern is clear. Keep browser middleware on browser\nroutes. Bypass that middleware for `defineAuthenticatePublicApi` routes. Let\nthe IAM service own credential validity and token lifecycle decisions.\n\n::read-more{to=\"\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\"}\n::\n\n::read-more{to=\"\u002Fdocs\u002Fauth-h3client\u002Fgetting-started\u002Fnuxt\"}\n::\n\n::read-more{to=\"\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fmiddleware\"}\n::","14 min read",{"title":69,"description":3803},[38,445,3811],"Nuxt","xUyiAnoFz2Pou6JciwM8wXFg6eZEmb_ukv_sIvGjdic",{"id":3814,"title":3815,"author":823,"authorGithub":824,"authorGithubUserName":825,"authorImg":826,"body":3816,"date":4928,"description":4929,"extension":815,"featured":53,"icon":3804,"image":4930,"meta":4931,"navigation":53,"path":4932,"rawbody":4933,"readingTime":4934,"seo":4935,"stem":4936,"tags":4937,"__hash__":4938},"blog\u002Fblog\u002Fhow-token-rotation-works.md","How Token Rotation Works: Access Tokens, Refresh Tokens, and the Deduplication Problem",{"type":808,"value":3817,"toc":4911},[3818,3821,3824,3826,3830,3833,3845,3863,3988,3997,4004,4006,4010,4013,4016,4022,4087,4090,4092,4096,4102,4105,4186,4198,4207,4209,4213,4216,4228,4343,4350,4421,4423,4427,4430,4433,4446,4449,4463,4538,4541,4544,4546,4550,4553,4558,4568,4583,4588,4604,4607,4609,4613,4616,4639,4657,4666,4672,4675,4677,4681,4691,4700,4768,4778,4872,4875,4877,4879,4882,4885,4891,4894,4902,4908],[830,3819,3820],{},"Most authentication systems issue a single credential — a session ID, a JWT, a cookie — and use it until it expires or the user logs out. The problem with that model is straightforward: if an attacker obtains that credential, they have as long as it lives to use it. The longer it lives, the bigger the exposure window.",[830,3822,3823],{},"Riavzon solves this with a dual-token architecture. Access tokens are short-lived and verified cryptographically. Refresh tokens are long-lived but stored as hashes in a database, consumed atomically, and wrapped in a reuse detection system that revokes every session the moment replay is detected. This post explains every layer of that architecture: why it is designed this way, how each piece works, and what happens when two requests from the same user arrive at the same time.",[852,3825],{},[855,3827,3829],{"id":3828},"the-two-token-model","The Two-Token Model",[830,3831,3832],{},"Every authenticated user in the system holds two credentials at once.",[830,3834,3835,3836,3840,3841,3844],{},"The ",[3837,3838,3839],"strong",{},"access token"," is a signed JWT. It lives in a ",[843,3842,3843],{},"__Secure-a"," cookie on the browser. Its lifetime is short — typically 15 minutes — and it is verified on every request without touching the database. The IAM service uses an LRU cache to hold every valid token, so verification is a cache lookup plus a cryptographic check, not a database query. When the token expires, the cache entry is evicted and the next verification call fails immediately.",[830,3846,3835,3847,3850,3851,3854,3855,3858,3859,3862],{},[3837,3848,3849],{},"refresh token"," is a 64-byte cryptographically random string, hex encoded. The browser holds the raw token in an ",[843,3852,3853],{},"httpOnly"," cookie named ",[843,3856,3857],{},"session",". The server never stores the raw token. Instead, it hashes it with SHA-256 and stores the hash in a MySQL ",[843,3860,3861],{},"refresh_tokens"," table. The raw token leaves the server exactly once, when it is issued, and the server never sees it again in plaintext.",[1089,3864,3866],{"className":1230,"code":3865,"language":5,"meta":811,"style":811},"{\n  \"visitor\": \"vis_abc123\",\n  \"roles\": [\"user\"],\n  \"sub\": \"42\",\n  \"jti\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"iat\": 1710000000,\n  \"exp\": 1710000900\n}\n",[843,3867,3868,3872,3892,3914,3934,3954,3970,3984],{"__ignoreMap":811},[1179,3869,3870],{"class":1181,"line":1182},[1179,3871,1240],{"class":1239},[1179,3873,3874,3876,3879,3881,3883,3885,3888,3890],{"class":1181,"line":812},[1179,3875,1215],{"class":1245},[1179,3877,3878],{"class":1248},"visitor",[1179,3880,1207],{"class":1245},[1179,3882,1255],{"class":1254},[1179,3884,1200],{"class":1199},[1179,3886,3887],{"class":1203},"vis_abc123",[1179,3889,1207],{"class":1199},[1179,3891,1036],{"class":1239},[1179,3893,3894,3896,3899,3901,3903,3905,3907,3910,3912],{"class":1181,"line":1212},[1179,3895,1215],{"class":1245},[1179,3897,3898],{"class":1248},"roles",[1179,3900,1207],{"class":1245},[1179,3902,1255],{"class":1254},[1179,3904,1777],{"class":1239},[1179,3906,1207],{"class":1199},[1179,3908,3909],{"class":1203},"user",[1179,3911,1207],{"class":1199},[1179,3913,1788],{"class":1239},[1179,3915,3916,3918,3921,3923,3925,3927,3930,3932],{"class":1181,"line":1283},[1179,3917,1215],{"class":1245},[1179,3919,3920],{"class":1248},"sub",[1179,3922,1207],{"class":1245},[1179,3924,1255],{"class":1254},[1179,3926,1200],{"class":1199},[1179,3928,3929],{"class":1203},"42",[1179,3931,1207],{"class":1199},[1179,3933,1036],{"class":1239},[1179,3935,3936,3938,3941,3943,3945,3947,3950,3952],{"class":1181,"line":1298},[1179,3937,1215],{"class":1245},[1179,3939,3940],{"class":1248},"jti",[1179,3942,1207],{"class":1245},[1179,3944,1255],{"class":1254},[1179,3946,1200],{"class":1199},[1179,3948,3949],{"class":1203},"550e8400-e29b-41d4-a716-446655440000",[1179,3951,1207],{"class":1199},[1179,3953,1036],{"class":1239},[1179,3955,3956,3958,3961,3963,3965,3968],{"class":1181,"line":1319},[1179,3957,1215],{"class":1245},[1179,3959,3960],{"class":1248},"iat",[1179,3962,1207],{"class":1245},[1179,3964,1255],{"class":1254},[1179,3966,3967],{"class":1330}," 1710000000",[1179,3969,1036],{"class":1239},[1179,3971,3972,3974,3977,3979,3981],{"class":1181,"line":1336},[1179,3973,1215],{"class":1245},[1179,3975,3976],{"class":1248},"exp",[1179,3978,1207],{"class":1245},[1179,3980,1255],{"class":1254},[1179,3982,3983],{"class":1330}," 1710000900\n",[1179,3985,3986],{"class":1181,"line":1353},[1179,3987,1459],{"class":1239},[830,3989,3990,3991,3993,3994,3996],{},"That is a typical access token payload. The ",[843,3992,3940],{}," is a UUID generated fresh on every issuance. It is also the key by which the token lives in the LRU cache. Deleting the cache entry for a ",[843,3995,3940],{}," revokes that token immediately, without a database write, without waiting for expiry.",[830,3998,3999,4000,4003],{},"The canary cookie — ",[843,4001,4002],{},"canary_id"," — ties the session to a specific device fingerprint. It is issued by the Bot Detector middleware and is required alongside both tokens for any sensitive operation. It is neither a credential nor an authentication factor on its own, but it binds the token family to the visitor context that created it, and any mismatch triggers anomaly detection.",[852,4005],{},[855,4007,4009],{"id":4008},"why-short-lived-access-tokens","Why Short-Lived Access Tokens",[830,4011,4012],{},"The conventional objection to short-lived tokens is the extra network round trips. If the token expires every 15 minutes, the user's browser needs to refresh it every 15 minutes. That cost is real, but the security benefit justifies it.",[830,4014,4015],{},"An access token that lives for 15 minutes and is stolen gives an attacker a 15-minute window. An access token that lives for 24 hours gives an attacker 24 hours. In practice, the difference between these windows matters enormously when you consider how often stolen credentials go undetected. The 15-minute window usually closes before the attacker can do meaningful damage. The 24-hour window rarely does.",[830,4017,4018,4019,4021],{},"More importantly, the LRU cache is the real enforcement boundary. An access token is not just valid because it carries the right signature. It is valid because it exists in the cache. This means revocation is instant and free. Deleting the cache entry with the token's ",[843,4020,3940],{}," terminates that token immediately, regardless of how long it has until expiry. Sessions can be force-terminated without a database write, without blocking, and without any propagation delay.",[1089,4023,4025],{"className":1745,"code":4024,"language":1748,"meta":811,"style":811},"import { tokenCache } from '@riavzon\u002Fauth'\n\nconst cache = tokenCache()\ncache.delete(rawToken) \u002F\u002F This token is now invalid. No database write needed.\n",[843,4026,4027,4047,4051,4067],{"__ignoreMap":811},[1179,4028,4029,4031,4033,4036,4038,4040,4042,4045],{"class":1181,"line":1182},[1179,4030,1851],{"class":1755},[1179,4032,1854],{"class":1239},[1179,4034,4035],{"class":1770},"tokenCache",[1179,4037,1860],{"class":1239},[1179,4039,1863],{"class":1755},[1179,4041,1819],{"class":1199},[1179,4043,4044],{"class":1203},"@riavzon\u002Fauth",[1179,4046,1825],{"class":1199},[1179,4048,4049],{"class":1181,"line":812},[1179,4050,1935],{"emptyLinePlaceholder":8},[1179,4052,4053,4056,4059,4061,4064],{"class":1181,"line":1212},[1179,4054,4055],{"class":1957},"const",[1179,4057,4058],{"class":2252}," cache",[1179,4060,2483],{"class":1254},[1179,4062,4063],{"class":1185}," tokenCache",[1179,4065,4066],{"class":1239},"()\n",[1179,4068,4069,4071,4073,4076,4078,4081,4083],{"class":1181,"line":1283},[1179,4070,2035],{"class":1770},[1179,4072,1053],{"class":1239},[1179,4074,4075],{"class":1185},"delete",[1179,4077,1968],{"class":1239},[1179,4079,4080],{"class":1770},"rawToken",[1179,4082,1954],{"class":1239},[1179,4084,4086],{"class":4085},"sghk6","\u002F\u002F This token is now invalid. No database write needed.\n",[830,4088,4089],{},"The two-gate verification model — cache check first, cryptographic check second — also means the cryptographic work only happens when the cache says the token could be valid. Revoked tokens fail at the first gate, before any cryptographic computation runs.",[852,4091],{},[855,4093,4095],{"id":4094},"why-hashed-refresh-tokens-in-the-database","Why Hashed Refresh Tokens in the Database",[830,4097,4098,4099,4101],{},"Long-lived tokens stored in plaintext are a liability. If the database is compromised, every session is compromised. Hashing the token before storing it breaks that link. An attacker with a dump of the ",[843,4100,3861],{}," table gets SHA-256 hashes — not the raw tokens they need to authenticate.",[830,4103,4104],{},"The storage schema for a refresh token row looks like this:",[863,4106,4107,4117],{},[866,4108,4109],{},[869,4110,4111,4114],{},[872,4112,4113],{},"Column",[872,4115,4116],{},"Value",[885,4118,4119,4132,4149,4163,4173],{},[869,4120,4121,4126],{},[890,4122,4123],{},[843,4124,4125],{},"token",[890,4127,4128,4131],{},[843,4129,4130],{},"sha256(rawToken)"," — never the raw value",[869,4133,4134,4139],{},[890,4135,4136],{},[843,4137,4138],{},"valid",[890,4140,4141,4144,4145,4148],{},[843,4142,4143],{},"1"," when active, ",[843,4146,4147],{},"0"," when revoked",[869,4150,4151,4155],{},[890,4152,4153],{},[843,4154,1158],{},[890,4156,4157,4159,4160,4162],{},[843,4158,4147],{}," when fresh, ",[843,4161,4143],{}," after first consumption",[869,4164,4165,4170],{},[890,4166,4167],{},[843,4168,4169],{},"session_started_at",[890,4171,4172],{},"Timestamp from the original login, carried across all rotations",[869,4174,4175,4179],{},[890,4176,4177],{},[843,4178,1379],{},[890,4180,4181,4182,4185],{},"Computed from ",[843,4183,4184],{},"refresh_ttl"," at insert time",[830,4187,3835,4188,4190,4191,4193,4194,4197],{},[843,4189,1158],{}," column is the core of the reuse detection system. It starts at zero. The moment the token is consumed — used to issue a new token pair — the database atomically sets it to ",[843,4192,4143],{},". Any second attempt to consume a token with ",[843,4195,4196],{},"usage_count > 0"," is treated as a replay attack, and all sessions for that user are immediately revoked.",[830,4199,3835,4200,4202,4203,4206],{},[843,4201,4169],{}," column persists the original login timestamp across every rotation. No matter how many times the token is rotated, the session chain traces back to the original authentication event. This is how ",[843,4204,4205],{},"MAX_SESSION_LIFE"," works: the system knows when the session began and can enforce an absolute ceiling on how long any session can live, regardless of how often it is refreshed.",[852,4208],{},[855,4210,4212],{"id":4211},"the-rotation-lifecycle","The Rotation Lifecycle",[830,4214,4215],{},"Rotation is the process that converts old credentials into new ones. It is the most security-sensitive operation in the system, and it runs in a strict sequence.",[830,4217,4218,4219,4222,4223,1159,4225,4227],{},"When the access token is about to expire, Auth H3 Client calls ",[843,4220,4221],{},"POST \u002Fauth\u002Fuser\u002Frefresh-session"," on the IAM service with the ",[843,4224,3857],{},[843,4226,4002],{}," cookies. The IAM rotation controller runs this sequence:",[4229,4230,4231,4236,4243,4247,4274,4278,4298,4304,4308,4321,4325],"steps",{},[4232,4233,4235],"h3",{"id":4234},"rate-limiting","Rate limiting",[830,4237,4238,4239,4242],{},"Three layered rate limiters run first: an IP limiter, a token-hash limiter, and a composite ",[843,4240,4241],{},"ip_tokenhash"," limiter. Each uses consecutive caches that escalate block duration on repeated violations. Brute force attempts are stopped before anything else runs.",[4232,4244,4246],{"id":4245},"anomaly-detection","Anomaly detection",[830,4248,4249,4252,4253,4255,4256,4259,4260,4263,4264,4266,4267,4270,4271,1053],{},[843,4250,4251],{},"strangeThings()"," runs nine sequential checks against the session. It verifies the ",[843,4254,4002],{}," binding, checks IP range consistency against historical records, compares the ",[843,4257,4258],{},"User-Agent"," fingerprint, validates that the session has not exceeded ",[843,4261,4262],{},"maxAllowedSessionsPerUser",", and checks that the token has not already been consumed (",[843,4265,4196],{},"). The first check that fails short-circuits the rest. If anomalies are recoverable, the service sends an MFA email and returns ",[843,4268,4269],{},"202",". If they are not recoverable, the token is revoked and the service returns ",[843,4272,4273],{},"401",[4232,4275,4277],{"id":4276},"atomic-consumption","Atomic consumption",[830,4279,4280,4283,4284,4287,4288,4290,4291,4293,4294,4297],{},[843,4281,4282],{},"consumeAndVerifyRefreshToken"," runs a single atomic ",[843,4285,4286],{},"UPDATE"," inside a transaction. It increments ",[843,4289,1158],{}," by one, but only if the row exists, is ",[843,4292,1150],{},", has ",[843,4295,4296],{},"usage_count = 0",", and has not expired. All four conditions must pass in the same transaction. If even one fails, no rows are affected.",[830,4299,4300,4301,4303],{},"If no rows are affected, the function investigates: the token might not exist, it might have been revoked, or it might have ",[843,4302,4196],{}," from a previous consumption. That last case is a reuse detection trigger — all sessions for the user are revoked immediately.",[4232,4305,4307],{"id":4306},"session-lifetime-check","Session lifetime check",[830,4309,4310,4311,4313,4314,4316,4317,4320],{},"If the token consumed successfully but ",[843,4312,4169],{}," is older than ",[843,4315,4205],{},", the controller revokes the token and returns ",[843,4318,4319],{},"401 Session is expired",". The session chain has lived as long as policy allows.",[4232,4322,4324],{"id":4323},"new-credential-issuance","New credential issuance",[830,4326,4327,4328,4331,4332,1032,4334,4336,4337,4339,4340,4342],{},"The old token is set to ",[843,4329,4330],{},"valid = 0",". A new 64-byte random refresh token is generated, hashed, and inserted with ",[843,4333,1150],{},[843,4335,4296],{},", and the same ",[843,4338,4169],{}," from the consumed token. A new access token is signed with a fresh ",[843,4341,3940],{}," and cached. Both are sent to the browser.",[830,4344,4345,4346,4349],{},"The success response carries the new access token in the body. The new refresh token arrives in the ",[843,4347,4348],{},"Set-Cookie"," header. The browser replaces its cookies transparently.",[1089,4351,4353],{"className":1230,"code":4352,"language":5,"meta":811,"style":811},"{\n  \"message\": \"Refresh & access tokens rotated\",\n  \"accessToken\": \"\u003Csigned jwt>\",\n  \"accessIat\": \"1710000000000\"\n}\n",[843,4354,4355,4359,4379,4399,4417],{"__ignoreMap":811},[1179,4356,4357],{"class":1181,"line":1182},[1179,4358,1240],{"class":1239},[1179,4360,4361,4363,4366,4368,4370,4372,4375,4377],{"class":1181,"line":812},[1179,4362,1215],{"class":1245},[1179,4364,4365],{"class":1248},"message",[1179,4367,1207],{"class":1245},[1179,4369,1255],{"class":1254},[1179,4371,1200],{"class":1199},[1179,4373,4374],{"class":1203},"Refresh & access tokens rotated",[1179,4376,1207],{"class":1199},[1179,4378,1036],{"class":1239},[1179,4380,4381,4383,4386,4388,4390,4392,4395,4397],{"class":1181,"line":1212},[1179,4382,1215],{"class":1245},[1179,4384,4385],{"class":1248},"accessToken",[1179,4387,1207],{"class":1245},[1179,4389,1255],{"class":1254},[1179,4391,1200],{"class":1199},[1179,4393,4394],{"class":1203},"\u003Csigned jwt>",[1179,4396,1207],{"class":1199},[1179,4398,1036],{"class":1239},[1179,4400,4401,4403,4406,4408,4410,4412,4415],{"class":1181,"line":1283},[1179,4402,1215],{"class":1245},[1179,4404,4405],{"class":1248},"accessIat",[1179,4407,1207],{"class":1245},[1179,4409,1255],{"class":1254},[1179,4411,1200],{"class":1199},[1179,4413,4414],{"class":1203},"1710000000000",[1179,4416,1221],{"class":1199},[1179,4418,4419],{"class":1181,"line":1298},[1179,4420,1459],{"class":1239},[852,4422],{},[855,4424,4426],{"id":4425},"the-deduplication-problem","The Deduplication Problem",[830,4428,4429],{},"Single-page applications create a problem that most token rotation systems ignore: concurrent requests.",[830,4431,4432],{},"Consider a user whose access token has just expired. Their browser has three in-flight requests — a profile fetch, a feed load, and a notification count. All three arrive at the server at the same moment. All three see an expired access token. All three decide to rotate.",[830,4434,4435,4436,4438,4439,4442,4443,4445],{},"Without deduplication, all three would call ",[843,4437,4221],{}," simultaneously. The first call consumes the refresh token (sets ",[843,4440,4441],{},"usage_count = 1","). The second call tries to consume the same token and finds ",[843,4444,4196],{},". The reuse detection system interprets this as a replay attack and revokes all sessions. The user is logged out, and they did nothing wrong.",[830,4447,4448],{},"This is not a theoretical edge case. It happens on any page with multiple parallel API calls, and it happens reliably whenever token expiry falls at a high-traffic moment.",[830,4450,4451,4452,4455,4456,4459,4460,4462],{},"Auth H3 Client solves this with ",[843,4453,4454],{},"lockAsyncAction",", a keyed async mutex. When ",[843,4457,4458],{},"ensureValidCredentials"," needs to rotate, it acquires a lock keyed on the refresh token value — the ",[843,4461,3857],{}," cookie — before making any call to the IAM service. A second request for the same session finds the lock held, waits for the first call to complete, and reuses its result.",[1089,4464,4466],{"className":1745,"code":4465,"language":1748,"meta":811,"style":811},"\u002F\u002F Inside ensureValidCredentials — simplified view\nconst result = await lockAsyncAction(refreshToken, async () => {\n  \u002F\u002F Only one call per session cookie value runs at a time.\n  \u002F\u002F All others wait here and get the same result.\n  return await callIAMRotation(sessionCookie, canaryCookie)\n})\n",[843,4467,4468,4473,4503,4508,4513,4534],{"__ignoreMap":811},[1179,4469,4470],{"class":1181,"line":1182},[1179,4471,4472],{"class":4085},"\u002F\u002F Inside ensureValidCredentials — simplified view\n",[1179,4474,4475,4477,4480,4482,4484,4487,4489,4492,4494,4496,4499,4501],{"class":1181,"line":812},[1179,4476,4055],{"class":1957},[1179,4478,4479],{"class":2252}," result",[1179,4481,2483],{"class":1254},[1179,4483,3395],{"class":1755},[1179,4485,4486],{"class":1185}," lockAsyncAction",[1179,4488,1968],{"class":1239},[1179,4490,4491],{"class":1770},"refreshToken",[1179,4493,1032],{"class":1239},[1179,4495,2230],{"class":1957},[1179,4497,4498],{"class":1239}," () ",[1179,4500,1958],{"class":1957},[1179,4502,1295],{"class":1239},[1179,4504,4505],{"class":1181,"line":1212},[1179,4506,4507],{"class":4085},"  \u002F\u002F Only one call per session cookie value runs at a time.\n",[1179,4509,4510],{"class":1181,"line":1283},[1179,4511,4512],{"class":4085},"  \u002F\u002F All others wait here and get the same result.\n",[1179,4514,4515,4517,4519,4522,4524,4527,4529,4532],{"class":1181,"line":1298},[1179,4516,2718],{"class":1755},[1179,4518,3395],{"class":1755},[1179,4520,4521],{"class":1185}," callIAMRotation",[1179,4523,1968],{"class":1239},[1179,4525,4526],{"class":1770},"sessionCookie",[1179,4528,1032],{"class":1239},[1179,4530,4531],{"class":1770},"canaryCookie",[1179,4533,2268],{"class":1239},[1179,4535,4536],{"class":1181,"line":1319},[1179,4537,1834],{"class":1239},[830,4539,4540],{},"The lock is keyed on the refresh token value itself, not on a user ID or session ID. This matters because a user might have multiple active sessions across devices. Each session has its own refresh token, so each gets its own independent lock. Concurrent rotation on one device does not block rotation on another.",[830,4542,4543],{},"The result is cached briefly after the lock releases. Requests that arrive after the first call completes but before the lock is fully released still get the cached result without making another call. This covers the common case where a burst of requests resolves in quick succession rather than simultaneously.",[852,4545],{},[855,4547,4549],{"id":4548},"reuse-detection-in-depth","Reuse Detection in Depth",[830,4551,4552],{},"The reuse detection system operates on a core assumption: a refresh token should only ever be consumed once. Any second consumption means either the token was stolen and replayed, or something in the rotation flow went wrong. Either way, the safest response is to terminate all sessions for that user immediately.",[830,4554,4555],{},[3837,4556,4557],{},"Scenario 1: An attacker steals a valid, unconsumed refresh token.",[830,4559,4560,4561,4563,4564,4567],{},"To use the stolen token, the attacker must also replicate the user's ",[843,4562,4002],{}," cookie and pass the fingerprint checks in ",[843,4565,4566],{},"strangeThings",". If the fingerprint does not match, the anomaly engine sends an MFA challenge to the real user's email before any rotation happens. The attacker cannot proceed without access to the user's email.",[830,4569,4570,4571,4573,4574,4576,4577,4579,4580,4582],{},"If the attacker somehow passes the fingerprint checks and consumes the token, ",[843,4572,1158],{}," becomes ",[843,4575,4143],{},". The next time the legitimate user's browser tries to rotate — which happens automatically as the access token approaches expiry — ",[843,4578,4282],{}," finds ",[843,4581,4196],{},". All sessions are revoked. Both the attacker and the legitimate user are forced to re-authenticate. The attacker cannot complete MFA without the user's email.",[830,4584,4585],{},[3837,4586,4587],{},"Scenario 2: An attacker steals a token that has already been rotated.",[830,4589,4590,4591,4593,4594,4596,4597,4599,4600,4603],{},"The stolen token has ",[843,4592,4441],{},". The attacker attempts to consume it. ",[843,4595,4282],{}," detects ",[843,4598,4196],{}," immediately, revokes all sessions for the user, and returns ",[843,4601,4602],{},"valid: false",". The attacker's attempt terminates the legitimate user's current session, but the attacker gains nothing — they still cannot authenticate.",[830,4605,4606],{},"In both scenarios, the worst outcome for the legitimate user is being forced to log in again and prove their identity through MFA. The attacker is locked out at every step.",[852,4608],{},[855,4610,4612],{"id":4611},"two-lifetime-controls","Two Lifetime Controls",[830,4614,4615],{},"Refresh tokens have two independent lifetime mechanisms, and understanding the difference between them matters.",[830,4617,4618,4624,4625,4627,4628,4631,4632,4634,4635,4638],{},[3837,4619,4620,4621,4623],{},"Token TTL (",[843,4622,4184],{},")"," controls how long a single refresh token row stays valid in the database. When a token expires, verification sets ",[843,4626,4330],{}," and clears ",[843,4629,4630],{},"last_mfa_at"," for the user. Clearing ",[843,4633,4630],{}," resets the ",[843,4636,4637],{},"byPassAnomaliesFor"," cooldown — the next session anomaly (if one occurs) will not be bypassed, though a clean login from the same device still passes without MFA.",[830,4640,4641,4646,4647,4649,4650,4653,4654,4656],{},[3837,4642,4643,4644,4623],{},"Session lifetime (",[843,4645,4205],{}," controls how long the entire session chain can survive. Every time a token is rotated, the new token inherits the same ",[843,4648,4169],{}," timestamp from the consumed token. That timestamp is the anchor. When ",[843,4651,4652],{},"Date.now() - session_started_at"," exceeds ",[843,4655,4205],{},", the rotation controller refuses to issue new credentials, even if the token itself has not expired yet.",[830,4658,4659,4660,4662,4663,4665],{},"The practical configuration is to set ",[843,4661,4184],{}," to something like 3 days and ",[843,4664,4205],{}," to 30 days. Individual tokens force periodic rotation and limit the exposure window of any single credential. The session ceiling prevents sessions from living indefinitely through continuous renewal.",[1089,4667,4670],{"className":4668,"code":4669,"language":1095},[1092],"refresh_ttl:      3 days    — Each token lives this long\nMAX_SESSION_LIFE: 30 days   — The session chain lives this long\n",[843,4671,4669],{"__ignoreMap":811},[830,4673,4674],{},"A \"remember me\" flow with a 3-day token TTL still expires the session completely after 30 days. The user must log in again, not just refresh.",[852,4676],{},[855,4678,4680],{"id":4679},"how-auth-h3-client-drives-this","How Auth H3 Client Drives This",[830,4682,4683,4684,4687,4688,4690],{},"Auth H3 Client, the gateway layer for Nuxt and Nitro applications, handles the entire rotation lifecycle transparently. Your application code never calls the rotation endpoint directly. Instead, every protected route wraps its handler in ",[843,4685,4686],{},"defineAuthenticatedEventHandler",", which calls ",[843,4689,4458],{}," before your code runs.",[830,4692,4693,4695,4696,4699],{},[843,4694,4458],{}," decides whether to rotate based on the metadata it receives from the IAM ",[843,4697,4698],{},"\u002Fsecret\u002Faccesstoken\u002Fmetadata"," endpoint. The decision logic covers every case:",[863,4701,4702,4711],{},[866,4703,4704],{},[869,4705,4706,4709],{},[872,4707,4708],{},"Metadata result",[872,4710,1493],{},[885,4712,4713,4721,4732,4742,4749,4760],{},[869,4714,4715,4718],{},[890,4716,4717],{},"No access token present",[890,4719,4720],{},"Rotate immediately",[869,4722,4723,4729],{},[890,4724,4725,4728],{},[843,4726,4727],{},"shouldRotate: true"," (within 25% of TTL)",[890,4730,4731],{},"Rotate proactively",[869,4733,4734,4739],{},[890,4735,4736],{},[843,4737,4738],{},"authorized: false",[890,4740,4741],{},"Rotate",[869,4743,4744,4747],{},[890,4745,4746],{},"Server error or no response",[890,4748,4741],{},[869,4750,4751,4757],{},[890,4752,4753,4756],{},[843,4754,4755],{},"mfa: true"," (IAM returned 202)",[890,4758,4759],{},"Return 202, do not rotate",[869,4761,4762,4765],{},[890,4763,4764],{},"Valid and within threshold",[890,4766,4767],{},"Set token on context, continue",[830,4769,4770,4771,4773,4774,4777],{},"The metadata response is cached in a ",[843,4772,713],{}," instance keyed by the access token value. The cache TTL is ",[843,4775,4776],{},"msUntilExp - refreshThreshold - 5 seconds",", so the cache expires just before the token would trigger a rotation check anyway. Requests within that window read the cached metadata without a network call.",[1089,4779,4781],{"className":1745,"code":4780,"language":1748,"meta":811,"style":811},"export default defineAuthenticatedEventHandler(async (event) => {\n  \u002F\u002F By the time this line runs:\n  \u002F\u002F - The access token has been verified or rotated\n  \u002F\u002F - New cookies have been applied to the response if rotation happened\n  \u002F\u002F - Concurrent requests from the same session were deduplicated\n  \u002F\u002F - event.context.authorizedData contains the verified session data\n  const { userId, roles } = event.context.authorizedData\n  return { userId }\n})\n",[843,4782,4783,4806,4811,4816,4821,4826,4831,4858,4868],{"__ignoreMap":811},[1179,4784,4785,4787,4789,4792,4794,4796,4798,4800,4802,4804],{"class":1181,"line":1182},[1179,4786,1756],{"class":1755},[1179,4788,1759],{"class":1755},[1179,4790,4791],{"class":1185}," defineAuthenticatedEventHandler",[1179,4793,1968],{"class":1239},[1179,4795,2230],{"class":1957},[1179,4797,2233],{"class":1239},[1179,4799,2236],{"class":1950},[1179,4801,1954],{"class":1239},[1179,4803,1958],{"class":1957},[1179,4805,1295],{"class":1239},[1179,4807,4808],{"class":1181,"line":812},[1179,4809,4810],{"class":4085},"  \u002F\u002F By the time this line runs:\n",[1179,4812,4813],{"class":1181,"line":1212},[1179,4814,4815],{"class":4085},"  \u002F\u002F - The access token has been verified or rotated\n",[1179,4817,4818],{"class":1181,"line":1283},[1179,4819,4820],{"class":4085},"  \u002F\u002F - New cookies have been applied to the response if rotation happened\n",[1179,4822,4823],{"class":1181,"line":1298},[1179,4824,4825],{"class":4085},"  \u002F\u002F - Concurrent requests from the same session were deduplicated\n",[1179,4827,4828],{"class":1181,"line":1319},[1179,4829,4830],{"class":4085},"  \u002F\u002F - event.context.authorizedData contains the verified session data\n",[1179,4832,4833,4835,4837,4839,4841,4843,4845,4847,4849,4851,4853,4855],{"class":1181,"line":1336},[1179,4834,2247],{"class":1957},[1179,4836,1854],{"class":1239},[1179,4838,1341],{"class":2252},[1179,4840,1032],{"class":1239},[1179,4842,3898],{"class":2252},[1179,4844,1860],{"class":1239},[1179,4846,2258],{"class":1254},[1179,4848,2699],{"class":1770},[1179,4850,1053],{"class":1239},[1179,4852,2704],{"class":1770},[1179,4854,1053],{"class":1239},[1179,4856,4857],{"class":1770},"authorizedData\n",[1179,4859,4860,4862,4864,4866],{"class":1181,"line":1353},[1179,4861,2718],{"class":1755},[1179,4863,1854],{"class":1239},[1179,4865,1341],{"class":1770},[1179,4867,3076],{"class":1239},[1179,4869,4870],{"class":1181,"line":1374},[1179,4871,1834],{"class":1239},[830,4873,4874],{},"The deduplication lock, the metadata cache, and the rotation decision all happen before the first line of your handler. From your handler's perspective, the session is always valid when it arrives.",[852,4876],{},[855,4878,3762],{"id":3761},[830,4880,4881],{},"The dual-token architecture exists because no single credential can satisfy both the performance requirement (fast verification, no database query on every request) and the security requirement (cheap revocation, short exposure windows).",[830,4883,4884],{},"Access tokens satisfy the performance requirement. They are verified in memory with a cache lookup and a signature check. Revoking one is a cache delete. The database is never involved.",[830,4886,4887,4888,4890],{},"Refresh tokens satisfy the security requirement. They are stored as hashes, consumed atomically, and protected by a reuse detection system that terminates all sessions at the first sign of replay. Their long TTL makes them practical for real users while ",[843,4889,4205],{}," ensures sessions cannot live indefinitely.",[830,4892,4893],{},"The deduplication layer sits between the two, preventing the concurrent-rotation problem that makes dual-token systems brittle in practice.",[830,4895,4896,4897,4901],{},"Read the full ",[4898,4899,4900],"a",{"href":366},"token reference"," for the IAM service",[830,4903,4904,4905,4907],{},"Read how ",[4898,4906,20],{"href":102}," manages session state and drives token rotation",[3786,4909,4910],{},"html pre.shiki code .sDd4n, html code.shiki .sDd4n{--shiki-light:#000000;--shiki-default:#000000;--shiki-dark:#F8F8F2}html pre.shiki code .saJyd, html code.shiki .saJyd{--shiki-light:#0451A5;--shiki-default:#0451A5;--shiki-dark:#8BE9FE}html pre.shiki code .s_W10, html code.shiki .s_W10{--shiki-light:#0451A5;--shiki-default:#0451A5;--shiki-dark:#8BE9FD}html pre.shiki code .saOXh, html code.shiki .saOXh{--shiki-light:#000000;--shiki-default:#000000;--shiki-dark:#FF79C6}html pre.shiki code .sFkSl, html code.shiki .sFkSl{--shiki-light:#A31515;--shiki-default:#A31515;--shiki-dark:#E9F284}html pre.shiki code .sFB1V, html code.shiki .sFB1V{--shiki-light:#A31515;--shiki-default:#A31515;--shiki-dark:#F1FA8C}html pre.shiki code .spgvN, html code.shiki .spgvN{--shiki-light:#098658;--shiki-default:#098658;--shiki-dark:#BD93F9}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZ328, html code.shiki .sZ328{--shiki-light:#AF00DB;--shiki-default:#AF00DB;--shiki-dark:#FF79C6}html pre.shiki code .sjsA6, html code.shiki .sjsA6{--shiki-light:#001080;--shiki-default:#001080;--shiki-dark:#F8F8F2}html pre.shiki code .sl46w, html code.shiki .sl46w{--shiki-light:#0000FF;--shiki-default:#0000FF;--shiki-dark:#FF79C6}html pre.shiki code .s3JHE, html code.shiki .s3JHE{--shiki-light:#0070C1;--shiki-default:#0070C1;--shiki-dark:#F8F8F2}html pre.shiki code .sHOzp, html code.shiki .sHOzp{--shiki-light:#795E26;--shiki-default:#795E26;--shiki-dark:#50FA7B}html pre.shiki code .sghk6, html code.shiki .sghk6{--shiki-light:#008000;--shiki-default:#008000;--shiki-dark:#6272A4}html pre.shiki code .sygFZ, html code.shiki .sygFZ{--shiki-light:#001080;--shiki-light-font-style:inherit;--shiki-default:#001080;--shiki-default-font-style:inherit;--shiki-dark:#FFB86C;--shiki-dark-font-style:italic}",{"title":811,"searchDepth":812,"depth":812,"links":4912},[4913,4914,4915,4916,4923,4924,4925,4926,4927],{"id":3828,"depth":812,"text":3829},{"id":4008,"depth":812,"text":4009},{"id":4094,"depth":812,"text":4095},{"id":4211,"depth":812,"text":4212,"children":4917},[4918,4919,4920,4921,4922],{"id":4234,"depth":1212,"text":4235},{"id":4245,"depth":1212,"text":4246},{"id":4276,"depth":1212,"text":4277},{"id":4306,"depth":1212,"text":4307},{"id":4323,"depth":1212,"text":4324},{"id":4425,"depth":812,"text":4426},{"id":4548,"depth":812,"text":4549},{"id":4611,"depth":812,"text":4612},{"id":4679,"depth":812,"text":4680},{"id":3761,"depth":812,"text":3762},"2026-04-13","A deep dive into the dual-token lifecycle, why short-lived access tokens paired with hashed refresh tokens are safer than sessions, and how concurrent rotation requests are coalesced.","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1614064641938-3bbee52942c7?w=1200&q=80",{},"\u002Fblog\u002Fhow-token-rotation-works","---\ntitle: \"How Token Rotation Works: Access Tokens, Refresh Tokens, and the Deduplication Problem\"\ndescription: \"A deep dive into the dual-token lifecycle, why short-lived access tokens paired with hashed refresh tokens are safer than sessions, and how concurrent rotation requests are coalesced.\"\nnavigation: false\ntags:\n    - Tokens\n    - Security\nimage: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1614064641938-3bbee52942c7?w=1200&q=80\"\nauthor: \"Sergio\"\nauthorImg: \"https:\u002F\u002Fgithub.com\u002FSergo706.png\"\nauthorGithub: \"https:\u002F\u002Fgithub.com\u002FSergo706\"\nauthorGithubUserName: \"Sergo706\"\nfeatured: false\ndate: 2026-04-13T10:00:00.000Z\nreadingTime: \"10 min read\"\n---\n\nMost authentication systems issue a single credential — a session ID, a JWT, a cookie — and use it until it expires or the user logs out. The problem with that model is straightforward: if an attacker obtains that credential, they have as long as it lives to use it. The longer it lives, the bigger the exposure window.\n\nRiavzon solves this with a dual-token architecture. Access tokens are short-lived and verified cryptographically. Refresh tokens are long-lived but stored as hashes in a database, consumed atomically, and wrapped in a reuse detection system that revokes every session the moment replay is detected. This post explains every layer of that architecture: why it is designed this way, how each piece works, and what happens when two requests from the same user arrive at the same time.\n\n---\n\n## The Two-Token Model\n\nEvery authenticated user in the system holds two credentials at once.\n\nThe **access token** is a signed JWT. It lives in a `__Secure-a` cookie on the browser. Its lifetime is short — typically 15 minutes — and it is verified on every request without touching the database. The IAM service uses an LRU cache to hold every valid token, so verification is a cache lookup plus a cryptographic check, not a database query. When the token expires, the cache entry is evicted and the next verification call fails immediately.\n\nThe **refresh token** is a 64-byte cryptographically random string, hex encoded. The browser holds the raw token in an `httpOnly` cookie named `session`. The server never stores the raw token. Instead, it hashes it with SHA-256 and stores the hash in a MySQL `refresh_tokens` table. The raw token leaves the server exactly once, when it is issued, and the server never sees it again in plaintext.\n\n```json\n{\n  \"visitor\": \"vis_abc123\",\n  \"roles\": [\"user\"],\n  \"sub\": \"42\",\n  \"jti\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"iat\": 1710000000,\n  \"exp\": 1710000900\n}\n```\n\nThat is a typical access token payload. The `jti` is a UUID generated fresh on every issuance. It is also the key by which the token lives in the LRU cache. Deleting the cache entry for a `jti` revokes that token immediately, without a database write, without waiting for expiry.\n\nThe canary cookie — `canary_id` — ties the session to a specific device fingerprint. It is issued by the Bot Detector middleware and is required alongside both tokens for any sensitive operation. It is neither a credential nor an authentication factor on its own, but it binds the token family to the visitor context that created it, and any mismatch triggers anomaly detection.\n\n---\n\n## Why Short-Lived Access Tokens\n\nThe conventional objection to short-lived tokens is the extra network round trips. If the token expires every 15 minutes, the user's browser needs to refresh it every 15 minutes. That cost is real, but the security benefit justifies it.\n\nAn access token that lives for 15 minutes and is stolen gives an attacker a 15-minute window. An access token that lives for 24 hours gives an attacker 24 hours. In practice, the difference between these windows matters enormously when you consider how often stolen credentials go undetected. The 15-minute window usually closes before the attacker can do meaningful damage. The 24-hour window rarely does.\n\nMore importantly, the LRU cache is the real enforcement boundary. An access token is not just valid because it carries the right signature. It is valid because it exists in the cache. This means revocation is instant and free. Deleting the cache entry with the token's `jti` terminates that token immediately, regardless of how long it has until expiry. Sessions can be force-terminated without a database write, without blocking, and without any propagation delay.\n\n```ts\nimport { tokenCache } from '@riavzon\u002Fauth'\n\nconst cache = tokenCache()\ncache.delete(rawToken) \u002F\u002F This token is now invalid. No database write needed.\n```\n\nThe two-gate verification model — cache check first, cryptographic check second — also means the cryptographic work only happens when the cache says the token could be valid. Revoked tokens fail at the first gate, before any cryptographic computation runs.\n\n---\n\n## Why Hashed Refresh Tokens in the Database\n\nLong-lived tokens stored in plaintext are a liability. If the database is compromised, every session is compromised. Hashing the token before storing it breaks that link. An attacker with a dump of the `refresh_tokens` table gets SHA-256 hashes — not the raw tokens they need to authenticate.\n\nThe storage schema for a refresh token row looks like this:\n\n| Column | Value |\n|---|---|\n| `token` | `sha256(rawToken)` — never the raw value |\n| `valid` | `1` when active, `0` when revoked |\n| `usage_count` | `0` when fresh, `1` after first consumption |\n| `session_started_at` | Timestamp from the original login, carried across all rotations |\n| `expiresAt` | Computed from `refresh_ttl` at insert time |\n\nThe `usage_count` column is the core of the reuse detection system. It starts at zero. The moment the token is consumed — used to issue a new token pair — the database atomically sets it to `1`. Any second attempt to consume a token with `usage_count > 0` is treated as a replay attack, and all sessions for that user are immediately revoked.\n\nThe `session_started_at` column persists the original login timestamp across every rotation. No matter how many times the token is rotated, the session chain traces back to the original authentication event. This is how `MAX_SESSION_LIFE` works: the system knows when the session began and can enforce an absolute ceiling on how long any session can live, regardless of how often it is refreshed.\n\n---\n\n## The Rotation Lifecycle\n\nRotation is the process that converts old credentials into new ones. It is the most security-sensitive operation in the system, and it runs in a strict sequence.\n\nWhen the access token is about to expire, Auth H3 Client calls `POST \u002Fauth\u002Fuser\u002Frefresh-session` on the IAM service with the `session` and `canary_id` cookies. The IAM rotation controller runs this sequence:\n\n::steps\n\n### Rate limiting\n\nThree layered rate limiters run first: an IP limiter, a token-hash limiter, and a composite `ip_tokenhash` limiter. Each uses consecutive caches that escalate block duration on repeated violations. Brute force attempts are stopped before anything else runs.\n\n### Anomaly detection\n\n`strangeThings()` runs nine sequential checks against the session. It verifies the `canary_id` binding, checks IP range consistency against historical records, compares the `User-Agent` fingerprint, validates that the session has not exceeded `maxAllowedSessionsPerUser`, and checks that the token has not already been consumed (`usage_count > 0`). The first check that fails short-circuits the rest. If anomalies are recoverable, the service sends an MFA email and returns `202`. If they are not recoverable, the token is revoked and the service returns `401`.\n\n### Atomic consumption\n\n`consumeAndVerifyRefreshToken` runs a single atomic `UPDATE` inside a transaction. It increments `usage_count` by one, but only if the row exists, is `valid = 1`, has `usage_count = 0`, and has not expired. All four conditions must pass in the same transaction. If even one fails, no rows are affected.\n\nIf no rows are affected, the function investigates: the token might not exist, it might have been revoked, or it might have `usage_count > 0` from a previous consumption. That last case is a reuse detection trigger — all sessions for the user are revoked immediately.\n\n### Session lifetime check\n\nIf the token consumed successfully but `session_started_at` is older than `MAX_SESSION_LIFE`, the controller revokes the token and returns `401 Session is expired`. The session chain has lived as long as policy allows.\n\n### New credential issuance\n\nThe old token is set to `valid = 0`. A new 64-byte random refresh token is generated, hashed, and inserted with `valid = 1`, `usage_count = 0`, and the same `session_started_at` from the consumed token. A new access token is signed with a fresh `jti` and cached. Both are sent to the browser.\n\n::\n\nThe success response carries the new access token in the body. The new refresh token arrives in the `Set-Cookie` header. The browser replaces its cookies transparently.\n\n```json\n{\n  \"message\": \"Refresh & access tokens rotated\",\n  \"accessToken\": \"\u003Csigned jwt>\",\n  \"accessIat\": \"1710000000000\"\n}\n```\n\n---\n\n## The Deduplication Problem\n\nSingle-page applications create a problem that most token rotation systems ignore: concurrent requests.\n\nConsider a user whose access token has just expired. Their browser has three in-flight requests — a profile fetch, a feed load, and a notification count. All three arrive at the server at the same moment. All three see an expired access token. All three decide to rotate.\n\nWithout deduplication, all three would call `POST \u002Fauth\u002Fuser\u002Frefresh-session` simultaneously. The first call consumes the refresh token (sets `usage_count = 1`). The second call tries to consume the same token and finds `usage_count > 0`. The reuse detection system interprets this as a replay attack and revokes all sessions. The user is logged out, and they did nothing wrong.\n\nThis is not a theoretical edge case. It happens on any page with multiple parallel API calls, and it happens reliably whenever token expiry falls at a high-traffic moment.\n\nAuth H3 Client solves this with `lockAsyncAction`, a keyed async mutex. When `ensureValidCredentials` needs to rotate, it acquires a lock keyed on the refresh token value — the `session` cookie — before making any call to the IAM service. A second request for the same session finds the lock held, waits for the first call to complete, and reuses its result.\n\n```ts\n\u002F\u002F Inside ensureValidCredentials — simplified view\nconst result = await lockAsyncAction(refreshToken, async () => {\n  \u002F\u002F Only one call per session cookie value runs at a time.\n  \u002F\u002F All others wait here and get the same result.\n  return await callIAMRotation(sessionCookie, canaryCookie)\n})\n```\n\nThe lock is keyed on the refresh token value itself, not on a user ID or session ID. This matters because a user might have multiple active sessions across devices. Each session has its own refresh token, so each gets its own independent lock. Concurrent rotation on one device does not block rotation on another.\n\nThe result is cached briefly after the lock releases. Requests that arrive after the first call completes but before the lock is fully released still get the cached result without making another call. This covers the common case where a burst of requests resolves in quick succession rather than simultaneously.\n\n---\n\n## Reuse Detection in Depth\n\nThe reuse detection system operates on a core assumption: a refresh token should only ever be consumed once. Any second consumption means either the token was stolen and replayed, or something in the rotation flow went wrong. Either way, the safest response is to terminate all sessions for that user immediately.\n\n**Scenario 1: An attacker steals a valid, unconsumed refresh token.**\n\nTo use the stolen token, the attacker must also replicate the user's `canary_id` cookie and pass the fingerprint checks in `strangeThings`. If the fingerprint does not match, the anomaly engine sends an MFA challenge to the real user's email before any rotation happens. The attacker cannot proceed without access to the user's email.\n\nIf the attacker somehow passes the fingerprint checks and consumes the token, `usage_count` becomes `1`. The next time the legitimate user's browser tries to rotate — which happens automatically as the access token approaches expiry — `consumeAndVerifyRefreshToken` finds `usage_count > 0`. All sessions are revoked. Both the attacker and the legitimate user are forced to re-authenticate. The attacker cannot complete MFA without the user's email.\n\n**Scenario 2: An attacker steals a token that has already been rotated.**\n\nThe stolen token has `usage_count = 1`. The attacker attempts to consume it. `consumeAndVerifyRefreshToken` detects `usage_count > 0` immediately, revokes all sessions for the user, and returns `valid: false`. The attacker's attempt terminates the legitimate user's current session, but the attacker gains nothing — they still cannot authenticate.\n\nIn both scenarios, the worst outcome for the legitimate user is being forced to log in again and prove their identity through MFA. The attacker is locked out at every step.\n\n---\n\n## Two Lifetime Controls\n\nRefresh tokens have two independent lifetime mechanisms, and understanding the difference between them matters.\n\n**Token TTL (`refresh_ttl`)** controls how long a single refresh token row stays valid in the database. When a token expires, verification sets `valid = 0` and clears `last_mfa_at` for the user. Clearing `last_mfa_at` resets the `byPassAnomaliesFor` cooldown — the next session anomaly (if one occurs) will not be bypassed, though a clean login from the same device still passes without MFA.\n\n**Session lifetime (`MAX_SESSION_LIFE`)** controls how long the entire session chain can survive. Every time a token is rotated, the new token inherits the same `session_started_at` timestamp from the consumed token. That timestamp is the anchor. When `Date.now() - session_started_at` exceeds `MAX_SESSION_LIFE`, the rotation controller refuses to issue new credentials, even if the token itself has not expired yet.\n\nThe practical configuration is to set `refresh_ttl` to something like 3 days and `MAX_SESSION_LIFE` to 30 days. Individual tokens force periodic rotation and limit the exposure window of any single credential. The session ceiling prevents sessions from living indefinitely through continuous renewal.\n\n```\nrefresh_ttl:      3 days    — Each token lives this long\nMAX_SESSION_LIFE: 30 days   — The session chain lives this long\n```\n\nA \"remember me\" flow with a 3-day token TTL still expires the session completely after 30 days. The user must log in again, not just refresh.\n\n---\n\n## How Auth H3 Client Drives This\n\nAuth H3 Client, the gateway layer for Nuxt and Nitro applications, handles the entire rotation lifecycle transparently. Your application code never calls the rotation endpoint directly. Instead, every protected route wraps its handler in `defineAuthenticatedEventHandler`, which calls `ensureValidCredentials` before your code runs.\n\n`ensureValidCredentials` decides whether to rotate based on the metadata it receives from the IAM `\u002Fsecret\u002Faccesstoken\u002Fmetadata` endpoint. The decision logic covers every case:\n\n| Metadata result | Action |\n|---|---|\n| No access token present | Rotate immediately |\n| `shouldRotate: true` (within 25% of TTL) | Rotate proactively |\n| `authorized: false` | Rotate |\n| Server error or no response | Rotate |\n| `mfa: true` (IAM returned 202) | Return 202, do not rotate |\n| Valid and within threshold | Set token on context, continue |\n\nThe metadata response is cached in a `MiniCache` instance keyed by the access token value. The cache TTL is `msUntilExp - refreshThreshold - 5 seconds`, so the cache expires just before the token would trigger a rotation check anyway. Requests within that window read the cached metadata without a network call.\n\n```ts\nexport default defineAuthenticatedEventHandler(async (event) => {\n  \u002F\u002F By the time this line runs:\n  \u002F\u002F - The access token has been verified or rotated\n  \u002F\u002F - New cookies have been applied to the response if rotation happened\n  \u002F\u002F - Concurrent requests from the same session were deduplicated\n  \u002F\u002F - event.context.authorizedData contains the verified session data\n  const { userId, roles } = event.context.authorizedData\n  return { userId }\n})\n```\n\nThe deduplication lock, the metadata cache, and the rotation decision all happen before the first line of your handler. From your handler's perspective, the session is always valid when it arrives.\n\n---\n\n## Summary\n\nThe dual-token architecture exists because no single credential can satisfy both the performance requirement (fast verification, no database query on every request) and the security requirement (cheap revocation, short exposure windows).\n\nAccess tokens satisfy the performance requirement. They are verified in memory with a cache lookup and a signature check. Revoking one is a cache delete. The database is never involved.\n\nRefresh tokens satisfy the security requirement. They are stored as hashes, consumed atomically, and protected by a reuse detection system that terminates all sessions at the first sign of replay. Their long TTL makes them practical for real users while `MAX_SESSION_LIFE` ensures sessions cannot live indefinitely.\n\nThe deduplication layer sits between the two, preventing the concurrent-rotation problem that makes dual-token systems brittle in practice.\n\n\nRead the full [token reference](\u002Fdocs\u002Fiam\u002Fessentials\u002Ftokens) for the IAM service\n\n\nRead how [Auth H3 Client](\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fsession) manages session state and drives token rotation\n\n","10 min read",{"title":3815,"description":4929},"blog\u002Fhow-token-rotation-works",[365,38],"GbsvB31LBRJ0U3cfO2xcPbmIspXuqJsH8lPWrRQ5pzc",{"id":4940,"title":73,"author":823,"authorGithub":824,"authorGithubUserName":825,"authorImg":826,"body":4941,"date":4928,"description":10259,"extension":815,"featured":53,"icon":3804,"image":10260,"meta":10261,"navigation":8,"path":74,"rawbody":10262,"readingTime":10263,"seo":10264,"stem":75,"tags":10265,"__hash__":10267},"blog\u002Fblog\u002Flayered-bot-defense.md",{"type":808,"value":4942,"toc":10222},[4943,4946,4949,4951,4955,4958,4961,4964,4967,4973,4975,4979,4982,4986,4989,4993,4996,5001,5116,5121,5124,5206,5211,5252,5256,5263,5410,5421,5426,5428,5432,5439,5443,5449,5453,5463,5470,5545,5552,5556,5562,5568,5585,5594,5675,5692,5698,5748,5757,5765,5829,5832,5841,5847,5860,5864,5867,5876,5882,5952,5957,5970,6012,6018,6024,6030,6033,6039,6087,6093,6102,6143,6145,6149,6158,6195,6198,6202,6208,6213,6217,6223,6228,6431,6436,6551,6554,6558,6561,6573,6582,6584,6588,6591,6602,6607,6640,6646,6649,6654,6672,6678,6680,6683,6686,7484,7489,7491,7495,7501,7508,7517,7706,7711,7715,7724,7891,7905,7909,7915,8348,8351,8435,8439,8457,8661,8989,8992,8996,9007,9107,9114,9118,9132,9673,9682,9684,9688,9698,9702,9709,9727,9758,9828,9840,9883,9887,9894,9898,9963,9970,10151,10155,10158,10190,10196,10198,10200,10203,10206,10209,10214,10219],[830,4944,4945],{},"Most bot detection systems operate on a single layer: a rule list, a rate limiter, or a third-party API call. The problem with that model is that any single signal can be spoofed. A bot can rotate IPs, forge user-agent strings, and slow its request rate to look human. Defeating it requires combining signals from multiple independent layers so that evading one does not defeat the others.",[830,4947,4948],{},"The Riavzon stack addresses this with three coordinated components. Shield Base compiles IP intelligence from a dozen external sources into binary databases. Bot Detector runs those databases through a two-phase, 17-checker pipeline that scores every incoming request. The IAM canary cookie ties each browser session to a fingerprint that follows it through every subsequent request. This post walks through every layer in detail — how each one works, what data it uses, and what happens when a bot hits the stack.",[852,4950],{},[855,4952,4954],{"id":4953},"the-three-layers-at-a-glance","The Three Layers at a Glance",[830,4956,4957],{},"Before going deep on each component, it helps to understand how they relate to one another.",[830,4959,4960],{},"Shield Base is a build-time tool. You run it once to produce a set of binary database files, then run it again periodically to refresh them. It has no runtime presence — it just produces the files that the other layers consume.",[830,4962,4963],{},"Bot Detector is a runtime Express middleware. It reads the Shield Base databases at startup and holds them in memory. Every request passes through its pipeline, which scores the request across behavioral, fingerprint, and reputation dimensions. If the score reaches the ban threshold, the middleware short-circuits the request before it touches any application logic.",[830,4965,4966],{},"The canary cookie is a per-session identifier, issued on first contact and carried on every subsequent request. Bot Detector uses it to track session state across requests — storing timing patterns, path history, and reputation scores keyed on the cookie value. The IAM service uses the same cookie to bind authentication tokens to a specific visitor fingerprint, enabling anomaly detection during token rotation.",[1089,4968,4971],{"className":4969,"code":4970,"language":1095},[1092],"Shield Base (build time)\n  └── Compiles MMDB + LMDB databases\n        └── Bot Detector (runtime middleware)\n              ├── Cheap phase: 10 synchronous checkers\n              ├── Heavy phase: 7 async checkers\n              └── Issues canary_id cookie on first request\n                    └── IAM service\n                          ├── Binds refresh tokens to canary fingerprint\n                          └── Flags anomalies during rotation\n",[843,4972,4970],{"__ignoreMap":811},[852,4974],{},[855,4976,4978],{"id":4977},"shield-base-compiling-the-intelligence-layer","Shield Base: Compiling the Intelligence Layer",[830,4980,4981],{},"Shield Base is a CLI tool that downloads, processes, and compiles external threat intelligence into binary formats that Bot Detector can query in microseconds at runtime. It produces two kinds of output: MMDB files for IP-range lookups and LMDB files for hash-keyed pattern matching.",[4232,4983,4985],{"id":4984},"why-binary-databases","Why Binary Databases",[830,4987,4988],{},"The raw data that feeds bot detection is enormous. BGP routing tables, geolocation datasets, Tor node lists, FireHOL threat feeds, and user-agent pattern databases together contain hundreds of millions of entries. Querying them naively at runtime is not practical. MMDB (MaxMind DB) encodes IP ranges into a binary trie that resolves any IP to its metadata in a single file seek. LMDB (Lightning Memory-Mapped Database) is a memory-mapped key-value store that delivers zero-copy reads with no serialization overhead. Both formats are loaded once at startup and kept in memory for the lifetime of the process.",[4232,4990,4992],{"id":4991},"the-14-data-sources","The 14 Data Sources",[830,4994,4995],{},"Shield Base downloads and compiles 14 distinct data sources, each targeting a different threat signal.",[830,4997,4998],{},[3837,4999,5000],{},"IP reputation and routing",[863,5002,5003,5018],{},[866,5004,5005],{},[869,5006,5007,5009,5012,5015],{},[872,5008,431],{},[872,5010,5011],{},"Output",[872,5013,5014],{},"Source",[872,5016,5017],{},"What it contains",[885,5019,5020,5036,5052,5068,5084,5100],{},[869,5021,5022,5025,5030,5033],{},[890,5023,5024],{},"ASN routing",[890,5026,5027],{},[843,5028,5029],{},"asn.mmdb",[890,5031,5032],{},"bgp.tools",[890,5034,5035],{},"Autonomous system numbers, ISP classification, network visibility",[869,5037,5038,5041,5046,5049],{},[890,5039,5040],{},"City geolocation",[890,5042,5043],{},[843,5044,5045],{},"city.mmdb",[890,5047,5048],{},"MaxMind Geofeed",[890,5050,5051],{},"IP-to-city mappings with coordinates, timezone, and subdivision",[869,5053,5054,5057,5062,5065],{},[890,5055,5056],{},"Country\u002Fgeography",[890,5058,5059],{},[843,5060,5061],{},"country.mmdb",[890,5063,5064],{},"Sapics ip-location-db",[890,5066,5067],{},"IPv4-to-country with continent and subregion data",[869,5069,5070,5073,5078,5081],{},[890,5071,5072],{},"Proxy detection",[890,5074,5075],{},[843,5076,5077],{},"proxy.mmdb",[890,5079,5080],{},"Custom proxy lists",[890,5082,5083],{},"Known VPN exit points and proxy server IPs",[869,5085,5086,5089,5094,5097],{},[890,5087,5088],{},"Tor nodes",[890,5090,5091],{},[843,5092,5093],{},"tor.mmdb",[890,5095,5096],{},"Torproject Onionoo API",[890,5098,5099],{},"Active Tor relays classified by role: exit, guard, bad exit",[869,5101,5102,5105,5110,5113],{},[890,5103,5104],{},"Verified crawlers",[890,5106,5107],{},[843,5108,5109],{},"goodBots.mmdb",[890,5111,5112],{},"Web crawler domain lists",[890,5114,5115],{},"IP ranges belonging to legitimate search engines and SEO crawlers",[830,5117,5118],{},[3837,5119,5120],{},"Threat intelligence (FireHOL)",[830,5122,5123],{},"FireHOL maintains multiple threat list tiers. Shield Base compiles all of them into separate MMDB files, which Bot Detector queries independently so that the scoring system can assign different penalty weights to each tier.",[863,5125,5126,5139],{},[866,5127,5128],{},[869,5129,5130,5133,5136],{},[872,5131,5132],{},"Level",[872,5134,5135],{},"File",[872,5137,5138],{},"What it tracks",[885,5140,5141,5154,5167,5180,5193],{},[869,5142,5143,5146,5151],{},[890,5144,5145],{},"L1",[890,5147,5148],{},[843,5149,5150],{},"firehol_l1.mmdb",[890,5152,5153],{},"Current attacks — minimum false positives, maximum severity",[869,5155,5156,5159,5164],{},[890,5157,5158],{},"L2",[890,5160,5161],{},[843,5162,5163],{},"firehol_l2.mmdb",[890,5165,5166],{},"Attacks observed in the last 48 hours, including dynamic IPs",[869,5168,5169,5172,5177],{},[890,5170,5171],{},"L3",[890,5173,5174],{},[843,5175,5176],{},"firehol_l3.mmdb",[890,5178,5179],{},"Attacks, spyware, and viruses tracked over the last 30 days",[869,5181,5182,5185,5190],{},[890,5183,5184],{},"L4",[890,5186,5187],{},[843,5188,5189],{},"firehol_l4.mmdb",[890,5191,5192],{},"Aggressive tracking with a higher false-positive rate",[869,5194,5195,5198,5203],{},[890,5196,5197],{},"Anonymous",[890,5199,5200],{},[843,5201,5202],{},"firehol_anonymous.mmdb",[890,5204,5205],{},"Tor exit nodes, I2P, VPNs, and other anonymity relays",[830,5207,5208],{},[3837,5209,5210],{},"Pattern databases (LMDB)",[863,5212,5213,5224],{},[866,5214,5215],{},[869,5216,5217,5219,5222],{},[872,5218,431],{},[872,5220,5221],{},"Directory",[872,5223,5017],{},[885,5225,5226,5239],{},[869,5227,5228,5231,5236],{},[890,5229,5230],{},"User-agent patterns",[890,5232,5233],{},[843,5234,5235],{},"useragent-db\u002Fuseragent.mdb",[890,5237,5238],{},"Known bot, scraper, and tool user-agent signatures with severity ratings",[869,5240,5241,5244,5249],{},[890,5242,5243],{},"Disposable emails",[890,5245,5246],{},[843,5247,5248],{},"email-db\u002Fdisposable-emails.mdb",[890,5250,5251],{},"Domain blocklist for temporary and disposable email providers",[4232,5253,5255],{"id":5254},"running-shield-base","Running Shield Base",[830,5257,5258,5259,5262],{},"The CLI accepts flags for individual sources or bulk compilation. The ",[843,5260,5261],{},"--parallel"," flag compiles all sources concurrently, which is the standard approach for periodic refreshes.",[5264,5265,5266,5341],"code-group",{},[1089,5267,5270],{"className":1172,"code":5268,"filename":5269,"language":1175,"meta":811,"style":811},"# Compile all sources in parallel\npnpm shield-base --all --parallel\n\n# Compile specific sources\npnpm shield-base --bgp --geo --tor --l1 --l2\n\n# Compile only LMDB pattern databases\npnpm shield-base --useragent --email\n","pnpm",[843,5271,5272,5277,5290,5294,5299,5320,5324,5329],{"__ignoreMap":811},[1179,5273,5274],{"class":1181,"line":1182},[1179,5275,5276],{"class":4085},"# Compile all sources in parallel\n",[1179,5278,5279,5281,5284,5287],{"class":1181,"line":812},[1179,5280,5269],{"class":1185},[1179,5282,5283],{"class":1203}," shield-base",[1179,5285,5286],{"class":1195}," --all",[1179,5288,5289],{"class":1195}," --parallel\n",[1179,5291,5292],{"class":1181,"line":1212},[1179,5293,1935],{"emptyLinePlaceholder":8},[1179,5295,5296],{"class":1181,"line":1283},[1179,5297,5298],{"class":4085},"# Compile specific sources\n",[1179,5300,5301,5303,5305,5308,5311,5314,5317],{"class":1181,"line":1298},[1179,5302,5269],{"class":1185},[1179,5304,5283],{"class":1203},[1179,5306,5307],{"class":1195}," --bgp",[1179,5309,5310],{"class":1195}," --geo",[1179,5312,5313],{"class":1195}," --tor",[1179,5315,5316],{"class":1195}," --l1",[1179,5318,5319],{"class":1195}," --l2\n",[1179,5321,5322],{"class":1181,"line":1319},[1179,5323,1935],{"emptyLinePlaceholder":8},[1179,5325,5326],{"class":1181,"line":1336},[1179,5327,5328],{"class":4085},"# Compile only LMDB pattern databases\n",[1179,5330,5331,5333,5335,5338],{"class":1181,"line":1353},[1179,5332,5269],{"class":1185},[1179,5334,5283],{"class":1203},[1179,5336,5337],{"class":1195}," --useragent",[1179,5339,5340],{"class":1195}," --email\n",[1089,5342,5345],{"className":1172,"code":5343,"filename":5344,"language":1175,"meta":811,"style":811},"# Compile all sources in parallel\nnpm run shield-base --all --parallel\n\n# Compile specific sources\nnpm run shield-base --bgp --geo --tor --l1 --l2\n\n# Compile only LMDB pattern databases\nnpm run shield-base --useragent --email\n","npm",[843,5346,5347,5351,5364,5368,5372,5390,5394,5398],{"__ignoreMap":811},[1179,5348,5349],{"class":1181,"line":1182},[1179,5350,5276],{"class":4085},[1179,5352,5353,5355,5358,5360,5362],{"class":1181,"line":812},[1179,5354,5344],{"class":1185},[1179,5356,5357],{"class":1203}," run",[1179,5359,5283],{"class":1203},[1179,5361,5286],{"class":1195},[1179,5363,5289],{"class":1195},[1179,5365,5366],{"class":1181,"line":1212},[1179,5367,1935],{"emptyLinePlaceholder":8},[1179,5369,5370],{"class":1181,"line":1283},[1179,5371,5298],{"class":4085},[1179,5373,5374,5376,5378,5380,5382,5384,5386,5388],{"class":1181,"line":1298},[1179,5375,5344],{"class":1185},[1179,5377,5357],{"class":1203},[1179,5379,5283],{"class":1203},[1179,5381,5307],{"class":1195},[1179,5383,5310],{"class":1195},[1179,5385,5313],{"class":1195},[1179,5387,5316],{"class":1195},[1179,5389,5319],{"class":1195},[1179,5391,5392],{"class":1181,"line":1319},[1179,5393,1935],{"emptyLinePlaceholder":8},[1179,5395,5396],{"class":1181,"line":1336},[1179,5397,5328],{"class":4085},[1179,5399,5400,5402,5404,5406,5408],{"class":1181,"line":1353},[1179,5401,5344],{"class":1185},[1179,5403,5357],{"class":1203},[1179,5405,5283],{"class":1203},[1179,5407,5337],{"class":1195},[1179,5409,5340],{"class":1195},[830,5411,5412,5413,5416,5417,5420],{},"Internally, ",[843,5414,5415],{},"executeAll"," runs 10 compilation tasks in parallel. Each task downloads its source data, processes it into the intermediate format, and compiles it using either the ",[843,5418,5419],{},"mmdbctl"," binary (for MMDB) or the native LMDB Node.js bindings. The output files land in a configured output directory that Bot Detector reads from at startup.",[1670,5422,5423],{},[830,5424,5425],{},"Shield Base requires a valid contact User-Agent for the BGP\u002FASN data fetch from bgp.tools. Configure this in your Shield Base settings before running the first compilation.",[852,5427],{},[855,5429,5431],{"id":5430},"bot-detector-the-two-phase-scoring-pipeline","Bot Detector: The Two-Phase Scoring Pipeline",[830,5433,5434,5435,5438],{},"Bot Detector is a middleware factory. You call ",[843,5436,5437],{},"configuration(config)"," once at startup to register your settings and mount the middleware on your Express router. From that point on, every request passes through the pipeline, accumulates a score, and either continues to the next handler or receives a ban response.",[4232,5440,5442],{"id":5441},"loading-the-databases","Loading the Databases",[830,5444,3835,5445,5448],{},[843,5446,5447],{},"DataSources"," class loads all Shield Base outputs at initialization. It opens 11 MMDB readers (ASN, city, country, good bots, Tor, proxy, and all five FireHOL levels) and 1 LMDB reader (user-agent patterns). It also accepts optional banned and high-risk MMDB files for custom enforcement lists. All readers stay open and memory-resident for the lifetime of the process. There are no per-request file operations — every lookup is an in-memory binary search.",[4232,5450,5452],{"id":5451},"scoring-mechanics","Scoring Mechanics",[830,5454,5455,5456,5459,5460,5462],{},"Every request starts with a score of zero. Checkers increment the score when they detect anomalies. The pipeline compares the running total against ",[843,5457,5458],{},"banScore"," (default: 100) after the cheap phase and again after the heavy phase. Reaching ",[843,5461,5458],{}," at any point ends the pipeline immediately and sends a ban response.",[830,5464,5465,5466,5469],{},"Between requests, a reputation healer decrements the stored score by ",[843,5467,5468],{},"restoredReputationPoints"," (default: 10) for every non-banned request. A visitor who accumulated a score of 35 on a suspicious-looking first request will recover to zero across three or four clean subsequent requests, assuming no new checkers fire.",[1089,5471,5473],{"className":1745,"code":5472,"language":1748,"meta":811,"style":811},"\u002F\u002F Default scoring configuration\nawait configuration({\n  banScore: 100,\n  maxScore: 100,\n  restoredReputationPoints: 10,\n  setNewComputedScore: false,\n  \u002F\u002F ...\n})\n",[843,5474,5475,5480,5490,5502,5513,5525,5536,5541],{"__ignoreMap":811},[1179,5476,5477],{"class":1181,"line":1182},[1179,5478,5479],{"class":4085},"\u002F\u002F Default scoring configuration\n",[1179,5481,5482,5485,5488],{"class":1181,"line":812},[1179,5483,5484],{"class":1755},"await",[1179,5486,5487],{"class":1185}," configuration",[1179,5489,1765],{"class":1239},[1179,5491,5492,5495,5497,5500],{"class":1181,"line":1212},[1179,5493,5494],{"class":1770},"  banScore",[1179,5496,1255],{"class":1774},[1179,5498,5499],{"class":1330}," 100",[1179,5501,1036],{"class":1239},[1179,5503,5504,5507,5509,5511],{"class":1181,"line":1283},[1179,5505,5506],{"class":1770},"  maxScore",[1179,5508,1255],{"class":1774},[1179,5510,5499],{"class":1330},[1179,5512,1036],{"class":1239},[1179,5514,5515,5518,5520,5523],{"class":1181,"line":1298},[1179,5516,5517],{"class":1770},"  restoredReputationPoints",[1179,5519,1255],{"class":1774},[1179,5521,5522],{"class":1330}," 10",[1179,5524,1036],{"class":1239},[1179,5526,5527,5530,5532,5534],{"class":1181,"line":1319},[1179,5528,5529],{"class":1770},"  setNewComputedScore",[1179,5531,1255],{"class":1774},[1179,5533,1807],{"class":1195},[1179,5535,1036],{"class":1239},[1179,5537,5538],{"class":1181,"line":1336},[1179,5539,5540],{"class":4085},"  \u002F\u002F ...\n",[1179,5542,5543],{"class":1181,"line":1353},[1179,5544,1834],{"class":1239},[830,5546,5547,5548,5551],{},"Setting ",[843,5549,5550],{},"setNewComputedScore: false"," (the recommended default) means the detector writes the computed score to the database only when no prior record exists. On subsequent requests, the reputation healer decrements the stored score without recomputing. This prevents a bot that varies its signals slightly between requests from oscillating between high and low scores — it accumulates a record and decays from it.",[4232,5553,5555],{"id":5554},"phase-one-the-cheap-checkers","Phase One: The Cheap Checkers",[830,5557,5558,5559,5561],{},"The cheap phase runs 10 synchronous checks. These checks use only in-memory data — parsed request headers, pre-loaded database lookups, and cached session state. They run in microseconds. If the cumulative score reaches ",[843,5560,5458],{}," at any point in this phase, the pipeline stops immediately.",[830,5563,5564,5567],{},[3837,5565,5566],{},"1. IP Validation"," — confirms the request carries a parseable, routable IP address. Malformed or missing IPs score 10 points. This catches raw tool invocations that do not set a legitimate source address.",[830,5569,5570,5573,5574,5576,5577,5580,5581,5584],{},[3837,5571,5572],{},"2. Good and Bad Bot Verification"," — checks the request's IP against ",[843,5575,5109],{},". If the IP belongs to a known crawler, the middleware performs a reverse DNS lookup to verify the IP actually belongs to the claimed crawler domain. A passing DNS check issues ",[843,5578,5579],{},"GOOD_BOT_IDENTIFIED"," and whitelists the request instantly — no further checks run. A failing DNS check (IP on the good-bot list but DNS does not verify) issues ",[843,5582,5583],{},"BAD_BOT_DETECTED"," at 100 points — an instant ban. This checker handles the common impersonation pattern where a bot claims a Google or Bing user-agent from an unrelated hosting IP.",[830,5586,5587,5590,5591,5593],{},[3837,5588,5589],{},"3. Browser and Device Fingerprint"," — parses the ",[843,5592,4258],{}," header and applies penalties for impossible or implausible combinations.",[863,5595,5596,5606],{},[866,5597,5598],{},[869,5599,5600,5603],{},[872,5601,5602],{},"Signal",[872,5604,5605],{},"Penalty",[885,5607,5608,5616,5623,5631,5639,5646,5653,5660,5667],{},[869,5609,5610,5613],{},[890,5611,5612],{},"CLI tool or HTTP library (curl, Python requests, etc.)",[890,5614,5615],{},"100",[869,5617,5618,5621],{},[890,5619,5620],{},"Internet Explorer",[890,5622,5615],{},[869,5624,5625,5628],{},[890,5626,5627],{},"Kali Linux OS",[890,5629,5630],{},"10",[869,5632,5633,5636],{},[890,5634,5635],{},"Impossible browser\u002FOS combination",[890,5637,5638],{},"30",[869,5640,5641,5644],{},[890,5642,5643],{},"Unknown browser type or name",[890,5645,5630],{},[869,5647,5648,5651],{},[890,5649,5650],{},"Desktop device without detectable OS",[890,5652,5630],{},[869,5654,5655,5658],{},[890,5656,5657],{},"Unknown device vendor",[890,5659,5630],{},[869,5661,5662,5665],{},[890,5663,5664],{},"Unknown browser version",[890,5666,5630],{},[869,5668,5669,5672],{},[890,5670,5671],{},"Unknown device model",[890,5673,5674],{},"5",[830,5676,5677,5680,5681,5684,5685,5688,5689,5691],{},[3837,5678,5679],{},"4. Locale Map Verification"," — compares the ",[843,5682,5683],{},"Accept-Language"," header against the IP's geolocation country. A browser claiming ",[843,5686,5687],{},"fr-FR"," language from an IP geolocated to South Korea is suspicious. Missing or malformed ",[843,5690,5683],{}," headers score 20 points. A confirmed mismatch between language and geo scores an additional 20 points.",[830,5693,5694,5697],{},[3837,5695,5696],{},"5. Known Threats (FireHOL)"," — queries all five FireHOL MMDB files against the request IP. Each tier scores independently, so an IP appearing on multiple lists accumulates points from each.",[863,5699,5700,5709],{},[866,5701,5702],{},[869,5703,5704,5707],{},[872,5705,5706],{},"FireHOL tier",[872,5708,5605],{},[885,5710,5711,5719,5727,5734,5741],{},[869,5712,5713,5716],{},[890,5714,5715],{},"Anonymity network (Tor, VPN, I2P)",[890,5717,5718],{},"20",[869,5720,5721,5724],{},[890,5722,5723],{},"L1 — critical current threats",[890,5725,5726],{},"40",[869,5728,5729,5732],{},[890,5730,5731],{},"L2 — attacks in last 48 hours",[890,5733,5638],{},[869,5735,5736,5739],{},[890,5737,5738],{},"L3 — attacks in last 30 days",[890,5740,5718],{},[869,5742,5743,5746],{},[890,5744,5745],{},"L4 — aggressive tracking",[890,5747,5630],{},[830,5749,5750,5753,5754,5756],{},[3837,5751,5752],{},"6. ASN Classification"," — queries ",[843,5755,5029],{}," to determine the Autonomous System the IP belongs to. Hosting and datacenter ASNs score 20 points. An ASN with unusually low visibility (few routes announced, below 15% of expected) scores an additional 10 points. The combination of hosting classification and low visibility scores a further 20 — this pattern is characteristic of freshly provisioned bot infrastructure.",[830,5758,5759,5753,5762,5764],{},[3837,5760,5761],{},"7. Tor Node Analysis",[843,5763,5093],{}," to classify the specific role of any Tor node. Different node types carry different penalties because they represent different risk profiles.",[863,5766,5767,5776],{},[866,5768,5769],{},[869,5770,5771,5774],{},[872,5772,5773],{},"Tor node type",[872,5775,5605],{},[885,5777,5778,5786,5793,5801,5808,5815,5822],{},[869,5779,5780,5783],{},[890,5781,5782],{},"Active running node",[890,5784,5785],{},"15",[869,5787,5788,5791],{},[890,5789,5790],{},"Exit node (base)",[890,5792,5718],{},[869,5794,5795,5798],{},[890,5796,5797],{},"Exit node (exit probability multiplier, up to +30)",[890,5799,5800],{},"dynamic",[869,5802,5803,5806],{},[890,5804,5805],{},"Web-capable exit node",[890,5807,5785],{},[869,5809,5810,5813],{},[890,5811,5812],{},"Guard node",[890,5814,5630],{},[869,5816,5817,5820],{},[890,5818,5819],{},"Bad exit (flagged by Tor directory)",[890,5821,5726],{},[869,5823,5824,5827],{},[890,5825,5826],{},"Obsolete version",[890,5828,5630],{},[830,5830,5831],{},"A high-probability exit node that is also flagged as a bad exit and running an obsolete version can accumulate 90 points from Tor analysis alone — enough to ban when combined with even minor signals from other checkers.",[830,5833,5834,5680,5837,5840],{},[3837,5835,5836],{},"8. Timezone Consistency",[843,5838,5839],{},"Timezone"," request header against the timezone inferred from the IP's geolocation. A browser reporting a Central European timezone from an IP geolocated to Hong Kong scores 20 points.",[830,5842,5843,5846],{},[3837,5844,5845],{},"9. Honeypot"," — checks the request path against a configurable list of trap URLs. Any request to a honeypot path scores an immediate ban. Legitimate users never visit URLs that are not linked anywhere in the application. Only crawlers following harvested or guessed paths hit them.",[830,5848,5849,5852,5853,1159,5856,5859],{},[3837,5850,5851],{},"10. Known Bad IPs"," — queries optional ",[843,5854,5855],{},"banned.mmdb",[843,5857,5858],{},"highRisk.mmdb"," files you maintain independently. Previously banned IPs score an instant ban. High-risk IPs score 30 points. This checker enables you to carry forward enforcement decisions across restarts and import external blocklists.",[4232,5861,5863],{"id":5862},"phase-two-the-heavy-checkers","Phase Two: The Heavy Checkers",[830,5865,5866],{},"The heavy phase runs only if the cheap phase did not trigger a ban. These seven checks require async operations — cache reads, timing calculations, database queries, and header analysis. They are deferred to the second phase because they are more expensive.",[830,5868,5869,5872,5873,5875],{},[3837,5870,5871],{},"11. Behavior Rate Verification"," — counts requests from this ",[843,5874,4002],{}," within a sliding window (default: 60 seconds, threshold: 30 requests). Exceeding the threshold scores 60 points. Unlike a simple IP-based rate limiter, this checker tracks per-session request rates. A bot that uses many IPs but reuses the same session cookie still triggers it.",[830,5877,5878,5881],{},[3837,5879,5880],{},"12. Proxy, ISP, and Cookie Verification"," — combines several signals into a single checker.",[863,5883,5884,5892],{},[866,5885,5886],{},[869,5887,5888,5890],{},[872,5889,5602],{},[872,5891,5605],{},[885,5893,5894,5905,5914,5922,5930,5938,5945],{},[869,5895,5896,5902],{},[890,5897,5898,5899,5901],{},"Missing ",[843,5900,4002],{}," cookie",[890,5903,5904],{},"80",[869,5906,5907,5912],{},[890,5908,5909,5910,4623],{},"Proxy detected (from ",[843,5911,5077],{},[890,5913,5726],{},[869,5915,5916,5919],{},[890,5917,5918],{},"Multi-source proxy confirmation (2-3 sources)",[890,5920,5921],{},"+10",[869,5923,5924,5927],{},[890,5925,5926],{},"Multi-source proxy confirmation (4+ sources)",[890,5928,5929],{},"+20",[869,5931,5932,5935],{},[890,5933,5934],{},"Hosting provider detected",[890,5936,5937],{},"50",[869,5939,5940,5943],{},[890,5941,5942],{},"Unknown ISP",[890,5944,5630],{},[869,5946,5947,5950],{},[890,5948,5949],{},"Unknown ORG",[890,5951,5630],{},[830,5953,3835,5954,5956],{},[843,5955,4002],{}," cookie check is the single highest-penalty individual signal in the pipeline at 80 points. Any request that does not carry a cookie is one triggering event away from a ban. This matters because the cookie is set on the very first request — a missing cookie on a subsequent request means either the client is rejecting cookies (a strong bot signal) or the request is coming from a tool that does not preserve session state.",[830,5958,5959,5962,5963,5965,5966,5969],{},[3837,5960,5961],{},"13. Session Coherence"," — uses the ",[843,5964,4002],{}," to retrieve the session's last known path from the session cache, then validates the incoming request's ",[843,5967,5968],{},"Referer"," header.",[863,5971,5972,5980],{},[866,5973,5974],{},[869,5975,5976,5978],{},[872,5977,5602],{},[872,5979,5605],{},[885,5981,5982,5994,6003],{},[869,5983,5984,5992],{},[890,5985,5898,5986,5988,5989,4623],{},[843,5987,5968],{}," on a same-origin request (",[843,5990,5991],{},"Sec-Fetch-Site: same-origin",[890,5993,5718],{},[869,5995,5996,6001],{},[890,5997,5998,6000],{},[843,5999,5968],{}," domain does not match the application domain",[890,6002,5638],{},[869,6004,6005,6010],{},[890,6006,6007,6009],{},[843,6008,5968],{}," path does not match the recorded last path",[890,6011,5630],{},[830,6013,6014,6015,6017],{},"Real browsers send a ",[843,6016,5968],{}," header when navigating within the same origin. Tools and scrapers that issue requests directly do not. A bot that correctly spoofs headers but does not correctly maintain session path history fails this check across multiple requests.",[830,6019,6020,6023],{},[3837,6021,6022],{},"14. Velocity Fingerprinting"," — collects timestamps for the last 10 requests from this session (minimum 5 required to evaluate) and computes the coefficient of variation (CV) of the inter-request intervals. The CV measures the relative variability of a set of values — a CV near zero means all intervals are nearly identical, which is characteristic of programmatic request scheduling.",[1089,6025,6028],{"className":6026,"code":6027,"language":1095},[1092],"CV = standard deviation \u002F mean\n\nCV \u003C 0.1 → timing too regular → penalty: 40\n",[843,6029,6027],{"__ignoreMap":811},[830,6031,6032],{},"Human browsing intervals are naturally irregular. Page load times, reading time, and click latency all vary. A bot that fires requests on a fixed timer — even a slow one — produces a CV far below the 0.1 threshold.",[830,6034,6035,6038],{},[3837,6036,6037],{},"15. User-Agent and Header Analysis"," — extends the cheap-phase fingerprint check with deeper inspection.",[863,6040,6041,6049],{},[866,6042,6043],{},[869,6044,6045,6047],{},[872,6046,5602],{},[872,6048,5605],{},[885,6050,6051,6058,6065,6073,6080],{},[869,6052,6053,6056],{},[890,6054,6055],{},"Headless browser detected (Puppeteer, Selenium, Playwright, PhantomJS)",[890,6057,5615],{},[869,6059,6060,6063],{},[890,6061,6062],{},"User-agent shorter than 10 characters",[890,6064,5904],{},[869,6066,6067,6070],{},[890,6068,6069],{},"Header anomaly score too high",[890,6071,6072],{},"variable",[869,6074,6075,6078],{},[890,6076,6077],{},"Path traversal attempt detected",[890,6079,6072],{},[869,6081,6082,6085],{},[890,6083,6084],{},"XSS scripting attempt detected",[890,6086,6072],{},[830,6088,6089,6092],{},[3837,6090,6091],{},"16. Geolocation Validation"," — penalizes missing geolocation data across nine dimensions: country, region, city, latitude\u002Flongitude, timezone, subregion, phone prefix, district, and continent. Each missing dimension scores 10 points. A request from an IP with no geolocation coverage can accumulate up to 90 points from this checker alone, making it trivially over the ban threshold when combined with any other signal. The checker also supports a configurable banned-country list.",[830,6094,6095,5753,6098,6101],{},[3837,6096,6097],{},"17. Known Bad User-Agents",[843,6099,6100],{},"useragent.mdb"," against the full user-agent string. The LMDB database stores patterns compiled from community-maintained lists of bot and scraper signatures, each rated by severity.",[863,6103,6104,6113],{},[866,6105,6106],{},[869,6107,6108,6111],{},[872,6109,6110],{},"Severity",[872,6112,5605],{},[885,6114,6115,6122,6129,6136],{},[869,6116,6117,6120],{},[890,6118,6119],{},"Critical",[890,6121,5615],{},[869,6123,6124,6127],{},[890,6125,6126],{},"High",[890,6128,5904],{},[869,6130,6131,6134],{},[890,6132,6133],{},"Medium",[890,6135,5638],{},[869,6137,6138,6141],{},[890,6139,6140],{},"Low",[890,6142,5630],{},[852,6144],{},[855,6146,6148],{"id":6147},"the-canary-cookie-bridging-sessions","The Canary Cookie: Bridging Sessions",[830,6150,3835,6151,6153,6154,6157],{},[843,6152,4002],{}," cookie is issued by the ",[843,6155,6156],{},"canaryCookieChecker"," middleware on the very first request from any browser. Its value is a 64-character hex string generated from 32 cryptographically random bytes.",[1089,6159,6161],{"className":1745,"code":6160,"language":1748,"meta":811,"style":811},"randomBytes(32).toString('hex')\n\u002F\u002F Example: \"a3f8e2c1d4b7a90f...\"  (64 hex characters)\n",[843,6162,6163,6190],{"__ignoreMap":811},[1179,6164,6165,6168,6170,6173,6176,6179,6181,6183,6186,6188],{"class":1181,"line":1182},[1179,6166,6167],{"class":1185},"randomBytes",[1179,6169,1968],{"class":1239},[1179,6171,6172],{"class":1330},"32",[1179,6174,6175],{"class":1239},").",[1179,6177,6178],{"class":1185},"toString",[1179,6180,1968],{"class":1239},[1179,6182,1780],{"class":1199},[1179,6184,6185],{"class":1203},"hex",[1179,6187,1780],{"class":1199},[1179,6189,2268],{"class":1239},[1179,6191,6192],{"class":1181,"line":812},[1179,6193,6194],{"class":4085},"\u002F\u002F Example: \"a3f8e2c1d4b7a90f...\"  (64 hex characters)\n",[830,6196,6197],{},"The cookie itself is opaque — it carries no embedded data and cannot be decoded. All the meaningful state lives server-side, keyed on the cookie value.",[4232,6199,6201],{"id":6200},"cookie-attributes","Cookie Attributes",[1089,6203,6206],{"className":6204,"code":6205,"language":1095},[1092],"name:      canary_id\nhttpOnly:  true\nsameSite:  lax\nsecure:    true\npath:      \u002F\nmaxAge:    7,776,000,000 ms  (90 days)\n",[843,6207,6205],{"__ignoreMap":811},[830,6209,3835,6210,6212],{},[843,6211,3853],{}," attribute prevents JavaScript from reading the cookie, blocking the class of attacks where a page script exfiltrates the cookie and reuses it from a different client. The 90-day maxAge matches the outer boundary for legitimate long-running sessions.",[4232,6214,6216],{"id":6215},"what-the-server-stores","What the Server Stores",[830,6218,6219,6220,6222],{},"When Bot Detector issues a ",[843,6221,4002],{},", it begins building a persistent record keyed on that value. This record accumulates across every subsequent request.",[830,6224,6225],{},[3837,6226,6227],{},"Visitor record (database, persistent):",[1089,6229,6231],{"className":1745,"code":6230,"language":1748,"meta":811,"style":811},"{\n  visitorId: UUID,\n  cookie: canary_id,\n  userAgent: string,\n  ipAddress: string,\n  device_type: string,\n  browser: string,\n  is_bot: boolean,\n  first_seen: timestamp,\n  last_seen: timestamp,\n  request_count: number,\n  deviceVendor: string,\n  deviceModel: string,\n  browserType: string,\n  browserVersion: string,\n  os: string,\n  activity_score: number,\n  country: string,\n  region: string,\n  city: string,\n  timezone: string,\n  \u002F\u002F ...additional geolocation fields\n}\n",[843,6232,6233,6237,6247,6256,6266,6275,6284,6293,6303,6313,6322,6332,6341,6350,6359,6368,6377,6386,6395,6404,6413,6422,6427],{"__ignoreMap":811},[1179,6234,6235],{"class":1181,"line":1182},[1179,6236,1240],{"class":1239},[1179,6238,6239,6242,6245],{"class":1181,"line":812},[1179,6240,6241],{"class":1239},"  visitorId: ",[1179,6243,6244],{"class":2252},"UUID",[1179,6246,1036],{"class":1239},[1179,6248,6249,6252,6254],{"class":1181,"line":1212},[1179,6250,6251],{"class":1239},"  cookie: ",[1179,6253,4002],{"class":1770},[1179,6255,1036],{"class":1239},[1179,6257,6258,6261,6264],{"class":1181,"line":1283},[1179,6259,6260],{"class":1239},"  userAgent: ",[1179,6262,6263],{"class":1770},"string",[1179,6265,1036],{"class":1239},[1179,6267,6268,6271,6273],{"class":1181,"line":1298},[1179,6269,6270],{"class":1239},"  ipAddress: ",[1179,6272,6263],{"class":1770},[1179,6274,1036],{"class":1239},[1179,6276,6277,6280,6282],{"class":1181,"line":1319},[1179,6278,6279],{"class":1239},"  device_type: ",[1179,6281,6263],{"class":1770},[1179,6283,1036],{"class":1239},[1179,6285,6286,6289,6291],{"class":1181,"line":1336},[1179,6287,6288],{"class":1239},"  browser: ",[1179,6290,6263],{"class":1770},[1179,6292,1036],{"class":1239},[1179,6294,6295,6298,6301],{"class":1181,"line":1353},[1179,6296,6297],{"class":1239},"  is_bot: ",[1179,6299,6300],{"class":1770},"boolean",[1179,6302,1036],{"class":1239},[1179,6304,6305,6308,6311],{"class":1181,"line":1374},[1179,6306,6307],{"class":1239},"  first_seen: ",[1179,6309,6310],{"class":1770},"timestamp",[1179,6312,1036],{"class":1239},[1179,6314,6315,6318,6320],{"class":1181,"line":1395},[1179,6316,6317],{"class":1239},"  last_seen: ",[1179,6319,6310],{"class":1770},[1179,6321,1036],{"class":1239},[1179,6323,6324,6327,6330],{"class":1181,"line":1415},[1179,6325,6326],{"class":1239},"  request_count: ",[1179,6328,6329],{"class":1770},"number",[1179,6331,1036],{"class":1239},[1179,6333,6334,6337,6339],{"class":1181,"line":1432},[1179,6335,6336],{"class":1239},"  deviceVendor: ",[1179,6338,6263],{"class":1770},[1179,6340,1036],{"class":1239},[1179,6342,6343,6346,6348],{"class":1181,"line":1450},[1179,6344,6345],{"class":1239},"  deviceModel: ",[1179,6347,6263],{"class":1770},[1179,6349,1036],{"class":1239},[1179,6351,6352,6355,6357],{"class":1181,"line":1456},[1179,6353,6354],{"class":1239},"  browserType: ",[1179,6356,6263],{"class":1770},[1179,6358,1036],{"class":1239},[1179,6360,6361,6364,6366],{"class":1181,"line":2079},[1179,6362,6363],{"class":1239},"  browserVersion: ",[1179,6365,6263],{"class":1770},[1179,6367,1036],{"class":1239},[1179,6369,6370,6373,6375],{"class":1181,"line":2090},[1179,6371,6372],{"class":1239},"  os: ",[1179,6374,6263],{"class":1770},[1179,6376,1036],{"class":1239},[1179,6378,6379,6382,6384],{"class":1181,"line":2096},[1179,6380,6381],{"class":1239},"  activity_score: ",[1179,6383,6329],{"class":1770},[1179,6385,1036],{"class":1239},[1179,6387,6388,6391,6393],{"class":1181,"line":2102},[1179,6389,6390],{"class":1239},"  country: ",[1179,6392,6263],{"class":1770},[1179,6394,1036],{"class":1239},[1179,6396,6397,6400,6402],{"class":1181,"line":2108},[1179,6398,6399],{"class":1239},"  region: ",[1179,6401,6263],{"class":1770},[1179,6403,1036],{"class":1239},[1179,6405,6406,6409,6411],{"class":1181,"line":2324},[1179,6407,6408],{"class":1239},"  city: ",[1179,6410,6263],{"class":1770},[1179,6412,1036],{"class":1239},[1179,6414,6415,6418,6420],{"class":1181,"line":2347},[1179,6416,6417],{"class":1239},"  timezone: ",[1179,6419,6263],{"class":1770},[1179,6421,1036],{"class":1239},[1179,6423,6424],{"class":1181,"line":2369},[1179,6425,6426],{"class":4085},"  \u002F\u002F ...additional geolocation fields\n",[1179,6428,6429],{"class":1181,"line":2389},[1179,6430,1459],{"class":1239},[830,6432,6433],{},[3837,6434,6435],{},"In-memory caches (fast lookup per request):",[863,6437,6438,6451],{},[866,6439,6440],{},[869,6441,6442,6445,6448],{},[872,6443,6444],{},"Cache",[872,6446,6447],{},"Key",[872,6449,6450],{},"What it holds",[885,6452,6453,6470,6487,6504,6518,6535],{},[869,6454,6455,6460,6464],{},[890,6456,6457],{},[843,6458,6459],{},"visitorCache",[890,6461,6462],{},[843,6463,4002],{},[890,6465,6466,6469],{},[843,6467,6468],{},"{ banned, visitor_id }"," — fast ban lookup",[869,6471,6472,6477,6481],{},[890,6473,6474],{},[843,6475,6476],{},"sessionCache",[890,6478,6479],{},[843,6480,4002],{},[890,6482,6483,6486],{},[843,6484,6485],{},"{ lastPath }"," — session coherence tracking",[869,6488,6489,6494,6498],{},[890,6490,6491],{},[843,6492,6493],{},"rateCache",[890,6495,6496],{},[843,6497,4002],{},[890,6499,6500,6503],{},[843,6501,6502],{},"{ score, timestamp, request_count }"," — behavioral rate",[869,6505,6506,6511,6515],{},[890,6507,6508],{},[843,6509,6510],{},"timingCache",[890,6512,6513],{},[843,6514,4002],{},[890,6516,6517],{},"Array of last 10 request timestamps — velocity fingerprint",[869,6519,6520,6525,6529],{},[890,6521,6522],{},[843,6523,6524],{},"reputationCache",[890,6526,6527],{},[843,6528,4002],{},[890,6530,6531,6534],{},[843,6532,6533],{},"{ isBot, score }"," — reputation healer state",[869,6536,6537,6542,6545],{},[890,6538,6539],{},[843,6540,6541],{},"dnsCache",[890,6543,6544],{},"IP",[890,6546,6547,6550],{},[843,6548,6549],{},"{ ip, trustedBot }"," — verified crawler result",[830,6552,6553],{},"The split between the persistent database record and the in-memory caches is intentional. The database record survives restarts and is queryable for analytics. The in-memory caches are ephemeral but fast — they hold exactly the data the pipeline needs per request, without deserializing a full database row.",[4232,6555,6557],{"id":6556},"the-canary-cookie-in-the-iam-service","The Canary Cookie in the IAM Service",[830,6559,6560],{},"The IAM service runs Bot Detector as part of its own middleware chain. Every request to the IAM service — login, logout, token rotation, MFA — passes through the same 17-checker pipeline before reaching any authentication logic.",[830,6562,6563,6564,6566,6567,6569,6570,6572],{},"When Bot Detector passes a request through, the IAM service reads the ",[843,6565,4002],{}," cookie and stores it alongside the refresh token family for that session. The ",[843,6568,4251],{}," anomaly detection function, which runs during every token rotation attempt, includes a ",[843,6571,4002],{}," binding check as one of its nine sequential verifications.",[830,6574,6575,6576,6578,6579,6581],{},"If the ",[843,6577,4002],{}," on a rotation request does not match the one recorded when the session was originally created, the anomaly detector triggers. Depending on the severity, it either sends an MFA challenge to the user's email or revokes the session entirely. This means an attacker who steals a valid refresh token but makes the rotation request from a different device — one with a different ",[843,6580,4002],{}," — cannot complete the rotation without also accessing the user's email.",[852,6583],{},[855,6585,6587],{"id":6586},"walking-through-a-bot-request","Walking Through a Bot Request",[830,6589,6590],{},"To make the pipeline concrete, here is what happens when a credential-stuffing bot attempts a login.",[830,6592,6593,6594,6597,6598,6601],{},"The bot sends a ",[843,6595,6596],{},"POST \u002Fauth\u002Fuser\u002Flogin"," request with a valid email and password combination. It uses a Python ",[843,6599,6600],{},"requests"," library with a spoofed user-agent string, from a residential proxy pool. It sends one request every 4 seconds on a fixed timer.",[830,6603,6604],{},[3837,6605,6606],{},"Cheap phase results:",[6608,6609,6610,6614,6617,6628,6634,6637],"ul",{},[6611,6612,6613],"li",{},"IP Validation: passes (valid IPv4).",[6611,6615,6616],{},"Good\u002FBad Bot: IP is not on the good-bot list. No instant ban.",[6611,6618,6619,6620,6623,6624,6627],{},"Browser and Device Fingerprint: The user-agent parses as Chrome, but the library headers are subtly wrong — no ",[843,6621,6622],{},"sec-ch-ua"," header family, no ",[843,6625,6626],{},"sec-fetch-*"," headers. Unknown browser type: +10. Impossible header combination: +30. Running total: 40.",[6611,6629,6630,6631,6633],{},"Locale Map: The ",[843,6632,5683],{}," header is missing. +20. Running total: 60.",[6611,6635,6636],{},"Known Threats: The residential proxy IP happens to appear on the FireHOL L3 list (a 30-day tracked threat). +20. Running total: 80.",[6611,6638,6639],{},"ASN Classification: The proxy's ASN is classified as hosting with low visibility. +20 + +10. Running total exceeds 100.",[830,6641,6642,6645],{},[3837,6643,6644],{},"The pipeline stops at the cheap phase."," The request receives a 403 response before the login handler runs. No database query for the user record. No password check. No rate limiter on the login endpoint needs to absorb the request.",[830,6647,6648],{},"Now consider a more sophisticated bot — one that uses a real browser, a real residential IP, and carefully spoofs all headers. The cheap phase may score only 10-20 points.",[830,6650,6651],{},[3837,6652,6653],{},"Heavy phase results:",[6608,6655,6656,6659,6669],{},[6611,6657,6658],{},"Behavior Rate: The bot fires at exactly 4-second intervals. After 5 requests, the velocity fingerprint computes CV = 0.02. +40. Running total: 50-60.",[6611,6660,6661,6662,6665,6666,6668],{},"Session Coherence: The bot navigates directly to ",[843,6663,6664],{},"\u002Fauth\u002Fuser\u002Flogin"," without going through the home page first. The ",[843,6667,5968],{}," header is absent on what looks like same-origin navigation. +20. Running total: 70-80.",[6611,6670,6671],{},"User-Agent and Header Analysis: Header mismatch and lack of acceptable HTTP configurations indicate automated access. +60. Running total: 130+.",[830,6673,6674,6677],{},[3837,6675,6676],{},"The pipeline stops at the heavy phase."," Even a well-configured bot that passes the cheap phase reveals itself through timing regularity, navigation patterns, and header analysis.",[852,6679],{},[855,6681,204],{"id":6682},"configuration",[830,6684,6685],{},"A realistic Bot Detector configuration that enables the full pipeline looks like this:",[1089,6687,6689],{"className":1745,"code":6688,"language":1748,"meta":811,"style":811},"import { configuration } from 'bot-detector'\n\nawait configuration({\n  store: {\n    main: { driver: 'sqlite', name: '.\u002Fbot-detector.db' }\n  },\n\n  banScore: 100,\n  maxScore: 100,\n  restoredReputationPoints: 10,\n  setNewComputedScore: false,\n\n  whiteList: ['203.0.113.0\u002F24'],\n\n  checkers: {\n    enableIpChecks: { enable: true, penalties: 10 },\n\n    enableGoodBotsChecks: {\n      enable: true,\n      banUnlistedBots: true,\n      penalties: 100\n    },\n\n    enableBrowserAndDeviceChecks: { enable: true },\n\n    localeMapsCheck: { enable: true },\n\n    enableKnownThreatsDetections: {\n      enable: true,\n      penalties: {\n        anonymityNetwork: 20,\n        fireholL1: 40,\n        fireholL2: 30,\n        fireholL3: 20,\n        fireholL4: 10\n      }\n    },\n\n    enableAsnClassification: { enable: true },\n\n    enableTorAnalysis: { enable: true },\n\n    enableTimezoneConsistency: { enable: true },\n\n    honeypot: {\n      enable: true,\n      paths: ['\u002Fadmin', '\u002F.env', '\u002Fwp-login.php', '\u002Fxmlrpc.php']\n    },\n\n    enableKnownBadIpsCheck: { enable: true },\n\n    enableBehaviorRateCheck: {\n      enable: true,\n      behavioral_window: 60_000,\n      behavioral_threshold: 30,\n      penalties: 60\n    },\n\n    enableProxyIspCookiesChecks: { enable: true },\n\n    enableSessionCoherence: { enable: true },\n\n    enableVelocityFingerprint: {\n      enable: true,\n      cvThreshold: 0.1\n    },\n\n    enableUaAndHeaderChecks: { enable: true },\n\n    enableGeoChecks: {\n      enable: true,\n      bannedCountries: []\n    },\n\n    knownBadUserAgents: { enable: true }\n  }\n})\n",[843,6690,6691,6710,6714,6722,6731,6767,6772,6776,6786,6796,6806,6816,6820,6838,6842,6851,6879,6883,6892,6903,6914,6924,6929,6933,6950,6954,6971,6975,6984,6994,7002,7014,7026,7037,7048,7057,7061,7065,7069,7086,7091,7109,7114,7132,7137,7147,7158,7204,7209,7214,7232,7237,7247,7258,7271,7283,7293,7298,7303,7321,7326,7344,7349,7359,7370,7381,7386,7391,7409,7414,7424,7435,7446,7451,7456,7474,7479],{"__ignoreMap":811},[1179,6692,6693,6695,6697,6699,6701,6703,6705,6708],{"class":1181,"line":1182},[1179,6694,1851],{"class":1755},[1179,6696,1854],{"class":1239},[1179,6698,6682],{"class":1770},[1179,6700,1860],{"class":1239},[1179,6702,1863],{"class":1755},[1179,6704,1819],{"class":1199},[1179,6706,6707],{"class":1203},"bot-detector",[1179,6709,1825],{"class":1199},[1179,6711,6712],{"class":1181,"line":812},[1179,6713,1935],{"emptyLinePlaceholder":8},[1179,6715,6716,6718,6720],{"class":1181,"line":1212},[1179,6717,5484],{"class":1755},[1179,6719,5487],{"class":1185},[1179,6721,1765],{"class":1239},[1179,6723,6724,6727,6729],{"class":1181,"line":1283},[1179,6725,6726],{"class":1770},"  store",[1179,6728,1255],{"class":1774},[1179,6730,1295],{"class":1239},[1179,6732,6733,6736,6738,6740,6743,6745,6747,6750,6752,6754,6756,6758,6760,6763,6765],{"class":1181,"line":1298},[1179,6734,6735],{"class":1770},"    main",[1179,6737,1255],{"class":1774},[1179,6739,1854],{"class":1239},[1179,6741,6742],{"class":1770},"driver",[1179,6744,1255],{"class":1774},[1179,6746,1819],{"class":1199},[1179,6748,6749],{"class":1203},"sqlite",[1179,6751,1780],{"class":1199},[1179,6753,1032],{"class":1239},[1179,6755,1121],{"class":1770},[1179,6757,1255],{"class":1774},[1179,6759,1819],{"class":1199},[1179,6761,6762],{"class":1203},".\u002Fbot-detector.db",[1179,6764,1780],{"class":1199},[1179,6766,3076],{"class":1239},[1179,6768,6769],{"class":1181,"line":1319},[1179,6770,6771],{"class":1239},"  },\n",[1179,6773,6774],{"class":1181,"line":1336},[1179,6775,1935],{"emptyLinePlaceholder":8},[1179,6777,6778,6780,6782,6784],{"class":1181,"line":1353},[1179,6779,5494],{"class":1770},[1179,6781,1255],{"class":1774},[1179,6783,5499],{"class":1330},[1179,6785,1036],{"class":1239},[1179,6787,6788,6790,6792,6794],{"class":1181,"line":1374},[1179,6789,5506],{"class":1770},[1179,6791,1255],{"class":1774},[1179,6793,5499],{"class":1330},[1179,6795,1036],{"class":1239},[1179,6797,6798,6800,6802,6804],{"class":1181,"line":1395},[1179,6799,5517],{"class":1770},[1179,6801,1255],{"class":1774},[1179,6803,5522],{"class":1330},[1179,6805,1036],{"class":1239},[1179,6807,6808,6810,6812,6814],{"class":1181,"line":1415},[1179,6809,5529],{"class":1770},[1179,6811,1255],{"class":1774},[1179,6813,1807],{"class":1195},[1179,6815,1036],{"class":1239},[1179,6817,6818],{"class":1181,"line":1432},[1179,6819,1935],{"emptyLinePlaceholder":8},[1179,6821,6822,6825,6827,6829,6831,6834,6836],{"class":1181,"line":1450},[1179,6823,6824],{"class":1770},"  whiteList",[1179,6826,1255],{"class":1774},[1179,6828,1777],{"class":1239},[1179,6830,1780],{"class":1199},[1179,6832,6833],{"class":1203},"203.0.113.0\u002F24",[1179,6835,1780],{"class":1199},[1179,6837,1788],{"class":1239},[1179,6839,6840],{"class":1181,"line":1456},[1179,6841,1935],{"emptyLinePlaceholder":8},[1179,6843,6844,6847,6849],{"class":1181,"line":2079},[1179,6845,6846],{"class":1770},"  checkers",[1179,6848,1255],{"class":1774},[1179,6850,1295],{"class":1239},[1179,6852,6853,6856,6858,6860,6863,6865,6867,6869,6872,6874,6876],{"class":1181,"line":2090},[1179,6854,6855],{"class":1770},"    enableIpChecks",[1179,6857,1255],{"class":1774},[1179,6859,1854],{"class":1239},[1179,6861,6862],{"class":1770},"enable",[1179,6864,1255],{"class":1774},[1179,6866,1258],{"class":1195},[1179,6868,1032],{"class":1239},[1179,6870,6871],{"class":1770},"penalties",[1179,6873,1255],{"class":1774},[1179,6875,5522],{"class":1330},[1179,6877,6878],{"class":1239}," },\n",[1179,6880,6881],{"class":1181,"line":2096},[1179,6882,1935],{"emptyLinePlaceholder":8},[1179,6884,6885,6888,6890],{"class":1181,"line":2102},[1179,6886,6887],{"class":1770},"    enableGoodBotsChecks",[1179,6889,1255],{"class":1774},[1179,6891,1295],{"class":1239},[1179,6893,6894,6897,6899,6901],{"class":1181,"line":2108},[1179,6895,6896],{"class":1770},"      enable",[1179,6898,1255],{"class":1774},[1179,6900,1258],{"class":1195},[1179,6902,1036],{"class":1239},[1179,6904,6905,6908,6910,6912],{"class":1181,"line":2324},[1179,6906,6907],{"class":1770},"      banUnlistedBots",[1179,6909,1255],{"class":1774},[1179,6911,1258],{"class":1195},[1179,6913,1036],{"class":1239},[1179,6915,6916,6919,6921],{"class":1181,"line":2347},[1179,6917,6918],{"class":1770},"      penalties",[1179,6920,1255],{"class":1774},[1179,6922,6923],{"class":1330}," 100\n",[1179,6925,6926],{"class":1181,"line":2369},[1179,6927,6928],{"class":1239},"    },\n",[1179,6930,6931],{"class":1181,"line":2389},[1179,6932,1935],{"emptyLinePlaceholder":8},[1179,6934,6935,6938,6940,6942,6944,6946,6948],{"class":1181,"line":2395},[1179,6936,6937],{"class":1770},"    enableBrowserAndDeviceChecks",[1179,6939,1255],{"class":1774},[1179,6941,1854],{"class":1239},[1179,6943,6862],{"class":1770},[1179,6945,1255],{"class":1774},[1179,6947,1258],{"class":1195},[1179,6949,6878],{"class":1239},[1179,6951,6952],{"class":1181,"line":2437},[1179,6953,1935],{"emptyLinePlaceholder":8},[1179,6955,6956,6959,6961,6963,6965,6967,6969],{"class":1181,"line":2449},[1179,6957,6958],{"class":1770},"    localeMapsCheck",[1179,6960,1255],{"class":1774},[1179,6962,1854],{"class":1239},[1179,6964,6862],{"class":1770},[1179,6966,1255],{"class":1774},[1179,6968,1258],{"class":1195},[1179,6970,6878],{"class":1239},[1179,6972,6973],{"class":1181,"line":2454},[1179,6974,1935],{"emptyLinePlaceholder":8},[1179,6976,6977,6980,6982],{"class":1181,"line":2459},[1179,6978,6979],{"class":1770},"    enableKnownThreatsDetections",[1179,6981,1255],{"class":1774},[1179,6983,1295],{"class":1239},[1179,6985,6986,6988,6990,6992],{"class":1181,"line":2465},[1179,6987,6896],{"class":1770},[1179,6989,1255],{"class":1774},[1179,6991,1258],{"class":1195},[1179,6993,1036],{"class":1239},[1179,6995,6996,6998,7000],{"class":1181,"line":2470},[1179,6997,6918],{"class":1770},[1179,6999,1255],{"class":1774},[1179,7001,1295],{"class":1239},[1179,7003,7004,7007,7009,7012],{"class":1181,"line":2475},[1179,7005,7006],{"class":1770},"        anonymityNetwork",[1179,7008,1255],{"class":1774},[1179,7010,7011],{"class":1330}," 20",[1179,7013,1036],{"class":1239},[1179,7015,7016,7019,7021,7024],{"class":1181,"line":2504},[1179,7017,7018],{"class":1770},"        fireholL1",[1179,7020,1255],{"class":1774},[1179,7022,7023],{"class":1330}," 40",[1179,7025,1036],{"class":1239},[1179,7027,7028,7031,7033,7035],{"class":1181,"line":2539},[1179,7029,7030],{"class":1770},"        fireholL2",[1179,7032,1255],{"class":1774},[1179,7034,2074],{"class":1330},[1179,7036,1036],{"class":1239},[1179,7038,7039,7042,7044,7046],{"class":1181,"line":2544},[1179,7040,7041],{"class":1770},"        fireholL3",[1179,7043,1255],{"class":1774},[1179,7045,7011],{"class":1330},[1179,7047,1036],{"class":1239},[1179,7049,7050,7053,7055],{"class":1181,"line":2549},[1179,7051,7052],{"class":1770},"        fireholL4",[1179,7054,1255],{"class":1774},[1179,7056,2087],{"class":1330},[1179,7058,7059],{"class":1181,"line":2554},[1179,7060,2093],{"class":1239},[1179,7062,7063],{"class":1181,"line":2565},[1179,7064,6928],{"class":1239},[1179,7066,7067],{"class":1181,"line":2580},[1179,7068,1935],{"emptyLinePlaceholder":8},[1179,7070,7071,7074,7076,7078,7080,7082,7084],{"class":1181,"line":2591},[1179,7072,7073],{"class":1770},"    enableAsnClassification",[1179,7075,1255],{"class":1774},[1179,7077,1854],{"class":1239},[1179,7079,6862],{"class":1770},[1179,7081,1255],{"class":1774},[1179,7083,1258],{"class":1195},[1179,7085,6878],{"class":1239},[1179,7087,7089],{"class":1181,"line":7088},40,[1179,7090,1935],{"emptyLinePlaceholder":8},[1179,7092,7094,7097,7099,7101,7103,7105,7107],{"class":1181,"line":7093},41,[1179,7095,7096],{"class":1770},"    enableTorAnalysis",[1179,7098,1255],{"class":1774},[1179,7100,1854],{"class":1239},[1179,7102,6862],{"class":1770},[1179,7104,1255],{"class":1774},[1179,7106,1258],{"class":1195},[1179,7108,6878],{"class":1239},[1179,7110,7112],{"class":1181,"line":7111},42,[1179,7113,1935],{"emptyLinePlaceholder":8},[1179,7115,7117,7120,7122,7124,7126,7128,7130],{"class":1181,"line":7116},43,[1179,7118,7119],{"class":1770},"    enableTimezoneConsistency",[1179,7121,1255],{"class":1774},[1179,7123,1854],{"class":1239},[1179,7125,6862],{"class":1770},[1179,7127,1255],{"class":1774},[1179,7129,1258],{"class":1195},[1179,7131,6878],{"class":1239},[1179,7133,7135],{"class":1181,"line":7134},44,[1179,7136,1935],{"emptyLinePlaceholder":8},[1179,7138,7140,7143,7145],{"class":1181,"line":7139},45,[1179,7141,7142],{"class":1770},"    honeypot",[1179,7144,1255],{"class":1774},[1179,7146,1295],{"class":1239},[1179,7148,7150,7152,7154,7156],{"class":1181,"line":7149},46,[1179,7151,6896],{"class":1770},[1179,7153,1255],{"class":1774},[1179,7155,1258],{"class":1195},[1179,7157,1036],{"class":1239},[1179,7159,7161,7164,7166,7168,7170,7173,7175,7177,7179,7182,7184,7186,7188,7191,7193,7195,7197,7200,7202],{"class":1181,"line":7160},47,[1179,7162,7163],{"class":1770},"      paths",[1179,7165,1255],{"class":1774},[1179,7167,1777],{"class":1239},[1179,7169,1780],{"class":1199},[1179,7171,7172],{"class":1203},"\u002Fadmin",[1179,7174,1780],{"class":1199},[1179,7176,1032],{"class":1239},[1179,7178,1780],{"class":1199},[1179,7180,7181],{"class":1203},"\u002F.env",[1179,7183,1780],{"class":1199},[1179,7185,1032],{"class":1239},[1179,7187,1780],{"class":1199},[1179,7189,7190],{"class":1203},"\u002Fwp-login.php",[1179,7192,1780],{"class":1199},[1179,7194,1032],{"class":1239},[1179,7196,1780],{"class":1199},[1179,7198,7199],{"class":1203},"\u002Fxmlrpc.php",[1179,7201,1780],{"class":1199},[1179,7203,2860],{"class":1239},[1179,7205,7207],{"class":1181,"line":7206},48,[1179,7208,6928],{"class":1239},[1179,7210,7212],{"class":1181,"line":7211},49,[1179,7213,1935],{"emptyLinePlaceholder":8},[1179,7215,7217,7220,7222,7224,7226,7228,7230],{"class":1181,"line":7216},50,[1179,7218,7219],{"class":1770},"    enableKnownBadIpsCheck",[1179,7221,1255],{"class":1774},[1179,7223,1854],{"class":1239},[1179,7225,6862],{"class":1770},[1179,7227,1255],{"class":1774},[1179,7229,1258],{"class":1195},[1179,7231,6878],{"class":1239},[1179,7233,7235],{"class":1181,"line":7234},51,[1179,7236,1935],{"emptyLinePlaceholder":8},[1179,7238,7240,7243,7245],{"class":1181,"line":7239},52,[1179,7241,7242],{"class":1770},"    enableBehaviorRateCheck",[1179,7244,1255],{"class":1774},[1179,7246,1295],{"class":1239},[1179,7248,7250,7252,7254,7256],{"class":1181,"line":7249},53,[1179,7251,6896],{"class":1770},[1179,7253,1255],{"class":1774},[1179,7255,1258],{"class":1195},[1179,7257,1036],{"class":1239},[1179,7259,7261,7264,7266,7269],{"class":1181,"line":7260},54,[1179,7262,7263],{"class":1770},"      behavioral_window",[1179,7265,1255],{"class":1774},[1179,7267,7268],{"class":1330}," 60_000",[1179,7270,1036],{"class":1239},[1179,7272,7274,7277,7279,7281],{"class":1181,"line":7273},55,[1179,7275,7276],{"class":1770},"      behavioral_threshold",[1179,7278,1255],{"class":1774},[1179,7280,2074],{"class":1330},[1179,7282,1036],{"class":1239},[1179,7284,7286,7288,7290],{"class":1181,"line":7285},56,[1179,7287,6918],{"class":1770},[1179,7289,1255],{"class":1774},[1179,7291,7292],{"class":1330}," 60\n",[1179,7294,7296],{"class":1181,"line":7295},57,[1179,7297,6928],{"class":1239},[1179,7299,7301],{"class":1181,"line":7300},58,[1179,7302,1935],{"emptyLinePlaceholder":8},[1179,7304,7306,7309,7311,7313,7315,7317,7319],{"class":1181,"line":7305},59,[1179,7307,7308],{"class":1770},"    enableProxyIspCookiesChecks",[1179,7310,1255],{"class":1774},[1179,7312,1854],{"class":1239},[1179,7314,6862],{"class":1770},[1179,7316,1255],{"class":1774},[1179,7318,1258],{"class":1195},[1179,7320,6878],{"class":1239},[1179,7322,7324],{"class":1181,"line":7323},60,[1179,7325,1935],{"emptyLinePlaceholder":8},[1179,7327,7329,7332,7334,7336,7338,7340,7342],{"class":1181,"line":7328},61,[1179,7330,7331],{"class":1770},"    enableSessionCoherence",[1179,7333,1255],{"class":1774},[1179,7335,1854],{"class":1239},[1179,7337,6862],{"class":1770},[1179,7339,1255],{"class":1774},[1179,7341,1258],{"class":1195},[1179,7343,6878],{"class":1239},[1179,7345,7347],{"class":1181,"line":7346},62,[1179,7348,1935],{"emptyLinePlaceholder":8},[1179,7350,7352,7355,7357],{"class":1181,"line":7351},63,[1179,7353,7354],{"class":1770},"    enableVelocityFingerprint",[1179,7356,1255],{"class":1774},[1179,7358,1295],{"class":1239},[1179,7360,7362,7364,7366,7368],{"class":1181,"line":7361},64,[1179,7363,6896],{"class":1770},[1179,7365,1255],{"class":1774},[1179,7367,1258],{"class":1195},[1179,7369,1036],{"class":1239},[1179,7371,7373,7376,7378],{"class":1181,"line":7372},65,[1179,7374,7375],{"class":1770},"      cvThreshold",[1179,7377,1255],{"class":1774},[1179,7379,7380],{"class":1330}," 0.1\n",[1179,7382,7384],{"class":1181,"line":7383},66,[1179,7385,6928],{"class":1239},[1179,7387,7389],{"class":1181,"line":7388},67,[1179,7390,1935],{"emptyLinePlaceholder":8},[1179,7392,7394,7397,7399,7401,7403,7405,7407],{"class":1181,"line":7393},68,[1179,7395,7396],{"class":1770},"    enableUaAndHeaderChecks",[1179,7398,1255],{"class":1774},[1179,7400,1854],{"class":1239},[1179,7402,6862],{"class":1770},[1179,7404,1255],{"class":1774},[1179,7406,1258],{"class":1195},[1179,7408,6878],{"class":1239},[1179,7410,7412],{"class":1181,"line":7411},69,[1179,7413,1935],{"emptyLinePlaceholder":8},[1179,7415,7417,7420,7422],{"class":1181,"line":7416},70,[1179,7418,7419],{"class":1770},"    enableGeoChecks",[1179,7421,1255],{"class":1774},[1179,7423,1295],{"class":1239},[1179,7425,7427,7429,7431,7433],{"class":1181,"line":7426},71,[1179,7428,6896],{"class":1770},[1179,7430,1255],{"class":1774},[1179,7432,1258],{"class":1195},[1179,7434,1036],{"class":1239},[1179,7436,7438,7441,7443],{"class":1181,"line":7437},72,[1179,7439,7440],{"class":1770},"      bannedCountries",[1179,7442,1255],{"class":1774},[1179,7444,7445],{"class":1239}," []\n",[1179,7447,7449],{"class":1181,"line":7448},73,[1179,7450,6928],{"class":1239},[1179,7452,7454],{"class":1181,"line":7453},74,[1179,7455,1935],{"emptyLinePlaceholder":8},[1179,7457,7459,7462,7464,7466,7468,7470,7472],{"class":1181,"line":7458},75,[1179,7460,7461],{"class":1770},"    knownBadUserAgents",[1179,7463,1255],{"class":1774},[1179,7465,1854],{"class":1239},[1179,7467,6862],{"class":1770},[1179,7469,1255],{"class":1774},[1179,7471,1258],{"class":1195},[1179,7473,3076],{"class":1239},[1179,7475,7477],{"class":1181,"line":7476},76,[1179,7478,1453],{"class":1239},[1179,7480,7482],{"class":1181,"line":7481},77,[1179,7483,1834],{"class":1239},[2595,7485,7486],{},[830,7487,7488],{},"Start with the cheap-phase checkers at conservative penalty values and raise them after observing traffic patterns. The FireHOL L4 level and ASN low-visibility penalties are the most likely to produce false positives on legitimate traffic from cloud-heavy regions.",[852,7490],{},[855,7492,7494],{"id":7493},"extending-the-pipeline-custom-checkers","Extending the Pipeline: Custom Checkers",[830,7496,7497,7498,1053],{},"Every built-in checker follows the same interface, and you can add your own with the exact same mechanism. The pipeline does not distinguish between built-in and custom checkers at runtime — they share the same scoring accumulation, the same short-circuit logic, and the same ",[843,7499,7500],{},"ValidationContext",[4232,7502,3835,7504,7507],{"id":7503},"the-ibotchecker-interface",[843,7505,7506],{},"IBotChecker"," Interface",[830,7509,7510,7511,7513,7514,7516],{},"A checker is a class that implements ",[843,7512,7506],{},". It declares which phase it belongs to, a condition that enables or disables it, and a ",[843,7515,636],{}," method that returns a numeric score and an array of reason codes.",[1089,7518,7520],{"className":1745,"code":7519,"language":1748,"meta":811,"style":811},"interface IBotChecker\u003CCode, TCustom = Record\u003Cstring, never>> {\n  name: string;\n  phase: 'cheap' | 'heavy';\n  isEnabled(config: BotDetectorConfig): boolean;\n  run(ctx: ValidationContext\u003CTCustom>, config: BotDetectorConfig):\n    | Promise\u003C{ score: number; reasons: Code[] }>\n    | { score: number; reasons: Code[] };\n}\n",[843,7521,7522,7559,7571,7596,7620,7653,7685,7702],{"__ignoreMap":811},[1179,7523,7524,7527,7530,7533,7537,7539,7542,7544,7547,7549,7551,7553,7556],{"class":1181,"line":1182},[1179,7525,7526],{"class":1957},"interface",[1179,7528,7529],{"class":3412}," IBotChecker",[1179,7531,7532],{"class":1239},"\u003C",[1179,7534,7536],{"class":7535},"sW-rI","Code",[1179,7538,1032],{"class":1239},[1179,7540,7541],{"class":7535},"TCustom",[1179,7543,2483],{"class":1254},[1179,7545,7546],{"class":7535}," Record",[1179,7548,7532],{"class":1239},[1179,7550,6263],{"class":3412},[1179,7552,1032],{"class":1239},[1179,7554,7555],{"class":3412},"never",[1179,7557,7558],{"class":1239},">> {\n",[1179,7560,7561,7564,7566,7568],{"class":1181,"line":812},[1179,7562,7563],{"class":1770},"  name",[1179,7565,1255],{"class":1254},[1179,7567,3423],{"class":3412},[1179,7569,7570],{"class":1239},";\n",[1179,7572,7573,7576,7578,7580,7583,7585,7587,7589,7592,7594],{"class":1181,"line":1212},[1179,7574,7575],{"class":1770},"  phase",[1179,7577,1255],{"class":1254},[1179,7579,1819],{"class":1199},[1179,7581,7582],{"class":1203},"cheap",[1179,7584,1780],{"class":1199},[1179,7586,3426],{"class":1254},[1179,7588,1819],{"class":1199},[1179,7590,7591],{"class":1203},"heavy",[1179,7593,1780],{"class":1199},[1179,7595,7570],{"class":1239},[1179,7597,7598,7601,7603,7606,7608,7611,7613,7615,7618],{"class":1181,"line":1283},[1179,7599,7600],{"class":1185},"  isEnabled",[1179,7602,1968],{"class":1239},[1179,7604,7605],{"class":1950},"config",[1179,7607,1255],{"class":1254},[1179,7609,7610],{"class":3412}," BotDetectorConfig",[1179,7612,4623],{"class":1239},[1179,7614,1255],{"class":1254},[1179,7616,7617],{"class":3412}," boolean",[1179,7619,7570],{"class":1239},[1179,7621,7622,7625,7627,7630,7632,7635,7637,7639,7642,7644,7646,7648,7650],{"class":1181,"line":1298},[1179,7623,7624],{"class":1185},"  run",[1179,7626,1968],{"class":1239},[1179,7628,7629],{"class":1950},"ctx",[1179,7631,1255],{"class":1254},[1179,7633,7634],{"class":3412}," ValidationContext",[1179,7636,7532],{"class":1239},[1179,7638,7541],{"class":7535},[1179,7640,7641],{"class":1239},">, ",[1179,7643,7605],{"class":1950},[1179,7645,1255],{"class":1254},[1179,7647,7610],{"class":3412},[1179,7649,4623],{"class":1239},[1179,7651,7652],{"class":1254},":\n",[1179,7654,7655,7658,7661,7664,7667,7669,7671,7674,7677,7679,7682],{"class":1181,"line":1319},[1179,7656,7657],{"class":1254},"    |",[1179,7659,7660],{"class":3412}," Promise",[1179,7662,7663],{"class":1239},"\u003C{ ",[1179,7665,7666],{"class":1770},"score",[1179,7668,1255],{"class":1254},[1179,7670,3560],{"class":3412},[1179,7672,7673],{"class":1239},"; ",[1179,7675,7676],{"class":1770},"reasons",[1179,7678,1255],{"class":1254},[1179,7680,7681],{"class":7535}," Code",[1179,7683,7684],{"class":1239},"[] }>\n",[1179,7686,7687,7689,7692,7694,7697,7699],{"class":1181,"line":1336},[1179,7688,7657],{"class":1254},[1179,7690,7691],{"class":1239}," { score: ",[1179,7693,6329],{"class":1770},[1179,7695,7696],{"class":1239},"; reasons: ",[1179,7698,7536],{"class":1770},[1179,7700,7701],{"class":1239},"[] };\n",[1179,7703,7704],{"class":1181,"line":1353},[1179,7705,1459],{"class":1239},[830,7707,3835,7708,7710],{},[843,7709,636],{}," method can be synchronous or async. Phase assignment is the only routing decision you make — everything else is handled by the pipeline.",[4232,7712,7714],{"id":7713},"what-the-pipeline-gives-you","What the Pipeline Gives You",[830,7716,7717,7718,7720,7721,7723],{},"Before your ",[843,7719,636],{}," method executes, the pipeline has already resolved every expensive lookup. All of this is available on ",[843,7722,7629],{}," at zero cost:",[863,7725,7726,7736],{},[866,7727,7728],{},[869,7729,7730,7733],{},[872,7731,7732],{},"Field",[872,7734,7735],{},"Contents",[885,7737,7738,7748,7758,7774,7784,7796,7818,7838,7851,7865,7878],{},[869,7739,7740,7745],{},[890,7741,7742],{},[843,7743,7744],{},"ctx.req",[890,7746,7747],{},"Full Express request (headers, path, cookies, method)",[869,7749,7750,7755],{},[890,7751,7752],{},[843,7753,7754],{},"ctx.ipAddress",[890,7756,7757],{},"Resolved client IP",[869,7759,7760,7765],{},[890,7761,7762],{},[843,7763,7764],{},"ctx.cookie",[890,7766,7767,7769,7770,7773],{},[843,7768,4002],{}," value, or ",[843,7771,7772],{},"undefined"," on first request",[869,7775,7776,7781],{},[890,7777,7778],{},[843,7779,7780],{},"ctx.geoData",[890,7782,7783],{},"Merged country, city, ASN, and proxy data",[869,7785,7786,7791],{},[890,7787,7788],{},[843,7789,7790],{},"ctx.tor",[890,7792,7793,7794],{},"Tor relay classification from ",[843,7795,5093],{},[869,7797,7798,7803],{},[890,7799,7800],{},[843,7801,7802],{},"ctx.bgp",[890,7804,7805,7806,1032,7809,1032,7812,1032,7815],{},"ASN routing data: ",[843,7807,7808],{},"asn_id",[843,7810,7811],{},"asn_name",[843,7813,7814],{},"classification",[843,7816,7817],{},"hits",[869,7819,7820,7825],{},[890,7821,7822],{},[843,7823,7824],{},"ctx.threatLevel",[890,7826,7827,7828,7830,7831,7834,7835],{},"Highest FireHOL tier matched (",[843,7829,4143],{},"–",[843,7832,7833],{},"4","), or ",[843,7836,7837],{},"null",[869,7839,7840,7845],{},[890,7841,7842],{},[843,7843,7844],{},"ctx.anon",[890,7846,7847,7850],{},[843,7848,7849],{},"true"," if IP is in the anonymity network database",[869,7852,7853,7858],{},[890,7854,7855],{},[843,7856,7857],{},"ctx.parsedUA",[890,7859,7860,7861,7864],{},"Parsed user-agent: browser, OS, device, ",[843,7862,7863],{},"browserType",", bot flags",[869,7866,7867,7872],{},[890,7868,7869],{},[843,7870,7871],{},"ctx.proxy",[890,7873,7874,7877],{},[843,7875,7876],{},"{ isProxy, proxyType }"," from proxy MMDB",[869,7879,7880,7885],{},[890,7881,7882],{},[843,7883,7884],{},"ctx.custom",[890,7886,7887,7888],{},"Your own per-request data, populated by ",[843,7889,7890],{},"buildCustomContext",[830,7892,7893,7896,7897,7900,7901,7904],{},[843,7894,7895],{},"ctx.bgp.classification"," is worth highlighting. The value ",[843,7898,7899],{},"\"Content\""," means the ASN is classified as a hosting or CDN network. ",[843,7902,7903],{},"\"Eyeballs\""," means residential or business internet. This single field lets a custom checker apply completely different logic for datacenter traffic versus consumer traffic without any additional lookup.",[4232,7906,7908],{"id":7907},"a-minimal-cheap-checker","A Minimal Cheap Checker",[830,7910,7911,7912,7914],{},"The example below penalises requests from a datacenter ASN that carry no ",[843,7913,5683],{}," header — a pattern common in automated clients that partially spoof browser headers but miss the locale details.",[1089,7916,7919],{"className":1745,"code":7917,"filename":7918,"language":1748,"meta":811,"style":811},"import { CheckerRegistry } from '@riavzon\u002Fbot-detector';\nimport type { IBotChecker, ValidationContext, BotDetectorConfig } from '@riavzon\u002Fbot-detector';\n\ntype Code = 'DATACENTER_NO_LOCALE' | 'BAD_BOT_DETECTED';\n\nclass DatacenterLocaleChecker implements IBotChecker\u003CCode> {\n  name = 'DatacenterLocaleChecker';\n  phase = 'cheap' as const;\n\n  isEnabled(_config: BotDetectorConfig): boolean {\n    return true;\n  }\n\n  run(ctx: ValidationContext, _config: BotDetectorConfig) {\n    const reasons: Code[] = [];\n    let score = 0;\n\n    const isHosting = ctx.bgp.classification === 'Content';\n    const hasLocale = Boolean(ctx.req.get('Accept-Language'));\n\n    if (isHosting && !hasLocale) {\n      score += 25;\n      reasons.push('DATACENTER_NO_LOCALE');\n    }\n\n    return { score, reasons };\n  }\n}\n\nCheckerRegistry.register(new DatacenterLocaleChecker());\n","datacenter-locale-checker.ts",[843,7920,7921,7943,7975,7979,8005,8009,8030,8045,8065,8069,8090,8098,8102,8106,8128,8148,8163,8167,8199,8236,8240,8260,8273,8294,8298,8302,8317,8321,8325,8329],{"__ignoreMap":811},[1179,7922,7923,7925,7927,7930,7932,7934,7936,7939,7941],{"class":1181,"line":1182},[1179,7924,1851],{"class":1755},[1179,7926,1854],{"class":1239},[1179,7928,7929],{"class":1770},"CheckerRegistry",[1179,7931,1860],{"class":1239},[1179,7933,1863],{"class":1755},[1179,7935,1819],{"class":1199},[1179,7937,7938],{"class":1203},"@riavzon\u002Fbot-detector",[1179,7940,1780],{"class":1199},[1179,7942,7570],{"class":1239},[1179,7944,7945,7947,7950,7952,7954,7956,7958,7960,7963,7965,7967,7969,7971,7973],{"class":1181,"line":812},[1179,7946,1851],{"class":1755},[1179,7948,7949],{"class":1755}," type",[1179,7951,1854],{"class":1239},[1179,7953,7506],{"class":1770},[1179,7955,1032],{"class":1239},[1179,7957,7500],{"class":1770},[1179,7959,1032],{"class":1239},[1179,7961,7962],{"class":1770},"BotDetectorConfig",[1179,7964,1860],{"class":1239},[1179,7966,1863],{"class":1755},[1179,7968,1819],{"class":1199},[1179,7970,7938],{"class":1203},[1179,7972,1780],{"class":1199},[1179,7974,7570],{"class":1239},[1179,7976,7977],{"class":1181,"line":1212},[1179,7978,1935],{"emptyLinePlaceholder":8},[1179,7980,7981,7984,7986,7988,7990,7993,7995,7997,7999,8001,8003],{"class":1181,"line":1283},[1179,7982,7983],{"class":1957},"type",[1179,7985,7681],{"class":3412},[1179,7987,2483],{"class":1254},[1179,7989,1819],{"class":1199},[1179,7991,7992],{"class":1203},"DATACENTER_NO_LOCALE",[1179,7994,1780],{"class":1199},[1179,7996,3426],{"class":1254},[1179,7998,1819],{"class":1199},[1179,8000,5583],{"class":1203},[1179,8002,1780],{"class":1199},[1179,8004,7570],{"class":1239},[1179,8006,8007],{"class":1181,"line":1298},[1179,8008,1935],{"emptyLinePlaceholder":8},[1179,8010,8011,8014,8018,8021,8023,8025,8027],{"class":1181,"line":1319},[1179,8012,8013],{"class":1957},"class",[1179,8015,8017],{"class":8016},"s5jk-"," DatacenterLocaleChecker",[1179,8019,8020],{"class":1957}," implements",[1179,8022,7529],{"class":3412},[1179,8024,7532],{"class":1239},[1179,8026,7536],{"class":7535},[1179,8028,8029],{"class":1239},"> {\n",[1179,8031,8032,8034,8036,8038,8041,8043],{"class":1181,"line":1336},[1179,8033,7563],{"class":1770},[1179,8035,2483],{"class":1254},[1179,8037,1819],{"class":1199},[1179,8039,8040],{"class":1203},"DatacenterLocaleChecker",[1179,8042,1780],{"class":1199},[1179,8044,7570],{"class":1239},[1179,8046,8047,8049,8051,8053,8055,8057,8060,8063],{"class":1181,"line":1353},[1179,8048,7575],{"class":1770},[1179,8050,2483],{"class":1254},[1179,8052,1819],{"class":1199},[1179,8054,7582],{"class":1203},[1179,8056,1780],{"class":1199},[1179,8058,8059],{"class":1755}," as",[1179,8061,8062],{"class":1957}," const",[1179,8064,7570],{"class":1239},[1179,8066,8067],{"class":1181,"line":1374},[1179,8068,1935],{"emptyLinePlaceholder":8},[1179,8070,8071,8073,8075,8078,8080,8082,8084,8086,8088],{"class":1181,"line":1395},[1179,8072,7600],{"class":1185},[1179,8074,1968],{"class":1239},[1179,8076,8077],{"class":1950},"_config",[1179,8079,1255],{"class":1254},[1179,8081,7610],{"class":3412},[1179,8083,4623],{"class":1239},[1179,8085,1255],{"class":1254},[1179,8087,7617],{"class":3412},[1179,8089,1295],{"class":1239},[1179,8091,8092,8094,8096],{"class":1181,"line":1415},[1179,8093,3048],{"class":1755},[1179,8095,1258],{"class":1195},[1179,8097,7570],{"class":1239},[1179,8099,8100],{"class":1181,"line":1432},[1179,8101,1453],{"class":1239},[1179,8103,8104],{"class":1181,"line":1450},[1179,8105,1935],{"emptyLinePlaceholder":8},[1179,8107,8108,8110,8112,8114,8116,8118,8120,8122,8124,8126],{"class":1181,"line":1456},[1179,8109,7624],{"class":1185},[1179,8111,1968],{"class":1239},[1179,8113,7629],{"class":1950},[1179,8115,1255],{"class":1254},[1179,8117,7634],{"class":3412},[1179,8119,1032],{"class":1239},[1179,8121,8077],{"class":1950},[1179,8123,1255],{"class":1254},[1179,8125,7610],{"class":3412},[1179,8127,2434],{"class":1239},[1179,8129,8130,8133,8136,8138,8140,8143,8145],{"class":1181,"line":2079},[1179,8131,8132],{"class":1957},"    const",[1179,8134,8135],{"class":2252}," reasons",[1179,8137,1255],{"class":1254},[1179,8139,7681],{"class":3412},[1179,8141,8142],{"class":1239},"[] ",[1179,8144,2258],{"class":1254},[1179,8146,8147],{"class":1239}," [];\n",[1179,8149,8150,8153,8156,8158,8161],{"class":1181,"line":2090},[1179,8151,8152],{"class":1957},"    let",[1179,8154,8155],{"class":1770}," score",[1179,8157,2483],{"class":1254},[1179,8159,8160],{"class":1330}," 0",[1179,8162,7570],{"class":1239},[1179,8164,8165],{"class":1181,"line":2096},[1179,8166,1935],{"emptyLinePlaceholder":8},[1179,8168,8169,8171,8174,8176,8179,8181,8184,8186,8188,8190,8192,8195,8197],{"class":1181,"line":2102},[1179,8170,8132],{"class":1957},[1179,8172,8173],{"class":2252}," isHosting",[1179,8175,2483],{"class":1254},[1179,8177,8178],{"class":1770}," ctx",[1179,8180,1053],{"class":1239},[1179,8182,8183],{"class":1770},"bgp",[1179,8185,1053],{"class":1239},[1179,8187,7814],{"class":1770},[1179,8189,2311],{"class":1254},[1179,8191,1819],{"class":1199},[1179,8193,8194],{"class":1203},"Content",[1179,8196,1780],{"class":1199},[1179,8198,7570],{"class":1239},[1179,8200,8201,8203,8206,8208,8211,8213,8215,8217,8220,8222,8225,8227,8229,8231,8233],{"class":1181,"line":2108},[1179,8202,8132],{"class":1957},[1179,8204,8205],{"class":2252}," hasLocale",[1179,8207,2483],{"class":1254},[1179,8209,8210],{"class":1185}," Boolean",[1179,8212,1968],{"class":1239},[1179,8214,7629],{"class":1770},[1179,8216,1053],{"class":1239},[1179,8218,8219],{"class":1770},"req",[1179,8221,1053],{"class":1239},[1179,8223,8224],{"class":1185},"get",[1179,8226,1968],{"class":1239},[1179,8228,1780],{"class":1199},[1179,8230,5683],{"class":1203},[1179,8232,1780],{"class":1199},[1179,8234,8235],{"class":1239},"));\n",[1179,8237,8238],{"class":1181,"line":2324},[1179,8239,1935],{"emptyLinePlaceholder":8},[1179,8241,8242,8244,8246,8249,8252,8255,8258],{"class":1181,"line":2347},[1179,8243,2398],{"class":1755},[1179,8245,2233],{"class":1239},[1179,8247,8248],{"class":1770},"isHosting",[1179,8250,8251],{"class":1254}," &&",[1179,8253,8254],{"class":1254}," !",[1179,8256,8257],{"class":1770},"hasLocale",[1179,8259,2434],{"class":1239},[1179,8261,8262,8265,8268,8271],{"class":1181,"line":2369},[1179,8263,8264],{"class":1770},"      score",[1179,8266,8267],{"class":1254}," +=",[1179,8269,8270],{"class":1330}," 25",[1179,8272,7570],{"class":1239},[1179,8274,8275,8278,8280,8283,8285,8287,8289,8291],{"class":1181,"line":2389},[1179,8276,8277],{"class":1770},"      reasons",[1179,8279,1053],{"class":1239},[1179,8281,8282],{"class":1185},"push",[1179,8284,1968],{"class":1239},[1179,8286,1780],{"class":1199},[1179,8288,7992],{"class":1203},[1179,8290,1780],{"class":1199},[1179,8292,8293],{"class":1239},");\n",[1179,8295,8296],{"class":1181,"line":2395},[1179,8297,2099],{"class":1239},[1179,8299,8300],{"class":1181,"line":2437},[1179,8301,1935],{"emptyLinePlaceholder":8},[1179,8303,8304,8306,8308,8310,8312,8314],{"class":1181,"line":2449},[1179,8305,3048],{"class":1755},[1179,8307,1854],{"class":1239},[1179,8309,7666],{"class":1770},[1179,8311,1032],{"class":1239},[1179,8313,7676],{"class":1770},[1179,8315,8316],{"class":1239}," };\n",[1179,8318,8319],{"class":1181,"line":2454},[1179,8320,1453],{"class":1239},[1179,8322,8323],{"class":1181,"line":2459},[1179,8324,1459],{"class":1239},[1179,8326,8327],{"class":1181,"line":2465},[1179,8328,1935],{"emptyLinePlaceholder":8},[1179,8330,8331,8333,8335,8338,8340,8343,8345],{"class":1181,"line":2470},[1179,8332,7929],{"class":1770},[1179,8334,1053],{"class":1239},[1179,8336,8337],{"class":1185},"register",[1179,8339,1968],{"class":1239},[1179,8341,8342],{"class":2810},"new",[1179,8344,8017],{"class":1185},[1179,8346,8347],{"class":1239},"());\n",[830,8349,8350],{},"Registration happens at module load time. A side-effect import in your server entry point is enough to activate the checker. Import order controls execution order within each phase.",[1089,8352,8355],{"className":1745,"code":8353,"filename":8354,"language":1748,"meta":811,"style":811},"import { defineConfiguration, detectBots } from '@riavzon\u002Fbot-detector';\nimport '.\u002Fdatacenter-locale-checker.js'; \u002F\u002F registers on import\n\nawait defineConfiguration({ \u002F* ... *\u002F });\napp.use(detectBots());\n","server.ts",[843,8356,8357,8383,8399,8403,8419],{"__ignoreMap":811},[1179,8358,8359,8361,8363,8366,8368,8371,8373,8375,8377,8379,8381],{"class":1181,"line":1182},[1179,8360,1851],{"class":1755},[1179,8362,1854],{"class":1239},[1179,8364,8365],{"class":1770},"defineConfiguration",[1179,8367,1032],{"class":1239},[1179,8369,8370],{"class":1770},"detectBots",[1179,8372,1860],{"class":1239},[1179,8374,1863],{"class":1755},[1179,8376,1819],{"class":1199},[1179,8378,7938],{"class":1203},[1179,8380,1780],{"class":1199},[1179,8382,7570],{"class":1239},[1179,8384,8385,8387,8389,8392,8394,8396],{"class":1181,"line":812},[1179,8386,1851],{"class":1755},[1179,8388,1819],{"class":1199},[1179,8390,8391],{"class":1203},".\u002Fdatacenter-locale-checker.js",[1179,8393,1780],{"class":1199},[1179,8395,7673],{"class":1239},[1179,8397,8398],{"class":4085},"\u002F\u002F registers on import\n",[1179,8400,8401],{"class":1181,"line":1212},[1179,8402,1935],{"emptyLinePlaceholder":8},[1179,8404,8405,8407,8410,8413,8416],{"class":1181,"line":1283},[1179,8406,5484],{"class":1755},[1179,8408,8409],{"class":1185}," defineConfiguration",[1179,8411,8412],{"class":1239},"({ ",[1179,8414,8415],{"class":4085},"\u002F* ... *\u002F",[1179,8417,8418],{"class":1239}," });\n",[1179,8420,8421,8424,8426,8429,8431,8433],{"class":1181,"line":1298},[1179,8422,8423],{"class":1770},"app",[1179,8425,1053],{"class":1239},[1179,8427,8428],{"class":1185},"use",[1179,8430,1968],{"class":1239},[1179,8432,8370],{"class":1185},[1179,8434,8347],{"class":1239},[4232,8436,8438],{"id":8437},"passing-application-context-into-checkers","Passing Application Context Into Checkers",[830,8440,3835,8441,8443,8444,8446,8447,1159,8449,8451,8452,8454,8455,1053],{},[843,8442,7890],{}," function runs once per request before any checker executes. It receives the raw Express request and returns the ",[843,8445,7884],{}," object. Passing the generic type through to ",[843,8448,7506],{},[843,8450,7500],{}," gives full IntelliSense on ",[843,8453,7884],{}," inside ",[843,8456,636],{},[1089,8458,8460],{"className":1745,"code":8459,"filename":8354,"language":1748,"meta":811,"style":811},"interface MyContext {\n  userId: string;\n  plan: 'free' | 'pro' | 'enterprise';\n  isInternal: boolean;\n}\n\napp.use(\n  detectBots\u003CMyContext>((req) => ({\n    userId:     req.user?.id   ?? 'anonymous',\n    plan:       req.user?.plan ?? 'free',\n    isInternal: req.ip === '127.0.0.1',\n  }))\n);\n",[843,8461,8462,8471,8482,8516,8527,8531,8535,8546,8568,8598,8627,8652,8657],{"__ignoreMap":811},[1179,8463,8464,8466,8469],{"class":1181,"line":1182},[1179,8465,7526],{"class":1957},[1179,8467,8468],{"class":3412}," MyContext",[1179,8470,1295],{"class":1239},[1179,8472,8473,8476,8478,8480],{"class":1181,"line":812},[1179,8474,8475],{"class":1770},"  userId",[1179,8477,1255],{"class":1254},[1179,8479,3423],{"class":3412},[1179,8481,7570],{"class":1239},[1179,8483,8484,8487,8489,8491,8494,8496,8498,8500,8503,8505,8507,8509,8512,8514],{"class":1181,"line":1212},[1179,8485,8486],{"class":1770},"  plan",[1179,8488,1255],{"class":1254},[1179,8490,1819],{"class":1199},[1179,8492,8493],{"class":1203},"free",[1179,8495,1780],{"class":1199},[1179,8497,3426],{"class":1254},[1179,8499,1819],{"class":1199},[1179,8501,8502],{"class":1203},"pro",[1179,8504,1780],{"class":1199},[1179,8506,3426],{"class":1254},[1179,8508,1819],{"class":1199},[1179,8510,8511],{"class":1203},"enterprise",[1179,8513,1780],{"class":1199},[1179,8515,7570],{"class":1239},[1179,8517,8518,8521,8523,8525],{"class":1181,"line":1283},[1179,8519,8520],{"class":1770},"  isInternal",[1179,8522,1255],{"class":1254},[1179,8524,7617],{"class":3412},[1179,8526,7570],{"class":1239},[1179,8528,8529],{"class":1181,"line":1298},[1179,8530,1459],{"class":1239},[1179,8532,8533],{"class":1181,"line":1319},[1179,8534,1935],{"emptyLinePlaceholder":8},[1179,8536,8537,8539,8541,8543],{"class":1181,"line":1336},[1179,8538,8423],{"class":1770},[1179,8540,1053],{"class":1239},[1179,8542,8428],{"class":1185},[1179,8544,8545],{"class":1239},"(\n",[1179,8547,8548,8551,8553,8556,8559,8561,8563,8565],{"class":1181,"line":1353},[1179,8549,8550],{"class":1185},"  detectBots",[1179,8552,7532],{"class":1239},[1179,8554,8555],{"class":7535},"MyContext",[1179,8557,8558],{"class":1239},">((",[1179,8560,8219],{"class":1950},[1179,8562,1954],{"class":1239},[1179,8564,1958],{"class":1957},[1179,8566,8567],{"class":1239}," ({\n",[1179,8569,8570,8572,8574,8577,8579,8581,8583,8586,8589,8591,8594,8596],{"class":1181,"line":1374},[1179,8571,2766],{"class":1770},[1179,8573,1255],{"class":1774},[1179,8575,8576],{"class":1770},"     req",[1179,8578,1053],{"class":1239},[1179,8580,3909],{"class":1770},[1179,8582,3017],{"class":1239},[1179,8584,8585],{"class":1770},"id",[1179,8587,8588],{"class":1254},"   ??",[1179,8590,1819],{"class":1199},[1179,8592,8593],{"class":1203},"anonymous",[1179,8595,1780],{"class":1199},[1179,8597,1036],{"class":1239},[1179,8599,8600,8603,8605,8608,8610,8612,8614,8617,8619,8621,8623,8625],{"class":1181,"line":1395},[1179,8601,8602],{"class":1770},"    plan",[1179,8604,1255],{"class":1774},[1179,8606,8607],{"class":1770},"       req",[1179,8609,1053],{"class":1239},[1179,8611,3909],{"class":1770},[1179,8613,3017],{"class":1239},[1179,8615,8616],{"class":1770},"plan",[1179,8618,3290],{"class":1254},[1179,8620,1819],{"class":1199},[1179,8622,8493],{"class":1203},[1179,8624,1780],{"class":1199},[1179,8626,1036],{"class":1239},[1179,8628,8629,8632,8634,8637,8639,8642,8644,8646,8648,8650],{"class":1181,"line":1415},[1179,8630,8631],{"class":1770},"    isInternal",[1179,8633,1255],{"class":1774},[1179,8635,8636],{"class":1770}," req",[1179,8638,1053],{"class":1239},[1179,8640,8641],{"class":1770},"ip",[1179,8643,2311],{"class":1254},[1179,8645,1819],{"class":1199},[1179,8647,2518],{"class":1203},[1179,8649,1780],{"class":1199},[1179,8651,1036],{"class":1239},[1179,8653,8654],{"class":1181,"line":1432},[1179,8655,8656],{"class":1239},"  }))\n",[1179,8658,8659],{"class":1181,"line":1450},[1179,8660,8293],{"class":1239},[1089,8662,8665],{"className":1745,"code":8663,"filename":8664,"language":1748,"meta":811,"style":811},"import type { IBotChecker, ValidationContext, BotDetectorConfig, BanReasonCode } from '@riavzon\u002Fbot-detector';\nimport type { MyContext } from '.\u002FmyContext.js';\n\nclass PlanAbuseChecker implements IBotChecker\u003CBanReasonCode, MyContext> {\n  name = 'PlanAbuseChecker';\n  phase = 'cheap' as const;\n\n  isEnabled(_config: BotDetectorConfig) { return true; }\n\n  run(ctx: ValidationContext\u003CMyContext>, _config: BotDetectorConfig) {\n    if (ctx.custom.isInternal) return { score: 0, reasons: [] };\n\n    if (ctx.custom.plan === 'free' && ctx.geoData.proxy) {\n      return { score: 20, reasons: ['PROXY_DETECTED'] };\n    }\n\n    return { score: 0, reasons: [] };\n  }\n}\n","plan-abuse-checker.ts",[843,8666,8667,8702,8725,8729,8750,8765,8783,8787,8810,8814,8840,8878,8882,8922,8953,8957,8961,8981,8985],{"__ignoreMap":811},[1179,8668,8669,8671,8673,8675,8677,8679,8681,8683,8685,8687,8690,8692,8694,8696,8698,8700],{"class":1181,"line":1182},[1179,8670,1851],{"class":1755},[1179,8672,7949],{"class":1755},[1179,8674,1854],{"class":1239},[1179,8676,7506],{"class":1770},[1179,8678,1032],{"class":1239},[1179,8680,7500],{"class":1770},[1179,8682,1032],{"class":1239},[1179,8684,7962],{"class":1770},[1179,8686,1032],{"class":1239},[1179,8688,8689],{"class":1770},"BanReasonCode",[1179,8691,1860],{"class":1239},[1179,8693,1863],{"class":1755},[1179,8695,1819],{"class":1199},[1179,8697,7938],{"class":1203},[1179,8699,1780],{"class":1199},[1179,8701,7570],{"class":1239},[1179,8703,8704,8706,8708,8710,8712,8714,8716,8718,8721,8723],{"class":1181,"line":812},[1179,8705,1851],{"class":1755},[1179,8707,7949],{"class":1755},[1179,8709,1854],{"class":1239},[1179,8711,8555],{"class":1770},[1179,8713,1860],{"class":1239},[1179,8715,1863],{"class":1755},[1179,8717,1819],{"class":1199},[1179,8719,8720],{"class":1203},".\u002FmyContext.js",[1179,8722,1780],{"class":1199},[1179,8724,7570],{"class":1239},[1179,8726,8727],{"class":1181,"line":1212},[1179,8728,1935],{"emptyLinePlaceholder":8},[1179,8730,8731,8733,8736,8738,8740,8742,8744,8746,8748],{"class":1181,"line":1283},[1179,8732,8013],{"class":1957},[1179,8734,8735],{"class":8016}," PlanAbuseChecker",[1179,8737,8020],{"class":1957},[1179,8739,7529],{"class":3412},[1179,8741,7532],{"class":1239},[1179,8743,8689],{"class":7535},[1179,8745,1032],{"class":1239},[1179,8747,8555],{"class":7535},[1179,8749,8029],{"class":1239},[1179,8751,8752,8754,8756,8758,8761,8763],{"class":1181,"line":1298},[1179,8753,7563],{"class":1770},[1179,8755,2483],{"class":1254},[1179,8757,1819],{"class":1199},[1179,8759,8760],{"class":1203},"PlanAbuseChecker",[1179,8762,1780],{"class":1199},[1179,8764,7570],{"class":1239},[1179,8766,8767,8769,8771,8773,8775,8777,8779,8781],{"class":1181,"line":1319},[1179,8768,7575],{"class":1770},[1179,8770,2483],{"class":1254},[1179,8772,1819],{"class":1199},[1179,8774,7582],{"class":1203},[1179,8776,1780],{"class":1199},[1179,8778,8059],{"class":1755},[1179,8780,8062],{"class":1957},[1179,8782,7570],{"class":1239},[1179,8784,8785],{"class":1181,"line":1336},[1179,8786,1935],{"emptyLinePlaceholder":8},[1179,8788,8789,8791,8793,8795,8797,8799,8802,8805,8807],{"class":1181,"line":1353},[1179,8790,7600],{"class":1185},[1179,8792,1968],{"class":1239},[1179,8794,8077],{"class":1950},[1179,8796,1255],{"class":1254},[1179,8798,7610],{"class":3412},[1179,8800,8801],{"class":1239},") { ",[1179,8803,8804],{"class":1755},"return",[1179,8806,1258],{"class":1195},[1179,8808,8809],{"class":1239},"; }\n",[1179,8811,8812],{"class":1181,"line":1374},[1179,8813,1935],{"emptyLinePlaceholder":8},[1179,8815,8816,8818,8820,8822,8824,8826,8828,8830,8832,8834,8836,8838],{"class":1181,"line":1395},[1179,8817,7624],{"class":1185},[1179,8819,1968],{"class":1239},[1179,8821,7629],{"class":1950},[1179,8823,1255],{"class":1254},[1179,8825,7634],{"class":3412},[1179,8827,7532],{"class":1239},[1179,8829,8555],{"class":7535},[1179,8831,7641],{"class":1239},[1179,8833,8077],{"class":1950},[1179,8835,1255],{"class":1254},[1179,8837,7610],{"class":3412},[1179,8839,2434],{"class":1239},[1179,8841,8842,8844,8846,8848,8850,8852,8854,8857,8859,8861,8863,8865,8867,8869,8871,8873,8875],{"class":1181,"line":1415},[1179,8843,2398],{"class":1755},[1179,8845,2233],{"class":1239},[1179,8847,7629],{"class":1770},[1179,8849,1053],{"class":1239},[1179,8851,1031],{"class":1770},[1179,8853,1053],{"class":1239},[1179,8855,8856],{"class":1770},"isInternal",[1179,8858,1954],{"class":1239},[1179,8860,8804],{"class":1755},[1179,8862,1854],{"class":1239},[1179,8864,7666],{"class":1770},[1179,8866,1255],{"class":1774},[1179,8868,8160],{"class":1330},[1179,8870,1032],{"class":1239},[1179,8872,7676],{"class":1770},[1179,8874,1255],{"class":1774},[1179,8876,8877],{"class":1239}," [] };\n",[1179,8879,8880],{"class":1181,"line":1432},[1179,8881,1935],{"emptyLinePlaceholder":8},[1179,8883,8884,8886,8888,8890,8892,8894,8896,8898,8900,8902,8904,8906,8908,8910,8912,8915,8917,8920],{"class":1181,"line":1450},[1179,8885,2398],{"class":1755},[1179,8887,2233],{"class":1239},[1179,8889,7629],{"class":1770},[1179,8891,1053],{"class":1239},[1179,8893,1031],{"class":1770},[1179,8895,1053],{"class":1239},[1179,8897,8616],{"class":1770},[1179,8899,2311],{"class":1254},[1179,8901,1819],{"class":1199},[1179,8903,8493],{"class":1203},[1179,8905,1780],{"class":1199},[1179,8907,8251],{"class":1254},[1179,8909,8178],{"class":1770},[1179,8911,1053],{"class":1239},[1179,8913,8914],{"class":1770},"geoData",[1179,8916,1053],{"class":1239},[1179,8918,8919],{"class":1770},"proxy",[1179,8921,2434],{"class":1239},[1179,8923,8924,8927,8929,8931,8933,8935,8937,8939,8941,8943,8945,8948,8950],{"class":1181,"line":1456},[1179,8925,8926],{"class":1755},"      return",[1179,8928,1854],{"class":1239},[1179,8930,7666],{"class":1770},[1179,8932,1255],{"class":1774},[1179,8934,7011],{"class":1330},[1179,8936,1032],{"class":1239},[1179,8938,7676],{"class":1770},[1179,8940,1255],{"class":1774},[1179,8942,1777],{"class":1239},[1179,8944,1780],{"class":1199},[1179,8946,8947],{"class":1203},"PROXY_DETECTED",[1179,8949,1780],{"class":1199},[1179,8951,8952],{"class":1239},"] };\n",[1179,8954,8955],{"class":1181,"line":2079},[1179,8956,2099],{"class":1239},[1179,8958,8959],{"class":1181,"line":2090},[1179,8960,1935],{"emptyLinePlaceholder":8},[1179,8962,8963,8965,8967,8969,8971,8973,8975,8977,8979],{"class":1181,"line":2096},[1179,8964,3048],{"class":1755},[1179,8966,1854],{"class":1239},[1179,8968,7666],{"class":1770},[1179,8970,1255],{"class":1774},[1179,8972,8160],{"class":1330},[1179,8974,1032],{"class":1239},[1179,8976,7676],{"class":1770},[1179,8978,1255],{"class":1774},[1179,8980,8877],{"class":1239},[1179,8982,8983],{"class":1181,"line":2102},[1179,8984,1453],{"class":1239},[1179,8986,8987],{"class":1181,"line":2108},[1179,8988,1459],{"class":1239},[830,8990,8991],{},"This pattern lets you apply business logic — plan tier, user role, internal traffic bypass — inside the same scoring pipeline that handles IP reputation and behavioral analysis, without any special wiring.",[4232,8993,8995],{"id":8994},"triggering-an-instant-ban","Triggering an Instant Ban",[830,8997,8998,8999,9002,9003,9006],{},"Returning ",[843,9000,9001],{},"'BAD_BOT_DETECTED'"," in the reasons array causes the pipeline to throw ",[843,9004,9005],{},"BadBotDetected"," immediately. No further checkers run, and the reputation healer does not execute. The visitor is banned without waiting for score accumulation.",[1089,9008,9010],{"className":1745,"code":9009,"language":1748,"meta":811,"style":811},"run(ctx: ValidationContext, _config: BotDetectorConfig) {\n  if (isDefinitelyABot(ctx)) {\n    return { score: 0, reasons: ['BAD_BOT_DETECTED'] };\n  }\n  return { score: 0, reasons: [] };\n}\n",[843,9011,9012,9035,9051,9079,9083,9103],{"__ignoreMap":811},[1179,9013,9014,9016,9018,9020,9023,9025,9027,9029,9031,9033],{"class":1181,"line":1182},[1179,9015,636],{"class":1185},[1179,9017,1968],{"class":1239},[1179,9019,7629],{"class":1770},[1179,9021,9022],{"class":1239},": ",[1179,9024,7500],{"class":1770},[1179,9026,1032],{"class":1239},[1179,9028,8077],{"class":1770},[1179,9030,9022],{"class":1239},[1179,9032,7962],{"class":1770},[1179,9034,2434],{"class":1239},[1179,9036,9037,9039,9041,9044,9046,9048],{"class":1181,"line":812},[1179,9038,2277],{"class":1755},[1179,9040,2233],{"class":1239},[1179,9042,9043],{"class":1185},"isDefinitelyABot",[1179,9045,1968],{"class":1239},[1179,9047,7629],{"class":1770},[1179,9049,9050],{"class":1239},")) {\n",[1179,9052,9053,9055,9057,9059,9061,9063,9065,9067,9069,9071,9073,9075,9077],{"class":1181,"line":1212},[1179,9054,3048],{"class":1755},[1179,9056,1854],{"class":1239},[1179,9058,7666],{"class":1770},[1179,9060,1255],{"class":1774},[1179,9062,8160],{"class":1330},[1179,9064,1032],{"class":1239},[1179,9066,7676],{"class":1770},[1179,9068,1255],{"class":1774},[1179,9070,1777],{"class":1239},[1179,9072,1780],{"class":1199},[1179,9074,5583],{"class":1203},[1179,9076,1780],{"class":1199},[1179,9078,8952],{"class":1239},[1179,9080,9081],{"class":1181,"line":1283},[1179,9082,1453],{"class":1239},[1179,9084,9085,9087,9089,9091,9093,9095,9097,9099,9101],{"class":1181,"line":1298},[1179,9086,2718],{"class":1755},[1179,9088,1854],{"class":1239},[1179,9090,7666],{"class":1770},[1179,9092,1255],{"class":1774},[1179,9094,8160],{"class":1330},[1179,9096,1032],{"class":1239},[1179,9098,7676],{"class":1770},[1179,9100,1255],{"class":1774},[1179,9102,8877],{"class":1239},[1179,9104,9105],{"class":1181,"line":1319},[1179,9106,1459],{"class":1239},[830,9108,9109,9110,9113],{},"The mirror is ",[843,9111,9112],{},"'GOOD_BOT_IDENTIFIED'",", which whitelists the request instantly. The built-in good-bot DNS verifier uses this same mechanism.",[4232,9115,9117],{"id":9116},"heavy-checkers-and-the-built-in-storage","Heavy Checkers and the Built-In Storage",[830,9119,9120,9121,9124,9125,9127,9128,9131],{},"Checkers that require I\u002FO — database queries, external API calls, cache reads — declare ",[843,9122,9123],{},"phase: 'heavy'",". The heavy phase only runs when the cheap phase score stays below ",[843,9126,5458],{},". Call ",[843,9129,9130],{},"getStorage()"," to access the same storage instance Bot Detector uses internally, keeping all cache I\u002FO in one place.",[1089,9133,9136],{"className":1745,"code":9134,"filename":9135,"language":1748,"meta":811,"style":811},"import { getStorage, CheckerRegistry } from '@riavzon\u002Fbot-detector';\nimport type { IBotChecker, ValidationContext, BotDetectorConfig } from '@riavzon\u002Fbot-detector';\n\nclass MyAsyncChecker implements IBotChecker\u003C'MY_REASON'> {\n  name = 'MyAsyncChecker';\n  phase = 'heavy' as const;\n\n  isEnabled(_config: BotDetectorConfig): boolean { return true; }\n\n  async run(ctx: ValidationContext, _config: BotDetectorConfig) {\n    if (!ctx.cookie) return { score: 0, reasons: [] };\n\n    const storage = getStorage();\n    const cacheKey = `custom:${ctx.cookie}`;\n\n    const cached = await storage.getItem\u003Cnumber>(cacheKey);\n    if (cached !== null) {\n      return { score: cached, reasons: cached > 0 ? ['MY_REASON' as const] : [] };\n    }\n\n    const result = await myDb.query('SELECT ...', [ctx.ipAddress]);\n    const score = result.isSuspicious ? 30 : 0;\n\n    await storage.setItem(cacheKey, score, { ttl: 300 });\n    return { score, reasons: score > 0 ? ['MY_REASON' as const] : [] };\n  }\n}\n\nCheckerRegistry.register(new MyAsyncChecker());\n","my-async-checker.ts",[843,9137,9138,9163,9193,9197,9219,9234,9252,9256,9282,9286,9311,9347,9351,9366,9396,9400,9430,9447,9494,9498,9502,9542,9568,9572,9605,9645,9649,9653,9657],{"__ignoreMap":811},[1179,9139,9140,9142,9144,9147,9149,9151,9153,9155,9157,9159,9161],{"class":1181,"line":1182},[1179,9141,1851],{"class":1755},[1179,9143,1854],{"class":1239},[1179,9145,9146],{"class":1770},"getStorage",[1179,9148,1032],{"class":1239},[1179,9150,7929],{"class":1770},[1179,9152,1860],{"class":1239},[1179,9154,1863],{"class":1755},[1179,9156,1819],{"class":1199},[1179,9158,7938],{"class":1203},[1179,9160,1780],{"class":1199},[1179,9162,7570],{"class":1239},[1179,9164,9165,9167,9169,9171,9173,9175,9177,9179,9181,9183,9185,9187,9189,9191],{"class":1181,"line":812},[1179,9166,1851],{"class":1755},[1179,9168,7949],{"class":1755},[1179,9170,1854],{"class":1239},[1179,9172,7506],{"class":1770},[1179,9174,1032],{"class":1239},[1179,9176,7500],{"class":1770},[1179,9178,1032],{"class":1239},[1179,9180,7962],{"class":1770},[1179,9182,1860],{"class":1239},[1179,9184,1863],{"class":1755},[1179,9186,1819],{"class":1199},[1179,9188,7938],{"class":1203},[1179,9190,1780],{"class":1199},[1179,9192,7570],{"class":1239},[1179,9194,9195],{"class":1181,"line":1212},[1179,9196,1935],{"emptyLinePlaceholder":8},[1179,9198,9199,9201,9204,9206,9208,9210,9212,9215,9217],{"class":1181,"line":1283},[1179,9200,8013],{"class":1957},[1179,9202,9203],{"class":8016}," MyAsyncChecker",[1179,9205,8020],{"class":1957},[1179,9207,7529],{"class":3412},[1179,9209,7532],{"class":1239},[1179,9211,1780],{"class":1199},[1179,9213,9214],{"class":1203},"MY_REASON",[1179,9216,1780],{"class":1199},[1179,9218,8029],{"class":1239},[1179,9220,9221,9223,9225,9227,9230,9232],{"class":1181,"line":1298},[1179,9222,7563],{"class":1770},[1179,9224,2483],{"class":1254},[1179,9226,1819],{"class":1199},[1179,9228,9229],{"class":1203},"MyAsyncChecker",[1179,9231,1780],{"class":1199},[1179,9233,7570],{"class":1239},[1179,9235,9236,9238,9240,9242,9244,9246,9248,9250],{"class":1181,"line":1319},[1179,9237,7575],{"class":1770},[1179,9239,2483],{"class":1254},[1179,9241,1819],{"class":1199},[1179,9243,7591],{"class":1203},[1179,9245,1780],{"class":1199},[1179,9247,8059],{"class":1755},[1179,9249,8062],{"class":1957},[1179,9251,7570],{"class":1239},[1179,9253,9254],{"class":1181,"line":1336},[1179,9255,1935],{"emptyLinePlaceholder":8},[1179,9257,9258,9260,9262,9264,9266,9268,9270,9272,9274,9276,9278,9280],{"class":1181,"line":1353},[1179,9259,7600],{"class":1185},[1179,9261,1968],{"class":1239},[1179,9263,8077],{"class":1950},[1179,9265,1255],{"class":1254},[1179,9267,7610],{"class":3412},[1179,9269,4623],{"class":1239},[1179,9271,1255],{"class":1254},[1179,9273,7617],{"class":3412},[1179,9275,1854],{"class":1239},[1179,9277,8804],{"class":1755},[1179,9279,1258],{"class":1195},[1179,9281,8809],{"class":1239},[1179,9283,9284],{"class":1181,"line":1374},[1179,9285,1935],{"emptyLinePlaceholder":8},[1179,9287,9288,9291,9293,9295,9297,9299,9301,9303,9305,9307,9309],{"class":1181,"line":1395},[1179,9289,9290],{"class":1957},"  async",[1179,9292,5357],{"class":1185},[1179,9294,1968],{"class":1239},[1179,9296,7629],{"class":1950},[1179,9298,1255],{"class":1254},[1179,9300,7634],{"class":3412},[1179,9302,1032],{"class":1239},[1179,9304,8077],{"class":1950},[1179,9306,1255],{"class":1254},[1179,9308,7610],{"class":3412},[1179,9310,2434],{"class":1239},[1179,9312,9313,9315,9317,9320,9322,9324,9327,9329,9331,9333,9335,9337,9339,9341,9343,9345],{"class":1181,"line":1415},[1179,9314,2398],{"class":1755},[1179,9316,2233],{"class":1239},[1179,9318,9319],{"class":1254},"!",[1179,9321,7629],{"class":1770},[1179,9323,1053],{"class":1239},[1179,9325,9326],{"class":1770},"cookie",[1179,9328,1954],{"class":1239},[1179,9330,8804],{"class":1755},[1179,9332,1854],{"class":1239},[1179,9334,7666],{"class":1770},[1179,9336,1255],{"class":1774},[1179,9338,8160],{"class":1330},[1179,9340,1032],{"class":1239},[1179,9342,7676],{"class":1770},[1179,9344,1255],{"class":1774},[1179,9346,8877],{"class":1239},[1179,9348,9349],{"class":1181,"line":1432},[1179,9350,1935],{"emptyLinePlaceholder":8},[1179,9352,9353,9355,9358,9360,9363],{"class":1181,"line":1450},[1179,9354,8132],{"class":1957},[1179,9356,9357],{"class":2252}," storage",[1179,9359,2483],{"class":1254},[1179,9361,9362],{"class":1185}," getStorage",[1179,9364,9365],{"class":1239},"();\n",[1179,9367,9368,9370,9373,9375,9378,9381,9383,9386,9388,9391,9394],{"class":1181,"line":1456},[1179,9369,8132],{"class":1957},[1179,9371,9372],{"class":2252}," cacheKey",[1179,9374,2483],{"class":1254},[1179,9376,9377],{"class":1203}," `custom:",[1179,9379,9380],{"class":1957},"${",[1179,9382,7629],{"class":1770},[1179,9384,1053],{"class":9385},"s1lnM",[1179,9387,9326],{"class":1770},[1179,9389,9390],{"class":1957},"}",[1179,9392,9393],{"class":1203},"`",[1179,9395,7570],{"class":1239},[1179,9397,9398],{"class":1181,"line":2079},[1179,9399,1935],{"emptyLinePlaceholder":8},[1179,9401,9402,9404,9407,9409,9411,9413,9415,9418,9420,9422,9425,9428],{"class":1181,"line":2090},[1179,9403,8132],{"class":1957},[1179,9405,9406],{"class":2252}," cached",[1179,9408,2483],{"class":1254},[1179,9410,3395],{"class":1755},[1179,9412,9357],{"class":1770},[1179,9414,1053],{"class":1239},[1179,9416,9417],{"class":1185},"getItem",[1179,9419,7532],{"class":1239},[1179,9421,6329],{"class":3412},[1179,9423,9424],{"class":1239},">(",[1179,9426,9427],{"class":1770},"cacheKey",[1179,9429,8293],{"class":1239},[1179,9431,9432,9434,9436,9439,9442,9445],{"class":1181,"line":2096},[1179,9433,2398],{"class":1755},[1179,9435,2233],{"class":1239},[1179,9437,9438],{"class":1770},"cached",[1179,9440,9441],{"class":1254}," !==",[1179,9443,9444],{"class":1195}," null",[1179,9446,2434],{"class":1239},[1179,9448,9449,9451,9453,9455,9457,9459,9461,9463,9465,9467,9470,9472,9475,9477,9479,9481,9483,9485,9487,9490,9492],{"class":1181,"line":2102},[1179,9450,8926],{"class":1755},[1179,9452,1854],{"class":1239},[1179,9454,7666],{"class":1770},[1179,9456,1255],{"class":1774},[1179,9458,9406],{"class":1770},[1179,9460,1032],{"class":1239},[1179,9462,7676],{"class":1770},[1179,9464,1255],{"class":1774},[1179,9466,9406],{"class":1770},[1179,9468,9469],{"class":1254}," >",[1179,9471,8160],{"class":1330},[1179,9473,9474],{"class":1254}," ?",[1179,9476,1777],{"class":1239},[1179,9478,1780],{"class":1199},[1179,9480,9214],{"class":1203},[1179,9482,1780],{"class":1199},[1179,9484,8059],{"class":1755},[1179,9486,8062],{"class":1957},[1179,9488,9489],{"class":1239},"] ",[1179,9491,1255],{"class":1254},[1179,9493,8877],{"class":1239},[1179,9495,9496],{"class":1181,"line":2108},[1179,9497,2099],{"class":1239},[1179,9499,9500],{"class":1181,"line":2324},[1179,9501,1935],{"emptyLinePlaceholder":8},[1179,9503,9504,9506,9508,9510,9512,9515,9517,9520,9522,9524,9527,9529,9532,9534,9536,9539],{"class":1181,"line":2347},[1179,9505,8132],{"class":1957},[1179,9507,4479],{"class":2252},[1179,9509,2483],{"class":1254},[1179,9511,3395],{"class":1755},[1179,9513,9514],{"class":1770}," myDb",[1179,9516,1053],{"class":1239},[1179,9518,9519],{"class":1185},"query",[1179,9521,1968],{"class":1239},[1179,9523,1780],{"class":1199},[1179,9525,9526],{"class":1203},"SELECT ...",[1179,9528,1780],{"class":1199},[1179,9530,9531],{"class":1239},", [",[1179,9533,7629],{"class":1770},[1179,9535,1053],{"class":1239},[1179,9537,9538],{"class":1770},"ipAddress",[1179,9540,9541],{"class":1239},"]);\n",[1179,9543,9544,9546,9548,9550,9552,9554,9557,9559,9561,9564,9566],{"class":1181,"line":2369},[1179,9545,8132],{"class":1957},[1179,9547,8155],{"class":2252},[1179,9549,2483],{"class":1254},[1179,9551,4479],{"class":1770},[1179,9553,1053],{"class":1239},[1179,9555,9556],{"class":1770},"isSuspicious",[1179,9558,9474],{"class":1254},[1179,9560,2074],{"class":1330},[1179,9562,9563],{"class":1254}," :",[1179,9565,8160],{"class":1330},[1179,9567,7570],{"class":1239},[1179,9569,9570],{"class":1181,"line":2389},[1179,9571,1935],{"emptyLinePlaceholder":8},[1179,9573,9574,9577,9579,9581,9584,9586,9588,9590,9592,9595,9598,9600,9603],{"class":1181,"line":2395},[1179,9575,9576],{"class":1755},"    await",[1179,9578,9357],{"class":1770},[1179,9580,1053],{"class":1239},[1179,9582,9583],{"class":1185},"setItem",[1179,9585,1968],{"class":1239},[1179,9587,9427],{"class":1770},[1179,9589,1032],{"class":1239},[1179,9591,7666],{"class":1770},[1179,9593,9594],{"class":1239},", { ",[1179,9596,9597],{"class":1770},"ttl",[1179,9599,1255],{"class":1774},[1179,9601,9602],{"class":1330}," 300",[1179,9604,8418],{"class":1239},[1179,9606,9607,9609,9611,9613,9615,9617,9619,9621,9623,9625,9627,9629,9631,9633,9635,9637,9639,9641,9643],{"class":1181,"line":2437},[1179,9608,3048],{"class":1755},[1179,9610,1854],{"class":1239},[1179,9612,7666],{"class":1770},[1179,9614,1032],{"class":1239},[1179,9616,7676],{"class":1770},[1179,9618,1255],{"class":1774},[1179,9620,8155],{"class":1770},[1179,9622,9469],{"class":1254},[1179,9624,8160],{"class":1330},[1179,9626,9474],{"class":1254},[1179,9628,1777],{"class":1239},[1179,9630,1780],{"class":1199},[1179,9632,9214],{"class":1203},[1179,9634,1780],{"class":1199},[1179,9636,8059],{"class":1755},[1179,9638,8062],{"class":1957},[1179,9640,9489],{"class":1239},[1179,9642,1255],{"class":1254},[1179,9644,8877],{"class":1239},[1179,9646,9647],{"class":1181,"line":2449},[1179,9648,1453],{"class":1239},[1179,9650,9651],{"class":1181,"line":2454},[1179,9652,1459],{"class":1239},[1179,9654,9655],{"class":1181,"line":2459},[1179,9656,1935],{"emptyLinePlaceholder":8},[1179,9658,9659,9661,9663,9665,9667,9669,9671],{"class":1181,"line":2465},[1179,9660,7929],{"class":1770},[1179,9662,1053],{"class":1239},[1179,9664,8337],{"class":1185},[1179,9666,1968],{"class":1239},[1179,9668,8342],{"class":2810},[1179,9670,9203],{"class":1185},[1179,9672,8347],{"class":1239},[2595,9674,9675],{},[830,9676,9677,9678,9681],{},"Use a namespaced key prefix for your cache entries (for example ",[843,9679,9680],{},"custom:",") to avoid collisions with the built-in cache keys that share the same storage instance.",[852,9683],{},[855,9685,9687],{"id":9686},"automatic-threat-compilation-the-generator","Automatic Threat Compilation: The Generator",[830,9689,3835,9690,9692,9693,1159,9695,9697],{},[843,9691,307],{}," checker — checker 10 in the cheap phase — queries two optional MMDB files: ",[843,9694,5855],{},[843,9696,5858],{},". These files do not come from Shield Base. Bot Detector generates them itself from its own accumulated traffic history.",[4232,9699,9701],{"id":9700},"what-gets-compiled","What Gets Compiled",[830,9703,9704,9705,9708],{},"Running ",[843,9706,9707],{},"bot-detector generate"," reads two tables from Bot Detector's database and compiles each into an MMDB file. Both compilations run in parallel.",[830,9710,9711,9715,9716,9719,9720,9723,9724,9726],{},[3837,9712,9713],{},[843,9714,5855],{}," — every row in the ",[843,9717,9718],{},"banned"," table with a non-null ",[843,9721,9722],{},"ip_address"," gets compiled into this file. Each entry stores the IP, score, country, user-agent, and reason codes from the original ban event. On subsequent visits, the Known Bad IPs checker matches the IP in microseconds in the cheap phase and issues ",[843,9725,5583],{}," immediately — the full 17-checker pipeline never runs for a confirmed repeat offender.",[830,9728,9729,9715,9733,9736,9737,9740,9741,9744,9745,9748,9749,9751,9752,9744,9755,9757],{},[3837,9730,9731],{},[843,9732,5858],{},[843,9734,9735],{},"visitors"," table where ",[843,9738,9739],{},"suspicious_activity_score"," is at or above ",[843,9742,9743],{},"generator.scoreThreshold"," (default ",[843,9746,9747],{},"70",") is compiled into this file. These are visitors who accumulated significant suspicion scores but were never pushed over ",[843,9750,5458],{},". On their next visit, they receive the ",[843,9753,9754],{},"highRiskPenalty",[843,9756,5638],{}," points) in the cheap phase, meaning far less effort from other checkers is needed to reach a ban.",[1089,9759,9761],{"className":1745,"code":9760,"language":1748,"meta":811,"style":811},"generator: {\n  scoreThreshold: 70,   \u002F\u002F minimum score to include in highRisk.mmdb\n  deleteAfterBuild: false,  \u002F\u002F if true, removes compiled rows from DB after build\n  mmdbctlPath: 'mmdbctl',   \u002F\u002F path to mmdbctl binary\n  generateTypes: false,     \u002F\u002F emit TypeScript type definitions alongside MMDB files\n}\n",[843,9762,9763,9768,9781,9795,9811,9824],{"__ignoreMap":811},[1179,9764,9765],{"class":1181,"line":1182},[1179,9766,9767],{"class":1239},"generator: {\n",[1179,9769,9770,9773,9775,9778],{"class":1181,"line":812},[1179,9771,9772],{"class":1239},"  scoreThreshold: ",[1179,9774,9747],{"class":1330},[1179,9776,9777],{"class":1239},",   ",[1179,9779,9780],{"class":4085},"\u002F\u002F minimum score to include in highRisk.mmdb\n",[1179,9782,9783,9786,9789,9792],{"class":1181,"line":1212},[1179,9784,9785],{"class":1239},"  deleteAfterBuild: ",[1179,9787,9788],{"class":1195},"false",[1179,9790,9791],{"class":1239},",  ",[1179,9793,9794],{"class":4085},"\u002F\u002F if true, removes compiled rows from DB after build\n",[1179,9796,9797,9800,9802,9804,9806,9808],{"class":1181,"line":1283},[1179,9798,9799],{"class":1239},"  mmdbctlPath: ",[1179,9801,1780],{"class":1199},[1179,9803,5419],{"class":1203},[1179,9805,1780],{"class":1199},[1179,9807,9777],{"class":1239},[1179,9809,9810],{"class":4085},"\u002F\u002F path to mmdbctl binary\n",[1179,9812,9813,9816,9818,9821],{"class":1181,"line":1298},[1179,9814,9815],{"class":1239},"  generateTypes: ",[1179,9817,9788],{"class":1195},[1179,9819,9820],{"class":1239},",     ",[1179,9822,9823],{"class":4085},"\u002F\u002F emit TypeScript type definitions alongside MMDB files\n",[1179,9825,9826],{"class":1181,"line":1319},[1179,9827,1459],{"class":1239},[830,9829,3835,9830,9833,9834,9836,9837,9839],{},[843,9831,9832],{},"scoreThreshold"," tradeoff is worth understanding. Lowering it to ",[843,9835,5726],{}," catches visitors with moderate suspicious history but risks false positives. Keeping it at ",[843,9838,9747],{}," or higher limits the file to visitors with strong behavioral evidence.",[863,9841,9842,9852],{},[866,9843,9844],{},[869,9845,9846,9849],{},[872,9847,9848],{},"Threshold",[872,9850,9851],{},"Effect",[885,9853,9854,9863,9873],{},[869,9855,9856,9860],{},[890,9857,9858],{},[843,9859,5726],{},[890,9861,9862],{},"Broader net — includes visitors with moderate accumulated scores",[869,9864,9865,9870],{},[890,9866,9867,9869],{},[843,9868,9747],{}," (default)",[890,9871,9872],{},"Balanced — strong suspicious history required",[869,9874,9875,9880],{},[890,9876,9877],{},[843,9878,9879],{},"90",[890,9881,9882],{},"Conservative — only the most suspicious non-banned visitors included",[4232,9884,9886],{"id":9885},"hot-reload","Hot Reload",[830,9888,9889,9890,9893],{},"Both MMDB files are opened with ",[843,9891,9892],{},"watchForUpdates: true",". When a new file is written to disk after a generation run, the MMDB reader reloads it automatically within seconds — no application restart, no traffic interruption. You can run generation against a live service and the updated databases take effect immediately.",[4232,9895,9897],{"id":9896},"running-generation","Running Generation",[5264,9899,9900,9918,9934,9948],{},[1089,9901,9903],{"className":1172,"code":9902,"filename":5269,"language":1175,"meta":811,"style":811},"pnpm dlx @riavzon\u002Fbot-detector generate\n",[843,9904,9905],{"__ignoreMap":811},[1179,9906,9907,9909,9912,9915],{"class":1181,"line":1182},[1179,9908,5269],{"class":1185},[1179,9910,9911],{"class":1203}," dlx",[1179,9913,9914],{"class":1203}," @riavzon\u002Fbot-detector",[1179,9916,9917],{"class":1203}," generate\n",[1089,9919,9922],{"className":1172,"code":9920,"filename":9921,"language":1175,"meta":811,"style":811},"yarn dlx @riavzon\u002Fbot-detector generate\n","yarn",[843,9923,9924],{"__ignoreMap":811},[1179,9925,9926,9928,9930,9932],{"class":1181,"line":1182},[1179,9927,9921],{"class":1185},[1179,9929,9911],{"class":1203},[1179,9931,9914],{"class":1203},[1179,9933,9917],{"class":1203},[1089,9935,9937],{"className":1172,"code":9936,"filename":5344,"language":1175,"meta":811,"style":811},"npx @riavzon\u002Fbot-detector generate\n",[843,9938,9939],{"__ignoreMap":811},[1179,9940,9941,9944,9946],{"class":1181,"line":1182},[1179,9942,9943],{"class":1185},"npx",[1179,9945,9914],{"class":1203},[1179,9947,9917],{"class":1203},[1089,9949,9952],{"className":1172,"code":9950,"filename":9951,"language":1175,"meta":811,"style":811},"bunx @riavzon\u002Fbot-detector generate\n","bun",[843,9953,9954],{"__ignoreMap":811},[1179,9955,9956,9959,9961],{"class":1181,"line":1182},[1179,9957,9958],{"class":1185},"bunx",[1179,9960,9914],{"class":1203},[1179,9962,9917],{"class":1203},[830,9964,9965,9966,9969],{},"For programmatic use — for example, triggering generation immediately after a bulk ban operation — call ",[843,9967,9968],{},"runGeneration()"," directly:",[1089,9971,9974],{"className":1745,"code":9972,"filename":9973,"language":1748,"meta":811,"style":811},"import { updateBannedIP, runGeneration } from '@riavzon\u002Fbot-detector';\nimport type { BannedInfo } from '@riavzon\u002Fbot-detector';\n\nfor (const ip of badIps) {\n  const info: BannedInfo = { score: 100, reasons: ['PREVIOUSLY_BANNED_IP'] };\n  await updateBannedIP('', ip, 'us', '', info);\n}\n\n\u002F\u002F Compile updated MMDB files immediately so the next request from these IPs\n\u002F\u002F hits the cheap-phase known-bad-IPs check rather than the full pipeline.\nawait runGeneration();\n","admin-script.ts",[843,9975,9976,10002,10025,10029,10049,10088,10124,10128,10132,10137,10142],{"__ignoreMap":811},[1179,9977,9978,9980,9982,9985,9987,9990,9992,9994,9996,9998,10000],{"class":1181,"line":1182},[1179,9979,1851],{"class":1755},[1179,9981,1854],{"class":1239},[1179,9983,9984],{"class":1770},"updateBannedIP",[1179,9986,1032],{"class":1239},[1179,9988,9989],{"class":1770},"runGeneration",[1179,9991,1860],{"class":1239},[1179,9993,1863],{"class":1755},[1179,9995,1819],{"class":1199},[1179,9997,7938],{"class":1203},[1179,9999,1780],{"class":1199},[1179,10001,7570],{"class":1239},[1179,10003,10004,10006,10008,10010,10013,10015,10017,10019,10021,10023],{"class":1181,"line":812},[1179,10005,1851],{"class":1755},[1179,10007,7949],{"class":1755},[1179,10009,1854],{"class":1239},[1179,10011,10012],{"class":1770},"BannedInfo",[1179,10014,1860],{"class":1239},[1179,10016,1863],{"class":1755},[1179,10018,1819],{"class":1199},[1179,10020,7938],{"class":1203},[1179,10022,1780],{"class":1199},[1179,10024,7570],{"class":1239},[1179,10026,10027],{"class":1181,"line":1212},[1179,10028,1935],{"emptyLinePlaceholder":8},[1179,10030,10031,10034,10036,10038,10041,10044,10047],{"class":1181,"line":1283},[1179,10032,10033],{"class":1755},"for",[1179,10035,2233],{"class":1239},[1179,10037,4055],{"class":1957},[1179,10039,10040],{"class":2252}," ip",[1179,10042,10043],{"class":1957}," of",[1179,10045,10046],{"class":1770}," badIps",[1179,10048,2434],{"class":1239},[1179,10050,10051,10053,10056,10058,10061,10063,10065,10067,10069,10071,10073,10075,10077,10079,10081,10084,10086],{"class":1181,"line":1298},[1179,10052,2247],{"class":1957},[1179,10054,10055],{"class":2252}," info",[1179,10057,1255],{"class":1254},[1179,10059,10060],{"class":3412}," BannedInfo",[1179,10062,2483],{"class":1254},[1179,10064,1854],{"class":1239},[1179,10066,7666],{"class":1770},[1179,10068,1255],{"class":1774},[1179,10070,5499],{"class":1330},[1179,10072,1032],{"class":1239},[1179,10074,7676],{"class":1770},[1179,10076,1255],{"class":1774},[1179,10078,1777],{"class":1239},[1179,10080,1780],{"class":1199},[1179,10082,10083],{"class":1203},"PREVIOUSLY_BANNED_IP",[1179,10085,1780],{"class":1199},[1179,10087,8952],{"class":1239},[1179,10089,10090,10092,10095,10097,10100,10102,10104,10106,10108,10111,10113,10115,10117,10119,10122],{"class":1181,"line":1319},[1179,10091,2568],{"class":1755},[1179,10093,10094],{"class":1185}," updateBannedIP",[1179,10096,1968],{"class":1239},[1179,10098,10099],{"class":1199},"''",[1179,10101,1032],{"class":1239},[1179,10103,8641],{"class":1770},[1179,10105,1032],{"class":1239},[1179,10107,1780],{"class":1199},[1179,10109,10110],{"class":1203},"us",[1179,10112,1780],{"class":1199},[1179,10114,1032],{"class":1239},[1179,10116,10099],{"class":1199},[1179,10118,1032],{"class":1239},[1179,10120,10121],{"class":1770},"info",[1179,10123,8293],{"class":1239},[1179,10125,10126],{"class":1181,"line":1336},[1179,10127,1459],{"class":1239},[1179,10129,10130],{"class":1181,"line":1353},[1179,10131,1935],{"emptyLinePlaceholder":8},[1179,10133,10134],{"class":1181,"line":1374},[1179,10135,10136],{"class":4085},"\u002F\u002F Compile updated MMDB files immediately so the next request from these IPs\n",[1179,10138,10139],{"class":1181,"line":1395},[1179,10140,10141],{"class":4085},"\u002F\u002F hits the cheap-phase known-bad-IPs check rather than the full pipeline.\n",[1179,10143,10144,10146,10149],{"class":1181,"line":1415},[1179,10145,5484],{"class":1755},[1179,10147,10148],{"class":1185}," runGeneration",[1179,10150,9365],{"class":1239},[4232,10152,10154],{"id":10153},"scheduling-generation","Scheduling Generation",[830,10156,10157],{},"The right generation frequency depends on traffic volume. A nightly run is a reasonable default. For higher-traffic applications where bans accumulate quickly, hourly generation keeps the banned MMDB current and prevents repeat offenders from absorbing pipeline capacity.",[1089,10159,10164],{"className":10160,"code":10161,"filename":10162,"language":10163,"meta":811,"style":811},"language-cron shiki shiki-themes light-plus light-plus dracula","# Nightly at 2:00 AM\n0 2 * * * cd \u002Fapp && npx bot-detector generate >> \u002Fvar\u002Flog\u002Fbot-detector-generate.log 2>&1\n\n# Hourly for high-traffic deployments\n0 * * * * cd \u002Fapp && npx bot-detector generate >> \u002Fvar\u002Flog\u002Fbot-detector-generate.log 2>&1\n","crontab","cron",[843,10165,10166,10171,10176,10180,10185],{"__ignoreMap":811},[1179,10167,10168],{"class":1181,"line":1182},[1179,10169,10170],{},"# Nightly at 2:00 AM\n",[1179,10172,10173],{"class":1181,"line":812},[1179,10174,10175],{},"0 2 * * * cd \u002Fapp && npx bot-detector generate >> \u002Fvar\u002Flog\u002Fbot-detector-generate.log 2>&1\n",[1179,10177,10178],{"class":1181,"line":1212},[1179,10179,1935],{"emptyLinePlaceholder":8},[1179,10181,10182],{"class":1181,"line":1283},[1179,10183,10184],{},"# Hourly for high-traffic deployments\n",[1179,10186,10187],{"class":1181,"line":1298},[1179,10188,10189],{},"0 * * * * cd \u002Fapp && npx bot-detector generate >> \u002Fvar\u002Flog\u002Fbot-detector-generate.log 2>&1\n",[830,10191,10192,10193,10195],{},"The generate command emits structured log lines including the entry count for each compiled database. Monitoring this output over time makes it easy to detect when ban volume spikes — a sudden increase in ",[843,10194,5855],{}," entries typically indicates a coordinated attack campaign starting.",[852,10197],{},[855,10199,3762],{"id":3761},[830,10201,10202],{},"Each of the three layers closes a gap that the others cannot. Shield Base provides static intelligence — historical threat reputation, network classification, and behavioral pattern databases — that no runtime analysis can replicate. Bot Detector performs dynamic behavioral analysis — velocity, session coherence, timing regularity — that static blocklists cannot catch. The canary cookie ties both together across sessions, making it impossible to reset accumulated behavioral signals simply by rotating IPs or changing headers.",[830,10204,10205],{},"A bot that evades Shield Base's IP reputation checks still faces 17 behavioral checkers. A bot that passes all 17 checkers on a single request still accumulates a session history that degrades its score over time. A bot that steals an authenticated session still cannot complete token rotation without matching the canary cookie fingerprint that was established on the original device.",[830,10207,10208],{},"The layered approach trades complexity for resilience. Each layer is effective in isolation. Together, they make the cost of a successful bot attack high enough that most attackers move on to easier targets.",[830,10210,4896,10211,10213],{},[4898,10212,230],{"href":35}," reference",[830,10215,4896,10216,10218],{},[4898,10217,40],{"href":42}," reference for database compilation options",[3786,10220,10221],{},"html pre.shiki code .sghk6, html code.shiki .sghk6{--shiki-light:#008000;--shiki-default:#008000;--shiki-dark:#6272A4}html pre.shiki code .sHOzp, html code.shiki .sHOzp{--shiki-light:#795E26;--shiki-default:#795E26;--shiki-dark:#50FA7B}html pre.shiki code .sFB1V, html code.shiki .sFB1V{--shiki-light:#A31515;--shiki-default:#A31515;--shiki-dark:#F1FA8C}html pre.shiki code .sjR7W, html code.shiki .sjR7W{--shiki-light:#0000FF;--shiki-default:#0000FF;--shiki-dark:#BD93F9}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZ328, html code.shiki .sZ328{--shiki-light:#AF00DB;--shiki-default:#AF00DB;--shiki-dark:#FF79C6}html pre.shiki code .sDd4n, html code.shiki .sDd4n{--shiki-light:#000000;--shiki-default:#000000;--shiki-dark:#F8F8F2}html pre.shiki code .sjsA6, html code.shiki .sjsA6{--shiki-light:#001080;--shiki-default:#001080;--shiki-dark:#F8F8F2}html pre.shiki code .s34zl, html code.shiki .s34zl{--shiki-light:#001080;--shiki-default:#001080;--shiki-dark:#FF79C6}html pre.shiki code .spgvN, html code.shiki .spgvN{--shiki-light:#098658;--shiki-default:#098658;--shiki-dark:#BD93F9}html pre.shiki code .sFkSl, html code.shiki .sFkSl{--shiki-light:#A31515;--shiki-default:#A31515;--shiki-dark:#E9F284}html pre.shiki code .s3JHE, html code.shiki .s3JHE{--shiki-light:#0070C1;--shiki-default:#0070C1;--shiki-dark:#F8F8F2}html pre.shiki code .sl46w, html code.shiki .sl46w{--shiki-light:#0000FF;--shiki-default:#0000FF;--shiki-dark:#FF79C6}html pre.shiki code .sFs1U, html code.shiki .sFs1U{--shiki-light:#267F99;--shiki-light-font-style:inherit;--shiki-default:#267F99;--shiki-default-font-style:inherit;--shiki-dark:#8BE9FD;--shiki-dark-font-style:italic}html pre.shiki code .sW-rI, html code.shiki .sW-rI{--shiki-light:#267F99;--shiki-light-font-style:inherit;--shiki-default:#267F99;--shiki-default-font-style:inherit;--shiki-dark:#FFB86C;--shiki-dark-font-style:italic}html pre.shiki code .saOXh, html code.shiki .saOXh{--shiki-light:#000000;--shiki-default:#000000;--shiki-dark:#FF79C6}html pre.shiki code .sygFZ, html code.shiki .sygFZ{--shiki-light:#001080;--shiki-light-font-style:inherit;--shiki-default:#001080;--shiki-default-font-style:inherit;--shiki-dark:#FFB86C;--shiki-dark-font-style:italic}html pre.shiki code .s5jk-, html code.shiki .s5jk-{--shiki-light:#267F99;--shiki-default:#267F99;--shiki-dark:#8BE9FD}html pre.shiki code .sakC6, html code.shiki .sakC6{--shiki-light:#0000FF;--shiki-light-font-weight:inherit;--shiki-default:#0000FF;--shiki-default-font-weight:inherit;--shiki-dark:#FF79C6;--shiki-dark-font-weight:bold}html pre.shiki code .s1lnM, html code.shiki .s1lnM{--shiki-light:#000000FF;--shiki-default:#000000FF;--shiki-dark:#F8F8F2}",{"title":811,"searchDepth":812,"depth":812,"links":10223},[10224,10225,10230,10236,10241,10242,10243,10252,10258],{"id":4953,"depth":812,"text":4954},{"id":4977,"depth":812,"text":4978,"children":10226},[10227,10228,10229],{"id":4984,"depth":1212,"text":4985},{"id":4991,"depth":1212,"text":4992},{"id":5254,"depth":1212,"text":5255},{"id":5430,"depth":812,"text":5431,"children":10231},[10232,10233,10234,10235],{"id":5441,"depth":1212,"text":5442},{"id":5451,"depth":1212,"text":5452},{"id":5554,"depth":1212,"text":5555},{"id":5862,"depth":1212,"text":5863},{"id":6147,"depth":812,"text":6148,"children":10237},[10238,10239,10240],{"id":6200,"depth":1212,"text":6201},{"id":6215,"depth":1212,"text":6216},{"id":6556,"depth":1212,"text":6557},{"id":6586,"depth":812,"text":6587},{"id":6682,"depth":812,"text":204},{"id":7493,"depth":812,"text":7494,"children":10244},[10245,10247,10248,10249,10250,10251],{"id":7503,"depth":1212,"text":10246},"The IBotChecker Interface",{"id":7713,"depth":1212,"text":7714},{"id":7907,"depth":1212,"text":7908},{"id":8437,"depth":1212,"text":8438},{"id":8994,"depth":1212,"text":8995},{"id":9116,"depth":1212,"text":9117},{"id":9686,"depth":812,"text":9687,"children":10253},[10254,10255,10256,10257],{"id":9700,"depth":1212,"text":9701},{"id":9885,"depth":1212,"text":9886},{"id":9896,"depth":1212,"text":9897},{"id":10153,"depth":1212,"text":10154},{"id":3761,"depth":812,"text":3762},"A complete walkthrough of the three-layer bot defense pipeline: from compiling IP intelligence databases with Shield Base, to running 17 checkers in two phases with Bot Detector, to fingerprinting sessions with the IAM canary cookie.","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555949963-aa79dcee981c?w=1200&q=80",{},"---\ntitle: \"Layered Bot Defense: How Shield Base, Bot Detector, and the IAM Canary Cookie Work Together\"\ndescription: \"A complete walkthrough of the three-layer bot defense pipeline: from compiling IP intelligence databases with Shield Base, to running 17 checkers in two phases with Bot Detector, to fingerprinting sessions with the IAM canary cookie.\"\ntags:\n  - Security\n  - Bot Detection\n  - Infrastructure\nimage: \"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555949963-aa79dcee981c?w=1200&q=80\"\nauthor: \"Sergio\"\nauthorImg: \"https:\u002F\u002Fgithub.com\u002FSergo706.png\"\nauthorGithub: \"https:\u002F\u002Fgithub.com\u002FSergo706\"\nauthorGithubUserName: \"Sergo706\"\nfeatured: false\ndate: 2026-04-13\nreadingTime: \"18 min read\"\n---\n\nMost bot detection systems operate on a single layer: a rule list, a rate limiter, or a third-party API call. The problem with that model is that any single signal can be spoofed. A bot can rotate IPs, forge user-agent strings, and slow its request rate to look human. Defeating it requires combining signals from multiple independent layers so that evading one does not defeat the others.\n\nThe Riavzon stack addresses this with three coordinated components. Shield Base compiles IP intelligence from a dozen external sources into binary databases. Bot Detector runs those databases through a two-phase, 17-checker pipeline that scores every incoming request. The IAM canary cookie ties each browser session to a fingerprint that follows it through every subsequent request. This post walks through every layer in detail — how each one works, what data it uses, and what happens when a bot hits the stack.\n\n---\n\n## The Three Layers at a Glance\n\nBefore going deep on each component, it helps to understand how they relate to one another.\n\nShield Base is a build-time tool. You run it once to produce a set of binary database files, then run it again periodically to refresh them. It has no runtime presence — it just produces the files that the other layers consume.\n\nBot Detector is a runtime Express middleware. It reads the Shield Base databases at startup and holds them in memory. Every request passes through its pipeline, which scores the request across behavioral, fingerprint, and reputation dimensions. If the score reaches the ban threshold, the middleware short-circuits the request before it touches any application logic.\n\nThe canary cookie is a per-session identifier, issued on first contact and carried on every subsequent request. Bot Detector uses it to track session state across requests — storing timing patterns, path history, and reputation scores keyed on the cookie value. The IAM service uses the same cookie to bind authentication tokens to a specific visitor fingerprint, enabling anomaly detection during token rotation.\n\n```\nShield Base (build time)\n  └── Compiles MMDB + LMDB databases\n        └── Bot Detector (runtime middleware)\n              ├── Cheap phase: 10 synchronous checkers\n              ├── Heavy phase: 7 async checkers\n              └── Issues canary_id cookie on first request\n                    └── IAM service\n                          ├── Binds refresh tokens to canary fingerprint\n                          └── Flags anomalies during rotation\n```\n\n---\n\n## Shield Base: Compiling the Intelligence Layer\n\nShield Base is a CLI tool that downloads, processes, and compiles external threat intelligence into binary formats that Bot Detector can query in microseconds at runtime. It produces two kinds of output: MMDB files for IP-range lookups and LMDB files for hash-keyed pattern matching.\n\n### Why Binary Databases\n\nThe raw data that feeds bot detection is enormous. BGP routing tables, geolocation datasets, Tor node lists, FireHOL threat feeds, and user-agent pattern databases together contain hundreds of millions of entries. Querying them naively at runtime is not practical. MMDB (MaxMind DB) encodes IP ranges into a binary trie that resolves any IP to its metadata in a single file seek. LMDB (Lightning Memory-Mapped Database) is a memory-mapped key-value store that delivers zero-copy reads with no serialization overhead. Both formats are loaded once at startup and kept in memory for the lifetime of the process.\n\n### The 14 Data Sources\n\nShield Base downloads and compiles 14 distinct data sources, each targeting a different threat signal.\n\n**IP reputation and routing**\n\n| Database | Output | Source | What it contains |\n|---|---|---|---|\n| ASN routing | `asn.mmdb` | bgp.tools | Autonomous system numbers, ISP classification, network visibility |\n| City geolocation | `city.mmdb` | MaxMind Geofeed | IP-to-city mappings with coordinates, timezone, and subdivision |\n| Country\u002Fgeography | `country.mmdb` | Sapics ip-location-db | IPv4-to-country with continent and subregion data |\n| Proxy detection | `proxy.mmdb` | Custom proxy lists | Known VPN exit points and proxy server IPs |\n| Tor nodes | `tor.mmdb` | Torproject Onionoo API | Active Tor relays classified by role: exit, guard, bad exit |\n| Verified crawlers | `goodBots.mmdb` | Web crawler domain lists | IP ranges belonging to legitimate search engines and SEO crawlers |\n\n**Threat intelligence (FireHOL)**\n\nFireHOL maintains multiple threat list tiers. Shield Base compiles all of them into separate MMDB files, which Bot Detector queries independently so that the scoring system can assign different penalty weights to each tier.\n\n| Level | File | What it tracks |\n|---|---|---|\n| L1 | `firehol_l1.mmdb` | Current attacks — minimum false positives, maximum severity |\n| L2 | `firehol_l2.mmdb` | Attacks observed in the last 48 hours, including dynamic IPs |\n| L3 | `firehol_l3.mmdb` | Attacks, spyware, and viruses tracked over the last 30 days |\n| L4 | `firehol_l4.mmdb` | Aggressive tracking with a higher false-positive rate |\n| Anonymous | `firehol_anonymous.mmdb` | Tor exit nodes, I2P, VPNs, and other anonymity relays |\n\n**Pattern databases (LMDB)**\n\n| Database | Directory | What it contains |\n|---|---|---|\n| User-agent patterns | `useragent-db\u002Fuseragent.mdb` | Known bot, scraper, and tool user-agent signatures with severity ratings |\n| Disposable emails | `email-db\u002Fdisposable-emails.mdb` | Domain blocklist for temporary and disposable email providers |\n\n### Running Shield Base\n\nThe CLI accepts flags for individual sources or bulk compilation. The `--parallel` flag compiles all sources concurrently, which is the standard approach for periodic refreshes.\n\n::code-group\n\n```bash [pnpm]\n# Compile all sources in parallel\npnpm shield-base --all --parallel\n\n# Compile specific sources\npnpm shield-base --bgp --geo --tor --l1 --l2\n\n# Compile only LMDB pattern databases\npnpm shield-base --useragent --email\n```\n\n```bash [npm]\n# Compile all sources in parallel\nnpm run shield-base --all --parallel\n\n# Compile specific sources\nnpm run shield-base --bgp --geo --tor --l1 --l2\n\n# Compile only LMDB pattern databases\nnpm run shield-base --useragent --email\n```\n\n::\n\nInternally, `executeAll` runs 10 compilation tasks in parallel. Each task downloads its source data, processes it into the intermediate format, and compiles it using either the `mmdbctl` binary (for MMDB) or the native LMDB Node.js bindings. The output files land in a configured output directory that Bot Detector reads from at startup.\n\n::note\nShield Base requires a valid contact User-Agent for the BGP\u002FASN data fetch from bgp.tools. Configure this in your Shield Base settings before running the first compilation.\n::\n\n---\n\n## Bot Detector: The Two-Phase Scoring Pipeline\n\nBot Detector is a middleware factory. You call `configuration(config)` once at startup to register your settings and mount the middleware on your Express router. From that point on, every request passes through the pipeline, accumulates a score, and either continues to the next handler or receives a ban response.\n\n### Loading the Databases\n\nThe `DataSources` class loads all Shield Base outputs at initialization. It opens 11 MMDB readers (ASN, city, country, good bots, Tor, proxy, and all five FireHOL levels) and 1 LMDB reader (user-agent patterns). It also accepts optional banned and high-risk MMDB files for custom enforcement lists. All readers stay open and memory-resident for the lifetime of the process. There are no per-request file operations — every lookup is an in-memory binary search.\n\n### Scoring Mechanics\n\nEvery request starts with a score of zero. Checkers increment the score when they detect anomalies. The pipeline compares the running total against `banScore` (default: 100) after the cheap phase and again after the heavy phase. Reaching `banScore` at any point ends the pipeline immediately and sends a ban response.\n\nBetween requests, a reputation healer decrements the stored score by `restoredReputationPoints` (default: 10) for every non-banned request. A visitor who accumulated a score of 35 on a suspicious-looking first request will recover to zero across three or four clean subsequent requests, assuming no new checkers fire.\n\n```ts\n\u002F\u002F Default scoring configuration\nawait configuration({\n  banScore: 100,\n  maxScore: 100,\n  restoredReputationPoints: 10,\n  setNewComputedScore: false,\n  \u002F\u002F ...\n})\n```\n\nSetting `setNewComputedScore: false` (the recommended default) means the detector writes the computed score to the database only when no prior record exists. On subsequent requests, the reputation healer decrements the stored score without recomputing. This prevents a bot that varies its signals slightly between requests from oscillating between high and low scores — it accumulates a record and decays from it.\n\n### Phase One: The Cheap Checkers\n\nThe cheap phase runs 10 synchronous checks. These checks use only in-memory data — parsed request headers, pre-loaded database lookups, and cached session state. They run in microseconds. If the cumulative score reaches `banScore` at any point in this phase, the pipeline stops immediately.\n\n**1. IP Validation** — confirms the request carries a parseable, routable IP address. Malformed or missing IPs score 10 points. This catches raw tool invocations that do not set a legitimate source address.\n\n**2. Good and Bad Bot Verification** — checks the request's IP against `goodBots.mmdb`. If the IP belongs to a known crawler, the middleware performs a reverse DNS lookup to verify the IP actually belongs to the claimed crawler domain. A passing DNS check issues `GOOD_BOT_IDENTIFIED` and whitelists the request instantly — no further checks run. A failing DNS check (IP on the good-bot list but DNS does not verify) issues `BAD_BOT_DETECTED` at 100 points — an instant ban. This checker handles the common impersonation pattern where a bot claims a Google or Bing user-agent from an unrelated hosting IP.\n\n**3. Browser and Device Fingerprint** — parses the `User-Agent` header and applies penalties for impossible or implausible combinations.\n\n| Signal | Penalty |\n|---|---|\n| CLI tool or HTTP library (curl, Python requests, etc.) | 100 |\n| Internet Explorer | 100 |\n| Kali Linux OS | 10 |\n| Impossible browser\u002FOS combination | 30 |\n| Unknown browser type or name | 10 |\n| Desktop device without detectable OS | 10 |\n| Unknown device vendor | 10 |\n| Unknown browser version | 10 |\n| Unknown device model | 5 |\n\n**4. Locale Map Verification** — compares the `Accept-Language` header against the IP's geolocation country. A browser claiming `fr-FR` language from an IP geolocated to South Korea is suspicious. Missing or malformed `Accept-Language` headers score 20 points. A confirmed mismatch between language and geo scores an additional 20 points.\n\n**5. Known Threats (FireHOL)** — queries all five FireHOL MMDB files against the request IP. Each tier scores independently, so an IP appearing on multiple lists accumulates points from each.\n\n| FireHOL tier | Penalty |\n|---|---|\n| Anonymity network (Tor, VPN, I2P) | 20 |\n| L1 — critical current threats | 40 |\n| L2 — attacks in last 48 hours | 30 |\n| L3 — attacks in last 30 days | 20 |\n| L4 — aggressive tracking | 10 |\n\n**6. ASN Classification** — queries `asn.mmdb` to determine the Autonomous System the IP belongs to. Hosting and datacenter ASNs score 20 points. An ASN with unusually low visibility (few routes announced, below 15% of expected) scores an additional 10 points. The combination of hosting classification and low visibility scores a further 20 — this pattern is characteristic of freshly provisioned bot infrastructure.\n\n**7. Tor Node Analysis** — queries `tor.mmdb` to classify the specific role of any Tor node. Different node types carry different penalties because they represent different risk profiles.\n\n| Tor node type | Penalty |\n|---|---|\n| Active running node | 15 |\n| Exit node (base) | 20 |\n| Exit node (exit probability multiplier, up to +30) | dynamic |\n| Web-capable exit node | 15 |\n| Guard node | 10 |\n| Bad exit (flagged by Tor directory) | 40 |\n| Obsolete version | 10 |\n\nA high-probability exit node that is also flagged as a bad exit and running an obsolete version can accumulate 90 points from Tor analysis alone — enough to ban when combined with even minor signals from other checkers.\n\n**8. Timezone Consistency** — compares the `Timezone` request header against the timezone inferred from the IP's geolocation. A browser reporting a Central European timezone from an IP geolocated to Hong Kong scores 20 points.\n\n**9. Honeypot** — checks the request path against a configurable list of trap URLs. Any request to a honeypot path scores an immediate ban. Legitimate users never visit URLs that are not linked anywhere in the application. Only crawlers following harvested or guessed paths hit them.\n\n**10. Known Bad IPs** — queries optional `banned.mmdb` and `highRisk.mmdb` files you maintain independently. Previously banned IPs score an instant ban. High-risk IPs score 30 points. This checker enables you to carry forward enforcement decisions across restarts and import external blocklists.\n\n### Phase Two: The Heavy Checkers\n\nThe heavy phase runs only if the cheap phase did not trigger a ban. These seven checks require async operations — cache reads, timing calculations, database queries, and header analysis. They are deferred to the second phase because they are more expensive.\n\n**11. Behavior Rate Verification** — counts requests from this `canary_id` within a sliding window (default: 60 seconds, threshold: 30 requests). Exceeding the threshold scores 60 points. Unlike a simple IP-based rate limiter, this checker tracks per-session request rates. A bot that uses many IPs but reuses the same session cookie still triggers it.\n\n**12. Proxy, ISP, and Cookie Verification** — combines several signals into a single checker.\n\n| Signal | Penalty |\n|---|---|\n| Missing `canary_id` cookie | 80 |\n| Proxy detected (from `proxy.mmdb`) | 40 |\n| Multi-source proxy confirmation (2-3 sources) | +10 |\n| Multi-source proxy confirmation (4+ sources) | +20 |\n| Hosting provider detected | 50 |\n| Unknown ISP | 10 |\n| Unknown ORG | 10 |\n\nThe `canary_id` cookie check is the single highest-penalty individual signal in the pipeline at 80 points. Any request that does not carry a cookie is one triggering event away from a ban. This matters because the cookie is set on the very first request — a missing cookie on a subsequent request means either the client is rejecting cookies (a strong bot signal) or the request is coming from a tool that does not preserve session state.\n\n**13. Session Coherence** — uses the `canary_id` to retrieve the session's last known path from the session cache, then validates the incoming request's `Referer` header.\n\n| Signal | Penalty |\n|---|---|\n| Missing `Referer` on a same-origin request (`Sec-Fetch-Site: same-origin`) | 20 |\n| `Referer` domain does not match the application domain | 30 |\n| `Referer` path does not match the recorded last path | 10 |\n\nReal browsers send a `Referer` header when navigating within the same origin. Tools and scrapers that issue requests directly do not. A bot that correctly spoofs headers but does not correctly maintain session path history fails this check across multiple requests.\n\n**14. Velocity Fingerprinting** — collects timestamps for the last 10 requests from this session (minimum 5 required to evaluate) and computes the coefficient of variation (CV) of the inter-request intervals. The CV measures the relative variability of a set of values — a CV near zero means all intervals are nearly identical, which is characteristic of programmatic request scheduling.\n\n```\nCV = standard deviation \u002F mean\n\nCV \u003C 0.1 → timing too regular → penalty: 40\n```\n\nHuman browsing intervals are naturally irregular. Page load times, reading time, and click latency all vary. A bot that fires requests on a fixed timer — even a slow one — produces a CV far below the 0.1 threshold.\n\n**15. User-Agent and Header Analysis** — extends the cheap-phase fingerprint check with deeper inspection.\n\n| Signal | Penalty |\n|---|---|\n| Headless browser detected (Puppeteer, Selenium, Playwright, PhantomJS) | 100 |\n| User-agent shorter than 10 characters | 80 |\n| Header anomaly score too high | variable |\n| Path traversal attempt detected | variable |\n| XSS scripting attempt detected | variable |\n\n**16. Geolocation Validation** — penalizes missing geolocation data across nine dimensions: country, region, city, latitude\u002Flongitude, timezone, subregion, phone prefix, district, and continent. Each missing dimension scores 10 points. A request from an IP with no geolocation coverage can accumulate up to 90 points from this checker alone, making it trivially over the ban threshold when combined with any other signal. The checker also supports a configurable banned-country list.\n\n**17. Known Bad User-Agents** — queries `useragent.mdb` against the full user-agent string. The LMDB database stores patterns compiled from community-maintained lists of bot and scraper signatures, each rated by severity.\n\n| Severity | Penalty |\n|---|---|\n| Critical | 100 |\n| High | 80 |\n| Medium | 30 |\n| Low | 10 |\n\n---\n\n## The Canary Cookie: Bridging Sessions\n\nThe `canary_id` cookie is issued by the `canaryCookieChecker` middleware on the very first request from any browser. Its value is a 64-character hex string generated from 32 cryptographically random bytes.\n\n```ts\nrandomBytes(32).toString('hex')\n\u002F\u002F Example: \"a3f8e2c1d4b7a90f...\"  (64 hex characters)\n```\n\nThe cookie itself is opaque — it carries no embedded data and cannot be decoded. All the meaningful state lives server-side, keyed on the cookie value.\n\n### Cookie Attributes\n\n```\nname:      canary_id\nhttpOnly:  true\nsameSite:  lax\nsecure:    true\npath:      \u002F\nmaxAge:    7,776,000,000 ms  (90 days)\n```\n\nThe `httpOnly` attribute prevents JavaScript from reading the cookie, blocking the class of attacks where a page script exfiltrates the cookie and reuses it from a different client. The 90-day maxAge matches the outer boundary for legitimate long-running sessions.\n\n### What the Server Stores\n\nWhen Bot Detector issues a `canary_id`, it begins building a persistent record keyed on that value. This record accumulates across every subsequent request.\n\n**Visitor record (database, persistent):**\n\n```ts\n{\n  visitorId: UUID,\n  cookie: canary_id,\n  userAgent: string,\n  ipAddress: string,\n  device_type: string,\n  browser: string,\n  is_bot: boolean,\n  first_seen: timestamp,\n  last_seen: timestamp,\n  request_count: number,\n  deviceVendor: string,\n  deviceModel: string,\n  browserType: string,\n  browserVersion: string,\n  os: string,\n  activity_score: number,\n  country: string,\n  region: string,\n  city: string,\n  timezone: string,\n  \u002F\u002F ...additional geolocation fields\n}\n```\n\n**In-memory caches (fast lookup per request):**\n\n| Cache | Key | What it holds |\n|---|---|---|\n| `visitorCache` | `canary_id` | `{ banned, visitor_id }` — fast ban lookup |\n| `sessionCache` | `canary_id` | `{ lastPath }` — session coherence tracking |\n| `rateCache` | `canary_id` | `{ score, timestamp, request_count }` — behavioral rate |\n| `timingCache` | `canary_id` | Array of last 10 request timestamps — velocity fingerprint |\n| `reputationCache` | `canary_id` | `{ isBot, score }` — reputation healer state |\n| `dnsCache` | IP | `{ ip, trustedBot }` — verified crawler result |\n\nThe split between the persistent database record and the in-memory caches is intentional. The database record survives restarts and is queryable for analytics. The in-memory caches are ephemeral but fast — they hold exactly the data the pipeline needs per request, without deserializing a full database row.\n\n### The Canary Cookie in the IAM Service\n\nThe IAM service runs Bot Detector as part of its own middleware chain. Every request to the IAM service — login, logout, token rotation, MFA — passes through the same 17-checker pipeline before reaching any authentication logic.\n\nWhen Bot Detector passes a request through, the IAM service reads the `canary_id` cookie and stores it alongside the refresh token family for that session. The `strangeThings()` anomaly detection function, which runs during every token rotation attempt, includes a `canary_id` binding check as one of its nine sequential verifications.\n\nIf the `canary_id` on a rotation request does not match the one recorded when the session was originally created, the anomaly detector triggers. Depending on the severity, it either sends an MFA challenge to the user's email or revokes the session entirely. This means an attacker who steals a valid refresh token but makes the rotation request from a different device — one with a different `canary_id` — cannot complete the rotation without also accessing the user's email.\n\n---\n\n## Walking Through a Bot Request\n\nTo make the pipeline concrete, here is what happens when a credential-stuffing bot attempts a login.\n\nThe bot sends a `POST \u002Fauth\u002Fuser\u002Flogin` request with a valid email and password combination. It uses a Python `requests` library with a spoofed user-agent string, from a residential proxy pool. It sends one request every 4 seconds on a fixed timer.\n\n**Cheap phase results:**\n\n- IP Validation: passes (valid IPv4).\n- Good\u002FBad Bot: IP is not on the good-bot list. No instant ban.\n- Browser and Device Fingerprint: The user-agent parses as Chrome, but the library headers are subtly wrong — no `sec-ch-ua` header family, no `sec-fetch-*` headers. Unknown browser type: +10. Impossible header combination: +30. Running total: 40.\n- Locale Map: The `Accept-Language` header is missing. +20. Running total: 60.\n- Known Threats: The residential proxy IP happens to appear on the FireHOL L3 list (a 30-day tracked threat). +20. Running total: 80.\n- ASN Classification: The proxy's ASN is classified as hosting with low visibility. +20 + +10. Running total exceeds 100.\n\n**The pipeline stops at the cheap phase.** The request receives a 403 response before the login handler runs. No database query for the user record. No password check. No rate limiter on the login endpoint needs to absorb the request.\n\nNow consider a more sophisticated bot — one that uses a real browser, a real residential IP, and carefully spoofs all headers. The cheap phase may score only 10-20 points.\n\n**Heavy phase results:**\n\n- Behavior Rate: The bot fires at exactly 4-second intervals. After 5 requests, the velocity fingerprint computes CV = 0.02. +40. Running total: 50-60.\n- Session Coherence: The bot navigates directly to `\u002Fauth\u002Fuser\u002Flogin` without going through the home page first. The `Referer` header is absent on what looks like same-origin navigation. +20. Running total: 70-80.\n- User-Agent and Header Analysis: Header mismatch and lack of acceptable HTTP configurations indicate automated access. +60. Running total: 130+.\n\n**The pipeline stops at the heavy phase.** Even a well-configured bot that passes the cheap phase reveals itself through timing regularity, navigation patterns, and header analysis.\n\n---\n\n## Configuration\n\nA realistic Bot Detector configuration that enables the full pipeline looks like this:\n\n```ts\nimport { configuration } from 'bot-detector'\n\nawait configuration({\n  store: {\n    main: { driver: 'sqlite', name: '.\u002Fbot-detector.db' }\n  },\n\n  banScore: 100,\n  maxScore: 100,\n  restoredReputationPoints: 10,\n  setNewComputedScore: false,\n\n  whiteList: ['203.0.113.0\u002F24'],\n\n  checkers: {\n    enableIpChecks: { enable: true, penalties: 10 },\n\n    enableGoodBotsChecks: {\n      enable: true,\n      banUnlistedBots: true,\n      penalties: 100\n    },\n\n    enableBrowserAndDeviceChecks: { enable: true },\n\n    localeMapsCheck: { enable: true },\n\n    enableKnownThreatsDetections: {\n      enable: true,\n      penalties: {\n        anonymityNetwork: 20,\n        fireholL1: 40,\n        fireholL2: 30,\n        fireholL3: 20,\n        fireholL4: 10\n      }\n    },\n\n    enableAsnClassification: { enable: true },\n\n    enableTorAnalysis: { enable: true },\n\n    enableTimezoneConsistency: { enable: true },\n\n    honeypot: {\n      enable: true,\n      paths: ['\u002Fadmin', '\u002F.env', '\u002Fwp-login.php', '\u002Fxmlrpc.php']\n    },\n\n    enableKnownBadIpsCheck: { enable: true },\n\n    enableBehaviorRateCheck: {\n      enable: true,\n      behavioral_window: 60_000,\n      behavioral_threshold: 30,\n      penalties: 60\n    },\n\n    enableProxyIspCookiesChecks: { enable: true },\n\n    enableSessionCoherence: { enable: true },\n\n    enableVelocityFingerprint: {\n      enable: true,\n      cvThreshold: 0.1\n    },\n\n    enableUaAndHeaderChecks: { enable: true },\n\n    enableGeoChecks: {\n      enable: true,\n      bannedCountries: []\n    },\n\n    knownBadUserAgents: { enable: true }\n  }\n})\n```\n\n::tip\nStart with the cheap-phase checkers at conservative penalty values and raise them after observing traffic patterns. The FireHOL L4 level and ASN low-visibility penalties are the most likely to produce false positives on legitimate traffic from cloud-heavy regions.\n::\n\n---\n\n## Extending the Pipeline: Custom Checkers\n\nEvery built-in checker follows the same interface, and you can add your own with the exact same mechanism. The pipeline does not distinguish between built-in and custom checkers at runtime — they share the same scoring accumulation, the same short-circuit logic, and the same `ValidationContext`.\n\n### The `IBotChecker` Interface\n\nA checker is a class that implements `IBotChecker`. It declares which phase it belongs to, a condition that enables or disables it, and a `run` method that returns a numeric score and an array of reason codes.\n\n```ts\ninterface IBotChecker\u003CCode, TCustom = Record\u003Cstring, never>> {\n  name: string;\n  phase: 'cheap' | 'heavy';\n  isEnabled(config: BotDetectorConfig): boolean;\n  run(ctx: ValidationContext\u003CTCustom>, config: BotDetectorConfig):\n    | Promise\u003C{ score: number; reasons: Code[] }>\n    | { score: number; reasons: Code[] };\n}\n```\n\nThe `run` method can be synchronous or async. Phase assignment is the only routing decision you make — everything else is handled by the pipeline.\n\n### What the Pipeline Gives You\n\nBefore your `run` method executes, the pipeline has already resolved every expensive lookup. All of this is available on `ctx` at zero cost:\n\n| Field | Contents |\n|---|---|\n| `ctx.req` | Full Express request (headers, path, cookies, method) |\n| `ctx.ipAddress` | Resolved client IP |\n| `ctx.cookie` | `canary_id` value, or `undefined` on first request |\n| `ctx.geoData` | Merged country, city, ASN, and proxy data |\n| `ctx.tor` | Tor relay classification from `tor.mmdb` |\n| `ctx.bgp` | ASN routing data: `asn_id`, `asn_name`, `classification`, `hits` |\n| `ctx.threatLevel` | Highest FireHOL tier matched (`1`–`4`), or `null` |\n| `ctx.anon` | `true` if IP is in the anonymity network database |\n| `ctx.parsedUA` | Parsed user-agent: browser, OS, device, `browserType`, bot flags |\n| `ctx.proxy` | `{ isProxy, proxyType }` from proxy MMDB |\n| `ctx.custom` | Your own per-request data, populated by `buildCustomContext` |\n\n`ctx.bgp.classification` is worth highlighting. The value `\"Content\"` means the ASN is classified as a hosting or CDN network. `\"Eyeballs\"` means residential or business internet. This single field lets a custom checker apply completely different logic for datacenter traffic versus consumer traffic without any additional lookup.\n\n### A Minimal Cheap Checker\n\nThe example below penalises requests from a datacenter ASN that carry no `Accept-Language` header — a pattern common in automated clients that partially spoof browser headers but miss the locale details.\n\n```ts [datacenter-locale-checker.ts]\nimport { CheckerRegistry } from '@riavzon\u002Fbot-detector';\nimport type { IBotChecker, ValidationContext, BotDetectorConfig } from '@riavzon\u002Fbot-detector';\n\ntype Code = 'DATACENTER_NO_LOCALE' | 'BAD_BOT_DETECTED';\n\nclass DatacenterLocaleChecker implements IBotChecker\u003CCode> {\n  name = 'DatacenterLocaleChecker';\n  phase = 'cheap' as const;\n\n  isEnabled(_config: BotDetectorConfig): boolean {\n    return true;\n  }\n\n  run(ctx: ValidationContext, _config: BotDetectorConfig) {\n    const reasons: Code[] = [];\n    let score = 0;\n\n    const isHosting = ctx.bgp.classification === 'Content';\n    const hasLocale = Boolean(ctx.req.get('Accept-Language'));\n\n    if (isHosting && !hasLocale) {\n      score += 25;\n      reasons.push('DATACENTER_NO_LOCALE');\n    }\n\n    return { score, reasons };\n  }\n}\n\nCheckerRegistry.register(new DatacenterLocaleChecker());\n```\n\nRegistration happens at module load time. A side-effect import in your server entry point is enough to activate the checker. Import order controls execution order within each phase.\n\n```ts [server.ts]\nimport { defineConfiguration, detectBots } from '@riavzon\u002Fbot-detector';\nimport '.\u002Fdatacenter-locale-checker.js'; \u002F\u002F registers on import\n\nawait defineConfiguration({ \u002F* ... *\u002F });\napp.use(detectBots());\n```\n\n### Passing Application Context Into Checkers\n\nThe `buildCustomContext` function runs once per request before any checker executes. It receives the raw Express request and returns the `ctx.custom` object. Passing the generic type through to `IBotChecker` and `ValidationContext` gives full IntelliSense on `ctx.custom` inside `run`.\n\n```ts [server.ts]\ninterface MyContext {\n  userId: string;\n  plan: 'free' | 'pro' | 'enterprise';\n  isInternal: boolean;\n}\n\napp.use(\n  detectBots\u003CMyContext>((req) => ({\n    userId:     req.user?.id   ?? 'anonymous',\n    plan:       req.user?.plan ?? 'free',\n    isInternal: req.ip === '127.0.0.1',\n  }))\n);\n```\n\n```ts [plan-abuse-checker.ts]\nimport type { IBotChecker, ValidationContext, BotDetectorConfig, BanReasonCode } from '@riavzon\u002Fbot-detector';\nimport type { MyContext } from '.\u002FmyContext.js';\n\nclass PlanAbuseChecker implements IBotChecker\u003CBanReasonCode, MyContext> {\n  name = 'PlanAbuseChecker';\n  phase = 'cheap' as const;\n\n  isEnabled(_config: BotDetectorConfig) { return true; }\n\n  run(ctx: ValidationContext\u003CMyContext>, _config: BotDetectorConfig) {\n    if (ctx.custom.isInternal) return { score: 0, reasons: [] };\n\n    if (ctx.custom.plan === 'free' && ctx.geoData.proxy) {\n      return { score: 20, reasons: ['PROXY_DETECTED'] };\n    }\n\n    return { score: 0, reasons: [] };\n  }\n}\n```\n\nThis pattern lets you apply business logic — plan tier, user role, internal traffic bypass — inside the same scoring pipeline that handles IP reputation and behavioral analysis, without any special wiring.\n\n### Triggering an Instant Ban\n\nReturning `'BAD_BOT_DETECTED'` in the reasons array causes the pipeline to throw `BadBotDetected` immediately. No further checkers run, and the reputation healer does not execute. The visitor is banned without waiting for score accumulation.\n\n```ts\nrun(ctx: ValidationContext, _config: BotDetectorConfig) {\n  if (isDefinitelyABot(ctx)) {\n    return { score: 0, reasons: ['BAD_BOT_DETECTED'] };\n  }\n  return { score: 0, reasons: [] };\n}\n```\n\nThe mirror is `'GOOD_BOT_IDENTIFIED'`, which whitelists the request instantly. The built-in good-bot DNS verifier uses this same mechanism.\n\n### Heavy Checkers and the Built-In Storage\n\nCheckers that require I\u002FO — database queries, external API calls, cache reads — declare `phase: 'heavy'`. The heavy phase only runs when the cheap phase score stays below `banScore`. Call `getStorage()` to access the same storage instance Bot Detector uses internally, keeping all cache I\u002FO in one place.\n\n```ts [my-async-checker.ts]\nimport { getStorage, CheckerRegistry } from '@riavzon\u002Fbot-detector';\nimport type { IBotChecker, ValidationContext, BotDetectorConfig } from '@riavzon\u002Fbot-detector';\n\nclass MyAsyncChecker implements IBotChecker\u003C'MY_REASON'> {\n  name = 'MyAsyncChecker';\n  phase = 'heavy' as const;\n\n  isEnabled(_config: BotDetectorConfig): boolean { return true; }\n\n  async run(ctx: ValidationContext, _config: BotDetectorConfig) {\n    if (!ctx.cookie) return { score: 0, reasons: [] };\n\n    const storage = getStorage();\n    const cacheKey = `custom:${ctx.cookie}`;\n\n    const cached = await storage.getItem\u003Cnumber>(cacheKey);\n    if (cached !== null) {\n      return { score: cached, reasons: cached > 0 ? ['MY_REASON' as const] : [] };\n    }\n\n    const result = await myDb.query('SELECT ...', [ctx.ipAddress]);\n    const score = result.isSuspicious ? 30 : 0;\n\n    await storage.setItem(cacheKey, score, { ttl: 300 });\n    return { score, reasons: score > 0 ? ['MY_REASON' as const] : [] };\n  }\n}\n\nCheckerRegistry.register(new MyAsyncChecker());\n```\n\n::tip\nUse a namespaced key prefix for your cache entries (for example `custom:`) to avoid collisions with the built-in cache keys that share the same storage instance.\n::\n\n---\n\n## Automatic Threat Compilation: The Generator\n\nThe `Known Bad IPs` checker — checker 10 in the cheap phase — queries two optional MMDB files: `banned.mmdb` and `highRisk.mmdb`. These files do not come from Shield Base. Bot Detector generates them itself from its own accumulated traffic history.\n\n### What Gets Compiled\n\nRunning `bot-detector generate` reads two tables from Bot Detector's database and compiles each into an MMDB file. Both compilations run in parallel.\n\n**`banned.mmdb`** — every row in the `banned` table with a non-null `ip_address` gets compiled into this file. Each entry stores the IP, score, country, user-agent, and reason codes from the original ban event. On subsequent visits, the Known Bad IPs checker matches the IP in microseconds in the cheap phase and issues `BAD_BOT_DETECTED` immediately — the full 17-checker pipeline never runs for a confirmed repeat offender.\n\n**`highRisk.mmdb`** — every row in the `visitors` table where `suspicious_activity_score` is at or above `generator.scoreThreshold` (default `70`) is compiled into this file. These are visitors who accumulated significant suspicion scores but were never pushed over `banScore`. On their next visit, they receive the `highRiskPenalty` (default `30` points) in the cheap phase, meaning far less effort from other checkers is needed to reach a ban.\n\n```ts\ngenerator: {\n  scoreThreshold: 70,   \u002F\u002F minimum score to include in highRisk.mmdb\n  deleteAfterBuild: false,  \u002F\u002F if true, removes compiled rows from DB after build\n  mmdbctlPath: 'mmdbctl',   \u002F\u002F path to mmdbctl binary\n  generateTypes: false,     \u002F\u002F emit TypeScript type definitions alongside MMDB files\n}\n```\n\nThe `scoreThreshold` tradeoff is worth understanding. Lowering it to `40` catches visitors with moderate suspicious history but risks false positives. Keeping it at `70` or higher limits the file to visitors with strong behavioral evidence.\n\n| Threshold | Effect |\n|---|---|\n| `40` | Broader net — includes visitors with moderate accumulated scores |\n| `70` (default) | Balanced — strong suspicious history required |\n| `90` | Conservative — only the most suspicious non-banned visitors included |\n\n### Hot Reload\n\nBoth MMDB files are opened with `watchForUpdates: true`. When a new file is written to disk after a generation run, the MMDB reader reloads it automatically within seconds — no application restart, no traffic interruption. You can run generation against a live service and the updated databases take effect immediately.\n\n### Running Generation\n\n::code-group\n\n```bash [pnpm]\npnpm dlx @riavzon\u002Fbot-detector generate\n```\n\n```bash [yarn]\nyarn dlx @riavzon\u002Fbot-detector generate\n```\n\n```bash [npm]\nnpx @riavzon\u002Fbot-detector generate\n```\n\n```bash [bun]\nbunx @riavzon\u002Fbot-detector generate\n```\n\n::\n\nFor programmatic use — for example, triggering generation immediately after a bulk ban operation — call `runGeneration()` directly:\n\n```ts [admin-script.ts]\nimport { updateBannedIP, runGeneration } from '@riavzon\u002Fbot-detector';\nimport type { BannedInfo } from '@riavzon\u002Fbot-detector';\n\nfor (const ip of badIps) {\n  const info: BannedInfo = { score: 100, reasons: ['PREVIOUSLY_BANNED_IP'] };\n  await updateBannedIP('', ip, 'us', '', info);\n}\n\n\u002F\u002F Compile updated MMDB files immediately so the next request from these IPs\n\u002F\u002F hits the cheap-phase known-bad-IPs check rather than the full pipeline.\nawait runGeneration();\n```\n\n### Scheduling Generation\n\nThe right generation frequency depends on traffic volume. A nightly run is a reasonable default. For higher-traffic applications where bans accumulate quickly, hourly generation keeps the banned MMDB current and prevents repeat offenders from absorbing pipeline capacity.\n\n```cron [crontab]\n# Nightly at 2:00 AM\n0 2 * * * cd \u002Fapp && npx bot-detector generate >> \u002Fvar\u002Flog\u002Fbot-detector-generate.log 2>&1\n\n# Hourly for high-traffic deployments\n0 * * * * cd \u002Fapp && npx bot-detector generate >> \u002Fvar\u002Flog\u002Fbot-detector-generate.log 2>&1\n```\n\nThe generate command emits structured log lines including the entry count for each compiled database. Monitoring this output over time makes it easy to detect when ban volume spikes — a sudden increase in `banned.mmdb` entries typically indicates a coordinated attack campaign starting.\n\n---\n\n## Summary\n\nEach of the three layers closes a gap that the others cannot. Shield Base provides static intelligence — historical threat reputation, network classification, and behavioral pattern databases — that no runtime analysis can replicate. Bot Detector performs dynamic behavioral analysis — velocity, session coherence, timing regularity — that static blocklists cannot catch. The canary cookie ties both together across sessions, making it impossible to reset accumulated behavioral signals simply by rotating IPs or changing headers.\n\nA bot that evades Shield Base's IP reputation checks still faces 17 behavioral checkers. A bot that passes all 17 checkers on a single request still accumulates a session history that degrades its score over time. A bot that steals an authenticated session still cannot complete token rotation without matching the canary cookie fingerprint that was established on the original device.\n\nThe layered approach trades complexity for resilience. Each layer is effective in isolation. Together, they make the cost of a successful bot attack high enough that most attackers move on to easier targets.\n\n\nRead the full [Bot Detector](\u002Fdocs\u002Fbot-detection) reference\n\n\n\nRead the full [Shield Base](\u002Fdocs\u002Fshield-base) reference for database compilation options\n\n","18 min read",{"title":73,"description":10259},[38,33,10266],"Infrastructure","qrRrFdD_8vsWWEP0DFOijKzFT1C836gEzCUNyAsDjhk",1780436271297]