[{"data":1,"prerenderedAt":7349},["ShallowReactive",2],{"navLinks":3,"sidebar_docs_navigation_\u002Fdocs\u002Fiam":64,"navigation":257,"navLinks_footer":837,"\u002Fdocs\u002Fiam\u002Fsecurity_page":850,"\u002Fdocs\u002Fiam\u002Fsecurity_surround":4672,"\u002Fdocs\u002Fiam\u002Fsecurity":4675},{"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":9,"path":66,"stem":67,"children":68,"page":53},"\u002Fdocs","docs",[69],{"title":27,"path":29,"stem":70,"children":71},"docs\u002Fiam\u002Findex",[72,73,76,216,219,236,240],{"title":27,"path":29,"stem":70},{"title":14,"path":74,"stem":75},"\u002Fdocs\u002Fiam\u002Fgetting-started","docs\u002Fiam\u002F00.getting-started",{"title":77,"path":78,"stem":79,"children":80},"Essentials","\u002Fdocs\u002Fiam\u002Fessentials","docs\u002Fiam\u002F01.essentials\u002Findex",[81,82,86,90,94,98,102,106,110,114,118,122,126,130,134,138,142,146,150,154,158,162,166],{"title":77,"path":78,"stem":79},{"title":83,"path":84,"stem":85},"Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Ftokens","docs\u002Fiam\u002F01.essentials\u002F00.tokens",{"title":87,"path":88,"stem":89},"Access Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Faccess-tokens","docs\u002Fiam\u002F01.essentials\u002F01.access-tokens",{"title":91,"path":92,"stem":93},"Refresh Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Frefresh-tokens","docs\u002Fiam\u002F01.essentials\u002F02.refresh-tokens",{"title":95,"path":96,"stem":97},"Anomaly Detection","\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies","docs\u002Fiam\u002F01.essentials\u002F03.anomalies",{"title":99,"path":100,"stem":101},"Signup","\u002Fdocs\u002Fiam\u002Fessentials\u002Fsignup","docs\u002Fiam\u002F01.essentials\u002F04.signup",{"title":103,"path":104,"stem":105},"Login","\u002Fdocs\u002Fiam\u002Fessentials\u002Flogin","docs\u002Fiam\u002F01.essentials\u002F05.login",{"title":107,"path":108,"stem":109},"Logout","\u002Fdocs\u002Fiam\u002Fessentials\u002Flogout","docs\u002Fiam\u002F01.essentials\u002F06.logout",{"title":111,"path":112,"stem":113},"OAuth","\u002Fdocs\u002Fiam\u002Fessentials\u002Foauth","docs\u002Fiam\u002F01.essentials\u002F07.oauth",{"title":115,"path":116,"stem":117},"Magic Links","\u002Fdocs\u002Fiam\u002Fessentials\u002Fmagic-links","docs\u002Fiam\u002F01.essentials\u002F08.magic-links",{"title":119,"path":120,"stem":121},"Emails","\u002Fdocs\u002Fiam\u002Fessentials\u002Femails","docs\u002Fiam\u002F01.essentials\u002F09.emails",{"title":123,"path":124,"stem":125},"MFA","\u002Fdocs\u002Fiam\u002Fessentials\u002Fmfa","docs\u002Fiam\u002F01.essentials\u002F10.mfa",{"title":127,"path":128,"stem":129},"Fingerprinting","\u002Fdocs\u002Fiam\u002Fessentials\u002Ffingerprinting","docs\u002Fiam\u002F01.essentials\u002F11.fingerprinting",{"title":131,"path":132,"stem":133},"Backend for Frontend","\u002Fdocs\u002Fiam\u002Fessentials\u002Fbff","docs\u002Fiam\u002F01.essentials\u002F12.bff",{"title":135,"path":136,"stem":137},"HMAC Authentication","\u002Fdocs\u002Fiam\u002Fessentials\u002Fhmac","docs\u002Fiam\u002F01.essentials\u002F13.hmac",{"title":139,"path":140,"stem":141},"XSS Protection","\u002Fdocs\u002Fiam\u002Fessentials\u002Fxss","docs\u002Fiam\u002F01.essentials\u002F14.xss",{"title":143,"path":144,"stem":145},"Logging","\u002Fdocs\u002Fiam\u002Fessentials\u002Flogging","docs\u002Fiam\u002F01.essentials\u002F15.logging",{"title":147,"path":148,"stem":149},"Rate Limiting","\u002Fdocs\u002Fiam\u002Fessentials\u002Frate-limiting","docs\u002Fiam\u002F01.essentials\u002F16.rate-limiting",{"title":151,"path":152,"stem":153},"Database","\u002Fdocs\u002Fiam\u002Fessentials\u002Fdatabase","docs\u002Fiam\u002F01.essentials\u002F17.database",{"title":155,"path":156,"stem":157},"Cookies","\u002Fdocs\u002Fiam\u002Fessentials\u002Fcookies","docs\u002Fiam\u002F01.essentials\u002F18.cookies",{"title":159,"path":160,"stem":161},"Service Startup","\u002Fdocs\u002Fiam\u002Fessentials\u002Fservice","docs\u002Fiam\u002F01.essentials\u002F19.service",{"title":163,"path":164,"stem":165},"Password Reset","\u002Fdocs\u002Fiam\u002Fessentials\u002Fpassword-reset","docs\u002Fiam\u002F01.essentials\u002F20.password-reset",{"title":167,"path":168,"stem":169,"children":170},"API Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi","docs\u002Fiam\u002F01.essentials\u002F21.api\u002Findex",[171,172,176,180,210,213],{"title":167,"path":168,"stem":169},{"title":173,"path":174,"stem":175},"Creating Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fcreation","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F00.creation",{"title":177,"path":178,"stem":179},"Verifying Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fverification","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F01.verification",{"title":181,"path":182,"stem":183,"children":184},"Manage Tokens","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002Findex",[185,186,190,194,198,202,206],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189},"Privileges","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Fprivilege","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F00.privilege",{"title":191,"path":192,"stem":193},"Revocation","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Frevocation","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F01.revocation",{"title":195,"path":196,"stem":197},"Rotation","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Frotation","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F02.rotation",{"title":199,"path":200,"stem":201},"IP Restriction","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Fip-updates","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F03.ip-updates",{"title":203,"path":204,"stem":205},"Metadata","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Fmetadata","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F04.metadata",{"title":207,"path":208,"stem":209},"Token Listing","\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fmanagement\u002Flist","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F02.management\u002F05.list",{"title":147,"path":211,"stem":212},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Frate-limiting","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F03.rate-limiting",{"title":38,"path":214,"stem":215},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fsecurity","docs\u002Fiam\u002F01.essentials\u002F21.api\u002F04.security",{"title":38,"path":217,"stem":218},"\u002Fdocs\u002Fiam\u002Fsecurity","docs\u002Fiam\u002F02.security",{"title":220,"path":221,"stem":222,"children":223,"page":53},"Guides","\u002Fdocs\u002Fiam\u002Fguides","docs\u002Fiam\u002F03.guides",[224,228,232],{"title":225,"path":226,"stem":227},"Deployment","\u002Fdocs\u002Fiam\u002Fguides\u002Fdeployment","docs\u002Fiam\u002F03.guides\u002Fdeployment",{"title":229,"path":230,"stem":231},"Operation Scripts","\u002Fdocs\u002Fiam\u002Fguides\u002Foperation-scripts","docs\u002Fiam\u002F03.guides\u002Foperation-scripts",{"title":233,"path":234,"stem":235},"Role-Based Access Control","\u002Fdocs\u002Fiam\u002Fguides\u002Frbac","docs\u002Fiam\u002F03.guides\u002Frbac",{"title":237,"path":238,"stem":239},"Configuration","\u002Fdocs\u002Fiam\u002Fconfiguration","docs\u002Fiam\u002F04.configuration",{"title":241,"path":242,"stem":243,"children":244,"page":53},"Api","\u002Fdocs\u002Fiam\u002Fapi","docs\u002Fiam\u002F05.API",[245,249,253],{"title":246,"path":247,"stem":248},"API Reference","\u002Fdocs\u002Fiam\u002Fapi\u002Fapi","docs\u002Fiam\u002F05.API\u002F00.api",{"title":250,"path":251,"stem":252},"Middleware Reference","\u002Fdocs\u002Fiam\u002Fapi\u002Fmiddlewares","docs\u002Fiam\u002F05.API\u002F02.middlewares",{"title":254,"path":255,"stem":256},"Routes Reference","\u002Fdocs\u002Fiam\u002Fapi\u002Froutes","docs\u002Fiam\u002F05.API\u002F03.routes",[258],{"title":9,"path":66,"stem":67,"children":259,"page":53},[260,398,516,521,577,644],{"title":20,"path":22,"stem":261,"children":262},"docs\u002Fauth-h3client\u002Findex",[263,264,273,307,331,353,356,376,379],{"title":20,"path":22,"stem":261},{"title":14,"path":265,"stem":266,"children":267},"\u002Fdocs\u002Fauth-h3client\u002Fgetting-started","docs\u002Fauth-h3client\u002F00.getting-started\u002Findex",[268,269],{"title":14,"path":265,"stem":266},{"title":270,"path":271,"stem":272},"Nuxt Module","\u002Fdocs\u002Fauth-h3client\u002Fgetting-started\u002Fnuxt","docs\u002Fauth-h3client\u002F00.getting-started\u002F00.nuxt",{"title":77,"path":274,"stem":275,"children":276},"\u002Fdocs\u002Fauth-h3client\u002Fessentials","docs\u002Fauth-h3client\u002F01.essentials\u002Findex",[277,278,282,286,290,294,298,301,304],{"title":77,"path":274,"stem":275},{"title":279,"path":280,"stem":281},"Session Management","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fsession","docs\u002Fauth-h3client\u002F01.essentials\u002F00.session",{"title":283,"path":284,"stem":285},"Route Protection","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Froute-protection","docs\u002Fauth-h3client\u002F01.essentials\u002F01.route-protection",{"title":287,"path":288,"stem":289},"CSRF Protection","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fcsrf","docs\u002Fauth-h3client\u002F01.essentials\u002F02.csrf",{"title":291,"path":292,"stem":293},"Auth Flows","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fauth-flows","docs\u002Fauth-h3client\u002F01.essentials\u002F03.auth-flows",{"title":295,"path":296,"stem":297},"OAuth and OIDC","\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Foauth","docs\u002Fauth-h3client\u002F01.essentials\u002F04.oauth",{"title":33,"path":299,"stem":300},"\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fbot-detection","docs\u002Fauth-h3client\u002F01.essentials\u002F05.bot-detection",{"title":155,"path":302,"stem":303},"\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Fcookies","docs\u002Fauth-h3client\u002F01.essentials\u002F06.cookies",{"title":143,"path":305,"stem":306},"\u002Fdocs\u002Fauth-h3client\u002Fessentials\u002Flogging","docs\u002Fauth-h3client\u002F01.essentials\u002F07.logging",{"title":123,"path":308,"stem":309,"children":310},"\u002Fdocs\u002Fauth-h3client\u002Fmfa","docs\u002Fauth-h3client\u002F02.mfa\u002Findex",[311,312,316,319,323,327],{"title":123,"path":308,"stem":309},{"title":313,"path":314,"stem":315},"Built-in MFA","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fbuilt-in-flow","docs\u002Fauth-h3client\u002F02.mfa\u002F01.built-in-flow",{"title":163,"path":317,"stem":318},"\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fpassword-reset","docs\u002Fauth-h3client\u002F02.mfa\u002F02.password-reset",{"title":320,"path":321,"stem":322},"Email Change","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Femail-change","docs\u002Fauth-h3client\u002F02.mfa\u002F03.email-change",{"title":324,"path":325,"stem":326},"Custom MFA Flow","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fcustom-flow","docs\u002Fauth-h3client\u002F02.mfa\u002F04.custom-flow",{"title":328,"path":329,"stem":330},"Client-Side MFA","\u002Fdocs\u002Fauth-h3client\u002Fmfa\u002Fclient-side","docs\u002Fauth-h3client\u002F02.mfa\u002F05.client-side",{"title":332,"path":333,"stem":334,"children":335},"Client-side","\u002Fdocs\u002Fauth-h3client\u002Fclient","docs\u002Fauth-h3client\u002F03.client\u002Findex",[336,337,341,345,349],{"title":332,"path":333,"stem":334},{"title":338,"path":339,"stem":340},"useAuthData","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fuse-auth-data","docs\u002Fauth-h3client\u002F03.client\u002F00.use-auth-data",{"title":342,"path":343,"stem":344},"useMagicLink","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fuse-magic-link","docs\u002Fauth-h3client\u002F03.client\u002F01.use-magic-link",{"title":346,"path":347,"stem":348},"executeRequest","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fexecute-request","docs\u002Fauth-h3client\u002F03.client\u002F02.execute-request",{"title":350,"path":351,"stem":352},"getCsrfToken","\u002Fdocs\u002Fauth-h3client\u002Fclient\u002Fget-csrf-token","docs\u002Fauth-h3client\u002F03.client\u002F03.get-csrf-token",{"title":38,"path":354,"stem":355},"\u002Fdocs\u002Fauth-h3client\u002Fsecurity","docs\u002Fauth-h3client\u002F04.security",{"title":220,"path":357,"stem":358,"children":359,"page":53},"\u002Fdocs\u002Fauth-h3client\u002Fguides","docs\u002Fauth-h3client\u002F05.guides",[360,364,368,372],{"title":361,"path":362,"stem":363},"H3 and Nitro Setup","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fh3-nitro","docs\u002Fauth-h3client\u002F05.guides\u002F00.h3-nitro",{"title":365,"path":366,"stem":367},"HMAC Inter-service Auth","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fhmac","docs\u002Fauth-h3client\u002F05.guides\u002Fhmac",{"title":369,"path":370,"stem":371},"Image Upload","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fimage-upload","docs\u002Fauth-h3client\u002F05.guides\u002Fimage-upload",{"title":373,"path":374,"stem":375},"mTLS Configuration","\u002Fdocs\u002Fauth-h3client\u002Fguides\u002Fmtls","docs\u002Fauth-h3client\u002F05.guides\u002Fmtls",{"title":237,"path":377,"stem":378},"\u002Fdocs\u002Fauth-h3client\u002Fconfiguration","docs\u002Fauth-h3client\u002F06.configuration",{"title":246,"path":380,"stem":381,"children":382},"\u002Fdocs\u002Fauth-h3client\u002Fapi","docs\u002Fauth-h3client\u002F07.api\u002Findex",[383,384,387,390,394],{"title":246,"path":380,"stem":381},{"title":254,"path":385,"stem":386},"\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fcontrollers","docs\u002Fauth-h3client\u002F07.api\u002F00.controllers",{"title":250,"path":388,"stem":389},"\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fmiddleware","docs\u002Fauth-h3client\u002F07.api\u002F01.middleware",{"title":391,"path":392,"stem":393},"Client-side Reference","\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Fcomposables","docs\u002Fauth-h3client\u002F07.api\u002F02.composables",{"title":395,"path":396,"stem":397},"Utilities","\u002Fdocs\u002Fauth-h3client\u002Fapi\u002Futilities","docs\u002Fauth-h3client\u002F07.api\u002F03.utilities",{"title":399,"path":35,"stem":400,"children":401},"Bot Detector","docs\u002Fbot-detection\u002Findex",[402,403,406,410,414,433,507,510,513],{"title":399,"path":35,"stem":400},{"title":14,"path":404,"stem":405},"\u002Fdocs\u002Fbot-detection\u002Fgetting-started","docs\u002Fbot-detection\u002F00.getting-started",{"title":407,"path":408,"stem":409},"CLI","\u002Fdocs\u002Fbot-detection\u002Fcli","docs\u002Fbot-detection\u002F01.cli",{"title":411,"path":412,"stem":413},"Data Sources","\u002Fdocs\u002Fbot-detection\u002Fdata-sources","docs\u002Fbot-detection\u002F02.data-sources",{"title":220,"path":415,"stem":416,"children":417,"page":53},"\u002Fdocs\u002Fbot-detection\u002Fguides","docs\u002Fbot-detection\u002F03.guides",[418,422,426,429],{"title":419,"path":420,"stem":421},"Custom Checkers","\u002Fdocs\u002Fbot-detection\u002Fguides\u002Fcustom","docs\u002Fbot-detection\u002F03.guides\u002FCUSTOM",{"title":423,"path":424,"stem":425},"Scheduling Database Generation","\u002Fdocs\u002Fbot-detection\u002Fguides\u002Fgenerate","docs\u002Fbot-detection\u002F03.guides\u002FGENERATE",{"title":143,"path":427,"stem":428},"\u002Fdocs\u002Fbot-detection\u002Fguides\u002Flogging","docs\u002Fbot-detection\u002F03.guides\u002FLOGGING",{"title":430,"path":431,"stem":432},"Score Modes and Reputation Healing","\u002Fdocs\u002Fbot-detection\u002Fguides\u002Fscore","docs\u002Fbot-detection\u002F03.guides\u002FSCORE",{"title":434,"path":435,"stem":436,"children":437},"Checkers","\u002Fdocs\u002Fbot-detection\u002Fcheckers","docs\u002Fbot-detection\u002F04.checkers\u002Findex",[438,439,443,447,451,455,459,463,467,471,475,479,483,487,491,495,499,503],{"title":434,"path":435,"stem":436},{"title":440,"path":441,"stem":442},"IP Validation","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fip-validation","docs\u002Fbot-detection\u002F04.checkers\u002F01.ip-validation",{"title":444,"path":445,"stem":446},"Good \u002F Bad Bot Verification","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fgood-bots","docs\u002Fbot-detection\u002F04.checkers\u002F02.good-bots",{"title":448,"path":449,"stem":450},"Browser & Device Fingerprint","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fbrowser-device","docs\u002Fbot-detection\u002F04.checkers\u002F03.browser-device",{"title":452,"path":453,"stem":454},"Locale Map","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Flocale-map","docs\u002Fbot-detection\u002F04.checkers\u002F04.locale-map",{"title":456,"path":457,"stem":458},"Known Threats","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fknown-threats","docs\u002Fbot-detection\u002F04.checkers\u002F05.known-threats",{"title":460,"path":461,"stem":462},"ASN Classification","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fasn-classification","docs\u002Fbot-detection\u002F04.checkers\u002F06.asn-classification",{"title":464,"path":465,"stem":466},"Tor Analysis","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Ftor-analysis","docs\u002Fbot-detection\u002F04.checkers\u002F07.tor-analysis",{"title":468,"path":469,"stem":470},"Timezone Consistency","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Ftimezone-consistency","docs\u002Fbot-detection\u002F04.checkers\u002F08.timezone-consistency",{"title":472,"path":473,"stem":474},"Honeypot","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fhoneypot","docs\u002Fbot-detection\u002F04.checkers\u002F09.honeypot",{"title":476,"path":477,"stem":478},"Known Bad IPs","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fknown-bad-ips","docs\u002Fbot-detection\u002F04.checkers\u002F10.known-bad-ips",{"title":480,"path":481,"stem":482},"Behavior Rate","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fbehavior-rate","docs\u002Fbot-detection\u002F04.checkers\u002F11.behavior-rate",{"title":484,"path":485,"stem":486},"Proxy \u002F ISP \u002F Cookie","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fproxy-isp-cookies","docs\u002Fbot-detection\u002F04.checkers\u002F12.proxy-isp-cookies",{"title":488,"path":489,"stem":490},"Session Coherence","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fsession-coherence","docs\u002Fbot-detection\u002F04.checkers\u002F13.session-coherence",{"title":492,"path":493,"stem":494},"Velocity Fingerprint","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fvelocity-fingerprint","docs\u002Fbot-detection\u002F04.checkers\u002F14.velocity-fingerprint",{"title":496,"path":497,"stem":498},"UA & Header Analysis","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fua-header","docs\u002Fbot-detection\u002F04.checkers\u002F15.ua-header",{"title":500,"path":501,"stem":502},"Geolocation","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fgeolocation","docs\u002Fbot-detection\u002F04.checkers\u002F16.geolocation",{"title":504,"path":505,"stem":506},"Known Bad User-Agents","\u002Fdocs\u002Fbot-detection\u002Fcheckers\u002Fknown-bad-ua","docs\u002Fbot-detection\u002F04.checkers\u002F17.known-bad-ua",{"title":38,"path":508,"stem":509},"\u002Fdocs\u002Fbot-detection\u002Fsecurity","docs\u002Fbot-detection\u002F04.security",{"title":246,"path":511,"stem":512},"\u002Fdocs\u002Fbot-detection\u002Fapi","docs\u002Fbot-detection\u002F05.api",{"title":237,"path":514,"stem":515},"\u002Fdocs\u002Fbot-detection\u002Fconfiguration","docs\u002Fbot-detection\u002F06.configuration",{"title":517,"path":11,"stem":518,"children":519},"Introduction","docs\u002Fgetting-started\u002Findex",[520],{"title":517,"path":11,"stem":518},{"title":27,"path":29,"stem":70,"children":522},[523,524,525,565,566,571,572],{"title":27,"path":29,"stem":70},{"title":14,"path":74,"stem":75},{"title":77,"path":78,"stem":79,"children":526},[527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549],{"title":77,"path":78,"stem":79},{"title":83,"path":84,"stem":85},{"title":87,"path":88,"stem":89},{"title":91,"path":92,"stem":93},{"title":95,"path":96,"stem":97},{"title":99,"path":100,"stem":101},{"title":103,"path":104,"stem":105},{"title":107,"path":108,"stem":109},{"title":111,"path":112,"stem":113},{"title":115,"path":116,"stem":117},{"title":119,"path":120,"stem":121},{"title":123,"path":124,"stem":125},{"title":127,"path":128,"stem":129},{"title":131,"path":132,"stem":133},{"title":135,"path":136,"stem":137},{"title":139,"path":140,"stem":141},{"title":143,"path":144,"stem":145},{"title":147,"path":148,"stem":149},{"title":151,"path":152,"stem":153},{"title":155,"path":156,"stem":157},{"title":159,"path":160,"stem":161},{"title":163,"path":164,"stem":165},{"title":167,"path":168,"stem":169,"children":550},[551,552,553,554,563,564],{"title":167,"path":168,"stem":169},{"title":173,"path":174,"stem":175},{"title":177,"path":178,"stem":179},{"title":181,"path":182,"stem":183,"children":555},[556,557,558,559,560,561,562],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189},{"title":191,"path":192,"stem":193},{"title":195,"path":196,"stem":197},{"title":199,"path":200,"stem":201},{"title":203,"path":204,"stem":205},{"title":207,"path":208,"stem":209},{"title":147,"path":211,"stem":212},{"title":38,"path":214,"stem":215},{"title":38,"path":217,"stem":218},{"title":220,"path":221,"stem":222,"children":567,"page":53},[568,569,570],{"title":225,"path":226,"stem":227},{"title":229,"path":230,"stem":231},{"title":233,"path":234,"stem":235},{"title":237,"path":238,"stem":239},{"title":241,"path":242,"stem":243,"children":573,"page":53},[574,575,576],{"title":246,"path":247,"stem":248},{"title":250,"path":251,"stem":252},{"title":254,"path":255,"stem":256},{"title":40,"path":42,"stem":578,"children":579},"docs\u002Fshield-base\u002Findex",[580,581,584,588,629,633,637,641],{"title":40,"path":42,"stem":578},{"title":14,"path":582,"stem":583},"\u002Fdocs\u002Fshield-base\u002Fgetting-started","docs\u002Fshield-base\u002F00.getting-started",{"title":585,"path":586,"stem":587},"CLI Reference","\u002Fdocs\u002Fshield-base\u002Fcli","docs\u002Fshield-base\u002F01.cli",{"title":411,"path":589,"stem":590,"children":591},"\u002Fdocs\u002Fshield-base\u002Fdata-sources","docs\u002Fshield-base\u002F02.data-sources\u002Findex",[592,593,597,601,605,609,613,617,621,625],{"title":411,"path":589,"stem":590},{"title":594,"path":595,"stem":596},"BGP \u002F ASN","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fbgp","docs\u002Fshield-base\u002F02.data-sources\u002Fbgp",{"title":598,"path":599,"stem":600},"City Geolocation","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fcity","docs\u002Fshield-base\u002F02.data-sources\u002Fcity",{"title":602,"path":603,"stem":604},"Country Geolocation","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fcountry","docs\u002Fshield-base\u002F02.data-sources\u002Fcountry",{"title":606,"path":607,"stem":608},"Verified Crawlers","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fcrawlers","docs\u002Fshield-base\u002F02.data-sources\u002Fcrawlers",{"title":610,"path":611,"stem":612},"Disposable Emails","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Femail","docs\u002Fshield-base\u002F02.data-sources\u002Femail",{"title":614,"path":615,"stem":616},"FireHOL Threat Intelligence","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Ffirehol","docs\u002Fshield-base\u002F02.data-sources\u002Ffirehol",{"title":618,"path":619,"stem":620},"Proxy Detection","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fproxy","docs\u002Fshield-base\u002F02.data-sources\u002Fproxy",{"title":622,"path":623,"stem":624},"Tor Nodes","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Ftor","docs\u002Fshield-base\u002F02.data-sources\u002Ftor",{"title":626,"path":627,"stem":628},"Suspicious User-Agents","\u002Fdocs\u002Fshield-base\u002Fdata-sources\u002Fuseragent","docs\u002Fshield-base\u002F02.data-sources\u002Fuseragent",{"title":630,"path":631,"stem":632},"Programmatic Usage","\u002Fdocs\u002Fshield-base\u002Fusage","docs\u002Fshield-base\u002F03.usage",{"title":634,"path":635,"stem":636},"Custom Data Sources","\u002Fdocs\u002Fshield-base\u002Fcustom-data-sources","docs\u002Fshield-base\u002F04.custom-data-sources",{"title":638,"path":639,"stem":640},"TypeScript Types","\u002Fdocs\u002Fshield-base\u002Ftypes","docs\u002Fshield-base\u002F05.types",{"title":246,"path":642,"stem":643},"\u002Fdocs\u002Fshield-base\u002Fapi","docs\u002Fshield-base\u002F06.api",{"title":395,"path":48,"stem":645,"children":646},"docs\u002Futils\u002Findex",[647,648,665,698,795],{"title":395,"path":48,"stem":645},{"title":649,"path":650,"stem":651,"children":652,"page":53},"Eslint","\u002Fdocs\u002Futils\u002Feslint","docs\u002Futils\u002Feslint",[653,657,661],{"title":654,"path":655,"stem":656},"React Config","\u002Fdocs\u002Futils\u002Feslint\u002Freact","docs\u002Futils\u002Feslint\u002Freact",{"title":658,"path":659,"stem":660},"TypeScript Config","\u002Fdocs\u002Futils\u002Feslint\u002Ftypescript","docs\u002Futils\u002Feslint\u002Ftypescript",{"title":662,"path":663,"stem":664},"Vue Config","\u002Fdocs\u002Futils\u002Feslint\u002Fvue","docs\u002Futils\u002Feslint\u002Fvue",{"title":666,"path":667,"stem":668,"children":669,"page":53},"Server","\u002Fdocs\u002Futils\u002Fserver","docs\u002Futils\u002Fserver",[670,674,678,682,686,690,694],{"title":671,"path":672,"stem":673},"Encryption","\u002Fdocs\u002Futils\u002Fserver\u002Fencryption","docs\u002Futils\u002Fserver\u002Fencryption",{"title":675,"path":676,"stem":677},"Path Resolver","\u002Fdocs\u002Futils\u002Fserver\u002Fpathresolver","docs\u002Futils\u002Fserver\u002FpathResolver",{"title":679,"path":680,"stem":681},"File Replacements","\u002Fdocs\u002Futils\u002Fserver\u002Freplace","docs\u002Futils\u002Fserver\u002Freplace",{"title":683,"path":684,"stem":685},"run","\u002Fdocs\u002Futils\u002Fserver\u002Frun","docs\u002Futils\u002Fserver\u002Frun",{"title":687,"path":688,"stem":689},"scheduleTask","\u002Fdocs\u002Futils\u002Fserver\u002Fscheduletask","docs\u002Futils\u002Fserver\u002FscheduleTask",{"title":691,"path":692,"stem":693},"spawnRun","\u002Fdocs\u002Futils\u002Fserver\u002Fspawnrun","docs\u002Futils\u002Fserver\u002FspawnRun",{"title":695,"path":696,"stem":697},"uploadCsv","\u002Fdocs\u002Futils\u002Fserver\u002Fuploadcsv","docs\u002Futils\u002Fserver\u002FuploadCsv",{"title":699,"path":700,"stem":701,"children":702,"page":53},"Shared","\u002Fdocs\u002Futils\u002Fshared","docs\u002Futils\u002Fshared",[703,707,711,715,719,723,727,731,735,739,743,747,751,755,759,763,767,771,775,779,783,787,791],{"title":704,"path":705,"stem":706},"BatchQueue","\u002Fdocs\u002Futils\u002Fshared\u002Fbatchqueue","docs\u002Futils\u002Fshared\u002FbatchQueue",{"title":708,"path":709,"stem":710},"capitalize","\u002Fdocs\u002Futils\u002Fshared\u002Fcapitalize","docs\u002Futils\u002Fshared\u002Fcapitalize",{"title":712,"path":713,"stem":714},"chunkProcess","\u002Fdocs\u002Futils\u002Fshared\u002Fchunkprocess","docs\u002Futils\u002Fshared\u002FchunkProcess",{"title":716,"path":717,"stem":718},"cleanObject","\u002Fdocs\u002Futils\u002Fshared\u002Fcleanobject","docs\u002Futils\u002Fshared\u002FcleanObject",{"title":720,"path":721,"stem":722},"createConfigManager","\u002Fdocs\u002Futils\u002Fshared\u002Fconfigurationdefiner","docs\u002Futils\u002Fshared\u002FconfigurationDefiner",{"title":724,"path":725,"stem":726},"debounce","\u002Fdocs\u002Futils\u002Fshared\u002Fdebounce","docs\u002Futils\u002Fshared\u002Fdebounce",{"title":728,"path":729,"stem":730},"ensureArray","\u002Fdocs\u002Futils\u002Fshared\u002Fensurearray","docs\u002Futils\u002Fshared\u002FensureArray",{"title":732,"path":733,"stem":734},"fetchWithRetry","\u002Fdocs\u002Futils\u002Fshared\u002Ffetchwithretry","docs\u002Futils\u002Fshared\u002FfetchWithRetry",{"title":736,"path":737,"stem":738},"filterEmptyValues","\u002Fdocs\u002Futils\u002Fshared\u002Ffilteremptyvalues","docs\u002Futils\u002Fshared\u002FfilterEmptyValues",{"title":740,"path":741,"stem":742},"findStringsInObject","\u002Fdocs\u002Futils\u002Fshared\u002Ffindobjectvalues","docs\u002Futils\u002Fshared\u002FfindObjectValues",{"title":744,"path":745,"stem":746},"fisherYatesShuffle","\u002Fdocs\u002Futils\u002Fshared\u002Ffisheryatesshuffle","docs\u002Futils\u002Fshared\u002FfisherYatesShuffle",{"title":748,"path":749,"stem":750},"getRandomImage","\u002Fdocs\u002Futils\u002Fshared\u002Fgetrandomimage","docs\u002Futils\u002Fshared\u002FgetRandomImage",{"title":752,"path":753,"stem":754},"isObjectHasValues","\u002Fdocs\u002Futils\u002Fshared\u002Fisobjecthasvalues","docs\u002Futils\u002Fshared\u002FisObjectHasValues",{"title":756,"path":757,"stem":758},"isAsyncOrPromise","\u002Fdocs\u002Futils\u002Fshared\u002Fispromise","docs\u002Futils\u002Fshared\u002FisPromise",{"title":760,"path":761,"stem":762},"MiniCache","\u002Fdocs\u002Futils\u002Fshared\u002Fminicache","docs\u002Futils\u002Fshared\u002FminiCache",{"title":764,"path":765,"stem":766},"parseCookies","\u002Fdocs\u002Futils\u002Fshared\u002Fparserawcookies","docs\u002Futils\u002Fshared\u002FparseRawCookies",{"title":768,"path":769,"stem":770},"safeAction","\u002Fdocs\u002Futils\u002Fshared\u002Fpromiselocker","docs\u002Futils\u002Fshared\u002FpromiseLocker",{"title":772,"path":773,"stem":774},"Random","\u002Fdocs\u002Futils\u002Fshared\u002Frandom","docs\u002Futils\u002Fshared\u002Frandom",{"title":776,"path":777,"stem":778},"range","\u002Fdocs\u002Futils\u002Fshared\u002Frange","docs\u002Futils\u002Fshared\u002Frange",{"title":780,"path":781,"stem":782},"rateLimiters","\u002Fdocs\u002Futils\u002Fshared\u002Fratelimiters","docs\u002Futils\u002Fshared\u002FrateLimiters",{"title":784,"path":785,"stem":786},"safeObjectMerge","\u002Fdocs\u002Futils\u002Fshared\u002Fsafemerge","docs\u002Futils\u002Fshared\u002FsafeMerge",{"title":788,"path":789,"stem":790},"textTruncation","\u002Fdocs\u002Futils\u002Fshared\u002Ftexttruncation","docs\u002Futils\u002Fshared\u002FtextTruncation",{"title":792,"path":793,"stem":794},"validateZodSchema","\u002Fdocs\u002Futils\u002Fshared\u002Fvalidatezodschema","docs\u002Futils\u002Fshared\u002FvalidateZodSchema",{"title":796,"path":797,"stem":798,"children":799},"Utility Types","\u002Fdocs\u002Futils\u002Ftypes","docs\u002Futils\u002Ftypes\u002Findex",[800,801,805,809,813,817,821,825,829,833],{"title":796,"path":797,"stem":798},{"title":802,"path":803,"stem":804},"Brand","\u002Fdocs\u002Futils\u002Ftypes\u002Fbrand","docs\u002Futils\u002Ftypes\u002FBrand",{"title":806,"path":807,"stem":808},"DeepPartial","\u002Fdocs\u002Futils\u002Ftypes\u002Fdeeppartial","docs\u002Futils\u002Ftypes\u002FDeepPartial",{"title":810,"path":811,"stem":812},"Merge","\u002Fdocs\u002Futils\u002Ftypes\u002Fmerge","docs\u002Futils\u002Ftypes\u002FMerge",{"title":814,"path":815,"stem":816},"NonNullable","\u002Fdocs\u002Futils\u002Ftypes\u002Fnonnullable","docs\u002Futils\u002Ftypes\u002FNonNullable",{"title":818,"path":819,"stem":820},"Prettify","\u002Fdocs\u002Futils\u002Ftypes\u002Fprettify","docs\u002Futils\u002Ftypes\u002FPrettify",{"title":822,"path":823,"stem":824},"PromiseType","\u002Fdocs\u002Futils\u002Ftypes\u002Fpromisetype","docs\u002Futils\u002Ftypes\u002FPromiseType",{"title":826,"path":827,"stem":828},"RequireKeys","\u002Fdocs\u002Futils\u002Ftypes\u002Frequirekeys","docs\u002Futils\u002Ftypes\u002FRequireKeys",{"title":830,"path":831,"stem":832},"StandardResponse","\u002Fdocs\u002Futils\u002Ftypes\u002Fstandardresponse","docs\u002Futils\u002Ftypes\u002FStandardResponse",{"title":834,"path":835,"stem":836},"ValueOf","\u002Fdocs\u002Futils\u002Ftypes\u002Fvalueof","docs\u002Futils\u002Ftypes\u002FValueOf",{"id":4,"extension":5,"links":838,"meta":849,"stem":62,"__hash__":63},[839,847,848],{"nested":8,"label":9,"icon":10,"to":11,"children":840},[841,842,843,844,845,846],{"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},{},{"id":851,"title":38,"body":852,"description":4664,"extension":4665,"icon":4666,"meta":4667,"module":4668,"navigation":8,"path":217,"rawbody":4669,"seo":4670,"stem":218,"__hash__":4671},"docs\u002Fdocs\u002Fiam\u002F02.security.md",{"type":853,"value":854,"toc":4597},"minimark",[855,873,876,883,886,891,909,913,997,1001,1004,1112,1116,1119,1138,1147,1153,1155,1159,1163,1190,1203,1280,1291,1305,1309,1326,1345,1351,1355,1366,1380,1385,1387,1391,1398,1402,1421,1460,1465,1469,1484,1494,1499,1503,1524,1533,1535,1539,1542,1618,1644,1677,1679,1683,1687,1698,1702,1709,1739,1742,1746,1755,1793,1796,1800,1803,2085,2090,2094,2110,2126,2133,2135,2139,2143,2156,2187,2197,2201,2206,2253,2260,2264,2279,2283,2289,2291,2295,2310,2328,2502,2513,2518,2520,2524,2527,2543,2549,2552,2650,2653,2744,2749,2751,2755,2761,2783,2810,2813,2822,2824,2828,2837,2841,2844,2885,2889,2904,2908,2995,3006,3008,3012,3020,3023,3069,3075,3081,3083,3087,3093,3096,3214,3228,3233,3235,3239,3252,3266,3269,3285,3287,3291,3294,3399,3404,3406,3410,3413,3478,3482,3484,3488,3517,3520,3568,3571,3576,3578,3582,3595,3743,3920,3925,3927,3931,3939,4053,4056,4060,4075,4077,4081,4084,4088,4091,4111,4128,4141,4145,4159,4163,4174,4179,4183,4186,4212,4215,4219,4228,4231,4237,4240,4244,4253,4262,4266,4273,4276,4335,4339,4342,4373,4375,4379,4383,4390,4394,4400,4404,4412,4416,4419,4423,4432,4434,4438,4593],[856,857,858,859,866,867,872],"p",{},"The IAM service is built on two principles: ",[860,861,865],"a",{"href":862,"rel":863},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FZero_trust_security_model",[864],"nofollow","Zero Trust"," and ",[860,868,871],{"href":869,"rel":870},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDefense_in_depth_(computing)",[864],"Defense in Depth",". Every layer of the authentication stack assumes the layers around it can fail and adds its own protection. A compromised password does not grant a session. A stolen token does not survive rotation. A replayed request does not pass anomaly detection. No single control stands alone.",[856,874,875],{},"This page is a reference for every security mechanism in the service. It covers what each control does, how the controls interact, what attack scenarios they defend against, and where the design has trade-offs or limitations.",[856,877,878,879,882],{},"If you are working with machine-to-machine credentials, see ",[860,880,881],{"href":214},"API token\nsecurity"," for the dedicated security\naround API token verification, storage, management, and abuse controls.",[884,885],"hr",{},[887,888,890],"h2",{"id":889},"network-isolation","Network isolation",[856,892,893,894,896,897,902,903,908],{},"The IAM service is designed to run behind a ",[860,895,131],{"href":132}," proxy inside a private ",[860,898,901],{"href":899,"rel":900},"https:\u002F\u002Fwww.docker.com\u002F",[864],"Docker"," network. The service is never exposed to the public internet. Browsers communicate with the BFF (a Nuxt server, an ",[860,904,907],{"href":905,"rel":906},"https:\u002F\u002Fh3.dev\u002F",[864],"h3"," server, or any HTTP proxy). The BFF communicates with the IAM service over the internal network. The database sits on the same internal network, unreachable from outside.",[907,910,912],{"id":911},"network-layers","Network layers",[914,915,916,932],"table",{},[917,918,919],"thead",{},[920,921,922,926,929],"tr",{},[923,924,925],"th",{},"Layer",[923,927,928],{},"Component",[923,930,931],{},"Exposure",[933,934,935,950,967,984],"tbody",{},[920,936,937,944,947],{},[938,939,940],"td",{},[941,942,943],"strong",{},"Edge",[938,945,946],{},"Reverse Proxy",[938,948,949],{},"Public. Handles TLS termination, request body size limits, and upstream rate limiting.",[920,951,952,957,964],{},[938,953,954],{},[941,955,956],{},"Application",[938,958,959,960,963],{},"BFF server (",[860,961,962],{"href":22},"auth-h3client",")",[938,965,966],{},"Semi-public. Receives browser requests through a proxy.",[920,968,969,974,981],{},[938,970,971],{},[941,972,973],{},"Service",[938,975,976,977,963],{},"IAM service (",[978,979,980],"code",{},"@riavzon\u002Fauth",[938,982,983],{},"Private. Accessible only from the BFF on the internal Docker network.",[920,985,986,991,994],{},[938,987,988],{},[941,989,990],{},"Data",[938,992,993],{},"MySQL",[938,995,996],{},"Private. Accessible only from the IAM service.",[907,998,1000],{"id":999},"container-hardening","Container hardening",[856,1002,1003],{},"The Docker container runs with a minimal security profile:",[914,1005,1006,1019],{},[917,1007,1008],{},[920,1009,1010,1013,1016],{},[923,1011,1012],{},"Setting",[923,1014,1015],{},"Value",[923,1017,1018],{},"Purpose",[933,1020,1021,1036,1057,1072,1091],{},[920,1022,1023,1028,1033],{},[938,1024,1025],{},[978,1026,1027],{},"read_only",[938,1029,1030],{},[978,1031,1032],{},"true",[938,1034,1035],{},"Filesystem is read-only. The process cannot write to the container image. Logs and data use mounted volumes.",[920,1037,1038,1043,1048],{},[938,1039,1040],{},[978,1041,1042],{},"cap_drop",[938,1044,1045],{},[978,1046,1047],{},"ALL",[938,1049,1050,1051,1056],{},"All ",[860,1052,1055],{"href":1053,"rel":1054},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fcapabilities.7.html",[864],"Linux capabilities"," are dropped. The process cannot bind privileged ports, modify the kernel, or escalate privileges.",[920,1058,1059,1064,1069],{},[938,1060,1061],{},[978,1062,1063],{},"user",[938,1065,1066],{},[978,1067,1068],{},"10001:10001",[938,1070,1071],{},"Runs as a non-root user and group.",[920,1073,1074,1079,1084],{},[938,1075,1076],{},[978,1077,1078],{},"security_opt",[938,1080,1081],{},[978,1082,1083],{},"no-new-privileges:true",[938,1085,1086,1087,1090],{},"Prevents the process from gaining new privileges through ",[978,1088,1089],{},"setuid"," binaries or other mechanisms.",[920,1092,1093,1098,1103],{},[938,1094,1095],{},[978,1096,1097],{},"pids_limit",[938,1099,1100],{},[978,1101,1102],{},"200",[938,1104,1105,1106,1111],{},"Limits the number of processes the container can spawn. Prevents ",[860,1107,1110],{"href":1108,"rel":1109},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FFork_bomb",[864],"fork bomb"," attacks.",[907,1113,1115],{"id":1114},"service-to-service-authentication","Service-to-service authentication",[856,1117,1118],{},"Trust between the BFF and the IAM service is not implicit, even on a private network. The service supports two independent verification layers:",[856,1120,1121,1124,1125,1130,1131,1134,1135,1137],{},[941,1122,1123],{},"HMAC signatures."," Every request from the BFF carries four headers: a client identifier, a millisecond timestamp, a unique request ID, and an ",[860,1126,1129],{"href":1127,"rel":1128},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHMAC",[864],"HMAC-SHA256"," signature computed from a shared secret. The IAM service recomputes the signature and compares it using ",[978,1132,1133],{},"crypto.timingSafeEqual",". This prevents unauthorized containers on the same network from calling IAM endpoints. See ",[860,1136,135],{"href":136}," for the full verification flow, replay detection, and clock-skew tolerance.",[856,1139,1140,1143,1144,1146],{},[941,1141,1142],{},"Mutual TLS (mTLS)."," The ",[860,1145,962],{"href":22}," supports configuration for a custom HTTPS agent. When configured, both the BFF and the IAM service present certificates, and each side verifies the other.",[1148,1149,1150],"tip",{},[856,1151,1152],{},"HMAC and mTLS are independent. You can enable either, both, or neither. HMAC alone is sufficient for most deployments where the Docker network is the trust boundary. Add mTLS when compliance requirements mandate encrypted internal traffic.",[884,1154],{},[887,1156,1158],{"id":1157},"credential-storage","Credential storage",[907,1160,1162],{"id":1161},"password-hashing","Password hashing",[856,1164,1165,1166,1171,1172,1177,1178,1183,1184,1189],{},"All passwords are hashed with ",[860,1167,1170],{"href":1168,"rel":1169},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FArgon2",[864],"Argon2id"," using the ",[860,1173,1176],{"href":1174,"rel":1175},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fargon2",[864],"argon2"," library. Argon2id is the ",[860,1179,1182],{"href":1180,"rel":1181},"https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FPassword_Storage_Cheat_Sheet.html",[864],"OWASP-recommended"," variant for password storage. It combines resistance to GPU-based parallel attacks (from Argon2d) with resistance to ",[860,1185,1188],{"href":1186,"rel":1187},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSide-channel_attack",[864],"side-channel attacks"," (from Argon2i).",[856,1191,1192,1193,1196,1197,1202],{},"A ",[941,1194,1195],{},"pepper"," is mixed into every hash as a secret key. The pepper is a long random hex string stored outside the database (in a secrets manager, environment variable, or ",[860,1198,1201],{"href":1199,"rel":1200},"https:\u002F\u002Fage-encryption.org\u002F",[864],"age","-encrypted configuration file). If the database is leaked, an attacker cannot crack the hashes without also obtaining the pepper. Rotating the pepper invalidates all existing hashes, forcing a password reset for every user.",[914,1204,1205,1217],{},[917,1206,1207],{},[920,1208,1209,1212,1215],{},[923,1210,1211],{},"Parameter",[923,1213,1214],{},"Default",[923,1216,1018],{},[933,1218,1219,1235,1250,1265],{},[920,1220,1221,1225,1228],{},[938,1222,1223],{},[978,1224,1195],{},[938,1226,1227],{},"Required",[938,1229,1230,1231,1234],{},"Secret key mixed into every hash via Argon2's ",[978,1232,1233],{},"secret"," parameter",[920,1236,1237,1242,1247],{},[938,1238,1239],{},[978,1240,1241],{},"hashLength",[938,1243,1244],{},[978,1245,1246],{},"50",[938,1248,1249],{},"Output hash length in bytes",[920,1251,1252,1257,1262],{},[938,1253,1254],{},[978,1255,1256],{},"timeCost",[938,1258,1259],{},[978,1260,1261],{},"4",[938,1263,1264],{},"Number of Argon2 iterations. Higher values increase brute-force resistance.",[920,1266,1267,1272,1277],{},[938,1268,1269],{},[978,1270,1271],{},"memoryCost",[938,1273,1274],{},[978,1275,1276],{},"262144",[938,1278,1279],{},"Memory usage in KiB (256 MiB). Higher values make GPU attacks more expensive.",[856,1281,1282,1283,1286,1287,1290],{},"The verification function (",[978,1284,1285],{},"verifyPassword",") catches all errors internally and returns ",[978,1288,1289],{},"false"," on any failure. No error details are leaked to the caller or the client.",[1292,1293,1294],"warning",{},[856,1295,1296,1297,866,1299,1301,1302,1304],{},"Increasing ",[978,1298,1256],{},[978,1300,1271],{}," makes hashing more expensive for attackers but also slows down your login and signup endpoints. Benchmark these values against your target response time before deploying. See ",[860,1303,237],{"href":238}," for all password hashing options.",[907,1306,1308],{"id":1307},"refresh-token-hashing","Refresh token hashing",[856,1310,1311,1312,1315,1316,1321,1322,1325],{},"Refresh tokens are 64-byte random strings generated with ",[978,1313,1314],{},"crypto.randomBytes(64)"," and hex-encoded to 128 characters. Before any database operation, the raw token is hashed with ",[860,1317,1320],{"href":1318,"rel":1319},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSHA-2",[864],"SHA-256",". The database stores only the hash. The raw token is sent to the client exactly once, inside an ",[978,1323,1324],{},"httpOnly"," cookie, and never stored in plaintext on the server.",[856,1327,1328,1329,1332,1333,1336,1337,1340,1341,1344],{},"The ",[978,1330,1331],{},"toDigestHex"," utility checks whether the input is already a valid SHA-256 hex string before hashing. If the input passes a Zod ",[978,1334,1335],{},"z.hash(\"sha256\")"," check, it passes through unchanged. If not, SHA-256 is applied. After hashing, ",[978,1338,1339],{},"ensureSha256Hex"," runs a strict regex validation (",[978,1342,1343],{},"\u002F^[a-f0-9]{64}$\u002Fi",") and throws if the result is malformed. This double gate prevents truncated or corrupted hashes from reaching the database.",[856,1346,1347,1348,1350],{},"See ",[860,1349,91],{"href":92}," for the full token lifecycle, storage schema, and rotation mechanics.",[907,1352,1354],{"id":1353},"mfa-code-hashing","MFA code hashing",[856,1356,1357,1358,1361,1362,1365],{},"One-time MFA codes are 7-digit numeric strings generated with ",[978,1359,1360],{},"crypto.randomInt(1_000_000, 9_999_999)",". The raw code is sent to the user's email. A SHA-256 hash of the code is stored in the ",[978,1363,1364],{},"mfa_codes"," table with a 7-minute expiry. The raw code never touches the database.",[856,1367,1368,1369,1372,1373,1375,1376,1379],{},"When the user submits the code, the service hashes the submission and compares the hash against the stored value inside a database transaction. Expired or used codes are deleted atomically. The ",[978,1370,1371],{},"token"," foreign key on ",[978,1374,1364],{}," cascades to ",[978,1377,1378],{},"refresh_tokens",", so revoking a user's refresh token automatically invalidates any pending MFA code tied to that session.",[856,1381,1347,1382,1384],{},[860,1383,123],{"href":124}," for the full OTP generation, delivery, verification, and rate limiting flow.",[884,1386],{},[887,1388,1390],{"id":1389},"token-lifecycle","Token lifecycle",[856,1392,1393,1394,1397],{},"The service uses a two-token architecture with a ",[978,1395,1396],{},"canary_id"," cookie for session binding.",[907,1399,1401],{"id":1400},"access-tokens","Access tokens",[856,1403,1404,1405,1410,1411,1416,1417,1420],{},"Access tokens are short-lived ",[860,1406,1409],{"href":1407,"rel":1408},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON_Web_Token",[864],"JWTs"," signed with a configurable algorithm (default HMAC). The service caches every generated token in an ",[860,1412,1415],{"href":1413,"rel":1414},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCache_replacement_policies#LRU",[864],"LRU"," cache at creation time. During verification, the token must pass both cryptographic signature validation ",[941,1418,1419],{},"and"," exist in the cache. This design means you can revoke any valid access token instantly by deleting its cache entry, without waiting for natural expiry.",[856,1422,1423,1424,1427,1428,1431,1432,1435,1436,1439,1440,1443,1444,1447,1448,1451,1452,1455,1456,1459],{},"The payload includes ",[978,1425,1426],{},"visitor"," (the device binding), ",[978,1429,1430],{},"roles",", ",[978,1433,1434],{},"sub"," (user ID), ",[978,1437,1438],{},"jti"," (unique token ID generated with ",[978,1441,1442],{},"crypto.randomUUID()","), ",[978,1445,1446],{},"iat",", and ",[978,1449,1450],{},"exp",". Additional custom claims can be merged via ",[978,1453,1454],{},"jwt.access_tokens.payload"," in the ",[860,1457,1458],{"href":238},"configuration",".",[856,1461,1347,1462,1464],{},[860,1463,87],{"href":88}," for cache management and verification semantics.",[907,1466,1468],{"id":1467},"refresh-tokens","Refresh tokens",[856,1470,1471,1472,1475,1476,1479,1480,1483],{},"Refresh tokens are opaque random strings stored hashed in MySQL. Each token row carries a ",[978,1473,1474],{},"usage_count"," column that is the core of the single-use enforcement system. A fresh token starts at ",[978,1477,1478],{},"0",", gets atomically incremented to ",[978,1481,1482],{},"1"," on consumption, and any second use triggers a kill switch that revokes every active session for that user.",[856,1485,1486,1487,1490,1491,1493],{},"The atomic consumption happens in a single ",[978,1488,1489],{},"UPDATE"," inside a database transaction: it increments ",[978,1492,1474],{}," by one, but only if the token exists, is valid, has never been consumed, and has not expired. All four conditions must pass for the row to be affected. If no row is affected, the function investigates why.",[856,1495,1347,1496,1498],{},[860,1497,91],{"href":92}," for the full schema, consumption flow, rotation helpers, and the reuse detection walkthrough.",[907,1500,1502],{"id":1501},"canary-cookie","Canary cookie",[856,1504,1328,1505,1507,1508,1510,1511,1431,1513,1431,1516,1519,1520,1523],{},[978,1506,1396],{}," cookie is a 64-character hex string generated by the ",[860,1509,399],{"href":404}," middleware on first contact. It is set as an ",[978,1512,1324],{},[978,1514,1515],{},"Secure",[978,1517,1518],{},"SameSite=Lax"," cookie with a 90-day TTL. The value is used as the primary key in the ",[978,1521,1522],{},"visitors"," table, which stores geo, device, and browser fingerprint data for the device.",[856,1525,1526,1527,1529,1530,1532],{},"Every refresh token operation compares the incoming ",[978,1528,1396],{}," against the stored value. A mismatch means the request is arriving from a different browser or device than the one that established the session, and adaptive MFA is triggered. See ",[860,1531,95],{"href":96}," check 2 for the exact comparison logic.",[884,1534],{},[887,1536,1538],{"id":1537},"cookie-security","Cookie security",[856,1540,1541],{},"All cookies set by the service use the strictest available flags:",[914,1543,1544,1556],{},[917,1545,1546],{},[920,1547,1548,1551,1553],{},[923,1549,1550],{},"Cookie",[923,1552,1018],{},[923,1554,1555],{},"Flags",[933,1557,1558,1581,1600],{},[920,1559,1560,1565,1568],{},[938,1561,1562],{},[978,1563,1564],{},"session",[938,1566,1567],{},"Raw refresh token",[938,1569,1570,1431,1572,1431,1574,1431,1577,1580],{},[978,1571,1324],{},[978,1573,1515],{},[978,1575,1576],{},"SameSite=Strict",[978,1578,1579],{},"Path=\u002F",", domain from config",[920,1582,1583,1587,1590],{},[938,1584,1585],{},[978,1586,1446],{},[938,1588,1589],{},"Token issued-at timestamp",[938,1591,1592,1431,1594,1431,1596,1431,1598],{},[978,1593,1324],{},[978,1595,1515],{},[978,1597,1576],{},[978,1599,1579],{},[920,1601,1602,1606,1609],{},[938,1603,1604],{},[978,1605,1396],{},[938,1607,1608],{},"Device fingerprint binding",[938,1610,1611,1431,1613,1431,1615,1617],{},[978,1612,1324],{},[978,1614,1515],{},[978,1616,1518],{},", 90-day TTL",[856,1619,1328,1620,1622,1623,1628,1629,1631,1632,1634,1635,1640,1641,1459],{},[978,1621,1324],{}," flag prevents JavaScript from reading the cookie, eliminating ",[860,1624,1627],{"href":1625,"rel":1626},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCross-site_scripting",[864],"XSS","-based token theft. The ",[978,1630,1576],{}," policy on the ",[978,1633,1564],{}," cookie prevents ",[860,1636,1639],{"href":1637,"rel":1638},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCross-site_request_forgery",[864],"CSRF"," attacks, the browser will not include the cookie on any cross-origin request, whether initiated by a form submission, a redirect, or an ",[978,1642,1643],{},"XMLHttpRequest",[856,1645,1328,1646,1649,1650,866,1653,1656,1657,1662,1663,1665,1666,1431,1669,1671,1672,1674,1675,1459],{},[978,1647,1648],{},"makeCookie"," utility also supports the ",[978,1651,1652],{},"__Host-",[978,1654,1655],{},"__Secure-"," ",[860,1658,1661],{"href":1659,"rel":1660},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FReference\u002FHeaders\u002FSet-Cookie#cookie_prefixes",[864],"cookie prefixes",". When a cookie name starts with ",[978,1664,1652],{},", the utility forces ",[978,1667,1668],{},"Secure=true",[978,1670,1579],{},", and removes the domain attribute. When the name starts with ",[978,1673,1655],{},", it forces ",[978,1676,1668],{},[884,1678],{},[887,1680,1682],{"id":1681},"input-validation-and-sanitization","Input validation and sanitization",[907,1684,1686],{"id":1685},"content-type-enforcement","Content-type enforcement",[856,1688,1689,1690,1693,1694,1697],{},"Every POST route requires ",[978,1691,1692],{},"Content-Type: application\u002Fjson",". The ",[978,1695,1696],{},"validateContentType"," middleware rejects requests with any other content type with HTTP 403. This prevents multipart, URL-encoded, or XML payloads from bypassing JSON-level input validation.",[907,1699,1701],{"id":1700},"request-body-limits","Request body limits",[856,1703,1704,1705,1708],{},"All routes set an explicit body size limit via ",[978,1706,1707],{},"express.json()",":",[914,1710,1711,1721],{},[917,1712,1713],{},[920,1714,1715,1718],{},[923,1716,1717],{},"Route group",[923,1719,1720],{},"Limit",[933,1722,1723,1731],{},[920,1724,1725,1728],{},[938,1726,1727],{},"Signup, login, MFA, password reset",[938,1729,1730],{},"1 KB",[920,1732,1733,1736],{},[938,1734,1735],{},"OAuth profile",[938,1737,1738],{},"4 KB",[856,1740,1741],{},"Requests with an empty body are rejected with HTTP 403 before JSON parsing begins.",[907,1743,1745],{"id":1744},"zod-schema-validation","Zod schema validation",[856,1747,1748,1749,1754],{},"Every user-supplied field passes through a ",[860,1750,1753],{"href":1751,"rel":1752},"https:\u002F\u002Fzod.dev\u002F",[864],"Zod"," schema before any business logic runs. The schemas enforce type, length, and pattern constraints:",[914,1756,1757,1767],{},[917,1758,1759],{},[920,1760,1761,1764],{},[923,1762,1763],{},"Field",[923,1765,1766],{},"Constraints",[933,1768,1769,1777,1785],{},[920,1770,1771,1774],{},[938,1772,1773],{},"Email",[938,1775,1776],{},"10 to 80 characters, strict regex pattern (no double dots, valid DNS segments), domain MX validation",[920,1778,1779,1782],{},[938,1780,1781],{},"Password",[938,1783,1784],{},"12 to 64 characters, must contain at least one lowercase, one uppercase, one digit, and one special character",[920,1786,1787,1790],{},[938,1788,1789],{},"Name",[938,1791,1792],{},"2 to 72 characters, letters only, 1 to 4 space-separated names",[856,1794,1795],{},"When validation fails, the service returns HTTP 400 with field-level error messages. The error response never reveals which specific condition failed in a way that would help an attacker enumerate valid inputs.",[907,1797,1799],{"id":1798},"xss-sanitization-pipeline","XSS sanitization pipeline",[856,1801,1802],{},"All user-supplied strings pass through a stage of sanitization pipeline before they reach the database. The pipeline is designed to catch layered encoding attacks, where an attacker nests multiple layers of URI encoding, HTML entities, or Unicode substitutions to bypass single-pass filters.",[1804,1805,1806,1811,1818,1822,1846,1850,1872,1876,1904,1910,1914,1921,1925,1928,1978,1991,1995,2002,2042,2059,2063],"steps",{"level":1261},[1807,1808,1810],"h4",{"id":1809},"length-guard","Length guard",[856,1812,1813,1814,1817],{},"Before any processing, the sanitizer rejects input longer than ",[978,1815,1816],{},"htmlSanitizer.maxAllowedInputLength"," (default 50000). Oversized input throws rather than entering the loop. This is the hard cap on CPU cost for a single call.",[1807,1819,1821],{"id":1820},"unicode-normalization","Unicode normalization",[856,1823,1824,1825,1830,1831,1834,1835,1838,1839,1842,1843,1459],{},"The input is normalized to ",[860,1826,1829],{"href":1827,"rel":1828},"https:\u002F\u002Funicode.org\u002Freports\u002Ftr15\u002F",[864],"NFKC",", which collapses visually similar characters to their canonical form Zero-width characters, soft hyphens, byte-order marks, and bidirectional override characters are stripped in the same pass. Halfwidth and\nfullwidth ASCII characters (",[978,1832,1833],{},"U+FF01"," through ",[978,1836,1837],{},"U+FF5E",") are transliterated back to standard ASCII. This defeats payloads that hide tags inside fullwidth substitutions such as ",[978,1840,1841],{},"\\uFF1C"," for ",[978,1844,1845],{},"\u003C",[1807,1847,1849],{"id":1848},"strict-uri-decode","Strict URI decode",[856,1851,1852,1855,1856,1859,1860,1863,1864,1867,1868,1871],{},[978,1853,1854],{},"decodeURIComponent"," is called once inside a ",[978,1857,1858],{},"try","\u002F",[978,1861,1862],{},"catch",". If the call throws (malformed percent-encoding like ",[978,1865,1866],{},"%ZZ","), the input is rejected immediately and returned as an empty string with ",[978,1869,1870],{},"htmlFound: true",". Legitimate input does not contain malformed URI sequences, so rejecting early keeps broken data out of the loop.",[1807,1873,1875],{"id":1874},"iterative-decoding-loop","Iterative decoding loop",[856,1877,1878,1879,1881,1882,1887,1888,1891,1892,1895,1896,1899,1900,1903],{},"The function alternates between ",[978,1880,1854],{}," and HTML entity decoding (via the ",[860,1883,1886],{"href":1884,"rel":1885},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fhe",[864],"he"," library) in a loop. Each pass strips one layer of encoding. The loop runs until the output stabilizes (no change between iterations) or the ",[978,1889,1890],{},"IrritationCount"," limit (default 50) is reached. This catches payloads like ",[978,1893,1894],{},"%253Cscript%253E",", which decodes to ",[978,1897,1898],{},"%3Cscript%3E"," on the first pass and ",[978,1901,1902],{},"\u003Cscript>"," on the second.",[856,1905,1906,1907,1909],{},"If the loop exceeds ",[978,1908,1890],{}," without stabilizing, the input is rejected entirely and returned as an empty string with the HTML detection flag set.",[1807,1911,1913],{"id":1912},"residual-cleanup","Residual cleanup",[856,1915,1916,1917,1920],{},"After the loop, zero-width characters are stripped again (the decoders may have reintroduced them) and any whitespace inside the bodies of surviving tag-like substrings is removed so that ",[978,1918,1919],{},"\u003Cscr\\tipt>"," cannot slip past the tag regex.",[1807,1922,1924],{"id":1923},"pattern-detection","Pattern detection",[856,1926,1927],{},"The decoded string is tested against three patterns:",[914,1929,1930,1940],{},[917,1931,1932],{},[920,1933,1934,1937],{},[923,1935,1936],{},"Pattern",[923,1938,1939],{},"Catches",[933,1941,1942,1952,1968],{},[920,1943,1944,1949],{},[938,1945,1946],{},[978,1947,1948],{},"\u002F\u003C\\s*\\\u002F?\\s*[A-Za-z][A-Za-z0-9-]*(?:\\s+[^>]*?)?\\s*>\u002Fi",[938,1950,1951],{},"Any HTML tag",[920,1953,1954,1959],{},[938,1955,1956],{},[978,1957,1958],{},"\u002Fon\\w+\\s*=\u002Fi",[938,1960,1961,1962,1431,1965,963],{},"Inline event handlers (",[978,1963,1964],{},"onclick=",[978,1966,1967],{},"onerror=",[920,1969,1970,1975],{},[938,1971,1972],{},[978,1973,1974],{},"\u002Fjavascript\\s*:\u002Fi",[938,1976,1977],{},"JavaScript protocol URIs",[856,1979,1980,1981,1983,1984,1986,1987,1990],{},"Any match sets ",[978,1982,1870],{},". That flag propagates to ",[978,1985,792],{},", which calls ",[978,1988,1989],{},"handleXSS"," and bans the IP.",[1807,1992,1994],{"id":1993},"sanitize-html-pass","sanitize-html pass",[856,1996,1997,1998,2001],{},"The string passes through ",[978,1999,2000],{},"sanitize-html"," with a strict configuration:",[2003,2004,2005,2012,2017,2022,2027,2032,2037],"ul",{},[2006,2007,2008,2011],"li",{},[978,2009,2010],{},"allowedTags: []",",",[2006,2013,2014,2011],{},[978,2015,2016],{},"allowedAttributes: {}",[2006,2018,2019,2011],{},[978,2020,2021],{},"allowedIframeHostnames: []",[2006,2023,2024,2011],{},[978,2025,2026],{},"allowedSchemes: []",[2006,2028,2029,2011],{},[978,2030,2031],{},"allowProtocolRelative: false",[2006,2033,2034,2011],{},[978,2035,2036],{},"nestingLimit: 10",[2006,2038,2039,1459],{},[978,2040,2041],{},"nonTextTags: ['script', 'style', 'noscript', 'iframe', 'svg']",[856,2043,1328,2044,2047,2048,2051,2052,2055,2056,2058],{},[978,2045,2046],{},"textFilter"," re-runs the tag regex on text nodes, and ",[978,2049,2050],{},"onOpenTag"," records any tag name and attribute set that the sanitizer had to strip. If the string shrinks during this pass, ",[978,2053,2054],{},"htmlFound"," is set to ",[978,2057,1032],{}," even if pattern detection did not trigger.",[1807,2060,2062],{"id":2061},"entity-encoding","Entity encoding",[856,2064,2065,2066,1431,2069,1431,2071,1431,2074,1431,2077,2080,2081,2084],{},"The final output is entity-encoded: ",[978,2067,2068],{},"&",[978,2070,1845],{},[978,2072,2073],{},">",[978,2075,2076],{},"\"",[978,2078,2079],{},"'",", backtick, and ",[978,2082,2083],{},"${"," are replaced with their entity or escaped equivalents. The backtick and template-literal escapes prevent injection into JavaScript template strings. The result is trimmed.",[856,2086,1347,2087,2089],{},[860,2088,139],{"href":140}," for the full pipeline details, Zod integration, and the automatic IP ban behavior.",[907,2091,2093],{"id":2092},"automatic-ip-banning-on-xss-detection","Automatic IP banning on XSS detection",[856,2095,2096,2097,2100,2101,2103,2104,2106,2107,2109],{},"When the Zod validation layer detects an ",[978,2098,2099],{},"'HTML found'"," error (emitted by the sanitization pipeline), the ",[978,2102,792],{}," function calls ",[978,2105,1989],{},". This function imports the ",[860,2108,399],{"href":35}," ban utilities and immediately:",[2111,2112,2113,2120,2123],"ol",{},[2006,2114,2115,2116,2119],{},"Bans the IP with the maximum score (equal to ",[978,2117,2118],{},"banScore",", default 100)",[2006,2121,2122],{},"Updates the banned IP record in the bot detector database",[2006,2124,2125],{},"Marks the visitor as a bot",[856,2127,2128,2129,2132],{},"The response returns HTTP 403 with ",[978,2130,2131],{},"{ banned: true }",". The attacker's IP is blocked from all subsequent requests.",[884,2134],{},[887,2136,2138],{"id":2137},"session-management","Session management",[907,2140,2142],{"id":2141},"token-rotation-on-every-use","Token rotation on every use",[856,2144,2145,2146,2151,2152,2155],{},"The service enforces ",[860,2147,2150],{"href":2148,"rel":2149},"https:\u002F\u002Fauth0.com\u002Fdocs\u002Fsecure\u002Ftokens\u002Frefresh-tokens\u002Frefresh-token-rotation",[864],"refresh token rotation"," on every access. When the BFF calls ",[978,2153,2154],{},"POST \u002Fauth\u002Fuser\u002Frefresh-session",", the controller:",[2111,2157,2158,2165,2168,2171,2174,2179],{},[2006,2159,2160,2161,2164],{},"Runs the full ",[860,2162,2163],{"href":96},"anomaly detection"," pipeline against the incoming request",[2006,2166,2167],{},"Consumes the current refresh token atomically",[2006,2169,2170],{},"Revokes the old token",[2006,2172,2173],{},"Generates a fresh refresh token with a new row",[2006,2175,2176,2177],{},"Generates a fresh access token with a new ",[978,2178,1438],{},[2006,2180,2181,2182,866,2184,2186],{},"Sets new ",[978,2183,1564],{},[978,2185,1446],{}," cookies on the response",[856,2188,2189,2190,2193,2194,1459],{},"The old token is dead after step 2. The new token is the only valid credential for future requests. If the old token is presented a second time , the consumption function detects ",[978,2191,2192],{},"usage_count > 0",", revokes all tokens for that user, and returns ",[978,2195,2196],{},"{ valid: false, reason: 'Token already used' }",[907,2198,2200],{"id":2199},"reuse-detection-and-the-kill-switch","Reuse detection and the kill switch",[856,2202,1328,2203,2205],{},[978,2204,1474],{}," column is the backbone of the reuse detection system. Here is how it handles the two possible stolen-token scenarios:",[1804,2207,2208,2212,2227,2231,2241],{"level":1261},[1807,2209,2211],{"id":2210},"legitimate-user-rotates-first","Legitimate user rotates first",[856,2213,2214,2215,2217,2218,2220,2221,2223,2224,2226],{},"The user's BFF rotates the token. ",[978,2216,1474],{}," goes from ",[978,2219,1478],{}," to ",[978,2222,1482],{},". The attacker presents the now-consumed token. The consumption function finds ",[978,2225,2192],{},", revokes all tokens for that user, and returns an error. The attacker's session is dead. The user is also logged out (they must re-authenticate), but the attacker gained nothing.",[1807,2228,2230],{"id":2229},"attacker-rotates-first","Attacker rotates first",[856,2232,2233,2234,2217,2236,2220,2238,2240],{},"The attacker uses the stolen token before the user does. ",[978,2235,1474],{},[978,2237,1478],{},[978,2239,1482],{},". The attacker receives new tokens. When the user's BFF later presents the now-consumed token, the same reuse detection fires: all tokens for that user are revoked, including the ones the attacker just received. The attacker's session is immediately invalidated.",[856,2242,2243,2244,2246,2247,2249,2250,2252],{},"In parallel, the ",[860,2245,2163],{"href":96}," pipeline notices the mismatch in ",[978,2248,1396],{},", IP address, or device fingerprint on the attacker's request and triggers adaptive ",[860,2251,123],{"href":124},". The attacker must complete a 7-digit OTP challenge sent to the user's email to continue. Without access to the user's inbox, the attacker cannot use the rotated token.",[856,2254,1347,2255,2259],{},[860,2256,2258],{"href":2257},"\u002Fdocs\u002Fiam\u002Fessentials\u002Frefresh-tokens#reuse-detection","Refresh Tokens: Reuse Detection"," for the database-level walkthrough of this flow.",[907,2261,2263],{"id":2262},"session-lifetime-enforcement","Session lifetime enforcement",[856,2265,2266,2267,2270,2271,2274,2275,2278],{},"Even with continuous rotation, sessions have a hard maximum lifetime. The ",[978,2268,2269],{},"MAX_SESSION_LIFE"," configuration defaults to 30 days and is checked during every rotation. The service compares ",[978,2272,2273],{},"Date.now()"," against ",[978,2276,2277],{},"session_started_at",", which is set once at token creation and carried forward to each new token on rotation. If the session has exceeded the maximum lifetime, the token is revoked, cookies are cleared, and the user must log in again.",[907,2280,2282],{"id":2281},"concurrent-session-limits","Concurrent session limits",[856,2284,1328,2285,2288],{},[978,2286,2287],{},"maxAllowedSessionsPerUser"," configuration (default 5) limits how many active refresh tokens a user can have simultaneously. When the anomaly detection pipeline counts valid tokens for a user and the count equals or exceeds this limit, adaptive MFA is triggered. This prevents an attacker from silently opening many sessions under a single account.",[884,2290],{},[887,2292,2294],{"id":2293},"anomaly-detection","Anomaly detection",[856,2296,2297,2298,2301,2302,1431,2304,1447,2307,2309],{},"Every time a refresh token is used, the ",[978,2299,2300],{},"strangeThings()"," function runs nine sequential checks against the request context. The checks query the ",[978,2303,1378],{},[978,2305,2306],{},"users",[978,2308,1522],{}," tables in a single join, then compare the stored fingerprint against the incoming request. The first check that fails short-circuits the rest.",[856,2311,2312,2313,2316,2317,2319,2320,2322,2323,2319,2325,2327],{},"Each failure carries a ",[978,2314,2315],{},"reqMFA"," flag. When ",[978,2318,2315],{}," is ",[978,2321,1032],{},", the caller sends an OTP email challenge and the user can recover by proving their identity. When ",[978,2324,2315],{},[978,2326,1289],{},", the request is a hard block: the token is revoked and the user must log in again from scratch.",[914,2329,2330,2349],{},[917,2331,2332],{},[920,2333,2334,2337,2340,2343,2346],{},[923,2335,2336],{},"#",[923,2338,2339],{},"Check",[923,2341,2342],{},"Trigger condition",[923,2344,2345],{},"Response",[923,2347,2348],{},"Rationale",[933,2350,2351,2367,2384,2403,2420,2436,2452,2470,2486],{},[920,2352,2353,2355,2358,2361,2364],{},[938,2354,1482],{},[938,2356,2357],{},"Token validity",[938,2359,2360],{},"Token missing, revoked, or replayed (during rotation)",[938,2362,2363],{},"Hard block",[938,2365,2366],{},"No identity to challenge. Session is dead.",[920,2368,2369,2372,2374,2379,2381],{},[938,2370,2371],{},"2",[938,2373,1502],{},[938,2375,2376,2378],{},[978,2377,1396],{}," does not match stored value",[938,2380,123],{},[938,2382,2383],{},"New device. User can prove identity.",[920,2385,2386,2389,2392,2398,2400],{},[938,2387,2388],{},"3",[938,2390,2391],{},"Idle detection",[938,2393,2394,2397],{},[978,2395,2396],{},"last_seen"," older than 24 hours",[938,2399,123],{},[938,2401,2402],{},"Stale session. Re-verify before continuing.",[920,2404,2405,2407,2410,2415,2417],{},[938,2406,1261],{},[938,2408,2409],{},"Session count",[938,2411,2412,2413],{},"Active sessions >= ",[978,2414,2287],{},[938,2416,123],{},[938,2418,2419],{},"Too many devices. User picks which to keep.",[920,2421,2422,2425,2428,2431,2433],{},[938,2423,2424],{},"5",[938,2426,2427],{},"Rapid creation",[938,2429,2430],{},"More than 3 valid tokens in 10 minutes",[938,2432,2363],{},[938,2434,2435],{},"Automated behavior. Non-human actor.",[920,2437,2438,2441,2444,2447,2449],{},[938,2439,2440],{},"6",[938,2442,2443],{},"IP range",[938,2445,2446],{},"IP outside the stored range",[938,2448,123],{},[938,2450,2451],{},"Network change. User can confirm.",[920,2453,2454,2457,2460,2465,2467],{},[938,2455,2456],{},"7",[938,2458,2459],{},"Suspicion score",[938,2461,2462,2463],{},"Bot detector score >= 25% of ",[978,2464,2118],{},[938,2466,123],{},[938,2468,2469],{},"Bot-like behavior accumulating. Early challenge.",[920,2471,2472,2475,2478,2481,2483],{},[938,2473,2474],{},"8",[938,2476,2477],{},"Proxy\u002Fhosting",[938,2479,2480],{},"Request from proxy or hosting without allow flag",[938,2482,123],{},[938,2484,2485],{},"New infrastructure. One-time MFA gate.",[920,2487,2488,2491,2494,2497,2499],{},[938,2489,2490],{},"9",[938,2492,2493],{},"Fingerprint loop",[938,2495,2496],{},"Any geo or User-Agent field mismatch",[938,2498,123],{},[938,2500,2501],{},"Environment changed. User can re-verify.",[856,2503,2504,2505,2508,2509,2512],{},"After a successful MFA challenge, the user's ",[978,2506,2507],{},"last_mfa_at"," timestamp is updated. For the duration of ",[978,2510,2511],{},"byPassAnomaliesFor"," (default 3 hours), check 4 (session count) is skipped entirely. All other checks still run regardless of when MFA was last completed.",[856,2514,1347,2515,2517],{},[860,2516,95],{"href":96}," for the complete function signature, data lookup, per-check details, and the MFA bypass window mechanics.",[884,2519],{},[887,2521,2523],{"id":2522},"multi-factor-authentication","Multi-factor authentication",[856,2525,2526],{},"The service ships with two MFA mechanisms:",[856,2528,2529,2532,2533,2536,2537,2539,2540,1459],{},[941,2530,2531],{},"Adaptive MFA"," is triggered automatically when anomaly detection returns ",[978,2534,2535],{},"reqMFA: true",". The user does not initiate it. The service detects the anomaly, generates a 7-digit OTP, hashes it with SHA-256, stores the hash in the ",[978,2538,1364],{}," table with a 7-minute expiry, and sends the code to the user's email inside a ",[860,2541,2542],{"href":116},"magic link",[856,2544,2545,2548],{},[941,2546,2547],{},"Custom MFA"," is triggered by your application for sensitive user actions (password change, fund transfer, account deletion). You call the provided initiation route, and the service handles code generation, email delivery, and verification identically to adaptive MFA.",[856,2550,2551],{},"Both mechanisms share the same verification function, which runs inside a database transaction:",[2111,2553,2554,2566,2571,2574,2589,2596,2599,2618,2621,2624,2631,2638],{},[2006,2555,2556,2557,1431,2560,1447,2563],{},"Rate-limit the IP, the JTI, and the submitted code hash through ",[978,2558,2559],{},"uniLimiter",[978,2561,2562],{},"ipLimit",[978,2564,2565],{},"usedJtiLimiter",[2006,2567,2568,2569],{},"Validate and sanitize the submitted code through ",[978,2570,792],{},[2006,2572,2573],{},"Hash the code with SHA-256",[2006,2575,2576,2579,2580,2582,2583,1431,2585,2588],{},[978,2577,2578],{},"SELECT ... FOR UPDATE"," the matching ",[978,2581,1364],{}," row by ",[978,2584,1438],{},[978,2586,2587],{},"code_hash",", not expired, not used",[2006,2590,2591,2592,2595],{},"Atomically ",[978,2593,2594],{},"DELETE"," the row (prevents replay)",[2006,2597,2598],{},"Block the submitted code hash in the limiter for 10 minutes",[2006,2600,2601,2602,2605,2606,1431,2609,1431,2612,1447,2615],{},"In a single ",[978,2603,2604],{},"UPDATE users JOIN visitors",", set ",[978,2607,2608],{},"users.last_mfa_at = UTC_TIMESTAMP()",[978,2610,2611],{},"users.visitor_id = currentVisitorId",[978,2613,2614],{},"visitors.proxy_allowed = 1",[978,2616,2617],{},"visitors.hosting_allowed = 1",[2006,2619,2620],{},"Block the JTI in the limiter for 20 minutes",[2006,2622,2623],{},"Commit the transaction",[2006,2625,2626,2627,2630],{},"Call ",[978,2628,2629],{},"updateVisitors()"," in the bot-detector to overwrite the stored fingerprint with the current request's geo and User-Agent data",[2006,2632,2633,2634,2637],{},"Verify the bound refresh token, then revoke it (or revoke every refresh token for the user when ",[978,2635,2636],{},"revokeAllTokensOnSuccess"," is true, used by the password reset flow)",[2006,2639,2640,2641,2644,2645,866,2647,2649],{},"Issue a new access token with a fresh ",[978,2642,2643],{},"randomUUID()"," JTI and a new refresh token row, then set the ",[978,2646,1446],{},[978,2648,1564],{}," cookies",[856,2651,2652],{},"MFA verification is protected by rate limiters and consecutive failure caches that work together:",[914,2654,2655,2666],{},[917,2656,2657],{},[920,2658,2659,2661,2664],{},[923,2660,925],{},[923,2662,2663],{},"Key",[923,2665,1018],{},[933,2667,2668,2681,2694,2707,2719,2732],{},[920,2669,2670,2673,2678],{},[938,2671,2672],{},"Union limiter",[938,2674,2675],{},[978,2676,2677],{},"{IP}_{JTI}",[938,2679,2680],{},"Limits attempts per IP and verification session",[920,2682,2683,2686,2691],{},[938,2684,2685],{},"Code hash limiter",[938,2687,2688],{},[978,2689,2690],{},"{code_hash}",[938,2692,2693],{},"Blocks a specific code after successful use (prevents replay)",[920,2695,2696,2699,2704],{},[938,2697,2698],{},"JTI limiter",[938,2700,2701],{},[978,2702,2703],{},"{JTI}",[938,2705,2706],{},"Prevents reuse of a verified JTI",[920,2708,2709,2712,2716],{},[938,2710,2711],{},"Consecutive cache (hash)",[938,2713,2714],{},[978,2715,2690],{},[938,2717,2718],{},"Tracks repeated submissions of the same wrong code (10 min)",[920,2720,2721,2724,2729],{},[938,2722,2723],{},"Consecutive cache (IP)",[938,2725,2726],{},[978,2727,2728],{},"{IP}",[938,2730,2731],{},"Progressive slowdown on repeated failures from same IP (10 min)",[920,2733,2734,2737,2741],{},[938,2735,2736],{},"Consecutive cache (JTI)",[938,2738,2739],{},[978,2740,2703],{},[938,2742,2743],{},"Tracks failures per verification session (20 min)",[856,2745,1347,2746,2748],{},[860,2747,123],{"href":124}," for the full lifecycle, OTP generation, email template, and verification flow.",[884,2750],{},[887,2752,2754],{"id":2753},"magic-link-security","Magic link security",[856,2756,2757,2760],{},[860,2758,2759],{"href":116},"Magic links"," are short-lived, single-use JWTs embedded as URL query parameters in emails. They are used for adaptive MFA, password reset, email update, and custom MFA flows.",[856,2762,2763,2764,2768,2769,2772,2773,2775,2776,2778,2779,2782],{},"Each MFA link is signed with ",[860,2765,2767],{"href":1127,"rel":2766},[864],"HMAC-SHA512"," using a dedicated secret key\n(",[978,2770,2771],{},"magic_links.jwt_secret_key","), separate from the main JWT secret. The\nJWT ID (",[978,2774,1438],{},") is built as ",[978,2777,1442],{}," concatenated with\n",[978,2780,2781],{},"crypto.randomBytes(64).toString('hex')",", producing a 36-character UUID\nfollowed by 128 hex characters. The extra entropy makes brute-force\nenumeration of active JTIs infeasible.",[856,2784,2785,2786,2789,2790,2793,2794,2797,2798,2800,2801,2803,2804,2809],{},"Each magic link URL also carries a ",[978,2787,2788],{},"random"," query parameter:\n",[978,2791,2792],{},"crypto.randomBytes(128).toString('hex')",", a 256-character hex string.\nThe server keeps only the SHA-256 digest of that value inside the signed\nJWT payload as ",[978,2795,2796],{},"randomHashed",". On verification, the middleware hashes the\nincoming ",[978,2799,2788],{},", loads ",[978,2802,2796],{}," from the decoded JWT, and\ncompares the two digests using ",[860,2805,2808],{"href":2806,"rel":2807},"https:\u002F\u002Fnodejs.org\u002Fapi\u002Fcrypto.html#cryptotimingsafeequabordera-b",[864],"timing-safe equality",". The raw value is\nnever persisted, and the comparison is constant-time.",[856,2811,2812],{},"Links are cached in a server-side LRU store. When a link is consumed, the cache entry is removed, which means a verified link cannot be replayed even if the JWT itself has not expired. Cache misses (expired or evicted entries) fail verification immediately without attempting JWT decoding.",[1292,2814,2815],{},[856,2816,2817,2818,2821],{},"Always use a separate ",[978,2819,2820],{},"jwt_secret_key"," for magic links. Never reuse the access-token or refresh-token secret. This limits the blast radius if one secret is compromised.",[884,2823],{},[887,2825,2827],{"id":2826},"rate-limiting","Rate limiting",[856,2829,2830,2831,2836],{},"The service uses ",[860,2832,2835],{"href":2833,"rel":2834},"https:\u002F\u002Fgithub.com\u002Fanimir\u002Fnode-rate-limiter-flexible",[864],"rate-limiter-flexible"," for all rate limiting. Every sensitive endpoint has its own named limiter group. The limiters store state in MySQL with in-memory mirrors for fast rejections.",[907,2838,2840],{"id":2839},"layered-design","Layered design",[856,2842,2843],{},"A typical endpoint has three limiter layers:",[914,2845,2846,2854],{},[917,2847,2848],{},[920,2849,2850,2852],{},[923,2851,925],{},[923,2853,1018],{},[933,2855,2856,2865,2875],{},[920,2857,2858,2862],{},[938,2859,2860],{},[941,2861,2672],{},[938,2863,2864],{},"Combines a burst limiter (low points, short window) and a slow limiter (higher points, longer window). Both are consumed on every attempt. If either rejects, the request is blocked.",[920,2866,2867,2872],{},[938,2868,2869],{},[941,2870,2871],{},"IP limiter",[938,2873,2874],{},"Per-IP rate limit applied independently of the union.",[920,2876,2877,2882],{},[938,2878,2879],{},[941,2880,2881],{},"Identity limiter",[938,2883,2884],{},"Keyed on email, OAuth subject, token hash, or user ID depending on the endpoint. Prevents attacks targeting a single identity.",[907,2886,2888],{"id":2887},"strike-system","Strike system",[856,2890,2891,2892,2895,2896,2899,2900,2903],{},"On top of the limiter layer, a consecutive failure cache tracks strikes per key in an LRU cache. When a key accumulates enough strikes (the ",[978,2893,2894],{},"maxBans"," threshold), the ",[978,2897,2898],{},"guard"," function permanently blocks the key via the limiter's ",[978,2901,2902],{},"block()"," method. Blocked keys are rejected immediately on subsequent requests without consuming limiter points.",[907,2905,2907],{"id":2906},"endpoint-coverage","Endpoint coverage",[914,2909,2910,2920],{},[917,2911,2912],{},[920,2913,2914,2917],{},[923,2915,2916],{},"Endpoint",[923,2918,2919],{},"Limiters",[933,2921,2922,2932,2942,2952,2961,2971,2979,2987],{},[920,2923,2924,2929],{},[938,2925,2926],{},[978,2927,2928],{},"POST \u002Flogin",[938,2930,2931],{},"Union (burst + slow), IP, email, composite (IP + email)",[920,2933,2934,2939],{},[938,2935,2936],{},[978,2937,2938],{},"POST \u002Fsignup",[938,2940,2941],{},"Union IP (burst + slow), union composite (burst + slow), email",[920,2943,2944,2949],{},[938,2945,2946],{},[978,2947,2948],{},"POST \u002Fauth\u002FOAuth\u002F:provider",[938,2950,2951],{},"Union IP (burst + slow), subject, composite (IP + subject)",[920,2953,2954,2958],{},[938,2955,2956],{},[978,2957,2154],{},[938,2959,2960],{},"Union access (burst + slow), union refresh (burst + slow), standalone refresh",[920,2962,2963,2968],{},[938,2964,2965],{},[978,2966,2967],{},"POST \u002Fauth\u002Fforgot-password",[938,2969,2970],{},"Union (burst + slow), IP, email",[920,2972,2973,2976],{},[938,2974,2975],{},"MFA email sending",[938,2977,2978],{},"Union (burst + slow), IP, user ID, global email cap",[920,2980,2981,2984],{},[938,2982,2983],{},"Link verification",[938,2985,2986],{},"Union (burst + slow)",[920,2988,2989,2992],{},[938,2990,2991],{},"Temp POST routes",[938,2993,2994],{},"Union (burst + slow), IP",[856,2996,1347,2997,2999,3000,866,3002,3005],{},[860,2998,147],{"href":148}," for the full limiter configuration, the ",[978,3001,2898],{},[978,3003,3004],{},"unionLimiter"," API, and the MySQL storage pool setup.",[884,3007],{},[887,3009,3011],{"id":3010},"bot-detection","Bot detection",[856,3013,1328,3014,3016,3017,3019],{},[860,3015,399],{"href":404}," middleware runs before any authentication route. It scores every request based on IP reputation, header consistency, geolocation anomalies, and device fingerprint deviations. The score accumulates in the ",[978,3018,1522],{}," table and feeds directly into anomaly detection (check 7).",[856,3021,3022],{},"The bot detector:",[2003,3024,3025,3031,3040,3047,3050,3059],{},[2006,3026,3027,3028,3030],{},"Generates the ",[978,3029,1396],{}," cookie on first contact",[2006,3032,3033,3034,3039],{},"Resolves geolocation from the client IP via local ",[860,3035,3038],{"href":3036,"rel":3037},"https:\u002F\u002Fmaxmind.github.io\u002FMaxMind-DB\u002F",[864],"MMDB"," databases",[2006,3041,3042,3043,3046],{},"Parses the ",[978,3044,3045],{},"User-Agent"," header into structured device, browser, and OS fields",[2006,3048,3049],{},"Upserts the visitor record with all fingerprint data",[2006,3051,3052,3053,3058],{},"Tracks ",[860,3054,3057],{"href":3055,"rel":3056},"https:\u002F\u002Fwww.torproject.org\u002F",[864],"Tor"," exit nodes and known hosting\u002Fproxy ranges",[2006,3060,3061,3062,3065,3066],{},"Maintains a ",[978,3063,3064],{},"suspicious_activity_score"," through the ",[860,3067,3068],{"href":435},"checker pipeline",[856,3070,3071,3072,3074],{},"The MFA threshold is deliberately low: anomaly check 7 triggers at 25% of ",[978,3073,2118],{}," (default 25 out of 100). This means MFA challenges start well before the visitor reaches the ban threshold, giving legitimate users a chance to prove themselves while slowing down automated attacks.",[856,3076,1347,3077,3080],{},[860,3078,3079],{"href":514},"Bot Detector Configuration"," for the full scoring model, checker options, and ban behavior.",[884,3082],{},[887,3084,3086],{"id":3085},"device-fingerprinting","Device fingerprinting",[856,3088,1328,3089,3092],{},[978,3090,3091],{},"getFingerPrint"," middleware builds a composite device fingerprint from the incoming request. It is a server-side fingerprint: no canvas hashing, no WebGL probing, no browser APIs. The data comes entirely from request headers and IP metadata.",[856,3094,3095],{},"The fingerprint combines:",[914,3097,3098,3108],{},[917,3099,3100],{},[920,3101,3102,3105],{},[923,3103,3104],{},"Category",[923,3106,3107],{},"Fields",[933,3109,3110,3157,3175,3196],{},[920,3111,3112,3116],{},[938,3113,3114],{},[941,3115,500],{},[938,3117,3118,1431,3121,1431,3124,1431,3127,1431,3130,1431,3133,1431,3136,1431,3139,1431,3142,1431,3145,1431,3148,1431,3151,1431,3154],{},[978,3119,3120],{},"country",[978,3122,3123],{},"countryCode",[978,3125,3126],{},"region",[978,3128,3129],{},"regionName",[978,3131,3132],{},"city",[978,3134,3135],{},"district",[978,3137,3138],{},"lat",[978,3140,3141],{},"lon",[978,3143,3144],{},"timezone",[978,3146,3147],{},"currency",[978,3149,3150],{},"isp",[978,3152,3153],{},"org",[978,3155,3156],{},"as_org",[920,3158,3159,3164],{},[938,3160,3161],{},[941,3162,3163],{},"Device",[938,3165,3166,1431,3169,1431,3172],{},[978,3167,3168],{},"device",[978,3170,3171],{},"deviceVendor",[978,3173,3174],{},"deviceModel",[920,3176,3177,3182],{},[938,3178,3179],{},[941,3180,3181],{},"Browser",[938,3183,3184,1431,3187,1431,3190,1431,3193],{},[978,3185,3186],{},"browser",[978,3188,3189],{},"browserType",[978,3191,3192],{},"browserVersion",[978,3194,3195],{},"os",[920,3197,3198,3203],{},[938,3199,3200],{},[941,3201,3202],{},"Network",[938,3204,3205,1431,3208,1431,3211],{},[978,3206,3207],{},"proxy",[978,3209,3210],{},"hosting",[978,3212,3213],{},"ipAddress",[856,3215,3216,3217,1431,3220,3223,3224,3227],{},"The anomaly detection fingerprint loop (check 9) compares each incoming field against the stored visitor record. The comparison skips any field where either side is ",[978,3218,3219],{},"null",[978,3221,3222],{},"undefined",", or the string ",[978,3225,3226],{},"'unknown'",". A single mismatch on a populated field triggers MFA.",[856,3229,1347,3230,3232],{},[860,3231,127],{"href":128}," for the full field reference and how fingerprint data flows through login, rotation, and MFA.",[884,3234],{},[887,3236,3238],{"id":3237},"trusted-devices-on-login","Trusted devices on login",[856,3240,1328,3241,3244,3245,3247,3248,3251],{},[978,3242,3243],{},"trustUserDeviceOnAuth"," configuration option controls whether the login controller re-baselines the user's visitor record. When set to ",[978,3246,1032],{},", a successful login calls ",[978,3249,3250],{},"trustVisitor()"," to:",[2111,3253,3254,3263],{},[2006,3255,3256,3257,3260,3261],{},"Point ",[978,3258,3259],{},"users.visitor_id"," at the visitor row associated with the current ",[978,3262,1396],{},[2006,3264,3265],{},"Overwrite the visitor fingerprint with the current request's geo and User-Agent data",[856,3267,3268],{},"This means the stored fingerprint always reflects the device the user most recently logged in from. Without this option, the fingerprint stays fixed to the initial device, which causes false positives when users legitimately switch devices.",[1292,3270,3271],{},[856,3272,3273,3274,2319,3276,3278,3279,3281,3282,3284],{},"When ",[978,3275,3243],{},[978,3277,1032],{},", a successful login marks the device as trusted and skips adaptive MFA when the ",[978,3280,1396],{}," changes on the next session. This reduces friction but weakens the anomaly detection layer. Set to ",[978,3283,1289],{}," for high-security deployments.",[884,3286],{},[887,3288,3290],{"id":3289},"signup-security","Signup security",[856,3292,3293],{},"The signup flow runs a specific sequence of checks before creating an account:",[1804,3295,3296,3300,3306,3310,3338,3342,3351,3355,3369,3373,3392,3396],{"level":1261},[1807,3297,3299],{"id":3298},"content-type-and-rate-limiting","Content-type and rate limiting",[856,3301,3302,3303,3305],{},"The request must have ",[978,3304,1692],{},". IP-based burst and slow limiters run first, followed by composite key (IP + email) limiters.",[1807,3307,3309],{"id":3308},"input-validation","Input validation",[856,3311,3312,3313,1431,3316,1431,3319,1431,3322,1431,3325,1447,3328,1693,3331,3333,3334,3337],{},"The Zod schema validates ",[978,3314,3315],{},"name",[978,3317,3318],{},"email",[978,3320,3321],{},"password",[978,3323,3324],{},"confirmedPassword",[978,3326,3327],{},"rememberUser",[978,3329,3330],{},"termsConsent",[978,3332,3330],{}," field must be the literal string ",[978,3335,3336],{},"\"on\"",". If any field contains HTML, the visitor is banned immediately.",[1807,3339,3341],{"id":3340},"email-domain-validation","Email domain validation",[856,3343,3344,3345,3350],{},"The service performs a DNS ",[860,3346,3349],{"href":3347,"rel":3348},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMX_record",[864],"MX record"," lookup on the email domain. If the domain has no mail server, the signup is rejected.",[1807,3352,3354],{"id":3353},"disposable-email-detection","Disposable email detection",[856,3356,3357,3358,3362,3363,3368],{},"The email domain is checked against the ",[860,3359,40],{"href":3360,"rel":3361},"https:\u002F\u002Fgithub.com\u002FniceSh3ll\u002FshieldBase-cli",[864]," disposable email blocklist (stored in a local ",[860,3364,3367],{"href":3365,"rel":3366},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLightning_Memory-Mapped_Database",[864],"LMDB"," database). Disposable domains are rejected.",[1807,3370,3372],{"id":3371},"password-breach-check","Password breach check",[856,3374,3375,3376,3381,3382,3387,3388,3391],{},"The password is checked against the ",[860,3377,3380],{"href":3378,"rel":3379},"https:\u002F\u002Fhaveibeenpwned.com\u002FAPI\u002Fv3#PwnedPasswords",[864],"Have I Been Pwned"," database using ",[860,3383,3386],{"href":3384,"rel":3385},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FK-anonymity",[864],"k-anonymity"," (only the first 5 characters of the SHA-1 hash are sent to the API). During signup, a breached password ",[941,3389,3390],{},"blocks"," account creation. The user must choose a different password.",[1807,3393,3395],{"id":3394},"password-hashing-and-account-creation","Password hashing and account creation",[856,3397,3398],{},"The password is hashed with Argon2id + pepper, and the user record is created in a database transaction. If the email already exists, the service returns HTTP 409.",[856,3400,1347,3401,3403],{},[860,3402,99],{"href":100}," for the full flow and response format.",[884,3405],{},[887,3407,3409],{"id":3408},"login-security","Login security",[856,3411,3412],{},"The login flow shares several checks with signup but has some differences:",[1804,3414,3415,3418,3421,3424,3431,3435,3447,3451,3454,3457,3468,3472],{"level":1261},[1807,3416,2827],{"id":3417},"rate-limiting-1",[856,3419,3420],{},"IP, email, and composite key (IP + email) rate limiters run before any database query.",[1807,3422,3309],{"id":3423},"input-validation-1",[856,3425,3312,3426,866,3428,3430],{},[978,3427,3318],{},[978,3429,3321],{}," with the same pattern and length constraints as signup.",[1807,3432,3434],{"id":3433},"user-lookup","User lookup",[856,3436,3437,3438,3441,3442,1459],{},"The service queries for a user with the submitted email. If no user is found, the response is ",[978,3439,3440],{},"{ ok: false, error: 'Invalid email or password' }",". The same error message is returned for invalid passwords to prevent ",[860,3443,3446],{"href":3444,"rel":3445},"https:\u002F\u002Fowasp.org\u002Fwww-project-web-security-testing-guide\u002Flatest\u002F4-Web_Application_Security_Testing\u002F03-Identity_Management_Testing\u002F04-Testing_for_Account_Enumeration_and_Guessable_User_Account",[864],"user enumeration",[1807,3448,3450],{"id":3449},"password-verification","Password verification",[856,3452,3453],{},"Argon2id verification runs with the stored hash and the pepper. The verification is internally constant-time (Argon2 handles this). On failure, the same generic error message is returned.",[1807,3455,3372],{"id":3456},"password-breach-check-1",[856,3458,3459,3460,3463,3464,3467],{},"The password is checked against Have I Been Pwned. During login, a breached password does ",[941,3461,3462],{},"not"," block the login. Instead, the response includes a ",[978,3465,3466],{},"breached"," warning message advising the user to change their password.",[1807,3469,3471],{"id":3470},"device-trust-and-token-issuance","Device trust and token issuance",[856,3473,3474,3475,3477],{},"If ",[978,3476,3243],{}," is enabled, the device fingerprint is re-baselined. A new refresh token and access token are generated. Rate limiter state is reset for the compositeKey on success.",[856,3479,1347,3480,3403],{},[860,3481,103],{"href":104},[884,3483],{},[887,3485,3487],{"id":3486},"oauth-security","OAuth security",[856,3489,3490,3491,1431,3494,1431,3497,1431,3500,1431,3503,1431,3506,1431,3509,3512,3513,3516],{},"OAuth social login providers are validated with Zod schemas. Each provider definition maps an incoming OAuth profile to the internal user schema. Providers can be defined with either a full custom Zod schema for complete control, or a field-type map shorthand that builds the schema automatically from type tokens (",[978,3492,3493],{},"'string'",[978,3495,3496],{},"'email'",[978,3498,3499],{},"'safeString'",[978,3501,3502],{},"'url'",[978,3504,3505],{},"'boolean'",[978,3507,3508],{},"'number'",[978,3510,3511],{},"'int'",", each optionally suffixed with ",[978,3514,3515],{},"?"," for optional fields).",[856,3518,3519],{},"OAuth requests pass through three rate limiters:",[914,3521,3522,3533],{},[917,3523,3524],{},[920,3525,3526,3529,3531],{},[923,3527,3528],{},"Limiter",[923,3530,2663],{},[923,3532,1018],{},[933,3534,3535,3546,3557],{},[920,3536,3537,3540,3543],{},[938,3538,3539],{},"IP burst + slow",[938,3541,3542],{},"IP address",[938,3544,3545],{},"Prevents rapid OAuth attempts from the same IP",[920,3547,3548,3551,3554],{},[938,3549,3550],{},"Subject",[938,3552,3553],{},"OAuth provider ID",[938,3555,3556],{},"Prevents enumeration of valid OAuth subjects",[920,3558,3559,3562,3565],{},[938,3560,3561],{},"Composite",[938,3563,3564],{},"IP + subject",[938,3566,3567],{},"Fine-grained control combining both dimensions",[856,3569,3570],{},"If the OAuth profile passes schema validation and the email is not already registered with a password-based account, the user is created. Duplicate emails return HTTP 409.",[856,3572,1347,3573,3575],{},[860,3574,111],{"href":112}," for the full provider definition and validation flow.",[884,3577],{},[887,3579,3581],{"id":3580},"security-headers","Security headers",[856,3583,3584,3585,3590,3591,3594],{},"The service registers ",[860,3586,3589],{"href":3587,"rel":3588},"https:\u002F\u002Fhelmetjs.github.io\u002F",[864],"Helmet"," in ",[978,3592,3593],{},"service.ts"," with the following explicit overrides:",[3596,3597,3602],"pre",{"className":3598,"code":3599,"language":3600,"meta":3601,"style":3601},"language-ts shiki shiki-themes light-plus light-plus dracula","helmet({\n  crossOriginEmbedderPolicy: true,\n  xFrameOptions: { action: 'deny' },\n  contentSecurityPolicy: {\n    directives: { 'frame-ancestors': [\"'none'\"] }\n  },\n  referrerPolicy: { policy: 'origin' }\n})\n","ts","",[978,3603,3604,3617,3634,3663,3674,3706,3712,3737],{"__ignoreMap":3601},[3605,3606,3609,3613],"span",{"class":3607,"line":3608},"line",1,[3605,3610,3612],{"class":3611},"sHOzp","helmet",[3605,3614,3616],{"class":3615},"sDd4n","({\n",[3605,3618,3620,3624,3627,3631],{"class":3607,"line":3619},2,[3605,3621,3623],{"class":3622},"sjsA6","  crossOriginEmbedderPolicy",[3605,3625,1708],{"class":3626},"s34zl",[3605,3628,3630],{"class":3629},"sjR7W"," true",[3605,3632,3633],{"class":3615},",\n",[3605,3635,3637,3640,3642,3645,3648,3650,3654,3658,3660],{"class":3607,"line":3636},3,[3605,3638,3639],{"class":3622},"  xFrameOptions",[3605,3641,1708],{"class":3626},[3605,3643,3644],{"class":3615}," { ",[3605,3646,3647],{"class":3622},"action",[3605,3649,1708],{"class":3626},[3605,3651,3653],{"class":3652},"sFkSl"," '",[3605,3655,3657],{"class":3656},"sFB1V","deny",[3605,3659,2079],{"class":3652},[3605,3661,3662],{"class":3615}," },\n",[3605,3664,3666,3669,3671],{"class":3607,"line":3665},4,[3605,3667,3668],{"class":3622},"  contentSecurityPolicy",[3605,3670,1708],{"class":3626},[3605,3672,3673],{"class":3615}," {\n",[3605,3675,3677,3680,3682,3684,3686,3689,3691,3693,3696,3698,3701,3703],{"class":3607,"line":3676},5,[3605,3678,3679],{"class":3622},"    directives",[3605,3681,1708],{"class":3626},[3605,3683,3644],{"class":3615},[3605,3685,2079],{"class":3652},[3605,3687,3688],{"class":3656},"frame-ancestors",[3605,3690,2079],{"class":3652},[3605,3692,1708],{"class":3626},[3605,3694,3695],{"class":3615}," [",[3605,3697,2076],{"class":3652},[3605,3699,3700],{"class":3656},"'none'",[3605,3702,2076],{"class":3652},[3605,3704,3705],{"class":3615},"] }\n",[3605,3707,3709],{"class":3607,"line":3708},6,[3605,3710,3711],{"class":3615},"  },\n",[3605,3713,3715,3718,3720,3722,3725,3727,3729,3732,3734],{"class":3607,"line":3714},7,[3605,3716,3717],{"class":3622},"  referrerPolicy",[3605,3719,1708],{"class":3626},[3605,3721,3644],{"class":3615},[3605,3723,3724],{"class":3622},"policy",[3605,3726,1708],{"class":3626},[3605,3728,3653],{"class":3652},[3605,3730,3731],{"class":3656},"origin",[3605,3733,2079],{"class":3652},[3605,3735,3736],{"class":3615}," }\n",[3605,3738,3740],{"class":3607,"line":3739},8,[3605,3741,3742],{"class":3615},"})\n",[914,3744,3745,3757],{},[917,3746,3747],{},[920,3748,3749,3752,3754],{},[923,3750,3751],{},"Header",[923,3753,1015],{},[923,3755,3756],{},"Source",[933,3758,3759,3772,3787,3801,3814,3826,3838,3849,3860,3872,3884,3896,3908],{},[920,3760,3761,3766,3769],{},[938,3762,3763],{},[978,3764,3765],{},"Content-Security-Policy",[938,3767,3768],{},"Helmet defaults merged with frame-ancestors 'none'",[938,3770,3771],{},"Custom + default",[920,3773,3774,3779,3784],{},[938,3775,3776],{},[978,3777,3778],{},"Strict-Transport-Security",[938,3780,3781],{},[978,3782,3783],{},"max-age=15552000; includeSubDomains",[938,3785,3786],{},"Helmet default",[920,3788,3789,3794,3799],{},[938,3790,3791],{},[978,3792,3793],{},"X-Content-Type-Options",[938,3795,3796],{},[978,3797,3798],{},"nosniff",[938,3800,3786],{},[920,3802,3803,3808,3811],{},[938,3804,3805],{},[978,3806,3807],{},"X-Frame-Options",[938,3809,3810],{},"DENY",[938,3812,3813],{},"Custom override",[920,3815,3816,3821,3824],{},[938,3817,3818],{},[978,3819,3820],{},"Cross-Origin-Embedder-Policy",[938,3822,3823],{},"require-corp",[938,3825,3813],{},[920,3827,3828,3833,3836],{},[938,3829,3830],{},[978,3831,3832],{},"Cross-Origin-Opener-Policy",[938,3834,3835],{},"same-origin",[938,3837,3786],{},[920,3839,3840,3845,3847],{},[938,3841,3842],{},[978,3843,3844],{},"Cross-Origin-Resource-Policy",[938,3846,3835],{},[938,3848,3786],{},[920,3850,3851,3856,3858],{},[938,3852,3853],{},[978,3854,3855],{},"Referrer-Policy",[938,3857,3731],{},[938,3859,3813],{},[920,3861,3862,3867,3870],{},[938,3863,3864],{},[978,3865,3866],{},"Origin-Agent-Cluster",[938,3868,3869],{},"?1",[938,3871,3786],{},[920,3873,3874,3879,3882],{},[938,3875,3876],{},[978,3877,3878],{},"X-DNS-Prefetch-Control",[938,3880,3881],{},"off",[938,3883,3786],{},[920,3885,3886,3891,3894],{},[938,3887,3888],{},[978,3889,3890],{},"X-Download-Options",[938,3892,3893],{},"noopen",[938,3895,3786],{},[920,3897,3898,3903,3906],{},[938,3899,3900],{},[978,3901,3902],{},"X-Permitted-Cross-Domain-Policies",[938,3904,3905],{},"none",[938,3907,3786],{},[920,3909,3910,3915,3917],{},[938,3911,3912],{},[978,3913,3914],{},"X-XSS-Protection",[938,3916,1478],{},[938,3918,3919],{},"Helmet default (v4+)",[1292,3921,3922],{},[856,3923,3924],{},"Helmet v4 and later emit X-XSS-Protection: 0 on purpose. The legacy\nbrowser XSS filter has been removed from modern browsers, and when it was\nactive it introduced cross-site leak bugs. Real XSS mitigation in this\nservice comes from the sanitization pipeline\nand the strict CSP, not this header.",[884,3926],{},[887,3928,3930],{"id":3929},"structured-logging","Structured logging",[856,3932,2830,3933,3938],{},[860,3934,3937],{"href":3935,"rel":3936},"https:\u002F\u002Fgetpino.io\u002F",[864],"Pino"," for structured JSON logging. Every security event is logged with relevant context at the appropriate level:",[914,3940,3941,3954],{},[917,3942,3943],{},[920,3944,3945,3948,3951],{},[923,3946,3947],{},"Event",[923,3949,3950],{},"Level",[923,3952,3953],{},"Context",[933,3955,3956,3969,3982,3994,4006,4018,4030,4042],{},[920,3957,3958,3961,3966],{},[938,3959,3960],{},"Login success",[938,3962,3963],{},[978,3964,3965],{},"info",[938,3967,3968],{},"User ID, IP",[920,3970,3971,3974,3979],{},[938,3972,3973],{},"Login failure",[938,3975,3976],{},[978,3977,3978],{},"warn",[938,3980,3981],{},"IP, reason",[920,3983,3984,3987,3991],{},[938,3985,3986],{},"Anomaly detection trigger",[938,3988,3989],{},[978,3990,3978],{},[938,3992,3993],{},"Check number, reason, IP, visitor ID",[920,3995,3996,3999,4003],{},[938,3997,3998],{},"Rate limit hit",[938,4000,4001],{},[978,4002,3978],{},[938,4004,4005],{},"Endpoint, IP, key",[920,4007,4008,4011,4015],{},[938,4009,4010],{},"XSS attempt",[938,4012,4013],{},[978,4014,3978],{},[938,4016,4017],{},"IP, payload summary, ban status",[920,4019,4020,4023,4027],{},[938,4021,4022],{},"Token revocation",[938,4024,4025],{},[978,4026,3965],{},[938,4028,4029],{},"User ID, reason",[920,4031,4032,4035,4039],{},[938,4033,4034],{},"MFA code sent",[938,4036,4037],{},[978,4038,3965],{},[938,4040,4041],{},"User ID",[920,4043,4044,4047,4051],{},[938,4045,4046],{},"MFA code verified",[938,4048,4049],{},[978,4050,3965],{},[938,4052,4041],{},[856,4054,4055],{},"Error responses never include stack traces, internal error messages, or database details. All server-side errors return a generic message to the client.",[907,4057,4059],{"id":4058},"configuration-encryption","Configuration encryption",[856,4061,4062,4063,4066,4067,4070,4071,4074],{},"Sensitive configuration (database credentials, JWT secrets, HMAC shared secrets, pepper, email API keys) is stored in an ",[860,4064,1201],{"href":1199,"rel":4065},[864],"-encrypted file (",[978,4068,4069],{},"config.json.age","). The decryption key is mounted as a Docker secret. The ",[978,4072,4073],{},"decrypt.sh"," script decrypts the file at container startup, the service loads it, and then deletes it from disk.",[884,4076],{},[887,4078,4080],{"id":4079},"attack-scenarios","Attack scenarios",[856,4082,4083],{},"This section walks through specific attack scenarios and how the service's security layers respond.",[907,4085,4087],{"id":4086},"stolen-refresh-token","Stolen refresh token",[856,4089,4090],{},"The most plausible attack vector. An attacker obtains a valid refresh token through phishing.",[856,4092,4093,4096,4097,2217,4099,2220,4101,4103,4104,4107,4108,4110],{},[941,4094,4095],{},"If the user rotates first:"," The user's BFF rotates the token before the attacker can use it. ",[978,4098,1474],{},[978,4100,1478],{},[978,4102,1482],{},". The attacker presents the now-consumed token. ",[978,4105,4106],{},"consumeAndVerifyRefreshToken"," detects ",[978,4109,2192],{}," and revokes all tokens for that user. The attacker's attempt fails. The user is logged out and must re-authenticate, but the session is safe.",[856,4112,4113,4116,4117,4119,4120,4122,4123,4127],{},[941,4114,4115],{},"If the attacker rotates first:"," The attacker uses the token before the user. The ",[860,4118,2163],{"href":96}," pipeline runs on the attacker's request. Even if the attacker somehow has the ",[978,4121,1396],{}," cookie, they still face check 6, check 8, and check 9 (the ",[860,4124,4126],{"href":4125},"\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies#device-fingerprint-consistency-loop","fingerprint consistency loop","). Any single mismatch on a populated field triggers adaptive MFA. The attacker must then complete a 7-digit OTP challenge sent to the user's email. Meanwhile, when the user's BFF presents the now-consumed token, reuse detection fires and revokes all sessions, including the attacker's newly issued tokens.",[4129,4130,4131],"note",{},[856,4132,4133,4134,4137,4138,4140],{},"The attacker must obtain ",[941,4135,4136],{},"four"," things to succeed: the refresh token, the ",[978,4139,1396],{}," cookie, a matching device fingerprint (same IP range, geo, browser, OS, device), and access to the user's email. Phishing can realistically obtain the token and the cookie. Spoofing the full fingerprint requires a residential proxy in the same IP range and an identical User-Agent. Completing the MFA challenge requires compromising the user's inbox. Each layer is an independent barrier.",[907,4142,4144],{"id":4143},"replay-attack-during-rotation","Replay attack during rotation",[856,4146,4147,4148,2220,4150,4152,4153,1693,4155,4158],{},"An attacker captures a rotation request (token + cookies) and replays it. The first use increments ",[978,4149,1474],{},[978,4151,1482],{},". The replay finds ",[978,4154,2192],{},[978,4156,4157],{},"strangeThings"," function detects the reuse and blocks the request. If the full consumption path runs, all sessions for the user are revoked.",[907,4160,4162],{"id":4161},"cross-site-scripting-xss","Cross-site scripting (XSS)",[856,4164,4165,4166,4168,4169,4171,4172,1459],{},"An attacker makes a payload containing obfuscated HTML or JavaScript and submits it through a form field (signup name, email, etc.). The input passes through the six-stage sanitization pipeline. The iterative decoding loop strips nested encodings. The pattern detection catches tags, event handlers, and JavaScript URIs. The Zod validation layer detects the ",[978,4167,2099],{}," error, calls ",[978,4170,1989],{},", and bans the attacker's IP with the maximum bot detector score. The response is HTTP 403 with ",[978,4173,2131],{},[856,4175,1347,4176,4178],{},[860,4177,139],{"href":140}," for the full pipeline.",[907,4180,4182],{"id":4181},"credential-stuffing","Credential stuffing",[856,4184,4185],{},"An attacker uses a list of leaked email\u002Fpassword pairs to attempt mass logins. The login endpoint is protected by four independent rate limiters:",[2111,4187,4188,4194,4200,4206],{},[2006,4189,4190,4193],{},[941,4191,4192],{},"IP burst limiter"," catches rapid-fire attempts from a single IP",[2006,4195,4196,4199],{},[941,4197,4198],{},"IP slow limiter"," catches distributed attempts spread over time",[2006,4201,4202,4205],{},[941,4203,4204],{},"Email limiter"," prevents attacks targeting a single account",[2006,4207,4208,4211],{},[941,4209,4210],{},"Composite key limiter"," (IP + email) catches the specific pairing",[856,4213,4214],{},"After consecutive failures, the strike system permanently blocks the key. The attacker receives HTTP 429.",[907,4216,4218],{"id":4217},"denial-of-service-via-token-reuse","Denial-of-service via token reuse",[856,4220,4221,4222,4224,4225,4227],{},"An attacker obtains a valid refresh token and uses a residential proxy in the victim's IP range, a spoofed User-Agent that matches the victim's browser, OS, and device fields, and the victim's ",[978,4223,1396],{}," cookie. With all fingerprint fields aligned, the attacker's request passes the ",[860,4226,2163],{"href":96}," pipeline cleanly. They send a single request.",[856,4229,4230],{},"The service rotates the token for the attacker. When the legitimate user makes their next request, the system detects reuse and triggers the kill switch: all tokens for the user are revoked. The user is logged out.",[856,4232,4233,4234,4236],{},"The attacker cannot hijack the session because the rotated token triggers MFA on the next anomaly, and the kill switch fires on reuse. The attack is also self-limiting: once the legitimate user logs in again with a fresh password authentication from a trusted device, the service issues a new refresh token and re-baselines the visitor fingerprint. The attacker's previously stolen token, ",[978,4235,1396],{},", and fingerprint data are now invalid. To repeat the attack, the attacker would need to steal the new credentials all over again.",[856,4238,4239],{},"The attack can persist only as long as the attacker holds valid, current credentials. A single clean login by the user breaks the cycle. This is a deliberate trade-off: the system accepts the risk of temporary user lockout to maintain zero tolerance for stolen tokens.",[907,4241,4243],{"id":4242},"device-compromise-malwarerat","Device compromise (malware\u002FRAT)",[856,4245,4246,4247,4252],{},"If an attacker compromises the victim's physical endpoint via malware or a ",[860,4248,4251],{"href":4249,"rel":4250},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FTrojan_horse_(computing)#Remote_access",[864],"Remote Access Trojan",", they operate as the user. They inherit the correct IP, cookies, and device fingerprint. The anomaly detector passes the request. The kill switch does not trigger.",[856,4254,4255,4256,4261],{},"The service is phishing-resistant but not malware-proof. Defense against endpoint compromise requires hardware-bound ",[860,4257,4260],{"href":4258,"rel":4259},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebAuthn",[864],"FIDO2\u002FWebAuthn"," factors, which are not currently enforced in the default configuration.",[907,4263,4265],{"id":4264},"cpu-exhaustion-via-sanitization","CPU exhaustion via sanitization",[856,4267,4268,4269,4272],{},"An attacker crafts a payload containing deeply nested encoded entities (for example, megabytes of ",[978,4270,4271],{},"%252525...","). The iterative decoding loop in the sanitization pipeline processes each layer, potentially blocking the Node.js event loop.",[856,4274,4275],{},"Mitigation is layered:",[914,4277,4278,4287],{},[917,4279,4280],{},[920,4281,4282,4284],{},[923,4283,925],{},[923,4285,4286],{},"Control",[933,4288,4289,4299,4311,4323],{},[920,4290,4291,4296],{},[938,4292,4293],{},[941,4294,4295],{},"Proxy",[938,4297,4298],{},"Should enforces strict request body size limits at the reverse proxy level",[920,4300,4301,4306],{},[938,4302,4303],{},[941,4304,4305],{},"Express",[938,4307,4308,4310],{},[978,4309,1707],{}," body size limits (1 KB for most routes)",[920,4312,4313,4318],{},[938,4314,4315],{},[941,4316,4317],{},"Sanitizer",[938,4319,4320,4322],{},[978,4321,1890],{}," (default 50) caps the decoding loop iterations",[920,4324,4325,4329],{},[938,4326,4327],{},[941,4328,4317],{},[938,4330,4331,4334],{},[978,4332,4333],{},"maxAllowedInputLength"," rejects oversized inputs before sanitization begins",[907,4336,4338],{"id":4337},"database-compromise","Database compromise",[856,4340,4341],{},"If an attacker gains read access to the database:",[2003,4343,4344,4350,4361,4367],{},[2006,4345,4346,4349],{},[941,4347,4348],{},"Passwords"," are hashed with Argon2id + pepper. Without the pepper, the hashes are not crackable.",[2006,4351,4352,4354,4355,866,4357,4360],{},[941,4353,1468],{}," are stored as SHA-256 hashes with ",[978,4356,1474],{},[978,4358,4359],{},"valid"," flags. The attacker cannot reuse them without the unhashed token, which exists only in the user's cookie.",[2006,4362,4363,4366],{},[941,4364,4365],{},"MFA codes"," are stored as SHA-256 hashes with a 7-minute expiry. The attacker cannot reverse the hash to obtain the code.",[2006,4368,4369,4372],{},[941,4370,4371],{},"Magic link JWTs"," are signed with a secret stored in the encrypted configuration file, not in the database.",[884,4374],{},[887,4376,4378],{"id":4377},"limitations","Limitations",[907,4380,4382],{"id":4381},"no-hardware-bound-authentication","No hardware-bound authentication",[856,4384,4385,4386,4389],{},"The service does not enforce ",[860,4387,4260],{"href":4258,"rel":4388},[864]," or hardware security keys. All authentication factors (password, OTP code, magic link) can be phished or intercepted by malware on the user's device. Adding WebAuthn as an optional second factor is a planned enhancement.",[907,4391,4393],{"id":4392},"false-positives-from-network-changes","False positives from network changes",[856,4395,4396,4397,4399],{},"The default anomaly detection configuration is tuned for high security. Legitimate users who travel, switch networks, or use VPNs may trigger false-positive anomaly detections, resulting in MFA challenges or session termination. The ",[978,4398,2511],{}," window (default 3 hours) mitigates this after a successful MFA challenge, but frequent IP changes within a single session can still cause repeated challenges.",[907,4401,4403],{"id":4402},"single-threaded-sanitization","Single-threaded sanitization",[856,4405,4406,4407,866,4409,4411],{},"The XSS sanitization loop runs synchronously on the Node.js event loop. While the ",[978,4408,1890],{},[978,4410,4333],{}," limits prevent worst-case scenarios, a crafted payload near the limits can still cause brief latency spikes. The upstream reverse proxy is the primary defense against volumetric attacks targeting this surface.",[907,4413,4415],{"id":4414},"no-per-session-encryption-at-rest","No per-session encryption at rest",[856,4417,4418],{},"Refresh tokens are hashed (not encrypted) in the database. SHA-256 is a one-way function and cannot be reversed, but it is not a keyed operation. If an attacker obtains both the database and a valid raw token (for example, from a browser memory dump), they can verify which row the token maps to. Full encryption at rest with a key management service would add a layer but is not currently implemented.",[907,4420,4422],{"id":4421},"email-only-mfa","Email-only MFA",[856,4424,4425,4426,4431],{},"The service currently supports email-based OTP as the only MFA channel. This means MFA security is bounded by the security of the user's email account. If the email account is compromised, the attacker can complete MFA challenges. Support for TOTP (",[860,4427,4430],{"href":4428,"rel":4429},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6238",[864],"RFC 6238",") and push-based authentication are potential future additions.",[884,4433],{},[887,4435,4437],{"id":4436},"summary","Summary",[914,4439,4440,4449],{},[917,4441,4442],{},[920,4443,4444,4446],{},[923,4445,925],{},[923,4447,4448],{},"Controls",[933,4450,4451,4460,4470,4480,4490,4500,4509,4518,4528,4537,4546,4555,4564,4574,4583],{},[920,4452,4453,4457],{},[938,4454,4455],{},[941,4456,943],{},[938,4458,4459],{},"Caddy with TLS termination, request body size limits, upstream rate limiting",[920,4461,4462,4467],{},[938,4463,4464],{},[941,4465,4466],{},"Transport",[938,4468,4469],{},"HMAC-SHA256 inter-service signatures, optional mTLS, replay detection via nonce cache",[920,4471,4472,4477],{},[938,4473,4474],{},[941,4475,4476],{},"Container",[938,4478,4479],{},"Read-only filesystem, all capabilities dropped, non-root user, PID limits",[920,4481,4482,4487],{},[938,4483,4484],{},[941,4485,4486],{},"Input",[938,4488,4489],{},"Content-type enforcement, body size limits, empty body rejection",[920,4491,4492,4497],{},[938,4493,4494],{},[941,4495,4496],{},"Strings",[938,4498,4499],{},"Six-stage XSS sanitization, Zod schema validation, automatic IP ban on HTML detection",[920,4501,4502,4506],{},[938,4503,4504],{},[941,4505,4348],{},[938,4507,4508],{},"Argon2id with pepper, Have I Been Pwned breach check",[920,4510,4511,4515],{},[938,4512,4513],{},[941,4514,83],{},[938,4516,4517],{},"Short-lived JWTs with LRU cache binding, refresh tokens hashed in MySQL, single-use enforcement",[920,4519,4520,4525],{},[938,4521,4522],{},[941,4523,4524],{},"Sessions",[938,4526,4527],{},"Canary cookie binding, nine-check anomaly detection on every use, 30-day hard session limit",[920,4529,4530,4534],{},[938,4531,4532],{},[941,4533,123],{},[938,4535,4536],{},"7-digit OTP with 7-minute expiry, SHA-256 hashed storage, cascading revocation via foreign keys",[920,4538,4539,4543],{},[938,4540,4541],{},[941,4542,2827],{},[938,4544,4545],{},"Per-endpoint union limiters (burst + slow), IP\u002Femail\u002Fcomposite identity limiters, strike-based permanent blocking",[920,4547,4548,4552],{},[938,4549,4550],{},[941,4551,3011],{},[938,4553,4554],{},"IP reputation scoring, Tor\u002Fproxy\u002Fhosting detection, device fingerprinting, configurable ban thresholds",[920,4556,4557,4561],{},[938,4558,4559],{},[941,4560,1773],{},[938,4562,4563],{},"Disposable email blocklist, DNS MX record validation, domain verification",[920,4565,4566,4571],{},[938,4567,4568],{},[941,4569,4570],{},"Headers",[938,4572,4573],{},"Helmet (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)",[920,4575,4576,4580],{},[938,4577,4578],{},[941,4579,143],{},[938,4581,4582],{},"Structured JSON logging with Pino, security event tracking, no PII leakage in error responses",[920,4584,4585,4590],{},[938,4586,4587],{},[941,4588,4589],{},"Secrets",[938,4591,4592],{},"age-encrypted configuration, Docker secrets, pepper stored outside database",[4594,4595,4596],"style",{},"html pre.shiki code .sHOzp, html code.shiki .sHOzp{--shiki-light:#795E26;--shiki-default:#795E26;--shiki-dark:#50FA7B}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 .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);}",{"title":3601,"searchDepth":3619,"depth":3619,"links":4598},[4599,4604,4609,4614,4615,4622,4628,4629,4630,4631,4636,4637,4638,4639,4640,4641,4642,4643,4646,4656,4663],{"id":889,"depth":3619,"text":890,"children":4600},[4601,4602,4603],{"id":911,"depth":3636,"text":912},{"id":999,"depth":3636,"text":1000},{"id":1114,"depth":3636,"text":1115},{"id":1157,"depth":3619,"text":1158,"children":4605},[4606,4607,4608],{"id":1161,"depth":3636,"text":1162},{"id":1307,"depth":3636,"text":1308},{"id":1353,"depth":3636,"text":1354},{"id":1389,"depth":3619,"text":1390,"children":4610},[4611,4612,4613],{"id":1400,"depth":3636,"text":1401},{"id":1467,"depth":3636,"text":1468},{"id":1501,"depth":3636,"text":1502},{"id":1537,"depth":3619,"text":1538},{"id":1681,"depth":3619,"text":1682,"children":4616},[4617,4618,4619,4620,4621],{"id":1685,"depth":3636,"text":1686},{"id":1700,"depth":3636,"text":1701},{"id":1744,"depth":3636,"text":1745},{"id":1798,"depth":3636,"text":1799},{"id":2092,"depth":3636,"text":2093},{"id":2137,"depth":3619,"text":2138,"children":4623},[4624,4625,4626,4627],{"id":2141,"depth":3636,"text":2142},{"id":2199,"depth":3636,"text":2200},{"id":2262,"depth":3636,"text":2263},{"id":2281,"depth":3636,"text":2282},{"id":2293,"depth":3619,"text":2294},{"id":2522,"depth":3619,"text":2523},{"id":2753,"depth":3619,"text":2754},{"id":2826,"depth":3619,"text":2827,"children":4632},[4633,4634,4635],{"id":2839,"depth":3636,"text":2840},{"id":2887,"depth":3636,"text":2888},{"id":2906,"depth":3636,"text":2907},{"id":3010,"depth":3619,"text":3011},{"id":3085,"depth":3619,"text":3086},{"id":3237,"depth":3619,"text":3238},{"id":3289,"depth":3619,"text":3290},{"id":3408,"depth":3619,"text":3409},{"id":3486,"depth":3619,"text":3487},{"id":3580,"depth":3619,"text":3581},{"id":3929,"depth":3619,"text":3930,"children":4644},[4645],{"id":4058,"depth":3636,"text":4059},{"id":4079,"depth":3619,"text":4080,"children":4647},[4648,4649,4650,4651,4652,4653,4654,4655],{"id":4086,"depth":3636,"text":4087},{"id":4143,"depth":3636,"text":4144},{"id":4161,"depth":3636,"text":4162},{"id":4181,"depth":3636,"text":4182},{"id":4217,"depth":3636,"text":4218},{"id":4242,"depth":3636,"text":4243},{"id":4264,"depth":3636,"text":4265},{"id":4337,"depth":3636,"text":4338},{"id":4377,"depth":3619,"text":4378,"children":4657},[4658,4659,4660,4661,4662],{"id":4381,"depth":3636,"text":4382},{"id":4392,"depth":3636,"text":4393},{"id":4402,"depth":3636,"text":4403},{"id":4414,"depth":3636,"text":4415},{"id":4421,"depth":3636,"text":4422},{"id":4436,"depth":3619,"text":4437},"Defense-in-depth security architecture of the IAM service, covering credential storage, token lifecycle, session binding, anomaly detection, network isolation, input sanitization, rate limiting, threat model, attack scenarios, and known limitations.","md","i-lucide-lock-keyhole",{},null,"---\ntitle: Security\ndescription: Defense-in-depth security architecture of the IAM service, covering credential storage, token lifecycle, session binding, anomaly detection, network isolation, input sanitization, rate limiting, threat model, attack scenarios, and known limitations.\nicon: i-lucide-lock-keyhole\n---\n\nThe IAM service is built on two principles: [Zero Trust](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FZero_trust_security_model) and [Defense in Depth](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDefense_in_depth_(computing)). Every layer of the authentication stack assumes the layers around it can fail and adds its own protection. A compromised password does not grant a session. A stolen token does not survive rotation. A replayed request does not pass anomaly detection. No single control stands alone.\n\nThis page is a reference for every security mechanism in the service. It covers what each control does, how the controls interact, what attack scenarios they defend against, and where the design has trade-offs or limitations.\n\nIf you are working with machine-to-machine credentials, see [API token\nsecurity](\u002Fdocs\u002Fiam\u002Fessentials\u002Fapi\u002Fsecurity) for the dedicated security\naround API token verification, storage, management, and abuse controls.\n\n---\n\n## Network isolation\n\nThe IAM service is designed to run behind a [Backend for Frontend](\u002Fdocs\u002Fiam\u002Fessentials\u002Fbff) proxy inside a private [Docker](https:\u002F\u002Fwww.docker.com\u002F) network. The service is never exposed to the public internet. Browsers communicate with the BFF (a Nuxt server, an [h3](https:\u002F\u002Fh3.dev\u002F) server, or any HTTP proxy). The BFF communicates with the IAM service over the internal network. The database sits on the same internal network, unreachable from outside.\n\n### Network layers\n\n| Layer | Component | Exposure |\n|---|---|---|\n| **Edge** | Reverse Proxy | Public. Handles TLS termination, request body size limits, and upstream rate limiting. |\n| **Application** | BFF server ([auth-h3client](\u002Fdocs\u002Fauth-h3client)) | Semi-public. Receives browser requests through a proxy. |\n| **Service** | IAM service (`@riavzon\u002Fauth`) | Private. Accessible only from the BFF on the internal Docker network. |\n| **Data** | MySQL | Private. Accessible only from the IAM service. |\n\n### Container hardening\n\nThe Docker container runs with a minimal security profile:\n\n| Setting | Value | Purpose |\n|---|---|---|\n| `read_only` | `true` | Filesystem is read-only. The process cannot write to the container image. Logs and data use mounted volumes. |\n| `cap_drop` | `ALL` | All [Linux capabilities](https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fcapabilities.7.html) are dropped. The process cannot bind privileged ports, modify the kernel, or escalate privileges. |\n| `user` | `10001:10001` | Runs as a non-root user and group. |\n| `security_opt` | `no-new-privileges:true` | Prevents the process from gaining new privileges through `setuid` binaries or other mechanisms. |\n| `pids_limit` | `200` | Limits the number of processes the container can spawn. Prevents [fork bomb](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FFork_bomb) attacks. |\n\n### Service-to-service authentication\n\nTrust between the BFF and the IAM service is not implicit, even on a private network. The service supports two independent verification layers:\n\n**HMAC signatures.** Every request from the BFF carries four headers: a client identifier, a millisecond timestamp, a unique request ID, and an [HMAC-SHA256](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHMAC) signature computed from a shared secret. The IAM service recomputes the signature and compares it using `crypto.timingSafeEqual`. This prevents unauthorized containers on the same network from calling IAM endpoints. See [HMAC Authentication](\u002Fdocs\u002Fiam\u002Fessentials\u002Fhmac) for the full verification flow, replay detection, and clock-skew tolerance.\n\n**Mutual TLS (mTLS).** The [auth-h3client](\u002Fdocs\u002Fauth-h3client) supports configuration for a custom HTTPS agent. When configured, both the BFF and the IAM service present certificates, and each side verifies the other. \n\n::tip\nHMAC and mTLS are independent. You can enable either, both, or neither. HMAC alone is sufficient for most deployments where the Docker network is the trust boundary. Add mTLS when compliance requirements mandate encrypted internal traffic.\n::\n\n---\n\n## Credential storage\n\n### Password hashing\n\nAll passwords are hashed with [Argon2id](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FArgon2) using the [argon2](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fargon2) library. Argon2id is the [OWASP-recommended](https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FPassword_Storage_Cheat_Sheet.html) variant for password storage. It combines resistance to GPU-based parallel attacks (from Argon2d) with resistance to [side-channel attacks](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSide-channel_attack) (from Argon2i).\n\nA **pepper** is mixed into every hash as a secret key. The pepper is a long random hex string stored outside the database (in a secrets manager, environment variable, or [age](https:\u002F\u002Fage-encryption.org\u002F)-encrypted configuration file). If the database is leaked, an attacker cannot crack the hashes without also obtaining the pepper. Rotating the pepper invalidates all existing hashes, forcing a password reset for every user.\n\n| Parameter | Default | Purpose |\n|---|---|---|\n| `pepper` | Required | Secret key mixed into every hash via Argon2's `secret` parameter |\n| `hashLength` | `50` | Output hash length in bytes |\n| `timeCost` | `4` | Number of Argon2 iterations. Higher values increase brute-force resistance. |\n| `memoryCost` | `262144` | Memory usage in KiB (256 MiB). Higher values make GPU attacks more expensive. |\n\nThe verification function (`verifyPassword`) catches all errors internally and returns `false` on any failure. No error details are leaked to the caller or the client.\n\n::warning\nIncreasing `timeCost` and `memoryCost` makes hashing more expensive for attackers but also slows down your login and signup endpoints. Benchmark these values against your target response time before deploying. See [Configuration](\u002Fdocs\u002Fiam\u002Fconfiguration) for all password hashing options.\n::\n\n### Refresh token hashing\n\nRefresh tokens are 64-byte random strings generated with `crypto.randomBytes(64)` and hex-encoded to 128 characters. Before any database operation, the raw token is hashed with [SHA-256](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSHA-2). The database stores only the hash. The raw token is sent to the client exactly once, inside an `httpOnly` cookie, and never stored in plaintext on the server.\n\nThe `toDigestHex` utility checks whether the input is already a valid SHA-256 hex string before hashing. If the input passes a Zod `z.hash(\"sha256\")` check, it passes through unchanged. If not, SHA-256 is applied. After hashing, `ensureSha256Hex` runs a strict regex validation (`\u002F^[a-f0-9]{64}$\u002Fi`) and throws if the result is malformed. This double gate prevents truncated or corrupted hashes from reaching the database.\n\nSee [Refresh Tokens](\u002Fdocs\u002Fiam\u002Fessentials\u002Frefresh-tokens) for the full token lifecycle, storage schema, and rotation mechanics.\n\n### MFA code hashing\n\nOne-time MFA codes are 7-digit numeric strings generated with `crypto.randomInt(1_000_000, 9_999_999)`. The raw code is sent to the user's email. A SHA-256 hash of the code is stored in the `mfa_codes` table with a 7-minute expiry. The raw code never touches the database.\n\nWhen the user submits the code, the service hashes the submission and compares the hash against the stored value inside a database transaction. Expired or used codes are deleted atomically. The `token` foreign key on `mfa_codes` cascades to `refresh_tokens`, so revoking a user's refresh token automatically invalidates any pending MFA code tied to that session.\n\nSee [MFA](\u002Fdocs\u002Fiam\u002Fessentials\u002Fmfa) for the full OTP generation, delivery, verification, and rate limiting flow.\n\n---\n\n## Token lifecycle\n\nThe service uses a two-token architecture with a `canary_id` cookie for session binding.\n\n### Access tokens\n\nAccess tokens are short-lived [JWTs](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON_Web_Token) signed with a configurable algorithm (default HMAC). The service caches every generated token in an [LRU](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCache_replacement_policies#LRU) cache at creation time. During verification, the token must pass both cryptographic signature validation **and** exist in the cache. This design means you can revoke any valid access token instantly by deleting its cache entry, without waiting for natural expiry.\n\nThe payload includes `visitor` (the device binding), `roles`, `sub` (user ID), `jti` (unique token ID generated with `crypto.randomUUID()`), `iat`, and `exp`. Additional custom claims can be merged via `jwt.access_tokens.payload` in the [configuration](\u002Fdocs\u002Fiam\u002Fconfiguration).\n\nSee [Access Tokens](\u002Fdocs\u002Fiam\u002Fessentials\u002Faccess-tokens) for cache management and verification semantics.\n\n### Refresh tokens\n\nRefresh tokens are opaque random strings stored hashed in MySQL. Each token row carries a `usage_count` column that is the core of the single-use enforcement system. A fresh token starts at `0`, gets atomically incremented to `1` on consumption, and any second use triggers a kill switch that revokes every active session for that user.\n\nThe atomic consumption happens in a single `UPDATE` inside a database transaction: it increments `usage_count` by one, but only if the token exists, is valid, has never been consumed, and has not expired. All four conditions must pass for the row to be affected. If no row is affected, the function investigates why.\n\nSee [Refresh Tokens](\u002Fdocs\u002Fiam\u002Fessentials\u002Frefresh-tokens) for the full schema, consumption flow, rotation helpers, and the reuse detection walkthrough.\n\n### Canary cookie\n\nThe `canary_id` cookie is a 64-character hex string generated by the [Bot Detector](\u002Fdocs\u002Fbot-detection\u002Fgetting-started) middleware on first contact. It is set as an `httpOnly`, `Secure`, `SameSite=Lax` cookie with a 90-day TTL. The value is used as the primary key in the `visitors` table, which stores geo, device, and browser fingerprint data for the device.\n\nEvery refresh token operation compares the incoming `canary_id` against the stored value. A mismatch means the request is arriving from a different browser or device than the one that established the session, and adaptive MFA is triggered. See [Anomaly Detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies) check 2 for the exact comparison logic.\n\n---\n\n## Cookie security\n\nAll cookies set by the service use the strictest available flags:\n\n| Cookie | Purpose | Flags |\n|---|---|---|\n| `session` | Raw refresh token | `httpOnly`, `Secure`, `SameSite=Strict`, `Path=\u002F`, domain from config |\n| `iat` | Token issued-at timestamp | `httpOnly`, `Secure`, `SameSite=Strict`, `Path=\u002F` |\n| `canary_id` | Device fingerprint binding | `httpOnly`, `Secure`, `SameSite=Lax`, 90-day TTL |\n\nThe `httpOnly` flag prevents JavaScript from reading the cookie, eliminating [XSS](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCross-site_scripting)-based token theft. The `SameSite=Strict` policy on the `session` cookie prevents [CSRF](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCross-site_request_forgery) attacks, the browser will not include the cookie on any cross-origin request, whether initiated by a form submission, a redirect, or an `XMLHttpRequest`.\n\nThe `makeCookie` utility also supports the `__Host-` and `__Secure-` [cookie prefixes](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FReference\u002FHeaders\u002FSet-Cookie#cookie_prefixes). When a cookie name starts with `__Host-`, the utility forces `Secure=true`, `Path=\u002F`, and removes the domain attribute. When the name starts with `__Secure-`, it forces `Secure=true`.\n\n---\n\n## Input validation and sanitization\n\n### Content-type enforcement\n\nEvery POST route requires `Content-Type: application\u002Fjson`. The `validateContentType` middleware rejects requests with any other content type with HTTP 403. This prevents multipart, URL-encoded, or XML payloads from bypassing JSON-level input validation.\n\n### Request body limits\n\nAll routes set an explicit body size limit via `express.json()`:\n\n| Route group | Limit |\n|---|---|\n| Signup, login, MFA, password reset | 1 KB |\n| OAuth profile | 4 KB |\n\nRequests with an empty body are rejected with HTTP 403 before JSON parsing begins.\n\n### Zod schema validation\n\nEvery user-supplied field passes through a [Zod](https:\u002F\u002Fzod.dev\u002F) schema before any business logic runs. The schemas enforce type, length, and pattern constraints:\n\n| Field | Constraints |\n|---|---|\n| Email | 10 to 80 characters, strict regex pattern (no double dots, valid DNS segments), domain MX validation |\n| Password | 12 to 64 characters, must contain at least one lowercase, one uppercase, one digit, and one special character |\n| Name | 2 to 72 characters, letters only, 1 to 4 space-separated names |\n\nWhen validation fails, the service returns HTTP 400 with field-level error messages. The error response never reveals which specific condition failed in a way that would help an attacker enumerate valid inputs.\n\n### XSS sanitization pipeline\n\nAll user-supplied strings pass through a stage of sanitization pipeline before they reach the database. The pipeline is designed to catch layered encoding attacks, where an attacker nests multiple layers of URI encoding, HTML entities, or Unicode substitutions to bypass single-pass filters.\n\n::steps{level=\"4\"}\n\n#### Length guard\n\nBefore any processing, the sanitizer rejects input longer than `htmlSanitizer.maxAllowedInputLength` (default 50000). Oversized input throws rather than entering the loop. This is the hard cap on CPU cost for a single call.\n\n#### Unicode normalization\n\nThe input is normalized to [NFKC](https:\u002F\u002Funicode.org\u002Freports\u002Ftr15\u002F), which collapses visually similar characters to their canonical form Zero-width characters, soft hyphens, byte-order marks, and bidirectional override characters are stripped in the same pass. Halfwidth and\nfullwidth ASCII characters (`U+FF01` through `U+FF5E`) are transliterated back to standard ASCII. This defeats payloads that hide tags inside fullwidth substitutions such as `\\uFF1C` for `\u003C`.\n\n#### Strict URI decode\n\n`decodeURIComponent` is called once inside a `try`\u002F`catch`. If the call throws (malformed percent-encoding like `%ZZ`), the input is rejected immediately and returned as an empty string with `htmlFound: true`. Legitimate input does not contain malformed URI sequences, so rejecting early keeps broken data out of the loop.\n\n#### Iterative decoding loop\n\nThe function alternates between `decodeURIComponent` and HTML entity decoding (via the [he](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fhe) library) in a loop. Each pass strips one layer of encoding. The loop runs until the output stabilizes (no change between iterations) or the `IrritationCount` limit (default 50) is reached. This catches payloads like `%253Cscript%253E`, which decodes to `%3Cscript%3E` on the first pass and `\u003Cscript>` on the second.\n\nIf the loop exceeds `IrritationCount` without stabilizing, the input is rejected entirely and returned as an empty string with the HTML detection flag set.\n\n\n#### Residual cleanup\n\nAfter the loop, zero-width characters are stripped again (the decoders may have reintroduced them) and any whitespace inside the bodies of surviving tag-like substrings is removed so that `\u003Cscr\\tipt>` cannot slip past the tag regex.\n\n#### Pattern detection\n\nThe decoded string is tested against three patterns:\n\n| Pattern | Catches |\n|---|---|\n| `\u002F\u003C\\s*\\\u002F?\\s*[A-Za-z][A-Za-z0-9-]*(?:\\s+[^>]*?)?\\s*>\u002Fi` | Any HTML tag |\n| `\u002Fon\\w+\\s*=\u002Fi` | Inline event handlers (`onclick=`, `onerror=`) |\n| `\u002Fjavascript\\s*:\u002Fi` | JavaScript protocol URIs |\n\nAny match sets `htmlFound: true`. That flag propagates to `validateZodSchema`, which calls `handleXSS` and bans the IP.\n\n#### sanitize-html pass\n\nThe string passes through `sanitize-html` with a strict configuration:\n- `allowedTags: []`,\n- `allowedAttributes: {}`,\n- `allowedIframeHostnames: []`,\n- `allowedSchemes: []`,\n- `allowProtocolRelative: false`, \n- `nestingLimit: 10`,\n- `nonTextTags: ['script', 'style', 'noscript', 'iframe', 'svg']`.\n\nThe `textFilter` re-runs the tag regex on text nodes, and `onOpenTag` records any tag name and attribute set that the sanitizer had to strip. If the string shrinks during this pass, `htmlFound` is set to `true` even if pattern detection did not trigger.\n\n#### Entity encoding\n\nThe final output is entity-encoded: `&`, `\u003C`, `>`, `\"`, `'`, backtick, and `${` are replaced with their entity or escaped equivalents. The backtick and template-literal escapes prevent injection into JavaScript template strings. The result is trimmed.\n\n::\n\nSee [XSS Protection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fxss) for the full pipeline details, Zod integration, and the automatic IP ban behavior.\n\n### Automatic IP banning on XSS detection\n\nWhen the Zod validation layer detects an `'HTML found'` error (emitted by the sanitization pipeline), the `validateZodSchema` function calls `handleXSS`. This function imports the [Bot Detector](\u002Fdocs\u002Fbot-detection) ban utilities and immediately:\n\n1. Bans the IP with the maximum score (equal to `banScore`, default 100)\n2. Updates the banned IP record in the bot detector database\n3. Marks the visitor as a bot\n\nThe response returns HTTP 403 with `{ banned: true }`. The attacker's IP is blocked from all subsequent requests.\n\n---\n\n## Session management\n\n### Token rotation on every use\n\nThe service enforces [refresh token rotation](https:\u002F\u002Fauth0.com\u002Fdocs\u002Fsecure\u002Ftokens\u002Frefresh-tokens\u002Frefresh-token-rotation) on every access. When the BFF calls `POST \u002Fauth\u002Fuser\u002Frefresh-session`, the controller:\n\n1. Runs the full [anomaly detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies) pipeline against the incoming request\n2. Consumes the current refresh token atomically\n3. Revokes the old token\n4. Generates a fresh refresh token with a new row\n5. Generates a fresh access token with a new `jti`\n6. Sets new `session` and `iat` cookies on the response\n\nThe old token is dead after step 2. The new token is the only valid credential for future requests. If the old token is presented a second time , the consumption function detects `usage_count > 0`, revokes all tokens for that user, and returns `{ valid: false, reason: 'Token already used' }`.\n\n### Reuse detection and the kill switch\n\nThe `usage_count` column is the backbone of the reuse detection system. Here is how it handles the two possible stolen-token scenarios:\n\n::steps{level=\"4\"}\n#### Legitimate user rotates first\n\nThe user's BFF rotates the token. `usage_count` goes from `0` to `1`. The attacker presents the now-consumed token. The consumption function finds `usage_count > 0`, revokes all tokens for that user, and returns an error. The attacker's session is dead. The user is also logged out (they must re-authenticate), but the attacker gained nothing.\n\n#### Attacker rotates first\n\nThe attacker uses the stolen token before the user does. `usage_count` goes from `0` to `1`. The attacker receives new tokens. When the user's BFF later presents the now-consumed token, the same reuse detection fires: all tokens for that user are revoked, including the ones the attacker just received. The attacker's session is immediately invalidated.\n\nIn parallel, the [anomaly detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies) pipeline notices the mismatch in `canary_id`, IP address, or device fingerprint on the attacker's request and triggers adaptive [MFA](\u002Fdocs\u002Fiam\u002Fessentials\u002Fmfa). The attacker must complete a 7-digit OTP challenge sent to the user's email to continue. Without access to the user's inbox, the attacker cannot use the rotated token.\n::\n\nSee [Refresh Tokens: Reuse Detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Frefresh-tokens#reuse-detection) for the database-level walkthrough of this flow.\n\n### Session lifetime enforcement\n\nEven with continuous rotation, sessions have a hard maximum lifetime. The `MAX_SESSION_LIFE` configuration defaults to 30 days and is checked during every rotation. The service compares `Date.now()` against `session_started_at`, which is set once at token creation and carried forward to each new token on rotation. If the session has exceeded the maximum lifetime, the token is revoked, cookies are cleared, and the user must log in again.\n\n### Concurrent session limits\n\nThe `maxAllowedSessionsPerUser` configuration (default 5) limits how many active refresh tokens a user can have simultaneously. When the anomaly detection pipeline counts valid tokens for a user and the count equals or exceeds this limit, adaptive MFA is triggered. This prevents an attacker from silently opening many sessions under a single account.\n\n---\n\n## Anomaly detection\n\nEvery time a refresh token is used, the `strangeThings()` function runs nine sequential checks against the request context. The checks query the `refresh_tokens`, `users`, and `visitors` tables in a single join, then compare the stored fingerprint against the incoming request. The first check that fails short-circuits the rest.\n\nEach failure carries a `reqMFA` flag. When `reqMFA` is `true`, the caller sends an OTP email challenge and the user can recover by proving their identity. When `reqMFA` is `false`, the request is a hard block: the token is revoked and the user must log in again from scratch.\n\n| # | Check | Trigger condition | Response | Rationale |\n|---|---|---|---|---|\n| 1 | Token validity | Token missing, revoked, or replayed (during rotation) | Hard block | No identity to challenge. Session is dead. |\n| 2 | Canary cookie | `canary_id` does not match stored value | MFA | New device. User can prove identity. |\n| 3 | Idle detection | `last_seen` older than 24 hours | MFA | Stale session. Re-verify before continuing. |\n| 4 | Session count | Active sessions >= `maxAllowedSessionsPerUser` | MFA | Too many devices. User picks which to keep. |\n| 5 | Rapid creation | More than 3 valid tokens in 10 minutes | Hard block | Automated behavior. Non-human actor. |\n| 6 | IP range | IP outside the stored range | MFA | Network change. User can confirm. |\n| 7 | Suspicion score | Bot detector score >= 25% of `banScore` | MFA | Bot-like behavior accumulating. Early challenge. |\n| 8 | Proxy\u002Fhosting | Request from proxy or hosting without allow flag | MFA | New infrastructure. One-time MFA gate. |\n| 9 | Fingerprint loop | Any geo or User-Agent field mismatch | MFA | Environment changed. User can re-verify. |\n\nAfter a successful MFA challenge, the user's `last_mfa_at` timestamp is updated. For the duration of `byPassAnomaliesFor` (default 3 hours), check 4 (session count) is skipped entirely. All other checks still run regardless of when MFA was last completed.\n\nSee [Anomaly Detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies) for the complete function signature, data lookup, per-check details, and the MFA bypass window mechanics.\n\n---\n\n## Multi-factor authentication\n\nThe service ships with two MFA mechanisms:\n\n**Adaptive MFA** is triggered automatically when anomaly detection returns `reqMFA: true`. The user does not initiate it. The service detects the anomaly, generates a 7-digit OTP, hashes it with SHA-256, stores the hash in the `mfa_codes` table with a 7-minute expiry, and sends the code to the user's email inside a [magic link](\u002Fdocs\u002Fiam\u002Fessentials\u002Fmagic-links).\n\n**Custom MFA** is triggered by your application for sensitive user actions (password change, fund transfer, account deletion). You call the provided initiation route, and the service handles code generation, email delivery, and verification identically to adaptive MFA.\n\nBoth mechanisms share the same verification function, which runs inside a database transaction:\n\n1. Rate-limit the IP, the JTI, and the submitted code hash through `uniLimiter`, `ipLimit`, and `usedJtiLimiter`\n2. Validate and sanitize the submitted code through `validateZodSchema`\n3. Hash the code with SHA-256\n4. `SELECT ... FOR UPDATE` the matching `mfa_codes` row by `jti`, `code_hash`, not expired, not used\n5. Atomically `DELETE` the row (prevents replay)\n6. Block the submitted code hash in the limiter for 10 minutes\n7. In a single `UPDATE users JOIN visitors`, set `users.last_mfa_at = UTC_TIMESTAMP()`, `users.visitor_id = currentVisitorId`, `visitors.proxy_allowed = 1`, and `visitors.hosting_allowed = 1`\n8. Block the JTI in the limiter for 20 minutes\n9. Commit the transaction\n10. Call `updateVisitors()` in the bot-detector to overwrite the stored fingerprint with the current request's geo and User-Agent data\n11. Verify the bound refresh token, then revoke it (or revoke every refresh token for the user when `revokeAllTokensOnSuccess` is true, used by the password reset flow)\n12. Issue a new access token with a fresh `randomUUID()` JTI and a new refresh token row, then set the `iat` and `session` cookies\n\nMFA verification is protected by rate limiters and consecutive failure caches that work together:\n\n| Layer | Key | Purpose |\n|---|---|---|\n| Union limiter | `{IP}_{JTI}` | Limits attempts per IP and verification session |\n| Code hash limiter | `{code_hash}` | Blocks a specific code after successful use (prevents replay) |\n| JTI limiter | `{JTI}` | Prevents reuse of a verified JTI |\n| Consecutive cache (hash) | `{code_hash}` | Tracks repeated submissions of the same wrong code (10 min) |\n| Consecutive cache (IP) | `{IP}` | Progressive slowdown on repeated failures from same IP (10 min) |\n| Consecutive cache (JTI) | `{JTI}` | Tracks failures per verification session (20 min) |\n\nSee [MFA](\u002Fdocs\u002Fiam\u002Fessentials\u002Fmfa) for the full lifecycle, OTP generation, email template, and verification flow.\n\n---\n\n## Magic link security\n\n[Magic links](\u002Fdocs\u002Fiam\u002Fessentials\u002Fmagic-links) are short-lived, single-use JWTs embedded as URL query parameters in emails. They are used for adaptive MFA, password reset, email update, and custom MFA flows.\n\nEach MFA link is signed with [HMAC-SHA512](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHMAC) using a dedicated secret key\n(`magic_links.jwt_secret_key`), separate from the main JWT secret. The\nJWT ID (`jti`) is built as `crypto.randomUUID()` concatenated with\n`crypto.randomBytes(64).toString('hex')`, producing a 36-character UUID\nfollowed by 128 hex characters. The extra entropy makes brute-force\nenumeration of active JTIs infeasible.\n\nEach magic link URL also carries a `random` query parameter:\n`crypto.randomBytes(128).toString('hex')`, a 256-character hex string.\nThe server keeps only the SHA-256 digest of that value inside the signed\nJWT payload as `randomHashed`. On verification, the middleware hashes the\nincoming `random`, loads `randomHashed` from the decoded JWT, and\ncompares the two digests using [timing-safe equality](https:\u002F\u002Fnodejs.org\u002Fapi\u002Fcrypto.html#cryptotimingsafeequabordera-b). The raw value is\nnever persisted, and the comparison is constant-time.\n\nLinks are cached in a server-side LRU store. When a link is consumed, the cache entry is removed, which means a verified link cannot be replayed even if the JWT itself has not expired. Cache misses (expired or evicted entries) fail verification immediately without attempting JWT decoding.\n\n::warning\nAlways use a separate `jwt_secret_key` for magic links. Never reuse the access-token or refresh-token secret. This limits the blast radius if one secret is compromised.\n::\n\n---\n\n## Rate limiting\n\nThe service uses [rate-limiter-flexible](https:\u002F\u002Fgithub.com\u002Fanimir\u002Fnode-rate-limiter-flexible) for all rate limiting. Every sensitive endpoint has its own named limiter group. The limiters store state in MySQL with in-memory mirrors for fast rejections.\n\n### Layered design\n\nA typical endpoint has three limiter layers:\n\n| Layer | Purpose |\n|---|---|\n| **Union limiter** | Combines a burst limiter (low points, short window) and a slow limiter (higher points, longer window). Both are consumed on every attempt. If either rejects, the request is blocked. |\n| **IP limiter** | Per-IP rate limit applied independently of the union. |\n| **Identity limiter** | Keyed on email, OAuth subject, token hash, or user ID depending on the endpoint. Prevents attacks targeting a single identity. |\n\n### Strike system\n\nOn top of the limiter layer, a consecutive failure cache tracks strikes per key in an LRU cache. When a key accumulates enough strikes (the `maxBans` threshold), the `guard` function permanently blocks the key via the limiter's `block()` method. Blocked keys are rejected immediately on subsequent requests without consuming limiter points.\n\n### Endpoint coverage\n\n| Endpoint | Limiters |\n|---|---|\n| `POST \u002Flogin` | Union (burst + slow), IP, email, composite (IP + email) |\n| `POST \u002Fsignup` | Union IP (burst + slow), union composite (burst + slow), email |\n| `POST \u002Fauth\u002FOAuth\u002F:provider` | Union IP (burst + slow), subject, composite (IP + subject) |\n| `POST \u002Fauth\u002Fuser\u002Frefresh-session` | Union access (burst + slow), union refresh (burst + slow), standalone refresh |\n| `POST \u002Fauth\u002Fforgot-password` | Union (burst + slow), IP, email |\n| MFA email sending | Union (burst + slow), IP, user ID, global email cap |\n| Link verification | Union (burst + slow) |\n| Temp POST routes | Union (burst + slow), IP |\n\nSee [Rate Limiting](\u002Fdocs\u002Fiam\u002Fessentials\u002Frate-limiting) for the full limiter configuration, the `guard` and `unionLimiter` API, and the MySQL storage pool setup.\n\n---\n\n## Bot detection\n\nThe [Bot Detector](\u002Fdocs\u002Fbot-detection\u002Fgetting-started) middleware runs before any authentication route. It scores every request based on IP reputation, header consistency, geolocation anomalies, and device fingerprint deviations. The score accumulates in the `visitors` table and feeds directly into anomaly detection (check 7).\n\nThe bot detector:\n\n- Generates the `canary_id` cookie on first contact\n- Resolves geolocation from the client IP via local [MMDB](https:\u002F\u002Fmaxmind.github.io\u002FMaxMind-DB\u002F) databases\n- Parses the `User-Agent` header into structured device, browser, and OS fields\n- Upserts the visitor record with all fingerprint data\n- Tracks [Tor](https:\u002F\u002Fwww.torproject.org\u002F) exit nodes and known hosting\u002Fproxy ranges\n- Maintains a `suspicious_activity_score` through the [checker pipeline](\u002Fdocs\u002Fbot-detection\u002Fcheckers)\n\nThe MFA threshold is deliberately low: anomaly check 7 triggers at 25% of `banScore` (default 25 out of 100). This means MFA challenges start well before the visitor reaches the ban threshold, giving legitimate users a chance to prove themselves while slowing down automated attacks.\n\nSee [Bot Detector Configuration](\u002Fdocs\u002Fbot-detection\u002Fconfiguration) for the full scoring model, checker options, and ban behavior.\n\n---\n\n## Device fingerprinting\n\nThe `getFingerPrint` middleware builds a composite device fingerprint from the incoming request. It is a server-side fingerprint: no canvas hashing, no WebGL probing, no browser APIs. The data comes entirely from request headers and IP metadata.\n\nThe fingerprint combines:\n\n| Category | Fields |\n|---|---|\n| **Geolocation** | `country`, `countryCode`, `region`, `regionName`, `city`, `district`, `lat`, `lon`, `timezone`, `currency`, `isp`, `org`, `as_org` |\n| **Device** | `device`, `deviceVendor`, `deviceModel` |\n| **Browser** | `browser`, `browserType`, `browserVersion`, `os` |\n| **Network** | `proxy`, `hosting`, `ipAddress` |\n\nThe anomaly detection fingerprint loop (check 9) compares each incoming field against the stored visitor record. The comparison skips any field where either side is `null`, `undefined`, or the string `'unknown'`. A single mismatch on a populated field triggers MFA.\n\nSee [Fingerprinting](\u002Fdocs\u002Fiam\u002Fessentials\u002Ffingerprinting) for the full field reference and how fingerprint data flows through login, rotation, and MFA.\n\n---\n\n## Trusted devices on login\n\nThe `trustUserDeviceOnAuth` configuration option controls whether the login controller re-baselines the user's visitor record. When set to `true`, a successful login calls `trustVisitor()` to:\n\n1. Point `users.visitor_id` at the visitor row associated with the current `canary_id`\n2. Overwrite the visitor fingerprint with the current request's geo and User-Agent data\n\nThis means the stored fingerprint always reflects the device the user most recently logged in from. Without this option, the fingerprint stays fixed to the initial device, which causes false positives when users legitimately switch devices.\n\n::warning\nWhen `trustUserDeviceOnAuth` is `true`, a successful login marks the device as trusted and skips adaptive MFA when the `canary_id` changes on the next session. This reduces friction but weakens the anomaly detection layer. Set to `false` for high-security deployments.\n::\n\n---\n\n## Signup security\n\nThe signup flow runs a specific sequence of checks before creating an account:\n\n::steps{level=\"4\"}\n#### Content-type and rate limiting\n\nThe request must have `Content-Type: application\u002Fjson`. IP-based burst and slow limiters run first, followed by composite key (IP + email) limiters.\n\n#### Input validation\n\nThe Zod schema validates `name`, `email`, `password`, `confirmedPassword`, `rememberUser`, and `termsConsent`. The `termsConsent` field must be the literal string `\"on\"`. If any field contains HTML, the visitor is banned immediately.\n\n#### Email domain validation\n\nThe service performs a DNS [MX record](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMX_record) lookup on the email domain. If the domain has no mail server, the signup is rejected.\n\n#### Disposable email detection\n\nThe email domain is checked against the [Shield Base](https:\u002F\u002Fgithub.com\u002FniceSh3ll\u002FshieldBase-cli) disposable email blocklist (stored in a local [LMDB](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLightning_Memory-Mapped_Database) database). Disposable domains are rejected.\n\n#### Password breach check\n\nThe password is checked against the [Have I Been Pwned](https:\u002F\u002Fhaveibeenpwned.com\u002FAPI\u002Fv3#PwnedPasswords) database using [k-anonymity](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FK-anonymity) (only the first 5 characters of the SHA-1 hash are sent to the API). During signup, a breached password **blocks** account creation. The user must choose a different password.\n\n#### Password hashing and account creation\n\nThe password is hashed with Argon2id + pepper, and the user record is created in a database transaction. If the email already exists, the service returns HTTP 409.\n::\n\nSee [Signup](\u002Fdocs\u002Fiam\u002Fessentials\u002Fsignup) for the full flow and response format.\n\n---\n\n## Login security\n\nThe login flow shares several checks with signup but has some differences:\n\n::steps{level=\"4\"}\n#### Rate limiting\n\nIP, email, and composite key (IP + email) rate limiters run before any database query.\n\n#### Input validation\n\nThe Zod schema validates `email` and `password` with the same pattern and length constraints as signup.\n\n#### User lookup\n\nThe service queries for a user with the submitted email. If no user is found, the response is `{ ok: false, error: 'Invalid email or password' }`. The same error message is returned for invalid passwords to prevent [user enumeration](https:\u002F\u002Fowasp.org\u002Fwww-project-web-security-testing-guide\u002Flatest\u002F4-Web_Application_Security_Testing\u002F03-Identity_Management_Testing\u002F04-Testing_for_Account_Enumeration_and_Guessable_User_Account).\n\n#### Password verification\n\nArgon2id verification runs with the stored hash and the pepper. The verification is internally constant-time (Argon2 handles this). On failure, the same generic error message is returned.\n\n#### Password breach check\n\nThe password is checked against Have I Been Pwned. During login, a breached password does **not** block the login. Instead, the response includes a `breached` warning message advising the user to change their password.\n\n#### Device trust and token issuance\n\nIf `trustUserDeviceOnAuth` is enabled, the device fingerprint is re-baselined. A new refresh token and access token are generated. Rate limiter state is reset for the compositeKey on success.\n::\n\nSee [Login](\u002Fdocs\u002Fiam\u002Fessentials\u002Flogin) for the full flow and response format.\n\n---\n\n## OAuth security\n\nOAuth social login providers are validated with Zod schemas. Each provider definition maps an incoming OAuth profile to the internal user schema. Providers can be defined with either a full custom Zod schema for complete control, or a field-type map shorthand that builds the schema automatically from type tokens (`'string'`, `'email'`, `'safeString'`, `'url'`, `'boolean'`, `'number'`, `'int'`, each optionally suffixed with `?` for optional fields).\n\nOAuth requests pass through three rate limiters:\n\n| Limiter | Key | Purpose |\n|---|---|---|\n| IP burst + slow | IP address | Prevents rapid OAuth attempts from the same IP |\n| Subject | OAuth provider ID | Prevents enumeration of valid OAuth subjects |\n| Composite | IP + subject | Fine-grained control combining both dimensions |\n\nIf the OAuth profile passes schema validation and the email is not already registered with a password-based account, the user is created. Duplicate emails return HTTP 409.\n\nSee [OAuth](\u002Fdocs\u002Fiam\u002Fessentials\u002Foauth) for the full provider definition and validation flow.\n\n---\n\n## Security headers\n\nThe service registers [Helmet](https:\u002F\u002Fhelmetjs.github.io\u002F) in `service.ts` with the following explicit overrides:\n```ts\nhelmet({\n  crossOriginEmbedderPolicy: true,\n  xFrameOptions: { action: 'deny' },\n  contentSecurityPolicy: {\n    directives: { 'frame-ancestors': [\"'none'\"] }\n  },\n  referrerPolicy: { policy: 'origin' }\n})\n```\n| Header | Value | Source |\n|---|---|---|\n| `Content-Security-Policy` | Helmet defaults merged with frame-ancestors 'none' | Custom + default |\n| `Strict-Transport-Security` | `max-age=15552000; includeSubDomains` | Helmet default |\n| `X-Content-Type-Options` | `nosniff` | Helmet default |\n| `X-Frame-Options` | DENY | Custom override |\n| `Cross-Origin-Embedder-Policy` | require-corp | Custom override |\n| `Cross-Origin-Opener-Policy` | same-origin | Helmet default |\n| `Cross-Origin-Resource-Policy` | same-origin | Helmet default |\n| `Referrer-Policy` | origin | Custom override |\n| `Origin-Agent-Cluster` | ?1 | Helmet default |\n| `X-DNS-Prefetch-Control` | off | Helmet default |\n| `X-Download-Options` | noopen | Helmet default |\n| `X-Permitted-Cross-Domain-Policies` | none | Helmet default |\n| `X-XSS-Protection` | 0 | Helmet default (v4+) |\n\n\n::warning\nHelmet v4 and later emit X-XSS-Protection: 0 on purpose. The legacy\nbrowser XSS filter has been removed from modern browsers, and when it was\nactive it introduced cross-site leak bugs. Real XSS mitigation in this\nservice comes from the sanitization pipeline\nand the strict CSP, not this header.\n::\n\n---\n\n## Structured logging\n\nThe service uses [Pino](https:\u002F\u002Fgetpino.io\u002F) for structured JSON logging. Every security event is logged with relevant context at the appropriate level:\n\n| Event | Level | Context |\n|---|---|---|\n| Login success | `info` | User ID, IP |\n| Login failure | `warn` | IP, reason |\n| Anomaly detection trigger | `warn` | Check number, reason, IP, visitor ID |\n| Rate limit hit | `warn` | Endpoint, IP, key |\n| XSS attempt | `warn` | IP, payload summary, ban status |\n| Token revocation | `info` | User ID, reason |\n| MFA code sent | `info` | User ID |\n| MFA code verified | `info` | User ID |\n\nError responses never include stack traces, internal error messages, or database details. All server-side errors return a generic message to the client.\n\n### Configuration encryption\n\nSensitive configuration (database credentials, JWT secrets, HMAC shared secrets, pepper, email API keys) is stored in an [age](https:\u002F\u002Fage-encryption.org\u002F)-encrypted file (`config.json.age`). The decryption key is mounted as a Docker secret. The `decrypt.sh` script decrypts the file at container startup, the service loads it, and then deletes it from disk. \n\n---\n\n## Attack scenarios\n\nThis section walks through specific attack scenarios and how the service's security layers respond.\n\n### Stolen refresh token\n\nThe most plausible attack vector. An attacker obtains a valid refresh token through phishing.\n\n**If the user rotates first:** The user's BFF rotates the token before the attacker can use it. `usage_count` goes from `0` to `1`. The attacker presents the now-consumed token. `consumeAndVerifyRefreshToken` detects `usage_count > 0` and revokes all tokens for that user. The attacker's attempt fails. The user is logged out and must re-authenticate, but the session is safe.\n\n**If the attacker rotates first:** The attacker uses the token before the user. The [anomaly detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies) pipeline runs on the attacker's request. Even if the attacker somehow has the `canary_id` cookie, they still face check 6, check 8, and check 9 (the [fingerprint consistency loop](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies#device-fingerprint-consistency-loop)). Any single mismatch on a populated field triggers adaptive MFA. The attacker must then complete a 7-digit OTP challenge sent to the user's email. Meanwhile, when the user's BFF presents the now-consumed token, reuse detection fires and revokes all sessions, including the attacker's newly issued tokens.\n\n::note\nThe attacker must obtain **four** things to succeed: the refresh token, the `canary_id` cookie, a matching device fingerprint (same IP range, geo, browser, OS, device), and access to the user's email. Phishing can realistically obtain the token and the cookie. Spoofing the full fingerprint requires a residential proxy in the same IP range and an identical User-Agent. Completing the MFA challenge requires compromising the user's inbox. Each layer is an independent barrier.\n::\n\n### Replay attack during rotation\n\nAn attacker captures a rotation request (token + cookies) and replays it. The first use increments `usage_count` to `1`. The replay finds `usage_count > 0`. The `strangeThings` function detects the reuse and blocks the request. If the full consumption path runs, all sessions for the user are revoked.\n\n### Cross-site scripting (XSS)\n\nAn attacker makes a payload containing obfuscated HTML or JavaScript and submits it through a form field (signup name, email, etc.). The input passes through the six-stage sanitization pipeline. The iterative decoding loop strips nested encodings. The pattern detection catches tags, event handlers, and JavaScript URIs. The Zod validation layer detects the `'HTML found'` error, calls `handleXSS`, and bans the attacker's IP with the maximum bot detector score. The response is HTTP 403 with `{ banned: true }`.\n\nSee [XSS Protection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fxss) for the full pipeline.\n\n### Credential stuffing\n\nAn attacker uses a list of leaked email\u002Fpassword pairs to attempt mass logins. The login endpoint is protected by four independent rate limiters:\n\n1. **IP burst limiter** catches rapid-fire attempts from a single IP\n2. **IP slow limiter** catches distributed attempts spread over time\n3. **Email limiter** prevents attacks targeting a single account\n4. **Composite key limiter** (IP + email) catches the specific pairing\n\nAfter consecutive failures, the strike system permanently blocks the key. The attacker receives HTTP 429.\n\n### Denial-of-service via token reuse\n\nAn attacker obtains a valid refresh token and uses a residential proxy in the victim's IP range, a spoofed User-Agent that matches the victim's browser, OS, and device fields, and the victim's `canary_id` cookie. With all fingerprint fields aligned, the attacker's request passes the [anomaly detection](\u002Fdocs\u002Fiam\u002Fessentials\u002Fanomalies) pipeline cleanly. They send a single request.\n\nThe service rotates the token for the attacker. When the legitimate user makes their next request, the system detects reuse and triggers the kill switch: all tokens for the user are revoked. The user is logged out.\n\nThe attacker cannot hijack the session because the rotated token triggers MFA on the next anomaly, and the kill switch fires on reuse. The attack is also self-limiting: once the legitimate user logs in again with a fresh password authentication from a trusted device, the service issues a new refresh token and re-baselines the visitor fingerprint. The attacker's previously stolen token, `canary_id`, and fingerprint data are now invalid. To repeat the attack, the attacker would need to steal the new credentials all over again.\n\nThe attack can persist only as long as the attacker holds valid, current credentials. A single clean login by the user breaks the cycle. This is a deliberate trade-off: the system accepts the risk of temporary user lockout to maintain zero tolerance for stolen tokens.\n\n### Device compromise (malware\u002FRAT)\n\nIf an attacker compromises the victim's physical endpoint via malware or a [Remote Access Trojan](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FTrojan_horse_(computing)#Remote_access), they operate as the user. They inherit the correct IP, cookies, and device fingerprint. The anomaly detector passes the request. The kill switch does not trigger.\n\nThe service is phishing-resistant but not malware-proof. Defense against endpoint compromise requires hardware-bound [FIDO2\u002FWebAuthn](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebAuthn) factors, which are not currently enforced in the default configuration.\n\n### CPU exhaustion via sanitization\n\nAn attacker crafts a payload containing deeply nested encoded entities (for example, megabytes of `%252525...`). The iterative decoding loop in the sanitization pipeline processes each layer, potentially blocking the Node.js event loop.\n\nMitigation is layered:\n\n| Layer | Control |\n|---|---|\n| **Proxy** | Should enforces strict request body size limits at the reverse proxy level |\n| **Express** | `express.json()` body size limits (1 KB for most routes) |\n| **Sanitizer** | `IrritationCount` (default 50) caps the decoding loop iterations |\n| **Sanitizer** | `maxAllowedInputLength` rejects oversized inputs before sanitization begins |\n\n### Database compromise\n\nIf an attacker gains read access to the database:\n\n- **Passwords** are hashed with Argon2id + pepper. Without the pepper, the hashes are not crackable.\n- **Refresh tokens** are stored as SHA-256 hashes with `usage_count` and `valid` flags. The attacker cannot reuse them without the unhashed token, which exists only in the user's cookie.\n- **MFA codes** are stored as SHA-256 hashes with a 7-minute expiry. The attacker cannot reverse the hash to obtain the code.\n- **Magic link JWTs** are signed with a secret stored in the encrypted configuration file, not in the database.\n\n---\n\n## Limitations\n\n### No hardware-bound authentication\n\nThe service does not enforce [FIDO2\u002FWebAuthn](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebAuthn) or hardware security keys. All authentication factors (password, OTP code, magic link) can be phished or intercepted by malware on the user's device. Adding WebAuthn as an optional second factor is a planned enhancement.\n\n### False positives from network changes\n\nThe default anomaly detection configuration is tuned for high security. Legitimate users who travel, switch networks, or use VPNs may trigger false-positive anomaly detections, resulting in MFA challenges or session termination. The `byPassAnomaliesFor` window (default 3 hours) mitigates this after a successful MFA challenge, but frequent IP changes within a single session can still cause repeated challenges.\n\n### Single-threaded sanitization\n\nThe XSS sanitization loop runs synchronously on the Node.js event loop. While the `IrritationCount` and `maxAllowedInputLength` limits prevent worst-case scenarios, a crafted payload near the limits can still cause brief latency spikes. The upstream reverse proxy is the primary defense against volumetric attacks targeting this surface.\n\n### No per-session encryption at rest\n\nRefresh tokens are hashed (not encrypted) in the database. SHA-256 is a one-way function and cannot be reversed, but it is not a keyed operation. If an attacker obtains both the database and a valid raw token (for example, from a browser memory dump), they can verify which row the token maps to. Full encryption at rest with a key management service would add a layer but is not currently implemented.\n\n### Email-only MFA\n\nThe service currently supports email-based OTP as the only MFA channel. This means MFA security is bounded by the security of the user's email account. If the email account is compromised, the attacker can complete MFA challenges. Support for TOTP ([RFC 6238](https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6238)) and push-based authentication are potential future additions.\n\n---\n\n## Summary\n\n| Layer | Controls |\n|---|---|\n| **Edge** | Caddy with TLS termination, request body size limits, upstream rate limiting |\n| **Transport** | HMAC-SHA256 inter-service signatures, optional mTLS, replay detection via nonce cache |\n| **Container** | Read-only filesystem, all capabilities dropped, non-root user, PID limits |\n| **Input** | Content-type enforcement, body size limits, empty body rejection |\n| **Strings** | Six-stage XSS sanitization, Zod schema validation, automatic IP ban on HTML detection |\n| **Passwords** | Argon2id with pepper, Have I Been Pwned breach check |\n| **Tokens** | Short-lived JWTs with LRU cache binding, refresh tokens hashed in MySQL, single-use enforcement |\n| **Sessions** | Canary cookie binding, nine-check anomaly detection on every use, 30-day hard session limit |\n| **MFA** | 7-digit OTP with 7-minute expiry, SHA-256 hashed storage, cascading revocation via foreign keys |\n| **Rate limiting** | Per-endpoint union limiters (burst + slow), IP\u002Femail\u002Fcomposite identity limiters, strike-based permanent blocking |\n| **Bot detection** | IP reputation scoring, Tor\u002Fproxy\u002Fhosting detection, device fingerprinting, configurable ban thresholds |\n| **Email** | Disposable email blocklist, DNS MX record validation, domain verification |\n| **Headers** | Helmet (CSP, HSTS, X-Frame-Options, X-Content-Type-Options) |\n| **Logging** | Structured JSON logging with Pino, security event tracking, no PII leakage in error responses |\n| **Secrets** | age-encrypted configuration, Docker secrets, pepper stored outside database |\n",{"title":38,"description":4664},"sghhUTNQY_PdXrd1KrhYCAaPQn6itx5ngkWpvkm13OQ",[4673,4674],{"title":38,"path":214,"stem":215,"children":-1},{"title":225,"path":226,"stem":227,"children":-1},{"id":851,"title":38,"body":4676,"description":4664,"extension":4665,"icon":4666,"meta":7347,"module":4668,"navigation":8,"path":217,"rawbody":4669,"seo":7348,"stem":218,"__hash__":4671},{"type":853,"value":4677,"toc":7280},[4678,4686,4688,4692,4694,4696,4706,4708,4766,4768,4770,4852,4854,4856,4867,4873,4877,4879,4881,4883,4897,4904,4966,4972,4982,4984,4993,5003,5007,5009,5015,5023,5027,5029,5031,5035,5037,5047,5067,5071,5073,5081,5087,5091,5093,5107,5113,5115,5117,5119,5185,5201,5222,5224,5226,5228,5234,5236,5240,5264,5266,5268,5273,5303,5305,5307,5309,5487,5491,5493,5503,5513,5517,5519,5521,5523,5530,5552,5558,5560,5564,5596,5600,5602,5610,5612,5616,5618,5620,5630,5642,5776,5782,5786,5788,5790,5792,5802,5806,5808,5872,5874,5948,5952,5954,5956,5960,5973,5988,5990,5996,5998,6000,6005,6007,6009,6045,6047,6055,6057,6127,6135,6137,6139,6145,6147,6175,6179,6183,6185,6187,6191,6193,6283,6291,6295,6297,6299,6307,6317,6319,6331,6333,6335,6337,6398,6402,6404,6406,6408,6449,6453,6455,6457,6475,6477,6515,6517,6521,6523,6525,6532,6634,6782,6786,6788,6790,6795,6889,6891,6893,6902,6904,6906,6908,6910,6912,6926,6936,6944,6946,6956,6958,6966,6970,6972,6974,6992,6994,6996,7002,7004,7008,7010,7012,7017,7022,7024,7028,7030,7080,7082,7084,7106,7108,7110,7112,7117,7119,7123,7125,7131,7133,7135,7137,7142,7144,7146,7278],[856,4679,858,4680,866,4683,872],{},[860,4681,865],{"href":862,"rel":4682},[864],[860,4684,871],{"href":869,"rel":4685},[864],[856,4687,875],{},[856,4689,878,4690,882],{},[860,4691,881],{"href":214},[884,4693],{},[887,4695,890],{"id":889},[856,4697,893,4698,896,4700,902,4703,908],{},[860,4699,131],{"href":132},[860,4701,901],{"href":899,"rel":4702},[864],[860,4704,907],{"href":905,"rel":4705},[864],[907,4707,912],{"id":911},[914,4709,4710,4720],{},[917,4711,4712],{},[920,4713,4714,4716,4718],{},[923,4715,925],{},[923,4717,928],{},[923,4719,931],{},[933,4721,4722,4732,4744,4756],{},[920,4723,4724,4728,4730],{},[938,4725,4726],{},[941,4727,943],{},[938,4729,946],{},[938,4731,949],{},[920,4733,4734,4738,4742],{},[938,4735,4736],{},[941,4737,956],{},[938,4739,959,4740,963],{},[860,4741,962],{"href":22},[938,4743,966],{},[920,4745,4746,4750,4754],{},[938,4747,4748],{},[941,4749,973],{},[938,4751,976,4752,963],{},[978,4753,980],{},[938,4755,983],{},[920,4757,4758,4762,4764],{},[938,4759,4760],{},[941,4761,990],{},[938,4763,993],{},[938,4765,996],{},[907,4767,1000],{"id":999},[856,4769,1003],{},[914,4771,4772,4782],{},[917,4773,4774],{},[920,4775,4776,4778,4780],{},[923,4777,1012],{},[923,4779,1015],{},[923,4781,1018],{},[933,4783,4784,4796,4811,4823,4837],{},[920,4785,4786,4790,4794],{},[938,4787,4788],{},[978,4789,1027],{},[938,4791,4792],{},[978,4793,1032],{},[938,4795,1035],{},[920,4797,4798,4802,4806],{},[938,4799,4800],{},[978,4801,1042],{},[938,4803,4804],{},[978,4805,1047],{},[938,4807,1050,4808,1056],{},[860,4809,1055],{"href":1053,"rel":4810},[864],[920,4812,4813,4817,4821],{},[938,4814,4815],{},[978,4816,1063],{},[938,4818,4819],{},[978,4820,1068],{},[938,4822,1071],{},[920,4824,4825,4829,4833],{},[938,4826,4827],{},[978,4828,1078],{},[938,4830,4831],{},[978,4832,1083],{},[938,4834,1086,4835,1090],{},[978,4836,1089],{},[920,4838,4839,4843,4847],{},[938,4840,4841],{},[978,4842,1097],{},[938,4844,4845],{},[978,4846,1102],{},[938,4848,1105,4849,1111],{},[860,4850,1110],{"href":1108,"rel":4851},[864],[907,4853,1115],{"id":1114},[856,4855,1118],{},[856,4857,4858,1124,4860,1130,4863,1134,4865,1137],{},[941,4859,1123],{},[860,4861,1129],{"href":1127,"rel":4862},[864],[978,4864,1133],{},[860,4866,135],{"href":136},[856,4868,4869,1143,4871,1146],{},[941,4870,1142],{},[860,4872,962],{"href":22},[1148,4874,4875],{},[856,4876,1152],{},[884,4878],{},[887,4880,1158],{"id":1157},[907,4882,1162],{"id":1161},[856,4884,1165,4885,1171,4888,1177,4891,1183,4894,1189],{},[860,4886,1170],{"href":1168,"rel":4887},[864],[860,4889,1176],{"href":1174,"rel":4890},[864],[860,4892,1182],{"href":1180,"rel":4893},[864],[860,4895,1188],{"href":1186,"rel":4896},[864],[856,4898,1192,4899,1196,4901,1202],{},[941,4900,1195],{},[860,4902,1201],{"href":1199,"rel":4903},[864],[914,4905,4906,4916],{},[917,4907,4908],{},[920,4909,4910,4912,4914],{},[923,4911,1211],{},[923,4913,1214],{},[923,4915,1018],{},[933,4917,4918,4930,4942,4954],{},[920,4919,4920,4924,4926],{},[938,4921,4922],{},[978,4923,1195],{},[938,4925,1227],{},[938,4927,1230,4928,1234],{},[978,4929,1233],{},[920,4931,4932,4936,4940],{},[938,4933,4934],{},[978,4935,1241],{},[938,4937,4938],{},[978,4939,1246],{},[938,4941,1249],{},[920,4943,4944,4948,4952],{},[938,4945,4946],{},[978,4947,1256],{},[938,4949,4950],{},[978,4951,1261],{},[938,4953,1264],{},[920,4955,4956,4960,4964],{},[938,4957,4958],{},[978,4959,1271],{},[938,4961,4962],{},[978,4963,1276],{},[938,4965,1279],{},[856,4967,1282,4968,1286,4970,1290],{},[978,4969,1285],{},[978,4971,1289],{},[1292,4973,4974],{},[856,4975,1296,4976,866,4978,1301,4980,1304],{},[978,4977,1256],{},[978,4979,1271],{},[860,4981,237],{"href":238},[907,4983,1308],{"id":1307},[856,4985,1311,4986,1315,4988,1321,4991,1325],{},[978,4987,1314],{},[860,4989,1320],{"href":1318,"rel":4990},[864],[978,4992,1324],{},[856,4994,1328,4995,1332,4997,1336,4999,1340,5001,1344],{},[978,4996,1331],{},[978,4998,1335],{},[978,5000,1339],{},[978,5002,1343],{},[856,5004,1347,5005,1350],{},[860,5006,91],{"href":92},[907,5008,1354],{"id":1353},[856,5010,1357,5011,1361,5013,1365],{},[978,5012,1360],{},[978,5014,1364],{},[856,5016,1368,5017,1372,5019,1375,5021,1379],{},[978,5018,1371],{},[978,5020,1364],{},[978,5022,1378],{},[856,5024,1347,5025,1384],{},[860,5026,123],{"href":124},[884,5028],{},[887,5030,1390],{"id":1389},[856,5032,1393,5033,1397],{},[978,5034,1396],{},[907,5036,1401],{"id":1400},[856,5038,1404,5039,1410,5042,1416,5045,1420],{},[860,5040,1409],{"href":1407,"rel":5041},[864],[860,5043,1415],{"href":1413,"rel":5044},[864],[941,5046,1419],{},[856,5048,1423,5049,1427,5051,1431,5053,1435,5055,1439,5057,1443,5059,1447,5061,1451,5063,1455,5065,1459],{},[978,5050,1426],{},[978,5052,1430],{},[978,5054,1434],{},[978,5056,1438],{},[978,5058,1442],{},[978,5060,1446],{},[978,5062,1450],{},[978,5064,1454],{},[860,5066,1458],{"href":238},[856,5068,1347,5069,1464],{},[860,5070,87],{"href":88},[907,5072,1468],{"id":1467},[856,5074,1471,5075,1475,5077,1479,5079,1483],{},[978,5076,1474],{},[978,5078,1478],{},[978,5080,1482],{},[856,5082,1486,5083,1490,5085,1493],{},[978,5084,1489],{},[978,5086,1474],{},[856,5088,1347,5089,1498],{},[860,5090,91],{"href":92},[907,5092,1502],{"id":1501},[856,5094,1328,5095,1507,5097,1510,5099,1431,5101,1431,5103,1519,5105,1523],{},[978,5096,1396],{},[860,5098,399],{"href":404},[978,5100,1324],{},[978,5102,1515],{},[978,5104,1518],{},[978,5106,1522],{},[856,5108,1526,5109,1529,5111,1532],{},[978,5110,1396],{},[860,5112,95],{"href":96},[884,5114],{},[887,5116,1538],{"id":1537},[856,5118,1541],{},[914,5120,5121,5131],{},[917,5122,5123],{},[920,5124,5125,5127,5129],{},[923,5126,1550],{},[923,5128,1018],{},[923,5130,1555],{},[933,5132,5133,5151,5169],{},[920,5134,5135,5139,5141],{},[938,5136,5137],{},[978,5138,1564],{},[938,5140,1567],{},[938,5142,5143,1431,5145,1431,5147,1431,5149,1580],{},[978,5144,1324],{},[978,5146,1515],{},[978,5148,1576],{},[978,5150,1579],{},[920,5152,5153,5157,5159],{},[938,5154,5155],{},[978,5156,1446],{},[938,5158,1589],{},[938,5160,5161,1431,5163,1431,5165,1431,5167],{},[978,5162,1324],{},[978,5164,1515],{},[978,5166,1576],{},[978,5168,1579],{},[920,5170,5171,5175,5177],{},[938,5172,5173],{},[978,5174,1396],{},[938,5176,1608],{},[938,5178,5179,1431,5181,1431,5183,1617],{},[978,5180,1324],{},[978,5182,1515],{},[978,5184,1518],{},[856,5186,1328,5187,1622,5189,1628,5192,1631,5194,1634,5196,1640,5199,1459],{},[978,5188,1324],{},[860,5190,1627],{"href":1625,"rel":5191},[864],[978,5193,1576],{},[978,5195,1564],{},[860,5197,1639],{"href":1637,"rel":5198},[864],[978,5200,1643],{},[856,5202,1328,5203,1649,5205,866,5207,1656,5209,1662,5212,1665,5214,1431,5216,1671,5218,1674,5220,1459],{},[978,5204,1648],{},[978,5206,1652],{},[978,5208,1655],{},[860,5210,1661],{"href":1659,"rel":5211},[864],[978,5213,1652],{},[978,5215,1668],{},[978,5217,1579],{},[978,5219,1655],{},[978,5221,1668],{},[884,5223],{},[887,5225,1682],{"id":1681},[907,5227,1686],{"id":1685},[856,5229,1689,5230,1693,5232,1697],{},[978,5231,1692],{},[978,5233,1696],{},[907,5235,1701],{"id":1700},[856,5237,1704,5238,1708],{},[978,5239,1707],{},[914,5241,5242,5250],{},[917,5243,5244],{},[920,5245,5246,5248],{},[923,5247,1717],{},[923,5249,1720],{},[933,5251,5252,5258],{},[920,5253,5254,5256],{},[938,5255,1727],{},[938,5257,1730],{},[920,5259,5260,5262],{},[938,5261,1735],{},[938,5263,1738],{},[856,5265,1741],{},[907,5267,1745],{"id":1744},[856,5269,1748,5270,1754],{},[860,5271,1753],{"href":1751,"rel":5272},[864],[914,5274,5275,5283],{},[917,5276,5277],{},[920,5278,5279,5281],{},[923,5280,1763],{},[923,5282,1766],{},[933,5284,5285,5291,5297],{},[920,5286,5287,5289],{},[938,5288,1773],{},[938,5290,1776],{},[920,5292,5293,5295],{},[938,5294,1781],{},[938,5296,1784],{},[920,5298,5299,5301],{},[938,5300,1789],{},[938,5302,1792],{},[856,5304,1795],{},[907,5306,1799],{"id":1798},[856,5308,1802],{},[1804,5310,5311,5313,5317,5319,5332,5334,5346,5348,5363,5367,5369,5373,5375,5377,5417,5425,5427,5431,5461,5471,5473],{"level":1261},[1807,5312,1810],{"id":1809},[856,5314,1813,5315,1817],{},[978,5316,1816],{},[1807,5318,1821],{"id":1820},[856,5320,1824,5321,1830,5324,1834,5326,1838,5328,1842,5330,1459],{},[860,5322,1829],{"href":1827,"rel":5323},[864],[978,5325,1833],{},[978,5327,1837],{},[978,5329,1841],{},[978,5331,1845],{},[1807,5333,1849],{"id":1848},[856,5335,5336,1855,5338,1859,5340,1863,5342,1867,5344,1871],{},[978,5337,1854],{},[978,5339,1858],{},[978,5341,1862],{},[978,5343,1866],{},[978,5345,1870],{},[1807,5347,1875],{"id":1874},[856,5349,1878,5350,1881,5352,1887,5355,1891,5357,1895,5359,1899,5361,1903],{},[978,5351,1854],{},[860,5353,1886],{"href":1884,"rel":5354},[864],[978,5356,1890],{},[978,5358,1894],{},[978,5360,1898],{},[978,5362,1902],{},[856,5364,1906,5365,1909],{},[978,5366,1890],{},[1807,5368,1913],{"id":1912},[856,5370,1916,5371,1920],{},[978,5372,1919],{},[1807,5374,1924],{"id":1923},[856,5376,1927],{},[914,5378,5379,5387],{},[917,5380,5381],{},[920,5382,5383,5385],{},[923,5384,1936],{},[923,5386,1939],{},[933,5388,5389,5397,5409],{},[920,5390,5391,5395],{},[938,5392,5393],{},[978,5394,1948],{},[938,5396,1951],{},[920,5398,5399,5403],{},[938,5400,5401],{},[978,5402,1958],{},[938,5404,1961,5405,1431,5407,963],{},[978,5406,1964],{},[978,5408,1967],{},[920,5410,5411,5415],{},[938,5412,5413],{},[978,5414,1974],{},[938,5416,1977],{},[856,5418,1980,5419,1983,5421,1986,5423,1990],{},[978,5420,1870],{},[978,5422,792],{},[978,5424,1989],{},[1807,5426,1994],{"id":1993},[856,5428,1997,5429,2001],{},[978,5430,2000],{},[2003,5432,5433,5437,5441,5445,5449,5453,5457],{},[2006,5434,5435,2011],{},[978,5436,2010],{},[2006,5438,5439,2011],{},[978,5440,2016],{},[2006,5442,5443,2011],{},[978,5444,2021],{},[2006,5446,5447,2011],{},[978,5448,2026],{},[2006,5450,5451,2011],{},[978,5452,2031],{},[2006,5454,5455,2011],{},[978,5456,2036],{},[2006,5458,5459,1459],{},[978,5460,2041],{},[856,5462,1328,5463,2047,5465,2051,5467,2055,5469,2058],{},[978,5464,2046],{},[978,5466,2050],{},[978,5468,2054],{},[978,5470,1032],{},[1807,5472,2062],{"id":2061},[856,5474,2065,5475,1431,5477,1431,5479,1431,5481,1431,5483,2080,5485,2084],{},[978,5476,2068],{},[978,5478,1845],{},[978,5480,2073],{},[978,5482,2076],{},[978,5484,2079],{},[978,5486,2083],{},[856,5488,1347,5489,2089],{},[860,5490,139],{"href":140},[907,5492,2093],{"id":2092},[856,5494,2096,5495,2100,5497,2103,5499,2106,5501,2109],{},[978,5496,2099],{},[978,5498,792],{},[978,5500,1989],{},[860,5502,399],{"href":35},[2111,5504,5505,5509,5511],{},[2006,5506,2115,5507,2119],{},[978,5508,2118],{},[2006,5510,2122],{},[2006,5512,2125],{},[856,5514,2128,5515,2132],{},[978,5516,2131],{},[884,5518],{},[887,5520,2138],{"id":2137},[907,5522,2142],{"id":2141},[856,5524,2145,5525,2151,5528,2155],{},[860,5526,2150],{"href":2148,"rel":5527},[864],[978,5529,2154],{},[2111,5531,5532,5536,5538,5540,5542,5546],{},[2006,5533,2160,5534,2164],{},[860,5535,2163],{"href":96},[2006,5537,2167],{},[2006,5539,2170],{},[2006,5541,2173],{},[2006,5543,2176,5544],{},[978,5545,1438],{},[2006,5547,2181,5548,866,5550,2186],{},[978,5549,1564],{},[978,5551,1446],{},[856,5553,2189,5554,2193,5556,1459],{},[978,5555,2192],{},[978,5557,2196],{},[907,5559,2200],{"id":2199},[856,5561,1328,5562,2205],{},[978,5563,1474],{},[1804,5565,5566,5568,5578,5580,5588],{"level":1261},[1807,5567,2211],{"id":2210},[856,5569,2214,5570,2217,5572,2220,5574,2223,5576,2226],{},[978,5571,1474],{},[978,5573,1478],{},[978,5575,1482],{},[978,5577,2192],{},[1807,5579,2230],{"id":2229},[856,5581,2233,5582,2217,5584,2220,5586,2240],{},[978,5583,1474],{},[978,5585,1478],{},[978,5587,1482],{},[856,5589,2243,5590,2246,5592,2249,5594,2252],{},[860,5591,2163],{"href":96},[978,5593,1396],{},[860,5595,123],{"href":124},[856,5597,1347,5598,2259],{},[860,5599,2258],{"href":2257},[907,5601,2263],{"id":2262},[856,5603,2266,5604,2270,5606,2274,5608,2278],{},[978,5605,2269],{},[978,5607,2273],{},[978,5609,2277],{},[907,5611,2282],{"id":2281},[856,5613,1328,5614,2288],{},[978,5615,2287],{},[884,5617],{},[887,5619,2294],{"id":2293},[856,5621,2297,5622,2301,5624,1431,5626,1447,5628,2309],{},[978,5623,2300],{},[978,5625,1378],{},[978,5627,2306],{},[978,5629,1522],{},[856,5631,2312,5632,2316,5634,2319,5636,2322,5638,2319,5640,2327],{},[978,5633,2315],{},[978,5635,2315],{},[978,5637,1032],{},[978,5639,2315],{},[978,5641,1289],{},[914,5643,5644,5658],{},[917,5645,5646],{},[920,5647,5648,5650,5652,5654,5656],{},[923,5649,2336],{},[923,5651,2339],{},[923,5653,2342],{},[923,5655,2345],{},[923,5657,2348],{},[933,5659,5660,5672,5686,5700,5714,5726,5738,5752,5764],{},[920,5661,5662,5664,5666,5668,5670],{},[938,5663,1482],{},[938,5665,2357],{},[938,5667,2360],{},[938,5669,2363],{},[938,5671,2366],{},[920,5673,5674,5676,5678,5682,5684],{},[938,5675,2371],{},[938,5677,1502],{},[938,5679,5680,2378],{},[978,5681,1396],{},[938,5683,123],{},[938,5685,2383],{},[920,5687,5688,5690,5692,5696,5698],{},[938,5689,2388],{},[938,5691,2391],{},[938,5693,5694,2397],{},[978,5695,2396],{},[938,5697,123],{},[938,5699,2402],{},[920,5701,5702,5704,5706,5710,5712],{},[938,5703,1261],{},[938,5705,2409],{},[938,5707,2412,5708],{},[978,5709,2287],{},[938,5711,123],{},[938,5713,2419],{},[920,5715,5716,5718,5720,5722,5724],{},[938,5717,2424],{},[938,5719,2427],{},[938,5721,2430],{},[938,5723,2363],{},[938,5725,2435],{},[920,5727,5728,5730,5732,5734,5736],{},[938,5729,2440],{},[938,5731,2443],{},[938,5733,2446],{},[938,5735,123],{},[938,5737,2451],{},[920,5739,5740,5742,5744,5748,5750],{},[938,5741,2456],{},[938,5743,2459],{},[938,5745,2462,5746],{},[978,5747,2118],{},[938,5749,123],{},[938,5751,2469],{},[920,5753,5754,5756,5758,5760,5762],{},[938,5755,2474],{},[938,5757,2477],{},[938,5759,2480],{},[938,5761,123],{},[938,5763,2485],{},[920,5765,5766,5768,5770,5772,5774],{},[938,5767,2490],{},[938,5769,2493],{},[938,5771,2496],{},[938,5773,123],{},[938,5775,2501],{},[856,5777,2504,5778,2508,5780,2512],{},[978,5779,2507],{},[978,5781,2511],{},[856,5783,1347,5784,2517],{},[860,5785,95],{"href":96},[884,5787],{},[887,5789,2523],{"id":2522},[856,5791,2526],{},[856,5793,5794,2532,5796,2536,5798,2539,5800,1459],{},[941,5795,2531],{},[978,5797,2535],{},[978,5799,1364],{},[860,5801,2542],{"href":116},[856,5803,5804,2548],{},[941,5805,2547],{},[856,5807,2551],{},[2111,5809,5810,5818,5822,5824,5834,5838,5840,5852,5854,5856,5860,5864],{},[2006,5811,2556,5812,1431,5814,1447,5816],{},[978,5813,2559],{},[978,5815,2562],{},[978,5817,2565],{},[2006,5819,2568,5820],{},[978,5821,792],{},[2006,5823,2573],{},[2006,5825,5826,2579,5828,2582,5830,1431,5832,2588],{},[978,5827,2578],{},[978,5829,1364],{},[978,5831,1438],{},[978,5833,2587],{},[2006,5835,2591,5836,2595],{},[978,5837,2594],{},[2006,5839,2598],{},[2006,5841,2601,5842,2605,5844,1431,5846,1431,5848,1447,5850],{},[978,5843,2604],{},[978,5845,2608],{},[978,5847,2611],{},[978,5849,2614],{},[978,5851,2617],{},[2006,5853,2620],{},[2006,5855,2623],{},[2006,5857,2626,5858,2630],{},[978,5859,2629],{},[2006,5861,2633,5862,2637],{},[978,5863,2636],{},[2006,5865,2640,5866,2644,5868,866,5870,2649],{},[978,5867,2643],{},[978,5869,1446],{},[978,5871,1564],{},[856,5873,2652],{},[914,5875,5876,5886],{},[917,5877,5878],{},[920,5879,5880,5882,5884],{},[923,5881,925],{},[923,5883,2663],{},[923,5885,1018],{},[933,5887,5888,5898,5908,5918,5928,5938],{},[920,5889,5890,5892,5896],{},[938,5891,2672],{},[938,5893,5894],{},[978,5895,2677],{},[938,5897,2680],{},[920,5899,5900,5902,5906],{},[938,5901,2685],{},[938,5903,5904],{},[978,5905,2690],{},[938,5907,2693],{},[920,5909,5910,5912,5916],{},[938,5911,2698],{},[938,5913,5914],{},[978,5915,2703],{},[938,5917,2706],{},[920,5919,5920,5922,5926],{},[938,5921,2711],{},[938,5923,5924],{},[978,5925,2690],{},[938,5927,2718],{},[920,5929,5930,5932,5936],{},[938,5931,2723],{},[938,5933,5934],{},[978,5935,2728],{},[938,5937,2731],{},[920,5939,5940,5942,5946],{},[938,5941,2736],{},[938,5943,5944],{},[978,5945,2703],{},[938,5947,2743],{},[856,5949,1347,5950,2748],{},[860,5951,123],{"href":124},[884,5953],{},[887,5955,2754],{"id":2753},[856,5957,5958,2760],{},[860,5959,2759],{"href":116},[856,5961,2763,5962,2768,5965,2772,5967,2775,5969,2778,5971,2782],{},[860,5963,2767],{"href":1127,"rel":5964},[864],[978,5966,2771],{},[978,5968,1438],{},[978,5970,1442],{},[978,5972,2781],{},[856,5974,2785,5975,2789,5977,2793,5979,2797,5981,2800,5983,2803,5985,2809],{},[978,5976,2788],{},[978,5978,2792],{},[978,5980,2796],{},[978,5982,2788],{},[978,5984,2796],{},[860,5986,2808],{"href":2806,"rel":5987},[864],[856,5989,2812],{},[1292,5991,5992],{},[856,5993,2817,5994,2821],{},[978,5995,2820],{},[884,5997],{},[887,5999,2827],{"id":2826},[856,6001,2830,6002,2836],{},[860,6003,2835],{"href":2833,"rel":6004},[864],[907,6006,2840],{"id":2839},[856,6008,2843],{},[914,6010,6011,6019],{},[917,6012,6013],{},[920,6014,6015,6017],{},[923,6016,925],{},[923,6018,1018],{},[933,6020,6021,6029,6037],{},[920,6022,6023,6027],{},[938,6024,6025],{},[941,6026,2672],{},[938,6028,2864],{},[920,6030,6031,6035],{},[938,6032,6033],{},[941,6034,2871],{},[938,6036,2874],{},[920,6038,6039,6043],{},[938,6040,6041],{},[941,6042,2881],{},[938,6044,2884],{},[907,6046,2888],{"id":2887},[856,6048,2891,6049,2895,6051,2899,6053,2903],{},[978,6050,2894],{},[978,6052,2898],{},[978,6054,2902],{},[907,6056,2907],{"id":2906},[914,6058,6059,6067],{},[917,6060,6061],{},[920,6062,6063,6065],{},[923,6064,2916],{},[923,6066,2919],{},[933,6068,6069,6077,6085,6093,6101,6109,6115,6121],{},[920,6070,6071,6075],{},[938,6072,6073],{},[978,6074,2928],{},[938,6076,2931],{},[920,6078,6079,6083],{},[938,6080,6081],{},[978,6082,2938],{},[938,6084,2941],{},[920,6086,6087,6091],{},[938,6088,6089],{},[978,6090,2948],{},[938,6092,2951],{},[920,6094,6095,6099],{},[938,6096,6097],{},[978,6098,2154],{},[938,6100,2960],{},[920,6102,6103,6107],{},[938,6104,6105],{},[978,6106,2967],{},[938,6108,2970],{},[920,6110,6111,6113],{},[938,6112,2975],{},[938,6114,2978],{},[920,6116,6117,6119],{},[938,6118,2983],{},[938,6120,2986],{},[920,6122,6123,6125],{},[938,6124,2991],{},[938,6126,2994],{},[856,6128,1347,6129,2999,6131,866,6133,3005],{},[860,6130,147],{"href":148},[978,6132,2898],{},[978,6134,3004],{},[884,6136],{},[887,6138,3011],{"id":3010},[856,6140,1328,6141,3016,6143,3019],{},[860,6142,399],{"href":404},[978,6144,1522],{},[856,6146,3022],{},[2003,6148,6149,6153,6158,6162,6164,6169],{},[2006,6150,3027,6151,3030],{},[978,6152,1396],{},[2006,6154,3033,6155,3039],{},[860,6156,3038],{"href":3036,"rel":6157},[864],[2006,6159,3042,6160,3046],{},[978,6161,3045],{},[2006,6163,3049],{},[2006,6165,3052,6166,3058],{},[860,6167,3057],{"href":3055,"rel":6168},[864],[2006,6170,3061,6171,3065,6173],{},[978,6172,3064],{},[860,6174,3068],{"href":435},[856,6176,3071,6177,3074],{},[978,6178,2118],{},[856,6180,1347,6181,3080],{},[860,6182,3079],{"href":514},[884,6184],{},[887,6186,3086],{"id":3085},[856,6188,1328,6189,3092],{},[978,6190,3091],{},[856,6192,3095],{},[914,6194,6195,6203],{},[917,6196,6197],{},[920,6198,6199,6201],{},[923,6200,3104],{},[923,6202,3107],{},[933,6204,6205,6239,6253,6269],{},[920,6206,6207,6211],{},[938,6208,6209],{},[941,6210,500],{},[938,6212,6213,1431,6215,1431,6217,1431,6219,1431,6221,1431,6223,1431,6225,1431,6227,1431,6229,1431,6231,1431,6233,1431,6235,1431,6237],{},[978,6214,3120],{},[978,6216,3123],{},[978,6218,3126],{},[978,6220,3129],{},[978,6222,3132],{},[978,6224,3135],{},[978,6226,3138],{},[978,6228,3141],{},[978,6230,3144],{},[978,6232,3147],{},[978,6234,3150],{},[978,6236,3153],{},[978,6238,3156],{},[920,6240,6241,6245],{},[938,6242,6243],{},[941,6244,3163],{},[938,6246,6247,1431,6249,1431,6251],{},[978,6248,3168],{},[978,6250,3171],{},[978,6252,3174],{},[920,6254,6255,6259],{},[938,6256,6257],{},[941,6258,3181],{},[938,6260,6261,1431,6263,1431,6265,1431,6267],{},[978,6262,3186],{},[978,6264,3189],{},[978,6266,3192],{},[978,6268,3195],{},[920,6270,6271,6275],{},[938,6272,6273],{},[941,6274,3202],{},[938,6276,6277,1431,6279,1431,6281],{},[978,6278,3207],{},[978,6280,3210],{},[978,6282,3213],{},[856,6284,3216,6285,1431,6287,3223,6289,3227],{},[978,6286,3219],{},[978,6288,3222],{},[978,6290,3226],{},[856,6292,1347,6293,3232],{},[860,6294,127],{"href":128},[884,6296],{},[887,6298,3238],{"id":3237},[856,6300,1328,6301,3244,6303,3247,6305,3251],{},[978,6302,3243],{},[978,6304,1032],{},[978,6306,3250],{},[2111,6308,6309,6315],{},[2006,6310,3256,6311,3260,6313],{},[978,6312,3259],{},[978,6314,1396],{},[2006,6316,3265],{},[856,6318,3268],{},[1292,6320,6321],{},[856,6322,3273,6323,2319,6325,3278,6327,3281,6329,3284],{},[978,6324,3243],{},[978,6326,1032],{},[978,6328,1396],{},[978,6330,1289],{},[884,6332],{},[887,6334,3290],{"id":3289},[856,6336,3293],{},[1804,6338,6339,6341,6345,6347,6365,6367,6372,6374,6382,6384,6394,6396],{"level":1261},[1807,6340,3299],{"id":3298},[856,6342,3302,6343,3305],{},[978,6344,1692],{},[1807,6346,3309],{"id":3308},[856,6348,3312,6349,1431,6351,1431,6353,1431,6355,1431,6357,1447,6359,1693,6361,3333,6363,3337],{},[978,6350,3315],{},[978,6352,3318],{},[978,6354,3321],{},[978,6356,3324],{},[978,6358,3327],{},[978,6360,3330],{},[978,6362,3330],{},[978,6364,3336],{},[1807,6366,3341],{"id":3340},[856,6368,3344,6369,3350],{},[860,6370,3349],{"href":3347,"rel":6371},[864],[1807,6373,3354],{"id":3353},[856,6375,3357,6376,3362,6379,3368],{},[860,6377,40],{"href":3360,"rel":6378},[864],[860,6380,3367],{"href":3365,"rel":6381},[864],[1807,6383,3372],{"id":3371},[856,6385,3375,6386,3381,6389,3387,6392,3391],{},[860,6387,3380],{"href":3378,"rel":6388},[864],[860,6390,3386],{"href":3384,"rel":6391},[864],[941,6393,3390],{},[1807,6395,3395],{"id":3394},[856,6397,3398],{},[856,6399,1347,6400,3403],{},[860,6401,99],{"href":100},[884,6403],{},[887,6405,3409],{"id":3408},[856,6407,3412],{},[1804,6409,6410,6412,6414,6416,6422,6424,6431,6433,6435,6437,6443,6445],{"level":1261},[1807,6411,2827],{"id":3417},[856,6413,3420],{},[1807,6415,3309],{"id":3423},[856,6417,3312,6418,866,6420,3430],{},[978,6419,3318],{},[978,6421,3321],{},[1807,6423,3434],{"id":3433},[856,6425,3437,6426,3441,6428,1459],{},[978,6427,3440],{},[860,6429,3446],{"href":3444,"rel":6430},[864],[1807,6432,3450],{"id":3449},[856,6434,3453],{},[1807,6436,3372],{"id":3456},[856,6438,3459,6439,3463,6441,3467],{},[941,6440,3462],{},[978,6442,3466],{},[1807,6444,3471],{"id":3470},[856,6446,3474,6447,3477],{},[978,6448,3243],{},[856,6450,1347,6451,3403],{},[860,6452,103],{"href":104},[884,6454],{},[887,6456,3487],{"id":3486},[856,6458,3490,6459,1431,6461,1431,6463,1431,6465,1431,6467,1431,6469,1431,6471,3512,6473,3516],{},[978,6460,3493],{},[978,6462,3496],{},[978,6464,3499],{},[978,6466,3502],{},[978,6468,3505],{},[978,6470,3508],{},[978,6472,3511],{},[978,6474,3515],{},[856,6476,3519],{},[914,6478,6479,6489],{},[917,6480,6481],{},[920,6482,6483,6485,6487],{},[923,6484,3528],{},[923,6486,2663],{},[923,6488,1018],{},[933,6490,6491,6499,6507],{},[920,6492,6493,6495,6497],{},[938,6494,3539],{},[938,6496,3542],{},[938,6498,3545],{},[920,6500,6501,6503,6505],{},[938,6502,3550],{},[938,6504,3553],{},[938,6506,3556],{},[920,6508,6509,6511,6513],{},[938,6510,3561],{},[938,6512,3564],{},[938,6514,3567],{},[856,6516,3570],{},[856,6518,1347,6519,3575],{},[860,6520,111],{"href":112},[884,6522],{},[887,6524,3581],{"id":3580},[856,6526,3584,6527,3590,6530,3594],{},[860,6528,3589],{"href":3587,"rel":6529},[864],[978,6531,3593],{},[3596,6533,6534],{"className":3598,"code":3599,"language":3600,"meta":3601,"style":3601},[978,6535,6536,6542,6552,6572,6580,6606,6610,6630],{"__ignoreMap":3601},[3605,6537,6538,6540],{"class":3607,"line":3608},[3605,6539,3612],{"class":3611},[3605,6541,3616],{"class":3615},[3605,6543,6544,6546,6548,6550],{"class":3607,"line":3619},[3605,6545,3623],{"class":3622},[3605,6547,1708],{"class":3626},[3605,6549,3630],{"class":3629},[3605,6551,3633],{"class":3615},[3605,6553,6554,6556,6558,6560,6562,6564,6566,6568,6570],{"class":3607,"line":3636},[3605,6555,3639],{"class":3622},[3605,6557,1708],{"class":3626},[3605,6559,3644],{"class":3615},[3605,6561,3647],{"class":3622},[3605,6563,1708],{"class":3626},[3605,6565,3653],{"class":3652},[3605,6567,3657],{"class":3656},[3605,6569,2079],{"class":3652},[3605,6571,3662],{"class":3615},[3605,6573,6574,6576,6578],{"class":3607,"line":3665},[3605,6575,3668],{"class":3622},[3605,6577,1708],{"class":3626},[3605,6579,3673],{"class":3615},[3605,6581,6582,6584,6586,6588,6590,6592,6594,6596,6598,6600,6602,6604],{"class":3607,"line":3676},[3605,6583,3679],{"class":3622},[3605,6585,1708],{"class":3626},[3605,6587,3644],{"class":3615},[3605,6589,2079],{"class":3652},[3605,6591,3688],{"class":3656},[3605,6593,2079],{"class":3652},[3605,6595,1708],{"class":3626},[3605,6597,3695],{"class":3615},[3605,6599,2076],{"class":3652},[3605,6601,3700],{"class":3656},[3605,6603,2076],{"class":3652},[3605,6605,3705],{"class":3615},[3605,6607,6608],{"class":3607,"line":3708},[3605,6609,3711],{"class":3615},[3605,6611,6612,6614,6616,6618,6620,6622,6624,6626,6628],{"class":3607,"line":3714},[3605,6613,3717],{"class":3622},[3605,6615,1708],{"class":3626},[3605,6617,3644],{"class":3615},[3605,6619,3724],{"class":3622},[3605,6621,1708],{"class":3626},[3605,6623,3653],{"class":3652},[3605,6625,3731],{"class":3656},[3605,6627,2079],{"class":3652},[3605,6629,3736],{"class":3615},[3605,6631,6632],{"class":3607,"line":3739},[3605,6633,3742],{"class":3615},[914,6635,6636,6646],{},[917,6637,6638],{},[920,6639,6640,6642,6644],{},[923,6641,3751],{},[923,6643,1015],{},[923,6645,3756],{},[933,6647,6648,6658,6670,6682,6692,6702,6712,6722,6732,6742,6752,6762,6772],{},[920,6649,6650,6654,6656],{},[938,6651,6652],{},[978,6653,3765],{},[938,6655,3768],{},[938,6657,3771],{},[920,6659,6660,6664,6668],{},[938,6661,6662],{},[978,6663,3778],{},[938,6665,6666],{},[978,6667,3783],{},[938,6669,3786],{},[920,6671,6672,6676,6680],{},[938,6673,6674],{},[978,6675,3793],{},[938,6677,6678],{},[978,6679,3798],{},[938,6681,3786],{},[920,6683,6684,6688,6690],{},[938,6685,6686],{},[978,6687,3807],{},[938,6689,3810],{},[938,6691,3813],{},[920,6693,6694,6698,6700],{},[938,6695,6696],{},[978,6697,3820],{},[938,6699,3823],{},[938,6701,3813],{},[920,6703,6704,6708,6710],{},[938,6705,6706],{},[978,6707,3832],{},[938,6709,3835],{},[938,6711,3786],{},[920,6713,6714,6718,6720],{},[938,6715,6716],{},[978,6717,3844],{},[938,6719,3835],{},[938,6721,3786],{},[920,6723,6724,6728,6730],{},[938,6725,6726],{},[978,6727,3855],{},[938,6729,3731],{},[938,6731,3813],{},[920,6733,6734,6738,6740],{},[938,6735,6736],{},[978,6737,3866],{},[938,6739,3869],{},[938,6741,3786],{},[920,6743,6744,6748,6750],{},[938,6745,6746],{},[978,6747,3878],{},[938,6749,3881],{},[938,6751,3786],{},[920,6753,6754,6758,6760],{},[938,6755,6756],{},[978,6757,3890],{},[938,6759,3893],{},[938,6761,3786],{},[920,6763,6764,6768,6770],{},[938,6765,6766],{},[978,6767,3902],{},[938,6769,3905],{},[938,6771,3786],{},[920,6773,6774,6778,6780],{},[938,6775,6776],{},[978,6777,3914],{},[938,6779,1478],{},[938,6781,3919],{},[1292,6783,6784],{},[856,6785,3924],{},[884,6787],{},[887,6789,3930],{"id":3929},[856,6791,2830,6792,3938],{},[860,6793,3937],{"href":3935,"rel":6794},[864],[914,6796,6797,6807],{},[917,6798,6799],{},[920,6800,6801,6803,6805],{},[923,6802,3947],{},[923,6804,3950],{},[923,6806,3953],{},[933,6808,6809,6819,6829,6839,6849,6859,6869,6879],{},[920,6810,6811,6813,6817],{},[938,6812,3960],{},[938,6814,6815],{},[978,6816,3965],{},[938,6818,3968],{},[920,6820,6821,6823,6827],{},[938,6822,3973],{},[938,6824,6825],{},[978,6826,3978],{},[938,6828,3981],{},[920,6830,6831,6833,6837],{},[938,6832,3986],{},[938,6834,6835],{},[978,6836,3978],{},[938,6838,3993],{},[920,6840,6841,6843,6847],{},[938,6842,3998],{},[938,6844,6845],{},[978,6846,3978],{},[938,6848,4005],{},[920,6850,6851,6853,6857],{},[938,6852,4010],{},[938,6854,6855],{},[978,6856,3978],{},[938,6858,4017],{},[920,6860,6861,6863,6867],{},[938,6862,4022],{},[938,6864,6865],{},[978,6866,3965],{},[938,6868,4029],{},[920,6870,6871,6873,6877],{},[938,6872,4034],{},[938,6874,6875],{},[978,6876,3965],{},[938,6878,4041],{},[920,6880,6881,6883,6887],{},[938,6882,4046],{},[938,6884,6885],{},[978,6886,3965],{},[938,6888,4041],{},[856,6890,4055],{},[907,6892,4059],{"id":4058},[856,6894,4062,6895,4066,6898,4070,6900,4074],{},[860,6896,1201],{"href":1199,"rel":6897},[864],[978,6899,4069],{},[978,6901,4073],{},[884,6903],{},[887,6905,4080],{"id":4079},[856,6907,4083],{},[907,6909,4087],{"id":4086},[856,6911,4090],{},[856,6913,6914,4096,6916,2217,6918,2220,6920,4103,6922,4107,6924,4110],{},[941,6915,4095],{},[978,6917,1474],{},[978,6919,1478],{},[978,6921,1482],{},[978,6923,4106],{},[978,6925,2192],{},[856,6927,6928,4116,6930,4119,6932,4122,6934,4127],{},[941,6929,4115],{},[860,6931,2163],{"href":96},[978,6933,1396],{},[860,6935,4126],{"href":4125},[4129,6937,6938],{},[856,6939,4133,6940,4137,6942,4140],{},[941,6941,4136],{},[978,6943,1396],{},[907,6945,4144],{"id":4143},[856,6947,4147,6948,2220,6950,4152,6952,1693,6954,4158],{},[978,6949,1474],{},[978,6951,1482],{},[978,6953,2192],{},[978,6955,4157],{},[907,6957,4162],{"id":4161},[856,6959,4165,6960,4168,6962,4171,6964,1459],{},[978,6961,2099],{},[978,6963,1989],{},[978,6965,2131],{},[856,6967,1347,6968,4178],{},[860,6969,139],{"href":140},[907,6971,4182],{"id":4181},[856,6973,4185],{},[2111,6975,6976,6980,6984,6988],{},[2006,6977,6978,4193],{},[941,6979,4192],{},[2006,6981,6982,4199],{},[941,6983,4198],{},[2006,6985,6986,4205],{},[941,6987,4204],{},[2006,6989,6990,4211],{},[941,6991,4210],{},[856,6993,4214],{},[907,6995,4218],{"id":4217},[856,6997,4221,6998,4224,7000,4227],{},[978,6999,1396],{},[860,7001,2163],{"href":96},[856,7003,4230],{},[856,7005,4233,7006,4236],{},[978,7007,1396],{},[856,7009,4239],{},[907,7011,4243],{"id":4242},[856,7013,4246,7014,4252],{},[860,7015,4251],{"href":4249,"rel":7016},[864],[856,7018,4255,7019,4261],{},[860,7020,4260],{"href":4258,"rel":7021},[864],[907,7023,4265],{"id":4264},[856,7025,4268,7026,4272],{},[978,7027,4271],{},[856,7029,4275],{},[914,7031,7032,7040],{},[917,7033,7034],{},[920,7035,7036,7038],{},[923,7037,925],{},[923,7039,4286],{},[933,7041,7042,7050,7060,7070],{},[920,7043,7044,7048],{},[938,7045,7046],{},[941,7047,4295],{},[938,7049,4298],{},[920,7051,7052,7056],{},[938,7053,7054],{},[941,7055,4305],{},[938,7057,7058,4310],{},[978,7059,1707],{},[920,7061,7062,7066],{},[938,7063,7064],{},[941,7065,4317],{},[938,7067,7068,4322],{},[978,7069,1890],{},[920,7071,7072,7076],{},[938,7073,7074],{},[941,7075,4317],{},[938,7077,7078,4334],{},[978,7079,4333],{},[907,7081,4338],{"id":4337},[856,7083,4341],{},[2003,7085,7086,7090,7098,7102],{},[2006,7087,7088,4349],{},[941,7089,4348],{},[2006,7091,7092,4354,7094,866,7096,4360],{},[941,7093,1468],{},[978,7095,1474],{},[978,7097,4359],{},[2006,7099,7100,4366],{},[941,7101,4365],{},[2006,7103,7104,4372],{},[941,7105,4371],{},[884,7107],{},[887,7109,4378],{"id":4377},[907,7111,4382],{"id":4381},[856,7113,4385,7114,4389],{},[860,7115,4260],{"href":4258,"rel":7116},[864],[907,7118,4393],{"id":4392},[856,7120,4396,7121,4399],{},[978,7122,2511],{},[907,7124,4403],{"id":4402},[856,7126,4406,7127,866,7129,4411],{},[978,7128,1890],{},[978,7130,4333],{},[907,7132,4415],{"id":4414},[856,7134,4418],{},[907,7136,4422],{"id":4421},[856,7138,4425,7139,4431],{},[860,7140,4430],{"href":4428,"rel":7141},[864],[884,7143],{},[887,7145,4437],{"id":4436},[914,7147,7148,7156],{},[917,7149,7150],{},[920,7151,7152,7154],{},[923,7153,925],{},[923,7155,4448],{},[933,7157,7158,7166,7174,7182,7190,7198,7206,7214,7222,7230,7238,7246,7254,7262,7270],{},[920,7159,7160,7164],{},[938,7161,7162],{},[941,7163,943],{},[938,7165,4459],{},[920,7167,7168,7172],{},[938,7169,7170],{},[941,7171,4466],{},[938,7173,4469],{},[920,7175,7176,7180],{},[938,7177,7178],{},[941,7179,4476],{},[938,7181,4479],{},[920,7183,7184,7188],{},[938,7185,7186],{},[941,7187,4486],{},[938,7189,4489],{},[920,7191,7192,7196],{},[938,7193,7194],{},[941,7195,4496],{},[938,7197,4499],{},[920,7199,7200,7204],{},[938,7201,7202],{},[941,7203,4348],{},[938,7205,4508],{},[920,7207,7208,7212],{},[938,7209,7210],{},[941,7211,83],{},[938,7213,4517],{},[920,7215,7216,7220],{},[938,7217,7218],{},[941,7219,4524],{},[938,7221,4527],{},[920,7223,7224,7228],{},[938,7225,7226],{},[941,7227,123],{},[938,7229,4536],{},[920,7231,7232,7236],{},[938,7233,7234],{},[941,7235,2827],{},[938,7237,4545],{},[920,7239,7240,7244],{},[938,7241,7242],{},[941,7243,3011],{},[938,7245,4554],{},[920,7247,7248,7252],{},[938,7249,7250],{},[941,7251,1773],{},[938,7253,4563],{},[920,7255,7256,7260],{},[938,7257,7258],{},[941,7259,4570],{},[938,7261,4573],{},[920,7263,7264,7268],{},[938,7265,7266],{},[941,7267,143],{},[938,7269,4582],{},[920,7271,7272,7276],{},[938,7273,7274],{},[941,7275,4589],{},[938,7277,4592],{},[4594,7279,4596],{},{"title":3601,"searchDepth":3619,"depth":3619,"links":7281},[7282,7287,7292,7297,7298,7305,7311,7312,7313,7314,7319,7320,7321,7322,7323,7324,7325,7326,7329,7339,7346],{"id":889,"depth":3619,"text":890,"children":7283},[7284,7285,7286],{"id":911,"depth":3636,"text":912},{"id":999,"depth":3636,"text":1000},{"id":1114,"depth":3636,"text":1115},{"id":1157,"depth":3619,"text":1158,"children":7288},[7289,7290,7291],{"id":1161,"depth":3636,"text":1162},{"id":1307,"depth":3636,"text":1308},{"id":1353,"depth":3636,"text":1354},{"id":1389,"depth":3619,"text":1390,"children":7293},[7294,7295,7296],{"id":1400,"depth":3636,"text":1401},{"id":1467,"depth":3636,"text":1468},{"id":1501,"depth":3636,"text":1502},{"id":1537,"depth":3619,"text":1538},{"id":1681,"depth":3619,"text":1682,"children":7299},[7300,7301,7302,7303,7304],{"id":1685,"depth":3636,"text":1686},{"id":1700,"depth":3636,"text":1701},{"id":1744,"depth":3636,"text":1745},{"id":1798,"depth":3636,"text":1799},{"id":2092,"depth":3636,"text":2093},{"id":2137,"depth":3619,"text":2138,"children":7306},[7307,7308,7309,7310],{"id":2141,"depth":3636,"text":2142},{"id":2199,"depth":3636,"text":2200},{"id":2262,"depth":3636,"text":2263},{"id":2281,"depth":3636,"text":2282},{"id":2293,"depth":3619,"text":2294},{"id":2522,"depth":3619,"text":2523},{"id":2753,"depth":3619,"text":2754},{"id":2826,"depth":3619,"text":2827,"children":7315},[7316,7317,7318],{"id":2839,"depth":3636,"text":2840},{"id":2887,"depth":3636,"text":2888},{"id":2906,"depth":3636,"text":2907},{"id":3010,"depth":3619,"text":3011},{"id":3085,"depth":3619,"text":3086},{"id":3237,"depth":3619,"text":3238},{"id":3289,"depth":3619,"text":3290},{"id":3408,"depth":3619,"text":3409},{"id":3486,"depth":3619,"text":3487},{"id":3580,"depth":3619,"text":3581},{"id":3929,"depth":3619,"text":3930,"children":7327},[7328],{"id":4058,"depth":3636,"text":4059},{"id":4079,"depth":3619,"text":4080,"children":7330},[7331,7332,7333,7334,7335,7336,7337,7338],{"id":4086,"depth":3636,"text":4087},{"id":4143,"depth":3636,"text":4144},{"id":4161,"depth":3636,"text":4162},{"id":4181,"depth":3636,"text":4182},{"id":4217,"depth":3636,"text":4218},{"id":4242,"depth":3636,"text":4243},{"id":4264,"depth":3636,"text":4265},{"id":4337,"depth":3636,"text":4338},{"id":4377,"depth":3619,"text":4378,"children":7340},[7341,7342,7343,7344,7345],{"id":4381,"depth":3636,"text":4382},{"id":4392,"depth":3636,"text":4393},{"id":4402,"depth":3636,"text":4403},{"id":4414,"depth":3636,"text":4415},{"id":4421,"depth":3636,"text":4422},{"id":4436,"depth":3619,"text":4437},{},{"title":38,"description":4664},1780436278917]