[{"data":1,"prerenderedAt":1339},["ShallowReactive",2],{"navigation":3,"\u002Fauth\u002Fmulti-frontend":189,"\u002Fauth\u002Fmulti-frontend-surround":1334},[4,34,60,116,137,153],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":33},"Getting Started","i-lucide-rocket","\u002Fgetting-started","1.getting-started",[10,15,18,23,28],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fgetting-started\u002Fintroduction","1.getting-started\u002F1.introduction","i-lucide-house",{"title":5,"path":16,"stem":17,"icon":6},"\u002Fgetting-started\u002Fgetting-started","1.getting-started\u002F2.getting-started",{"title":19,"path":20,"stem":21,"icon":22},"Choosing a Preset","\u002Fgetting-started\u002Fchoosing-a-preset","1.getting-started\u002F3.choosing-a-preset","i-lucide-git-branch",{"title":24,"path":25,"stem":26,"icon":27},"Deployment","\u002Fgetting-started\u002Fdeployment","1.getting-started\u002F4.deployment","i-lucide-cloud",{"title":29,"path":30,"stem":31,"icon":32},"Background Maintenance","\u002Fgetting-started\u002Fbackground-maintenance","1.getting-started\u002F5.background-maintenance","i-lucide-timer-reset",false,{"title":35,"icon":36,"path":37,"stem":38,"children":39,"page":33},"Build","i-lucide-wrench","\u002Fbuild","2.build",[40,45,50,55],{"title":41,"path":42,"stem":43,"icon":44},"Routers & Prefixes","\u002Fbuild\u002Frouters-and-prefixes","2.build\u002F1.routers-and-prefixes","i-lucide-route",{"title":46,"path":47,"stem":48,"icon":49},"Configuration","\u002Fbuild\u002Fconfiguration","2.build\u002F2.configuration","i-lucide-settings",{"title":51,"path":52,"stem":53,"icon":54},"Authorization Dependencies","\u002Fbuild\u002Fauthorization-dependencies","2.build\u002F3.authorization-dependencies","i-lucide-shield-check",{"title":56,"path":57,"stem":58,"icon":59},"Command Line","\u002Fbuild\u002Fcli","2.build\u002F4.cli","i-lucide-terminal",{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":33},"Auth","i-lucide-lock","\u002Fauth","3.auth",[66,71,76,81,86,91,96,101,106,111],{"title":67,"path":68,"stem":69,"icon":70},"OAuth & Social Login","\u002Fauth\u002Foauth-and-social-login","3.auth\u002F1.oauth-and-social-login","i-lucide-log-in",{"title":72,"path":73,"stem":74,"icon":75},"Multi-Frontend Support","\u002Fauth\u002Fmulti-frontend","3.auth\u002F10.multi-frontend","i-lucide-layout-grid",{"title":77,"path":78,"stem":79,"icon":80},"Sessions & Audit","\u002Fauth\u002Fsessions-and-audit","3.auth\u002F2.sessions-and-audit","i-lucide-monitor-smartphone",{"title":82,"path":83,"stem":84,"icon":85},"Passwordless & Messaging","\u002Fauth\u002Fpasswordless-and-messaging","3.auth\u002F3.passwordless-and-messaging","i-lucide-mail",{"title":87,"path":88,"stem":89,"icon":90},"JWT Tokens","\u002Fauth\u002Fjwt-tokens","3.auth\u002F4.jwt-tokens","i-lucide-key-round",{"title":92,"path":93,"stem":94,"icon":95},"User Management API","\u002Fauth\u002Fuser-management-api","3.auth\u002F5.user-management-api","i-lucide-users",{"title":97,"path":98,"stem":99,"icon":100},"User Invitations","\u002Fauth\u002Fuser-invitations","3.auth\u002F6.user-invitations","i-lucide-send",{"title":102,"path":103,"stem":104,"icon":105},"Roles & Permissions","\u002Fauth\u002Froles-and-permissions","3.auth\u002F7.roles-and-permissions","i-lucide-shield",{"title":107,"path":108,"stem":109,"icon":110},"ABAC","\u002Fauth\u002Fabac","3.auth\u002F8.abac","i-lucide-filter",{"title":112,"path":113,"stem":114,"icon":115},"User Status","\u002Fauth\u002Fuser-status","3.auth\u002F9.user-status","i-lucide-user-cog",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":33},"Enterprise","i-lucide-building-2","\u002Fenterprise","4.enterprise",[122,127,132],{"title":123,"path":124,"stem":125,"icon":126},"Core Authorization Concepts","\u002Fenterprise\u002Fcore-authorization-concepts","4.enterprise\u002F1.core-authorization-concepts","i-lucide-network",{"title":128,"path":129,"stem":130,"icon":131},"Entities","\u002Fenterprise\u002Fentities","4.enterprise\u002F2.entities","i-lucide-folder-tree",{"title":133,"path":134,"stem":135,"icon":136},"Entity Memberships","\u002Fenterprise\u002Fentity-memberships","4.enterprise\u002F3.entity-memberships","i-lucide-user-plus",{"title":138,"icon":139,"path":140,"stem":141,"children":142,"page":33},"Integrations","i-lucide-plug","\u002Fintegrations","5.integrations",[143,148],{"title":144,"path":145,"stem":146,"icon":147},"API Keys","\u002Fintegrations\u002Fapi-keys","5.integrations\u002F1.api-keys","i-lucide-key",{"title":149,"path":150,"stem":151,"icon":152},"OutlabsAuth UI","\u002Fintegrations\u002Foutlabsauth-ui","5.integrations\u002F2.outlabsauth-ui","i-lucide-layout-dashboard",{"title":154,"icon":155,"path":156,"stem":157,"children":158,"page":33},"Reference","i-lucide-book-marked","\u002Freference","6.reference",[159,164,169,174,179,184],{"title":160,"path":161,"stem":162,"icon":163},"Data Models","\u002Freference\u002Fdata-models","6.reference\u002F1.data-models","i-lucide-database",{"title":165,"path":166,"stem":167,"icon":168},"Activity Tracking","\u002Freference\u002Factivity-tracking","6.reference\u002F2.activity-tracking","i-lucide-activity",{"title":170,"path":171,"stem":172,"icon":173},"Testing","\u002Freference\u002Ftesting","6.reference\u002F3.testing","i-lucide-flask-conical",{"title":175,"path":176,"stem":177,"icon":178},"Observability","\u002Freference\u002Fobservability","6.reference\u002F4.observability","i-lucide-eye",{"title":180,"path":181,"stem":182,"icon":183},"Metrics Reference","\u002Freference\u002Fmetrics-reference","6.reference\u002F5.metrics-reference","i-lucide-chart-bar",{"title":185,"path":186,"stem":187,"icon":188},"Log Events Reference","\u002Freference\u002Flog-events-reference","6.reference\u002F6.log-events-reference","i-lucide-scroll-text",{"id":190,"title":72,"body":191,"description":1327,"extension":1328,"links":1329,"meta":1330,"navigation":1331,"path":73,"seo":1332,"stem":74,"__hash__":1333},"docs\u002F3.auth\u002F10.multi-frontend.md",{"type":192,"value":193,"toc":1314},"minimark",[194,208,231,242,247,325,332,336,600,603,646,650,673,676,714,728,738,742,828,854,860,867,877,955,972,1083,1092,1096,1126,1135,1150,1157,1172,1196,1199,1223,1235,1239,1250,1279,1282,1286,1310],[195,196,197,198,202,203,207],"p",{},"One outlabsAuth mount can serve ",[199,200,201],"strong",{},"several first-party frontends"," of the same\nplatform — an admin console plus a customer portal, say. Password-reset mail,\ninvite links, magic links, OAuth landings, and issued sessions must all land\non the frontend the account actually belongs to. Multi-frontend support\n(DD-059, ",[204,205,206],"code",{},"0.1.0a25+",") makes that a designed-in concept instead of three\nhost workarounds.",[195,209,210,213,214,218,219,222,223,226,227,230],{},[199,211,212],{},"Scope boundary, stated up front:"," one deployment = one platform = one user\npool, with N first-party frontends. Profiles decide ",[215,216,217],"em",{},"where links land",", ",[215,220,221],{},"how\nmail is branded",", and ",[215,224,225],{},"which users may authenticate through which app"," — they\nare ",[199,228,229],{},"not"," tenants and create no credential isolation (RBAC and root-entity\nscoping remain the data boundary). Running several unrelated SaaS products off\none deployment stays out of scope: genuinely distinct products get separate\ndeployments.",[195,232,233,234,237,238,241],{},"Everything below is opt-in. A minimal host with no profiles sees ",[199,235,236],{},"zero\nbehavior change"," — single-composer mail construction is byte-identical, and\nrequests without an ",[204,239,240],{},"app"," key behave exactly as before.",[243,244,246],"h2",{"id":245},"the-moving-pieces","The moving pieces",[248,249,250,263],"table",{},[251,252,253],"thead",{},[254,255,256,260],"tr",{},[257,258,259],"th",{},"Piece",[257,261,262],{},"What it does",[264,265,266,280,290,304,314],"tbody",{},[254,267,268,274],{},[269,270,271],"td",{},[204,272,273],{},"FrontendProfile",[269,275,276,277],{},"Immutable declaration of one frontend: key, branding, registered origins, per-flow route templates, redirect policy, ",[204,278,279],{},"accepted_audiences",[254,281,282,287],{},[269,283,284],{},[204,285,286],{},"FrontendProfileRegistry",[269,288,289],{},"Startup-validated set of profiles (duplicate keys, non-HTTPS origins, and malformed templates are rejected at wiring time)",[254,291,292,297],{},[269,293,294],{},[204,295,296],{},"FrontendProfileResolver",[269,298,299,300,303],{},"The one canonical resolution component — host-supplied resolver over a typed context, resolved ",[199,301,302],{},"once"," per operation, registered keys only",[254,305,306,311],{},[269,307,308],{},[204,309,310],{},"profile_id",[269,312,313],{},"The resolved key, persisted downstream: mail intents, challenge rows, OAuth state, session records, audit events",[254,315,316,322],{},[269,317,318,321],{},[204,319,320],{},"azp"," claim",[269,323,324],{},"Session provenance: which profile minted this session, re-validated at refresh rotation",[195,326,327,328,331],{},"A profile — not the resolver, not the caller — owns URL construction and\nbranding. Selecting a profile for a flow it declares unsupported (",[204,329,330],{},"route = None",") is a wiring error at startup or a fail-closed delivery error at send\ntime, never a guessed link.",[243,333,335],{"id":334},"declaring-profiles","Declaring profiles",[337,338,343],"pre",{"className":339,"code":340,"language":341,"meta":342,"style":342},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","from outlabs_auth.frontend import (\n    FrontendProfile,\n    FrontendProfileRegistry,\n    FrontendProfileResolver,\n    FrontendRoutes,\n    RedirectPolicy,\n    route_by_root_entity_slug,\n)\n\nregistry = FrontendProfileRegistry([\n    FrontendProfile(\n        key=\"console\",                          # stable, non-secret\n        app_name=\"Operations Console\",          # branding for subjects\u002Fbodies\n        public_origins=(\"https:\u002F\u002Fconsole.example.com\",),\n        routes=FrontendRoutes(\n            login=\"\u002Flogin\",\n            password_reset=\"\u002Frecovery\u002F{token}\",          # path placement\n            accept_invite=\"\u002Faccept-invite?token={token}\", # query placement\n            magic_link=\"\u002Fauth\u002Fmagic-link?token={token}\",\n            oauth_success=\"\u002Fauth\u002Foauth\u002Fcallback\",\n            oauth_error=\"\u002Flogin\",\n        ),\n        accepted_audiences=(\"internal\",),        # partitioned: staff only\n        support_email=\"support@example.com\",\n    ),\n    FrontendProfile(\n        key=\"portal\",\n        app_name=\"Agent Portal\",\n        public_origins=(\"https:\u002F\u002Fportal.example.com\",),\n        routes=FrontendRoutes(\n            login=\"\u002Fsign-in\",\n            password_reset=\"\u002Frecovery\u002F{token}\",\n            accept_invite=None,                  # portal has no invite page —\n                                               # invites fail closed, never guessed\n        ),\n        accepted_audiences=(\"agent\",),\n    ),\n])\n\nresolver = FrontendProfileResolver(\n    registry,\n    route_by_root_entity_slug({\"internal-org\": \"console\", \"agent-practice\": \"portal\"}),\n)\n","python","",[204,344,345,353,359,365,371,377,383,389,395,402,408,414,420,426,432,438,444,450,456,462,468,474,480,486,492,498,503,509,515,521,526,532,538,544,550,555,561,566,572,577,583,589,595],{"__ignoreMap":342},[346,347,350],"span",{"class":348,"line":349},"line",1,[346,351,352],{},"from outlabs_auth.frontend import (\n",[346,354,356],{"class":348,"line":355},2,[346,357,358],{},"    FrontendProfile,\n",[346,360,362],{"class":348,"line":361},3,[346,363,364],{},"    FrontendProfileRegistry,\n",[346,366,368],{"class":348,"line":367},4,[346,369,370],{},"    FrontendProfileResolver,\n",[346,372,374],{"class":348,"line":373},5,[346,375,376],{},"    FrontendRoutes,\n",[346,378,380],{"class":348,"line":379},6,[346,381,382],{},"    RedirectPolicy,\n",[346,384,386],{"class":348,"line":385},7,[346,387,388],{},"    route_by_root_entity_slug,\n",[346,390,392],{"class":348,"line":391},8,[346,393,394],{},")\n",[346,396,398],{"class":348,"line":397},9,[346,399,401],{"emptyLinePlaceholder":400},true,"\n",[346,403,405],{"class":348,"line":404},10,[346,406,407],{},"registry = FrontendProfileRegistry([\n",[346,409,411],{"class":348,"line":410},11,[346,412,413],{},"    FrontendProfile(\n",[346,415,417],{"class":348,"line":416},12,[346,418,419],{},"        key=\"console\",                          # stable, non-secret\n",[346,421,423],{"class":348,"line":422},13,[346,424,425],{},"        app_name=\"Operations Console\",          # branding for subjects\u002Fbodies\n",[346,427,429],{"class":348,"line":428},14,[346,430,431],{},"        public_origins=(\"https:\u002F\u002Fconsole.example.com\",),\n",[346,433,435],{"class":348,"line":434},15,[346,436,437],{},"        routes=FrontendRoutes(\n",[346,439,441],{"class":348,"line":440},16,[346,442,443],{},"            login=\"\u002Flogin\",\n",[346,445,447],{"class":348,"line":446},17,[346,448,449],{},"            password_reset=\"\u002Frecovery\u002F{token}\",          # path placement\n",[346,451,453],{"class":348,"line":452},18,[346,454,455],{},"            accept_invite=\"\u002Faccept-invite?token={token}\", # query placement\n",[346,457,459],{"class":348,"line":458},19,[346,460,461],{},"            magic_link=\"\u002Fauth\u002Fmagic-link?token={token}\",\n",[346,463,465],{"class":348,"line":464},20,[346,466,467],{},"            oauth_success=\"\u002Fauth\u002Foauth\u002Fcallback\",\n",[346,469,471],{"class":348,"line":470},21,[346,472,473],{},"            oauth_error=\"\u002Flogin\",\n",[346,475,477],{"class":348,"line":476},22,[346,478,479],{},"        ),\n",[346,481,483],{"class":348,"line":482},23,[346,484,485],{},"        accepted_audiences=(\"internal\",),        # partitioned: staff only\n",[346,487,489],{"class":348,"line":488},24,[346,490,491],{},"        support_email=\"support@example.com\",\n",[346,493,495],{"class":348,"line":494},25,[346,496,497],{},"    ),\n",[346,499,501],{"class":348,"line":500},26,[346,502,413],{},[346,504,506],{"class":348,"line":505},27,[346,507,508],{},"        key=\"portal\",\n",[346,510,512],{"class":348,"line":511},28,[346,513,514],{},"        app_name=\"Agent Portal\",\n",[346,516,518],{"class":348,"line":517},29,[346,519,520],{},"        public_origins=(\"https:\u002F\u002Fportal.example.com\",),\n",[346,522,524],{"class":348,"line":523},30,[346,525,437],{},[346,527,529],{"class":348,"line":528},31,[346,530,531],{},"            login=\"\u002Fsign-in\",\n",[346,533,535],{"class":348,"line":534},32,[346,536,537],{},"            password_reset=\"\u002Frecovery\u002F{token}\",\n",[346,539,541],{"class":348,"line":540},33,[346,542,543],{},"            accept_invite=None,                  # portal has no invite page —\n",[346,545,547],{"class":348,"line":546},34,[346,548,549],{},"                                               # invites fail closed, never guessed\n",[346,551,553],{"class":348,"line":552},35,[346,554,479],{},[346,556,558],{"class":348,"line":557},36,[346,559,560],{},"        accepted_audiences=(\"agent\",),\n",[346,562,564],{"class":348,"line":563},37,[346,565,497],{},[346,567,569],{"class":348,"line":568},38,[346,570,571],{},"])\n",[346,573,575],{"class":348,"line":574},39,[346,576,401],{"emptyLinePlaceholder":400},[346,578,580],{"class":348,"line":579},40,[346,581,582],{},"resolver = FrontendProfileResolver(\n",[346,584,586],{"class":348,"line":585},41,[346,587,588],{},"    registry,\n",[346,590,592],{"class":348,"line":591},42,[346,593,594],{},"    route_by_root_entity_slug({\"internal-org\": \"console\", \"agent-practice\": \"portal\"}),\n",[346,596,598],{"class":348,"line":597},43,[346,599,394],{},[195,601,602],{},"Route-template rules:",[604,605,606,632,643],"ul",{},[607,608,609,610,218,613,218,616,619,620,623,624,627,628,631],"li",{},"Token flows (",[204,611,612],{},"password_reset",[204,614,615],{},"accept_invite",[204,617,618],{},"magic_link",") need exactly\none ",[204,621,622],{},"{token}"," placeholder; both ",[204,625,626],{},"?token={token}"," and ",[204,629,630],{},"\u002Frecovery\u002F{token}","\nplacements are first-class.",[607,633,634,635,638,639,642],{},"Origins must be absolute HTTPS outside local development\n(",[204,636,637],{},"FrontendProfileRegistry(..., local_dev=True)"," permits\n",[204,640,641],{},"http:\u002F\u002Flocalhost:3000",").",[607,644,645],{},"Profiles are immutable after startup.",[243,647,649],{"id":648},"the-resolver-is-host-code","The resolver is host code",[195,651,652,653,656,657,660,661,664,665,668,669,672],{},"The audience key differs per host — root-entity slug, entity type, role,\nmembership scope, or the requested profile — so the mapping stays host-owned.\nThe resolver receives a typed ",[204,654,655],{},"FrontendResolutionContext"," (flow kind,\nrecipient, ",[204,658,659],{},"root_entity_id\u002Fslug\u002Ftype",", actor and target entity for invites,\nthe requested profile key, request origin as ",[215,662,663],{},"evidence, never authority",", and\nthe caller's ",[204,666,667],{},"session"," when one exists). It may be ",[199,670,671],{},"async"," and may query\nhost data, but returns only a registered key.",[195,674,675],{},"Library helpers cover the observed cases:",[337,677,679],{"className":339,"code":678,"language":341,"meta":342,"style":342},"from outlabs_auth.frontend import route_by_root_entity_slug, route_by_root_entity_type\n\nroute_by_root_entity_slug({\"internal-org\": \"console\", \"agent-practice\": \"portal\"})\nroute_by_root_entity_type(\n    {\"agent_practice\": \"portal\", \"brokerage\": \"portal\"},\n    slug_overrides={\"internal-org\": \"console\"},   # canonical slug beats type\n)\n",[204,680,681,686,690,695,700,705,710],{"__ignoreMap":342},[346,682,683],{"class":348,"line":349},[346,684,685],{},"from outlabs_auth.frontend import route_by_root_entity_slug, route_by_root_entity_type\n",[346,687,688],{"class":348,"line":355},[346,689,401],{"emptyLinePlaceholder":400},[346,691,692],{"class":348,"line":361},[346,693,694],{},"route_by_root_entity_slug({\"internal-org\": \"console\", \"agent-practice\": \"portal\"})\n",[346,696,697],{"class":348,"line":367},[346,698,699],{},"route_by_root_entity_type(\n",[346,701,702],{"class":348,"line":373},[346,703,704],{},"    {\"agent_practice\": \"portal\", \"brokerage\": \"portal\"},\n",[346,706,707],{"class":348,"line":379},[346,708,709],{},"    slug_overrides={\"internal-org\": \"console\"},   # canonical slug beats type\n",[346,711,712],{"class":348,"line":385},[346,713,394],{},[195,715,716,717,720,721,723,724,727],{},"Both take ",[204,718,719],{},"honor_requested=True"," (a frontend-originated ",[204,722,240],{}," key is accepted\nwhen identity has no opinion; a requested key that contradicts identity is a\nhard mismatch) and ",[204,725,726],{},"on_unresolved=None"," (the explicit-unresolved posture —\ndeclare a profile for genuinely unresolvable contexts, or keep fail-closed).",[195,729,730,733,734,737],{},[199,731,732],{},"Failure policy is fail closed."," Unknown profile, unsupported flow,\nresolver exception, or user\u002Fprofile mismatch → no send, a structured\ndelivery-failure result, and a log record. Enumeration-resistant endpoints\nkeep their opaque 204\u002F202 outward response. A declared default profile\n(",[204,735,736],{},"FrontendProfileResolver(..., default=\"console\")",") applies only to genuinely\nunambiguous contexts — never as an exception fallback. Reset-confirmation\nnotices may fall back to the default's neutral, link-free message so the\nsecurity signal still reaches the user.",[243,739,741],{"id":740},"mail-per-audience-from-one-mount","Mail per audience from one mount",[337,743,745],{"className":339,"code":744,"language":341,"meta":342,"style":342},"from outlabs_auth.mail import ComposedAuthMailService, DefaultAuthMailComposer\n\nmail_service = ComposedAuthMailService(\n    provider=provider,\n    composers={\n        \"console\": DefaultAuthMailComposer.from_profile(registry.get(\"console\")),\n        \"portal\": DefaultAuthMailComposer.from_profile(registry.get(\"portal\")),\n    },\n    resolver=resolver,\n    # default=\"console\",   # optional declared default (see failure policy)\n)\n\nauth = EnterpriseRBAC(\n    database_url=..., secret_key=...,\n    transactional_mail_service=mail_service,\n    frontend_resolver=resolver,   # also powers challenges, OAuth, and the sign-in gate\n)\n",[204,746,747,752,756,761,766,771,776,781,786,791,796,800,804,809,814,819,824],{"__ignoreMap":342},[346,748,749],{"class":348,"line":349},[346,750,751],{},"from outlabs_auth.mail import ComposedAuthMailService, DefaultAuthMailComposer\n",[346,753,754],{"class":348,"line":355},[346,755,401],{"emptyLinePlaceholder":400},[346,757,758],{"class":348,"line":361},[346,759,760],{},"mail_service = ComposedAuthMailService(\n",[346,762,763],{"class":348,"line":367},[346,764,765],{},"    provider=provider,\n",[346,767,768],{"class":348,"line":373},[346,769,770],{},"    composers={\n",[346,772,773],{"class":348,"line":379},[346,774,775],{},"        \"console\": DefaultAuthMailComposer.from_profile(registry.get(\"console\")),\n",[346,777,778],{"class":348,"line":385},[346,779,780],{},"        \"portal\": DefaultAuthMailComposer.from_profile(registry.get(\"portal\")),\n",[346,782,783],{"class":348,"line":391},[346,784,785],{},"    },\n",[346,787,788],{"class":348,"line":397},[346,789,790],{},"    resolver=resolver,\n",[346,792,793],{"class":348,"line":404},[346,794,795],{},"    # default=\"console\",   # optional declared default (see failure policy)\n",[346,797,798],{"class":348,"line":410},[346,799,394],{},[346,801,802],{"class":348,"line":416},[346,803,401],{"emptyLinePlaceholder":400},[346,805,806],{"class":348,"line":422},[346,807,808],{},"auth = EnterpriseRBAC(\n",[346,810,811],{"class":348,"line":428},[346,812,813],{},"    database_url=..., secret_key=...,\n",[346,815,816],{"class":348,"line":434},[346,817,818],{},"    transactional_mail_service=mail_service,\n",[346,820,821],{"class":348,"line":440},[346,822,823],{},"    frontend_resolver=resolver,   # also powers challenges, OAuth, and the sign-in gate\n",[346,825,826],{"class":348,"line":446},[346,827,394],{},[195,829,830,831,834,835,838,839,841,842,845,846,849,850,853],{},"Each send resolves the recipient's profile once and composes with that\nprofile's branding and route templates — forgot-password mail for an internal\nuser lands on ",[204,832,833],{},"console.example.com",", an agent's on ",[204,836,837],{},"portal.example.com",", from\none mount. Intents now also carry ",[204,840,659],{}," (the library\nenriches them from the user row + request-scoped cache), and invite mail\nfinally gets its advertised ",[204,843,844],{},"target_entity_name"," \u002F ",[204,847,848],{},"inviter_email"," \u002F\n",[204,851,852],{},"role_names"," metadata from persisted invite state.",[195,855,856,859],{},[204,857,858],{},"ComposedAuthMailService(provider=..., composer=...)"," — the single-composer\nform — is unchanged.",[243,861,863,864],{"id":862},"challenges-registered-destinations-canonical-next_url","Challenges: registered destinations, canonical ",[204,865,866],{},"next_url",[195,868,869,870,872,873,876],{},"Forgot-password, magic-link, and access-code requests accept an optional\n",[204,871,240],{}," field — a ",[199,874,875],{},"registered profile key, never a URL",":",[337,878,882],{"className":879,"code":880,"language":881,"meta":342,"style":342},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","POST \u002Fauth\u002Fmagic-link\u002Frequest\n{ \"email\": \"agent@example.com\", \"app\": \"portal\", \"redirect_url\": \"\u002Fdashboard\" }\n","json",[204,883,884,890],{"__ignoreMap":342},[346,885,886],{"class":348,"line":349},[346,887,889],{"class":888},"sTEyZ","POST \u002Fauth\u002Fmagic-link\u002Frequest\n",[346,891,892,896,899,903,906,908,910,914,916,919,921,923,925,927,929,932,934,936,938,941,943,945,947,950,952],{"class":348,"line":355},[346,893,895],{"class":894},"sMK4o","{",[346,897,898],{"class":894}," \"",[346,900,902],{"class":901},"spNyl","email",[346,904,905],{"class":894},"\"",[346,907,876],{"class":894},[346,909,898],{"class":894},[346,911,913],{"class":912},"sfazB","agent@example.com",[346,915,905],{"class":894},[346,917,918],{"class":894},",",[346,920,898],{"class":894},[346,922,240],{"class":901},[346,924,905],{"class":894},[346,926,876],{"class":894},[346,928,898],{"class":894},[346,930,931],{"class":912},"portal",[346,933,905],{"class":894},[346,935,918],{"class":894},[346,937,898],{"class":894},[346,939,940],{"class":901},"redirect_url",[346,942,905],{"class":894},[346,944,876],{"class":894},[346,946,898],{"class":894},[346,948,949],{"class":912},"\u002Fdashboard",[346,951,905],{"class":894},[346,953,954],{"class":894}," }\n",[195,956,957,958,961,962,964,965,968,969,876],{},"At request time the library resolves the profile (fail closed on unknown\nkeys, identity mismatches, disallowed return targets, or a profile with no\nlanding route for the flow), validates the return target against the\nprofile's ",[204,959,960],{},"RedirectPolicy"," (relative path, or an absolute URL on a registered\norigin), and persists ",[204,963,310],{}," + the canonical target on the challenge\nrow. ",[199,966,967],{},"Verification returns the canonical destination"," on ",[204,970,971],{},"LoginResponse",[337,973,975],{"className":879,"code":974,"language":881,"meta":342,"style":342},"{\n  \"access_token\": \"…\",\n  \"refresh_token\": \"…\",\n  \"token_type\": \"bearer\",\n  \"expires_in\": 900,\n  \"next_url\": \"https:\u002F\u002Fportal.example.com\u002Fdashboard\"\n}\n",[204,976,977,982,1004,1023,1043,1060,1078],{"__ignoreMap":342},[346,978,979],{"class":348,"line":349},[346,980,981],{"class":894},"{\n",[346,983,984,987,990,992,994,996,999,1001],{"class":348,"line":355},[346,985,986],{"class":894},"  \"",[346,988,989],{"class":901},"access_token",[346,991,905],{"class":894},[346,993,876],{"class":894},[346,995,898],{"class":894},[346,997,998],{"class":912},"…",[346,1000,905],{"class":894},[346,1002,1003],{"class":894},",\n",[346,1005,1006,1008,1011,1013,1015,1017,1019,1021],{"class":348,"line":361},[346,1007,986],{"class":894},[346,1009,1010],{"class":901},"refresh_token",[346,1012,905],{"class":894},[346,1014,876],{"class":894},[346,1016,898],{"class":894},[346,1018,998],{"class":912},[346,1020,905],{"class":894},[346,1022,1003],{"class":894},[346,1024,1025,1027,1030,1032,1034,1036,1039,1041],{"class":348,"line":367},[346,1026,986],{"class":894},[346,1028,1029],{"class":901},"token_type",[346,1031,905],{"class":894},[346,1033,876],{"class":894},[346,1035,898],{"class":894},[346,1037,1038],{"class":912},"bearer",[346,1040,905],{"class":894},[346,1042,1003],{"class":894},[346,1044,1045,1047,1050,1052,1054,1058],{"class":348,"line":373},[346,1046,986],{"class":894},[346,1048,1049],{"class":901},"expires_in",[346,1051,905],{"class":894},[346,1053,876],{"class":894},[346,1055,1057],{"class":1056},"sbssI"," 900",[346,1059,1003],{"class":894},[346,1061,1062,1064,1066,1068,1070,1072,1075],{"class":348,"line":379},[346,1063,986],{"class":894},[346,1065,866],{"class":901},[346,1067,905],{"class":894},[346,1069,876],{"class":894},[346,1071,898],{"class":894},[346,1073,1074],{"class":912},"https:\u002F\u002Fportal.example.com\u002Fdashboard",[346,1076,1077],{"class":894},"\"\n",[346,1079,1080],{"class":348,"line":385},[346,1081,1082],{"class":894},"}\n",[195,1084,1085,1086,1088,1089,1091],{},"The frontend navigates from the server-validated ",[204,1087,866],{}," instead of\ntrusting a redirect value in its own URL query. Raw ",[204,1090,940],{}," remains\naccepted for one compatibility window — validated when profiles are\nconfigured — and is then retired.",[243,1093,1095],{"id":1094},"oauth-profile-bound-state","OAuth: profile-bound state",[195,1097,1098,1101,1102,1104,1105,1108,1109,1111,1112,845,1115,1118,1119,1122,1123,876],{},[204,1099,1100],{},"\u002Fauthorize"," accepts a registered ",[204,1103,240],{}," key; the signed ",[199,1106,1107],{},"and persisted","\nstate binds ",[204,1110,310],{}," plus a unique flow nonce, and per-profile binding\ncookies let concurrent same-provider flows from different frontends coexist.\nThe callback consumes state, then resolves the bound profile's registered\n",[204,1113,1114],{},"oauth_success",[204,1116,1117],{},"oauth_error"," landings. Login ",[199,1120,1121],{},"and"," association routers\nboth implement this, and both factories are now exported from\n",[204,1124,1125],{},"outlabs_auth.routers",[337,1127,1129],{"className":339,"code":1128,"language":341,"meta":342,"style":342},"from outlabs_auth.routers import get_oauth_router, get_oauth_associate_router\n",[204,1130,1131],{"__ignoreMap":342},[346,1132,1133],{"class":348,"line":349},[346,1134,1128],{},[195,1136,1137,1138,845,1141,1144,1145,1149],{},"Construction-time ",[204,1139,1140],{},"success_redirect_url",[204,1142,1143],{},"error_redirect_url"," keep working\nas the single-profile degenerate case. Details: ",[1146,1147,1148],"a",{"href":68},"OAuth & Social\nLogin",".",[243,1151,1153,1154,1156],{"id":1152},"sessions-azp-provenance-and-sign-in-gating","Sessions: ",[204,1155,320],{}," provenance and sign-in gating",[195,1158,1159,1160,1165,1166,1169,1170,1149],{},"Every minted session records the resolved profile key as an ",[199,1161,1162,1164],{},[204,1163,320],{},"-style\nclaim"," (authorized party) on the tokens and the refresh row, preserved and\nre-validated at rotation. ",[204,1167,1168],{},"aud"," stays the platform\u002Fresource audience — there\nis deliberately no per-profile ",[204,1171,1168],{},[195,1173,1174,1175,1177,1178,1180,1181,1184,1185,1188,1189,1192,1193,1195],{},"Sign-in requests name their frontend (",[204,1176,240],{}," on the login request), and each\nprofile's ",[204,1179,279],{}," is enforced at ",[199,1182,1183],{},"every"," minting path —\npassword login, magic-link verify, access-code verify, OAuth callback,\ninvite-accept auto-login, and refresh. Off-audience sign-ins get a stable\n",[204,1186,1187],{},"403 wrong_application",". A profile with ",[199,1190,1191],{},"no"," ",[204,1194,279],{}," accepts\neveryone — the shared\u002FSSO mode — so partitioned and shared frontends are both\nplain configuration.",[195,1197,1198],{},"For endpoint families that must never serve another app's sessions, declare\nthem app-scoped — this check is enforced, not advisory:",[337,1200,1202],{"className":339,"code":1201,"language":341,"meta":342,"style":342},"from outlabs_auth.frontend import require_app\n\n@app.get(\"\u002Fconsole\u002Freports\", dependencies=[Depends(require_app(auth, \"console\"))])\nasync def console_reports(): ...\n",[204,1203,1204,1209,1213,1218],{"__ignoreMap":342},[346,1205,1206],{"class":348,"line":349},[346,1207,1208],{},"from outlabs_auth.frontend import require_app\n",[346,1210,1211],{"class":348,"line":355},[346,1212,401],{"emptyLinePlaceholder":400},[346,1214,1215],{"class":348,"line":361},[346,1216,1217],{},"@app.get(\"\u002Fconsole\u002Freports\", dependencies=[Depends(require_app(auth, \"console\"))])\n",[346,1219,1220],{"class":348,"line":367},[346,1221,1222],{},"async def console_reports(): ...\n",[195,1224,1225,1228,1229,1231,1232,1234],{},[199,1226,1227],{},"Honest semantics:"," this is level-2 separation — defense in depth,\nconsistency, and audit signal on top of RBAC. A public SPA cannot\nauthenticate its ",[204,1230,240],{}," selector, so ",[204,1233,320],{}," is provenance plus server-side\ngating, not credential isolation. If one product's credentials must be inert\nin another product's trust domain, that's level 3: separate deployments.",[243,1236,1238],{"id":1237},"route-contract-tests","Route-contract tests",[195,1240,1241,1242,1245,1246,1249],{},"Declared route templates are promises your frontends must keep.\n",[204,1243,1244],{},"outlabs_auth.frontend.contract"," asserts them against the real route trees —\nadapters for Nuxt ",[204,1247,1248],{},"pages\u002F",", TanStack flat-file routes, and route-constants\nfiles:",[337,1251,1253],{"className":339,"code":1252,"language":341,"meta":342,"style":342},"from outlabs_auth.frontend.contract import assert_profile_routes, routes_from_nuxt_pages\n\ndef test_portal_routes_exist():\n    available = routes_from_nuxt_pages(Path(\"..\u002Fcustomer-portal\u002Fapp\u002Fpages\"))\n    assert_profile_routes(registry.get(\"portal\"), available)\n",[204,1254,1255,1260,1264,1269,1274],{"__ignoreMap":342},[346,1256,1257],{"class":348,"line":349},[346,1258,1259],{},"from outlabs_auth.frontend.contract import assert_profile_routes, routes_from_nuxt_pages\n",[346,1261,1262],{"class":348,"line":355},[346,1263,401],{"emptyLinePlaceholder":400},[346,1265,1266],{"class":348,"line":361},[346,1267,1268],{},"def test_portal_routes_exist():\n",[346,1270,1271],{"class":348,"line":367},[346,1272,1273],{},"    available = routes_from_nuxt_pages(Path(\"..\u002Fcustomer-portal\u002Fapp\u002Fpages\"))\n",[346,1275,1276],{"class":348,"line":373},[346,1277,1278],{},"    assert_profile_routes(registry.get(\"portal\"), available)\n",[195,1280,1281],{},"Keep one per frontend per profile so a renamed frontend route fails your\nbackend suite before it ships a dead link.",[243,1283,1285],{"id":1284},"related-pages","Related pages",[604,1287,1288,1293,1298,1303],{},[607,1289,1290,1292],{},[1146,1291,82],{"href":83}," — challenge flows, delivery ownership",[607,1294,1295,1297],{},[1146,1296,67],{"href":68}," — provider routers, association",[607,1299,1300,1302],{},[1146,1301,77],{"href":78}," — token lifecycle and audit events",[607,1304,1305,1306,1309],{},"Maintainer design record: ",[204,1307,1308],{},"docs\u002FMULTI_FRONTEND_SUPPORT.md"," + DD-059 in the repo",[1311,1312,1313],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":342,"searchDepth":349,"depth":355,"links":1315},[1316,1317,1318,1319,1320,1322,1323,1325,1326],{"id":245,"depth":355,"text":246},{"id":334,"depth":355,"text":335},{"id":648,"depth":355,"text":649},{"id":740,"depth":355,"text":741},{"id":862,"depth":355,"text":1321},"Challenges: registered destinations, canonical next_url",{"id":1094,"depth":355,"text":1095},{"id":1152,"depth":355,"text":1324},"Sessions: azp provenance and sign-in gating",{"id":1237,"depth":355,"text":1238},{"id":1284,"depth":355,"text":1285},"One mount, several first-party frontends — profiles, resolution, and sign-in gating.","md",null,{},{"icon":75},{"title":72,"description":1327},"mfOj2D8xQKYutV1ojtCHVYuxBzIqQ0AQ8EDiZTYHGqA",[1335,1337],{"title":67,"path":68,"stem":69,"description":1336,"icon":70,"children":-1},"Provider routers, invite-only login, link and unlink.",{"title":77,"path":78,"stem":79,"description":1338,"icon":80,"children":-1},"Active sessions and user audit search.",1787472745731]