[{"data":1,"prerenderedAt":807},["ShallowReactive",2],{"docs:doc:\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fbrowser":3,"docs:pages:docs_en":737},{"id":4,"title":5,"alt":6,"body":7,"description":727,"extension":728,"key":729,"meta":730,"navigation":90,"order":731,"path":732,"placeholder":733,"seo":734,"stem":735,"__hash__":736},"docs_en\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fbrowser.md","Plain browser","\u002Fpl\u002Fdocs\u002Fprzewodniki\u002Fframeworki\u002Fprzegladarka",{"type":8,"value":9,"toc":720},"minimark",[10,19,44,47,52,55,187,213,251,255,262,391,404,414,418,488,504,507,573,588,592,655,669,682,686,711,716],[11,12,13,14,18],"p",{},"For a single-page application with no module of ours: Vite and vanilla\nTypeScript, React, Svelte, Solid, or a framework we have never heard of.\n",[15,16,17],"code",{},"@kleora-io\u002Fbrowser"," is the authorization-code-with-PKCE client and knows\nnothing about your framework.",[20,21,26],"pre",{"className":22,"code":23,"language":24,"meta":25,"style":25},"language-bash shiki shiki-themes github-light","npm install @kleora-io\u002Fbrowser\n","bash","",[15,27,28],{"__ignoreMap":25},[29,30,33,37,41],"span",{"class":31,"line":32},"line",1,[29,34,36],{"class":35},"s7eDp","npm",[29,38,40],{"class":39},"sYBdl"," install",[29,42,43],{"class":39}," @kleora-io\u002Fbrowser\n",[11,45,46],{},"No runtime dependencies, ES modules only.",[48,49,51],"h2",{"id":50},"_1-create-the-client-once","1. Create the client once",[11,53,54],{},"One instance for the whole application, in its own module. Creating a second one\nis how two halves of a page end up with two different ideas of who is signed in.",[20,56,60],{"className":57,"code":58,"language":59,"meta":25,"style":25},"language-ts shiki shiki-themes github-light","\u002F\u002F src\u002Fkleora.ts\nimport { createKleora } from '@kleora-io\u002Fbrowser'\n\nexport const kleora = createKleora({\n  issuer: import.meta.env.VITE_KLEORA_ISSUER,\n  clientId: import.meta.env.VITE_KLEORA_CLIENT_ID,\n  redirectUri: location.origin + '\u002Fcallback',\n  storage: 'session',\n})\n","ts",[15,61,62,68,85,92,114,137,156,170,181],{"__ignoreMap":25},[29,63,64],{"class":31,"line":32},[29,65,67],{"class":66},"sAwPA","\u002F\u002F src\u002Fkleora.ts\n",[29,69,71,75,79,82],{"class":31,"line":70},2,[29,72,74],{"class":73},"sD7c4","import",[29,76,78],{"class":77},"sgsFI"," { createKleora } ",[29,80,81],{"class":73},"from",[29,83,84],{"class":39}," '@kleora-io\u002Fbrowser'\n",[29,86,88],{"class":31,"line":87},3,[29,89,91],{"emptyLinePlaceholder":90},true,"\n",[29,93,95,98,101,105,108,111],{"class":31,"line":94},4,[29,96,97],{"class":73},"export",[29,99,100],{"class":73}," const",[29,102,104],{"class":103},"sYu0t"," kleora",[29,106,107],{"class":73}," =",[29,109,110],{"class":35}," createKleora",[29,112,113],{"class":77},"({\n",[29,115,117,120,122,125,128,131,134],{"class":31,"line":116},5,[29,118,119],{"class":77},"  issuer: ",[29,121,74],{"class":73},[29,123,124],{"class":77},".",[29,126,127],{"class":103},"meta",[29,129,130],{"class":77},".env.",[29,132,133],{"class":103},"VITE_KLEORA_ISSUER",[29,135,136],{"class":77},",\n",[29,138,140,143,145,147,149,151,154],{"class":31,"line":139},6,[29,141,142],{"class":77},"  clientId: ",[29,144,74],{"class":73},[29,146,124],{"class":77},[29,148,127],{"class":103},[29,150,130],{"class":77},[29,152,153],{"class":103},"VITE_KLEORA_CLIENT_ID",[29,155,136],{"class":77},[29,157,159,162,165,168],{"class":31,"line":158},7,[29,160,161],{"class":77},"  redirectUri: location.origin ",[29,163,164],{"class":73},"+",[29,166,167],{"class":39}," '\u002Fcallback'",[29,169,136],{"class":77},[29,171,173,176,179],{"class":31,"line":172},8,[29,174,175],{"class":77},"  storage: ",[29,177,178],{"class":39},"'session'",[29,180,136],{"class":77},[29,182,184],{"class":31,"line":183},9,[29,185,186],{"class":77},"})\n",[11,188,189,192,193,196,197,200,201,204,205,208,209,212],{},[15,190,191],{},"issuer",", ",[15,194,195],{},"clientId"," and ",[15,198,199],{},"redirectUri"," are the three required options; the rest\nhave defaults. ",[15,202,203],{},"scope"," defaults to ",[15,206,207],{},"openid profile email offline_access"," — keep\n",[15,210,211],{},"offline_access"," or no refresh token is issued and nothing can be renewed\nsilently.",[214,215,218],"callout",{"title":216,"type":217},"Why storage: 'session' here","note",[11,219,220,221,224,225,228,229,196,232,235,236,238,239,242,243,246,247,250],{},"The default is ",[15,222,223],{},"memory",", which holds both tokens in a closure and survives\nnothing — a reload then depends on ",[15,226,227],{},"checkSession()",", and its silent path only\nworks where your application and its issuer are the same site.\n",[15,230,231],{},"http:\u002F\u002Flocalhost:5173",[15,233,234],{},"acme.sandbox.kleora.eu"," are not, so in\ndevelopment ",[15,237,223],{}," means a visible round trip on every reload.\n",[15,240,241],{},"session"," keeps the refresh token in ",[15,244,245],{},"sessionStorage",": tab-scoped, cleared when\nthe tab closes, and enough to survive a reload without the round trip.\n",[15,248,249],{},"localStorage"," is not an option and will not become one.",[48,252,254],{"id":253},"_2-handle-the-callback","2. Handle the callback",[11,256,257,258,261],{},"The issuer sends the browser back to ",[15,259,260],{},"\u002Fcallback"," with an authorization code.\nRedeem it there, then put the person back where they started:",[20,263,265],{"className":57,"code":264,"language":59,"meta":25,"style":25},"import { kleora } from '.\u002Fkleora'\n\nif (location.pathname === '\u002Fcallback') {\n  const { appState } = await kleora.handleRedirectCallback()\n  const returnTo = (appState as { returnTo?: string })?.returnTo ?? '\u002F'\n  history.replaceState(null, '', returnTo)\n}\n",[15,266,267,279,283,299,328,364,386],{"__ignoreMap":25},[29,268,269,271,274,276],{"class":31,"line":32},[29,270,74],{"class":73},[29,272,273],{"class":77}," { kleora } ",[29,275,81],{"class":73},[29,277,278],{"class":39}," '.\u002Fkleora'\n",[29,280,281],{"class":31,"line":70},[29,282,91],{"emptyLinePlaceholder":90},[29,284,285,288,291,294,296],{"class":31,"line":87},[29,286,287],{"class":73},"if",[29,289,290],{"class":77}," (location.pathname ",[29,292,293],{"class":73},"===",[29,295,167],{"class":39},[29,297,298],{"class":77},") {\n",[29,300,301,304,307,310,313,316,319,322,325],{"class":31,"line":94},[29,302,303],{"class":73},"  const",[29,305,306],{"class":77}," { ",[29,308,309],{"class":103},"appState",[29,311,312],{"class":77}," } ",[29,314,315],{"class":73},"=",[29,317,318],{"class":73}," await",[29,320,321],{"class":77}," kleora.",[29,323,324],{"class":35},"handleRedirectCallback",[29,326,327],{"class":77},"()\n",[29,329,330,332,335,337,340,343,345,349,352,355,358,361],{"class":31,"line":116},[29,331,303],{"class":73},[29,333,334],{"class":103}," returnTo",[29,336,107],{"class":73},[29,338,339],{"class":77}," (appState ",[29,341,342],{"class":73},"as",[29,344,306],{"class":77},[29,346,348],{"class":347},"sqxcx","returnTo",[29,350,351],{"class":73},"?:",[29,353,354],{"class":103}," string",[29,356,357],{"class":77}," })?.returnTo ",[29,359,360],{"class":73},"??",[29,362,363],{"class":39}," '\u002F'\n",[29,365,366,369,372,375,378,380,383],{"class":31,"line":139},[29,367,368],{"class":77},"  history.",[29,370,371],{"class":35},"replaceState",[29,373,374],{"class":77},"(",[29,376,377],{"class":103},"null",[29,379,192],{"class":77},[29,381,382],{"class":39},"''",[29,384,385],{"class":77},", returnTo)\n",[29,387,388],{"class":31,"line":158},[29,389,390],{"class":77},"}\n",[11,392,393,396,397,196,400,403],{},[15,394,395],{},"handleRedirectCallback()"," validates ",[15,398,399],{},"state",[15,401,402],{},"iss",", redeems the code with\nthe PKCE verifier, validates the ID token against the issuer's key set, stores\nthe tokens and strips the credentials out of the address bar.",[11,405,406,407,409,410,413],{},"Your dev server has to serve the application's HTML for ",[15,408,260],{}," as well as\nfor ",[15,411,412],{},"\u002F",". Vite's dev server does that for unknown paths already; a plain static\nfile server does not, and answers 404 to the one navigation that matters.",[48,415,417],{"id":416},"_3-sign-in-and-know-when-you-are-signed-in","3. Sign in, and know when you are signed in",[20,419,421],{"className":57,"code":420,"language":59,"meta":25,"style":25},"if (!(await kleora.checkSession())) {\n  await kleora.loginWithRedirect({ returnTo: location.pathname })\n}\n\nconst user = kleora.getUser() \u002F\u002F sub, email, name, tenant, tenant_slug…\n",[15,422,423,446,459,463,467],{"__ignoreMap":25},[29,424,425,427,430,433,435,438,440,443],{"class":31,"line":32},[29,426,287],{"class":73},[29,428,429],{"class":77}," (",[29,431,432],{"class":73},"!",[29,434,374],{"class":77},[29,436,437],{"class":73},"await",[29,439,321],{"class":77},[29,441,442],{"class":35},"checkSession",[29,444,445],{"class":77},"())) {\n",[29,447,448,451,453,456],{"class":31,"line":70},[29,449,450],{"class":73},"  await",[29,452,321],{"class":77},[29,454,455],{"class":35},"loginWithRedirect",[29,457,458],{"class":77},"({ returnTo: location.pathname })\n",[29,460,461],{"class":31,"line":87},[29,462,390],{"class":77},[29,464,465],{"class":31,"line":94},[29,466,91],{"emptyLinePlaceholder":90},[29,468,469,472,475,477,479,482,485],{"class":31,"line":116},[29,470,471],{"class":73},"const",[29,473,474],{"class":103}," user",[29,476,107],{"class":73},[29,478,321],{"class":77},[29,480,481],{"class":35},"getUser",[29,483,484],{"class":77},"() ",[29,486,487],{"class":66},"\u002F\u002F sub, email, name, tenant, tenant_slug…\n",[11,489,490,492,493,496,497,499,500,503],{},[15,491,227],{}," tries a fresh cached token first, then a stored refresh token,\nthen a silent ",[15,494,495],{},"prompt=none"," round trip in a hidden frame. ",[15,498,348],{}," is carried\nthrough the redirect and handed back as ",[15,501,502],{},"appState.returnTo"," — which is what step\n2 reads.",[11,505,506],{},"To react to sign-in, sign-out and refresh rather than polling:",[20,508,510],{"className":57,"code":509,"language":59,"meta":25,"style":25},"const unsubscribe = kleora.onAuthChange(({ user, isAuthenticated }) => {\n  render(isAuthenticated ? user : null)\n})\n",[15,511,512,546,569],{"__ignoreMap":25},[29,513,514,516,519,521,523,526,529,532,534,537,540,543],{"class":31,"line":32},[29,515,471],{"class":73},[29,517,518],{"class":103}," unsubscribe",[29,520,107],{"class":73},[29,522,321],{"class":77},[29,524,525],{"class":35},"onAuthChange",[29,527,528],{"class":77},"(({ ",[29,530,531],{"class":347},"user",[29,533,192],{"class":77},[29,535,536],{"class":347},"isAuthenticated",[29,538,539],{"class":77}," }) ",[29,541,542],{"class":73},"=>",[29,544,545],{"class":77}," {\n",[29,547,548,551,554,557,560,563,566],{"class":31,"line":70},[29,549,550],{"class":35},"  render",[29,552,553],{"class":77},"(isAuthenticated ",[29,555,556],{"class":73},"?",[29,558,559],{"class":77}," user ",[29,561,562],{"class":73},":",[29,564,565],{"class":103}," null",[29,567,568],{"class":77},")\n",[29,570,571],{"class":31,"line":87},[29,572,186],{"class":77},[11,574,575,576,579,580,584,585,587],{},"There is also ",[15,577,578],{},"kleora.hasLikelySession()",", which answers whether this browser\ncompleted a sign-in that was never followed by a sign-out. It is a ",[581,582,583],"strong",{},"render\nhint"," — it lets you show your shell instead of a sign-in flash while\nverification runs. It is never an authorisation decision, and never proof of a\nlive session; only ",[15,586,227],{}," or a successful API call is that.",[48,589,591],{"id":590},"_4-call-your-own-api","4. Call your own API",[20,593,595],{"className":57,"code":594,"language":59,"meta":25,"style":25},"const token = await kleora.getAccessToken()\n\nawait fetch('\u002Fapi\u002Finvoices', {\n  headers: { Authorization: `Bearer ${token}` },\n})\n",[15,596,597,615,619,634,651],{"__ignoreMap":25},[29,598,599,601,604,606,608,610,613],{"class":31,"line":32},[29,600,471],{"class":73},[29,602,603],{"class":103}," token",[29,605,107],{"class":73},[29,607,318],{"class":73},[29,609,321],{"class":77},[29,611,612],{"class":35},"getAccessToken",[29,614,327],{"class":77},[29,616,617],{"class":31,"line":70},[29,618,91],{"emptyLinePlaceholder":90},[29,620,621,623,626,628,631],{"class":31,"line":87},[29,622,437],{"class":73},[29,624,625],{"class":35}," fetch",[29,627,374],{"class":77},[29,629,630],{"class":39},"'\u002Fapi\u002Finvoices'",[29,632,633],{"class":77},", {\n",[29,635,636,639,642,645,648],{"class":31,"line":94},[29,637,638],{"class":77},"  headers: { Authorization: ",[29,640,641],{"class":39},"`Bearer ${",[29,643,644],{"class":77},"token",[29,646,647],{"class":39},"}`",[29,649,650],{"class":77}," },\n",[29,652,653],{"class":31,"line":116},[29,654,186],{"class":77},[11,656,657,660,661,664,665,668],{},[15,658,659],{},"getAccessToken()"," returns the cached token until it is within 30 seconds of\nexpiry, then refreshes; concurrent callers share one refresh. Refresh tokens\nrotate on every use. If your API answers ",[15,662,663],{},"401"," for a token this client still\nbelieves in, retry once with ",[15,666,667],{},"getAccessToken({ force: true })"," — that discards\nthe cached token instead of re-sending the rejected one.",[11,670,671,672,677,678,681],{},"Verifying that token is the ",[673,674,676],"a",{"href":675},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fexpress","Express page","\n— or ",[15,679,680],{},"verify_access_token"," if your API is Python.",[48,683,685],{"id":684},"_5-sign-out","5. Sign out",[20,687,689],{"className":57,"code":688,"language":59,"meta":25,"style":25},"await kleora.logout({ returnTo: location.origin + '\u002F' })\n",[15,690,691],{"__ignoreMap":25},[29,692,693,695,697,700,703,705,708],{"class":31,"line":32},[29,694,437],{"class":73},[29,696,321],{"class":77},[29,698,699],{"class":35},"logout",[29,701,702],{"class":77},"({ returnTo: location.origin ",[29,704,164],{"class":73},[29,706,707],{"class":39}," '\u002F'",[29,709,710],{"class":77}," })\n",[11,712,713,715],{},[15,714,348],{}," is required and must be one of the client's registered post-logout\nURIs, matched exactly. Local state is cleared before anything is fetched, so a\nsign-out completes locally even when the issuer cannot be reached.",[717,718,719],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}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 pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":25,"searchDepth":87,"depth":87,"links":721},[722,723,724,725,726],{"id":50,"depth":70,"text":51},{"id":253,"depth":70,"text":254},{"id":416,"depth":70,"text":417},{"id":590,"depth":70,"text":591},{"id":684,"depth":70,"text":685},"One client instance, one callback route, and an access token for every request your application makes.","md","guides\u002Fquickstarts\u002Fbrowser",{},10,"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fbrowser",false,{"title":5,"description":727},"en\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fbrowser","TWzWXHNTkOK7xAHOdEZRlUsAj-nNLDUGJPGoV3_Gypg",[738,743,749,754,760,765,770,771,776,781,786,791,797,802],{"path":739,"title":740,"description":741,"order":731,"key":742},"\u002Fen\u002Fdocs\u002Fgetting-started","Getting started","From an empty project to a working sign-in, and the packages that get you there.","getting-started\u002Findex",{"path":744,"title":745,"description":746,"order":747,"key":748},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Finstallation","Installing an SDK","The five packages we publish, what each one is for, and the configuration each one takes.",20,"getting-started\u002Finstallation",{"path":750,"title":751,"description":752,"order":731,"key":753},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Fquickstart","Quickstart","Create an App, wire up the browser SDK, and sign in for the first time — entirely in sandbox.","getting-started\u002Fquickstart",{"path":755,"title":756,"description":757,"order":758,"key":759},"\u002Fen\u002Fdocs\u002Fguides\u002Farchitecture","How Kleora is put together","The shape of the hosted service — one address per environment, two API surfaces, and where state lives. And why self-hosting is not something we support today.",30,"guides\u002Farchitecture",{"path":761,"title":762,"description":763,"order":747,"key":764},"\u002Fen\u002Fdocs\u002Fguides\u002Fconcepts","Workspaces, roles and environments","The five words the product is built from, which of them your tokens carry, and how a change to a role reaches a running application.","guides\u002Fconcepts",{"path":766,"title":767,"description":768,"order":747,"key":769},"\u002Fen\u002Fdocs\u002Fguides","Guides","Wiring Kleora into your stack, the words the product uses, and how the hosted service is put together.","guides\u002Findex",{"path":732,"title":5,"description":727,"order":731,"key":729},{"path":675,"title":772,"description":773,"order":774,"key":775},"Express","Verifying the access token on your own API — middleware, permissions, and the three answers a refusal can have.",40,"guides\u002Fquickstarts\u002Fexpress",{"path":777,"title":778,"description":779,"order":731,"key":780},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts","Framework quickstarts","One page per stack — Next.js, Nuxt, Express and the plain browser — and the two redirect URIs all of them share.","guides\u002Fquickstarts\u002Findex",{"path":782,"title":783,"description":784,"order":747,"key":785},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fnextjs","Next.js","The browser SDK in an App Router application — a lazy client, a callback route, and token verification in a route handler.","guides\u002Fquickstarts\u002Fnextjs",{"path":787,"title":788,"description":789,"order":758,"key":790},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fnuxt","Nuxt","The Nuxt module — two lines of config, a callback page you do not write, and a route middleware that protects a page from its own meta.","guides\u002Fquickstarts\u002Fnuxt",{"path":792,"title":793,"description":794,"order":795,"key":796},"\u002Fen\u002Fdocs","Kleora documentation","Add hosted sign-in to your application, verify the token on your API, and manage users, workspaces and roles from one console.",0,"index",{"path":798,"title":799,"description":800,"order":731,"key":801},"\u002Fen\u002Fdocs\u002Freference\u002Fcode-highlighting","Code highlighting","One fence per preloaded Shiki grammar — a fixture, not a reference page.","reference\u002Fcode-highlighting",{"path":803,"title":804,"description":805,"order":758,"key":806},"\u002Fen\u002Fdocs\u002Freference","Reference","Reference material for the HTTP APIs and the SDK packages.","reference\u002Findex",1790698197623]